Skip to content

Conversation

@rasulkireev
Copy link
Owner

@rasulkireev rasulkireev commented Nov 2, 2025

This pull request contains changes generated by a Cursor Cloud Agent

Open in Cursor Open in Web

Summary by CodeRabbit

  • New Features

    • Manage competitors per project, view analytics, add competitors, and generate SEO-friendly comparison titles and full markdown "VS" blog posts (2000+ words) with one-click generation and a post detail view.
  • UI

    • Added "Competitors" nav, project competitors page, modals, action buttons, status/limit indicators, and a post detail screen with copy-as-Markdown/HTML actions.
  • Chores

    • Updated AI integration and dependency upgrades.

Co-authored-by: me <me@rasulkireev.com>
@cursor
Copy link

cursor bot commented Nov 2, 2025

Cursor Agent can help with this pull request. Just @cursor in comments and I'll start working on changes in this branch.
Learn more about Cursor Agents

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 2, 2025

Caution

Review failed

The pull request is closed.

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Adds competitor-focused content generation: new Perplexity chat agents and prompts, Competitor.blog_post storage and generation, profile quota fields and checks, API endpoint to generate VS titles/posts, frontend UI/controllers and templates for competitor management, a DB migration, and dependency upgrades for pydantic-ai.

Changes

Cohort / File(s) Change Summary
Agents & Schemas
core/agents.py, core/schemas.py
Added competitor_vs_title_agent, competitor_vs_blog_post_agent, add_competitor_vs_context() and new pydantic contexts CompetitorVsTitleContext, CompetitorVsPostContext, plus exposed TitleSuggestion. Uses chat model (Perplexity Sonar) and markdown formatting helpers.
Agent Prompts
core/agent_system_prompts.py, core/prompts.py
Added markdown_lists() prompt, made add_project_pages() generic, applied markdown lists to new agents; minor lint markers added in prompts.
Models & Migrations
core/models.py, core/migrations/0038_competitor_blog_post.py
Switched to OpenAIChatModel usage and result.output shape; added Competitor.blog_post TextField and Competitor.generate_vs_blog_post(); extended Profile with competitor quotas/flags; enriched Keyword fields; added migration to add blog_post.
API Schemas & Views
core/api/schemas.py, core/api/views.py
Added GenerateCompetitorVsTitleIn/Out schemas and POST /generate-competitor-vs-title handler with permission/plan checks, logging, and structured error handling; adjusted add-competitor flow to require/fetch homepage content synchronously.
Tasks & Utilities
core/tasks.py, core/utils.py
Updated competitor analysis tasks to fetch homepage content and call populate_name_description (skip expensive analysis); switched AI-result reads from result.dataresult.output; updated docstrings/logging and return messages.
Views, URLs & Templates
core/views.py, core/urls.py, frontend/templates/*
Added ProjectCompetitorsView and CompetitorBlogPostDetailView; tightened GeneratedBlogPostDetailView query scope; new competitor routes; added sidebar link and templates project_competitors.html, competitor_blog_post_detail.html.
Frontend Controllers
frontend/src/controllers/*
Added add_competitor_controller.js, competitor_vs_post_controller.js, copy_markdown_controller.js; removed competitor_analysis_controller.js. Controllers handle modal add, trigger VS generation, and copy-markdown UX.
Config & Dependencies
pyproject.toml, requirements.txt, CHANGELOG.md
Upgraded pydantic-ai to ^1.9.1 and related pydantic-ai packages; expanded requirements with many new runtime/instrumentation packages; updated CHANGELOG (0.0.7).

Sequence Diagram(s)

sequenceDiagram
    participant User as Frontend (User)
    participant API as API Server
    participant DB as Database
    participant Agent as Agent Layer
    participant Perplexity as Perplexity Sonar

    User->>API: POST /generate-competitor-vs-title { competitor_id }
    API->>DB: Validate competitor & project ownership
    API->>API: Check profile limits (competitor_posts_limit, blog_posts_limit)
    API->>Agent: generate_vs_blog_post(context via add_competitor_vs_context)
    Agent->>Perplexity: Invoke chat-model (markdown_lists + project pages)
    Perplexity-->>Agent: Returns title and markdown blog post
    Agent->>DB: Persist Competitor.blog_post (and any title suggestion)
    API-->>User: 200 OK { status: "success", competitor_id }
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Focus review on:
    • core/models.py: pervasive result.dataresult.output changes and chat-model integration.
    • core/api/views.py: permission/plan-limit enforcement and add-competitor synchronous fetch/error paths.
    • Competitor.generate_vs_blog_post(): prompt composition, formatting helpers, and persistence.
    • Frontend controllers ↔ API wiring, CSRF handling, and template/controller consistency.
    • Migration 0038_competitor_blog_post.py and DB compatibility.

Possibly related PRs

Poem

🐇 I nibble prompts by lantern light,
I weave a VS post through code and night.
Titles sprout, markdown sings,
Competitors bowed to my tiny paws and springs —
Copy, paste, and hop to flight! ✨

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Title Check ⚠️ Warning The pull request title "Add competitor vs. post title generation" is partially related to the changeset but does not fully capture the main point of the changes. While the PR does add title generation capabilities (through competitor_vs_title_agent), the more substantial deliverable is the ability to generate full, 2000+ word SEO-optimized blog posts comparing competitors (through competitor_vs_blog_post_agent). The raw summary emphasizes that the blog post generation is the primary feature with extensive backend support including a new blog_post field on the Competitor model, a generate_vs_blog_post method, and comprehensive frontend UI components. The title's focus on "title generation" significantly understates the scope of this feature, as a reviewer scanning the PR history would not immediately understand that this PR enables generating complete comparison blog posts, not just titles. Consider revising the title to better reflect the main deliverable, such as "Add competitor vs. blog post generation" or "Add competitor comparison blog post generation," which would more accurately convey that this PR enables generating full blog posts comparing the user's product with competitors, not merely generating titles. The current title is misleading and could cause reviewers to underestimate the scope and significance of the changes.
Docstring Coverage ⚠️ Warning Docstring coverage is 23.08% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3b23579 and 68a7657.

📒 Files selected for processing (2)
  • CHANGELOG.md (1 hunks)
  • frontend/templates/project/competitor_blog_post_detail.html (1 hunks)

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@rasulkireev rasulkireev marked this pull request as ready for review November 2, 2025 18:34
…r/add-competitor-vs-post-title-generation-63a8
Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Greptile Overview

Greptile Summary

Adds competitor comparison blog post title generation feature using Perplexity Sonar AI model.

Key Changes:

  • Created new competitor_vs_title_agent in core/agents.py that uses Perplexity Sonar API for web-search enabled title generation
  • Added generate_vs_title() method to Competitor model for generating comparison titles
  • Implemented /generate-competitor-vs-title API endpoint with authentication, permission checks, and quota limits
  • Added VS_COMPETITOR content type to categorize comparison blog posts
  • Created comprehensive system prompts for competitive comparison content generation and writing
  • Added CompetitorVsTitleContext schema to structure agent input data

Implementation Quality:

  • Follows existing patterns in the codebase (agent structure, API endpoint design, error handling)
  • Proper authentication and authorization checks in place
  • Uses async task queue for keyword processing
  • Comprehensive error logging with structured logging
  • Clean separation of concerns across layers (agents, models, views, schemas)

Confidence Score: 5/5

  • This PR is safe to merge with minimal risk
  • The implementation follows established patterns in the codebase, includes proper authentication/authorization, error handling, and logging. Only minor style improvement suggested (unused import). The changes are well-structured and consistent with the existing architecture.
  • No files require special attention

Important Files Changed

File Analysis

Filename Score Overview
core/agents.py 4/5 Added new competitor_vs_title_agent using Perplexity Sonar model for generating competitor comparison titles with custom system prompts
core/models.py 5/5 Added generate_vs_title() method to Competitor model that generates comparison blog post titles using the new agent
core/api/views.py 5/5 Added /generate-competitor-vs-title API endpoint with proper authentication, permissions, and error handling

Sequence Diagram

sequenceDiagram
    participant User
    participant API as API View<br/>(generate_competitor_vs_title)
    participant Competitor as Competitor Model
    participant Agent as competitor_vs_title_agent
    participant Perplexity as Perplexity Sonar API
    participant DB as Database

    User->>API: POST /generate-competitor-vs-title<br/>{competitor_id}
    API->>API: Authenticate user<br/>(session_auth)
    API->>DB: Get Competitor by ID
    DB-->>API: Return Competitor
    API->>API: Check permissions<br/>(project.profile == profile)
    API->>API: Check generation limits<br/>(can_generate_title_suggestions)
    
    API->>Competitor: generate_vs_title()
    Competitor->>Competitor: Create CompetitorVsTitleContext<br/>(project_details, competitor_details)
    Competitor->>Agent: run_agent_synchronously()<br/>with context
    Agent->>Perplexity: Request title generation<br/>with web search capability
    Perplexity-->>Agent: Return TitleSuggestion
    Agent-->>Competitor: Return result
    
    Competitor->>DB: Create BlogPostTitleSuggestion<br/>(VS_COMPETITOR type)
    DB-->>Competitor: Return title_suggestion
    Competitor->>DB: async_task: save_title_suggestion_keywords
    Competitor-->>API: Return title_suggestion
    
    API->>API: render_to_string()<br/>(title_suggestion_card.html)
    API-->>User: Return success response<br/>with suggestion + HTML
Loading

7 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

core/agents.py Outdated

@competitor_vs_title_agent.system_prompt
def add_competitor_vs_context(ctx):
from pydantic_ai import RunContext
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: RunContext is imported but never used in the function

Suggested change
from pydantic_ai import RunContext
def add_competitor_vs_context(ctx):
Prompt To Fix With AI
This is a comment left during a code review.
Path: core/agents.py
Line: 121:121

Comment:
**style:** `RunContext` is imported but never used in the function

```suggestion
def add_competitor_vs_context(ctx):
```

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
core/agents.py (1)

1-115: Prevent import-time crash when Perplexity API key is absent

Accessing settings.PERPLEXITY_API_KEY at import time will raise AttributeError (and break the module) whenever the key isn’t configured. Add a strict check and fail fast with a clear error before constructing the provider.

+from django.core.exceptions import ImproperlyConfigured
@@
-competitor_vs_title_agent = Agent(
-    OpenAIModel(
-        AIModel.PERPLEXITY_SONAR,
-        provider=OpenAIProvider(
-            base_url="https://api.perplexity.ai",
-            api_key=settings.PERPLEXITY_API_KEY,
-        ),
-    ),
+perplexity_api_key = getattr(settings, "PERPLEXITY_API_KEY", "")
+if not perplexity_api_key:
+    raise ImproperlyConfigured(
+        "PERPLEXITY_API_KEY must be configured to use competitor vs. title generation."
+    )
+
+competitor_vs_title_agent = Agent(
+    OpenAIModel(
+        AIModel.PERPLEXITY_SONAR,
+        provider=OpenAIProvider(
+            base_url="https://api.perplexity.ai",
+            api_key=perplexity_api_key,
+        ),
+    ),
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4ebe04a and e0ef80a.

📒 Files selected for processing (7)
  • core/agents.py (3 hunks)
  • core/api/schemas.py (1 hunks)
  • core/api/views.py (2 hunks)
  • core/choices.py (1 hunks)
  • core/models.py (1 hunks)
  • core/prompts.py (2 hunks)
  • core/schemas.py (1 hunks)
🧰 Additional context used
📓 Path-based instructions (4)
**/*.py

📄 CodeRabbit inference engine (.cursor/rules/backend.mdc)

**/*.py: Prioritize readability and maintainability; follow PEP 8
Use descriptive variable/function names with underscores

In Python, use try-except blocks that catch specific exception types; do not catch broad Exception.

**/*.py: Follow PEP 8 for Python code style
Use descriptive variable names with underscores (snake_case) in Python
Prefer Django's built-in features over external libraries

**/*.py: Use structlog for logging in Python code (avoid print and the standard logging module)
Include contextual key-value fields in log messages (e.g., error=str(e), exc_info=True, profile_id=profile.id)

**/*.py: Use descriptive, full-word variable names; avoid abbreviations and single-letter variables in Python
Provide context in variable names when format or type matters (e.g., include _iso_format, date)
Extract unchanging values into UPPER_CASE constants
Use intermediary variables to name parsed groups instead of using index access directly
Naming conventions: use is_/has_/can_ for booleans; include 'date' for dates; snake_case for variables/functions; PascalCase for classes
Define variables close to where they are used to keep lifespan short
Name things after what they do, not how they're used; ensure names make sense without extra context
Avoid generic names like data, info, manager; use specific, intention-revealing names
Function names should include necessary context without being verbose
If naming is hard, split functions into smaller focused parts
Maintain consistency: reuse the same verbs and nouns for the same concepts; name variables after the functions that create them
Use more descriptive names for longer-lived variables
Avoid else statements by using guard clauses for early returns
Replace simple conditionals with direct assignment when both branches call the same function with different values
Use dictionaries as dispatch tables instead of multiple equal-probability elif chains
Validate input before processing to prevent errors propagating in...

Files:

  • core/schemas.py
  • core/choices.py
  • core/api/schemas.py
  • core/agents.py
  • core/prompts.py
  • core/api/views.py
  • core/models.py
core/**/*.py

📄 CodeRabbit inference engine (.cursor/rules/backend.mdc)

Leverage Django's ORM; avoid raw SQL when possible

Files:

  • core/schemas.py
  • core/choices.py
  • core/api/schemas.py
  • core/agents.py
  • core/prompts.py
  • core/api/views.py
  • core/models.py
core/api/**/*.py

📄 CodeRabbit inference engine (.cursor/rules/backend.mdc)

core/api/**/*.py: django-ninja generate openapi documentation, so make sure to populate views with relevant data in order for it to show up in the OpenAPI docs.
Use Pydantic models for schema validation
Use django-ninja's authentication classes

Implement REST APIs with django-ninja using Pydantic schemas under core/api/

Files:

  • core/api/schemas.py
  • core/api/views.py
core/models.py

📄 CodeRabbit inference engine (CLAUDE.md)

core/models.py: Place business logic in Django models (fat models pattern)
Validate simple constraints in the database and place complex domain logic in Django models

Files:

  • core/models.py
🧠 Learnings (2)
📚 Learning: 2025-10-04T08:52:37.437Z
Learnt from: CR
Repo: rasulkireev/TuxSEO PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-10-04T08:52:37.437Z
Learning: Applies to core/agents/**/*.py : Implement AI agents using pydantic-ai under core/agents/

Applied to files:

  • core/agents.py
📚 Learning: 2025-10-04T08:52:37.437Z
Learnt from: CR
Repo: rasulkireev/TuxSEO PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-10-04T08:52:37.437Z
Learning: Always add AGENTS.md into AI context

Applied to files:

  • core/agents.py
🧬 Code graph analysis (4)
core/schemas.py (1)
core/models.py (3)
  • project_details (349-364)
  • competitor_details (1376-1381)
  • competitor_details (1486-1494)
core/agents.py (4)
core/choices.py (2)
  • AIModel (112-114)
  • get_default_ai_model (120-122)
core/schemas.py (4)
  • BlogPostGenerationContext (180-187)
  • CompetitorVsTitleContext (255-259)
  • ProjectDetails (15-97)
  • TitleSuggestion (149-160)
core/models.py (4)
  • project_details (349-364)
  • competitor_details (1376-1381)
  • competitor_details (1486-1494)
  • add_language_specification (508-514)
core/agent_system_prompts.py (1)
  • add_language_specification (118-123)
core/api/views.py (2)
core/api/schemas.py (2)
  • GenerateCompetitorVsTitleIn (134-135)
  • GenerateCompetitorVsTitleOut (138-142)
core/models.py (4)
  • Competitor (1345-1559)
  • can_generate_title_suggestions (266-267)
  • product_name (171-176)
  • generate_vs_title (1525-1559)
core/models.py (3)
core/schemas.py (1)
  • CompetitorVsTitleContext (255-259)
core/model_utils.py (1)
  • run_agent_synchronously (18-75)
core/choices.py (1)
  • ContentType (4-7)
🪛 Flake8 (7.3.0)
core/agents.py

[error] 121-121: 'pydantic_ai.RunContext' imported but unused

(F401)

🪛 Ruff (0.14.2)
core/api/views.py

660-672: Consider moving this statement to an else block

(TRY300)


685-685: Use explicit conversion flag

Replace with conversion flag

(RUF010)

@rasulkireev
Copy link
Owner Author

@coderabbitai full review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 2, 2025

✅ Actions performed

Full review triggered.

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Greptile Overview

Greptile Summary

Adds AI-powered competitor comparison blog post generation using Perplexity Sonar with web search capabilities.

Key Changes:

  • Added Competitor.blog_post field to store generated comparison content
  • Created two new AI agents using Perplexity Sonar for title suggestions and content generation
  • Added /api/generate-competitor-vs-title endpoint with proper authorization and limit checks
  • Implemented profile limits for competitor features (competitor_limit, competitor_posts_limit)
  • Optimized competitor creation flow by removing expensive analysis step (cost savings)
  • Added new Stimulus controller and template for displaying comparison blog posts
  • Included proper Pydantic schemas for context management

Architecture:
The feature uses Perplexity's Sonar model which can search the web in real-time to gather current information about both products before generating the comparison content. This ensures up-to-date and factually accurate comparisons.

Cost Optimization:
The PR includes smart cost optimization by making the initial competitor analysis lightweight (only fetches homepage content) while deferring the expensive VS blog post generation to a manual user action.

Confidence Score: 4/5

  • This PR is safe to merge with minor considerations
  • The implementation follows Django and project conventions well, includes proper error handling and authorization checks, and demonstrates good architectural decisions (cost optimization, proper separation of concerns). One minor issue was already flagged (unused import in core/agents.py:121). The code quality is high with proper logging, limit checks, and user-friendly error messages.
  • No files require special attention - the implementation is solid across all changed files

Important Files Changed

File Analysis

Filename Score Overview
core/models.py 4/5 Added competitor VS blog post generation capability with new blog_post field and generate_vs_blog_post() method. Also added profile limit properties for competitor features.
core/agents.py 4/5 Added two new AI agents (competitor_vs_title_agent and competitor_vs_blog_post_agent) using Perplexity Sonar for generating comparison content. Clean implementation with proper system prompts.
core/schemas.py 5/5 Added Pydantic schemas for competitor VS post context (CompetitorVsTitleContext, CompetitorVsPostContext). Schemas are well-structured with proper field descriptions.
core/api/views.py 4/5 Modified add_competitor endpoint to be more lightweight (removed expensive analysis call), added new /generate-competitor-vs-title endpoint with proper limit checks and error handling.
core/tasks.py 5/5 Updated analyze_project_competitor to skip expensive analysis step (cost optimization). Task now only fetches homepage content and populates basic details.
frontend/src/controllers/competitor_vs_post_controller.js 5/5 New Stimulus controller for generating competitor VS blog posts. Clean async implementation with proper loading states and error handling.

Sequence Diagram

sequenceDiagram
    participant User
    participant Frontend
    participant API
    participant Competitor
    participant Agent
    participant Perplexity
    
    User->>Frontend: Click "Generate Post"
    Frontend->>API: POST /api/generate-competitor-vs-title
    API->>API: Check profile limits
    alt Limit reached
        API-->>Frontend: Error: Limit reached
        Frontend-->>User: Show error message
    else Can generate
        API->>Competitor: generate_vs_blog_post()
        Competitor->>Agent: Call competitor_vs_blog_post_agent
        Agent->>Perplexity: Research both products
        Perplexity-->>Agent: Web search results
        Agent->>Agent: Generate comparison content
        Agent-->>Competitor: Return markdown blog post
        Competitor->>Competitor: Save to blog_post field
        Competitor-->>API: Success
        API-->>Frontend: Success response
        Frontend->>Frontend: Replace button with "View Post"
        Frontend-->>User: Show success message
        User->>Frontend: Click "View Post"
        Frontend->>Frontend: Navigate to detail page
    end
Loading

22 files reviewed, no comments

Edit Code Review Agent Settings | Greptile

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
core/api/views.py (1)

575-659: Drop the # noqa: E501 directives

Ruff reports every # noqa: E501 here as unused (RUF100), so the lint job fails. Reflow the messages instead of suppressing the warning.

     if not profile.can_add_competitors:
         return {
             "status": "error",
-            "message": f"You have reached the competitor limit for your {profile.product_name} plan. Please upgrade to add more competitors.",  # noqa: E501
+            "message": (
+                "You have reached the competitor limit for your "
+                f"{profile.product_name} plan. Please upgrade to add more competitors."
+            ),
         }
...
     if not profile.can_generate_competitor_posts:
         return {
             "status": "error",
-            "message": f"You have reached the competitor post generation limit for your {profile.product_name} plan. Please upgrade to generate more competitor comparison posts.",  # noqa: E501
+            "message": (
+                "You have reached the competitor post generation limit for your "
+                f"{profile.product_name} plan. Please upgrade to generate more competitor comparison posts."
+            ),
         }
 
     if not profile.can_generate_blog_posts:
         return {
             "status": "error",
-            "message": f"You have reached the content generation limit for your {profile.product_name} plan",  # noqa: E501
+            "message": (
+                "You have reached the content generation limit for your "
+                f"{profile.product_name} plan"
+            ),
         }
🧹 Nitpick comments (2)
requirements.txt (1)

1-201: Consider documenting dependency purpose and audit strategy.

This update introduces ~50+ new or upgraded dependencies to support agent and competitor generation workflows. While the additions are reasonable for the feature scope, there is no inline documentation or companion changelog explaining:

  1. Why each new dependency category was added (e.g., temporalio for workflows, keyring for secrets, openapi-pydantic for schema generation).
  2. The supply chain impact and any security vetting performed.
  3. A strategy for managing the growing dependency footprint (lock file, vulnerability scanning, etc.).

Recommendation: Consider adding comments to group related dependencies or creating a DEPENDENCIES.md documenting the purpose of new packages. Additionally, ensure a dependency audit/lock strategy is in place.

frontend/src/controllers/add_competitor_controller.js (1)

66-74: Emit a namespaced CustomEvent after successful add

Per the Stimulus controller guidelines we follow, this controller should broadcast a bubbled, namespaced CustomEvent when the action succeeds so other controllers can react without a full page reload. Dispatching the event (with the relevant payload in event.detail) keeps behaviors decoupled and avoids violating the “controllers talk via events” rule. Please fire the event right before showing the toast/reload.

As per coding guidelines

       if (data.status === "success") {
+        const competitorAddedEvent = new CustomEvent("competitor:added", {
+          detail: {
+            competitorId: data.competitor_id,
+            projectId: this.projectIdValue,
+            url,
+          },
+          bubbles: true,
+        });
+        this.element.dispatchEvent(competitorAddedEvent);
+
         showMessage("Competitor added! Analysis will continue in the background. The page will reload.", "success");
         setTimeout(() => {
           window.location.reload();
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e0ef80a and 3b23579.

⛔ Files ignored due to path filters (1)
  • poetry.lock is excluded by !**/*.lock
📒 Files selected for processing (22)
  • CHANGELOG.md (1 hunks)
  • core/agent_system_prompts.py (2 hunks)
  • core/agents.py (3 hunks)
  • core/api/schemas.py (1 hunks)
  • core/api/views.py (3 hunks)
  • core/migrations/0038_competitor_blog_post.py (1 hunks)
  • core/models.py (20 hunks)
  • core/prompts.py (5 hunks)
  • core/schemas.py (1 hunks)
  • core/tasks.py (3 hunks)
  • core/urls.py (2 hunks)
  • core/utils.py (1 hunks)
  • core/views.py (3 hunks)
  • frontend/src/controllers/add_competitor_controller.js (1 hunks)
  • frontend/src/controllers/competitor_analysis_controller.js (0 hunks)
  • frontend/src/controllers/competitor_vs_post_controller.js (1 hunks)
  • frontend/src/controllers/copy_markdown_controller.js (1 hunks)
  • frontend/templates/base_project.html (1 hunks)
  • frontend/templates/project/competitor_blog_post_detail.html (1 hunks)
  • frontend/templates/project/project_competitors.html (1 hunks)
  • pyproject.toml (1 hunks)
  • requirements.txt (7 hunks)
💤 Files with no reviewable changes (1)
  • frontend/src/controllers/competitor_analysis_controller.js
🚧 Files skipped from review as they are similar to previous changes (1)
  • core/schemas.py
🧰 Additional context used
📓 Path-based instructions (15)
**/*.html

📄 CodeRabbit inference engine (.cursor/rules/frontend.mdc)

**/*.html: Prefer Stimulus JS for adding interactivity to Django templates instead of raw script elements
Leverage Stimulus data attributes to connect HTML elements with JavaScript functionality
Utilize Stimulus targets to reference specific elements within a controller
Employ Stimulus actions to handle user interactions and events

Files:

  • frontend/templates/project/competitor_blog_post_detail.html
  • frontend/templates/base_project.html
  • frontend/templates/project/project_competitors.html
**/*.{html,htm}

📄 CodeRabbit inference engine (.cursor/rules/ui-ux-design-guidelines.mdc)

Always generate semantic HTML

Files:

  • frontend/templates/project/competitor_blog_post_detail.html
  • frontend/templates/base_project.html
  • frontend/templates/project/project_competitors.html
{**/*.{html,htm,css,scss},**/*_controller.@(js|ts)}

📄 CodeRabbit inference engine (.cursor/rules/ui-ux-design-guidelines.mdc)

Always favor the utility-first Tailwind approach; avoid creating reusable style classes and prefer reuse via template components

Files:

  • frontend/templates/project/competitor_blog_post_detail.html
  • frontend/src/controllers/competitor_vs_post_controller.js
  • frontend/src/controllers/add_competitor_controller.js
  • frontend/src/controllers/copy_markdown_controller.js
  • frontend/templates/base_project.html
  • frontend/templates/project/project_competitors.html
frontend/templates/**/*.html

📄 CodeRabbit inference engine (.cursor/rules/stimulus-events.mdc)

Avoid data-*-outlet links for sibling controller communication when using the event-based approach; keep controllers self-contained

Use semantic HTML elements (e.g., dialog, details/summary) in templates

Files:

  • frontend/templates/project/competitor_blog_post_detail.html
  • frontend/templates/base_project.html
  • frontend/templates/project/project_competitors.html
frontend/src/controllers/**/*.js

📄 CodeRabbit inference engine (.cursor/rules/frontend.mdc)

frontend/src/controllers/**/*.js: Use Stimulus controllers to encapsulate JavaScript behavior and keep it separate from HTML structure
New controllers should be created in the frontend/src/controllers directory

Use Stimulus.js controllers for interactive frontend behavior

Files:

  • frontend/src/controllers/competitor_vs_post_controller.js
  • frontend/src/controllers/add_competitor_controller.js
  • frontend/src/controllers/copy_markdown_controller.js
**/*.js

📄 CodeRabbit inference engine (.cursor/rules/stimulus-general.mdc)

**/*.js: Add semicolons at the end of statements in JavaScript files
Use double quotes instead of single quotes for string literals in JavaScript files

Files:

  • frontend/src/controllers/competitor_vs_post_controller.js
  • frontend/src/controllers/add_competitor_controller.js
  • frontend/src/controllers/copy_markdown_controller.js
{**/*.{css,scss},**/*_controller.@(js|ts)}

📄 CodeRabbit inference engine (.cursor/rules/ui-ux-design-guidelines.mdc)

Never create new styles without explicitly receiving permission to do so

Files:

  • frontend/src/controllers/competitor_vs_post_controller.js
  • frontend/src/controllers/add_competitor_controller.js
  • frontend/src/controllers/copy_markdown_controller.js
frontend/src/controllers/**/*_controller.js

📄 CodeRabbit inference engine (.cursor/rules/stimulus-events.mdc)

frontend/src/controllers/**/*_controller.js: Dispatch a CustomEvent from the actor Stimulus controller to signal actions to other controllers
Name custom events descriptively and namespaced (e.g., "suggestion:move")
Set bubbles: true on CustomEvent so ancestor listeners (window/document) can receive it
Include all necessary payload in event.detail (e.g., element and destination)
Dispatch the event from the controller element (this.element.dispatchEvent(event))
In listener controllers, register global event listeners in connect() and remove them in disconnect()
Bind handler methods and keep a reference (e.g., this.boundMove = this.move.bind(this)) to correctly remove listeners
Attach listeners to window or document to catch bubbled custom events from anywhere on the page
Inspect event.detail in the handler and act conditionally based on its contents (e.g., match destination)

Files:

  • frontend/src/controllers/competitor_vs_post_controller.js
  • frontend/src/controllers/add_competitor_controller.js
  • frontend/src/controllers/copy_markdown_controller.js
**/*.py

📄 CodeRabbit inference engine (.cursor/rules/backend.mdc)

**/*.py: Prioritize readability and maintainability; follow PEP 8
Use descriptive variable/function names with underscores

In Python, use try-except blocks that catch specific exception types; do not catch broad Exception.

**/*.py: Follow PEP 8 for Python code style
Use descriptive variable names with underscores (snake_case) in Python
Prefer Django's built-in features over external libraries

**/*.py: Use structlog for logging in Python code (avoid print and the standard logging module)
Include contextual key-value fields in log messages (e.g., error=str(e), exc_info=True, profile_id=profile.id)

**/*.py: Use descriptive, full-word variable names; avoid abbreviations and single-letter variables in Python
Provide context in variable names when format or type matters (e.g., include _iso_format, date)
Extract unchanging values into UPPER_CASE constants
Use intermediary variables to name parsed groups instead of using index access directly
Naming conventions: use is_/has_/can_ for booleans; include 'date' for dates; snake_case for variables/functions; PascalCase for classes
Define variables close to where they are used to keep lifespan short
Name things after what they do, not how they're used; ensure names make sense without extra context
Avoid generic names like data, info, manager; use specific, intention-revealing names
Function names should include necessary context without being verbose
If naming is hard, split functions into smaller focused parts
Maintain consistency: reuse the same verbs and nouns for the same concepts; name variables after the functions that create them
Use more descriptive names for longer-lived variables
Avoid else statements by using guard clauses for early returns
Replace simple conditionals with direct assignment when both branches call the same function with different values
Use dictionaries as dispatch tables instead of multiple equal-probability elif chains
Validate input before processing to prevent errors propagating in...

Files:

  • core/utils.py
  • core/tasks.py
  • core/api/views.py
  • core/agents.py
  • core/api/schemas.py
  • core/urls.py
  • core/migrations/0038_competitor_blog_post.py
  • core/agent_system_prompts.py
  • core/models.py
  • core/views.py
  • core/prompts.py
core/**/*.py

📄 CodeRabbit inference engine (.cursor/rules/backend.mdc)

Leverage Django's ORM; avoid raw SQL when possible

Files:

  • core/utils.py
  • core/tasks.py
  • core/api/views.py
  • core/agents.py
  • core/api/schemas.py
  • core/urls.py
  • core/migrations/0038_competitor_blog_post.py
  • core/agent_system_prompts.py
  • core/models.py
  • core/views.py
  • core/prompts.py
**/{utils,repository}.py

📄 CodeRabbit inference engine (AGENTS.md)

Avoid generic filenames like utils.py and repository.py; use specific names instead

Files:

  • core/utils.py
core/tasks.py

📄 CodeRabbit inference engine (CLAUDE.md)

Define background tasks using django-q2 in core/tasks.py

Files:

  • core/tasks.py
core/api/**/*.py

📄 CodeRabbit inference engine (.cursor/rules/backend.mdc)

core/api/**/*.py: django-ninja generate openapi documentation, so make sure to populate views with relevant data in order for it to show up in the OpenAPI docs.
Use Pydantic models for schema validation
Use django-ninja's authentication classes

Implement REST APIs with django-ninja using Pydantic schemas under core/api/

Files:

  • core/api/views.py
  • core/api/schemas.py
core/models.py

📄 CodeRabbit inference engine (CLAUDE.md)

core/models.py: Place business logic in Django models (fat models pattern)
Validate simple constraints in the database and place complex domain logic in Django models

Files:

  • core/models.py
core/views.py

📄 CodeRabbit inference engine (CLAUDE.md)

Keep Django views skinny: handle request/response only

Files:

  • core/views.py
🧠 Learnings (5)
📚 Learning: 2025-10-04T08:52:37.437Z
Learnt from: CR
Repo: rasulkireev/TuxSEO PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-10-04T08:52:37.437Z
Learning: Applies to frontend/src/controllers/**/*.js : Use Stimulus.js controllers for interactive frontend behavior

Applied to files:

  • frontend/src/controllers/competitor_vs_post_controller.js
  • frontend/src/controllers/add_competitor_controller.js
  • frontend/src/controllers/copy_markdown_controller.js
📚 Learning: 2025-10-04T08:52:37.437Z
Learnt from: CR
Repo: rasulkireev/TuxSEO PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-10-04T08:52:37.437Z
Learning: Always add AGENTS.md into AI context

Applied to files:

  • core/agents.py
📚 Learning: 2025-10-04T08:52:37.437Z
Learnt from: CR
Repo: rasulkireev/TuxSEO PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-10-04T08:52:37.437Z
Learning: Applies to core/agents/**/*.py : Implement AI agents using pydantic-ai under core/agents/

Applied to files:

  • core/agents.py
  • core/models.py
📚 Learning: 2025-10-04T08:52:58.590Z
Learnt from: CR
Repo: rasulkireev/TuxSEO PR: 0
File: .cursor/rules/agent-rules.mdc:0-0
Timestamp: 2025-10-04T08:52:58.590Z
Learning: Always add AGENTS.md into context

Applied to files:

  • core/agents.py
📚 Learning: 2025-10-04T08:52:37.437Z
Learnt from: CR
Repo: rasulkireev/TuxSEO PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-10-04T08:52:37.437Z
Learning: Applies to core/tasks.py : Define background tasks using django-q2 in core/tasks.py

Applied to files:

  • core/models.py
🧬 Code graph analysis (8)
frontend/src/controllers/competitor_vs_post_controller.js (1)
frontend/src/utils/messages.js (1)
  • showMessage (2-39)
frontend/src/controllers/add_competitor_controller.js (2)
frontend/src/utils/messages.js (1)
  • showMessage (2-39)
frontend/src/controllers/competitor_vs_post_controller.js (3)
  • response (25-34)
  • data (41-41)
  • error (37-37)
core/tasks.py (1)
core/models.py (4)
  • Project (361-827)
  • find_competitors (705-789)
  • get_and_save_list_of_competitors (791-827)
  • populate_name_description (1472-1509)
core/api/views.py (2)
core/api/schemas.py (2)
  • GenerateCompetitorVsTitleIn (134-135)
  • GenerateCompetitorVsTitleOut (138-141)
core/models.py (10)
  • can_add_competitors (321-322)
  • product_name (173-178)
  • Competitor (1405-1635)
  • get_page_content (456-484)
  • get_page_content (1339-1363)
  • get_page_content (1446-1470)
  • populate_name_description (1472-1509)
  • can_generate_competitor_posts (325-326)
  • can_generate_blog_posts (272-273)
  • generate_vs_blog_post (1588-1635)
core/agents.py (4)
core/agent_system_prompts.py (3)
  • markdown_lists (30-36)
  • add_language_specification (132-137)
  • add_project_pages (65-113)
core/choices.py (2)
  • AIModel (117-119)
  • get_default_ai_model (125-127)
core/schemas.py (6)
  • BlogPostGenerationContext (180-187)
  • CompetitorVsPostContext (262-275)
  • CompetitorVsTitleContext (255-259)
  • ProjectDetails (15-97)
  • ProjectPageDetails (100-127)
  • TitleSuggestion (149-160)
core/models.py (4)
  • project_details (404-419)
  • competitor_details (1439-1444)
  • competitor_details (1549-1557)
  • add_language_specification (563-569)
core/urls.py (1)
core/views.py (2)
  • ProjectCompetitorsView (824-862)
  • CompetitorBlogPostDetailView (912-930)
core/models.py (3)
core/agent_system_prompts.py (1)
  • markdown_lists (30-36)
core/schemas.py (3)
  • ProjectDetails (15-97)
  • CompetitorVsPostContext (262-275)
  • ProjectPageContext (169-177)
core/model_utils.py (1)
  • run_agent_synchronously (18-75)
core/views.py (1)
core/models.py (10)
  • Competitor (1405-1635)
  • Project (361-827)
  • competitor_limit (276-280)
  • number_of_competitors (290-293)
  • number_of_competitors (750-755)
  • can_add_competitors (321-322)
  • competitor_posts_limit (283-287)
  • number_of_competitor_posts_generated (296-304)
  • can_generate_competitor_posts (325-326)
  • is_on_free_plan (181-182)
🪛 Ruff (0.14.2)
core/tasks.py

249-249: Unused noqa directive (non-enabled: E501)

Remove unused noqa directive

(RUF100)


266-266: Use explicit conversion flag

Replace with conversion flag

(RUF010)

core/api/views.py

576-576: Unused noqa directive (non-enabled: E501)

Remove unused noqa directive

(RUF100)


652-652: Unused noqa directive (non-enabled: E501)

Remove unused noqa directive

(RUF100)


658-658: Unused noqa directive (non-enabled: E501)

Remove unused noqa directive

(RUF100)


681-685: Consider moving this statement to an else block

(TRY300)


698-698: Use explicit conversion flag

Replace with conversion flag

(RUF010)

core/agents.py

208-208: Unused noqa directive (non-enabled: E501)

Remove unused noqa directive

(RUF100)

core/migrations/0038_competitor_blog_post.py

8-10: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


12-18: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)

core/agent_system_prompts.py

36-36: Unused noqa directive (non-enabled: E501)

Remove unused noqa directive

(RUF100)

core/models.py

750-750: Unused function argument: ctx

(ARG001)


1622-1622: Unused noqa directive (non-enabled: E501)

Remove unused noqa directive

(RUF100)

core/prompts.py

21-21: Unused noqa directive (non-enabled: E501)

Remove unused noqa directive

(RUF100)


61-61: Unused noqa directive (non-enabled: E501)

Remove unused noqa directive

(RUF100)


148-148: Unused noqa directive (non-enabled: E501)

Remove unused noqa directive

(RUF100)


195-195: Unused noqa directive (non-enabled: E501)

Remove unused noqa directive

(RUF100)


227-227: Unused noqa directive (non-enabled: E501)

Remove unused noqa directive

(RUF100)

🔇 Additional comments (5)
CHANGELOG.md (1)

20-21: CHANGELOG entries are clear and well-formatted.

The new entries accurately document the PydanticAI library upgrade, agent refactoring for centralization, and the new Competitors page feature, all in line with the PR objectives.

Also applies to: 28-28

pyproject.toml (1)

38-38: Pydantic-ai v1.9.1 upgrade verified—no issues found.

The codebase has been successfully updated for v1.9.1 compatibility. All 20+ agent executions use the correct async pattern via run_agent_synchronously, all 40+ result accesses follow the new result.output pattern, and all Agent instantiations use the correct v1.9.1 API with proper parameters (output_type, deps_type, system_prompt). No deprecated patterns remain, and all imports are compatible with the new version.

requirements.txt (3)

70-70: Clarify hardware constraint logic for hf-xet package.

Line 70 specifies hf-xet with a complex platform constraint: platform_machine == "x86_64" or platform_machine == "amd64" or platform_machine == "AMD64" or platform_machine == "arm64" or platform_machine == "aarch64". This is unusual because:

  1. The condition essentially includes most modern architectures (essentially all except older/embedded systems).
  2. The original constraint (if any) is unclear — was this broadened intentionally?
  3. Consider whether this should be inverted (exclude unsupported platforms) or simplified.

Clarify the intent or simplify the constraint for maintainability.


109-109: Outdated pre-release OpenTelemetry versions in production dependencies.

Lines 109 and 114 pin outdated beta versions of OpenTelemetry instrumentation packages:

  • opentelemetry-instrumentation-httpx==0.56b0 (latest available: 0.58b0)
  • opentelemetry-util-http==0.56b0 (latest available: 0.59b0)

No stable releases currently exist for these packages—only beta versions are available. However, the pinned versions are outdated. Consider updating to the latest available versions (0.58b0 and 0.59b0 respectively) for bug fixes and improvements, while acknowledging that production use still carries the stability risk inherent to pre-release software.


134-138: Correct invalid pydantic-ai version: 1.9.1 does not exist (latest is v1.7.0).

pydantic-ai v1.9.1 does not appear to exist on GitHub releases, with v1.7.0 (Oct 27, 2025) being the latest available. The requirements.txt specifies pydantic-ai==1.9.1 which would cause pip install to fail.

Update line 135 to use a valid version (e.g., pydantic-ai==1.7.0 or the intended version if this was a typo). The codebase actively uses pydantic_ai (Agent, RunContext, OpenAIChatModel, OpenAIProvider, capture_run_messages), so ensure the specified version is compatible with the imports in core/agents.py, core/models.py, and core/utils.py.

⛔ Skipped due to learnings
Learnt from: CR
Repo: rasulkireev/TuxSEO PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-10-04T08:52:37.437Z
Learning: Applies to core/agents/**/*.py : Implement AI agents using pydantic-ai under core/agents/

Comment on lines 17 to 28
## [0.0.7] - 2025-11-02
### Changed
- Project UI updated to be more intuitive.
- PydanticAI library upgrade.
- Competitor blog post generation agent moved to centralized agents.py file for easier maintenance.

### Added
- Copy as HTML on Generated Blog Posts.
- PDF Generation for blog posts.
- Centralized location of all AI models used in the app.
- Referrer model to display banners for expected referrer like producthunt

- Competitors page to view all competitors for any given project
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Multiple entries for the same version (0.0.7) with different dates.

The CHANGELOG contains several entries for version 0.0.7 dated 2025-11-02, 2025-11-01, 2025-10-29, etc. This makes it unclear which release date is authoritative. Typically, all changes for a given version are consolidated under a single, final release date. Consider consolidating all v0.0.7 entries into a single dated entry or clarifying the versioning strategy if this pattern is intentional.

🤖 Prompt for AI Agents
In CHANGELOG.md around lines 17 to 28, there are multiple separate entries for
version 0.0.7 with different dates; consolidate these into a single v0.0.7 entry
using the final/authoritative release date (pick the intended date), merge and
deduplicate all bullet points under the appropriate sections
(Added/Changed/Fixed), remove the extra v0.0.7 headings, and ensure the
consolidated entry follows the project's changelog format and ordering.

Comment on lines +603 to 612
logger.warning(
"[Add Competitor] Failed to get page content for competitor",
competitor_id=competitor.id,
url=data.url,
project_id=project.id,
)
return {
"status": "error",
"message": "Failed to get page content for this competitor URL",
"message": "Failed to get page content for competitor",
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Clean up failed competitor creations

If get_page_content() fails we return an error but leave the freshly created Competitor row in place. That phantom record still counts against plan limits and blocks the user from retrying (duplicate URL constraint). Please delete the object before returning the error so we don’t strand unusable competitors.

         else:
             logger.warning(
                 "[Add Competitor] Failed to get page content for competitor",
                 competitor_id=competitor.id,
                 url=data.url,
                 project_id=project.id,
             )
+            competitor.delete()
             return {
                 "status": "error",
                 "message": "Failed to get page content for competitor",
             }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
logger.warning(
"[Add Competitor] Failed to get page content for competitor",
competitor_id=competitor.id,
url=data.url,
project_id=project.id,
)
return {
"status": "error",
"message": "Failed to get page content for this competitor URL",
"message": "Failed to get page content for competitor",
}
logger.warning(
"[Add Competitor] Failed to get page content for competitor",
competitor_id=competitor.id,
url=data.url,
project_id=project.id,
)
competitor.delete()
return {
"status": "error",
"message": "Failed to get page content for competitor",
}
🤖 Prompt for AI Agents
In core/api/views.py around lines 603 to 612, when get_page_content() fails you
currently log and return an error but leave the newly created Competitor row in
the DB; delete the created competitor before returning the error (e.g., call
competitor.delete() or the appropriate ORM cleanup) and ensure this deletion
happens inside the failure branch (or a try/except/finally) so no phantom
competitor remains to count against plan limits or block retries.

Comment on lines +47 to +72
showMessage("VS blog post generated successfully!", "success");

const viewPostUrl = `/project/${this.projectIdValue}/competitor/${this.competitorIdValue}/post/`;

button.outerHTML = `
<a href="${viewPostUrl}"
class="inline-flex items-center px-3 py-1.5 text-xs font-medium text-white bg-gray-900 rounded-md border border-gray-900 hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-gray-500">
<svg class="mr-1.5 w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path>
</svg>
View Post
</a>
`;

} catch (error) {
button.disabled = false;
button.innerHTML = `
<svg class="mr-1.5 w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path>
</svg>
Generate Post
`;

showMessage(error.message, "error");
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Broadcast generation results via CustomEvent.

Per our Stimulus conventions, this controller should notify the rest of the UI through a bubbling CustomEvent both on success and on failure. Suggest patch:

       showMessage("VS blog post generated successfully!", "success");

+      const generatedEvent = new CustomEvent("competitor-vs-post:generated", {
+        bubbles: true,
+        detail: {
+          competitorId: this.competitorIdValue,
+          projectId: this.projectIdValue
+        }
+      });
+      this.element.dispatchEvent(generatedEvent);
+
       const viewPostUrl = `/project/${this.projectIdValue}/competitor/${this.competitorIdValue}/post/`;

@@
       showMessage(error.message, "error");
+
+      const errorEvent = new CustomEvent("competitor-vs-post:error", {
+        bubbles: true,
+        detail: {
+          competitorId: this.competitorIdValue,
+          projectId: this.projectIdValue,
+          errorMessage: error.message
+        }
+      });
+      this.element.dispatchEvent(errorEvent);

As per coding guidelines.

Comment on lines +9 to +29
async copy() {
await navigator.clipboard.writeText(this.contentValue);

const button = this.buttonTarget;
const originalText = button.innerHTML;

button.innerHTML = `
<svg class="mr-2 w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
Copied!
`;
button.classList.remove("hover:bg-gray-50");
button.classList.add("bg-green-50", "text-green-700", "border-green-300");

setTimeout(() => {
button.innerHTML = originalText;
button.classList.remove("bg-green-50", "text-green-700", "border-green-300");
button.classList.add("hover:bg-gray-50");
}, 2000);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Dispatch a bubbling copy event from the controller.

Our Stimulus controllers are expected to signal actions via CustomEvent so other controllers can react. Please emit a namespaced, bubbling event with the copied payload right after the clipboard write succeeds. One possible patch:

   async copy() {
     await navigator.clipboard.writeText(this.contentValue);

+    const copiedEvent = new CustomEvent("copy-markdown:copied", {
+      bubbles: true,
+      detail: {
+        content: this.contentValue,
+        sourceElement: this.element
+      }
+    });
+    this.element.dispatchEvent(copiedEvent);
+
     const button = this.buttonTarget;
     const originalText = button.innerHTML;

As per coding guidelines.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async copy() {
await navigator.clipboard.writeText(this.contentValue);
const button = this.buttonTarget;
const originalText = button.innerHTML;
button.innerHTML = `
<svg class="mr-2 w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
Copied!
`;
button.classList.remove("hover:bg-gray-50");
button.classList.add("bg-green-50", "text-green-700", "border-green-300");
setTimeout(() => {
button.innerHTML = originalText;
button.classList.remove("bg-green-50", "text-green-700", "border-green-300");
button.classList.add("hover:bg-gray-50");
}, 2000);
}
async copy() {
await navigator.clipboard.writeText(this.contentValue);
const copiedEvent = new CustomEvent("copy-markdown:copied", {
bubbles: true,
detail: {
content: this.contentValue,
sourceElement: this.element
}
});
this.element.dispatchEvent(copiedEvent);
const button = this.buttonTarget;
const originalText = button.innerHTML;
button.innerHTML = `
<svg class="mr-2 w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
Copied!
`;
button.classList.remove("hover:bg-gray-50");
button.classList.add("bg-green-50", "text-green-700", "border-green-300");
setTimeout(() => {
button.innerHTML = originalText;
button.classList.remove("bg-green-50", "text-green-700", "border-green-300");
button.classList.add("hover:bg-gray-50");
}, 2000);
}
🤖 Prompt for AI Agents
In frontend/src/controllers/copy_markdown_controller.js around lines 9 to 29,
after the await navigator.clipboard.writeText(this.contentValue) call dispatch a
namespaced, bubbling CustomEvent (e.g. "copy:markdown:copied") with the copied
text in its detail so other Stimulus controllers can react; construct the event
with detail: { text: this.contentValue } and options { bubbles: true, composed:
true } and dispatch it from the controller root (this.element) immediately after
the successful write before updating the button UI.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (9)
CHANGELOG.md (1)

17-28: Multiple 0.0.7 version entries remain unresolved.

As previously noted, this CHANGELOG contains multiple separate entries for version 0.0.7 with different dates. The new additions (PydanticAI library upgrade and Competitors page) continue this pattern. Consider consolidating all v0.0.7 entries into a single dated entry with all changes grouped by type.

core/tasks.py (2)

240-249: LGTM! Correctly implements the cost-saving workflow.

The logic change from analyze_competitor() to populate_name_description() aligns with the documented goal of reducing costs. The inline comments clearly explain why the expensive analysis path is intentionally skipped.

However, the unused # noqa: E501 directive on line 249 should be removed as previously noted, since E501 is not enabled in the Ruff configuration.


260-266: Error handling updated appropriately.

The error message update from "Error analyzing competitor" to "Error processing competitor" correctly reflects the simplified workflow.

However, as previously noted, line 266 should use an f-string conversion flag ({e!s} or just {e}) instead of {str(e)} to satisfy Ruff rule RUF010.

core/prompts.py (1)

21-21: Remove unused # noqa: E501 directives.

As previously noted, all five # noqa: E501 directives added in this file (lines 21, 61, 148, 195, and 227) are flagged by Ruff as unused (RUF100) because the E501 rule is not enabled in the project's configuration. Please remove these directives to resolve the linting warnings.

Also applies to: 61-61, 148-148, 195-195, 227-227

core/agent_system_prompts.py (1)

30-36: Remove the unused # noqa: E501 directive.

The existing review comment already flags this issue. Ruff reports RUF100 because E501 is not enabled in the linter configuration. Remove the directive to clean up the linting output.

frontend/src/controllers/copy_markdown_controller.js (1)

9-29: Dispatch a bubbling CustomEvent after clipboard write.

The existing review comment already covers this requirement comprehensively. Per coding guidelines, dispatch a namespaced, bubbling CustomEvent (e.g., "copy-markdown:copied") with the copied content in event.detail immediately after the successful clipboard write so other Stimulus controllers can react to the copy action.

As per coding guidelines.

frontend/src/controllers/competitor_vs_post_controller.js (1)

12-73: Dispatch bubbling CustomEvents for success and error outcomes.

The existing review comment already covers this requirement comprehensively. Per coding guidelines, dispatch namespaced, bubbling CustomEvents (e.g., "competitor-vs-post:generated" and "competitor-vs-post:error") with relevant payload in event.detail so other Stimulus controllers can react to generation outcomes.

As per coding guidelines.

core/agents.py (1)

203-208: Remove unused noqa directive.

The # noqa: E501 on line 208 is unnecessary since the multi-line string doesn't trigger line-length warnings.

Apply this diff:

         For example, if the text is 'sample text[1]', the link should be inserted like this: '[sample text](https://www.example.com)'.
-    """  # noqa: E501
+    """
core/api/views.py (1)

592-612: Delete competitor on content fetch failure.

If get_page_content() fails, the newly created Competitor object remains in the database, counting against plan limits and preventing retries due to the duplicate URL constraint.

Apply this diff:

         got_content = competitor.get_page_content()
         if got_content:
             competitor.populate_name_description()
             logger.info(
                 "[Add Competitor] Successfully populated competitor details",
                 competitor_id=competitor.id,
                 competitor_name=competitor.name,
                 project_id=project.id,
             )
         else:
             logger.warning(
                 "[Add Competitor] Failed to get page content for competitor",
                 competitor_id=competitor.id,
                 url=data.url,
                 project_id=project.id,
             )
+            competitor.delete()
             return {
                 "status": "error",
                 "message": "Failed to get page content for competitor",
             }
🧹 Nitpick comments (3)
requirements.txt (1)

1-201: Large dependency surface expansion.

The upgrade to pydantic-ai 1.9.1 introduces numerous new dependencies including AI tooling (ag-ui-protocol, fastmcp, genai-prices), platform-specific packages (jeepney, secretstorage for Linux; pywin32-ctypes for Windows), OpenTelemetry instrumentation, and various utility libraries. While this expansion is expected given the major version upgrade and new competitor analysis features, consider reviewing whether all transitive dependencies are necessary for your use case.

core/views.py (1)

912-931: Move title formatting to the Competitor model.

The blog post title format is duplicated here and in core/models.py:1596 within Competitor.generate_vs_blog_post(). Consider adding a property to the Competitor model to centralize this logic.

Add a property to the Competitor model in core/models.py:

@property
def vs_blog_post_title(self):
    return f"{self.project.name} vs. {self.name}: Which is Better?"

Then update this view:

-        context["blog_post_title"] = f"{project.name} vs. {competitor.name}: Which is Better?"
+        context["blog_post_title"] = competitor.vs_blog_post_title

And update core/models.py line 1596 to use the same property.

core/api/views.py (1)

633-700: LGTM with optional style improvement.

The endpoint properly checks permissions and limits, handles errors appropriately, and returns a clean response structure.

Optional: Consider moving the success return to an else block for clarity (suggested by static analysis):

     try:
         logger.info(
             "Generating VS competitor blog post",
             competitor_id=competitor.id,
             competitor_name=competitor.name,
             project_id=project.id,
             profile_id=profile.id,
         )

         blog_post_content = competitor.generate_vs_blog_post()

         logger.info(
             "VS competitor blog post generated successfully",
             competitor_id=competitor.id,
             competitor_name=competitor.name,
             content_length=len(blog_post_content),
             project_id=project.id,
             profile_id=profile.id,
         )

-        return {
-            "status": "success",
-            "message": "VS blog post generated successfully!",
-            "competitor_id": competitor.id,
-        }
-
     except Exception as e:
         logger.error(
             "Failed to generate competitor vs. blog post",
             error=str(e),
             exc_info=True,
             competitor_id=data.competitor_id,
             project_id=project.id,
             profile_id=profile.id,
         )
         return {
             "status": "error",
             "message": f"Failed to generate competitor comparison blog post: {str(e)}",
         }
+    else:
+        return {
+            "status": "success",
+            "message": "VS blog post generated successfully!",
+            "competitor_id": competitor.id,
+        }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d8988c8 and 3b23579.

⛔ Files ignored due to path filters (1)
  • poetry.lock is excluded by !**/*.lock
📒 Files selected for processing (22)
  • CHANGELOG.md (1 hunks)
  • core/agent_system_prompts.py (2 hunks)
  • core/agents.py (3 hunks)
  • core/api/schemas.py (1 hunks)
  • core/api/views.py (3 hunks)
  • core/migrations/0038_competitor_blog_post.py (1 hunks)
  • core/models.py (20 hunks)
  • core/prompts.py (5 hunks)
  • core/schemas.py (1 hunks)
  • core/tasks.py (3 hunks)
  • core/urls.py (2 hunks)
  • core/utils.py (1 hunks)
  • core/views.py (3 hunks)
  • frontend/src/controllers/add_competitor_controller.js (1 hunks)
  • frontend/src/controllers/competitor_analysis_controller.js (0 hunks)
  • frontend/src/controllers/competitor_vs_post_controller.js (1 hunks)
  • frontend/src/controllers/copy_markdown_controller.js (1 hunks)
  • frontend/templates/base_project.html (1 hunks)
  • frontend/templates/project/competitor_blog_post_detail.html (1 hunks)
  • frontend/templates/project/project_competitors.html (1 hunks)
  • pyproject.toml (1 hunks)
  • requirements.txt (7 hunks)
💤 Files with no reviewable changes (1)
  • frontend/src/controllers/competitor_analysis_controller.js
🧰 Additional context used
📓 Path-based instructions (15)
**/*.py

📄 CodeRabbit inference engine (.cursor/rules/backend.mdc)

**/*.py: Prioritize readability and maintainability; follow PEP 8
Use descriptive variable/function names with underscores

In Python, use try-except blocks that catch specific exception types; do not catch broad Exception.

**/*.py: Follow PEP 8 for Python code style
Use descriptive variable names with underscores (snake_case) in Python
Prefer Django's built-in features over external libraries

**/*.py: Use structlog for logging in Python code (avoid print and the standard logging module)
Include contextual key-value fields in log messages (e.g., error=str(e), exc_info=True, profile_id=profile.id)

**/*.py: Use descriptive, full-word variable names; avoid abbreviations and single-letter variables in Python
Provide context in variable names when format or type matters (e.g., include _iso_format, date)
Extract unchanging values into UPPER_CASE constants
Use intermediary variables to name parsed groups instead of using index access directly
Naming conventions: use is_/has_/can_ for booleans; include 'date' for dates; snake_case for variables/functions; PascalCase for classes
Define variables close to where they are used to keep lifespan short
Name things after what they do, not how they're used; ensure names make sense without extra context
Avoid generic names like data, info, manager; use specific, intention-revealing names
Function names should include necessary context without being verbose
If naming is hard, split functions into smaller focused parts
Maintain consistency: reuse the same verbs and nouns for the same concepts; name variables after the functions that create them
Use more descriptive names for longer-lived variables
Avoid else statements by using guard clauses for early returns
Replace simple conditionals with direct assignment when both branches call the same function with different values
Use dictionaries as dispatch tables instead of multiple equal-probability elif chains
Validate input before processing to prevent errors propagating in...

Files:

  • core/agent_system_prompts.py
  • core/api/schemas.py
  • core/prompts.py
  • core/schemas.py
  • core/utils.py
  • core/views.py
  • core/api/views.py
  • core/migrations/0038_competitor_blog_post.py
  • core/urls.py
  • core/tasks.py
  • core/agents.py
  • core/models.py
core/**/*.py

📄 CodeRabbit inference engine (.cursor/rules/backend.mdc)

Leverage Django's ORM; avoid raw SQL when possible

Files:

  • core/agent_system_prompts.py
  • core/api/schemas.py
  • core/prompts.py
  • core/schemas.py
  • core/utils.py
  • core/views.py
  • core/api/views.py
  • core/migrations/0038_competitor_blog_post.py
  • core/urls.py
  • core/tasks.py
  • core/agents.py
  • core/models.py
frontend/src/controllers/**/*.js

📄 CodeRabbit inference engine (.cursor/rules/frontend.mdc)

frontend/src/controllers/**/*.js: Use Stimulus controllers to encapsulate JavaScript behavior and keep it separate from HTML structure
New controllers should be created in the frontend/src/controllers directory

Use Stimulus.js controllers for interactive frontend behavior

Files:

  • frontend/src/controllers/add_competitor_controller.js
  • frontend/src/controllers/competitor_vs_post_controller.js
  • frontend/src/controllers/copy_markdown_controller.js
**/*.js

📄 CodeRabbit inference engine (.cursor/rules/stimulus-general.mdc)

**/*.js: Add semicolons at the end of statements in JavaScript files
Use double quotes instead of single quotes for string literals in JavaScript files

Files:

  • frontend/src/controllers/add_competitor_controller.js
  • frontend/src/controllers/competitor_vs_post_controller.js
  • frontend/src/controllers/copy_markdown_controller.js
{**/*.{css,scss},**/*_controller.@(js|ts)}

📄 CodeRabbit inference engine (.cursor/rules/ui-ux-design-guidelines.mdc)

Never create new styles without explicitly receiving permission to do so

Files:

  • frontend/src/controllers/add_competitor_controller.js
  • frontend/src/controllers/competitor_vs_post_controller.js
  • frontend/src/controllers/copy_markdown_controller.js
{**/*.{html,htm,css,scss},**/*_controller.@(js|ts)}

📄 CodeRabbit inference engine (.cursor/rules/ui-ux-design-guidelines.mdc)

Always favor the utility-first Tailwind approach; avoid creating reusable style classes and prefer reuse via template components

Files:

  • frontend/src/controllers/add_competitor_controller.js
  • frontend/templates/project/competitor_blog_post_detail.html
  • frontend/src/controllers/competitor_vs_post_controller.js
  • frontend/templates/base_project.html
  • frontend/src/controllers/copy_markdown_controller.js
  • frontend/templates/project/project_competitors.html
frontend/src/controllers/**/*_controller.js

📄 CodeRabbit inference engine (.cursor/rules/stimulus-events.mdc)

frontend/src/controllers/**/*_controller.js: Dispatch a CustomEvent from the actor Stimulus controller to signal actions to other controllers
Name custom events descriptively and namespaced (e.g., "suggestion:move")
Set bubbles: true on CustomEvent so ancestor listeners (window/document) can receive it
Include all necessary payload in event.detail (e.g., element and destination)
Dispatch the event from the controller element (this.element.dispatchEvent(event))
In listener controllers, register global event listeners in connect() and remove them in disconnect()
Bind handler methods and keep a reference (e.g., this.boundMove = this.move.bind(this)) to correctly remove listeners
Attach listeners to window or document to catch bubbled custom events from anywhere on the page
Inspect event.detail in the handler and act conditionally based on its contents (e.g., match destination)

Files:

  • frontend/src/controllers/add_competitor_controller.js
  • frontend/src/controllers/competitor_vs_post_controller.js
  • frontend/src/controllers/copy_markdown_controller.js
**/*.html

📄 CodeRabbit inference engine (.cursor/rules/frontend.mdc)

**/*.html: Prefer Stimulus JS for adding interactivity to Django templates instead of raw script elements
Leverage Stimulus data attributes to connect HTML elements with JavaScript functionality
Utilize Stimulus targets to reference specific elements within a controller
Employ Stimulus actions to handle user interactions and events

Files:

  • frontend/templates/project/competitor_blog_post_detail.html
  • frontend/templates/base_project.html
  • frontend/templates/project/project_competitors.html
**/*.{html,htm}

📄 CodeRabbit inference engine (.cursor/rules/ui-ux-design-guidelines.mdc)

Always generate semantic HTML

Files:

  • frontend/templates/project/competitor_blog_post_detail.html
  • frontend/templates/base_project.html
  • frontend/templates/project/project_competitors.html
frontend/templates/**/*.html

📄 CodeRabbit inference engine (.cursor/rules/stimulus-events.mdc)

Avoid data-*-outlet links for sibling controller communication when using the event-based approach; keep controllers self-contained

Use semantic HTML elements (e.g., dialog, details/summary) in templates

Files:

  • frontend/templates/project/competitor_blog_post_detail.html
  • frontend/templates/base_project.html
  • frontend/templates/project/project_competitors.html
core/api/**/*.py

📄 CodeRabbit inference engine (.cursor/rules/backend.mdc)

core/api/**/*.py: django-ninja generate openapi documentation, so make sure to populate views with relevant data in order for it to show up in the OpenAPI docs.
Use Pydantic models for schema validation
Use django-ninja's authentication classes

Implement REST APIs with django-ninja using Pydantic schemas under core/api/

Files:

  • core/api/schemas.py
  • core/api/views.py
**/{utils,repository}.py

📄 CodeRabbit inference engine (AGENTS.md)

Avoid generic filenames like utils.py and repository.py; use specific names instead

Files:

  • core/utils.py
core/views.py

📄 CodeRabbit inference engine (CLAUDE.md)

Keep Django views skinny: handle request/response only

Files:

  • core/views.py
core/tasks.py

📄 CodeRabbit inference engine (CLAUDE.md)

Define background tasks using django-q2 in core/tasks.py

Files:

  • core/tasks.py
core/models.py

📄 CodeRabbit inference engine (CLAUDE.md)

core/models.py: Place business logic in Django models (fat models pattern)
Validate simple constraints in the database and place complex domain logic in Django models

Files:

  • core/models.py
🧠 Learnings (5)
📚 Learning: 2025-10-04T08:52:37.437Z
Learnt from: CR
Repo: rasulkireev/TuxSEO PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-10-04T08:52:37.437Z
Learning: Applies to frontend/src/controllers/**/*.js : Use Stimulus.js controllers for interactive frontend behavior

Applied to files:

  • frontend/src/controllers/add_competitor_controller.js
  • frontend/src/controllers/competitor_vs_post_controller.js
  • frontend/src/controllers/copy_markdown_controller.js
📚 Learning: 2025-10-04T08:52:37.437Z
Learnt from: CR
Repo: rasulkireev/TuxSEO PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-10-04T08:52:37.437Z
Learning: Applies to core/agents/**/*.py : Implement AI agents using pydantic-ai under core/agents/

Applied to files:

  • CHANGELOG.md
  • core/agents.py
  • core/models.py
📚 Learning: 2025-10-04T08:52:58.590Z
Learnt from: CR
Repo: rasulkireev/TuxSEO PR: 0
File: .cursor/rules/agent-rules.mdc:0-0
Timestamp: 2025-10-04T08:52:58.590Z
Learning: Always add AGENTS.md into AI context

Applied to files:

  • core/agents.py
📚 Learning: 2025-10-04T08:52:58.590Z
Learnt from: CR
Repo: rasulkireev/TuxSEO PR: 0
File: .cursor/rules/agent-rules.mdc:0-0
Timestamp: 2025-10-04T08:52:58.590Z
Learning: Always add AGENTS.md into context

Applied to files:

  • core/agents.py
📚 Learning: 2025-10-04T08:52:37.437Z
Learnt from: CR
Repo: rasulkireev/TuxSEO PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-10-04T08:52:37.437Z
Learning: Applies to core/tasks.py : Define background tasks using django-q2 in core/tasks.py

Applied to files:

  • core/models.py
🪛 Ruff (0.14.2)
core/agent_system_prompts.py

36-36: Unused noqa directive (non-enabled: E501)

Remove unused noqa directive

(RUF100)

core/prompts.py

21-21: Unused noqa directive (non-enabled: E501)

Remove unused noqa directive

(RUF100)


61-61: Unused noqa directive (non-enabled: E501)

Remove unused noqa directive

(RUF100)


148-148: Unused noqa directive (non-enabled: E501)

Remove unused noqa directive

(RUF100)


195-195: Unused noqa directive (non-enabled: E501)

Remove unused noqa directive

(RUF100)


227-227: Unused noqa directive (non-enabled: E501)

Remove unused noqa directive

(RUF100)

core/api/views.py

576-576: Unused noqa directive (non-enabled: E501)

Remove unused noqa directive

(RUF100)


652-652: Unused noqa directive (non-enabled: E501)

Remove unused noqa directive

(RUF100)


658-658: Unused noqa directive (non-enabled: E501)

Remove unused noqa directive

(RUF100)


681-685: Consider moving this statement to an else block

(TRY300)


698-698: Use explicit conversion flag

Replace with conversion flag

(RUF010)

core/migrations/0038_competitor_blog_post.py

8-10: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


12-18: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)

core/tasks.py

249-249: Unused noqa directive (non-enabled: E501)

Remove unused noqa directive

(RUF100)


266-266: Use explicit conversion flag

Replace with conversion flag

(RUF010)

core/agents.py

208-208: Unused noqa directive (non-enabled: E501)

Remove unused noqa directive

(RUF100)

core/models.py

750-750: Unused function argument: ctx

(ARG001)


1622-1622: Unused noqa directive (non-enabled: E501)

Remove unused noqa directive

(RUF100)

🔇 Additional comments (12)
core/utils.py (1)

207-207: LGTM! Correct API update for pydantic-ai 1.9.1.

The change from result.data to result.output correctly reflects the API changes in pydantic-ai's upgrade from 0.2.9 to 1.9.1. This aligns with the OpenAIChatModel migration mentioned in the AI summary.

pyproject.toml (1)

38-38: LGTM! Major version upgrade from 0.2.9 to 1.9.1.

This major version upgrade of pydantic-ai is intentional and necessary for the new competitor-focused content generation features. The codebase has been updated accordingly (e.g., result.output instead of result.data in core/utils.py).

core/tasks.py (1)

192-212: LGTM! Clear documentation of the cost-saving workflow.

The updated docstring accurately describes the new workflow: finding competitors via Perplexity, fetching homepage content, and populating basic details, while explicitly noting that full competitor analysis is skipped to save costs and VS blog post generation is manual.

frontend/templates/base_project.html (1)

107-117: LGTM! Consistent navigation item addition.

The new "Competitors" navigation item follows the established pattern for sidebar menu items, including proper active state styling, appropriate SVG icon, and correct URL routing to project_competitors. The placement between "Your Pages" and "Publish History" is logical.

core/api/schemas.py (1)

134-142: LGTM! Well-structured API schemas.

The new GenerateCompetitorVsTitleIn and GenerateCompetitorVsTitleOut schemas follow the established naming conventions and patterns in this file. The field types are appropriate, and the optional competitor_id in the output schema allows for flexible error handling.

core/agent_system_prompts.py (1)

65-70: LGTM: Generic RunContext improves reusability.

The signature change from RunContext[BlogPostGenerationContext] to generic RunContext with a clear docstring is an excellent improvement that allows this function to work with multiple context types (BlogPostGenerationContext, CompetitorVsPostContext, etc.) while maintaining type safety.

core/views.py (1)

824-862: LGTM!

The implementation properly isolates competitors by project and user profile, and exposes the necessary context for free-plan limits and analytics in the UI.

frontend/templates/project/project_competitors.html (2)

85-159: LGTM!

The modal form is properly structured with CSRF protection, accessible ARIA attributes, semantic HTML, and appropriate input validation (required + type="url").


201-290: LGTM!

The competitors table properly handles conditional states (blog post exists vs. not, free plan limits), includes accessible markup, and integrates cleanly with Stimulus controllers for interactivity.

core/agents.py (1)

91-151: LGTM!

The competitor VS title agent is properly configured with Perplexity Sonar for web-search capabilities, clear system prompts, and appropriate context building. The previously flagged unused import issue has been resolved.

core/models.py (2)

275-327: LGTM!

The competitor limit properties follow the established pattern for other plan-based limits and correctly count competitors and generated posts across all user projects.


1432-1434: LGTM!

The blog_post field is appropriately defined as a TextField with sensible defaults for storing generated markdown content.

<!-- Blog Post Content -->
{% if competitor.blog_post %}
<!-- Copy Action -->
<div class="flex justify-end mb-4" data-controller="copy-markdown" data-copy-markdown-content-value="{{ competitor.blog_post }}">
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Escape or JSON-encode the blog_post content for the data attribute.

Embedding competitor.blog_post directly into a data attribute can break HTML if the content contains double quotes, newlines, or other special characters. While Django auto-escapes by default, for complex text content in data attributes, use the escapejs filter or Django's json_script tag to ensure proper encoding.

Apply this diff to properly escape the content:

-    <div class="flex justify-end mb-4" data-controller="copy-markdown" data-copy-markdown-content-value="{{ competitor.blog_post }}">
+    <div class="flex justify-end mb-4" data-controller="copy-markdown" data-copy-markdown-content-value="{{ competitor.blog_post|escapejs }}">
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div class="flex justify-end mb-4" data-controller="copy-markdown" data-copy-markdown-content-value="{{ competitor.blog_post }}">
<div class="flex justify-end mb-4" data-controller="copy-markdown" data-copy-markdown-content-value="{{ competitor.blog_post|escapejs }}">

@rasulkireev rasulkireev merged commit 63f648d into main Nov 3, 2025
1 of 2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants