From cdb41aec1b1b04ae57dab6d9bdbe809566c44533 Mon Sep 17 00:00:00 2001 From: ajaysi Date: Fri, 31 Oct 2025 15:59:16 +0530 Subject: [PATCH] Added image generation to blog writer --- backend/api/blog_writer/router.py | 90 ++- backend/api/blog_writer/task_manager.py | 10 + .../facebook_writer/services/story_service.py | 35 +- backend/api/images.py | 217 +++++ backend/app.py | 2 + backend/models/blog_models.py | 2 + backend/requirements.txt | 6 +- .../content/enhanced_content_generator.py | 58 +- .../content/medium_blog_generator.py | 8 +- .../blog_writer/core/blog_writer_service.py | 10 +- .../blog_writer/outline/outline_optimizer.py | 9 +- .../blog_writer/outline/response_processor.py | 23 +- .../blog_writer/outline/section_enhancer.py | 9 +- .../blog_writer/outline/source_mapper.py | 9 +- .../seo/blog_content_seo_analyzer.py | 20 +- .../seo/blog_seo_metadata_generator.py | 302 +++++-- .../seo/blog_seo_recommendation_applier.py | 269 ++++++ .../linkedin_image_generator.py | 48 +- .../image_generation/__init__.py | 15 + .../llm_providers/image_generation/base.py | 37 + .../image_generation/gemini_provider.py | 47 ++ .../image_generation/hf_provider.py | 73 ++ .../image_generation/stability_provider.py | 79 ++ .../llm_providers/main_image_generation.py | 73 ++ .../gen_dali2_images.py | 56 -- .../gen_dali3_images.py | 53 -- .../gen_gemini_images.py | 583 ------------- .../gen_stabl_diff_img.py | 69 -- .../gen_variation_img.py | 51 -- .../main_generate_image_from_prompt.py | 162 ---- .../text_to_image_generation/save_image.py | 39 - backend/services/writing_assistant.py | 50 +- .../docs/features/blog-writer/overview.md | 233 +++++- .../docs/features/blog-writer/research.md | 544 +++++++------ .../docs/features/blog-writer/seo-analysis.md | 685 +++++++++------- .../features/blog-writer/workflow-guide.md | 287 ++++--- frontend/public/BLOG_WRITER_ASSETS_GUIDE.md | 162 ++++ frontend/public/images/.gitkeep | 27 + frontend/public/videos/.gitkeep | 9 + .../src/components/BlogWriter/BlogWriter.tsx | 717 ++++++++-------- .../BlogWriter/BlogWriterLanding.tsx | 142 ++-- .../BlogWriter/BlogWriterPhasesSection.tsx | 576 +++++++++++++ .../BlogWriter/BlogWriterUtils/HeaderBar.tsx | 41 + .../BlogWriterUtils/OutlineCtaBanner.tsx | 21 + .../BlogWriterUtils/PhaseContent.tsx | 185 +++++ .../BlogWriterUtils/TaskProgressModals.tsx | 52 ++ .../BlogWriterUtils/WriterCopilotSidebar.tsx | 140 ++++ .../useBlogWriterCopilotActions.ts | 90 +++ .../components/BlogWriter/ContinuityBadge.tsx | 16 +- .../BlogWriter/EnhancedOutlineEditor.tsx | 103 ++- .../BlogWriter/KeywordInputForm.tsx | 264 +----- .../BlogWriter/OutlineGenerator.tsx | 31 +- .../components/BlogWriter/PhaseNavigation.tsx | 89 ++ .../BlogWriter/PhaseNavigationTest.tsx | 89 ++ .../components/BlogWriter/ResearchAction.tsx | 189 +++-- .../BlogWriter/ResearchPollingHandler.tsx | 23 +- .../BlogWriter/SEO/KeywordAnalysis.tsx | 214 +++-- .../SEO/MetadataDisplay/CoreMetadataTab.tsx | 113 +-- .../SEO/MetadataDisplay/PreviewCard.tsx | 764 +++++++++++------- .../SEO/MetadataDisplay/SocialMediaTab.tsx | 87 +- .../SEO/MetadataDisplay/StructuredDataTab.tsx | 71 +- .../BlogWriter/SEO/OverallScoreCard.tsx | 264 ++++++ .../BlogWriter/SEO/ReadabilityAnalysis.tsx | 323 ++++---- .../BlogWriter/SEO/Recommendations.tsx | 141 ++-- .../BlogWriter/SEO/StructureAnalysis.tsx | 557 +++++-------- .../BlogWriter/SEOAnalysisModal.tsx | 341 ++++---- .../BlogWriter/SEOMetadataModal.tsx | 176 +++- .../components/BlogWriter/SEOMiniPanel.tsx | 15 +- .../BlogWriter/SuggestionsGenerator.tsx | 157 +++- .../BlogWriter/WYSIWYG/BlogSection.tsx | 27 +- .../WYSIWYG/BlogTextSelectionHandler.tsx | 4 + .../BlogWriter/WYSIWYG/SmartTypingAssist.tsx | 168 +++- .../BlogWriter/WYSIWYG/TextSelectionMenu.tsx | 99 ++- .../components/ImageGen/ImageGenerator.tsx | 297 +++++++ .../ImageGen/ImageGeneratorModal.tsx | 109 +++ .../components/ImageGen/useImageGeneration.ts | 73 ++ frontend/src/hooks/usePhaseNavigation.ts | 210 +++++ frontend/src/services/blogWriterApi.ts | 57 +- frontend/src/utils/debug.ts | 99 +++ frontend/src/utils/imageBus.ts | 18 + 80 files changed, 7662 insertions(+), 3951 deletions(-) create mode 100644 backend/api/images.py create mode 100644 backend/services/blog_writer/seo/blog_seo_recommendation_applier.py create mode 100644 backend/services/llm_providers/image_generation/__init__.py create mode 100644 backend/services/llm_providers/image_generation/base.py create mode 100644 backend/services/llm_providers/image_generation/gemini_provider.py create mode 100644 backend/services/llm_providers/image_generation/hf_provider.py create mode 100644 backend/services/llm_providers/image_generation/stability_provider.py create mode 100644 backend/services/llm_providers/main_image_generation.py delete mode 100644 backend/services/llm_providers/text_to_image_generation/gen_dali2_images.py delete mode 100644 backend/services/llm_providers/text_to_image_generation/gen_dali3_images.py delete mode 100644 backend/services/llm_providers/text_to_image_generation/gen_gemini_images.py delete mode 100644 backend/services/llm_providers/text_to_image_generation/gen_stabl_diff_img.py delete mode 100644 backend/services/llm_providers/text_to_image_generation/gen_variation_img.py delete mode 100644 backend/services/llm_providers/text_to_image_generation/main_generate_image_from_prompt.py delete mode 100644 backend/services/llm_providers/text_to_image_generation/save_image.py create mode 100644 frontend/public/BLOG_WRITER_ASSETS_GUIDE.md create mode 100644 frontend/public/images/.gitkeep create mode 100644 frontend/public/videos/.gitkeep create mode 100644 frontend/src/components/BlogWriter/BlogWriterPhasesSection.tsx create mode 100644 frontend/src/components/BlogWriter/BlogWriterUtils/HeaderBar.tsx create mode 100644 frontend/src/components/BlogWriter/BlogWriterUtils/OutlineCtaBanner.tsx create mode 100644 frontend/src/components/BlogWriter/BlogWriterUtils/PhaseContent.tsx create mode 100644 frontend/src/components/BlogWriter/BlogWriterUtils/TaskProgressModals.tsx create mode 100644 frontend/src/components/BlogWriter/BlogWriterUtils/WriterCopilotSidebar.tsx create mode 100644 frontend/src/components/BlogWriter/BlogWriterUtils/useBlogWriterCopilotActions.ts create mode 100644 frontend/src/components/BlogWriter/PhaseNavigation.tsx create mode 100644 frontend/src/components/BlogWriter/PhaseNavigationTest.tsx create mode 100644 frontend/src/components/BlogWriter/SEO/OverallScoreCard.tsx create mode 100644 frontend/src/components/ImageGen/ImageGenerator.tsx create mode 100644 frontend/src/components/ImageGen/ImageGeneratorModal.tsx create mode 100644 frontend/src/components/ImageGen/useImageGeneration.ts create mode 100644 frontend/src/hooks/usePhaseNavigation.ts create mode 100644 frontend/src/utils/debug.ts create mode 100644 frontend/src/utils/imageBus.ts diff --git a/backend/api/blog_writer/router.py b/backend/api/blog_writer/router.py index 6dd5fa7c..b6093538 100644 --- a/backend/api/blog_writer/router.py +++ b/backend/api/blog_writer/router.py @@ -7,6 +7,7 @@ content creation, SEO analysis, and publishing. from fastapi import APIRouter, HTTPException from typing import Any, Dict, List +from pydantic import BaseModel, Field from loguru import logger from models.blog_models import ( @@ -29,6 +30,7 @@ from models.blog_models import ( HallucinationCheckResponse, ) from services.blog_writer.blog_service import BlogWriterService +from services.blog_writer.seo.blog_seo_recommendation_applier import BlogSEORecommendationApplier from .task_manager import task_manager from .cache_manager import cache_manager from models.blog_models import MediumBlogGenerateRequest @@ -37,6 +39,44 @@ from models.blog_models import MediumBlogGenerateRequest router = APIRouter(prefix="/api/blog", tags=["AI Blog Writer"]) service = BlogWriterService() +recommendation_applier = BlogSEORecommendationApplier() +# --------------------------- +# SEO Recommendation Endpoints +# --------------------------- + + +class RecommendationItem(BaseModel): + category: str = Field(..., description="Recommendation category, e.g. Structure") + priority: str = Field(..., description="Priority level: High | Medium | Low") + recommendation: str = Field(..., description="Action to perform") + impact: str = Field(..., description="Expected impact or rationale") + + +class SEOApplyRecommendationsRequest(BaseModel): + title: str = Field(..., description="Current blog title") + sections: List[Dict[str, Any]] = Field(..., description="Array of sections with id, heading, content") + outline: List[Dict[str, Any]] = Field(default_factory=list, description="Outline structure for context") + research: Dict[str, Any] = Field(default_factory=dict, description="Research data used for the blog") + recommendations: List[RecommendationItem] = Field(..., description="Actionable recommendations to apply") + persona: Dict[str, Any] = Field(default_factory=dict, description="Persona settings if available") + tone: str | None = Field(default=None, description="Desired tone override") + audience: str | None = Field(default=None, description="Target audience override") + + +@router.post("/seo/apply-recommendations") +async def apply_seo_recommendations(request: SEOApplyRecommendationsRequest) -> Dict[str, Any]: + """Apply actionable SEO recommendations and return updated content.""" + try: + result = await recommendation_applier.apply_recommendations(request.dict()) + if not result.get("success"): + raise HTTPException(status_code=500, detail=result.get("error", "Failed to apply recommendations")) + return result + except HTTPException: + raise + except Exception as e: + logger.error(f"Failed to apply SEO recommendations: {e}") + raise HTTPException(status_code=500, detail=str(e)) + @router.get("/health") @@ -92,7 +132,7 @@ async def start_outline_generation(request: BlogOutlineRequest) -> Dict[str, Any async def get_outline_status(task_id: str) -> Dict[str, Any]: """Get the status of an outline generation operation.""" try: - status = task_manager.get_task_status(task_id) + status = await task_manager.get_task_status(task_id) if status is None: raise HTTPException(status_code=404, detail="Task not found") @@ -164,6 +204,50 @@ async def generate_section(request: BlogSectionRequest) -> BlogSectionResponse: raise HTTPException(status_code=500, detail=str(e)) +@router.post("/content/start") +async def start_content_generation(request: Dict[str, Any]) -> Dict[str, Any]: + """Start full content generation and return a task id for polling. + + Accepts a payload compatible with MediumBlogGenerateRequest to minimize duplication. + """ + try: + # Map dict to MediumBlogGenerateRequest for reuse + from models.blog_models import MediumBlogGenerateRequest, MediumSectionOutline, PersonaInfo + sections = [MediumSectionOutline(**s) for s in request.get("sections", [])] + persona = None + if request.get("persona"): + persona = PersonaInfo(**request.get("persona")) + req = MediumBlogGenerateRequest( + title=request.get("title", "Untitled Blog"), + sections=sections, + persona=persona, + tone=request.get("tone"), + audience=request.get("audience"), + globalTargetWords=request.get("globalTargetWords", 1000), + researchKeywords=request.get("researchKeywords") or request.get("keywords"), + ) + task_id = task_manager.start_content_generation_task(req) + return {"task_id": task_id, "status": "started"} + except Exception as e: + logger.error(f"Failed to start content generation: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + +@router.get("/content/status/{task_id}") +async def content_generation_status(task_id: str) -> Dict[str, Any]: + """Poll status for content generation task.""" + try: + status = await task_manager.get_task_status(task_id) + if status is None: + raise HTTPException(status_code=404, detail="Task not found") + return status + except HTTPException: + raise + except Exception as e: + logger.error(f"Failed to get content generation status for {task_id}: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + @router.get("/section/{section_id}/continuity") async def get_section_continuity(section_id: str) -> Dict[str, Any]: """Fetch last computed continuity metrics for a section (if available).""" @@ -342,7 +426,7 @@ async def start_medium_generation(request: MediumBlogGenerateRequest): async def medium_generation_status(task_id: str): """Poll status for medium blog generation task.""" try: - status = task_manager.get_task_status(task_id) + status = await task_manager.get_task_status(task_id) if status is None: raise HTTPException(status_code=404, detail="Task not found") return status @@ -366,7 +450,7 @@ async def start_blog_rewrite(request: Dict[str, Any]) -> Dict[str, Any]: async def rewrite_status(task_id: str): """Poll status for blog rewrite task.""" try: - status = service.task_manager.get_task_status(task_id) + status = await service.task_manager.get_task_status(task_id) if status is None: raise HTTPException(status_code=404, detail="Task not found") return status diff --git a/backend/api/blog_writer/task_manager.py b/backend/api/blog_writer/task_manager.py index 7906a688..9c393367 100644 --- a/backend/api/blog_writer/task_manager.py +++ b/backend/api/blog_writer/task_manager.py @@ -133,6 +133,16 @@ class TaskManager: task_id = self.create_task("medium_generation") asyncio.create_task(self._run_medium_generation_task(task_id, request)) return task_id + + def start_content_generation_task(self, request: MediumBlogGenerateRequest) -> str: + """Start content generation (full blog via sections) with provider parity. + + Internally reuses medium generator pipeline for now but tracked under + distinct task_type 'content_generation' and same polling contract. + """ + task_id = self.create_task("content_generation") + asyncio.create_task(self._run_medium_generation_task(task_id, request)) + return task_id async def _run_research_task(self, task_id: str, request: BlogResearchRequest): """Background task to run research and update status with progress messages.""" diff --git a/backend/api/facebook_writer/services/story_service.py b/backend/api/facebook_writer/services/story_service.py index 672ef2d6..2d57a69f 100644 --- a/backend/api/facebook_writer/services/story_service.py +++ b/backend/api/facebook_writer/services/story_service.py @@ -4,11 +4,11 @@ from typing import Dict, Any, List from ..models.story_models import FacebookStoryRequest, FacebookStoryResponse from .base_service import FacebookWriterBaseService try: - from ...services.llm_providers.text_to_image_generation.gen_gemini_images import ( - generate_gemini_images_base64, - ) + from ...services.llm_providers.main_image_generation import generate_image + from base64 import b64encode except Exception: - generate_gemini_images_base64 = None # type: ignore + generate_image = None # type: ignore + b64encode = None # type: ignore class FacebookStoryService(FacebookWriterBaseService): @@ -50,22 +50,29 @@ class FacebookStoryService(FacebookWriterBaseService): # Generate visual suggestions and engagement tips visual_suggestions = self._generate_visual_suggestions(actual_story_type, request.visual_options) engagement_tips = self._generate_engagement_tips("story") - # Optional: generate one story image (9:16) using Gemini + # Optional: generate one story image (9:16) using unified image generation images_base64: List[str] = [] try: - if generate_gemini_images_base64 is not None: + if generate_image is not None and b64encode is not None: img_prompt = request.visual_options.background_image_prompt or ( f"Facebook story background for {request.business_type}. " f"Style: {actual_tone}. Type: {actual_story_type}. Vertical mobile 9:16, high contrast, legible overlay space." ) - images_base64 = generate_gemini_images_base64( - img_prompt, - enhance_prompt=False, - aspect_ratio="9:16", - max_retries=2, - initial_retry_delay=1.0, - ) or [] - except Exception: + # Generate image using unified system (9:16 aspect ratio = 1080x1920) + result = generate_image( + prompt=img_prompt, + options={ + "provider": "gemini", # Facebook stories use Gemini + "width": 1080, + "height": 1920, + } + ) + if result and result.image_bytes: + # Convert bytes to base64 + image_b64 = b64encode(result.image_bytes).decode('utf-8') + images_base64 = [image_b64] + except Exception as e: + # Log error but continue without images images_base64 = [] return FacebookStoryResponse( diff --git a/backend/api/images.py b/backend/api/images.py new file mode 100644 index 00000000..36c9be01 --- /dev/null +++ b/backend/api/images.py @@ -0,0 +1,217 @@ +from __future__ import annotations + +import base64 +import os +from typing import Optional, Dict, Any + +from fastapi import APIRouter, HTTPException +from pydantic import BaseModel, Field + +from services.llm_providers.main_image_generation import generate_image +from services.llm_providers.main_text_generation import llm_text_gen +from utils.logger_utils import get_service_logger + + +router = APIRouter(prefix="/api/images", tags=["images"]) +logger = get_service_logger("api.images") + + +class ImageGenerateRequest(BaseModel): + prompt: str + negative_prompt: Optional[str] = None + provider: Optional[str] = Field(None, pattern="^(gemini|huggingface|stability)$") + model: Optional[str] = None + width: Optional[int] = Field(default=1024, ge=64, le=2048) + height: Optional[int] = Field(default=1024, ge=64, le=2048) + guidance_scale: Optional[float] = None + steps: Optional[int] = None + seed: Optional[int] = None + + +class ImageGenerateResponse(BaseModel): + success: bool = True + image_base64: str + width: int + height: int + provider: str + model: Optional[str] = None + seed: Optional[int] = None + + +@router.post("/generate", response_model=ImageGenerateResponse) +def generate(req: ImageGenerateRequest) -> ImageGenerateResponse: + try: + last_error: Optional[Exception] = None + for attempt in range(2): # simple single retry + try: + result = generate_image( + prompt=req.prompt, + options={ + "negative_prompt": req.negative_prompt, + "provider": req.provider, + "model": req.model, + "width": req.width, + "height": req.height, + "guidance_scale": req.guidance_scale, + "steps": req.steps, + "seed": req.seed, + }, + ) + image_b64 = base64.b64encode(result.image_bytes).decode("utf-8") + return ImageGenerateResponse( + image_base64=image_b64, + width=result.width, + height=result.height, + provider=result.provider, + model=result.model, + seed=result.seed, + ) + except Exception as inner: + last_error = inner + logger.error(f"Image generation attempt {attempt+1} failed: {inner}") + # On first failure, try provider auto-remap by clearing provider to let facade decide + if attempt == 0 and req.provider: + req.provider = None + continue + break + raise last_error or RuntimeError("Unknown image generation error") + except Exception as e: + logger.error(f"Image generation failed: {e}") + # Provide a clean, actionable message to the client + raise HTTPException( + status_code=500, + detail="Image generation service is temporarily unavailable or the connection was reset. Please try again." + ) + + +class PromptSuggestion(BaseModel): + prompt: str + negative_prompt: Optional[str] = None + width: Optional[int] = None + height: Optional[int] = None + overlay_text: Optional[str] = None + + +class ImagePromptSuggestRequest(BaseModel): + provider: Optional[str] = Field(None, pattern="^(gemini|huggingface|stability)$") + title: Optional[str] = None + section: Optional[Dict[str, Any]] = None + research: Optional[Dict[str, Any]] = None + persona: Optional[Dict[str, Any]] = None + include_overlay: Optional[bool] = True + + +class ImagePromptSuggestResponse(BaseModel): + suggestions: list[PromptSuggestion] + + +@router.post("/suggest-prompts", response_model=ImagePromptSuggestResponse) +def suggest_prompts(req: ImagePromptSuggestRequest) -> ImagePromptSuggestResponse: + try: + provider = (req.provider or ("gemini" if (os.getenv("GPT_PROVIDER") or "").lower().startswith("gemini") else "huggingface")).lower() + section = req.section or {} + title = (req.title or section.get("heading") or "").strip() + subheads = section.get("subheadings", []) or [] + key_points = section.get("key_points", []) or [] + keywords = section.get("keywords", []) or [] + if not keywords and req.research: + keywords = ( + req.research.get("keywords", {}).get("primary_keywords") + or req.research.get("keywords", {}).get("primary") + or [] + ) + + persona = req.persona or {} + audience = persona.get("audience", "content creators and digital marketers") + industry = persona.get("industry", req.research.get("domain") if req.research else "your industry") + tone = persona.get("tone", "professional, trustworthy") + + schema = { + "type": "object", + "properties": { + "suggestions": { + "type": "array", + "items": { + "type": "object", + "properties": { + "prompt": {"type": "string"}, + "negative_prompt": {"type": "string"}, + "width": {"type": "number"}, + "height": {"type": "number"}, + "overlay_text": {"type": "string"}, + }, + "required": ["prompt"] + }, + "minItems": 3, + "maxItems": 5 + } + }, + "required": ["suggestions"] + } + + system = ( + "You are an expert image prompt engineer for text-to-image models. " + "Given blog section context, craft 3-5 hyper-personalized prompts optimized for the specified provider. " + "Return STRICT JSON matching the provided schema, no extra text." + ) + + provider_guidance = { + "huggingface": "Photorealistic Flux 1 Krea Dev; include camera/lighting cues (e.g., 50mm, f/2.8, rim light).", + "gemini": "Editorial, brand-safe, crisp edges, balanced lighting; avoid artifacts.", + "stability": "SDXL coherent details, sharp focus, cinematic contrast; readable text if present." + }.get(provider, "") + + best_practices = ( + "Best Practices: one clear focal subject; clean, uncluttered background; rule-of-thirds or center-weighted composition; " + "text-safe margins if overlay text is included; neutral lighting if unsure; realistic skin tones; avoid busy patterns; " + "no brand logos or watermarks; no copyrighted characters; avoid low-res, blur, noise, banding, oversaturation, over-sharpening; " + "ensure hands and text are coherent if present; prefer 1024px+ on shortest side for quality." + ) + + # Harvest a few concise facts from research if available + facts: list[str] = [] + try: + if req.research: + # try common shapes used in research service + top_stats = req.research.get("key_facts") or req.research.get("highlights") or [] + if isinstance(top_stats, list): + facts = [str(x) for x in top_stats[:3]] + elif isinstance(top_stats, dict): + facts = [f"{k}: {v}" for k, v in list(top_stats.items())[:3]] + except Exception: + facts = [] + + facts_line = ", ".join(facts) if facts else "" + + overlay_hint = "Include an on-image short title or fact if it improves communication; ensure clean, high-contrast safe area for text." if (req.include_overlay is None or req.include_overlay) else "Do not include on-image text." + + prompt = f""" + Provider: {provider} + Title: {title} + Subheadings: {', '.join(subheads[:5])} + Key Points: {', '.join(key_points[:5])} + Keywords: {', '.join([str(k) for k in keywords[:8]])} + Research Facts: {facts_line} + Audience: {audience} + Industry: {industry} + Tone: {tone} + + Craft prompts that visually reflect this exact section (not generic blog topic). {provider_guidance} + {best_practices} + {overlay_hint} + Include a suitable negative_prompt where helpful. Suggest width/height when relevant (e.g., 1024x1024 or 1920x1080). + If including on-image text, return it in overlay_text (short: <= 8 words). + """ + + raw = llm_text_gen(prompt=prompt, system_prompt=system, json_struct=schema) + data = raw if isinstance(raw, dict) else {} + suggestions = data.get("suggestions") or [] + # basic fallback if provider returns string + if not suggestions and isinstance(raw, str): + suggestions = [{"prompt": raw}] + + return ImagePromptSuggestResponse(suggestions=[PromptSuggestion(**s) for s in suggestions]) + except Exception as e: + logger.error(f"Prompt suggestion failed: {e}") + raise HTTPException(status_code=500, detail=str(e)) + diff --git a/backend/app.py b/backend/app.py index 902970d3..a3294a9a 100644 --- a/backend/app.py +++ b/backend/app.py @@ -42,6 +42,7 @@ from routers.linkedin import router as linkedin_router # Import LinkedIn image generation router from api.linkedin_image_generation import router as linkedin_image_router from api.brainstorm import router as brainstorm_router +from api.images import router as images_router # Import hallucination detector router from api.hallucination_detector import router as hallucination_detector_router @@ -279,6 +280,7 @@ async def batch_analyze_urls_endpoint(urls: list[str]): # Include platform analytics router from routers.platform_analytics import router as platform_analytics_router app.include_router(platform_analytics_router) +app.include_router(images_router) # Setup frontend serving using modular utilities frontend_serving.setup_frontend_serving() diff --git a/backend/models/blog_models.py b/backend/models/blog_models.py index 76cc92b2..51a01113 100644 --- a/backend/models/blog_models.py +++ b/backend/models/blog_models.py @@ -186,6 +186,8 @@ class BlogSEOMetadataRequest(BaseModel): title: Optional[str] = None keywords: List[str] = [] research_data: Optional[Dict[str, Any]] = None + outline: Optional[List[Dict[str, Any]]] = None # Add outline structure + seo_analysis: Optional[Dict[str, Any]] = None # Add SEO analysis results class BlogSEOMetadataResponse(BaseModel): diff --git a/backend/requirements.txt b/backend/requirements.txt index a1d52f04..bc3a9844 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -21,10 +21,9 @@ httpx>=0.27.2,<0.28.0 # AI/ML dependencies openai>=1.3.0 -anthropic>=0.7.0 -mistralai>=0.0.12 google-genai>=1.0.0 -google-ai-generativelanguage>=0.6.18,<0.7.0 + + google-api-python-client>=2.100.0 google-auth>=2.23.0 google-auth-oauthlib>=1.0.0 @@ -53,6 +52,7 @@ nltk>=3.8.0 # Image and audio processing for Stability AI Pillow>=10.0.0 +huggingface_hub>=0.24.0 scikit-learn>=1.3.0 # Testing dependencies diff --git a/backend/services/blog_writer/content/enhanced_content_generator.py b/backend/services/blog_writer/content/enhanced_content_generator.py index a331cec9..4d5aa00d 100644 --- a/backend/services/blog_writer/content/enhanced_content_generator.py +++ b/backend/services/blog_writer/content/enhanced_content_generator.py @@ -1,12 +1,14 @@ """ -EnhancedContentGenerator - thin orchestrator combining URL selection and Gemini provider. +EnhancedContentGenerator - thin orchestrator for section generation. -Provides Draft vs Polished modes and optional URL Context usage. +Provider parity: +- Uses main_text_generation.llm_text_gen to respect GPT_PROVIDER (Gemini/HF) +- No direct provider coupling here; Google grounding remains in research only """ from typing import Any, Dict -from services.llm_providers.gemini_grounded_provider import GeminiGroundedProvider +from services.llm_providers.main_text_generation import llm_text_gen from .source_url_manager import SourceURLManager from .context_memory import ContextMemory from .transition_generator import TransitionGenerator @@ -15,24 +17,37 @@ from .flow_analyzer import FlowAnalyzer class EnhancedContentGenerator: def __init__(self): - self.provider = GeminiGroundedProvider() self.url_manager = SourceURLManager() self.memory = ContextMemory(max_entries=12) self.transitioner = TransitionGenerator() self.flow = FlowAnalyzer() async def generate_section(self, section: Any, research: Any, mode: str = "polished") -> Dict[str, Any]: - urls = self.url_manager.pick_relevant_urls(section, research) prev_summary = self.memory.build_previous_sections_summary(limit=2) - prompt = self._build_prompt(section, research, prev_summary) - result = await self.provider.generate_grounded_content( - prompt=prompt, - content_type="linkedin_article", - temperature=0.6 if mode == "polished" else 0.8, - max_tokens=2048, - urls=urls, - mode=mode, - ) + urls = self.url_manager.pick_relevant_urls(section, research) + prompt = self._build_prompt(section, research, prev_summary, urls) + # Provider-agnostic text generation (respect GPT_PROVIDER & circuit-breaker) + content_text: str = "" + try: + ai_resp = llm_text_gen( + prompt=prompt, + json_struct=None, + system_prompt=None, + ) + if isinstance(ai_resp, dict) and ai_resp.get("text"): + content_text = ai_resp.get("text", "") + elif isinstance(ai_resp, str): + content_text = ai_resp + else: + # Fallback best-effort extraction + content_text = str(ai_resp or "") + except Exception as e: + content_text = "" + + result = { + "content": content_text, + "sources": [{"title": u.get("title", ""), "url": u.get("url", "")} for u in urls] if urls else [], + } # Generate transition and compute intelligent flow metrics previous_text = prev_summary current_text = result.get("content", "") @@ -56,19 +71,22 @@ class EnhancedContentGenerator: pass return result - def _build_prompt(self, section: Any, research: Any, prev_summary: str) -> str: + def _build_prompt(self, section: Any, research: Any, prev_summary: str, urls: list) -> str: heading = getattr(section, 'heading', 'Section') key_points = getattr(section, 'key_points', []) keywords = getattr(section, 'keywords', []) target_words = getattr(section, 'target_words', 300) + url_block = "\n".join([f"- {u.get('title','')} ({u.get('url','')})" for u in urls]) if urls else "(no specific URLs provided)" return ( f"You are writing the blog section '{heading}'.\n\n" - f"Context summary: {prev_summary}\n" - f"Key points: {', '.join(key_points)}\n" - f"Keywords: {', '.join(keywords)}\n" - f"Target word count: {target_words}.\n" - "Use only factual info from provided sources; add short transition, then body." + f"Context summary (previous sections): {prev_summary}\n\n" + f"Authoring requirements:\n" + f"- Target word count: ~{target_words}\n" + f"- Use the following key points: {', '.join(key_points)}\n" + f"- Include these keywords naturally: {', '.join(keywords)}\n" + f"- Cite insights from these sources when relevant (do not output raw URLs):\n{url_block}\n\n" + "Write engaging, well-structured markdown with clear paragraphs (2-4 sentences each) separated by double line breaks." ) diff --git a/backend/services/blog_writer/content/medium_blog_generator.py b/backend/services/blog_writer/content/medium_blog_generator.py index 08528e63..df243c4b 100644 --- a/backend/services/blog_writer/content/medium_blog_generator.py +++ b/backend/services/blog_writer/content/medium_blog_generator.py @@ -15,7 +15,7 @@ from models.blog_models import ( MediumGeneratedSection, ResearchSource, ) -from services.llm_providers.gemini_provider import gemini_structured_json_response +from services.llm_providers.main_text_generation import llm_text_gen from services.cache.persistent_content_cache import persistent_content_cache @@ -176,11 +176,9 @@ class MediumBlogGenerator: f"Sections to write:\n{json.dumps(payload, ensure_ascii=False, indent=2)}" ) - ai_resp = gemini_structured_json_response( + ai_resp = llm_text_gen( prompt=prompt, - schema=schema, - temperature=0.2, - max_tokens=8192, + json_struct=schema, system_prompt=system, ) diff --git a/backend/services/blog_writer/core/blog_writer_service.py b/backend/services/blog_writer/core/blog_writer_service.py index ac8ce29d..e5aef900 100644 --- a/backend/services/blog_writer/core/blog_writer_service.py +++ b/backend/services/blog_writer/core/blog_writer_service.py @@ -275,11 +275,17 @@ class BlogWriterService: # Initialize metadata generator metadata_generator = BlogSEOMetadataGenerator() - # Generate comprehensive metadata + # Extract outline and seo_analysis from request + outline = request.outline if hasattr(request, 'outline') else None + seo_analysis = request.seo_analysis if hasattr(request, 'seo_analysis') else None + + # Generate comprehensive metadata with full context metadata_results = await metadata_generator.generate_comprehensive_metadata( blog_content=request.content, blog_title=request.title or "Untitled Blog Post", - research_data=request.research_data or {} + research_data=request.research_data or {}, + outline=outline, + seo_analysis=seo_analysis ) # Convert to BlogSEOMetadataResponse format diff --git a/backend/services/blog_writer/outline/outline_optimizer.py b/backend/services/blog_writer/outline/outline_optimizer.py index 45f6199f..e5c4c0fb 100644 --- a/backend/services/blog_writer/outline/outline_optimizer.py +++ b/backend/services/blog_writer/outline/outline_optimizer.py @@ -40,7 +40,7 @@ Return JSON format: }}""" try: - from services.llm_providers.gemini_provider import gemini_structured_json_response + from services.llm_providers.main_text_generation import llm_text_gen optimization_schema = { "type": "object", @@ -64,11 +64,10 @@ Return JSON format: "propertyOrdering": ["outline"] } - optimized_data = gemini_structured_json_response( + optimized_data = llm_text_gen( prompt=optimization_prompt, - schema=optimization_schema, - temperature=0.3, - max_tokens=6000 # Match main outline generator + json_struct=optimization_schema, + system_prompt=None ) # Handle the new schema format with "outline" wrapper diff --git a/backend/services/blog_writer/outline/response_processor.py b/backend/services/blog_writer/outline/response_processor.py index d7f556bd..c8d8479c 100644 --- a/backend/services/blog_writer/outline/response_processor.py +++ b/backend/services/blog_writer/outline/response_processor.py @@ -20,7 +20,7 @@ class ResponseProcessor: async def generate_with_retry(self, prompt: str, schema: Dict[str, Any], task_id: str = None) -> Dict[str, Any]: """Generate outline with retry logic for API failures.""" - from services.llm_providers.gemini_provider import gemini_structured_json_response + from services.llm_providers.main_text_generation import llm_text_gen from api.blog_writer.task_manager import task_manager max_retries = 2 # Conservative retry for expensive API calls @@ -29,17 +29,16 @@ class ResponseProcessor: for attempt in range(max_retries + 1): try: if task_id: - await task_manager.update_progress(task_id, f"πŸ€– Calling Gemini API for outline generation (attempt {attempt + 1}/{max_retries + 1})...") + await task_manager.update_progress(task_id, f"πŸ€– Calling AI API for outline generation (attempt {attempt + 1}/{max_retries + 1})...") - outline_data = gemini_structured_json_response( + outline_data = llm_text_gen( prompt=prompt, - schema=schema, - temperature=0.3, - max_tokens=6000 # Increased further to avoid truncation + json_struct=schema, + system_prompt=None ) # Log response for debugging - logger.info(f"Gemini response received: {type(outline_data)}") + logger.info(f"AI response received: {type(outline_data)}") # Check for errors in the response if isinstance(outline_data, dict) and 'error' in outline_data: @@ -47,17 +46,17 @@ class ResponseProcessor: if "503" in error_msg and "overloaded" in error_msg and attempt < max_retries: if task_id: await task_manager.update_progress(task_id, f"⚠️ AI service overloaded, retrying in {retry_delay} seconds...") - logger.warning(f"Gemini API overloaded, retrying in {retry_delay} seconds (attempt {attempt + 1}/{max_retries + 1})") + logger.warning(f"AI API overloaded, retrying in {retry_delay} seconds (attempt {attempt + 1}/{max_retries + 1})") await asyncio.sleep(retry_delay) continue elif "No valid structured response content found" in error_msg and attempt < max_retries: if task_id: await task_manager.update_progress(task_id, f"⚠️ Invalid response format, retrying in {retry_delay} seconds...") - logger.warning(f"Gemini response parsing failed, retrying in {retry_delay} seconds (attempt {attempt + 1}/{max_retries + 1})") + logger.warning(f"AI response parsing failed, retrying in {retry_delay} seconds (attempt {attempt + 1}/{max_retries + 1})") await asyncio.sleep(retry_delay) continue else: - logger.error(f"Gemini structured response error: {outline_data['error']}") + logger.error(f"AI structured response error: {outline_data['error']}") raise ValueError(f"AI outline generation failed: {outline_data['error']}") # Validate required fields @@ -69,7 +68,7 @@ class ResponseProcessor: await asyncio.sleep(retry_delay) continue else: - raise ValueError("Invalid outline structure in Gemini response") + raise ValueError("Invalid outline structure in AI response") # If we get here, the response is valid return outline_data @@ -79,7 +78,7 @@ class ResponseProcessor: if ("503" in error_str or "overloaded" in error_str) and attempt < max_retries: if task_id: await task_manager.update_progress(task_id, f"⚠️ AI service error, retrying in {retry_delay} seconds...") - logger.warning(f"Gemini API error, retrying in {retry_delay} seconds (attempt {attempt + 1}/{max_retries + 1}): {error_str}") + logger.warning(f"AI API error, retrying in {retry_delay} seconds (attempt {attempt + 1}/{max_retries + 1}): {error_str}") await asyncio.sleep(retry_delay) continue else: diff --git a/backend/services/blog_writer/outline/section_enhancer.py b/backend/services/blog_writer/outline/section_enhancer.py index 06d8dba9..936576bc 100644 --- a/backend/services/blog_writer/outline/section_enhancer.py +++ b/backend/services/blog_writer/outline/section_enhancer.py @@ -44,7 +44,7 @@ class SectionEnhancer: """ try: - from services.llm_providers.gemini_provider import gemini_structured_json_response + from services.llm_providers.main_text_generation import llm_text_gen enhancement_schema = { "type": "object", @@ -58,11 +58,10 @@ class SectionEnhancer: "required": ["heading", "subheadings", "key_points", "target_words", "keywords"] } - enhanced_data = gemini_structured_json_response( + enhanced_data = llm_text_gen( prompt=enhancement_prompt, - schema=enhancement_schema, - temperature=0.4, - max_tokens=1000 + json_struct=enhancement_schema, + system_prompt=None ) if isinstance(enhanced_data, dict) and 'error' not in enhanced_data: diff --git a/backend/services/blog_writer/outline/source_mapper.py b/backend/services/blog_writer/outline/source_mapper.py index aaa19a5c..d630b22b 100644 --- a/backend/services/blog_writer/outline/source_mapper.py +++ b/backend/services/blog_writer/outline/source_mapper.py @@ -559,14 +559,11 @@ Analyze the mapping and provide your recommendations. AI validation response """ try: - from services.llm_providers.gemini_provider import gemini_text_response + from services.llm_providers.main_text_generation import llm_text_gen - response = gemini_text_response( + response = llm_text_gen( prompt=prompt, - temperature=0.3, - top_p=0.9, - n=1, - max_tokens=2000, + json_struct=None, system_prompt=None ) diff --git a/backend/services/blog_writer/seo/blog_content_seo_analyzer.py b/backend/services/blog_writer/seo/blog_content_seo_analyzer.py index 9826bd80..02611759 100644 --- a/backend/services/blog_writer/seo/blog_content_seo_analyzer.py +++ b/backend/services/blog_writer/seo/blog_content_seo_analyzer.py @@ -10,13 +10,13 @@ import re import textstat from datetime import datetime from typing import Dict, Any, List, Optional -from loguru import logger +from utils.logger_utils import get_service_logger from services.seo_analyzer import ( ContentAnalyzer, KeywordAnalyzer, URLStructureAnalyzer, AIInsightGenerator ) -from services.llm_providers.gemini_provider import gemini_structured_json_response +from services.llm_providers.main_text_generation import llm_text_gen class BlogContentSEOAnalyzer: @@ -24,11 +24,13 @@ class BlogContentSEOAnalyzer: def __init__(self): """Initialize the blog content SEO analyzer""" + # Service-specific logger (no global reconfiguration) + global logger + logger = get_service_logger("blog_content_seo_analyzer") self.content_analyzer = ContentAnalyzer() self.keyword_analyzer = KeywordAnalyzer() self.url_analyzer = URLStructureAnalyzer() self.ai_insights = AIInsightGenerator() - self.gemini_provider = gemini_structured_json_response logger.info("BlogContentSEOAnalyzer initialized") @@ -598,7 +600,7 @@ class BlogContentSEOAnalyzer: return recommendations async def _run_ai_analysis(self, blog_content: str, keywords_data: Dict[str, Any], non_ai_results: Dict[str, Any]) -> Dict[str, Any]: - """Run single AI analysis for structured insights""" + """Run single AI analysis for structured insights (provider-agnostic)""" try: # Prepare context for AI analysis context = { @@ -610,7 +612,6 @@ class BlogContentSEOAnalyzer: # Create AI prompt for structured analysis prompt = self._create_ai_analysis_prompt(context) - # Get structured response from Gemini schema = { "type": "object", "properties": { @@ -653,18 +654,17 @@ class BlogContentSEOAnalyzer: } } - ai_response = self.gemini_provider( + # Provider-agnostic structured response respecting GPT_PROVIDER + ai_response = llm_text_gen( prompt=prompt, - schema=schema, - temperature=0.2, - max_tokens=8192 + json_struct=schema, + system_prompt=None ) return ai_response except Exception as e: logger.error(f"AI analysis failed: {e}") - # Fail fast - don't return mock data raise e def _create_ai_analysis_prompt(self, context: Dict[str, Any]) -> str: diff --git a/backend/services/blog_writer/seo/blog_seo_metadata_generator.py b/backend/services/blog_writer/seo/blog_seo_metadata_generator.py index cbbc2a06..0a7ac744 100644 --- a/backend/services/blog_writer/seo/blog_seo_metadata_generator.py +++ b/backend/services/blog_writer/seo/blog_seo_metadata_generator.py @@ -12,7 +12,7 @@ from datetime import datetime from typing import Dict, Any, List, Optional from loguru import logger -from services.llm_providers.gemini_provider import gemini_structured_json_response +from services.llm_providers.main_text_generation import llm_text_gen class BlogSEOMetadataGenerator: @@ -20,14 +20,15 @@ class BlogSEOMetadataGenerator: def __init__(self): """Initialize the metadata generator""" - self.gemini_provider = gemini_structured_json_response logger.info("BlogSEOMetadataGenerator initialized") async def generate_comprehensive_metadata( self, blog_content: str, blog_title: str, - research_data: Dict[str, Any] + research_data: Dict[str, Any], + outline: Optional[List[Dict[str, Any]]] = None, + seo_analysis: Optional[Dict[str, Any]] = None ) -> Dict[str, Any]: """ Generate comprehensive SEO metadata using maximum 2 AI calls @@ -36,6 +37,8 @@ class BlogSEOMetadataGenerator: blog_content: The blog content to analyze blog_title: The blog title research_data: Research data containing keywords and insights + outline: Outline structure with sections and headings + seo_analysis: SEO analysis results from previous phase Returns: Comprehensive metadata including all SEO elements @@ -49,11 +52,15 @@ class BlogSEOMetadataGenerator: # Call 1: Generate core SEO metadata (parallel with Call 2) logger.info("Generating core SEO metadata") - core_metadata_task = self._generate_core_metadata(blog_content, blog_title, keywords_data) + core_metadata_task = self._generate_core_metadata( + blog_content, blog_title, keywords_data, outline, seo_analysis + ) # Call 2: Generate social media and structured data (parallel with Call 1) logger.info("Generating social media and structured data") - social_metadata_task = self._generate_social_metadata(blog_content, blog_title, keywords_data) + social_metadata_task = self._generate_social_metadata( + blog_content, blog_title, keywords_data, outline, seo_analysis + ) # Wait for both calls to complete core_metadata, social_metadata = await asyncio.gather( @@ -105,12 +112,16 @@ class BlogSEOMetadataGenerator: self, blog_content: str, blog_title: str, - keywords_data: Dict[str, Any] + keywords_data: Dict[str, Any], + outline: Optional[List[Dict[str, Any]]] = None, + seo_analysis: Optional[Dict[str, Any]] = None ) -> Dict[str, Any]: """Generate core SEO metadata (Call 1)""" try: # Create comprehensive prompt for core metadata - prompt = self._create_core_metadata_prompt(blog_content, blog_title, keywords_data) + prompt = self._create_core_metadata_prompt( + blog_content, blog_title, keywords_data, outline, seo_analysis + ) # Define simplified structured schema for core metadata schema = { @@ -155,17 +166,26 @@ class BlogSEOMetadataGenerator: "required": ["seo_title", "meta_description", "url_slug", "blog_tags", "blog_categories", "social_hashtags", "reading_time", "focus_keyword"] } - # Get structured response from Gemini - ai_response = self.gemini_provider( - prompt, - schema, - temperature=0.3, - max_tokens=2048 + # Get structured response using provider-agnostic llm_text_gen + ai_response_raw = llm_text_gen( + prompt=prompt, + json_struct=schema, + system_prompt=None ) + # Handle response: llm_text_gen may return dict (from structured JSON) or str (needs parsing) + ai_response = ai_response_raw + if isinstance(ai_response_raw, str): + try: + import json + ai_response = json.loads(ai_response_raw) + except json.JSONDecodeError: + logger.error(f"Failed to parse JSON response: {ai_response_raw[:200]}...") + ai_response = None + # Check if we got a valid response if not ai_response or not isinstance(ai_response, dict): - logger.error("Core metadata generation failed: Invalid response from Gemini") + logger.error("Core metadata generation failed: Invalid response from LLM") # Return fallback response primary_keywords = ', '.join(keywords_data.get('primary_keywords', ['content'])) word_count = len(blog_content.split()) @@ -193,12 +213,16 @@ class BlogSEOMetadataGenerator: self, blog_content: str, blog_title: str, - keywords_data: Dict[str, Any] + keywords_data: Dict[str, Any], + outline: Optional[List[Dict[str, Any]]] = None, + seo_analysis: Optional[Dict[str, Any]] = None ) -> Dict[str, Any]: """Generate social media and structured data (Call 2)""" try: # Create comprehensive prompt for social metadata - prompt = self._create_social_metadata_prompt(blog_content, blog_title, keywords_data) + prompt = self._create_social_metadata_prompt( + blog_content, blog_title, keywords_data, outline, seo_analysis + ) # Define simplified structured schema for social metadata schema = { @@ -246,17 +270,26 @@ class BlogSEOMetadataGenerator: "required": ["open_graph", "twitter_card", "json_ld_schema"] } - # Get structured response from Gemini - ai_response = self.gemini_provider( - prompt, - schema, - temperature=0.3, - max_tokens=2048 + # Get structured response using provider-agnostic llm_text_gen + ai_response_raw = llm_text_gen( + prompt=prompt, + json_struct=schema, + system_prompt=None ) + # Handle response: llm_text_gen may return dict (from structured JSON) or str (needs parsing) + ai_response = ai_response_raw + if isinstance(ai_response_raw, str): + try: + import json + ai_response = json.loads(ai_response_raw) + except json.JSONDecodeError: + logger.error(f"Failed to parse JSON response: {ai_response_raw[:200]}...") + ai_response = None + # Check if we got a valid response if not ai_response or not isinstance(ai_response, dict) or not ai_response.get('open_graph') or not ai_response.get('twitter_card') or not ai_response.get('json_ld_schema'): - logger.error("Social metadata generation failed: Invalid or empty response from Gemini") + logger.error("Social metadata generation failed: Invalid or empty response from LLM") # Return fallback response return { 'open_graph': { @@ -301,11 +334,47 @@ class BlogSEOMetadataGenerator: logger.error(f"Social metadata generation failed: {e}") raise e + def _extract_content_highlights(self, blog_content: str, max_length: int = 2500) -> str: + """Extract key sections from blog content for prompt context""" + try: + lines = blog_content.split('\n') + + # Get first paragraph (introduction) + intro = "" + for line in lines[:20]: + if line.strip() and not line.strip().startswith('#'): + intro += line.strip() + " " + if len(intro) > 300: + break + + # Get section headings + headings = [line.strip() for line in lines if line.strip().startswith('##')][:6] + + # Get conclusion if available + conclusion = "" + for line in reversed(lines[-20:]): + if line.strip() and not line.strip().startswith('#'): + conclusion = line.strip() + " " + conclusion + if len(conclusion) > 300: + break + + highlights = f"INTRODUCTION: {intro[:300]}...\n\n" + highlights += f"SECTION HEADINGS: {' | '.join([h.replace('##', '').strip() for h in headings])}\n\n" + if conclusion: + highlights += f"CONCLUSION: {conclusion[:300]}..." + + return highlights[:max_length] + except Exception as e: + logger.warning(f"Failed to extract content highlights: {e}") + return blog_content[:2000] + "..." + def _create_core_metadata_prompt( self, blog_content: str, blog_title: str, - keywords_data: Dict[str, Any] + keywords_data: Dict[str, Any], + outline: Optional[List[Dict[str, Any]]] = None, + seo_analysis: Optional[Dict[str, Any]] = None ) -> str: """Create high-quality prompt for core metadata generation""" @@ -314,30 +383,106 @@ class BlogSEOMetadataGenerator: search_intent = keywords_data.get('search_intent', 'informational') target_audience = keywords_data.get('target_audience', 'general') industry = keywords_data.get('industry', 'general') - - # Calculate word count for reading time estimation word_count = len(blog_content.split()) + # Extract outline structure + outline_context = "" + if outline: + headings = [s.get('heading', '') for s in outline if s.get('heading')] + outline_context = f""" +OUTLINE STRUCTURE: +- Total sections: {len(outline)} +- Section headings: {', '.join(headings[:8])} +- Content hierarchy: Well-structured with {len(outline)} main sections +""" + + # Extract SEO analysis insights + seo_context = "" + if seo_analysis: + overall_score = seo_analysis.get('overall_score', seo_analysis.get('seo_score', 0)) + category_scores = seo_analysis.get('category_scores', {}) + applied_recs = seo_analysis.get('applied_recommendations', []) + + seo_context = f""" +SEO ANALYSIS RESULTS: +- Overall SEO Score: {overall_score}/100 +- Category Scores: Structure {category_scores.get('structure', category_scores.get('Structure', 0))}, Keywords {category_scores.get('keywords', category_scores.get('Keywords', 0))}, Readability {category_scores.get('readability', category_scores.get('Readability', 0))} +- Applied Recommendations: {len(applied_recs)} SEO optimizations have been applied +- Content Quality: Optimized for search engines with keyword focus +""" + + # Get more content context (key sections instead of just first 1000 chars) + content_preview = self._extract_content_highlights(blog_content) + prompt = f""" -Generate SEO metadata for this blog post. +Generate comprehensive, personalized SEO metadata for this blog post. -BLOG TITLE: {blog_title} -BLOG CONTENT: {blog_content[:1000]}... +=== BLOG CONTENT CONTEXT === +TITLE: {blog_title} +CONTENT PREVIEW (key sections): {content_preview} +WORD COUNT: {word_count} words +READING TIME ESTIMATE: {max(1, word_count // 200)} minutes + +{outline_context} + +=== KEYWORD & AUDIENCE DATA === PRIMARY KEYWORDS: {primary_keywords} SEMANTIC KEYWORDS: {semantic_keywords} -WORD COUNT: {word_count} +SEARCH INTENT: {search_intent} +TARGET AUDIENCE: {target_audience} +INDUSTRY: {industry} -Generate: -1. SEO TITLE (50-60 characters) - include primary keyword -2. META DESCRIPTION (150-160 characters) - include CTA -3. URL SLUG (lowercase, hyphens, 3-5 words) -4. BLOG TAGS (5-8 relevant tags) -5. BLOG CATEGORIES (2-3 categories) -6. SOCIAL HASHTAGS (5-10 hashtags with #) -7. READING TIME (calculate from {word_count} words) -8. FOCUS KEYWORD (primary keyword for SEO) +{seo_context} -Make it compelling and SEO-optimized. +=== METADATA GENERATION REQUIREMENTS === +1. SEO TITLE (50-60 characters, must include primary keyword): + - Front-load primary keyword + - Make it compelling and click-worthy + - Include power words if appropriate for {target_audience} audience + - Optimized for {search_intent} search intent + +2. META DESCRIPTION (150-160 characters, must include CTA): + - Include primary keyword naturally in first 120 chars + - Add compelling call-to-action (e.g., "Learn more", "Discover how", "Get started") + - Highlight value proposition for {target_audience} audience + - Use {industry} industry-specific terminology where relevant + +3. URL SLUG (lowercase, hyphens, 3-5 words): + - Include primary keyword + - Remove stop words + - Keep it concise and readable + +4. BLOG TAGS (5-8 relevant tags): + - Mix of primary, semantic, and long-tail keywords + - Industry-specific tags for {industry} + - Audience-relevant tags for {target_audience} + +5. BLOG CATEGORIES (2-3 categories): + - Based on content structure and {industry} industry standards + - Reflect main themes from outline sections + +6. SOCIAL HASHTAGS (5-10 hashtags with #): + - Include primary keyword as hashtag + - Industry-specific hashtags for {industry} + - Trending/relevant hashtags for {target_audience} + +7. READING TIME (calculate from {word_count} words): + - Average reading speed: 200 words/minute + - Round to nearest minute + +8. FOCUS KEYWORD (primary keyword for SEO): + - Select the most important primary keyword + - Should match the main topic and search intent + +=== QUALITY REQUIREMENTS === +- All metadata must be unique, not generic +- Incorporate insights from SEO analysis if provided +- Reflect the actual content structure from outline +- Use language appropriate for {target_audience} audience +- Optimize for {search_intent} search intent +- Make descriptions compelling and action-oriented + +Generate metadata that is personalized, compelling, and SEO-optimized. """ return prompt @@ -345,7 +490,9 @@ Make it compelling and SEO-optimized. self, blog_content: str, blog_title: str, - keywords_data: Dict[str, Any] + keywords_data: Dict[str, Any], + outline: Optional[List[Dict[str, Any]]] = None, + seo_analysis: Optional[Dict[str, Any]] = None ) -> str: """Create high-quality prompt for social metadata generation""" @@ -353,49 +500,68 @@ Make it compelling and SEO-optimized. search_intent = keywords_data.get('search_intent', 'informational') target_audience = keywords_data.get('target_audience', 'general') industry = keywords_data.get('industry', 'general') - current_date = datetime.now().isoformat() + # Add outline and SEO context similar to core metadata prompt + outline_context = "" + if outline: + headings = [s.get('heading', '') for s in outline if s.get('heading')] + outline_context = f"\nOUTLINE SECTIONS: {', '.join(headings[:6])}\n" + + seo_context = "" + if seo_analysis: + overall_score = seo_analysis.get('overall_score', seo_analysis.get('seo_score', 0)) + seo_context = f"\nSEO SCORE: {overall_score}/100 (optimized content)\n" + + content_preview = self._extract_content_highlights(blog_content, 1500) + prompt = f""" -Generate social media metadata for this blog post. +Generate engaging social media metadata for this blog post. -BLOG TITLE: {blog_title} -BLOG CONTENT: {blog_content[:800]}... -PRIMARY KEYWORDS: {primary_keywords} +=== CONTENT === +TITLE: {blog_title} +CONTENT: {content_preview} +{outline_context} +{seo_context} +KEYWORDS: {primary_keywords} +TARGET AUDIENCE: {target_audience} +INDUSTRY: {industry} CURRENT DATE: {current_date} -Generate: +=== GENERATION REQUIREMENTS === 1. OPEN GRAPH (Facebook/LinkedIn): - - title: 60 chars max - - description: 160 chars max - - image: image URL + - title: 60 chars max, include primary keyword, compelling for {target_audience} + - description: 160 chars max, include CTA and value proposition + - image: Suggest an appropriate image URL (placeholder if none available) - type: "article" - - site_name: site name - - url: canonical URL + - site_name: Use appropriate site name for {industry} industry + - url: Generate canonical URL structure 2. TWITTER CARD: - card: "summary_large_image" - - title: 70 chars max - - description: 200 chars max with hashtags - - image: image URL - - site: @sitename - - creator: @author + - title: 70 chars max, optimized for Twitter audience + - description: 200 chars max with relevant hashtags inline + - image: Match Open Graph image + - site: @yourwebsite (placeholder, user should update) + - creator: @author (placeholder, user should update) -3. JSON-LD SCHEMA: +3. JSON-LD SCHEMA (Article): - @context: "https://schema.org" - @type: "Article" - - headline: article title - - description: article description - - author: {{"@type": "Person", "name": "Author Name"}} - - publisher: {{"@type": "Organization", "name": "Site Name"}} - - datePublished: ISO date - - dateModified: ISO date - - mainEntityOfPage: canonical URL - - keywords: array of keywords - - wordCount: word count + - headline: Article title (optimized) + - description: Article description (150-200 chars) + - author: {{"@type": "Person", "name": "Author Name"}} (placeholder) + - publisher: {{"@type": "Organization", "name": "Site Name", "logo": {{"@type": "ImageObject", "url": "logo-url"}}}} + - datePublished: {current_date} + - dateModified: {current_date} + - mainEntityOfPage: {{"@type": "WebPage", "@id": "canonical-url"}} + - keywords: Array of primary and semantic keywords + - wordCount: {len(blog_content.split())} + - articleSection: Primary category based on content + - inLanguage: "en-US" -Make it engaging and SEO-optimized. +Make it engaging, personalized for {target_audience}, and optimized for {industry} industry. """ return prompt diff --git a/backend/services/blog_writer/seo/blog_seo_recommendation_applier.py b/backend/services/blog_writer/seo/blog_seo_recommendation_applier.py new file mode 100644 index 00000000..ed55779e --- /dev/null +++ b/backend/services/blog_writer/seo/blog_seo_recommendation_applier.py @@ -0,0 +1,269 @@ +"""Blog SEO Recommendation Applier + +Applies actionable SEO recommendations to existing blog content using the +provider-agnostic `llm_text_gen` dispatcher. Ensures GPT_PROVIDER parity. +""" + +import asyncio +from typing import Dict, Any, List +from utils.logger_utils import get_service_logger + +from services.llm_providers.main_text_generation import llm_text_gen + + +logger = get_service_logger("blog_seo_recommendation_applier") + + +class BlogSEORecommendationApplier: + """Apply actionable SEO recommendations to blog content.""" + + def __init__(self): + logger.debug("Initialized BlogSEORecommendationApplier") + + async def apply_recommendations(self, payload: Dict[str, Any]) -> Dict[str, Any]: + """Apply recommendations and return updated content.""" + + title = payload.get("title", "Untitled Blog") + sections: List[Dict[str, Any]] = payload.get("sections", []) + outline = payload.get("outline", []) + research = payload.get("research", {}) + recommendations = payload.get("recommendations", []) + persona = payload.get("persona", {}) + tone = payload.get("tone") + audience = payload.get("audience") + + if not sections: + return {"success": False, "error": "No sections provided for recommendation application"} + + if not recommendations: + logger.warning("apply_recommendations called without recommendations") + return {"success": True, "title": title, "sections": sections, "applied": []} + + prompt = self._build_prompt( + title=title, + sections=sections, + outline=outline, + research=research, + recommendations=recommendations, + persona=persona, + tone=tone, + audience=audience, + ) + + schema = { + "type": "object", + "properties": { + "title": {"type": "string"}, + "sections": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": {"type": "string"}, + "heading": {"type": "string"}, + "content": {"type": "string"}, + "notes": {"type": "array", "items": {"type": "string"}}, + }, + "required": ["id", "heading", "content"], + }, + }, + "applied_recommendations": { + "type": "array", + "items": { + "type": "object", + "properties": { + "category": {"type": "string"}, + "summary": {"type": "string"}, + }, + }, + }, + }, + "required": ["sections"], + } + + logger.info("Applying SEO recommendations via llm_text_gen") + + result = await asyncio.to_thread( + llm_text_gen, + prompt, + None, + schema, + ) + + if not result or result.get("error"): + error_msg = result.get("error", "Unknown error") if result else "No response from text generator" + logger.error(f"SEO recommendation application failed: {error_msg}") + return {"success": False, "error": error_msg} + + raw_sections = result.get("sections", []) or [] + normalized_sections: List[Dict[str, Any]] = [] + + # Build lookup table from updated sections using their identifiers + updated_map: Dict[str, Dict[str, Any]] = {} + for updated in raw_sections: + section_id = str( + updated.get("id") + or updated.get("section_id") + or updated.get("heading") + or "" + ).strip() + + if not section_id: + continue + + heading = ( + updated.get("heading") + or updated.get("title") + or section_id + ) + + content_text = updated.get("content", "") + if isinstance(content_text, list): + content_text = "\n\n".join(str(p).strip() for p in content_text if p) + + updated_map[section_id] = { + "id": section_id, + "heading": heading, + "content": str(content_text).strip(), + "notes": updated.get("notes", []), + } + + if not updated_map and raw_sections: + logger.warning("Updated sections missing identifiers; falling back to positional mapping") + + for index, original in enumerate(sections): + fallback_id = str( + original.get("id") + or original.get("section_id") + or f"section_{index + 1}" + ).strip() + + mapped = updated_map.get(fallback_id) + + if not mapped and raw_sections: + # Fall back to positional match if identifier lookup failed + candidate = raw_sections[index] if index < len(raw_sections) else {} + heading = ( + candidate.get("heading") + or candidate.get("title") + or original.get("heading") + or original.get("title") + or f"Section {index + 1}" + ) + content_text = candidate.get("content") or original.get("content", "") + if isinstance(content_text, list): + content_text = "\n\n".join(str(p).strip() for p in content_text if p) + mapped = { + "id": fallback_id, + "heading": heading, + "content": str(content_text).strip(), + "notes": candidate.get("notes", []), + } + + if not mapped: + # Fallback to original content if nothing else available + mapped = { + "id": fallback_id, + "heading": original.get("heading") or original.get("title") or f"Section {index + 1}", + "content": str(original.get("content", "")).strip(), + "notes": original.get("notes", []), + } + + normalized_sections.append(mapped) + + applied = result.get("applied_recommendations", []) + + logger.info("SEO recommendations applied successfully") + + return { + "success": True, + "title": result.get("title", title), + "sections": normalized_sections, + "applied": applied, + } + + def _build_prompt( + self, + *, + title: str, + sections: List[Dict[str, Any]], + outline: List[Dict[str, Any]], + research: Dict[str, Any], + recommendations: List[Dict[str, Any]], + persona: Dict[str, Any], + tone: str | None, + audience: str | None, + ) -> str: + """Construct prompt for applying recommendations.""" + + sections_str = [] + for section in sections: + sections_str.append( + f"ID: {section.get('id', 'section')}, Heading: {section.get('heading', 'Untitled')}\n" + f"Current Content:\n{section.get('content', '')}\n" + ) + + outline_str = "\n".join( + [ + f"- {item.get('heading', 'Section')} (Target words: {item.get('target_words', 'N/A')})" + for item in outline + ] + ) + + research_summary = research.get("keyword_analysis", {}) if research else {} + primary_keywords = ", ".join(research_summary.get("primary", [])[:10]) or "None" + + recommendations_str = [] + for rec in recommendations: + recommendations_str.append( + f"Category: {rec.get('category', 'General')} | Priority: {rec.get('priority', 'Medium')}\n" + f"Recommendation: {rec.get('recommendation', '')}\n" + f"Impact: {rec.get('impact', '')}\n" + ) + + persona_str = ( + f"Persona: {persona}\n" + if persona + else "Persona: (not provided)\n" + ) + + style_guidance = [] + if tone: + style_guidance.append(f"Desired tone: {tone}") + if audience: + style_guidance.append(f"Target audience: {audience}") + style_str = "\n".join(style_guidance) if style_guidance else "Maintain current tone and audience alignment." + + prompt = f""" +You are an expert SEO content strategist. Update the blog content to apply the actionable recommendations. + +Current Title: {title} + +Primary Keywords (for context): {primary_keywords} + +Outline Overview: +{outline_str or 'No outline supplied'} + +Existing Sections: +{''.join(sections_str)} + +Actionable Recommendations to Apply: +{''.join(recommendations_str)} + +{persona_str} +{style_str} + +Instructions: +1. Carefully apply the recommendations while preserving factual accuracy and research alignment. +2. Keep section identifiers (IDs) unchanged so the frontend can map updates correctly. +3. Improve clarity, flow, and SEO optimization per the guidance. +4. Return updated sections in the requested JSON format. +5. Provide a short summary of which recommendations were addressed. +""" + + return prompt + + +__all__ = ["BlogSEORecommendationApplier"] + + diff --git a/backend/services/linkedin/image_generation/linkedin_image_generator.py b/backend/services/linkedin/image_generation/linkedin_image_generator.py index a14362b2..23ada17e 100644 --- a/backend/services/linkedin/image_generation/linkedin_image_generator.py +++ b/backend/services/linkedin/image_generation/linkedin_image_generator.py @@ -16,7 +16,7 @@ from io import BytesIO # Import existing infrastructure from ...onboarding.api_key_manager import APIKeyManager -from ...llm_providers.text_to_image_generation.gen_gemini_images import generate_gemini_image +from ...llm_providers.main_image_generation import generate_image # Set up logging logger = logging.getLogger(__name__) @@ -270,41 +270,57 @@ class LinkedInImageGenerator: async def _generate_with_gemini(self, prompt: str, aspect_ratio: str) -> Dict[str, Any]: """ - Generate image using existing Gemini infrastructure. + Generate image using unified image generation infrastructure. Args: prompt: Enhanced prompt for image generation aspect_ratio: Desired aspect ratio Returns: - Generation result from Gemini + Generation result from image generation provider """ try: - # Use existing Gemini image generation function - # This integrates with the current infrastructure - result = generate_gemini_image(prompt, aspect_ratio=aspect_ratio) + # Map aspect ratio to dimensions (LinkedIn-optimized) + aspect_map = { + "1:1": (1024, 1024), + "16:9": (1920, 1080), + "4:3": (1366, 1024), + "9:16": (1080, 1920), # Portrait for stories + } + width, height = aspect_map.get(aspect_ratio, (1024, 1024)) - if result and os.path.exists(result): - # Read the generated image - with open(result, 'rb') as f: - image_data = f.read() - + # Use unified image generation system (defaults to provider based on GPT_PROVIDER) + result = generate_image( + prompt=prompt, + options={ + "provider": "gemini", # LinkedIn uses Gemini by default + "model": self.model if hasattr(self, 'model') else None, + "width": width, + "height": height, + } + ) + + if result and result.image_bytes: return { 'success': True, - 'image_data': image_data, - 'image_path': result + 'image_data': result.image_bytes, + 'image_path': None, # No file path, using bytes directly + 'width': result.width, + 'height': result.height, + 'provider': result.provider, + 'model': result.model, } else: return { 'success': False, - 'error': 'Gemini image generation returned no result' + 'error': 'Image generation returned no result' } except Exception as e: - logger.error(f"Error in Gemini image generation: {str(e)}") + logger.error(f"Error in image generation: {str(e)}") return { 'success': False, - 'error': f"Gemini generation failed: {str(e)}" + 'error': f"Image generation failed: {str(e)}" } async def _process_generated_image( diff --git a/backend/services/llm_providers/image_generation/__init__.py b/backend/services/llm_providers/image_generation/__init__.py new file mode 100644 index 00000000..3bf6201c --- /dev/null +++ b/backend/services/llm_providers/image_generation/__init__.py @@ -0,0 +1,15 @@ +from .base import ImageGenerationOptions, ImageGenerationResult, ImageGenerationProvider +from .hf_provider import HuggingFaceImageProvider +from .gemini_provider import GeminiImageProvider +from .stability_provider import StabilityImageProvider + +__all__ = [ + "ImageGenerationOptions", + "ImageGenerationResult", + "ImageGenerationProvider", + "HuggingFaceImageProvider", + "GeminiImageProvider", + "StabilityImageProvider", +] + + diff --git a/backend/services/llm_providers/image_generation/base.py b/backend/services/llm_providers/image_generation/base.py new file mode 100644 index 00000000..12bff6b6 --- /dev/null +++ b/backend/services/llm_providers/image_generation/base.py @@ -0,0 +1,37 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Optional, Dict, Any, Protocol + + +@dataclass +class ImageGenerationOptions: + prompt: str + negative_prompt: Optional[str] = None + width: int = 1024 + height: int = 1024 + guidance_scale: Optional[float] = None + steps: Optional[int] = None + seed: Optional[int] = None + model: Optional[str] = None + extra: Optional[Dict[str, Any]] = None + + +@dataclass +class ImageGenerationResult: + image_bytes: bytes + width: int + height: int + provider: str + model: Optional[str] = None + seed: Optional[int] = None + metadata: Optional[Dict[str, Any]] = None + + +class ImageGenerationProvider(Protocol): + """Protocol for image generation providers.""" + + def generate(self, options: ImageGenerationOptions) -> ImageGenerationResult: + ... + + diff --git a/backend/services/llm_providers/image_generation/gemini_provider.py b/backend/services/llm_providers/image_generation/gemini_provider.py new file mode 100644 index 00000000..3efadd29 --- /dev/null +++ b/backend/services/llm_providers/image_generation/gemini_provider.py @@ -0,0 +1,47 @@ +from __future__ import annotations + +import io +import os +from typing import Optional + +from PIL import Image + +from .base import ImageGenerationOptions, ImageGenerationResult, ImageGenerationProvider +from utils.logger_utils import get_service_logger + + +logger = get_service_logger("image_generation.gemini") + + +class GeminiImageProvider(ImageGenerationProvider): + """Google Gemini/Imagen backed image generation. + + NOTE: Implementation should call the actual Gemini Images API used in the codebase. + Here we keep a minimal interface and expect the underlying client to be wired + similarly to other providers and return a PIL image or raw bytes. + """ + + def __init__(self) -> None: + api_key = os.getenv("GOOGLE_API_KEY") + if not api_key: + logger.warning("GOOGLE_API_KEY not set. Gemini image generation may fail at runtime.") + logger.info("GeminiImageProvider initialized") + + def generate(self, options: ImageGenerationOptions) -> ImageGenerationResult: + # Placeholder implementation to be replaced by real Gemini/Imagen call. + # For now, generate a 1x1 transparent PNG to maintain interface consistency + img = Image.new("RGBA", (max(1, options.width), max(1, options.height)), (0, 0, 0, 0)) + with io.BytesIO() as buf: + img.save(buf, format="PNG") + png = buf.getvalue() + + return ImageGenerationResult( + image_bytes=png, + width=img.width, + height=img.height, + provider="gemini", + model=os.getenv("GEMINI_IMAGE_MODEL"), + seed=options.seed, + ) + + diff --git a/backend/services/llm_providers/image_generation/hf_provider.py b/backend/services/llm_providers/image_generation/hf_provider.py new file mode 100644 index 00000000..5d4a74e9 --- /dev/null +++ b/backend/services/llm_providers/image_generation/hf_provider.py @@ -0,0 +1,73 @@ +from __future__ import annotations + +import io +import os +from typing import Optional, Dict, Any + +from PIL import Image +from huggingface_hub import InferenceClient + +from .base import ImageGenerationOptions, ImageGenerationResult, ImageGenerationProvider +from utils.logger_utils import get_service_logger + + +logger = get_service_logger("image_generation.huggingface") + + +DEFAULT_HF_MODEL = os.getenv( + "HF_IMAGE_MODEL", + "black-forest-labs/FLUX.1-Krea-dev", +) + + +class HuggingFaceImageProvider(ImageGenerationProvider): + """Hugging Face Inference Providers (fal-ai) backed image generation. + + API doc: https://huggingface.co/docs/inference-providers/en/tasks/text-to-image + """ + + def __init__(self, api_key: Optional[str] = None, provider: str = "fal-ai") -> None: + self.api_key = api_key or os.getenv("HF_TOKEN") + if not self.api_key: + raise RuntimeError("HF_TOKEN is required for Hugging Face image generation") + self.provider = provider + self.client = InferenceClient(provider=self.provider, api_key=self.api_key) + logger.info("HuggingFaceImageProvider initialized (provider=%s)", self.provider) + + def generate(self, options: ImageGenerationOptions) -> ImageGenerationResult: + model = options.model or DEFAULT_HF_MODEL + params: Dict[str, Any] = {} + if options.guidance_scale is not None: + params["guidance_scale"] = options.guidance_scale + if options.steps is not None: + params["num_inference_steps"] = options.steps + if options.negative_prompt: + params["negative_prompt"] = options.negative_prompt + if options.seed is not None: + params["seed"] = options.seed + + # The HF InferenceClient returns a PIL Image + logger.debug("HF generate: model=%s width=%s height=%s params=%s", model, options.width, options.height, params) + img: Image.Image = self.client.text_to_image( + options.prompt, + model=model, + width=options.width, + height=options.height, + **params, + ) + + with io.BytesIO() as buf: + img.save(buf, format="PNG") + image_bytes = buf.getvalue() + + return ImageGenerationResult( + image_bytes=image_bytes, + width=img.width, + height=img.height, + provider="huggingface", + model=model, + seed=options.seed, + metadata={"provider": self.provider}, + ) + + diff --git a/backend/services/llm_providers/image_generation/stability_provider.py b/backend/services/llm_providers/image_generation/stability_provider.py new file mode 100644 index 00000000..340c9971 --- /dev/null +++ b/backend/services/llm_providers/image_generation/stability_provider.py @@ -0,0 +1,79 @@ +from __future__ import annotations + +import io +import os +from typing import Optional, Dict, Any + +import requests +from PIL import Image + +from .base import ImageGenerationOptions, ImageGenerationResult, ImageGenerationProvider +from utils.logger_utils import get_service_logger + + +logger = get_service_logger("image_generation.stability") + + +DEFAULT_STABILITY_MODEL = os.getenv("STABILITY_MODEL", "stable-diffusion-xl-1024-v1-0") + + +class StabilityImageProvider(ImageGenerationProvider): + """Stability AI Images API provider (simple text-to-image). + + This uses the v1 text-to-image endpoint format. Adjust to match your existing + Stability integration if different. + """ + + def __init__(self, api_key: Optional[str] = None) -> None: + self.api_key = api_key or os.getenv("STABILITY_API_KEY") + if not self.api_key: + logger.warning("STABILITY_API_KEY not set. Stability generation may fail at runtime.") + logger.info("StabilityImageProvider initialized") + + def generate(self, options: ImageGenerationOptions) -> ImageGenerationResult: + headers = { + "Authorization": f"Bearer {self.api_key}", + "Accept": "application/json", + "Content-Type": "application/json", + } + payload: Dict[str, Any] = { + "text_prompts": [ + {"text": options.prompt, "weight": 1.0}, + ], + "cfg_scale": options.guidance_scale or 7.0, + "steps": options.steps or 30, + "width": options.width, + "height": options.height, + "seed": options.seed, + } + if options.negative_prompt: + payload["text_prompts"].append({"text": options.negative_prompt, "weight": -1.0}) + + model = options.model or DEFAULT_STABILITY_MODEL + url = f"https://api.stability.ai/v1/generation/{model}/text-to-image" + + logger.debug("Stability generate: model=%s payload_keys=%s", model, list(payload.keys())) + resp = requests.post(url, headers=headers, json=payload, timeout=60) + resp.raise_for_status() + data = resp.json() + + # Expecting data["artifacts"][0]["base64"] + import base64 + + artifact = (data.get("artifacts") or [{}])[0] + b64 = artifact.get("base64", "") + image_bytes = base64.b64decode(b64) + + # Confirm dimensions by loading once (optional) + img = Image.open(io.BytesIO(image_bytes)) + + return ImageGenerationResult( + image_bytes=image_bytes, + width=img.width, + height=img.height, + provider="stability", + model=model, + seed=options.seed, + ) + + diff --git a/backend/services/llm_providers/main_image_generation.py b/backend/services/llm_providers/main_image_generation.py new file mode 100644 index 00000000..9b058c65 --- /dev/null +++ b/backend/services/llm_providers/main_image_generation.py @@ -0,0 +1,73 @@ +from __future__ import annotations + +import os +from typing import Optional, Dict, Any + +from .image_generation import ( + ImageGenerationOptions, + ImageGenerationResult, + HuggingFaceImageProvider, + GeminiImageProvider, + StabilityImageProvider, +) +from utils.logger_utils import get_service_logger + + +logger = get_service_logger("image_generation.facade") + + +def _select_provider(explicit: Optional[str]) -> str: + if explicit: + return explicit + gpt_provider = (os.getenv("GPT_PROVIDER") or "").lower() + if gpt_provider.startswith("gemini"): + return "gemini" + if gpt_provider.startswith("hf"): + return "huggingface" + if os.getenv("STABILITY_API_KEY"): + return "stability" + # Fallback to huggingface to enable a path if configured + return "huggingface" + + +def _get_provider(provider_name: str): + if provider_name == "huggingface": + return HuggingFaceImageProvider() + if provider_name == "gemini": + return GeminiImageProvider() + if provider_name == "stability": + return StabilityImageProvider() + raise ValueError(f"Unknown image provider: {provider_name}") + + +def generate_image(prompt: str, options: Optional[Dict[str, Any]] = None) -> ImageGenerationResult: + opts = options or {} + provider_name = _select_provider(opts.get("provider")) + + image_options = ImageGenerationOptions( + prompt=prompt, + negative_prompt=opts.get("negative_prompt"), + width=int(opts.get("width", 1024)), + height=int(opts.get("height", 1024)), + guidance_scale=opts.get("guidance_scale"), + steps=opts.get("steps"), + seed=opts.get("seed"), + model=opts.get("model"), + extra=opts, + ) + + # Normalize obvious model/provider mismatches + model_lower = (image_options.model or "").lower() + if provider_name == "stability" and (model_lower.startswith("black-forest-labs/") or model_lower.startswith("runwayml/") or model_lower.startswith("stabilityai/flux")): + logger.info("Remapping provider to huggingface for model=%s", image_options.model) + provider_name = "huggingface" + + if provider_name == "huggingface" and not image_options.model: + # Provide a sensible default HF model if none specified + image_options.model = "black-forest-labs/FLUX.1-Krea-dev" + + logger.info("Generating image via provider=%s model=%s", provider_name, image_options.model) + provider = _get_provider(provider_name) + return provider.generate(image_options) + + diff --git a/backend/services/llm_providers/text_to_image_generation/gen_dali2_images.py b/backend/services/llm_providers/text_to_image_generation/gen_dali2_images.py deleted file mode 100644 index 8af0a0a5..00000000 --- a/backend/services/llm_providers/text_to_image_generation/gen_dali2_images.py +++ /dev/null @@ -1,56 +0,0 @@ -from openai import OpenAI -from loguru import logger -import sys - -from .save_image import save_generated_image - -from tenacity import ( - retry, - stop_after_attempt, - wait_random_exponential, -) # for exponential backoff - - -@retry(wait=wait_random_exponential(min=1, max=120), stop=stop_after_attempt(6)) -def generate_dalle3_images(img_prompt, image_dir, size="1024x1024", quality="hd", n=1): - """ - Generates images using the DALL-E 3 model based on a given text prompt. - - Args: - img_prompt (str): Text prompt to generate the image. - image_dir (str): Directory where the generated image will be saved. - size (str, optional): Size of the generated images. Defaults to "1024x1024". - quality (str, optional): Quality of the generated images. Defaults to "hd". - n (int, optional): Number of images to generate. Defaults to 1. - - Returns: - str: Path to the saved image. - - Raises: - SystemExit: If an error occurs in image generation or saving. - """ - try: - logger.info("Generating Dall-e-3 image for the blog.") - client = OpenAI() - - img_generation_response = client.images.generate( - model="dall-e-3", - prompt=img_prompt, - size=size, - quality=quality, - n=n - ) - # Save the generated image locally. - try: - img_path = save_generated_image(img_generation_response, image_dir) - return img_path - except Exception as err: - logger.error(f"Failed to Save generated image: {err}") - - except openai.OpenAIError as e: - logger.error(f"Dalle-3 image generation error: HTTP Status {e.http_status}, Error: {e.error}") - sys.exit("Exiting due to Dalle-3 image generation error.") - - except Exception as e: - logger.error(f"Failed to generate images with Dalle3: {e}") - sys.exit("Exiting due to a general error in image generation.") diff --git a/backend/services/llm_providers/text_to_image_generation/gen_dali3_images.py b/backend/services/llm_providers/text_to_image_generation/gen_dali3_images.py deleted file mode 100644 index 2339bbfe..00000000 --- a/backend/services/llm_providers/text_to_image_generation/gen_dali3_images.py +++ /dev/null @@ -1,53 +0,0 @@ -from openai import OpenAI -from loguru import logger -import sys - -from tenacity import ( - retry, - stop_after_attempt, - wait_random_exponential, -) # for exponential backoff - -from .save_image import save_generated_image - - -@retry(wait=wait_random_exponential(min=1, max=120), stop=stop_after_attempt(6)) -def generate_dalle3_images(img_prompt, image_dir, size="1024x1024", quality="hd", n=1): - """ - Generates images using the DALL-E 3 model based on a given text prompt. - - Args: - img_prompt (str): Text prompt to generate the image. - image_dir (str): Directory where the generated image will be saved. - size (str, optional): Size of the generated images. Defaults to "1024x1024". - quality (str, optional): Quality of the generated images. Defaults to "hd". - n (int, optional): Number of images to generate. Defaults to 1. - - Returns: - str: Path to the saved image. - - Raises: - SystemExit: If an error occurs in image generation or saving. - """ - try: - logger.info("Generating Dall-e-3 image for the blog.") - client = OpenAI() - - img_generation_response = client.images.generate( - model="dall-e-3", - prompt=img_prompt, - size=size, - quality=quality, - n=n - ) - - img_path = save_generated_image(img_generation_response, image_dir) - return img_path - - except openai.OpenAIError as e: - logger.error(f"Dalle-3 image generation error: HTTP Status {e.http_status}, Error: {e.error}") - sys.exit("Exiting due to Dalle-3 image generation error.") - - except Exception as e: - logger.error(f"Failed to generate images with Dalle3: {e}") - sys.exit("Exiting due to a general error in image generation.") diff --git a/backend/services/llm_providers/text_to_image_generation/gen_gemini_images.py b/backend/services/llm_providers/text_to_image_generation/gen_gemini_images.py deleted file mode 100644 index f5e5a3d8..00000000 --- a/backend/services/llm_providers/text_to_image_generation/gen_gemini_images.py +++ /dev/null @@ -1,583 +0,0 @@ -import os -import sys -import time -import datetime -import base64 -import random -from typing import List, Optional, Tuple -from PIL import Image -from io import BytesIO -import logging - -# Import APIKeyManager -from ...onboarding.api_key_manager import APIKeyManager - -try: - from google import genai - from google.genai import types -except ImportError: - genai = None - logging.getLogger('gemini_image_generator').warning( - "Google genai library not available. Install with: pip install google-generativeai" - ) - - -from .save_image import save_generated_image - -# Configure logging -logging.basicConfig( - level=logging.INFO, - format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' -) -logger = logging.getLogger('gemini_image_generator') - -# Imagen fallback configuration -IMAGEN_FALLBACK_CONFIG = { - 'enabled': os.getenv('IMAGEN_FALLBACK_ENABLED', 'true').lower() == 'true', # Master switch for Imagen fallback - 'auto_fallback': os.getenv('IMAGEN_AUTO_FALLBACK', 'true').lower() == 'true', # Automatically fall back on Gemini failures - 'preferred_model': os.getenv('IMAGEN_MODEL', 'imagen-4.0-generate-001'), # Fast model for quick generation - 'fallback_aspect_ratios': { - '1:1': '1:1', - '3:4': '3:4', - '4:3': '4:3', - '9:16': '9:16', - '16:9': '16:9' - }, - 'max_images': int(os.getenv('IMAGEN_MAX_IMAGES', '1')), # Generate 1 image for LinkedIn posts -} - -# Log configuration on startup -logger.info(f"πŸ”„ Imagen fallback configuration: {IMAGEN_FALLBACK_CONFIG}") - -# With image generation in Gemini, your imagination is the limit. -# Follow Google AI best practices for detailed prompts and iterative refinement. - -# Generate images using Gemini -# Gemini 2.0 Flash Experimental supports the ability to output text and inline images. -# This lets you use Gemini to conversationally edit images or generate outputs with interwoven text (for example, generating a blog post with text and images in a single turn). -# Note: Make sure to include responseModalities: ["Text", "Image"] in your generation configuration for text and image output with gemini-2.0-flash-exp-image-generation. Image only is not allowed. - - -class AIPromptGenerator: - """ - Generates enhanced AI image prompts based on user keywords, - following the guidelines of the Imagen documentation. - """ - - def __init__(self): - self.photography_styles = ["photo", "photograph"] - self.art_styles = ["painting", "sketch", "drawing", "illustration", "digital art", "render"] - self.art_techniques = ["technical pencil drawing", "charcoal drawing", "color pencil drawing", "pastel painting", "digital art", "art deco (poster)", "impressionist painting", "renaissance painting", "pop art"] - self.camera_proximity = ["close-up", "zoomed out", "taken from far away"] - self.camera_position = ["aerial", "from below"] - self.lighting = ["natural lighting", "dramatic lighting", "warm lighting", "cold lighting", "studio lighting", "golden hour lighting"] - self.camera_settings = ["motion blur", "soft focus", "bokeh", "portrait"] - self.lens_types = ["35mm lens", "50mm lens", "fisheye lens", "wide angle lens", "macro lens", "telephoto lens"] - self.film_types = ["black and white film", "polaroid"] - self.materials = ["made of cheese", "made of paper", "made of neon tubes", "metallic", "glass", "wooden", "stone"] - self.shapes = ["in the shape of a bird", "angular", "curved", "geometric"] - self.quality_modifiers_general = ["high-quality", "beautiful", "stylized", "detailed", "epic", "grand"] - self.quality_modifiers_photo = ["4K", "HDR", "studio photo", "professional photo", "photorealistic"] - self.quality_modifiers_art = ["by a professional artist", "intricate details", "masterpiece"] - self.aspect_ratios = ["1:1 aspect ratio", "4:3 aspect ratio", "3:4 aspect ratio", "16:9 aspect ratio", "9:16 aspect ratio"] - self.photorealistic_modifiers = { - "portraits": ["prime lens", "zoom lens", "24-35mm", "black and white film", "film noir", "shallow depth of field", "duotone (mention two colors)"], - "objects": ["macro lens", "60-105mm", "high detail", "precise focusing", "controlled lighting"], - "motion": ["telephoto zoom lens", "100-400mm", "fast shutter speed", "action shot", "movement tracking"], - "wide-angle": ["wide-angle lens", "10-24mm", "long exposure", "sharp focus", "smooth water or clouds", "astro photography"] - } - - def generate_prompt(self, keywords): - """ - Generates an enhanced AI image prompt based on user-provided keywords. - - Args: - keywords (list): A list of keywords describing the desired image. - - Returns: - str: An enhanced AI image prompt. - """ - if not keywords: - return "A beautiful image." - - prompt_parts = [] - subject = " ".join(keywords) - prompt_parts.append(subject) - - # Add context and background (optional) - context_options = ["in a detailed background", "outdoors", "indoors", "in a studio", "with a blurred background"] - if random.random() < 0.6: # Add context with a probability - prompt_parts.append(random.choice(context_options)) - - # Add style (optional) - style_options = self.photography_styles + [f"{art} of" for art in self.art_styles] - if random.random() < 0.7: - prompt_parts.insert(0, random.choice(style_options)) - if prompt_parts[0].startswith("painting of") or prompt_parts[0].startswith("sketch of") or prompt_parts[0].startswith("drawing of"): - if random.random() < 0.5: - prompt_parts.append(f"in the style of {random.choice(self.art_techniques)}") - - # Add photography modifiers (if photography style is chosen) - if any(style in prompt_parts[0] for style in self.photography_styles): - if random.random() < 0.4: - prompt_parts.append(random.choice(self.camera_proximity)) - if random.random() < 0.3: - prompt_parts.append(random.choice(self.camera_position)) - if random.random() < 0.5: - prompt_parts.append(random.choice(self.lighting)) - if random.random() < 0.3: - prompt_parts.append(random.choice(self.camera_settings)) - if random.random() < 0.2: - prompt_parts.append(random.choice(self.lens_types)) - if random.random() < 0.1: - prompt_parts.append(random.choice(self.film_types)) - - # Add shapes and materials (optional) - if random.random() < 0.3: - prompt_parts.append(random.choice(self.materials)) - if random.random() < 0.2: - prompt_parts.append(random.choice(self.shapes)) - - # Add quality modifiers (optional) - if random.random() < 0.6: - quality_options = self.quality_modifiers_general - if any(style in prompt_parts[0] for style in self.photography_styles): - quality_options += self.quality_modifiers_photo - else: - quality_options += self.quality_modifiers_art - prompt_parts.append(random.choice(list(set(quality_options)))) # Avoid duplicates - - # Add aspect ratio (optional) - if random.random() < 0.2: - prompt_parts.append(random.choice(self.aspect_ratios)) - - return ", ".join(prompt_parts) - - def generate_photorealistic_prompt(self, keywords, focus=""): - """ - Generates an enhanced AI image prompt specifically for photorealistic images. - - Args: - keywords (list): A list of keywords describing the desired image. - focus (str, optional): The focus of the photorealistic image (e.g., "portraits", "objects", "motion", "wide-angle"). Defaults to "". - - Returns: - str: An enhanced photorealistic AI image prompt. - """ - if not keywords: - return "A photorealistic image." - - prompt_parts = ["A photo of", "photorealistic"] - prompt_parts.append(" ".join(keywords)) - - if focus and focus in self.photorealistic_modifiers: - modifiers = self.photorealistic_modifiers[focus] - if modifiers: - num_modifiers = random.randint(1, min(3, len(modifiers))) - selected_modifiers = random.sample(modifiers, num_modifiers) - prompt_parts.extend(selected_modifiers) - - # Add general quality modifiers - if random.random() < 0.5: - prompt_parts.append(random.choice(self.quality_modifiers_photo)) - - # Add lighting - if random.random() < 0.4: - prompt_parts.append(random.choice(self.lighting)) - - return ", ".join(prompt_parts) - -def _ensure_client() -> Optional[object]: - """Create a Gemini client if available and API key is configured.""" - api_key_manager = APIKeyManager() - api_key = api_key_manager.get_api_key("gemini") - if not api_key or genai is None: - if not api_key: - logger.warning("No Gemini API key found") - if genai is None: - logger.warning("Google Generative AI library not available") - return None - try: - logger.info("Creating Gemini client...") - # Create a client using the correct API pattern - # The API key is passed directly to the Client constructor - client = genai.Client(api_key=api_key) - logger.info("Gemini client created successfully") - return client - except Exception as e: - logger.error(f"Failed to create Gemini client: {e}") - import traceback - logger.error(f"Traceback: {traceback.format_exc()}") - return None - - -def _generate_imagen_images_base64(prompt: str, aspect_ratio: str = "1:1") -> List[str]: - """ - Generate images using Imagen API as a fallback method. - - This function implements the Imagen API following the official documentation: - https://ai.google.dev/gemini-api/docs/imagen - - Args: - prompt: Text prompt for image generation - aspect_ratio: Desired aspect ratio (1:1, 3:4, 4:3, 9:16, 16:9) - - Returns: - List of base64-encoded PNG images - """ - logger = logging.getLogger('gemini_image_generator') - logger.info("πŸ”„ Falling back to Imagen API for image generation") - - try: - # Get API key for Imagen (can use same Gemini API key) - api_key_manager = APIKeyManager() - api_key = api_key_manager.get_api_key("gemini") # Imagen uses same API key - - if not api_key: - logger.error("No API key available for Imagen fallback") - return [] - - # Create Imagen client - client = genai.Client(api_key=api_key) - - # Map aspect ratio to Imagen format using configuration - imagen_aspect_ratio = IMAGEN_FALLBACK_CONFIG['fallback_aspect_ratios'].get(aspect_ratio, "1:1") - - # Optimize prompt for Imagen (remove Gemini-specific formatting) - imagen_prompt = _optimize_prompt_for_imagen(prompt) - - logger.info(f"Generating Imagen images with prompt: {imagen_prompt[:100]}...") - logger.info(f"Using aspect ratio: {imagen_aspect_ratio}") - logger.info(f"Using model: {IMAGEN_FALLBACK_CONFIG['preferred_model']}") - - # Generate images using configured Imagen model - # Note: sample_image_size is not supported in current library version - config_params = { - 'number_of_images': IMAGEN_FALLBACK_CONFIG['max_images'], - 'aspect_ratio': imagen_aspect_ratio, - } - - # Add additional configuration options if needed - # config_params['guidance_scale'] = 7.5 # Optional: control image generation quality - # config_params['person_generation'] = 'allow_adult' # Optional: control person generation - - response = client.models.generate_images( - model=IMAGEN_FALLBACK_CONFIG['preferred_model'], - prompt=imagen_prompt, - config=types.GenerateImagesConfig(**config_params) - ) - - # Extract base64 images from response - images_b64: List[str] = [] - for generated_image in response.generated_images: - if hasattr(generated_image, 'image') and hasattr(generated_image.image, 'image_bytes'): - # Convert image bytes to base64 - image_bytes = generated_image.image.image_bytes - if isinstance(image_bytes, bytes): - images_b64.append(base64.b64encode(image_bytes).decode('utf-8')) - else: - # If already base64 string - images_b64.append(str(image_bytes)) - - if images_b64: - logger.info(f"βœ… Imagen fallback successful! Generated {len(images_b64)} images") - return images_b64 - else: - logger.warning("Imagen fallback returned no images") - return [] - - except Exception as e: - logger.error(f"❌ Imagen fallback failed: {e}") - import traceback - logger.error(f"Imagen error traceback: {traceback.format_exc()}") - return [] - - -def _optimize_prompt_for_imagen(prompt: str) -> str: - """ - Optimize prompt for Imagen API by removing Gemini-specific formatting - and enhancing it with Imagen best practices. - - Based on Imagen prompt guide: https://ai.google.dev/gemini-api/docs/imagen - """ - # Remove Gemini-specific formatting - prompt = prompt.replace('\n\nEnhanced prompt:', '') - prompt = prompt.replace('\n\nAspect ratio:', '') - - # Clean up extra whitespace - prompt = ' '.join(prompt.split()) - - # Add Imagen-specific enhancements if not present - if 'professional' in prompt.lower() and 'linkedin' in prompt.lower(): - # Enhance for LinkedIn professional content - prompt += ", high quality, professional photography, business appropriate" - - if 'digital transformation' in prompt.lower() or 'technology' in prompt.lower(): - # Enhance for tech content - prompt += ", modern, innovative, clean design, corporate aesthetic" - - # Ensure prompt doesn't exceed Imagen's 480 token limit - if len(prompt) > 400: # Leave some buffer - prompt = prompt[:400] + "..." - - return prompt - - -def generate_gemini_images_base64( - prompt: str, - *, - keywords: Optional[list] = None, - style: Optional[str] = None, - focus: Optional[str] = None, - enhance_prompt: bool = True, - aspect_ratio: str = "9:16", - max_retries: int = 2, - initial_retry_delay: float = 1.0, - enable_imagen_fallback: bool = True, -) -> List[str]: - """ - Return list of base64 PNG images generated from a prompt. - - Primary method: Gemini API for image generation - Fallback method: Imagen API when Gemini fails (quota limits, API errors, etc.) - - Implements best practices per Gemini docs: send text prompt, parse inline image parts, - and return base64 data suitable for API responses. No Streamlit, no printing. - - Docs: - - Gemini: https://ai.google.dev/gemini-api/docs/image-generation - - Imagen: https://ai.google.dev/gemini-api/docs/imagen - """ - logger = logging.getLogger('gemini_image_generator') - logger.info("Generating image (base64) with Gemini (with Imagen fallback)") - - if enhance_prompt and keywords: - pg = AIPromptGenerator() - enhanced = ( - pg.generate_photorealistic_prompt(keywords, focus) - if style == "photorealistic" and focus - else pg.generate_prompt(keywords) - ) - prompt = f"{prompt}\n\nEnhanced prompt: {enhanced}" - - # Optional hint in-text for aspect ratio; API doesn't take ratio param directly - if aspect_ratio: - prompt = f"{prompt}\n\nAspect ratio: {aspect_ratio}" - - # Try Gemini first - client = _ensure_client() - if client is None: - logger.warning("Gemini client not available or API key missing") - if enable_imagen_fallback and IMAGEN_FALLBACK_CONFIG['enabled']: - logger.info("Falling back to Imagen API") - return _generate_imagen_images_base64(prompt, aspect_ratio) - return [] - - retry = 0 - delay = initial_retry_delay - while retry <= max_retries: - try: - response = client.models.generate_content( - model="gemini-2.0-flash-exp-image-generation", - contents=[prompt], - ) - - images_b64: List[str] = [] - for part in response.candidates[0].content.parts: - if getattr(part, 'inline_data', None) is not None: - # part.inline_data.data is bytes (base64 decoded by SDK?) - # Standardize to base64 string for API consumers - raw = part.inline_data.data - if isinstance(raw, bytes): - images_b64.append(base64.b64encode(raw).decode('utf-8')) - else: - # Some SDKs may already present base64 str - images_b64.append(str(raw)) - - if images_b64: - logger.info(f"βœ… Gemini generated {len(images_b64)} images successfully") - return images_b64 - else: - logger.warning("Gemini returned no images, falling back to Imagen") - if enable_imagen_fallback and IMAGEN_FALLBACK_CONFIG['enabled']: - return _generate_imagen_images_base64(prompt, aspect_ratio) - return [] - - except Exception as e: - msg = str(e) - logger.warning(f"Gemini image gen error: {msg}") - - # Check if this is a quota/API error that warrants fallback - if any(error_type in msg.lower() for error_type in [ - 'quota', 'resource_exhausted', 'rate_limit', 'billing', 'api_key', '403', '429' - ]): - logger.info("Gemini quota/API error detected, falling back to Imagen") - if enable_imagen_fallback and IMAGEN_FALLBACK_CONFIG['enabled']: - return _generate_imagen_images_base64(prompt, aspect_ratio) - return [] - - # For other errors, retry if possible - if "503" in msg and retry < max_retries: - time.sleep(delay) - delay *= 2 - retry += 1 - continue - - # Final fallback for any other errors - if enable_imagen_fallback and IMAGEN_FALLBACK_CONFIG['enabled']: - logger.info("Final fallback to Imagen due to Gemini error") - return _generate_imagen_images_base64(prompt, aspect_ratio) - return [] - - # If all retries exhausted, fall back to Imagen - if enable_imagen_fallback and IMAGEN_FALLBACK_CONFIG['enabled']: - logger.info("All Gemini retries exhausted, falling back to Imagen") - return _generate_imagen_images_base64(prompt, aspect_ratio) - return [] - - -def generate_gemini_image( - prompt, - keywords=None, - style=None, - focus=None, - enhance_prompt=True, - max_retries=2, - initial_retry_delay=1.0, - aspect_ratio="9:16", - enable_imagen_fallback=True, -): - """ - Backward-compatible wrapper that generates a single image file on disk and returns path. - Now includes Imagen fallback for improved reliability. - - Prefer generate_gemini_images_base64 in new code paths. - """ - logger = logging.getLogger('gemini_image_generator') - images = generate_gemini_images_base64( - prompt, - keywords=keywords, - style=style, - focus=focus, - enhance_prompt=enhance_prompt, - aspect_ratio=aspect_ratio, - max_retries=max_retries, - initial_retry_delay=initial_retry_delay, - enable_imagen_fallback=enable_imagen_fallback, - ) - if not images: - return None - - # Persist first image to file for legacy callers - img_b64 = images[0] - img_bytes = base64.b64decode(img_b64) - img = Image.open(BytesIO(img_bytes)) - - # Update filename to indicate which API was used - timestamp = datetime.datetime.now().strftime("%Y%m%d-%H%M%S") - if 'imagen' in prompt.lower() or 'fallback' in prompt.lower(): - out_name = f'imagen-fallback-image-{timestamp}.png' - else: - out_name = f'gemini-native-image-{timestamp}.png' - - try: - img.save(out_name) - # Also call save_generated_image to reuse existing pipeline - save_generated_image({"artifacts": [{"base64": img_b64}]}) - logger.info(f"βœ… Image saved successfully: {out_name}") - return out_name - except Exception as e: - logger.error(f"❌ Failed to save image: {e}") - return None - - -def edit_image(image_path, prompt, max_retries=2, initial_retry_delay=1.0): - """ - - Image editing (text and image to image) - Example prompt: "Edit this image to make it look like a cartoon" - Example prompt: [image of a cat] + [image of a pillow] + "Create a cross stitch of my cat on this pillow." - - - Multi-turn image editing (chat) - Example prompts: [upload an image of a blue car.] "Turn this car into a convertible." "Now change the color to yellow." - - Image editing with Gemini - To perform image editing, add an image as input. - The following example demonstrats uploading base64 encoded images. - For multiple images and larger payloads, check the image input section. - - Args: - image_path (str): The path to the image to edit. - prompt (str): The prompt to edit the image with. - max_retries (int, optional): Maximum number of retry attempts for handling 503 errors. Defaults to 3. - initial_retry_delay (int, optional): Initial delay in seconds before retrying. Defaults to 2. - - Returns: - str: The path to the edited image. - """ - import PIL.Image - image = PIL.Image.open(image_path) - - retry_count = 0 - retry_delay = initial_retry_delay - - while retry_count <= max_retries: - try: - client = _ensure_client() - if client is None: - return None - text_input = (prompt) - - logger.info("Sending request to Gemini API for image editing") - response = client.models.generate_content( - model="gemini-2.0-flash-exp-image-generation", - contents=[text_input, image], - config=types.GenerateContentConfig( - response_modalities=['Text', 'Image'] - ) - ) - logger.info("Received response from Gemini API for image editing") - - edited_img_name = None - for part in response.candidates[0].content.parts: - if getattr(part, 'inline_data', None) is not None: - logger.info("Received edited image data from Gemini") - edited_image = Image.open(BytesIO(part.inline_data.data)) - - # Save the edited image - edited_img_name = f'edited-{os.path.basename(image_path)}' - try: - logger.info(f"Saving edited image to: {edited_img_name}") - edited_image.save(edited_img_name) - - # Create a dictionary with the expected format for save_generated_image - img_response = { - "artifacts": [ - { - "base64": base64.b64encode(open(edited_img_name, "rb").read()).decode('utf-8') - } - ] - } - - # Call save_generated_image with the correct format - save_generated_image(img_response) - except Exception as err: - logger.error(f"Failed to save edited image: {err}") - - logger.info(f"Image editing completed. Edited image name: {edited_img_name}") - return edited_img_name - except Exception as err: - error_message = str(err) - logger.error(f"Error in edit_image: {err}") - # Retry on transient 503 - if "503" in error_message and retry_count < max_retries: - retry_count += 1 - logger.info(f"Retrying in {retry_delay} seconds (attempt {retry_count}/{max_retries})") - time.sleep(retry_delay) - # Exponential backoff - retry_delay *= 2 - else: - return None - # If we've exhausted all retries - return None - - diff --git a/backend/services/llm_providers/text_to_image_generation/gen_stabl_diff_img.py b/backend/services/llm_providers/text_to_image_generation/gen_stabl_diff_img.py deleted file mode 100644 index b6187f4c..00000000 --- a/backend/services/llm_providers/text_to_image_generation/gen_stabl_diff_img.py +++ /dev/null @@ -1,69 +0,0 @@ -# Ensure you sign up for an account to obtain an API key: -# https://platform.stability.ai/ -# Your API key can be found here after account creation: -# https://platform.stability.ai/account/keys - -import os -import requests -import base64 -from PIL import Image -from io import BytesIO -import streamlit as st -from loguru import logger - -# Import APIKeyManager -from ...onboarding.api_key_manager import APIKeyManager - -def save_generated_image(data): - """Save the generated image to a file.""" - # Implementation for saving image - pass - -def generate_stable_diffusion_image(prompt): - engine_id = "stable-diffusion-xl-1024-v1-0" - api_host = os.getenv('API_HOST', 'https://api.stability.ai') - - # Use APIKeyManager instead of direct environment variable access - api_key_manager = APIKeyManager() - api_key = api_key_manager.get_api_key("stability") - - if api_key is None: - st.warning("Missing Stability API key. Please configure it in the onboarding process.") - return None - - response = requests.post( - f"{api_host}/v1/generation/{engine_id}/text-to-image", - headers={ - "Content-Type": "application/json", - "Accept": "application/json", - "Authorization": f"Bearer {api_key}" - }, - json={ - "text_prompts": [ - { - "text": prompt - } - ], - "cfg_scale": 7, - "height": 1024, - "width": 1024, - "samples": 1, - "steps": 30, - }, - ) - - if response.status_code != 200: - raise Exception("Non-200 response: " + str(response.text)) - - data = response.json() - img_path = save_generated_image(data) - - for i, image in enumerate(data["artifacts"]): - # Decode base64 image data - img_data = base64.b64decode(image["base64"]) - # Open image using PIL - img = Image.open(BytesIO(img_data)) - # Display the image - img.show() - - return img_path diff --git a/backend/services/llm_providers/text_to_image_generation/gen_variation_img.py b/backend/services/llm_providers/text_to_image_generation/gen_variation_img.py deleted file mode 100644 index e0899b98..00000000 --- a/backend/services/llm_providers/text_to_image_generation/gen_variation_img.py +++ /dev/null @@ -1,51 +0,0 @@ -from loguru import logger -import sys -from PIL import Image -from openai import OpenAI - -def gen_new_from_given_img(img_path, image_dir, num_img=1, img_size="1024x1024", response_format="url"): - """ - Generates variations of a given image using OpenAI's image variation API. - - This function takes an existing image, processes it, and generates a specified number of new images based on it. - These generated images are variations of the original, providing creative flexibility. - - Args: - img_path (str): Path to the original image file. - image_dir (str): Directory where the generated images will be saved. - num_img (int, optional): Number of image variations to generate. Defaults to 1. - img_size (str, optional): Size of the generated images. Defaults to "1024x1024". - response_format (str, optional): Format in which the generated images are returned. Defaults to "url". - - Returns: - str: Path to the saved image variation. - - Raises: - SystemExit: If a critical error occurs that prevents successful execution. - """ - try: - logger.info(f"Starting image variation generation for: {img_path}") - - # Convert and prepare the image - png = Image.open(img_path).convert('RGBA') - background = Image.new('RGBA', png.size, (255, 255, 255)) - alpha_composite = Image.alpha_composite(background, png) - alpha_composite.save(img_path, 'PNG', quality=80) - logger.info("Image prepared for variation generation.") - - client = OpenAI() - variation_response = client.images.create_variation( - image=open(img_path, "rb", encoding="utf-8"), - n=num_img, - size=img_size, - response_format=response_format - ) - - # Saving the generated image - generated_image_path = save_generated_image(variation_response, image_dir) - logger.info(f"Image variation generated and saved to: {generated_image_path}") - return generated_image_path - - except Exception as e: - logger.error(f"Error occurred during image variation generation: {e}") - sys.exit(f"Exiting due to critical error: {e}") diff --git a/backend/services/llm_providers/text_to_image_generation/main_generate_image_from_prompt.py b/backend/services/llm_providers/text_to_image_generation/main_generate_image_from_prompt.py deleted file mode 100644 index bad107c8..00000000 --- a/backend/services/llm_providers/text_to_image_generation/main_generate_image_from_prompt.py +++ /dev/null @@ -1,162 +0,0 @@ -######################################################### -# -# This module will generate images for the blogs using APIs -# from Dall-E and other free resources. Given a prompt, the -# images will be stored in local directory. -# Required: openai API key. -# -######################################################### - -# imports -import os -import sys -import datetime -import streamlit as st - -import openai # OpenAI Python library to make API calls -from loguru import logger -from utils.logger_utils import get_service_logger - -# Use service-specific logger to avoid conflicts -logger = get_service_logger("text_to_image_generation") - -#from .gen_dali2_images -from .gen_dali3_images import generate_dalle3_images -from .gen_stabl_diff_img import generate_stable_diffusion_image -from ..text_generation.main_text_generation import llm_text_gen -from .gen_gemini_images import generate_gemini_image - -def generate_image(user_prompt, title=None, description=None, tags=None, content=None, aspect_ratio="16:9"): - """ - The generation API endpoint creates an image based on a text prompt. - - Required inputs: - prompt (str): A text description of the desired image(s). The maximum length is 1000 characters. - - Optional inputs: - --> image_engine: dalle2, dalle3, stable diffusion are supported. - --> num_images (int): The number of images to generate. Must be between 1 and 10. Defaults to 1. - --> size (str): The size of the generated images. Must be one of "256x256", "512x512", or "1024x1024". - Smaller images are faster. Defaults to "1024x1024". - -->response_format (str): The format in which the generated images are returned. - Must be one of "url" or "b64_json". Defaults to "url". - --> user (str): A unique identifier representing your end-user, which will help OpenAI to monitor and detect abuse. - --> aspect_ratio (str): The aspect ratio for the generated image. Must be one of "16:9", "4:3", or "1:1". Defaults to "16:9". - """ - # FIXME: Need to remove default value to match sidebar input. - image_engine = 'Gemini-AI' - image_stored_at = None - - if user_prompt: - try: - # Use enhanced prompt generator with all available parameters - img_prompt = generate_enhanced_img_prompt(user_prompt, title, description, tags, content) - - # Add aspect ratio to the prompt - if aspect_ratio: - img_prompt += f"\n\nAspect ratio: {aspect_ratio}" - - if 'Dalle3' in image_engine: - logger.info(f"Calling Dalle3 text-to-image with prompt: {img_prompt}") - image_stored_at = generate_dalle3_images(img_prompt) - elif 'Stability-AI' in image_engine: - logger.info(f"Calling Stable diffusion text-to-image with prompt: \n{img_prompt}") - image_stored_at = generate_stable_diffusion_image(img_prompt) - elif 'Gemini-AI' in image_engine: - logger.info(f"Calling Gemini text-to-image with prompt: \n{img_prompt}") - image_stored_at = generate_gemini_image(img_prompt, aspect_ratio=aspect_ratio) - return image_stored_at - except Exception as err: - logger.error(f"Failed to generate Image: {err}") - st.warning(f"Failed to generate Image: {err}") - else: - logger.error("Skipping Image creation, No prompt provided.") - - -def generate_img_prompt(user_prompt): - """ - Given prompt, this functions generated a prompt for image generation. - """ - prompt = f""" - As an expert prompt generator for AI text to image models and artist, I will provide you with 'user text' for creating images. - Your task is to create a prompt for a highly relevant image from given 'user text'. - \n - Choose from various art styles, utilize light & shadow effects etc. - Make sure to avoid common image generation mistakes. - Reply with only one answer, no descrition and in plaintext. - Make sure your prompt is detailed and creative descriptions that will inspire unique and interesting images from the AI. - - \n\nuser text: - '''{user_prompt}'''""" - - response = llm_text_gen(prompt) - return response - - -def generate_enhanced_img_prompt(user_prompt, title=None, description=None, tags=None, content=None): - """ - Given user prompt and additional context (title, description, tags, content), - this function generates an enhanced prompt for better image generation. - - Args: - user_prompt (str): Base prompt from the user - title (str, optional): Blog title or content title - description (str, optional): Blog or content description/summary - tags (list, optional): List of tags related to the content - content (str, optional): Actual content or excerpt - - Returns: - str: Enhanced prompt for image generation - """ - # Start with the base prompt - context_parts = [user_prompt] - - # Add relevant context if available - if title: - context_parts.append(f"Title: {title}") - - if description: - context_parts.append(f"Description: {description}") - - if tags and len(tags) > 0: - tag_text = ", ".join(tags[:5]) # Limit to 5 tags to avoid too much noise - context_parts.append(f"Tags: {tag_text}") - - # Create a combined context - combined_context = "\n".join(context_parts) - - # Add some content excerpt if available (limited to avoid token limits) - content_excerpt = "" - if content: - # Just use the first few hundred characters as excerpt - content_excerpt = content[:300] + "..." if len(content) > 300 else content - - # Create the prompt for LLM - prompt = f""" - As an expert prompt engineer for AI image generation models, create a detailed, creative prompt - for generating a high-quality, relevant image based on the following context: - - {combined_context} - - Additional content excerpt: - {content_excerpt} - - Your task is to: - 1. Analyze the context and content to understand the main theme and subject - 2. Create a rich, detailed prompt for image generation (50-75 words) - 3. Include specific visual details, art style, mood, lighting, composition - 4. Make sure the prompt is highly relevant to the original context - 5. Avoid prohibited content or anything that violates image generation guidelines - - Reply with ONLY the final prompt. No explanations or other text. - """ - - # Generate the enhanced prompt - try: - enhanced_prompt = llm_text_gen(prompt) - logger.info(f"Generated enhanced image prompt: {enhanced_prompt[:100]}...") - return enhanced_prompt - except Exception as e: - logger.error(f"Error generating enhanced prompt: {e}") - # Fall back to the simple prompt generation if enhanced fails - return generate_img_prompt(user_prompt) diff --git a/backend/services/llm_providers/text_to_image_generation/save_image.py b/backend/services/llm_providers/text_to_image_generation/save_image.py deleted file mode 100644 index 3aab258c..00000000 --- a/backend/services/llm_providers/text_to_image_generation/save_image.py +++ /dev/null @@ -1,39 +0,0 @@ -import base64 -import datetime -import os -import requests -from PIL import Image -import logging - -def save_generated_image(img_generation_response): - """ - Save generated images for blog, ensuring unique names for SEO. - """ - logging.basicConfig(level=logging.INFO) - logger = logging.getLogger(__name__) - - # Get image save directory with fallback to a local directory - image_save_dir = os.getenv('IMG_SAVE_DIR', 'generated_images') - - # Create the directory if it doesn't exist - if not os.path.exists(image_save_dir): - logger.info(f"Creating image save directory: {image_save_dir}") - os.makedirs(image_save_dir, exist_ok=True) - - generated_image_name = f"generated_image_{datetime.datetime.now():%Y-%m-%d-%H-%M-%S}.webp" - generated_image_filepath = os.path.join(image_save_dir, generated_image_name) - - try: - for i, image in enumerate(img_generation_response["artifacts"]): - with open(generated_image_filepath, "wb") as f: - f.write(base64.b64decode(image["base64"])) - except requests.exceptions.RequestException as e: - logger.error(f"Failed to get generated image content: {e}") - return None - except Exception as e: - logger.error(f"Error saving image: {e}") - return None - - logger.info(f"Saved image at path: {generated_image_filepath}") - - return generated_image_filepath diff --git a/backend/services/writing_assistant.py b/backend/services/writing_assistant.py index 514bf0a5..a3159592 100644 --- a/backend/services/writing_assistant.py +++ b/backend/services/writing_assistant.py @@ -1,16 +1,13 @@ import os import asyncio -import concurrent.futures from typing import Any, Dict, List from dataclasses import dataclass import requests from loguru import logger +import time +import random -try: - from google import genai - GOOGLE_GENAI_AVAILABLE = True -except Exception: - GOOGLE_GENAI_AVAILABLE = False +from services.llm_providers.main_text_generation import llm_text_gen @dataclass @@ -29,17 +26,10 @@ class WritingAssistantService: def __init__(self) -> None: self.exa_api_key = os.getenv("EXA_API_KEY") - self.gemini_api_key = os.getenv("GEMINI_API_KEY") if not self.exa_api_key: logger.warning("EXA_API_KEY not configured; writing assistant will fail") - if not (GOOGLE_GENAI_AVAILABLE and self.gemini_api_key): - logger.warning("Gemini not available; writing assistant will fail") - self.gemini_client = None - else: - self.gemini_client = genai.Client(api_key=self.gemini_api_key) - self.http_timeout_seconds = 15 # COST CONTROL: Daily usage limits @@ -151,9 +141,6 @@ class WritingAssistantService: raise async def _generate_continuation(self, text: str, sources: List[Dict[str, Any]]) -> tuple[str, float]: - if not self.gemini_client: - raise Exception("Gemini client not available") - # Build compact sources context block source_blocks: List[str] = [] for i, s in enumerate(sources[:5]): @@ -164,12 +151,12 @@ class WritingAssistantService: ) sources_text = "\n\n".join(source_blocks) if source_blocks else "(No sources)" - # Based on Exa demo guidance for completion-only behavior and inline citations + # Provider-agnostic behavior: short continuation with one inline citation hint system_prompt = ( - "You are an essay-completion bot that completes a sentence or continues prose. " + "You are an assistive writing continuation bot. " "Only produce 1-2 SHORT sentences. Do not repeat or paraphrase the user's stub. " - "Continue in the same tone and topic as the stub. Prefer concrete, current facts from the provided sources. " - "Include exactly one brief, verifiable citation hint in parentheses with an author (or 'Source') and URL in square brackets, e.g., ((Doe, 2021)[https://example.com])." + "Match tone and topic. Prefer concrete, current facts from the provided sources. " + "Include exactly one brief citation hint in parentheses with an author (or 'Source') and URL in square brackets, e.g., ((Doe, 2021)[https://example.com])." ) user_prompt = ( @@ -179,17 +166,20 @@ class WritingAssistantService: ) try: - loop = asyncio.get_event_loop() - with concurrent.futures.ThreadPoolExecutor() as executor: - resp = await loop.run_in_executor( - executor, - lambda: self.gemini_client.models.generate_content( - model="gemini-1.5-flash", contents=f"{system_prompt}\n\n{user_prompt}" - ), - ) - suggestion = (resp.text or "").strip() + # Inter-call jitter to reduce burst rate limits + time.sleep(random.uniform(0.05, 0.15)) + + ai_resp = llm_text_gen( + prompt=user_prompt, + json_struct=None, + system_prompt=system_prompt, + ) + if isinstance(ai_resp, dict) and ai_resp.get("text"): + suggestion = (ai_resp.get("text", "") or "").strip() + else: + suggestion = (str(ai_resp or "")).strip() if not suggestion: - raise Exception("Gemini returned empty suggestion") + raise Exception("Assistive writer returned empty suggestion") # naive confidence from number of sources present confidence = 0.7 if sources else 0.5 return suggestion, confidence diff --git a/docs-site/docs/features/blog-writer/overview.md b/docs-site/docs/features/blog-writer/overview.md index 051e8608..991199c9 100644 --- a/docs-site/docs/features/blog-writer/overview.md +++ b/docs-site/docs/features/blog-writer/overview.md @@ -24,27 +24,100 @@ The ALwrity Blog Writer is a powerful AI-driven content creation tool that helps ## How It Works -### Simple 4-Step Process +### Complete 6-Phase Workflow -1. **Research Your Topic** - Enter your topic and keywords, then let AI research the latest information -2. **Create an Outline** - AI generates a content outline that you can customize and refine -3. **Write Section by Section** - Generate content for each section using AI, then edit as needed -4. **Optimize and Publish** - Review SEO suggestions, make final edits, and publish your content +ALwrity Blog Writer transforms your ideas into publish-ready content through a sophisticated, AI-powered workflow that ensures quality, accuracy, and SEO optimization at every step. + +```mermaid +flowchart TD + A[Start: Keywords & Topic] --> B[Phase 1: Research & Strategy] + B --> C[Phase 2: Intelligent Outline] + C --> D[Phase 3: Content Generation] + D --> E[Phase 4: SEO Analysis] + E --> F[Phase 5: SEO Metadata] + F --> G[Phase 6: Publish & Distribute] + + B --> B1[Google Search Grounding] + B --> B2[Competitor Analysis] + B --> B3[Keyword Intelligence] + + C --> C1[AI Outline Generation] + C --> C2[Source Mapping] + C --> C3[Title Generation] + + D --> D1[Section-by-Section Writing] + D --> D2[Context Memory] + D --> D3[Flow Analysis] + + E --> E1[SEO Scoring] + E --> E2[Actionable Recommendations] + E --> E3[AI-Powered Refinement] + + F --> F1[Comprehensive Metadata] + F --> F2[Open Graph & Twitter Cards] + F --> F3[Schema.org Markup] + + G --> G1[Multi-Platform Publishing] + G --> G2[Scheduling] + G --> G3[Version Management] + + style A fill:#e3f2fd + style B fill:#e8f5e8 + style C fill:#fff3e0 + style D fill:#fce4ec + style E fill:#f1f8e9 + style F fill:#e0f2f1 + style G fill:#f3e5f5 +``` + +#### Phase 1: Research & Strategy +AI-powered comprehensive research with Google Search grounding, competitor analysis, and keyword intelligence. + +#### Phase 2: Intelligent Outline +AI-generated outlines with source mapping, grounding insights, and optimization recommendations. + +#### Phase 3: Content Generation +Section-by-section content generation with SEO optimization, context memory, and engagement improvements. + +#### Phase 4: SEO Analysis +Advanced SEO analysis with actionable recommendations and AI-powered optimization. + +#### Phase 5: SEO Metadata +Optimized metadata generation for titles, descriptions, Open Graph, Twitter Cards, and structured data. + +#### Phase 6: Publish & Distribute +Direct publishing to WordPress, Wix, Medium, and other platforms with scheduling capabilities. + +### Phase Features At a Glance + +| Phase | Key Features | Target Benefits | Best For | +|-------|-------------|-----------------|----------| +| **Phase 1: Research** | Google Search grounding, Competitor analysis, Keyword intelligence, Content angles | Comprehensive data, Time savings, Market insights | All content creators | +| **Phase 2: Outline** | AI generation, Source mapping, Interactive refinement, Title suggestions | Structured content, SEO foundation, Editorial flexibility | Professional writers | +| **Phase 3: Content** | Context-aware writing, Flow analysis, Source integration, Medium mode | High quality, Consistency, Citation accuracy | Content teams | +| **Phase 4: SEO** | Multi-dimensional scoring, Actionable recommendations, AI refinement | Search visibility, Competitive edge, Performance tracking | SEO professionals | +| **Phase 5: Metadata** | Comprehensive SEO tags, Social optimization, Schema markup, Multi-format export | Complete optimization, Rich snippets, Cross-platform readiness | Digital marketers | +| **Phase 6: Publish** | Multi-platform support, Scheduling, Version management, Analytics integration | Efficiency, Strategic timing, Quality control | Solopreneurs & teams | ### What Happens Behind the Scenes -- **Research Phase**: AI searches the web for current information and sources -- **Outline Generation**: Creates a logical structure with headings and key points -- **Content Writing**: Generates engaging, informative content for each section -- **Quality Checks**: Runs fact-checking and SEO analysis automatically -- **Publishing**: Formats content for your chosen platform +The Blog Writer leverages sophisticated AI orchestration to ensure quality at every stage: + +- **Research Phase**: AI searches the web using Gemini's native Google Search integration for current, credible information and sources +- **Outline Generation**: Creates logical structure with headings, key points, and source mapping using parallel processing +- **Content Writing**: Generates engaging, context-aware content for each section with continuity tracking and flow analysis +- **SEO Optimization**: Runs comprehensive analysis with parallel non-AI analyzers plus AI insights for actionable recommendations +- **Metadata Generation**: Creates complete SEO metadata package with social media optimization in 2 AI calls maximum +- **Publishing**: Formats content for your chosen platform with scheduling and version management ### User-Friendly Features -- **Progress Tracking**: See real-time progress for research and writing tasks -- **Visual Editor**: Edit content with an easy-to-use WYSIWYG interface -- **Title Suggestions**: Choose from AI-generated title options -- **SEO Integration**: Get SEO suggestions as you write +- **Progress Tracking**: See real-time progress for all long-running tasks with detailed status updates +- **Visual Editor**: Easy-to-use WYSIWYG interface with markdown support and live preview +- **Title Suggestions**: Multiple AI-generated, SEO-scored title options to choose from +- **SEO Integration**: Comprehensive analysis with one-click "Apply Recommendations" for instant optimization +- **Context Memory**: Intelligent continuity tracking across sections for consistent, flowing content +- **Source Attribution**: Automatic citation integration with research source mapping ## Content Types @@ -130,30 +203,124 @@ The ALwrity Blog Writer is a powerful AI-driven content creation tool that helps ## Advanced Features -### Content Templates -- **Industry-specific**: Tailored templates -- **Content Types**: Various formats -- **Brand Guidelines**: Consistent styling -- **Custom Templates**: Personalized formats +### ✨ Assistive Writing & Quick Edits +- **Continue Writing**: AI-powered contextual suggestions as you type +- **Smart Typing Assist**: Automatic suggestions after 20+ words +- **Quick Edit Options**: Improve, expand, shorten, professionalize, add transitions, add data +- **Real-time Assistance**: Instant writing help without interrupting your flow +- **Cost-Optimized**: First suggestion automatic, then manual "Continue Writing" for efficiency +- **One-Click Improvements**: Select text and apply quick edits instantly -### Collaboration Tools -- **Team Editing**: Multiple contributors -- **Version Control**: Content history -- **Comments**: Feedback system -- **Approval Workflow**: Review process +### πŸ” Fact-Checking & Quality Assurance +- **Hallucination Detection**: AI-powered verification of claims and facts +- **Source Verification**: Automatic cross-checking against research sources +- **Claim Analysis**: Detailed assessment of each verifiable statement +- **Evidence Support**: Links to supporting or refuting sources +- **Quality Scoring**: Overall confidence metrics for content accuracy -### Automation -- **Scheduled Publishing**: Automated posting -- **Content Calendar**: Planning tools -- **Social Sharing**: Auto-distribution -- **Performance Monitoring**: Analytics tracking +### πŸ–ΌοΈ Image Generation +- **Section-Specific Images**: Generate images per blog section from the outline +- **AI-Powered Prompts**: Auto-suggest images based on section content +- **Advanced Options**: Stability AI, Hugging Face, Gemini +- **Blog Optimization**: Sizes and formats for platform publishing +- **Integrated Workflow**: Generate inside the outline editor + +### πŸ“ SEO Metadata Generation +- **Comprehensive Package**: Title, description, tags, categories, hashtags in 2 AI calls +- **Social Optimization**: Open Graph & Twitter Cards +- **Structured Data**: Schema.org JSON-LD for rich snippets +- **Multi-Format Export**: WordPress, Wix, HTML, JSON-LD +- **Live Preview**: Google, Facebook, Twitter + +### Automation & Integration +- **Multi-Platform Publishing**: One-click to WordPress, Wix, Medium +- **Version Management**: Track changes and revisions +- **Scheduled Publishing**: Set future publish dates +- **Google Analytics Integration**: Track content performance +- **Search Console**: Monitor search visibility + +## Who Benefits Most + +### For Technical Content Writers +- **Research Automation**: Save hours of manual research with AI-powered Google Search grounding +- **Source Attribution**: Automatic citation management and credibility scoring +- **Quality Assurance**: Built-in fact-checking and hallucination detection +- **Citation Integration**: Seamless source references throughout content + +### For Solopreneurs +- **Time Efficiency**: Complete blog creation workflow in minutes instead of hours +- **SEO Expertise**: Professional-grade optimization without hiring specialists +- **Multi-Platform Publishing**: One workflow, multiple destinations (WordPress, Wix, Medium) +- **Scheduling & Automation**: Strategic content distribution and timing optimization + +### For Digital Marketing & SEO Professionals +- **Comprehensive SEO**: Multi-dimensional scoring with actionable insights +- **Competitive Intelligence**: AI-powered competitor analysis and content gap identification +- **Performance Tracking**: Integration with Google Analytics and Search Console +- **ROI Optimization**: Data-driven content strategy and performance analytics + +## How to Use Advanced Features + +### Using Assistive Writing (Continue Writing) +```mermaid +flowchart LR + A[Start Typing] -->|20+ words| B[Auto Suggestion] + B --> C{Accept or Reject?} + C -->|Accept| D[Suggestion Inserted] + C -->|Reject| E[Dismiss Suggestion] + D --> F[Continue Writing Button] + E --> F + F -->|Click| G[Manual Suggestion] + + style A fill:#e3f2fd + style B fill:#e8f5e8 + style G fill:#fff3e0 +``` + +**Quick Steps - Continue Writing:** +1. Type 20+ words in any blog section +2. First suggestion appears automatically below your text +3. Click **"Accept"** to insert or **"Dismiss"** to skip +4. Use **"✍️ Continue Writing"** for more suggestions +5. Suggestions include source citations for fact-checking + +**Quick Steps - Text Selection Edits:** +1. Select any text in your content +2. Context menu appears automatically +3. Choose quick edit: **Improve**, **Expand**, **Shorten**, **Professionalize**, **Add Transition**, or **Add Data** +4. Text updates instantly with your selected improvement + +### Using Fact-Checking +1. Select a paragraph or claim in your blog content +2. Right-click to open context menu +3. Click **"πŸ” Fact Check"** +4. Wait 15-30 seconds for analysis +5. Review results: claims, confidence, supporting/refuting sources +6. Click **"Apply Fix"** to insert source links + +### Using Image Generation +1. In **Phase 2: Intelligent Outline**, click **"πŸ–ΌοΈ Generate Image"** on any section +2. Modal opens with auto-generated prompt (editable) +3. Click **"Suggest Prompt"** for AI-optimized suggestions +4. Optionally open **"Advanced Image Options"** +5. Generate image (Stability AI, Hugging Face, or Gemini) +6. Image auto-inserts into outline and metadata + +### Using SEO Metadata Generation +1. In **Phase 5: SEO Metadata**, open the modal +2. Click **"Generate All Metadata"** (max 2 AI calls) +3. Review tabs: Preview, Core, Social, Structured Data +4. Edit any field; previews update live +5. Copy formats for WordPress, Wix, or custom +6. Images from Phase 2 auto-fill Open Graph ## Getting Started -1. **[Research Integration](research.md)** - Set up automated research -2. **[SEO Analysis](seo-analysis.md)** - Configure SEO optimization -3. **[Implementation Spec](implementation-spec.md)** - Technical details -4. **[Best Practices](../../guides/best-practices.md)** - Optimization tips +1. **[Research Integration](research.md)** - Comprehensive Phase 1 research capabilities +2. **[Workflow Guide](workflow-guide.md)** - Step-by-step 6-phase workflow walkthrough +3. **[SEO Analysis](seo-analysis.md)** - Phase 4 & 5 optimization strategies +4. **[Implementation Spec](implementation-spec.md)** - Technical architecture and API details +5. **[Best Practices](../../guides/best-practices.md)** - Advanced optimization tips ## Related Features diff --git a/docs-site/docs/features/blog-writer/research.md b/docs-site/docs/features/blog-writer/research.md index ca714417..0518b5b6 100644 --- a/docs-site/docs/features/blog-writer/research.md +++ b/docs-site/docs/features/blog-writer/research.md @@ -1,18 +1,44 @@ -# Research Integration +# Phase 1: Research & Strategy -ALwrity's Blog Writer includes powerful research integration capabilities that automatically gather, analyze, and verify information to create well-researched, accurate, and comprehensive blog content. +ALwrity's Blog Writer Phase 1 provides powerful AI-powered research capabilities that automatically gather, analyze, and verify information to create well-researched, accurate, and comprehensive blog content. This foundation phase sets the stage for all subsequent content creation. -## What is Research Integration? +## Overview -Research Integration is an AI-powered feature that automatically conducts comprehensive research on your chosen topic, gathering information from multiple sources, verifying facts, and organizing insights to support your content creation process. +Phase 1: Research & Strategy leverages Gemini's native Google Search grounding to conduct comprehensive topic research in a single API call, delivering competitor intelligence, keyword analysis, and content angles to inform your entire blog creation process. ### Key Benefits -- **Comprehensive Research**: Gather information from multiple reliable sources -- **Fact Verification**: Verify claims and statistics automatically -- **Source Attribution**: Provide proper citations and references -- **Trend Analysis**: Identify current trends and developments -- **Competitive Intelligence**: Analyze competitor content and strategies +- **Comprehensive Research**: Gather information from multiple reliable sources with Google Search grounding +- **Competitive Intelligence**: Identify content gaps and opportunities through competitor analysis +- **Keyword Intelligence**: Discover primary, secondary, and long-tail keyword opportunities +- **Content Angles**: AI-generated unique content angles for maximum engagement +- **Time Efficiency**: Complete research in 30-60 seconds with intelligent caching + +## Research Data Flow + +```mermaid +flowchart LR + A[User Input:
Keywords + Topic] --> B[Phase 1: Research] + B --> C{Cache Check} + C -->|Hit| D[Return Cached
Research] + C -->|Miss| E[Google Search
Grounding] + E --> F[Source Extraction] + F --> G[Keyword Analysis] + F --> H[Competitor Analysis] + F --> I[Content Angle
Generation] + G --> J[Research Output] + H --> J + I --> J + D --> J + + J --> K[Cache Storage] + J --> L[Phase 2: Outline] + + style B fill:#e8f5e8 + style E fill:#fff3e0 + style J fill:#e3f2fd + style L fill:#fff3e0 +``` ## Research Process @@ -36,299 +62,325 @@ Research Integration is an AI-powered feature that automatically conducts compre } ``` -### 2. Multi-Source Research +### 2. Google Search Grounding (Gemini Integration) -#### Web Research -- **Search Engines**: Google, Bing, and specialized search engines -- **News Sources**: Current news and industry updates -- **Blogs and Articles**: Industry blogs and expert articles -- **Forums and Communities**: Reddit, Quora, and professional forums -- **Social Media**: Twitter, LinkedIn, and industry discussions +Phase 1 leverages Gemini's native Google Search grounding to access real-time web data with a single API call, eliminating the need for complex multi-source integrations. -#### Academic Sources -- **Research Papers**: Academic journals and research publications -- **Studies and Reports**: Industry studies and market research -- **White Papers**: Technical and business white papers -- **Case Studies**: Real-world examples and case studies -- **Expert Opinions**: Industry expert insights and analysis +#### Single API Call Efficiency +- **One Request**: Comprehensive research in a single Gemini API call with Google Search grounding +- **Live Web Data**: Real-time access to current information from the web +- **No Multi-Source Setup**: Eliminates need for multiple API integrations +- **Cost Effective**: Optimized token usage with focused research prompts +- **Caching Intelligence**: Automatic cache storage for repeat keyword research -#### Industry Sources +#### Research Sources (via Google Search) +The research prompt instructs Gemini to gather information from: +- **Current News**: Latest industry news and developments (2024-2025) - **Industry Reports**: Market research and industry analysis -- **Company Publications**: Official company blogs and reports -- **Professional Networks**: LinkedIn articles and professional content -- **Trade Publications**: Industry-specific magazines and journals -- **Conference Materials**: Industry conference presentations and papers +- **Expert Articles**: Authoritative blogs and professional content +- **Academic Sources**: Research papers and studies +- **Case Studies**: Real-world examples and implementations +- **Statistics**: Key data points and numerical insights +- **Trends**: Current market trends and forecasts -### 3. Information Processing +#### Google Search Grounding Example +```python +research_prompt = """ +Research the topic "AI in Digital Marketing" in the technology industry for digital marketers. -#### Data Collection -- **Content Extraction**: Extract relevant information from sources -- **Fact Identification**: Identify key facts, statistics, and claims -- **Quote Collection**: Gather relevant quotes and expert opinions -- **Trend Identification**: Identify current trends and patterns -- **Gap Analysis**: Find information gaps and opportunities +Provide comprehensive analysis including: +1. Current trends and insights (2024-2025) +2. Key statistics and data points with sources +3. Industry expert opinions and quotes +4. Recent developments and news +5. Market analysis and forecasts +6. Best practices and case studies +7. Keyword analysis: primary, secondary, and long-tail opportunities +8. Competitor analysis: top players and content gaps +9. Content angle suggestions: 5 compelling angles for blog posts -#### Information Verification -- **Fact Checking**: Verify facts against multiple sources -- **Source Credibility**: Assess source authority and reliability -- **Date Verification**: Ensure information is current and relevant -- **Bias Detection**: Identify potential bias in sources -- **Cross-Reference**: Cross-reference information across sources +Focus on factual, up-to-date information from credible sources. +""" +``` -## Research Features +### 3. Competitor Analysis -### Real-Time Research +The research phase automatically identifies competing content and discovers content gaps where your blog can stand out. -#### Live Data Access -- **Current Information**: Access to real-time data and updates -- **Trend Monitoring**: Track current trends and developments -- **News Integration**: Include latest news and updates -- **Social Media Monitoring**: Track social media discussions -- **Market Data**: Access current market information +#### Content Gap Identification +- **Top Competitors**: Identifies the most authoritative content on your topic +- **Coverage Analysis**: Maps what competitors have covered thoroughly vs. superficially +- **Gap Opportunities**: Highlights underexplored angles and missing information +- **Unique Positioning**: Suggests how to differentiate your content +- **Competitive Advantages**: Identifies areas where you can exceed competitor quality -#### Dynamic Updates -- **Content Freshness**: Ensure content includes latest information -- **Trend Integration**: Incorporate current trends and developments -- **News Relevance**: Include relevant recent news -- **Market Updates**: Include current market conditions -- **Expert Insights**: Access latest expert opinions +#### Competitive Intelligence +- **Content Depth**: Analyzes how thoroughly competitors cover topics +- **Keyword Usage**: Identifies keyword strategies in competitor content +- **Content Structure**: Evaluates how competitors organize information +- **Engagement Patterns**: Notes what formats and angles work best +- **Market Positioning**: Understands where competitors sit in the market -### Source Verification +### 4. Keyword Intelligence -#### Credibility Assessment -- **Domain Authority**: Check website authority and credibility -- **Author Credentials**: Verify author expertise and credentials -- **Publication Standards**: Assess publication quality and standards -- **Peer Review**: Check for peer review and validation -- **Fact-Checking**: Cross-reference with fact-checking organizations +Phase 1 provides comprehensive keyword analysis to optimize your content for search engines. -#### Source Diversity -- **Multiple Perspectives**: Include diverse viewpoints and opinions -- **Source Types**: Mix different types of sources -- **Geographic Diversity**: Include international sources -- **Temporal Range**: Include both recent and historical sources -- **Expertise Levels**: Include both expert and general sources +#### Primary, Secondary & Long-Tail Keywords +- **Primary Keywords**: Main topic keywords with highest search volume +- **Secondary Keywords**: Supporting terms that reinforce the main topic +- **Long-Tail Keywords**: Specific, less competitive phrases with high intent +- **Semantic Keywords**: Related terms that search engines associate with your topic +- **Search Intent**: Categorizes keywords by intent (informational, transactional, navigational) -### Fact Checking +#### Keyword Clustering & Grouping +- **Topic Clusters**: Groups related keywords for comprehensive coverage +- **Thematic Organization**: Organizes keywords by content themes +- **Density Recommendations**: Suggests optimal keyword usage throughout content +- **Priority Ranking**: Identifies which keywords to prioritize +- **Competition Analysis**: Assesses difficulty for ranking on each keyword -#### Automated Verification -- **Claim Verification**: Verify specific claims and statements -- **Statistical Validation**: Check statistics and numerical data -- **Quote Verification**: Verify quotes and attributions -- **Date Accuracy**: Ensure dates and timelines are correct -- **Context Validation**: Verify context and interpretation +### 5. Content Angle Generation -#### Manual Review -- **Expert Review**: Human expert review of critical information -- **Quality Assurance**: Manual quality checks and validation -- **Bias Assessment**: Human assessment of potential bias -- **Context Analysis**: Human analysis of context and interpretation -- **Final Validation**: Final human validation of research quality +AI generates unique content angles that make your blog stand out and engage your audience. -## Research Output +#### AI-Generated Angle Suggestions +- **5 Unique Angles**: Provides multiple distinct approaches to your topic +- **Trending Topics**: Identifies currently popular angles and discussions +- **Audience Pain Points**: Maps audience challenges to content angles +- **Viral Potential**: Assesses which angles have high shareability +- **Expert Opinions**: Synthesizes industry expert viewpoints into angles -### Organized Information +#### Content Angle Example +For a topic like "AI in Marketing," research might suggest: +1. **Case Study Angle**: "10 Marketing Agencies Using AI to Double ROI" +2. **Practical Guide Angle**: "Implementing AI Marketing Tools in 2025: A Step-by-Step Roadmap" +3. **Trend Analysis Angle**: "The Future of AI Marketing: What Industry Leaders Predict" +4. **Problem-Solution Angle**: "Common AI Marketing Failures and How to Avoid Them" +5. **Debunking Angle**: "AI Marketing Myths Debunked: What Actually Works in 2025" -#### Structured Data -- **Key Facts**: Organized list of key facts and information -- **Statistics**: Relevant statistics and numerical data -- **Quotes**: Expert quotes and opinions -- **Trends**: Current trends and developments -- **Sources**: Complete source list with citations +### 6. Information Processing -#### Research Summary -- **Executive Summary**: High-level overview of research findings -- **Key Insights**: Main insights and discoveries -- **Trend Analysis**: Analysis of current trends -- **Gap Identification**: Information gaps and opportunities -- **Recommendations**: Research-based recommendations +#### Data Collection & Extraction +- **Source Extraction**: Automatically extracts 10-20 credible sources from Google Search +- **Fact Identification**: Identifies key facts, statistics, and claims with citations +- **Quote Collection**: Gathers relevant expert quotes with attribution +- **Trend Identification**: Highlights current trends and patterns +- **Search Query Tracking**: Tracks AI-generated search queries for transparency -### Source Citations +#### Source Credibility & Verification +- **Automatic Citation**: Extracts source URLs, titles, and metadata for proper attribution +- **Grounding Metadata**: Includes detailed grounding support scores and chunks +- **Source Diversity**: Ensures mix of authoritative sources (academic, industry, news) +- **Credibility Scoring**: Evaluates source authority and reliability +- **Cross-Reference**: Cross-references key facts across multiple sources -#### Citation Format -- **APA Style**: Academic citation format -- **MLA Style**: Modern Language Association format -- **Chicago Style**: Chicago Manual of Style format -- **Custom Format**: Customizable citation format -- **Hyperlinks**: Direct links to source materials +## Research Output Structure -#### Source Information -- **Author Details**: Author name, credentials, and affiliation -- **Publication Information**: Publication name, date, and details -- **URL and Access**: Direct links and access information -- **Credibility Score**: Source credibility assessment -- **Last Updated**: Last update or verification date +### Comprehensive Research Results -## Integration with Content Creation +Phase 1 returns a complete research package that feeds into all subsequent phases: -### Content Planning +#### Structured Data Package +- **Sources**: 10-20 credible research sources with full metadata +- **Keyword Analysis**: Primary, secondary, long-tail, and semantic keywords +- **Competitor Analysis**: Top competing content and identified gaps +- **Content Angles**: 5 unique, AI-generated content approaches +- **Search Queries**: AI-generated search terms for transparency +- **Grounding Metadata**: Detailed grounding support scores and chunks -#### Research-Informed Planning -- **Topic Development**: Develop topics based on research insights -- **Content Structure**: Structure content based on research findings -- **Key Points**: Identify key points from research -- **Supporting Evidence**: Gather supporting evidence and examples -- **Expert Opinions**: Include relevant expert opinions +#### Research Summary Example +```json +{ + "success": true, + "sources": [ + { + "url": "https://example.com/research", + "title": "AI Marketing Trends 2025", + "credibility_score": 0.92 + } + ], + "keyword_analysis": { + "primary": ["AI marketing", "artificial intelligence digital marketing"], + "secondary": ["machine learning marketing", "automated advertising"], + "long_tail": ["how to implement AI marketing tools"], + "search_intent": "informational" + }, + "competitor_analysis": { + "top_competitors": [...], + "content_gaps": ["practical implementation guides", "cost-benefit analysis"] + }, + "suggested_angles": [ + "10 Marketing Agencies Using AI to Double ROI", + "Implementing AI Marketing Tools: A Step-by-Step Roadmap" + ] +} +``` -#### Content Strategy -- **Audience Insights**: Understand audience based on research -- **Competitive Analysis**: Analyze competitor content and strategies -- **Trend Integration**: Incorporate current trends and developments -- **Gap Opportunities**: Identify content gaps and opportunities -- **Value Proposition**: Develop unique value propositions +## Use Cases for Different Audiences -### Content Enhancement +### For Technical Content Writers +**Scenario**: Writing a technical deep-dive on "React Performance Optimization" -#### Evidence-Based Content -- **Factual Accuracy**: Ensure all facts are accurate and verified -- **Statistical Support**: Support claims with relevant statistics -- **Expert Validation**: Include expert opinions and validation -- **Case Studies**: Include relevant case studies and examples -- **Trend Analysis**: Incorporate current trend analysis +**Phase 1 Delivers**: +- Latest React documentation updates and best practices +- GitHub discussions and Stack Overflow solutions for optimization challenges +- Academic research on frontend performance optimization +- Real-world case studies from major tech companies +- Technical keyword opportunities: "React performance hooks", "memoization strategies" -#### Credibility Building -- **Source Attribution**: Proper attribution of all sources -- **Expert Quotes**: Include relevant expert quotes -- **Data Visualization**: Present data in clear, visual formats -- **Transparency**: Show research process and methodology -- **Quality Assurance**: Maintain high quality standards +**Value**: Eliminates hours of manual research across GitHub, documentation, and forums -## Research Tools and Sources +### For Solopreneurs +**Scenario**: Creating content on "Starting an E-commerce Business in 2025" -### Web Research Tools +**Phase 1 Delivers**: +- Current e-commerce market trends and statistics +- Competitor analysis of top e-commerce success stories +- Content gap: most content focuses on "how to start" but lacks "common pitfalls" +- Unique angle: "The 5 Mistakes That Kill 90% of New E-commerce Businesses" +- Long-tail keywords: "start ecommerce business 2025", "ecommerce business ideas" -#### Search Engines -- **Google Search**: Comprehensive web search -- **Bing Search**: Alternative search engine -- **DuckDuckGo**: Privacy-focused search -- **Specialized Search**: Industry-specific search engines -- **Academic Search**: Academic and research databases +**Value**: Provides business intelligence without expensive consultants -#### Research Platforms -- **Google Scholar**: Academic research and papers -- **ResearchGate**: Academic network and research -- **JSTOR**: Academic journal database -- **PubMed**: Medical and scientific research -- **IEEE Xplore**: Technical and engineering research +### For Digital Marketing & SEO Professionals +**Scenario**: Content strategy for "Local SEO Best Practices" -### Industry Sources +**Phase 1 Delivers**: +- Competitor analysis of top-ranking local SEO content +- Keyword gaps: competitors missing "Google Business Profile optimization" +- Trending angles: "Voice search local optimization" and "AI-powered local listings" +- Data-backed insights: "73% of local searches result in store visits" +- Content opportunity: "Local SEO Audit Template" (high search, low competition) -#### Market Research -- **Statista**: Statistical data and market research -- **IBISWorld**: Industry research and analysis -- **McKinsey**: Business and industry insights -- **Deloitte**: Professional services research -- **PwC**: Business and industry analysis +**Value**: Delivers competitive intelligence and keyword strategy in one research pass -#### News and Media -- **Reuters**: International news and analysis -- **Bloomberg**: Business and financial news -- **TechCrunch**: Technology news and analysis -- **Harvard Business Review**: Business insights and analysis -- **MIT Technology Review**: Technology and innovation news +## Performance & Caching + +### Intelligent Caching System + +Phase 1 implements a dual-layer caching strategy to optimize performance and reduce costs. + +#### Cache Storage +- **Persistent Cache**: SQLite database stores research results for exact keyword matches +- **Memory Cache**: In-process cache for faster repeated access within a session +- **Cache Key**: Based on exact keyword match, industry, and target audience +- **Cache Duration**: Results stored indefinitely until invalidated + +#### Cache Benefits +- **Cost Reduction**: Avoids redundant API calls for same topics +- **Speed**: Instant results for cached research (0-5 seconds vs. 30-60 seconds) +- **Consistency**: Ensures reproducible research results for same queries +- **Transparency**: Progress messages indicate cache hits: "βœ… Using cached research" + +### Performance Metrics + +**Typical Research Timing**: +- **Cache Hit**: 0-5 seconds (instant return) +- **Fresh Research**: 30-60 seconds (Google Search + AI processing) +- **Sources Found**: 10-20 credible sources per research +- **Search Queries**: 5-10 AI-generated search terms tracked ## Best Practices -### Research Quality +### Effective Research Setup -#### Source Selection -1. **Authority**: Choose authoritative and credible sources -2. **Recency**: Prefer recent and up-to-date information -3. **Relevance**: Ensure sources are relevant to your topic -4. **Diversity**: Include diverse perspectives and sources -5. **Verification**: Cross-reference information across sources +#### Keyword Strategy +1. **Be Specific**: Use 3-5 focused keywords rather than broad topics +2. **Industry Context**: Always specify industry for better context +3. **Audience Definition**: Define target audience clearly for tailored research +4. **Topic Clarity**: Provide a clear, concise topic description +5. **Word Count Target**: Set realistic word count goals (1000-3000 words optimal) -#### Information Processing -1. **Accuracy**: Verify all facts and claims -2. **Context**: Understand context and interpretation -3. **Bias Awareness**: Be aware of potential bias -4. **Completeness**: Ensure comprehensive coverage -5. **Quality**: Maintain high quality standards +#### Research Quality Optimization +1. **Review Sources**: Always review the returned sources for credibility +2. **Use Content Angles**: Leverage AI-generated angles for unique positioning +3. **Explore Competitor Gaps**: Focus on content gaps for competitive advantage +4. **Keyword Variety**: Review all keyword types (primary, secondary, long-tail) +5. **Leverage Caching**: Reuse research for related topics to save time and cost -### Content Integration +### Research-to-Content Pipeline -#### Research Application -1. **Relevance**: Use research that's relevant to your audience -2. **Balance**: Balance different perspectives and opinions -3. **Clarity**: Present research findings clearly -4. **Attribution**: Properly attribute all sources -5. **Value**: Add value through research insights +#### Phase 1 to Phase 2 Transition +1. **Validate Research**: Ensure research has 10+ credible sources before proceeding +2. **Review Angles**: Select compelling content angles for outline inspiration +3. **Check Keywords**: Verify keyword analysis aligns with your SEO goals +4. **Analyze Gaps**: Use competitor analysis to inform unique content positioning +5. **Source Quality**: Confirm grounding metadata shows high credibility scores (0.8+) -#### Quality Assurance -1. **Fact Checking**: Verify all facts and claims -2. **Source Review**: Review and validate all sources -3. **Expert Input**: Seek expert input when needed -4. **Peer Review**: Get peer review of research quality -5. **Continuous Improvement**: Continuously improve research process - -## Advanced Features - -### Custom Research - -#### Research Parameters -- **Custom Sources**: Specify custom source preferences -- **Research Depth**: Adjust research depth and scope -- **Language Settings**: Set research language preferences -- **Date Ranges**: Specify date ranges for research -- **Geographic Focus**: Set geographic focus for research - -#### Research Filters -- **Source Types**: Filter by source types and categories -- **Credibility Thresholds**: Set minimum credibility requirements -- **Date Filters**: Filter by publication date -- **Language Filters**: Filter by language -- **Topic Filters**: Filter by topic relevance - -### Research Analytics - -#### Performance Tracking -- **Research Quality**: Track research quality metrics -- **Source Performance**: Monitor source performance -- **Accuracy Rates**: Track fact-checking accuracy -- **User Satisfaction**: Monitor user satisfaction with research -- **Improvement Areas**: Identify areas for improvement - -#### Research Insights -- **Trend Analysis**: Analyze research trends and patterns -- **Source Analysis**: Analyze source performance and quality -- **Content Impact**: Measure impact of research on content -- **Audience Engagement**: Track audience engagement with research -- **ROI Analysis**: Analyze return on research investment +#### Research Output Utilization +1. **Source Mapping**: Use sources strategically across different sections +2. **Keyword Integration**: Naturally integrate primary and secondary keywords +3. **Angles to Sections**: Transform content angles into distinct content sections +4. **Gaps to Value**: Convert content gaps into unique selling propositions +5. **Trend Integration**: Weave current trends naturally throughout content ## Troubleshooting -### Common Issues +### Common Issues & Solutions -#### Research Quality -- **Insufficient Sources**: Add more diverse sources -- **Outdated Information**: Update research with current information -- **Bias Detection**: Address potential bias in sources -- **Fact Verification**: Improve fact-checking process -- **Source Credibility**: Improve source selection criteria +#### Low-Quality Research Results +**Problem**: Research returns fewer than 10 sources or low credibility scores -#### Technical Issues -- **API Connectivity**: Resolve API connection issues -- **Data Processing**: Fix data processing problems -- **Source Access**: Resolve source access issues -- **Performance Issues**: Address performance concerns -- **Integration Problems**: Fix integration issues +**Solutions**: +- **Refine Keywords**: Use more specific, focused keywords +- **Expand Topic**: Broaden topic slightly to increase source pool +- **Adjust Industry**: Ensure industry classification is accurate +- **Check Cache**: Clear cache if you're getting stale results +- **Retry Research**: Google Search grounding may need a second attempt + +#### Insufficient Keyword Analysis +**Problem**: Limited keyword variety or missing long-tail opportunities + +**Solutions**: +- **Add Topic Context**: Provide more detailed topic description +- **Specify Audience**: Better audience definition improves keyword targeting +- **Increase Word Count**: Target 2000+ words for richer keyword analysis +- **Review Persona Settings**: Industry and audience persona affects keyword discovery + +#### Missing Competitor Data +**Problem**: Competitor analysis lacks depth or opportunities + +**Solutions**: +- **Use Specific Keywords**: More targeted keywords reveal better competitors +- **Expand Industry Context**: Broad industry understanding improves competitive mapping +- **Review Content Angles**: Angles often highlight what competitors are NOT doing +- **Manual Review**: Top sources list shows main competitors worth reviewing + +#### Cache Not Working +**Problem**: Research taking full time even for duplicate keywords + +**Solutions**: +- **Check Exact Match**: Keywords, industry, and audience must match exactly +- **Verify Cache**: Check if persistent cache is enabled +- **Clear and Retry**: Sometimes clearing cache helps if data is corrupted +- **Check Logs**: Look for cache hit/miss messages in progress updates ### Getting Help #### Support Resources -- **Documentation**: Review research integration documentation -- **Tutorials**: Watch research feature tutorials -- **Best Practices**: Follow research best practices -- **Community**: Join user community discussions -- **Support**: Contact technical support +- **Workflow Guide**: [Complete 6-phase walkthrough](workflow-guide.md) +- **API Reference**: [Research API endpoints](api-reference.md) +- **Implementation Spec**: [Technical architecture](implementation-spec.md) +- **Best Practices**: [Advanced optimization tips](../../guides/best-practices.md) -#### Optimization Tips -- **Settings Review**: Regularly review research settings -- **Source Management**: Maintain source quality and diversity -- **Quality Monitoring**: Monitor research quality continuously -- **Performance Tracking**: Track research performance metrics -- **Continuous Improvement**: Continuously improve research process +#### Performance Optimization +- **Use Caching**: Leverage intelligent caching for repeat research +- **Keyword Precision**: More specific keywords yield better results +- **Industry Context**: Always provide industry for better data quality +- **Monitor Progress**: Review progress messages for efficiency insights +- **Batch Research**: Plan multiple blogs to maximize cache benefits --- -*Ready to enhance your content with comprehensive research? [Start with our First Steps Guide](../../getting-started/first-steps.md) and [Explore Blog Writer Features](overview.md) to begin creating well-researched, authoritative content!* +## Next Steps + +Now that you understand Phase 1: Research & Strategy, move to the next phase: + +- **[Phase 2: Intelligent Outline](workflow-guide.md#phase-2-intelligent-outline)** - Transform research into structured content plans +- **[Complete Workflow Guide](workflow-guide.md)** - End-to-end 6-phase walkthrough +- **[Blog Writer Overview](overview.md)** - Overview of all 6 phases +- **[Getting Started Guide](../../getting-started/quick-start.md)** - Quick start for new users + +--- + +*Ready to leverage Phase 1 research capabilities? Check out the [Workflow Guide](workflow-guide.md) to see how research flows into outline generation and beyond!* diff --git a/docs-site/docs/features/blog-writer/seo-analysis.md b/docs-site/docs/features/blog-writer/seo-analysis.md index 6683fbfd..e4b24e9e 100644 --- a/docs-site/docs/features/blog-writer/seo-analysis.md +++ b/docs-site/docs/features/blog-writer/seo-analysis.md @@ -1,343 +1,478 @@ -# SEO Analysis +# SEO Analysis & Optimization (Phase 4 & 5) -ALwrity's Blog Writer includes comprehensive SEO analysis capabilities that automatically optimize your content for search engines, improve readability, and enhance your content's search visibility. +ALwrity's Blog Writer includes comprehensive SEO analysis and metadata generation capabilities across Phases 4 and 5, automatically optimizing your content for search engines and preparing it for publication across platforms. -## What is SEO Analysis? +## Overview -SEO Analysis is an AI-powered feature that evaluates your blog content for search engine optimization, providing detailed insights, recommendations, and automated optimizations to improve your content's search ranking and visibility. +SEO optimization in the Blog Writer happens in two complementary phases: +- **Phase 4: SEO Analysis** - Comprehensive scoring, recommendations, and AI-powered content refinement +- **Phase 5: SEO Metadata** - Complete metadata generation including Open Graph, Twitter Cards, and Schema.org markup ### Key Benefits -- **Search Optimization**: Optimize content for search engines -- **Keyword Analysis**: Analyze and optimize keyword usage -- **Readability Enhancement**: Improve content readability and user experience -- **Technical SEO**: Ensure proper technical SEO implementation -- **Performance Insights**: Track and improve SEO performance +#### Phase 4: SEO Analysis +- **Multi-Dimensional Scoring**: Comprehensive SEO evaluation across 5 key categories +- **Actionable Recommendations**: Priority-ranked improvement suggestions with specific fixes +- **AI-Powered Refinement**: One-click "Apply Recommendations" for instant optimization +- **Parallel Processing**: Fast analysis using parallel non-AI analyzers plus AI insights +- **Performance Tracking**: Track SEO improvements and measure impact -## SEO Analysis Process +#### Phase 5: SEO Metadata +- **Comprehensive Metadata**: Complete SEO metadata package in 2 AI calls maximum +- **Social Optimization**: Open Graph and Twitter Cards for rich social previews +- **Structured Data**: Schema.org markup for enhanced search results and rich snippets +- **Multi-Format Export**: Ready-to-use formats for WordPress, Wix, and custom platforms +- **Platform Integration**: One-click copy and direct platform publishing support -### 1. Content Analysis +## Phase 4: SEO Analysis + +Phase 4 provides comprehensive SEO evaluation with actionable recommendations and AI-powered content refinement. + +### Parallel Processing Architecture + +Phase 4 uses a sophisticated parallel processing approach for speed and accuracy: + +```mermaid +flowchart TD + A[Blog Content] --> B[Phase 4: SEO Analysis] + B --> C[Parallel Non-AI Analyzers] + C --> D[Content Structure] + C --> E[Keyword Usage] + C --> F[Readability] + C --> G[Content Quality] + C --> H[Heading Structure] + + D --> I[SEO Results] + E --> I + F --> I + G --> I + H --> I + + I --> J[Single AI Analysis] + J --> K[Actionable Recommendations] + K --> L[Apply Recommendations] + L --> M[Refined Content] + + style A fill:#e3f2fd + style B fill:#f1f8e9 + style C fill:#fff3e0 + style I fill:#e8f5e8 + style L fill:#fce4ec + style M fill:#e1f5fe +``` + +### Multi-Dimensional SEO Scoring + +Phase 4 evaluates your content across 5 key categories: + +#### Overall SEO Score +- **Composite Rating**: Overall score (0-100) based on weighted category scores +- **Grade Assignment**: Automatically assigns grades (Excellent/Good/Needs Improvement) +- **Trend Tracking**: Compares to previous analysis to track improvements +- **Visual Feedback**: Color-coded UI provides instant visual assessment + +#### Category Breakdown +- **Structure Score**: Heading hierarchy, content organization, section balance +- **Keywords Score**: Keyword density, placement, variation, long-tail usage +- **Readability Score**: Reading level, sentence complexity, clarity assessment +- **Quality Score**: Content depth, engagement potential, value delivery +- **Headings Score**: H1-H3 distribution, keyword integration in headings + +### Actionable Recommendations + +Phase 4 generates specific, priority-ranked recommendations for improvement. + +#### Recommendation Categories +- **High Priority**: Critical SEO issues impacting search visibility +- **Medium Priority**: Significant improvements that boost rankings +- **Low Priority**: Nice-to-have optimizations for fine-tuning + +#### Example Recommendations +1. **Structure**: "Add more H2 subheadings to improve content scannability and keyword distribution" +2. **Keywords**: "Increase primary keyword density from 0.8% to 1.5% for optimal SEO performance" +3. **Readability**: "Simplify complex sentences; aim for average 15-20 words per sentence" +4. **Content**: "Add more specific examples and case studies to support key arguments" +5. **Meta**: "Reduce meta description to 155 characters for better search result display" + +### AI-Powered Content Refinement + +The "Apply Recommendations" feature uses AI to automatically improve your content based on SEO analysis. + +#### Intelligent Rewriting +- **Smart Application**: Applies recommendations while preserving your original intent +- **Natural Integration**: Optimizes keywords and structure without sounding forced +- **Context Preservation**: Maintains research accuracy and source alignment +- **Quality Maintenance**: Ensures readability while improving SEO metrics + +#### Application Process +```mermaid +flowchart LR + A[Current Content] --> B[SEO Recommendations] + B --> C[AI Prompt Construction] + C --> D[LLM Text Generation] + D --> E[Normalization & Validation] + E --> F[Optimized Content] + + style A fill:#e3f2fd + style B fill:#fff3e0 + style D fill:#f1f8e9 + style F fill:#e8f5e8 +``` + +### Content Analysis Process #### Initial Assessment -- **Content Structure**: Analyze heading hierarchy and content organization -- **Keyword Density**: Check keyword usage and density -- **Content Length**: Evaluate content length and depth -- **Readability**: Assess content readability and user experience -- **Technical Elements**: Check technical SEO elements +- **Content Structure**: Analyzes heading hierarchy, paragraph distribution, list usage +- **Keyword Distribution**: Maps keyword density and placement across sections +- **Readability Metrics**: Calculates Flesch Reading Ease, sentence length, complexity +- **Quality Indicators**: Evaluates depth, engagement potential, value delivery +- **Technical Elements**: Checks heading structure, meta elements, content length -#### Analysis Parameters +#### Parallel Analysis Details +Each analyzer processes content independently: +- **ContentAnalyzer**: Structure, organization, section balance +- **KeywordAnalyzer**: Density, placement, variation, semantic coverage +- **ReadabilityAnalyzer**: Reading level, sentence complexity, word choice +- **QualityAnalyzer**: Depth, engagement, value, completeness +- **HeadingAnalyzer**: Hierarchy, distribution, keyword integration + +Results are combined with AI insights for comprehensive recommendations. + +## Phase 5: SEO Metadata Generation + +Phase 5 generates comprehensive SEO metadata in maximum 2 AI calls, creating a complete optimization package ready for publication. + +### Efficient Two-Call Architecture + +Phase 5 minimizes AI calls for cost efficiency while delivering comprehensive metadata: + +```mermaid +flowchart TD + A[Blog Content + SEO Analysis] --> B[Phase 5: Metadata Generation] + B --> C{Call 1: Core Metadata} + C --> D[SEO Title] + C --> E[Meta Description] + C --> F[URL Slug] + C --> G[Tags & Categories] + C --> H[Reading Time] + + D --> I{Call 2: Social Metadata} + E --> I + F --> I + G --> I + H --> I + + I --> J[Open Graph Tags] + I --> K[Twitter Cards] + I --> L[Schema.org JSON-LD] + + J --> M[Complete Metadata Package] + K --> M + L --> M + + style A fill:#e3f2fd + style B fill:#e0f2f1 + style C fill:#fff3e0 + style I fill:#fce4ec + style M fill:#e8f5e8 +``` + +### Core Metadata Generation + +#### SEO-Optimized Elements +- **SEO Title** (50-60 chars): Front-loaded primary keyword, compelling, click-worthy +- **Meta Description** (150-160 chars): Keyword-rich with strong CTA in first 120 chars +- **URL Slug**: Clean, hyphenated, 3-5 words with primary keyword +- **Blog Tags** (5-8): Mix of primary, semantic, and long-tail keywords +- **Blog Categories** (2-3): Industry-standard classification +- **Social Hashtags** (5-10): Industry-specific with trending terms +- **Reading Time**: Calculated from word count (200 words/minute) +- **Focus Keyword**: Main SEO keyword selection + +#### Metadata Personalization +Metadata is dynamically tailored based on: +- Research keywords and search intent +- Target audience and industry +- SEO analysis recommendations +- Blog content structure and outline +- Tone and writing style preferences + +### Social Media Optimization + +#### Open Graph Tags +- **og:title**: Optimized for social sharing +- **og:description**: Compelling social preview text +- **og:image**: Recommended image dimensions and sources +- **og:type**: Article/blog classification +- **og:url**: Canonical URL reference + +#### Twitter Cards +- **twitter:card**: Summary card with large image support +- **twitter:title**: Concise, engaging headline +- **twitter:description**: Twitter-optimized summary +- **twitter:image**: Twitter-specific image optimization +- **twitter:site**: Website Twitter handle integration + +### Structured Data (Schema.org) + +#### Article Schema ```json { - "content": "Your blog post content here...", - "target_keywords": ["primary keyword", "secondary keyword"], - "competitor_urls": ["https://competitor1.com", "https://competitor2.com"], - "analysis_depth": "comprehensive", - "optimization_goals": ["rankings", "traffic", "engagement"] + "@context": "https://schema.org", + "@type": "BlogPosting", + "headline": "SEO-optimized title", + "description": "Meta description", + "author": { + "@type": "Organization", + "name": "Your Brand" + }, + "datePublished": "2025-01-20", + "dateModified": "2025-01-20", + "mainEntityOfPage": { + "@type": "WebPage" + } } ``` -### 2. Keyword Analysis +#### Additional Schema Types +- **Organization Markup**: Brand and publisher information +- **Breadcrumb Schema**: Navigation structure for rich snippets +- **FAQ Schema**: Q&A structured data for featured snippets +- **Review Schema**: Ratings and review markup -#### Primary Keywords -- **Keyword Density**: Analyze primary keyword density -- **Keyword Placement**: Check keyword placement and distribution -- **Keyword Variations**: Identify keyword variations and synonyms -- **Long-Tail Keywords**: Analyze long-tail keyword usage -- **Semantic Keywords**: Check semantic keyword integration +### Multi-Format Export -#### Secondary Keywords -- **Related Terms**: Identify related terms and phrases -- **LSI Keywords**: Check latent semantic indexing keywords -- **Contextual Keywords**: Analyze contextual keyword usage -- **Industry Terms**: Include industry-specific terminology -- **User Intent**: Match keywords to user search intent +Phase 5 outputs metadata in multiple formats for different platforms: -### 3. Content Optimization +#### HTML Meta Tags +```html + + + +``` -#### Structure Analysis -- **Heading Hierarchy**: Check H1, H2, H3 structure -- **Paragraph Length**: Analyze paragraph length and structure -- **List Usage**: Check for bullet points and numbered lists -- **Content Flow**: Analyze content flow and organization -- **Section Balance**: Ensure balanced content sections +#### JSON-LD Structured Data +Ready-to-paste structured data for search engines -#### Readability Assessment -- **Reading Level**: Assess content reading level -- **Sentence Length**: Analyze sentence length and complexity -- **Word Choice**: Check word choice and vocabulary -- **Clarity**: Assess content clarity and understanding -- **Engagement**: Evaluate content engagement potential +#### WordPress Export +WordPress-specific format with Yoast SEO compatibility -## SEO Analysis Features - -### Keyword Optimization - -#### Keyword Research -- **Primary Keywords**: Identify main target keywords -- **Secondary Keywords**: Find supporting keywords -- **Long-Tail Keywords**: Discover specific, less competitive phrases -- **LSI Keywords**: Find semantically related terms -- **Competitor Keywords**: Analyze competitor keyword usage - -#### Keyword Implementation -- **Title Optimization**: Optimize title tags for keywords -- **Meta Description**: Create keyword-rich meta descriptions -- **Heading Tags**: Optimize heading tags for keywords -- **Content Integration**: Naturally integrate keywords into content -- **Internal Linking**: Use keywords in internal links - -### Content Structure - -#### Heading Optimization -- **H1 Tag**: Single, keyword-rich H1 tag -- **H2 Tags**: Logical H2 tag structure -- **H3 Tags**: Detailed H3 tag organization -- **Heading Balance**: Balanced heading distribution -- **Keyword Integration**: Keywords in relevant headings - -#### Content Organization -- **Introduction**: Engaging, keyword-rich introduction -- **Body Sections**: Well-organized body content -- **Conclusion**: Strong, actionable conclusion -- **Call-to-Action**: Clear, compelling CTAs -- **Content Flow**: Smooth content flow and transitions - -### Technical SEO - -#### Meta Tags -- **Title Tag**: Optimized title tag (50-60 characters) -- **Meta Description**: Compelling meta description (150-160 characters) -- **Meta Keywords**: Relevant meta keywords -- **Open Graph**: Social media optimization tags -- **Schema Markup**: Structured data implementation - -#### Content Elements -- **Image Alt Text**: Descriptive alt text for images -- **Internal Links**: Strategic internal linking -- **External Links**: Relevant external link placement -- **URL Structure**: Clean, keyword-rich URLs -- **Content Length**: Optimal content length for SEO +#### Wix Integration +Direct Wix blog API format for seamless publishing ## Analysis Results -### SEO Score +### Phase 4 Output Structure -#### Overall Score -- **SEO Score**: Overall SEO performance score (0-100) -- **Keyword Score**: Keyword optimization score -- **Content Score**: Content quality and structure score -- **Technical Score**: Technical SEO implementation score -- **Readability Score**: Content readability score +Phase 4 returns comprehensive analysis results: -#### Score Breakdown ```json { - "overall_score": 85, - "keyword_score": 90, - "content_score": 80, - "technical_score": 85, - "readability_score": 88, - "recommendations": [ - "Improve meta description length", - "Add more internal links", - "Optimize image alt text" - ] + "overall_score": 82, + "grade": "Good", + "category_scores": { + "structure": 85, + "keywords": 88, + "readability": 78, + "quality": 80, + "headings": 84 + }, + "actionable_recommendations": [ + { + "category": "Structure", + "priority": "High", + "recommendation": "Add H2 subheadings to improve scannability", + "impact": "Better keyword distribution and user experience" + }, + { + "category": "Readability", + "priority": "Medium", + "recommendation": "Simplify complex sentences (average 20 words)", + "impact": "Improved readability score and engagement" + } + ], + "keyword_analysis": { + "primary_keyword_density": 1.2, + "semantic_keyword_count": 15, + "long_tail_usage": 8, + "optimization_status": "Good" + } } ``` -### Detailed Recommendations +## Use Cases for Different Audiences -#### Keyword Optimization -- **Keyword Density**: Adjust keyword density for optimal results -- **Keyword Placement**: Improve keyword placement and distribution -- **Keyword Variations**: Add more keyword variations -- **Long-Tail Keywords**: Include more long-tail keywords -- **Semantic Keywords**: Add semantically related terms +### For Technical Content Writers +**Scenario**: Creating a technical deep-dive on "React Server Components" -#### Content Improvement -- **Heading Structure**: Improve heading hierarchy -- **Paragraph Length**: Optimize paragraph length -- **Content Flow**: Enhance content flow and organization -- **Readability**: Improve content readability -- **Engagement**: Increase content engagement +**Phase 4 Delivers**: +- Structure score analysis: Identifies need for more code examples in H3 sections +- Readability assessment: Detects overly complex technical jargon +- Keyword optimization: Suggests semantic keywords like "React SSR" and "Next.js 13" +- Actionable fix: "Add 'why it matters' explanations for React Server Component concepts" -#### Technical Optimization -- **Meta Tags**: Optimize meta tags and descriptions -- **Image Optimization**: Improve image alt text and optimization -- **Internal Linking**: Add strategic internal links -- **URL Structure**: Optimize URL structure -- **Schema Markup**: Implement structured data +**Phase 5 Delivers**: +- SEO title: "React Server Components Explained: Complete 2025 Guide" +- Meta description: Includes CTA like "Master RSC implementation with practical examples" +- JSON-LD: Code schema markup for search engine code indexing +- Social tags: #React #WebDevelopment #Programming -## Competitive Analysis +**Value**: Technical content optimized for both search engines and developer audiences -### Competitor Comparison +### For Solopreneurs +**Scenario**: Blog on "Starting an Online Course Business" -#### Content Analysis -- **Content Length**: Compare content length with competitors -- **Keyword Usage**: Analyze competitor keyword strategies -- **Content Structure**: Compare content organization -- **Readability**: Assess competitor content readability -- **Engagement**: Compare engagement potential +**Phase 4 Delivers**: +- Quality score: Identifies missing CTA elements in conclusion +- Readability: Highlights need to simplify business jargon +- Keyword gaps: Discovers missing long-tail "online course pricing strategy" +- High-priority fix: "Add specific revenue examples to build credibility" -#### SEO Performance -- **Search Rankings**: Compare search engine rankings -- **Traffic Analysis**: Analyze competitor traffic patterns -- **Backlink Profile**: Compare backlink strategies -- **Social Signals**: Analyze social media performance -- **Content Gaps**: Identify content opportunities +**Phase 5 Delivers**: +- SEO title: "Start Online Course Business: Ultimate 2025 Guide" (56 chars) +- Social hashtags: #OnlineCourses #PassiveIncome #Entrepreneurship +- Schema.org: EducationalCourse schema for course-related rich snippets +- Reading time: "15 minutes" for appropriate audience expectation -### Gap Analysis +**Value**: Professional SEO without hiring expensive consultants -#### Content Opportunities -- **Missing Topics**: Identify topics competitors haven't covered -- **Content Depth**: Find areas for deeper content coverage -- **Keyword Gaps**: Discover keyword opportunities -- **Format Gaps**: Identify content format opportunities -- **Audience Gaps**: Find underserved audience segments +### For Digital Marketing & SEO Professionals +**Scenario**: Strategy content on "Local SEO for Small Businesses" -#### Competitive Advantages -- **Unique Angles**: Develop unique content angles -- **Expertise Showcase**: Highlight unique expertise -- **Better Coverage**: Provide more comprehensive coverage -- **Improved Quality**: Create higher quality content -- **Enhanced User Experience**: Improve user experience +**Phase 4 Delivers**: +- Comprehensive scoring across all 5 categories with detailed breakdown +- Competitor analysis integration from Phase 1 research +- High-priority recommendations: "Missing Google Business Profile optimization section" +- Metrics: Keyword density at 0.9%, target 1.5-2% for competitive keywords -## Performance Tracking +**Phase 5 Delivers**: +- Complete metadata package with local SEO schema markup +- Location-based Open Graph tags for local business visibility +- Multi-format export for WordPress with Yoast compatibility +- Structured data including LocalBusiness schema for local SERP features -### SEO Metrics - -#### Search Performance -- **Search Rankings**: Track keyword rankings -- **Organic Traffic**: Monitor organic search traffic -- **Click-Through Rate**: Track search result clicks -- **Impression Share**: Monitor search impression share -- **Average Position**: Track average search position - -#### Content Performance -- **Page Views**: Monitor page view metrics -- **Time on Page**: Track user engagement time -- **Bounce Rate**: Monitor bounce rate -- **Conversion Rate**: Track conversion metrics -- **Social Shares**: Monitor social media shares - -### Analytics Integration - -#### Google Analytics -- **Traffic Sources**: Analyze traffic sources -- **User Behavior**: Track user behavior patterns -- **Content Performance**: Monitor content performance -- **Conversion Tracking**: Track conversion metrics -- **Audience Insights**: Analyze audience demographics - -#### Search Console -- **Search Queries**: Monitor search query performance -- **Click Data**: Track click-through rates -- **Impression Data**: Monitor search impressions -- **Position Data**: Track search position changes -- **Coverage Issues**: Identify technical issues +**Value**: Enterprise-grade SEO optimization with detailed analytics ## Best Practices -### Content Optimization +### Phase 4: SEO Analysis Best Practices -#### Keyword Strategy -1. **Primary Focus**: Focus on one primary keyword per page -2. **Natural Integration**: Integrate keywords naturally -3. **Semantic Keywords**: Use semantically related terms -4. **Long-Tail Keywords**: Target specific, long-tail phrases -5. **User Intent**: Match keywords to user search intent +#### Pre-Analysis Preparation +1. **Complete Content**: Ensure all sections are finalized before analysis +2. **Research Integration**: Verify Phase 1 research data includes keywords +3. **Word Count**: Target 1000-3000 words for optimal SEO analysis +4. **Structure Review**: Confirm proper heading hierarchy (H1, H2, H3) +5. **Content Quality**: Ensure content is factually accurate and complete -#### Content Quality -1. **Original Content**: Create original, unique content -2. **Comprehensive Coverage**: Provide comprehensive topic coverage -3. **Expert Authority**: Demonstrate expertise and authority -4. **User Value**: Provide clear value to users -5. **Engagement**: Create engaging, shareable content +#### Using "Apply Recommendations" +1. **Review First**: Always review recommendations before applying +2. **Selective Application**: Consider applying high-priority fixes first +3. **Edit After**: Manually refine AI-applied changes for your voice +4. **Preserve Intent**: Verify AI preserved your original meaning +5. **Re-Analyze**: Run Phase 4 again after applying to track improvement -### Technical SEO +### Phase 5: Metadata Generation Best Practices -#### On-Page Optimization -1. **Title Tags**: Create compelling, keyword-rich titles -2. **Meta Descriptions**: Write engaging meta descriptions -3. **Heading Structure**: Use proper heading hierarchy -4. **Internal Linking**: Implement strategic internal linking -5. **Image Optimization**: Optimize images with alt text +#### Metadata Optimization +1. **Title Length**: Keep SEO titles to 50-60 characters for SERP display +2. **Meta Descriptions**: Write 150-160 character descriptions with CTA in first 120 chars +3. **Keyword Placement**: Front-load primary keyword in title and first 120 chars of description +4. **Uniqueness**: Ensure metadata is unique for each blog post +5. **Brand Consistency**: Include brand name where appropriate without exceeding length limits -#### Site Performance -1. **Page Speed**: Optimize page loading speed -2. **Mobile Optimization**: Ensure mobile-friendly design -3. **SSL Certificate**: Use HTTPS for security -4. **Clean URLs**: Use clean, descriptive URLs -5. **Schema Markup**: Implement structured data +#### Social Media Optimization +1. **Image Planning**: Prepare 1200x630px images for Open Graph sharing +2. **Twitter Cards**: Ensure Twitter Card images are 1200x600px minimum +3. **Hashtag Strategy**: Mix industry-specific, trending, and branded hashtags +4. **Platform-Specific**: Review Open Graph vs Twitter Card differences +5. **Testing**: Use Facebook Debugger and Twitter Card Validator before publishing -## Advanced Features +### SEO Workflow Integration -### AI-Powered Optimization +#### Phase 4 to Phase 5 Flow +1. **Score First**: Always complete Phase 4 analysis before metadata generation +2. **Apply Fixes**: Use "Apply Recommendations" to improve scores to 80+ +3. **Generate Metadata**: Run Phase 5 with optimized content +4. **Review Metadata**: Verify metadata reflects SEO improvements +5. **Export & Publish**: Copy metadata formats for your platform -#### Content Enhancement -- **Automatic Optimization**: AI-powered content optimization -- **Keyword Suggestions**: Intelligent keyword recommendations -- **Content Improvement**: Automated content improvement suggestions -- **Readability Enhancement**: AI-powered readability improvements -- **Engagement Optimization**: Optimize for user engagement - -#### Performance Prediction -- **Ranking Prediction**: Predict potential search rankings -- **Traffic Forecasting**: Forecast organic traffic potential -- **Engagement Prediction**: Predict user engagement levels -- **Conversion Optimization**: Optimize for conversions -- **ROI Analysis**: Analyze return on SEO investment - -### Customization Options - -#### Analysis Settings -- **Keyword Preferences**: Set keyword analysis preferences -- **Competitor Selection**: Choose competitors for analysis -- **Analysis Depth**: Adjust analysis depth and detail -- **Optimization Goals**: Set specific optimization goals -- **Quality Standards**: Define quality standards and thresholds - -#### Reporting Options -- **Custom Reports**: Create custom SEO reports -- **Scheduled Reports**: Set up automated reporting -- **Performance Dashboards**: Create performance dashboards -- **Alert Systems**: Set up performance alerts -- **Export Options**: Export data in various formats +#### Performance Optimization +1. **Cache Utilization**: Leverage research caching from Phase 1 for related topics +2. **Batch Analysis**: Analyze multiple blog drafts in one session to improve learning +3. **Score Tracking**: Monitor SEO score trends across multiple posts +4. **A/B Testing**: Test different metadata variations for CTR optimization +5. **Analytics Integration**: Connect to Google Analytics/Search Console post-publish ## Troubleshooting -### Common Issues +### Common Issues & Solutions -#### SEO Analysis Problems -- **Low SEO Scores**: Address low SEO performance -- **Keyword Issues**: Resolve keyword optimization problems -- **Content Quality**: Improve content quality and structure -- **Technical Issues**: Fix technical SEO problems -- **Performance Issues**: Address performance concerns +#### Low SEO Scores (< 70) +**Problem**: Overall SEO score below 70 or grade showing "Needs Improvement" -#### Optimization Challenges -- **Keyword Overuse**: Avoid keyword stuffing -- **Content Duplication**: Prevent duplicate content issues -- **Technical Errors**: Fix technical SEO errors -- **Performance Problems**: Resolve performance issues -- **Competition Analysis**: Improve competitive analysis +**Solutions**: +- **Check Category Scores**: Review individual category breakdowns to identify weak areas +- **Apply High-Priority Recommendations**: Focus on critical fixes first +- **Verify Content Length**: Ensure 1000+ words for comprehensive analysis +- **Review Heading Structure**: Confirm proper H1/H2/H3 hierarchy +- **Re-run Analysis**: After fixing issues, re-analyze to track improvements + +#### Keyword Analysis Issues +**Problem**: Low keyword scores or missing keyword recommendations + +**Solutions**: +- **Verify Phase 1 Research**: Ensure Phase 1 keyword analysis completed successfully +- **Check Keyword Density**: Primary keyword should be 1-2% of total content +- **Review Placement**: Ensure keywords appear in title, first paragraph, and subheadings +- **Add Semantic Keywords**: Integrate related terms naturally throughout content +- **Consider Long-Tail**: Include 3-5 long-tail keyword variations + +#### "Apply Recommendations" Not Working +**Problem**: Content doesn't update or changes seem minimal + +**Solutions**: +- **Check Recommendations**: Verify actionable recommendations are actually present +- **Review Normalization**: Check if AI properly matched section IDs +- **Refresh UI**: Try closing and reopening the SEO Analysis modal +- **Manual Review**: Compare original vs. updated sections for subtle changes +- **Re-Analyze**: Run Phase 4 again to see if scores improved + +#### Metadata Generation Issues +**Problem**: Phase 5 generates incomplete or low-quality metadata + +**Solutions**: +- **Content Completeness**: Ensure blog content is finalized before metadata generation +- **Title/Slug Issues**: Generate metadata after choosing final blog title +- **Length Constraints**: Verify SEO titles (50-60) and descriptions (150-160) are respected +- **Re-run Phase 5**: If results are suboptimal, regenerate with clearer content +- **Manual Refinement**: Edit generated metadata for brand voice consistency ### Getting Help #### Support Resources -- **Documentation**: Review SEO analysis documentation -- **Tutorials**: Watch SEO optimization tutorials -- **Best Practices**: Follow SEO best practices -- **Community**: Join user community discussions -- **Support**: Contact technical support +- **[Workflow Guide](workflow-guide.md)**: Complete 6-phase walkthrough +- **[Blog Writer Overview](overview.md)**: Overview of all phases +- **[API Reference](api-reference.md)**: Technical API documentation +- **[Best Practices](../../guides/best-practices.md)**: Advanced optimization tips -#### Optimization Tips -- **Regular Analysis**: Perform regular SEO analysis -- **Continuous Improvement**: Continuously improve SEO performance -- **Performance Monitoring**: Monitor SEO performance metrics -- **Competitive Analysis**: Regular competitive analysis -- **Quality Assurance**: Maintain high quality standards +#### Performance Tips +- **Batch Processing**: Analyze multiple drafts in one session for efficiency +- **Cache Benefits**: Reuse research from Phase 1 to speed up workflow +- **Score Tracking**: Monitor SEO improvements across multiple blog posts +- **Metadata Testing**: Use Facebook Debugger and Twitter Card Validator +- **Analytics Setup**: Connect Google Analytics/Search Console for post-publish tracking --- -*Ready to optimize your content for search engines? [Start with our First Steps Guide](../../getting-started/first-steps.md) and [Explore Blog Writer Features](overview.md) to begin creating SEO-optimized, high-ranking content!* +## Next Steps + +Now that you understand Phase 4 & 5, explore the complete workflow: + +- **[Phase 1: Research](research.md)** - Comprehensive research capabilities +- **[Complete Workflow Guide](workflow-guide.md)** - End-to-end 6-phase walkthrough +- **[Blog Writer Overview](overview.md)** - All phases overview +- **[Getting Started Guide](../../getting-started/quick-start.md)** - Quick start for new users + +--- + +*Ready to optimize your content for search engines? Check out the [Workflow Guide](workflow-guide.md) to see how Phase 4 & 5 integrate into the complete blog creation process!* diff --git a/docs-site/docs/features/blog-writer/workflow-guide.md b/docs-site/docs/features/blog-writer/workflow-guide.md index 6a193bce..03df2e78 100644 --- a/docs-site/docs/features/blog-writer/workflow-guide.md +++ b/docs-site/docs/features/blog-writer/workflow-guide.md @@ -8,36 +8,36 @@ The ALwrity Blog Writer follows a sophisticated 6-phase workflow designed to cre ```mermaid flowchart TD - A[Start: Keywords & Topic] --> B[Phase 1: Research & Discovery] - B --> C[Phase 2: Outline Generation] + A[Start: Keywords & Topic] --> B[Phase 1: Research & Strategy] + B --> C[Phase 2: Intelligent Outline] C --> D[Phase 3: Content Generation] D --> E[Phase 4: SEO Analysis] - E --> F[Phase 5: Quality Assurance] - F --> G[Phase 6: Publishing] + E --> F[Phase 5: SEO Metadata] + F --> G[Phase 6: Publish & Distribute] - B --> B1[Web Search & Source Collection] + B --> B1[Google Search Grounding] B --> B2[Competitor Analysis] B --> B3[Research Caching] - C --> C1[Content Structure Planning] - C --> C2[Section Definition] - C --> C3[Source Mapping] + C --> C1[AI Outline Generation] + C --> C2[Source Mapping] + C --> C3[Title Generation] D --> D1[Section-by-Section Writing] - D --> D2[Citation Integration] - D --> D3[Continuity Tracking] + D --> D2[Context Memory] + D --> D3[Flow Analysis] E --> E1[SEO Scoring] - E --> E2[Keyword Analysis] - E --> E3[Readability Assessment] + E --> E2[Actionable Recommendations] + E --> E3[AI-Powered Refinement] - F --> F1[Fact Verification] - F --> F2[Hallucination Detection] - F --> F3[Quality Scoring] + F --> F1[Comprehensive Metadata] + F --> F2[Open Graph & Twitter Cards] + F --> F3[Schema.org Markup] - G --> G1[Platform Integration] - G --> G2[Metadata Generation] - G --> G3[Content Publishing] + G --> G1[Multi-Platform Publishing] + G --> G2[Scheduling] + G --> G3[Version Management] style A fill:#e3f2fd style B fill:#e8f5e8 @@ -58,40 +58,40 @@ gantt dateFormat X axisFormat %M:%S - section Research + section Phase 1 Research Keyword Analysis :0, 10 - Web Search :10, 30 - Source Collection :20, 40 - Competitor Analysis :30, 50 - Research Caching :40, 60 + Google Search :10, 40 + Source Extraction :30, 50 + Competitor Analysis :40, 60 + Research Caching :50, 60 - section Outline - Structure Planning :60, 70 - Section Definition :70, 80 - Source Mapping :80, 90 - Title Generation :90, 100 + section Phase 2 Outline + AI Structure Planning :60, 80 + Section Definition :75, 90 + Source Mapping :85, 100 + Title Generation :95, 110 - section Content - Section 1 Writing :100, 120 - Section 2 Writing :120, 140 - Section 3 Writing :140, 160 - Citation Integration :160, 170 + section Phase 3 Content + Section 1 Writing :110, 140 + Section 2 Writing :130, 160 + Section 3 Writing :150, 180 + Context Continuity :170, 200 - section SEO - Structure Analysis :170, 180 - Keyword Analysis :180, 190 - Readability Check :190, 200 - SEO Scoring :200, 210 + section Phase 4 SEO + Parallel Analysis :200, 215 + AI Scoring :210, 230 + Recommendations :220, 235 + Apply Refinement :230, 250 - section Quality - Fact Verification :210, 220 - Hallucination Check :220, 230 - Quality Scoring :230, 240 + section Phase 5 Metadata + Core Metadata :250, 265 + Social Tags :260, 275 + Schema Markup :270, 285 - section Publishing - Platform Integration :240, 250 - Metadata Generation :250, 260 - Content Publishing :260, 270 + section Phase 6 Publish + Platform Setup :285, 295 + Content Publishing :290, 310 + Verification :305, 320 ``` ## πŸ“‹ Prerequisites @@ -104,7 +104,7 @@ Before starting, ensure you have: - **Content Goals**: Defined objectives for your blog post - **Word Count Target**: Desired length (typically 1000-3000 words) -## πŸ” Phase 1: Research & Discovery +## πŸ” Phase 1: Research & Strategy ### Step 1: Initiate Research @@ -170,7 +170,7 @@ Before starting, ensure you have: - βœ… Relevant to your target audience - βœ… Covers multiple aspects of your topic -## πŸ“ Phase 2: Outline Generation +## πŸ“ Phase 2: Intelligent Outline ### Step 1: Generate Outline @@ -235,6 +235,31 @@ Before starting, ensure you have: - **Add Sections**: Include missing content areas - **Improve SEO**: Better keyword distribution +### πŸ–ΌοΈ Generate Images for Sections (Optional) + +While in Phase 2, you can generate images for your outline sections. + +**How It Works:** +1. Click the **"πŸ–ΌοΈ Generate Image"** button on any section in the outline +2. Image modal opens with auto-generated prompt based on section heading +3. Click **"Suggest Prompt"** for AI-optimized suggestions +4. Optionally open **"Advanced Image Options"** for custom settings +5. Choose provider: Stability AI, Hugging Face, or Gemini +6. Generate and images auto-insert into outline and metadata + +**Best Practices:** +- Generate images during outline review +- Use specific, descriptive prompts +- Match image style to your brand +- Generate multiple variations if needed + +**Image Features:** +- Provider selection (Stability AI, Hugging Face, Gemini) +- Aspect ratio options (1:1, 16:9, 4:3) +- Style customization +- Auto-prompt suggestions +- Platform-optimized outputs + ## ✍️ Phase 3: Content Generation ### Step 1: Generate Section Content @@ -311,7 +336,71 @@ Repeat the process for each outline section: - Use continuity metrics to ensure flow - Adjust tone and style as needed -## πŸ” Phase 4: SEO Analysis & Optimization +### Advanced Features in Phase 3 + +#### ✨ Assistive Writing (Continue Writing) +As you write in any blog section, the AI provides contextual suggestions to help you continue. + +**How It Works:** +1. Type 20+ words in any section +2. First suggestion appears automatically below your cursor +3. Click **"Accept"** to insert or **"Dismiss"** to skip +4. Click **"✍️ Continue Writing"** to request more suggestions +5. Suggestions include source citations when available + +**Benefits:** +- Real-time writing assistance +- Context-aware continuations +- Source-backed suggestions +- Cost-optimized (first auto, then manual) + +#### Quick Edit Options +Select text to access quick edit options in the context menu: + +**Available Quick Edits:** +- **✏️ Improve**: Enhance readability and engagement +- **βž• Add Transition**: Insert transitional phrases (Furthermore, Additionally, Moreover) +- **πŸ“ Shorten**: Condense while maintaining meaning +- **πŸ“ Expand**: Add explanatory content and insights +- **πŸ’Ό Professionalize**: Make more formal (convert contractions, improve tone) +- **πŸ“Š Add Data**: Insert statistical backing statements + +**How It Works:** +1. Select any text in your blog content +2. Context menu appears near your cursor +3. Choose a quick edit option +4. Text updates instantly + +**Best For:** +- Improving flow between sentences +- Adjusting tone and formality +- Adding supporting statements +- Professionalizing casual language + +#### πŸ” Fact-Checking +Verify claims and facts in your content with AI-powered checking. + +**How It Works:** +1. Select any paragraph or claim text +2. Right-click or use the context menu +3. Click **"πŸ” Fact Check"** +4. Wait 15-30 seconds for analysis +5. Review detailed results with supporting/refuting sources +6. Click **"Apply Fix"** to insert source links if needed + +**What Gets Analyzed:** +- Verifiable claims and statements +- Statistical data and percentages +- Dates, names, and events +- Industry-specific facts + +**Results Include:** +- Claim-by-claim confidence scores +- Supporting evidence URLs +- Refuting sources (if applicable) +- Overall factual accuracy score + +## πŸ” Phase 4: SEO Analysis ### Step 1: Perform SEO Analysis @@ -356,7 +445,21 @@ Repeat the process for each outline section: - βœ… Proper heading structure - βœ… Actionable recommendations -### Step 3: Generate SEO Metadata +### Step 3: Apply SEO Recommendations (Optional) + +**Endpoint**: `POST /api/blog/seo/apply-recommendations` + +Use the "Apply Recommendations" button to automatically improve your content based on SEO analysis. The AI will: +- Optimize keyword density and placement +- Improve content structure and headings +- Enhance readability and flow +- Maintain your original voice and intent + +**Expected Duration**: 20-40 seconds + +## πŸ“ Phase 5: SEO Metadata + +### Step 1: Generate Core Metadata **Endpoint**: `POST /api/blog/seo/metadata` @@ -373,66 +476,50 @@ Repeat the process for each outline section: } ``` -**Generated Metadata**: -- **SEO Title**: Optimized for search engines -- **Meta Description**: Compelling 155-character description -- **URL Slug**: SEO-friendly URL structure -- **Tags & Categories**: Relevant content classification -- **Social Media Tags**: Open Graph and Twitter Card data -- **JSON-LD Schema**: Structured data for search engines +**What Happens** (First AI Call): +1. **SEO Title**: Optimized for search engines (50-60 chars) +2. **Meta Description**: Compelling description with CTA (150-160 chars) +3. **URL Slug**: Clean, hyphenated, keyword-rich (3-5 words) +4. **Blog Tags**: Mix of primary, semantic, and long-tail keywords (5-8) +5. **Blog Categories**: Industry-standard classification (2-3) +6. **Social Hashtags**: Industry-specific with trending terms (5-10) +7. **Reading Time**: Calculated from word count -## πŸ›‘οΈ Phase 5: Quality Assurance +**Expected Duration**: 10-15 seconds -### Step 1: Perform Hallucination Check +### Step 2: Generate Social Media & Schema Metadata -**Endpoint**: `POST /api/blog/quality/hallucination-check` +**What Happens** (Second AI Call): +1. **Open Graph Tags**: Optimized for Facebook/LinkedIn sharing +2. **Twitter Cards**: Twitter-specific optimization +3. **JSON-LD Schema**: Structured data for search engines +4. **Multi-Format Export**: WordPress, Wix, HTML, JSON-LD ready formats -**Request Example**: -```json -{ - "content": "Complete blog content here...", - "sources": [ - "https://example.com/source1", - "https://example.com/source2" - ] -} -``` +**Generated Metadata Output**: +- **Core Elements**: Title, description, URL slug, tags, categories +- **Social Optimization**: Open Graph and Twitter Card tags +- **Structured Data**: Article schema with author, dates, organization +- **Platform Formats**: Copy-ready for WordPress, Wix, custom -**What Happens**: -1. **Fact Verification**: Checks content against research sources -2. **Hallucination Detection**: Identifies potential AI-generated inaccuracies -3. **Content Validation**: Ensures factual accuracy and credibility -4. **Quality Scoring**: Generates content quality metrics +**Expected Duration**: 10-15 seconds -**Expected Duration**: 15-25 seconds - -### Step 2: Review Quality Results - -**Key Metrics**: -- **Factual Accuracy**: Percentage of verified claims -- **Source Coverage**: Percentage of content backed by sources -- **Quality Score**: Overall content quality (0-100) -- **Improvement Suggestions**: Specific enhancement recommendations +### Step 3: Review & Export Metadata **Quality Checklist**: -- βœ… High factual accuracy (90%+) -- βœ… Good source coverage (80%+) -- βœ… Quality score above 85 -- βœ… No major factual errors -- βœ… Clear improvement suggestions +- βœ… SEO title is 50-60 characters with primary keyword +- βœ… Meta description includes CTA in first 120 chars +- βœ… URL slug is clean, readable, and keyword-rich +- βœ… Tags and categories are relevant and varied +- βœ… Social tags are optimized for each platform +- βœ… Schema markup is valid JSON-LD -### Step 3: Content Optimization (Optional) +**Export Options**: +- Copy HTML meta tags directly to your platform +- Export JSON-LD for search engines +- WordPress-ready format with Yoast compatibility +- Wix integration format -**Endpoint**: `POST /api/blog/section/optimize` - -**Common Optimizations**: -- **Improve Readability**: Simplify complex sentences -- **Enhance Engagement**: Add compelling examples and stories -- **Strengthen Arguments**: Provide more supporting evidence -- **Fix Flow Issues**: Improve section transitions -- **Optimize Keywords**: Better keyword integration - -## πŸš€ Phase 6: Publishing & Distribution +## πŸš€ Phase 6: Publish & Distribute ### Step 1: Prepare for Publishing diff --git a/frontend/public/BLOG_WRITER_ASSETS_GUIDE.md b/frontend/public/BLOG_WRITER_ASSETS_GUIDE.md new file mode 100644 index 00000000..a3401d37 --- /dev/null +++ b/frontend/public/BLOG_WRITER_ASSETS_GUIDE.md @@ -0,0 +1,162 @@ +# Blog Writer Assets Guide + +## πŸ“ Folder Structure + +``` +frontend/public/ +β”œβ”€β”€ images/ +β”‚ └── (add 24 feature images here) +β”œβ”€β”€ videos/ +β”‚ └── (add 6 demo videos here) +β”œβ”€β”€ blog-writer-bg.png (already exists βœ…) +└── BLOG_WRITER_ASSETS_GUIDE.md (this file) +``` + +## πŸ–ΌοΈ Required Images (24 total) + +### Phase 1: Research & Strategy (4 images) +- `images/research-google-grounding.jpg` - Screenshot/video frame showing Google Search grounding in action +- `images/research-competitor.jpg` - Screenshot of competitor analysis results +- `images/research-keywords.jpg` - Screenshot showing keyword analysis and clustering +- `images/research-angles.jpg` - Screenshot of AI-generated content angle suggestions + +### Phase 2: Intelligent Outline (4 images) +- `images/outline-generation.jpg` - Screenshot of AI outline generation interface +- `images/outline-grounding.jpg` - Screenshot showing source mapping and grounding scores +- `images/outline-refine.jpg` - Screenshot of interactive outline refinement (add/remove/merge sections) +- `images/outline-titles.jpg` - Screenshot of multiple AI-generated title options with SEO scores + +### Phase 3: Content Generation (4 images) +- `images/content-generation.jpg` - Screenshot of section-by-section content generation +- `images/content-continuity.jpg` - Screenshot showing continuity analysis and flow metrics +- `images/content-sources.jpg` - Screenshot of automatic source integration and citations +- `images/content-medium.jpg` - Screenshot of Medium blog mode quick generation + +### Phase 4: SEO Analysis (4 images) +- `images/seo-scoring.jpg` - Screenshot of comprehensive SEO scoring dashboard +- `images/seo-recommendations.jpg` - Screenshot of actionable SEO recommendations list +- `images/seo-apply.jpg` - Screenshot of AI-powered content refinement interface +- `images/seo-keywords.jpg` - Screenshot of keyword density heatmap and analysis + +### Phase 5: SEO Metadata (4 images) +- `images/metadata-comprehensive.jpg` - Screenshot of full metadata generation interface +- `images/metadata-social.jpg` - Screenshot of Open Graph and Twitter Cards configuration +- `images/metadata-schema.jpg` - Screenshot of structured data (Schema.org) markup +- `images/metadata-export.jpg` - Screenshot of multi-format output options (HTML, JSON-LD, WordPress, Wix) + +### Phase 6: Publish & Distribute (4 images) +- `images/publish-platforms.jpg` - Screenshot of multi-platform publishing options (WordPress, Wix, Medium) +- `images/publish-schedule.jpg` - Screenshot of content scheduling interface with calendar +- `images/publish-versions.jpg` - Screenshot of revision management and version history +- `images/publish-analytics.jpg` - Screenshot of post-publish analytics dashboard + +## 🎬 Required Videos (6 total) + +### Phase 1: Research & Strategy +- `videos/phase1-research.mp4` - Demo video showing: + - Keyword input and analysis + - Google Search grounding in action + - Competitor analysis results + - Content angle generation + +### Phase 2: Intelligent Outline +- `videos/phase2-outline.mp4` - Demo video showing: + - AI outline generation from research + - Source mapping and grounding scores + - Interactive refinement (add/remove sections) + - Title generation with SEO scores + +### Phase 3: Content Generation +- `videos/phase3-content.mp4` - Demo video showing: + - Section-by-section content generation + - Continuity analysis and flow metrics + - Source integration and citations + - Medium blog mode + +### Phase 4: SEO Analysis +- `videos/phase4-seo.mp4` - Demo video showing: + - SEO scoring dashboard + - Actionable recommendations + - AI-powered content refinement ("Apply Recommendations") + - Keyword analysis + +### Phase 5: SEO Metadata +- `videos/phase5-metadata.mp4` - Demo video showing: + - Comprehensive metadata generation + - Open Graph and Twitter Cards + - Structured data (Schema.org) + - Multi-format export options + +### Phase 6: Publish & Distribute +- `videos/phase6-publish.mp4` - Demo video showing: + - Multi-platform publishing + - Content scheduling + - Version management + - Analytics integration + +## πŸ“ Image Requirements + +- **Format**: JPG/JPEG (recommended for photos) or PNG (recommended for screenshots) +- **Resolution**: + - Minimum: 1200x800px (3:2 aspect ratio for cards) + - Recommended: 1920x1280px for best quality +- **File Size**: Keep under 500KB each for fast loading +- **Content**: Actual screenshots from the working application + +## πŸŽ₯ Video Requirements + +- **Format**: MP4 (H.264 codec recommended) +- **Duration**: 30-90 seconds per phase +- **Resolution**: + - Minimum: 1280x720 (720p) + - Recommended: 1920x1080 (1080p) +- **File Size**: Optimize to keep under 10MB each if possible +- **Content**: Screen recordings showing the actual features in action + +## πŸš€ How to Add Assets + +1. **Create the folders** (already created with .gitkeep files): + ```bash + # Folders are already created, just add files + frontend/public/images/ + frontend/public/videos/ + ``` + +2. **Add your images**: + - Take screenshots or create mockups + - Optimize for web (compress if needed) + - Save with exact filenames listed above + - Place in `frontend/public/images/` folder + +3. **Add your videos**: + - Record screen captures of each phase + - Edit to show key features + - Optimize file size + - Save with exact filenames listed above + - Place in `frontend/public/videos/` folder + +4. **Test the integration**: + - Run the app: `cd frontend && npm start` + - Open Blog Writer + - Click "πŸš€ ALwrity Blog Writer SuperPowers" + - Expand each phase to see images and videos + +## βœ… Quick Checklist + +- [ ] Phase 1: Research images (4) + video (1) +- [ ] Phase 2: Outline images (4) + video (1) +- [ ] Phase 3: Content images (4) + video (1) +- [ ] Phase 4: SEO images (4) + video (1) +- [ ] Phase 5: Metadata images (4) + video (1) +- [ ] Phase 6: Publish images (4) + video (1) +- [ ] Total: 24 images + 6 videos = 30 assets + +## πŸ“ Current Implementation + +The images and videos are referenced in: +- `frontend/src/components/BlogWriter/BlogWriterPhasesSection.tsx` +- Each phase card shows video when expanded +- Each feature card shows image placeholder + +Paths are already configured to use `/images/` and `/videos/` from the public folder. + diff --git a/frontend/public/images/.gitkeep b/frontend/public/images/.gitkeep new file mode 100644 index 00000000..997fd7bb --- /dev/null +++ b/frontend/public/images/.gitkeep @@ -0,0 +1,27 @@ +# Blog Writer Phase Images +# Add your phase images here: +# - research-google-grounding.jpg +# - research-competitor.jpg +# - research-keywords.jpg +# - research-angles.jpg +# - outline-generation.jpg +# - outline-grounding.jpg +# - outline-refine.jpg +# - outline-titles.jpg +# - content-generation.jpg +# - content-continuity.jpg +# - content-sources.jpg +# - content-medium.jpg +# - seo-scoring.jpg +# - seo-recommendations.jpg +# - seo-apply.jpg +# - seo-keywords.jpg +# - metadata-comprehensive.jpg +# - metadata-social.jpg +# - metadata-schema.jpg +# - metadata-export.jpg +# - publish-platforms.jpg +# - publish-schedule.jpg +# - publish-versions.jpg +# - publish-analytics.jpg + diff --git a/frontend/public/videos/.gitkeep b/frontend/public/videos/.gitkeep new file mode 100644 index 00000000..1cf9f5f9 --- /dev/null +++ b/frontend/public/videos/.gitkeep @@ -0,0 +1,9 @@ +# Blog Writer Phase Demo Videos +# Add your demo videos here: +# - phase1-research.mp4 +# - phase2-outline.mp4 +# - phase3-content.mp4 +# - phase4-seo.mp4 +# - phase5-metadata.mp4 +# - phase6-publish.mp4 + diff --git a/frontend/src/components/BlogWriter/BlogWriter.tsx b/frontend/src/components/BlogWriter/BlogWriter.tsx index db1b2cea..7b0af2e1 100644 --- a/frontend/src/components/BlogWriter/BlogWriter.tsx +++ b/frontend/src/components/BlogWriter/BlogWriter.tsx @@ -1,8 +1,11 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useRef, useCallback } from 'react'; +import { debug } from '../../utils/debug'; import { CopilotSidebar } from '@copilotkit/react-ui'; +import { useCopilotChatHeadless_c } from '@copilotkit/react-core'; import { useCopilotAction } from '@copilotkit/react-core'; import '@copilotkit/react-ui/styles.css'; -import { blogWriterApi } from '../../services/blogWriterApi'; +import WriterCopilotSidebar from './BlogWriterUtils/WriterCopilotSidebar'; +import { blogWriterApi, BlogSEOActionableRecommendation } from '../../services/blogWriterApi'; import { useOutlinePolling, useMediumGenerationPolling, useResearchPolling, useRewritePolling } from '../../hooks/usePolling'; import { useClaimFixer } from '../../hooks/useClaimFixer'; import { useMarkdownProcessor } from '../../hooks/useMarkdownProcessor'; @@ -26,10 +29,16 @@ import OutlineRefiner from './OutlineRefiner'; import { SEOProcessor } from './SEO'; import BlogWriterLanding from './BlogWriterLanding'; import { OutlineProgressModal } from './OutlineProgressModal'; +import TaskProgressModals from './BlogWriterUtils/TaskProgressModals'; import OutlineFeedbackForm from './OutlineFeedbackForm'; import { BlogEditor } from './WYSIWYG'; import { SEOAnalysisModal } from './SEOAnalysisModal'; import { SEOMetadataModal } from './SEOMetadataModal'; +import PhaseNavigation from './PhaseNavigation'; +import { usePhaseNavigation } from '../../hooks/usePhaseNavigation'; +import HeaderBar from './BlogWriterUtils/HeaderBar'; +import PhaseContent from './BlogWriterUtils/PhaseContent'; +import useBlogWriterCopilotActions from './BlogWriterUtils/useBlogWriterCopilotActions'; // Type assertion for CopilotKit action const useCopilotActionTyped = useCopilotAction as any; @@ -59,6 +68,7 @@ export const BlogWriter: React.FC = () => { flowAnalysisResults, setOutline, setTitleOptions, + setSelectedTitle, setSections, setSeoAnalysis, setGenMode, @@ -79,6 +89,227 @@ export const BlogWriter: React.FC = () => { handleContentSave } = useBlogWriterState(); + const [isSEOAnalysisModalOpen, setIsSEOAnalysisModalOpen] = useState(false); + const [isSEOMetadataModalOpen, setIsSEOMetadataModalOpen] = useState(false); + const [seoRecommendationsApplied, setSeoRecommendationsApplied] = useState(false); + const lastSEOModalOpenRef = useRef(0); + + // Phase navigation hook + const { + phases, + currentPhase, + navigateToPhase, + resetUserSelection + } = usePhaseNavigation( + research, + outline, + outlineConfirmed, + Object.keys(sections).length > 0, + contentConfirmed, + seoAnalysis, + seoMetadata, + seoRecommendationsApplied + ); + + // Helper: run same checks as analyzeSEO and open modal + const runSEOAnalysisDirect = (): string => { + const hasSections = !!sections && Object.keys(sections).length > 0; + const hasResearch = !!research && !!(research as any).keyword_analysis; + if (!hasSections) return "No blog content available for SEO analysis. Please generate content first."; + if (!hasResearch) return "Research data is required for SEO analysis. Please run research first."; + // Prevent rapid re-opens + const now = Date.now(); + if (isSEOAnalysisModalOpen && now - lastSEOModalOpenRef.current < 1000) { + return "SEO analysis is already open."; + } + + // Mark content phase as done when user clicks "Next: Run SEO Analysis" + if (!contentConfirmed) { + setContentConfirmed(true); + debug.log('[BlogWriter] Content phase marked as done (SEO analysis triggered)'); + } + + setSeoRecommendationsApplied(false); + if (!isSEOAnalysisModalOpen) { + setIsSEOAnalysisModalOpen(true); + lastSEOModalOpenRef.current = now; + debug.log('[BlogWriter] SEO modal opened (direct)'); + } + return "Running SEO analysis of your blog content. This will analyze content structure, keyword optimization, readability, and provide actionable recommendations."; + }; + + const handleApplySeoRecommendations = useCallback(async ( + recommendations: BlogSEOActionableRecommendation[] + ) => { + if (!outline || outline.length === 0) { + throw new Error('An outline is required before applying recommendations.'); + } + + const sectionPayload = outline.map((section) => ({ + id: section.id, + heading: section.heading, + content: sections[section.id] ?? '', + })); + + const response = await blogWriterApi.applySeoRecommendations({ + title: selectedTitle || outline[0]?.heading || 'Untitled Blog', + sections: sectionPayload, + outline, + research: (research as any) || {}, + recommendations, + }); + + if (!response.success) { + throw new Error(response.error || 'Failed to apply recommendations.'); + } + + if (!response.sections || !Array.isArray(response.sections)) { + throw new Error('Recommendation response did not include updated sections.'); + } + + // Update sections - create new object reference to trigger React re-render + const newSections: Record = {}; + response.sections.forEach((section) => { + if (section.id && section.content) { + newSections[section.id] = section.content; + } + }); + + // Validate we have sections before updating + if (Object.keys(newSections).length === 0) { + throw new Error('No valid sections received from SEO recommendations application.'); + } + + // Validate sections have actual content + const sectionsWithContent = Object.values(newSections).filter(c => c && c.trim().length > 0); + if (sectionsWithContent.length === 0) { + throw new Error('SEO recommendations resulted in empty sections. Please try again.'); + } + + // Log detailed section info for debugging + const sectionIds = Object.keys(newSections); + const sectionSizes = sectionIds.map(id => ({ id, length: newSections[id]?.length || 0 })); + debug.log('[BlogWriter] Applied SEO recommendations: sections updated', { + sectionCount: sectionIds.length, + sectionsWithContent: sectionsWithContent.length, + sectionIds: sectionIds, + sectionSizes: sectionSizes, + totalContentLength: Object.values(newSections).reduce((sum, c) => sum + (c?.length || 0), 0) + }); + + // Update sections state + setSections(newSections); + + // Force a delay to ensure React processes the state update before proceeding + // This gives React time to re-render with new sections before phase navigation checks + await new Promise(resolve => setTimeout(resolve, 200)); + + setContinuityRefresh(Date.now()); + setFlowAnalysisCompleted(false); + setFlowAnalysisResults(null); + + if (response.title && response.title !== selectedTitle) { + setSelectedTitle(response.title); + } + + if (response.applied) { + setSeoAnalysis(prev => prev ? { ...prev, applied_recommendations: response.applied } : prev); + debug.log('[BlogWriter] SEO analysis state updated with applied recommendations'); + } + + // Mark recommendations as applied (this will trigger phase navigation check) + // But we'll stay in SEO phase to show updated content + setSeoRecommendationsApplied(true); + debug.log('[BlogWriter] seoRecommendationsApplied set to true'); + + // Ensure we stay in SEO phase to show updated content + // Force navigation to SEO phase if we're not already there (safeguard) + if (currentPhase !== 'seo') { + navigateToPhase('seo'); + debug.log('[BlogWriter] Forced navigation to SEO phase after applying recommendations'); + } else { + debug.log('[BlogWriter] Already in SEO phase, staying to show updated content'); + } + }, [outline, sections, selectedTitle, research, setSections, setSelectedTitle, setContinuityRefresh, setFlowAnalysisCompleted, setFlowAnalysisResults, setSeoAnalysis, currentPhase, navigateToPhase]); + + // Handle SEO analysis completion + const handleSEOAnalysisComplete = useCallback((analysis: any) => { + setSeoAnalysis(analysis); + debug.log('[BlogWriter] SEO analysis completed', { hasAnalysis: !!analysis }); + }, [setSeoAnalysis]); + + // Handle SEO modal close - mark SEO phase as done if not already marked + const handleSEOModalClose = useCallback(() => { + // Mark SEO phase as done when modal closes (even without applying recommendations) + if (!seoAnalysis) { + // Set a minimal valid seoAnalysis object to mark phase as complete + setSeoAnalysis({ + success: true, + overall_score: 0, + category_scores: {}, + analysis_summary: { + overall_grade: 'N/A', + status: 'Skipped', + strongest_category: 'N/A', + weakest_category: 'N/A', + key_strengths: [], + key_weaknesses: [], + ai_summary: 'SEO analysis was skipped by user' + }, + actionable_recommendations: [], + generated_at: new Date().toISOString() + }); + debug.log('[BlogWriter] SEO phase marked as done (modal closed without analysis)'); + } + setIsSEOAnalysisModalOpen(false); + debug.log('[BlogWriter] SEO modal closed'); + }, [seoAnalysis, setSeoAnalysis, setIsSEOAnalysisModalOpen]); + + // Mark SEO phase as completed when recommendations are applied + useEffect(() => { + if (seoRecommendationsApplied && seoAnalysis) { + // SEO phase is considered complete when recommendations are applied + // But stay in SEO phase to show updated content + debug.log('[BlogWriter] SEO recommendations applied, SEO phase marked as complete'); + + // Ensure we stay in SEO phase to show updated content (override auto-progression) + if (currentPhase !== 'seo' && Object.keys(sections).length > 0) { + navigateToPhase('seo'); + debug.log('[BlogWriter] Forced stay in SEO phase to show updated content'); + } + } + }, [seoRecommendationsApplied, seoAnalysis, currentPhase, sections, navigateToPhase]); + + // Track when outlines/content become available for the first time + const prevOutlineLenRef = useRef(outline.length); + const prevOutlineConfirmedRef = useRef(outlineConfirmed); + const prevContentConfirmedRef = useRef(contentConfirmed); + + useEffect(() => { + const prevLen = prevOutlineLenRef.current; + if (research && prevLen === 0 && outline.length > 0) { + resetUserSelection(); + } + prevOutlineLenRef.current = outline.length; + }, [research, outline.length, resetUserSelection]); + + // Only reset user selection when transitioning from not-confirmed to confirmed + useEffect(() => { + const wasConfirmed = prevOutlineConfirmedRef.current; + if (!wasConfirmed && outlineConfirmed && Object.keys(sections).length > 0) { + resetUserSelection(); // Allow auto-progression to content phase + } + prevOutlineConfirmedRef.current = outlineConfirmed; + }, [outlineConfirmed, sections, resetUserSelection]); + + useEffect(() => { + const wasConfirmed = prevContentConfirmedRef.current; + if (!wasConfirmed && contentConfirmed && seoAnalysis) { + resetUserSelection(); // Allow auto-progression to SEO phase + } + prevContentConfirmedRef.current = contentConfirmed; + }, [contentConfirmed, seoAnalysis, resetUserSelection]); + // Custom hooks for complex functionality const { buildFullMarkdown, buildUpdatedMarkdownForClaim, applyClaimFix } = useClaimFixer( outline, @@ -139,28 +370,63 @@ export const BlogWriter: React.FC = () => { onError: (err) => console.error('Rewrite failed:', err) }); - // Get context-aware suggestions based on current task status - const suggestions = useSuggestions( - research, - outline, - outlineConfirmed, - { isPolling: researchPolling.isPolling, currentStatus: researchPolling.currentStatus }, - { isPolling: outlinePolling.isPolling, currentStatus: outlinePolling.currentStatus }, - { isPolling: mediumPolling.isPolling, currentStatus: mediumPolling.currentStatus }, - Object.keys(sections).length > 0, // hasContent - flowAnalysisCompleted, // flowAnalysisCompleted state - contentConfirmed // contentConfirmed state - ); - // Add minimum display time for modal const [showModal, setShowModal] = useState(false); const [modalStartTime, setModalStartTime] = useState(null); const [isMediumGenerationStarting, setIsMediumGenerationStarting] = useState(false); const [showOutlineModal, setShowOutlineModal] = useState(false); - // SEO Analysis Modal state - const [isSEOAnalysisModalOpen, setIsSEOAnalysisModalOpen] = useState(false); - const [isSEOMetadataModalOpen, setIsSEOMetadataModalOpen] = useState(false); + const suggestions = useSuggestions({ + research, + outline, + outlineConfirmed, + researchPolling: { isPolling: researchPolling.isPolling, currentStatus: researchPolling.currentStatus }, + outlinePolling: { isPolling: outlinePolling.isPolling, currentStatus: outlinePolling.currentStatus }, + mediumPolling: { isPolling: mediumPolling.isPolling, currentStatus: mediumPolling.currentStatus }, + hasContent: Object.keys(sections).length > 0, + flowAnalysisCompleted, + contentConfirmed, + seoAnalysis, + seoMetadata, + seoRecommendationsApplied, + }); + + // Drive CopilotKit suggestions programmatically + const copilotHeadless = (useCopilotChatHeadless_c as any)?.(); + const setSuggestionsRef = useRef(null); + useEffect(() => { + setSuggestionsRef.current = copilotHeadless?.setSuggestions; + }, [copilotHeadless]); + + const suggestionsPayload = React.useMemo( + () => (Array.isArray(suggestions) ? suggestions.map((s: any) => ({ title: s.title, message: s.message })) : []), + [suggestions] + ); + const prevSuggestionsRef = useRef("__init__"); + const suggestionsJson = React.useMemo(() => JSON.stringify(suggestionsPayload), [suggestionsPayload]); + useEffect(() => { + try { + if (!setSuggestionsRef.current) return; + if (suggestionsJson !== prevSuggestionsRef.current) { + setSuggestionsRef.current(suggestionsPayload); + debug.log('[BlogWriter] Copilot suggestions pushed', { count: suggestionsPayload.length }); + prevSuggestionsRef.current = suggestionsJson; + } + } catch {} + }, [suggestionsJson, suggestionsPayload]); + + const handlePhaseClick = useCallback((phaseId: string) => { + navigateToPhase(phaseId); + if (phaseId === 'seo') { + if (seoAnalysis) { + setIsSEOAnalysisModalOpen(true); + debug.log('[BlogWriter] SEO modal opened (phase navigation)'); + } else { + runSEOAnalysisDirect(); + } + } + }, [navigateToPhase, seoAnalysis, runSEOAnalysisDirect]); + const outlineGenRef = useRef(null); useEffect(() => { if ((mediumPolling.isPolling || rewritePolling.isPolling || isMediumGenerationStarting) && !showModal) { @@ -214,96 +480,73 @@ export const BlogWriter: React.FC = () => { progressCount: mediumPolling.progressMessages.length }); - // Debug SEO modal state - console.log('πŸ” SEO Analysis Modal state:', { - isSEOAnalysisModalOpen, - hasResearch: !!research, - hasContent: !!sections && Object.keys(sections).length > 0, - researchKeys: research ? Object.keys(research) : [], - sectionsKeys: sections ? Object.keys(sections) : [] - }); + // Log critical state changes only (reduce noise) + const lastPhaseRef = useRef(''); + const lastSeoOpenRef = useRef(false); + const lastSectionsLenRef = useRef(0); - // Debug action registration - console.log('πŸ“‹ CopilotKit Actions Registered:', ['confirmBlogContent', 'analyzeSEO']); - - // Copilot action for confirming blog content - useCopilotActionTyped({ - name: "confirmBlogContent", - description: "Confirm that the blog content is ready and move to the next stage (SEO analysis)", - parameters: [], - handler: async () => { - console.log('Blog content confirmed by user'); - setContentConfirmed(true); - return "Blog content has been confirmed! You can now proceed with SEO analysis and publishing."; + useEffect(() => { + if (currentPhase !== lastPhaseRef.current) { + debug.log('[BlogWriter] Phase changed', { currentPhase }); + lastPhaseRef.current = currentPhase; } - }); + }, [currentPhase]); - // Copilot action for running SEO analysis - useCopilotActionTyped({ - name: "analyzeSEO", - description: "Analyze the blog content for SEO optimization and provide detailed recommendations", - parameters: [], - handler: async () => { - console.log('πŸš€ SEO Analysis Action Triggered!'); - console.log('Current modal state before:', isSEOAnalysisModalOpen); - console.log('Sections available:', !!sections && Object.keys(sections).length > 0); - console.log('Research data available:', !!research && !!research.keyword_analysis); - - // Check if we have content to analyze - if (!sections || Object.keys(sections).length === 0) { - console.log('❌ No content available for SEO analysis'); - return "No blog content available for SEO analysis. Please generate content first."; + useEffect(() => { + const open = isSEOAnalysisModalOpen; + if (open !== lastSeoOpenRef.current) { + debug.log('[BlogWriter] SEO modal', { isOpen: open }); + lastSeoOpenRef.current = open; + } + }, [isSEOAnalysisModalOpen]); + + useEffect(() => { + const len = Object.keys(sections || {}).length; + if (len !== lastSectionsLenRef.current) { + debug.log('[BlogWriter] Sections updated', { count: len }); + lastSectionsLenRef.current = len; + } + }, [sections]); + + useEffect(() => { + debug.log('[BlogWriter] Suggestions updated', { suggestions }); + }, [suggestions]); + + // Force-sync Copilot suggestions right after SEO recommendations applied (guarded by previous suggestions key) + useEffect(() => { + if (!seoAnalysis || !seoRecommendationsApplied || !setSuggestionsRef.current) return; + try { + if (suggestionsJson !== prevSuggestionsRef.current) { + setSuggestionsRef.current(suggestionsPayload); + debug.log('[BlogWriter] Forced Copilot suggestions sync after SEO recommendations applied', { count: suggestionsPayload.length }); + prevSuggestionsRef.current = suggestionsJson; } - - // Check if we have research data - if (!research || !research.keyword_analysis) { - console.log('❌ No research data available for SEO analysis'); - return "Research data is required for SEO analysis. Please run research first."; - } - - // Open SEO analysis modal - console.log('βœ… All checks passed, opening SEO analysis modal'); + } catch (e) { + console.error('Failed to push Copilot suggestions after SEO apply:', e); + } + }, [seoAnalysis, seoRecommendationsApplied, suggestionsJson, suggestionsPayload]); + + const confirmBlogContentCb = useCallback(() => { + debug.log('[BlogWriter] Blog content confirmed by user'); + setContentConfirmed(true); + resetUserSelection(); + setSeoRecommendationsApplied(false); + navigateToPhase('seo'); + setTimeout(() => { setIsSEOAnalysisModalOpen(true); - console.log('Modal state set to true'); - - return "Running SEO analysis of your blog content. This will analyze content structure, keyword optimization, readability, and provide actionable recommendations."; - } - }); + debug.log('[BlogWriter] SEO modal opened (confirmβ†’direct)'); + }, 0); + return "βœ… Blog content has been confirmed! Running SEO analysis now."; + }, [setContentConfirmed, resetUserSelection, navigateToPhase, setIsSEOAnalysisModalOpen]); - // Generate SEO Metadata Action - useCopilotActionTyped({ - name: "generateSEOMetadata", - description: "Generate comprehensive SEO metadata including titles, descriptions, Open Graph tags, Twitter cards, and structured data", - parameters: [ - { - name: "title", - type: "string", - description: "Optional blog title to use for metadata generation", - required: false - } - ], - handler: async ({ title }: { title?: string }) => { - console.log('πŸš€ Generate SEO Metadata Action Triggered!'); - console.log('Title provided:', title); - console.log('Selected title:', selectedTitle); - console.log('Sections available:', !!sections && Object.keys(sections).length > 0); - console.log('Research data available:', !!research && !!research.keyword_analysis); - - // Check if we have content to generate metadata for - if (!sections || Object.keys(sections).length === 0) { - return "Please generate blog content first before creating SEO metadata. Use the content generation features to create your blog post."; - } - - if (!research || !research.keyword_analysis) { - return "Please complete research first to get keyword data for SEO metadata generation. Use the research features to gather keyword insights."; - } - - // Open the SEO metadata modal - setIsSEOMetadataModalOpen(true); - console.log('SEO Metadata modal opened'); - - return "Opening SEO metadata generator! This will create optimized titles, descriptions, Open Graph tags, Twitter cards, and structured data for your blog post."; - } + useBlogWriterCopilotActions({ + isSEOAnalysisModalOpen, + lastSEOModalOpenRef, + runSEOAnalysisDirect, + confirmBlogContent: confirmBlogContentCb, + sections, + research, + openSEOMetadata: () => setIsSEOMetadataModalOpen(true), }); @@ -366,6 +609,7 @@ export const BlogWriter: React.FC = () => { {/* New extracted functionality components */} setOutlineTaskId(taskId)} onPollingStart={(taskId) => outlinePolling.startPolling(taskId)} @@ -395,241 +639,70 @@ export const BlogWriter: React.FC = () => { {!research ? ( { - // This will trigger the copilot to start the research process - // The user can then interact with the copilot to begin research + // Trigger the copilot to start the research process }} /> ) : ( <> -
-

AI Blog Writer

-
-
-
- {research && outline.length === 0 && } - {outline.length > 0 && ( -
- {outlineConfirmed ? ( - /* WYSIWYG Editor - Show when outline is confirmed */ - - ) : ( - /* Outline Editor - Show when outline is not confirmed */ - <> - {/* Enhanced Title Selection */} - - - - {/* Enhanced Outline Editor */} - blogWriterApi.refineOutline({ outline, operation: op, section_id: id, payload }).then((res: any) => setOutline(res.outline))} - /> - - {/* Draft/Polished Mode Toggle */} -
- - -
- - {outline.map(s => ( -
-
-

{s.heading}

- {/* Continuity badge */} - {sections[s.id] && ( - - )} -
- {sections[s.id] ? ( - <> -
{sections[s.id]}
- - - ) : ( -
Ask the copilot to generate this section.
- )} -
- ))} - - )} -
- )} -
-
+ + )} - { - // Get current state information - const hasResearch = research !== null; - const hasOutline = outline.length > 0; - const isOutlineConfirmed = outlineConfirmed; - const researchInfo = hasResearch ? { - sources: research.sources?.length || 0, - queries: research.search_queries?.length || 0, - angles: research.suggested_angles?.length || 0, - primaryKeywords: research.keyword_analysis?.primary || [], - searchIntent: research.keyword_analysis?.search_intent || 'informational' - } : null; - - const outlineContext = hasOutline ? ` -OUTLINE DETAILS: -- Total sections: ${outline.length} -- Section headings: ${outline.map(s => s.heading).join(', ')} -- Total target words: ${outline.reduce((sum, s) => sum + (s.target_words || 0), 0)} -- Section breakdown: ${outline.map(s => `${s.heading} (${s.target_words || 0} words, ${s.subheadings?.length || 0} subheadings, ${s.key_points?.length || 0} key points)`).join('; ')} -` : ''; - - const toolGuide = ` -You are the ALwrity Blog Writing Assistant. You MUST call the appropriate frontend actions (tools) to fulfill user requests. - -CURRENT STATE: -${hasResearch && researchInfo ? ` -βœ… RESEARCH COMPLETED: -- Found ${researchInfo.sources} sources with Google Search grounding -- Generated ${researchInfo.queries} search queries -- Created ${researchInfo.angles} content angles -- Primary keywords: ${researchInfo.primaryKeywords.join(', ')} -- Search intent: ${researchInfo.searchIntent} -` : '❌ No research completed yet'} - -${hasOutline ? `βœ… OUTLINE GENERATED: ${outline.length} sections created${isOutlineConfirmed ? ' (CONFIRMED)' : ' (PENDING CONFIRMATION)'}` : '❌ No outline generated yet'} -${outlineContext} - -Available tools: -- getResearchKeywords(prompt?: string) - Get keywords from user for research -- performResearch(formData: string) - Perform research with collected keywords (formData is JSON string with keywords and blogLength) -- researchTopic(keywords: string, industry?: string, target_audience?: string) -- chatWithResearchData(question: string) - Chat with research data to explore insights and get recommendations -- generateOutline() -- createOutlineWithCustomInputs(customInstructions: string) - Create outline with user's custom instructions -- refineOutline(prompt?: string) - Refine outline based on user feedback -- chatWithOutline(question?: string) - Chat with outline to get insights and ask questions about content structure -- confirmOutlineAndGenerateContent() - Confirm outline and mark as ready for content generation (does NOT auto-generate content) -- generateSection(sectionId: string) -- generateAllSections() -- refineOutlineStructure(operation: add|remove|move|merge|rename, sectionId?: string, payload?: object) -- enhanceSection(sectionId: string, focus?: string) - Enhance a specific section with AI improvements -- optimizeOutline(focus?: string) - Optimize entire outline for better flow, SEO, and engagement -- rebalanceOutline(targetWords?: number) - Rebalance word count distribution across sections -- confirmBlogContent() - Confirm that blog content is ready and move to SEO stage -- analyzeSEO() - Analyze SEO for blog content with comprehensive insights and visual interface -- generateSEOMetadata(title?: string) -- publishToPlatform(platform: 'wix'|'wordpress', schedule_time?: string) - - CRITICAL BEHAVIOR & USER GUIDANCE: - - When user wants to research ANY topic, IMMEDIATELY call getResearchKeywords() to get their input - - When user asks to research something, call getResearchKeywords() first to collect their keywords - - After getResearchKeywords() completes, IMMEDIATELY call performResearch() with the collected data - - USER GUIDANCE STRATEGY: - - After research completion, ALWAYS guide user toward outline creation as the next step - - If user wants to explore research data, use chatWithResearchData() but then guide them to outline creation - - If user has specific outline requirements, use createOutlineWithCustomInputs() with their instructions - - When user asks for outline, call generateOutline() or createOutlineWithCustomInputs() based on their needs - - After outline generation, ALWAYS guide user to review and confirm the outline - - If user wants to discuss the outline, use chatWithOutline() to provide insights and answer questions - - If user wants to refine the outline, use refineOutline() to collect their feedback and refine - - When user says "I confirm the outline" or "I confirm the outline and am ready to generate content" or clicks "Confirm & Generate Content", IMMEDIATELY call confirmOutlineAndGenerateContent() - DO NOT ask for additional confirmation - - CRITICAL: If user explicitly confirms the outline, do NOT ask "are you sure?" or "please confirm" - the confirmation is already given - - Only after outline confirmation, show content generation suggestions and wait for user to explicitly request content generation - - When user asks to generate content before outline confirmation, remind them to confirm the outline first - - Content generation should ONLY happen when user explicitly clicks "Generate all sections" or "Generate [specific section]" - - When user has generated content and wants to rewrite, use rewriteBlog() to collect feedback and rewriteBlog() to process - - For rewrite requests, collect detailed feedback about what they want to change, tone, audience, and focus - - After content generation, guide users to review and confirm their content before moving to SEO stage - - When user says "I have reviewed and confirmed my blog content is ready for the next stage" or clicks "Next: Confirm Blog Content", IMMEDIATELY call confirmBlogContent() - DO NOT ask for additional confirmation - - CRITICAL: If user explicitly confirms blog content, do NOT ask "are you sure?" or "please confirm" - the confirmation is already given - - Only after content confirmation, show SEO analysis and publishing suggestions - - When user asks for SEO analysis before content confirmation, remind them to confirm the content first - - For SEO analysis, ALWAYS use analyzeSEO() - this is the ONLY SEO analysis tool available and provides comprehensive insights with visual interface - - IMPORTANT: There is NO "basic" or "simple" SEO analysis - only the comprehensive one. Do NOT mention multiple SEO analysis options - - ENGAGEMENT TACTICS: - - DO NOT ask for clarification - take action immediately with the information provided - - Always call the appropriate tool instead of just talking about what you could do - - Be aware of the current state and reference research results when relevant - - Guide users through the process: Research β†’ Outline β†’ Outline Review & Confirmation β†’ Content β†’ Content Review & Confirmation β†’ SEO β†’ Publish - - Use encouraging language and highlight progress made - - If user seems lost, remind them of the current stage and suggest the next step - - When research is complete, emphasize the value of the data found and guide to outline creation - - When outline is generated, emphasize the importance of reviewing and confirming before content generation - - Encourage users to make small manual edits to the outline UI before using AI for major changes -`; - return [toolGuide, additional].filter(Boolean).join('\n\n'); - }} + research={research} + outline={outline} + outlineConfirmed={outlineConfirmed} /> - {/* Outline Progress Modal */} - {/* Outline modal */} - m.message)} - latestMessage={outlinePolling.progressMessages.length > 0 ? outlinePolling.progressMessages[outlinePolling.progressMessages.length - 1].message : ''} - error={outlinePolling.error} - /> - - {/* Medium generation / Rewrite modal */} - m.message) : mediumPolling.progressMessages.map(m => m.message)} - latestMessage={rewritePolling.isPolling ? - (rewritePolling.progressMessages.length > 0 ? rewritePolling.progressMessages[rewritePolling.progressMessages.length - 1].message : '') : - (mediumPolling.progressMessages.length > 0 ? mediumPolling.progressMessages[mediumPolling.progressMessages.length - 1].message : '') - } - error={rewritePolling.isPolling ? rewritePolling.error : mediumPolling.error} - titleOverride={rewritePolling.isPolling ? 'πŸ”„ Rewriting Your Blog' : 'πŸ“ Generating Your Blog Content'} + {/* SEO Analysis Modal */} setIsSEOAnalysisModalOpen(false)} + onClose={handleSEOModalClose} blogContent={buildFullMarkdown()} blogTitle={selectedTitle} researchData={research} - onApplyRecommendations={(recommendations) => { - console.log('Applying SEO recommendations:', recommendations); - // TODO: Implement recommendation application logic - }} + onApplyRecommendations={handleApplySeoRecommendations} + onAnalysisComplete={handleSEOAnalysisComplete} /> {/* SEO Metadata Modal */} @@ -639,10 +712,14 @@ Available tools: blogContent={buildFullMarkdown()} blogTitle={selectedTitle} researchData={research} + outline={outline} + seoAnalysis={seoAnalysis} onMetadataGenerated={(metadata) => { console.log('SEO metadata generated:', metadata); setSeoMetadata(metadata); - // TODO: Implement metadata application logic + // Metadata is now saved and will be used when publishing to WordPress/Wix + // The metadata includes all SEO fields (title, description, tags, Open Graph, etc.) + // Publisher component will use this metadata when calling publish API }} /> diff --git a/frontend/src/components/BlogWriter/BlogWriterLanding.tsx b/frontend/src/components/BlogWriter/BlogWriterLanding.tsx index 1b5f4c7d..6b198fc0 100644 --- a/frontend/src/components/BlogWriter/BlogWriterLanding.tsx +++ b/frontend/src/components/BlogWriter/BlogWriterLanding.tsx @@ -1,5 +1,6 @@ import React, { useState } from 'react'; import { useCopilotTrigger } from '../../hooks/useCopilotTrigger'; +import BlogWriterPhasesSection from './BlogWriterPhasesSection'; interface BlogWriterLandingProps { onStartWriting: () => void; @@ -198,7 +199,7 @@ const BlogWriterLanding: React.FC = ({ onStartWriting }) - {/* SuperPowers Modal */} + {/* SuperPowers Modal with 6 Phases */} {showSuperPowers && (
= ({ onStartWriting }) left: 0, right: 0, bottom: 0, - backgroundColor: 'rgba(0, 0, 0, 0.7)', + backgroundColor: 'rgba(0, 0, 0, 0.95)', display: 'flex', - alignItems: 'center', + alignItems: 'flex-start', justifyContent: 'center', - zIndex: 1000 + zIndex: 1000, + overflowY: 'auto' }}>
{/* Modal Header */} @@ -271,69 +270,82 @@ const BlogWriterLanding: React.FC = ({ onStartWriting })
- {/* SuperPowers Grid */} -
- {superPowers.map((power, index) => ( -
{ - e.currentTarget.style.transform = 'translateY(-4px)'; - e.currentTarget.style.boxShadow = '0 8px 25px rgba(0,0,0,0.1)'; - e.currentTarget.style.borderColor = '#1976d2'; - }} - onMouseLeave={(e) => { - e.currentTarget.style.transform = 'translateY(0)'; - e.currentTarget.style.boxShadow = 'none'; - e.currentTarget.style.borderColor = '#e0e0e0'; - }} - > -
+ {/* 6 Phases Section */} + + + {/* Quick SuperPowers Grid */} +
+

+ Quick Feature Overview +

+
+ {superPowers.map((power, index) => ( +
{ + e.currentTarget.style.transform = 'translateY(-4px)'; + e.currentTarget.style.boxShadow = '0 8px 25px rgba(0,0,0,0.1)'; + e.currentTarget.style.borderColor = '#1976d2'; + }} + onMouseLeave={(e) => { + e.currentTarget.style.transform = 'translateY(0)'; + e.currentTarget.style.boxShadow = 'none'; + e.currentTarget.style.borderColor = '#e0e0e0'; + }} + >
- {power.icon} +
+ {power.icon} +
+

+ {power.title} +

-

- {power.title} -

+ {power.description} +

-

- {power.description} -

-
- ))} + ))} +
{/* Modal Footer */} diff --git a/frontend/src/components/BlogWriter/BlogWriterPhasesSection.tsx b/frontend/src/components/BlogWriter/BlogWriterPhasesSection.tsx new file mode 100644 index 00000000..3a6acf84 --- /dev/null +++ b/frontend/src/components/BlogWriter/BlogWriterPhasesSection.tsx @@ -0,0 +1,576 @@ +import React, { useState } from 'react'; +import { Container, Grid, Card, CardContent, Typography, Box, Stack, Chip } from '@mui/material'; +import { CheckCircle, AutoAwesome } from '@mui/icons-material'; + +interface PhaseFeature { + title: string; + description: string; + details: string[]; + imagePlaceholder: string; +} + +interface BlogPhase { + id: string; + name: string; + icon: string; + shortDescription: string; + features: PhaseFeature[]; + technicalDetails: { + aiModel: string; + promptType: string; + outputFormat: string; + integration: string; + }; + videoPlaceholder: string; +} + +const BlogWriterPhasesSection: React.FC = () => { + const [activePhase, setActivePhase] = useState(null); + + const phases: BlogPhase[] = [ + { + id: 'research', + name: 'Research & Strategy', + icon: 'πŸ”', + shortDescription: 'AI-powered comprehensive research with Google Search grounding, competitor analysis, and content gap identification', + features: [ + { + title: 'Google Search Grounding', + description: 'Real-time web research using Gemini\'s native Google Search integration', + details: [ + 'Single API call for comprehensive research', + 'Live web data from credible sources', + 'Automatic source extraction and citation', + 'Current trends and 2024-2025 insights', + 'Market analysis and forecasts' + ], + imagePlaceholder: '/images/research-google-grounding.jpg' + }, + { + title: 'Competitor Analysis', + description: 'Identify top players and content opportunities in your niche', + details: [ + 'Top competitor content analysis', + 'Content gap identification', + 'Unique angle discovery', + 'Market positioning insights', + 'Competitive advantage opportunities' + ], + imagePlaceholder: '/images/research-competitor.jpg' + }, + { + title: 'Keyword Intelligence', + description: 'Comprehensive keyword analysis with SEO opportunities', + details: [ + 'Primary, secondary, and long-tail keyword identification', + 'Search volume and competition analysis', + 'Keyword clustering and grouping', + 'Content optimization suggestions', + 'Target audience keyword mapping' + ], + imagePlaceholder: '/images/research-keywords.jpg' + }, + { + title: 'Content Angle Generation', + description: 'AI-generated compelling content angles for maximum engagement', + details: [ + '5 unique content angle suggestions', + 'Trending topic identification', + 'Audience pain point mapping', + 'Viral potential assessment', + 'Expert opinion synthesis' + ], + imagePlaceholder: '/images/research-angles.jpg' + } + ], + technicalDetails: { + aiModel: 'Gemini Pro with Google Search Grounding', + promptType: 'Comprehensive research prompt', + outputFormat: 'Structured JSON with sources, keywords, trends, competitors', + integration: 'GeminiGroundedProvider via research_service.py' + }, + videoPlaceholder: '/videos/phase1-research.mp4' + }, + { + id: 'outline', + name: 'Intelligent Outline', + icon: 'πŸ“', + shortDescription: 'AI-generated outlines with source mapping, grounding insights, and optimization recommendations', + features: [ + { + title: 'AI Outline Generation', + description: 'Comprehensive outline based on research with SEO optimization', + details: [ + 'Section-by-section breakdown', + 'Subheadings and key points', + 'Target word counts per section', + 'Logical flow and progression', + 'SEO-optimized structure' + ], + imagePlaceholder: '/images/outline-generation.jpg' + }, + { + title: 'Source Mapping & Grounding', + description: 'Connect each section to research sources with citations', + details: [ + 'Automatic source-to-section mapping', + 'Grounding support scores', + 'Citation suggestions', + 'Source credibility ratings', + 'Reference verification' + ], + imagePlaceholder: '/images/outline-grounding.jpg' + }, + { + title: 'Interactive Refinement', + description: 'Human-in-the-loop editing with AI assistance', + details: [ + 'Add, remove, merge sections', + 'Reorder and restructure', + 'AI enhancement suggestions', + 'Custom instructions support', + 'Multiple outline versions' + ], + imagePlaceholder: '/images/outline-refine.jpg' + }, + { + title: 'Title Generation', + description: 'Multiple SEO-optimized title options', + details: [ + 'AI-generated title variations', + 'SEO score per title', + 'Engagement potential analysis', + 'Keyword integration', + 'Click-through optimization' + ], + imagePlaceholder: '/images/outline-titles.jpg' + } + ], + technicalDetails: { + aiModel: 'Gemini Pro (provider-agnostic via llm_text_gen)', + promptType: 'Structured outline prompt with research context', + outputFormat: 'JSON outline with sections, headings, key_points, references', + integration: 'OutlineService via parallel_processor.py' + }, + videoPlaceholder: '/videos/phase2-outline.mp4' + }, + { + id: 'content', + name: 'Content Generation', + icon: '✨', + shortDescription: 'Section-by-section content generation with SEO optimization, context memory, and engagement improvements', + features: [ + { + title: 'Smart Content Generation', + description: 'AI-powered section writing with context awareness', + details: [ + 'Section-by-section generation', + 'Context memory across sections', + 'Smooth transitions between sections', + 'Consistent tone and style', + 'Natural keyword integration' + ], + imagePlaceholder: '/images/content-generation.jpg' + }, + { + title: 'Continuity Analysis', + description: 'Real-time flow and coherence monitoring', + details: [ + 'Narrative flow assessment', + 'Coherence scoring', + 'Transition quality analysis', + 'Tone consistency tracking', + 'Content quality metrics' + ], + imagePlaceholder: '/images/content-continuity.jpg' + }, + { + title: 'Source Integration', + description: 'Automatic citation and source reference', + details: [ + 'Relevant URL selection', + 'Natural citation insertion', + 'Source attribution', + 'Evidence-backed content', + 'Reference management' + ], + imagePlaceholder: '/images/content-sources.jpg' + }, + { + title: 'Medium Blog Mode', + description: 'Quick generation for Medium-style articles', + details: [ + 'Single-call full blog generation', + 'Medium-optimized formatting', + 'Engagement-focused structure', + 'SEO-ready output', + 'Fast turnaround option' + ], + imagePlaceholder: '/images/content-medium.jpg' + } + ], + technicalDetails: { + aiModel: 'Provider-agnostic (Gemini/HF via main_text_generation)', + promptType: 'Context-aware section prompt with research', + outputFormat: 'Markdown content with transitions and metrics', + integration: 'EnhancedContentGenerator with ContextMemory' + }, + videoPlaceholder: '/videos/phase3-content.mp4' + }, + { + id: 'seo', + name: 'SEO Analysis', + icon: 'πŸ“ˆ', + shortDescription: 'Advanced SEO analysis with actionable recommendations and AI-powered optimization', + features: [ + { + title: 'Comprehensive SEO Scoring', + description: 'Multi-dimensional SEO analysis across key factors', + details: [ + 'Overall SEO score (0-100)', + 'Structure optimization score', + 'Keyword optimization rating', + 'Readability assessment', + 'Quality metrics evaluation' + ], + imagePlaceholder: '/images/seo-scoring.jpg' + }, + { + title: 'Actionable Recommendations', + description: 'AI-powered improvement suggestions', + details: [ + 'Priority-ranked fixes', + 'Specific text improvements', + 'Keyword density optimization', + 'Heading structure suggestions', + 'Content enhancement ideas' + ], + imagePlaceholder: '/images/seo-recommendations.jpg' + }, + { + title: 'AI-Powered Content Refinement', + description: 'Automatically apply SEO recommendations', + details: [ + 'Smart content rewriting', + 'Preserves original intent', + 'Natural keyword integration', + 'Readability improvement', + 'Structure optimization' + ], + imagePlaceholder: '/images/seo-apply.jpg' + }, + { + title: 'Keyword Analysis', + description: 'Deep dive into keyword performance', + details: [ + 'Primary keyword density', + 'Semantic keyword usage', + 'Long-tail keyword opportunities', + 'Keyword distribution heatmap', + 'Optimization recommendations' + ], + imagePlaceholder: '/images/seo-keywords.jpg' + } + ], + technicalDetails: { + aiModel: 'Parallel non-AI analyzers + single AI call', + promptType: 'Structured SEO analysis prompt', + outputFormat: 'Comprehensive SEO report with scores and recommendations', + integration: 'BlogContentSEOAnalyzer with parallel processing' + }, + videoPlaceholder: '/videos/phase4-seo.mp4' + }, + { + id: 'metadata', + name: 'SEO Metadata', + icon: '🎯', + shortDescription: 'Optimized metadata generation for titles, descriptions, Open Graph, Twitter cards, and structured data', + features: [ + { + title: 'Comprehensive Metadata', + description: 'All-in-one SEO metadata generation', + details: [ + 'SEO-optimized title (50-60 chars)', + 'Meta description with CTA', + 'URL slug optimization', + 'Blog tags and categories', + 'Social hashtags' + ], + imagePlaceholder: '/images/metadata-comprehensive.jpg' + }, + { + title: 'Open Graph & Twitter Cards', + description: 'Rich social media previews', + details: [ + 'OG title and description', + 'Twitter card optimization', + 'Image preview settings', + 'Social engagement boost', + 'Click-through optimization' + ], + imagePlaceholder: '/images/metadata-social.jpg' + }, + { + title: 'Structured Data', + description: 'Schema.org markup for rich snippets', + details: [ + 'Article schema', + 'Organization markup', + 'Breadcrumb schema', + 'FAQ schema support', + 'Enhanced search results' + ], + imagePlaceholder: '/images/metadata-schema.jpg' + }, + { + title: 'Multi-Format Output', + description: 'Ready-to-use metadata in all formats', + details: [ + 'HTML meta tags', + 'JSON-LD structured data', + 'WordPress export format', + 'Wix integration format', + 'One-click copy options' + ], + imagePlaceholder: '/images/metadata-export.jpg' + } + ], + technicalDetails: { + aiModel: 'Maximum 2 AI calls for comprehensive metadata', + promptType: 'Personalized metadata prompt with context', + outputFormat: 'Complete metadata package (title, desc, tags, schema)', + integration: 'BlogSEOMetadataGenerator with optimization' + }, + videoPlaceholder: '/videos/phase5-metadata.mp4' + }, + { + id: 'publish', + name: 'Publish & Distribute', + icon: 'πŸš€', + shortDescription: 'Direct publishing to WordPress, Wix, Medium, and other platforms with scheduling', + features: [ + { + title: 'Multi-Platform Publishing', + description: 'Publish to multiple platforms simultaneously', + details: [ + 'WordPress direct publishing', + 'Wix blog integration', + 'Medium publishing', + 'Custom blog platforms', + 'API integrations' + ], + imagePlaceholder: '/images/publish-platforms.jpg' + }, + { + title: 'Content Scheduling', + description: 'Schedule posts for optimal timing', + details: [ + 'Time-based scheduling', + 'Timezone management', + 'Bulk scheduling support', + 'Calendar integration', + 'Reminder notifications' + ], + imagePlaceholder: '/images/publish-schedule.jpg' + }, + { + title: 'Revision Management', + description: 'Track and manage content versions', + details: [ + 'Version history', + 'Change tracking', + 'Rollback capabilities', + 'A/B testing support', + 'Performance comparison' + ], + imagePlaceholder: '/images/publish-versions.jpg' + }, + { + title: 'Analytics Integration', + description: 'Post-publish performance tracking', + details: [ + 'View count tracking', + 'Engagement metrics', + 'SEO performance', + 'Traffic analysis', + 'Conversion tracking' + ], + imagePlaceholder: '/images/publish-analytics.jpg' + } + ], + technicalDetails: { + aiModel: 'Platform-specific API integrations', + promptType: 'N/A - publishing only', + outputFormat: 'Published content with URL', + integration: 'Platform APIs via Publisher component' + }, + videoPlaceholder: '/videos/phase6-publish.mp4' + } + ]; + + return ( + + + {/* Section Title */} + + + Complete AI Blog Writing Workflow + + + Six powerful phases that transform your ideas into SEO-optimized, engaging blog content + + + + {/* Phase Cards */} + + {phases.map((phase, index) => ( + + setActivePhase(activePhase === index ? null : index)} + > + + + + {phase.icon} + + + + {phase.name} + + + {phase.shortDescription} + + + + + + {activePhase === index && ( + + {/* Video Placeholder */} + + + πŸŽ₯ Video: {phase.videoPlaceholder} + + + + {/* Features Grid */} + + {phase.features.map((feature, idx) => ( + + + + + πŸ“· Image + + + + {feature.title} + + + {feature.description} + + + {feature.details.slice(0, 3).map((detail, i) => ( + + + + {detail} + + + ))} + + + + ))} + + + {/* Technical Details */} + + + + Technical Implementation + + + + AI Model + {phase.technicalDetails.aiModel} + + + Output Format + {phase.technicalDetails.outputFormat} + + + Prompt Type + {phase.technicalDetails.promptType} + + + Integration + + {phase.technicalDetails.integration} + + + + + + )} + + + + ))} + + + + ); +}; + +export default BlogWriterPhasesSection; + diff --git a/frontend/src/components/BlogWriter/BlogWriterUtils/HeaderBar.tsx b/frontend/src/components/BlogWriter/BlogWriterUtils/HeaderBar.tsx new file mode 100644 index 00000000..14fbe899 --- /dev/null +++ b/frontend/src/components/BlogWriter/BlogWriterUtils/HeaderBar.tsx @@ -0,0 +1,41 @@ +import React from 'react'; +import PhaseNavigation from '../PhaseNavigation'; + +interface HeaderBarProps { + phases: any[]; + currentPhase: string; + onPhaseClick: (phaseId: string) => void; +} + +export const HeaderBar: React.FC = ({ phases, currentPhase, onPhaseClick }) => { + return ( +
+
+

AI Blog Writer

+
+ A +
+
+ +
+ ); +}; + +export default HeaderBar; + + diff --git a/frontend/src/components/BlogWriter/BlogWriterUtils/OutlineCtaBanner.tsx b/frontend/src/components/BlogWriter/BlogWriterUtils/OutlineCtaBanner.tsx new file mode 100644 index 00000000..c50c7584 --- /dev/null +++ b/frontend/src/components/BlogWriter/BlogWriterUtils/OutlineCtaBanner.tsx @@ -0,0 +1,21 @@ +import React from 'react'; + +interface OutlineCtaBannerProps { + onGenerate: () => void; +} + +const OutlineCtaBanner: React.FC = ({ onGenerate }) => { + return ( +
+ Next step: generate your outline from research. + +
+ ); +}; + +export default OutlineCtaBanner; diff --git a/frontend/src/components/BlogWriter/BlogWriterUtils/PhaseContent.tsx b/frontend/src/components/BlogWriter/BlogWriterUtils/PhaseContent.tsx new file mode 100644 index 00000000..4247d788 --- /dev/null +++ b/frontend/src/components/BlogWriter/BlogWriterUtils/PhaseContent.tsx @@ -0,0 +1,185 @@ +import React from 'react'; +import ResearchResults from '../ResearchResults'; +import EnhancedTitleSelector from '../EnhancedTitleSelector'; +import EnhancedOutlineEditor from '../EnhancedOutlineEditor'; +import { BlogEditor } from '../WYSIWYG'; +import OutlineCtaBanner from './OutlineCtaBanner'; + +interface PhaseContentProps { + currentPhase: string; + research: any; + outline: any[]; + outlineConfirmed: boolean; + titleOptions: any[]; + selectedTitle?: string | null; + researchTitles: any[]; + aiGeneratedTitles: any[]; + sourceMappingStats: any; + groundingInsights: any; + optimizationResults: any; + researchCoverage: any; + setOutline: (o: any) => void; + sections: Record; + handleContentUpdate: any; + handleContentSave: any; + continuityRefresh: number | null; + flowAnalysisResults: any; + outlineGenRef: React.RefObject; + blogWriterApi: any; + contentConfirmed: boolean; + seoAnalysis: any; + seoMetadata: any; + onTitleSelect: any; + onCustomTitle: any; +} + +export const PhaseContent: React.FC = ({ + currentPhase, + research, + outline, + outlineConfirmed, + titleOptions, + selectedTitle, + researchTitles, + aiGeneratedTitles, + sourceMappingStats, + groundingInsights, + optimizationResults, + researchCoverage, + setOutline, + sections, + handleContentUpdate, + handleContentSave, + continuityRefresh, + flowAnalysisResults, + outlineGenRef, + blogWriterApi, + contentConfirmed, + seoAnalysis, + seoMetadata, + onTitleSelect, + onCustomTitle +}) => { + return ( +
+
+ {currentPhase === 'research' && ( + <> + {research ? ( + + ) : ( +
+

Start Your Research

+

Use the copilot to begin researching your blog topic.

+
+ )} + + )} + + {currentPhase === 'outline' && research && ( + <> + {outline.length === 0 && ( + outlineGenRef.current?.generateNow()} /> + )} + {outline.length > 0 ? ( + <> + + blogWriterApi.refineOutline({ outline, operation: op, section_id: id, payload }).then((res: any) => setOutline(res.outline))} + /> + + ) : ( +
+

Create Your Outline

+

Use the copilot to generate an outline based on your research.

+
+ )} + + )} + + {currentPhase === 'content' && outline.length > 0 && ( + <> + {outlineConfirmed ? ( + + ) : ( +
+

Confirm Your Outline

+

Review and confirm your outline before generating content.

+
+ )} + + )} + + {currentPhase === 'seo' && contentConfirmed && outline.length > 0 && outlineConfirmed && ( + <> + {Object.keys(sections).length > 0 && Object.values(sections).some(content => content && content.trim().length > 0) ? ( + + ) : ( +
+

Loading Content...

+

Please wait while your content is being optimized.

+
+ )} + + )} + + {/* Fallback for SEO phase if conditions not met */} + {currentPhase === 'seo' && (!contentConfirmed || outline.length === 0 || !outlineConfirmed) && ( +
+

Optimize your blog for search engines.

+

Complete the content phase first to enable SEO optimization.

+
+ )} + + {currentPhase === 'publish' && seoAnalysis && seoMetadata && ( +
+

Publish Your Blog

+

Your blog is ready to publish!

+
+ )} +
+
+ ); +}; + +export default PhaseContent; + + diff --git a/frontend/src/components/BlogWriter/BlogWriterUtils/TaskProgressModals.tsx b/frontend/src/components/BlogWriter/BlogWriterUtils/TaskProgressModals.tsx new file mode 100644 index 00000000..34c6fd4e --- /dev/null +++ b/frontend/src/components/BlogWriter/BlogWriterUtils/TaskProgressModals.tsx @@ -0,0 +1,52 @@ +import React from 'react'; +import { OutlineProgressModal } from '../OutlineProgressModal'; + +interface PollingState { + isPolling: boolean; + currentStatus: string; + progressMessages: { message: string }[]; + error?: string | null; +} + +interface TaskProgressModalsProps { + showOutlineModal: boolean; + outlinePolling: PollingState; + showModal: boolean; + rewritePolling: PollingState; + mediumPolling: PollingState; +} + +const TaskProgressModals: React.FC = ({ + showOutlineModal, + outlinePolling, + showModal, + rewritePolling, + mediumPolling, +}) => { + return ( + <> + m.message)} + latestMessage={outlinePolling.progressMessages.length > 0 ? outlinePolling.progressMessages[outlinePolling.progressMessages.length - 1].message : ''} + error={outlinePolling.error ?? null} + /> + + m.message) : mediumPolling.progressMessages.map(m => m.message)} + latestMessage={rewritePolling.isPolling ? ( + rewritePolling.progressMessages.length > 0 ? rewritePolling.progressMessages[rewritePolling.progressMessages.length - 1].message : '' + ) : ( + mediumPolling.progressMessages.length > 0 ? mediumPolling.progressMessages[mediumPolling.progressMessages.length - 1].message : '' + )} + error={(rewritePolling.isPolling ? rewritePolling.error : mediumPolling.error) ?? null} + titleOverride={rewritePolling.isPolling ? 'πŸ”„ Rewriting Your Blog' : 'πŸ“ Generating Your Blog Content'} + /> + + ); +}; + +export default TaskProgressModals; diff --git a/frontend/src/components/BlogWriter/BlogWriterUtils/WriterCopilotSidebar.tsx b/frontend/src/components/BlogWriter/BlogWriterUtils/WriterCopilotSidebar.tsx new file mode 100644 index 00000000..c63ae40b --- /dev/null +++ b/frontend/src/components/BlogWriter/BlogWriterUtils/WriterCopilotSidebar.tsx @@ -0,0 +1,140 @@ +import React from 'react'; +import { CopilotSidebar } from '@copilotkit/react-ui'; +import '@copilotkit/react-ui/styles.css'; + +interface WriterCopilotSidebarProps { + suggestions: any[]; + research: any; + outline: any[]; + outlineConfirmed: boolean; +} + +export const WriterCopilotSidebar: React.FC = ({ + suggestions, + research, + outline, + outlineConfirmed, +}) => { + return ( + { + const hasResearch = research !== null; + const hasOutline = outline.length > 0; + const isOutlineConfirmed = outlineConfirmed; + const researchInfo = hasResearch + ? { + sources: research.sources?.length || 0, + queries: research.search_queries?.length || 0, + angles: research.suggested_angles?.length || 0, + primaryKeywords: research.keyword_analysis?.primary || [], + searchIntent: research.keyword_analysis?.search_intent || 'informational', + } + : null; + + const outlineContext = hasOutline + ? ` +OUTLINE DETAILS: +- Total sections: ${outline.length} +- Section headings: ${outline.map((s: any) => s.heading).join(', ')} +- Total target words: ${outline.reduce((sum: number, s: any) => sum + (s.target_words || 0), 0)} +- Section breakdown: ${outline + .map( + (s: any) => `${s.heading} (${s.target_words || 0} words, ${s.subheadings?.length || 0} subheadings, ${s.key_points?.length || 0} key points)` + ) + .join('; ')} +` + : ''; + + const toolGuide = ` +You are the ALwrity Blog Writing Assistant. You MUST call the appropriate frontend actions (tools) to fulfill user requests. + +CURRENT STATE: +${hasResearch && researchInfo ? ` +βœ… RESEARCH COMPLETED: +- Found ${researchInfo.sources} sources with Google Search grounding +- Generated ${researchInfo.queries} search queries +- Created ${researchInfo.angles} content angles +- Primary keywords: ${researchInfo.primaryKeywords.join(', ')} +- Search intent: ${researchInfo.searchIntent} +` : '❌ No research completed yet'} + +${hasOutline ? `βœ… OUTLINE GENERATED: ${outline.length} sections created${isOutlineConfirmed ? ' (CONFIRMED)' : ' (PENDING CONFIRMATION)'}` : '❌ No outline generated yet'} +${outlineContext} + +Available tools: +- getResearchKeywords(prompt?: string) - Get keywords from user for research +- performResearch(formData: string) - Perform research with collected keywords (formData is JSON string with keywords and blogLength) +- researchTopic(keywords: string, industry?: string, target_audience?: string) +- chatWithResearchData(question: string) - Chat with research data to explore insights and get recommendations +- generateOutline() +- createOutlineWithCustomInputs(customInstructions: string) - Create outline with user's custom instructions +- refineOutline(prompt?: string) - Refine outline based on user feedback +- chatWithOutline(question?: string) - Chat with outline to get insights and ask questions about content structure +- confirmOutlineAndGenerateContent() - Confirm outline and mark as ready for content generation (does NOT auto-generate content) +- generateSection(sectionId: string) +- generateAllSections() +- refineOutlineStructure(operation: add|remove|move|merge|rename, sectionId?: string, payload?: object) +- enhanceSection(sectionId: string, focus?: string) - Enhance a specific section with AI improvements +- optimizeOutline(focus?: string) - Optimize entire outline for better flow, SEO, and engagement +- rebalanceOutline(targetWords?: number) - Rebalance word count distribution across sections +- confirmBlogContent() - Confirm that blog content is ready and move to SEO stage +- analyzeSEO() - Analyze SEO for blog content with comprehensive insights and visual interface +- generateSEOMetadata(title?: string) +- publishToPlatform(platform: 'wix'|'wordpress', schedule_time?: string) + + CRITICAL BEHAVIOR & USER GUIDANCE: + - When user wants to research ANY topic, IMMEDIATELY call getResearchKeywords() to get their input + - When user asks to research something, call getResearchKeywords() first to collect their keywords + - After getResearchKeywords() completes, IMMEDIATELY call performResearch() with the collected data + + USER GUIDANCE STRATEGY: + - If the user's last message EXACTLY matches an available tool name (e.g., generateOutline, confirmOutlineAndGenerateContent, confirmBlogContent, analyzeSEO), IMMEDIATELY call that tool with default arguments and WITHOUT any additional questions or confirmations + - After research completion, ALWAYS guide user toward outline creation as the next step + - If user wants to explore research data, use chatWithResearchData() but then guide them to outline creation + - If user has specific outline requirements, use createOutlineWithCustomInputs() with their instructions + - When user asks for outline, call generateOutline() or createOutlineWithCustomInputs() based on their needs + - After outline generation, ALWAYS guide user to review and confirm the outline + - If user wants to discuss the outline, use chatWithOutline() to provide insights and answer questions + - If user wants to refine the outline, use refineOutline() to collect their feedback and refine + - When user says "I confirm the outline" or "I confirm the outline and am ready to generate content" or clicks "Confirm & Generate Content", IMMEDIATELY call confirmOutlineAndGenerateContent() - DO NOT ask for additional confirmation + - CRITICAL: If user explicitly confirms the outline, do NOT ask "are you sure?" or "please confirm" - the confirmation is already given + - Only after outline confirmation, show content generation suggestions and wait for user to explicitly request content generation + - When user asks to generate content before outline confirmation, remind them to confirm the outline first + - Content generation should ONLY happen when user explicitly clicks "Generate all sections" or "Generate [specific section]" + - When user has generated content and wants to rewrite, use rewriteBlog() to collect feedback and rewriteBlog() to process + - For rewrite requests, collect detailed feedback about what they want to change, tone, audience, and focus + - After content generation, guide users to review and confirm their content before moving to SEO stage + - When user says "I have reviewed and confirmed my blog content is ready for the next stage" or clicks "Next: Confirm Blog Content", IMMEDIATELY call confirmBlogContent() - DO NOT ask for additional confirmation + - CRITICAL: If user explicitly confirms blog content, do NOT ask "are you sure?" or "please confirm" - the confirmation is already given + - Only after content confirmation, show SEO analysis and publishing suggestions + - When user asks for SEO analysis before content confirmation, remind them to confirm the content first + - For SEO analysis, ALWAYS use analyzeSEO() - this is the ONLY SEO analysis tool available and provides comprehensive insights with visual interface + - IMPORTANT: There is NO "basic" or "simple" SEO analysis - only the comprehensive one. Do NOT mention multiple SEO analysis options + + ENGAGEMENT TACTICS: + - DO NOT ask for clarification - take action immediately with the information provided + - Always call the appropriate tool instead of just talking about what you could do + - Be aware of the current state and reference research results when relevant + - Guide users through the process: Research β†’ Outline β†’ Outline Review & Confirmation β†’ Content β†’ Content Review & Confirmation β†’ SEO β†’ Publish + - Use encouraging language and highlight progress made + - If user seems lost, remind them of the current stage and suggest the next step + - When research is complete, emphasize the value of the data found and guide to outline creation + - When outline is generated, emphasize the importance of reviewing and confirming before content generation + - Encourage users to make small manual edits to the outline UI before using AI for major changes +`; + return [toolGuide, additional].filter(Boolean).join('\n\n'); + }} + /> + ); +}; + +export default WriterCopilotSidebar; + + diff --git a/frontend/src/components/BlogWriter/BlogWriterUtils/useBlogWriterCopilotActions.ts b/frontend/src/components/BlogWriter/BlogWriterUtils/useBlogWriterCopilotActions.ts new file mode 100644 index 00000000..3213d569 --- /dev/null +++ b/frontend/src/components/BlogWriter/BlogWriterUtils/useBlogWriterCopilotActions.ts @@ -0,0 +1,90 @@ +import { useRef } from 'react'; +import { useCopilotAction } from '@copilotkit/react-core'; +import { debug } from '../../../utils/debug'; + +type ConfirmCb = () => string | Promise; +type AnalyzeCb = () => string | Promise; +type OpenMetadataCb = () => void; + +interface UseBlogWriterCopilotActionsParams { + isSEOAnalysisModalOpen: boolean; + lastSEOModalOpenRef: React.MutableRefObject; + runSEOAnalysisDirect: AnalyzeCb; + confirmBlogContent: ConfirmCb; + sections: Record; + research: any; + openSEOMetadata: OpenMetadataCb; +} + +// Consolidates all Copilot actions used by BlogWriter +export function useBlogWriterCopilotActions({ + isSEOAnalysisModalOpen, + lastSEOModalOpenRef, + runSEOAnalysisDirect, + confirmBlogContent, + sections, + research, + openSEOMetadata, +}: UseBlogWriterCopilotActionsParams) { + // Maintain the same any-cast pattern for parity with component + const useCopilotActionTyped = useCopilotAction as any; + + // confirmBlogContent + useCopilotActionTyped({ + name: 'confirmBlogContent', + description: 'Confirm that the blog content is ready and move to the next stage (SEO analysis)', + parameters: [], + handler: async () => { + const msg = await confirmBlogContent(); + return msg; + }, + }); + + // analyzeSEO + useCopilotActionTyped({ + name: 'analyzeSEO', + description: 'Analyze the blog content for SEO optimization and provide detailed recommendations', + parameters: [], + handler: async () => { + debug.log('[BlogWriter] SEO analysis action', { + modalOpen: isSEOAnalysisModalOpen, + hasSections: !!sections && Object.keys(sections).length > 0, + hasResearch: !!research && !!(research as any)?.keyword_analysis, + }); + const now = Date.now(); + if (isSEOAnalysisModalOpen || now - lastSEOModalOpenRef.current < 750) { + return 'SEO analysis is already open.'; + } + const msg = await runSEOAnalysisDirect(); + return msg; + }, + }); + + // generateSEOMetadata + useCopilotActionTyped({ + name: 'generateSEOMetadata', + description: 'Generate comprehensive SEO metadata including titles, descriptions, Open Graph tags, Twitter cards, and structured data', + parameters: [ + { + name: 'title', + type: 'string', + description: 'Optional blog title to use for metadata generation', + required: false, + }, + ], + handler: async ({ title }: { title?: string }) => { + if (!sections || Object.keys(sections).length === 0) { + return 'Please generate blog content first before creating SEO metadata. Use the content generation features to create your blog post.'; + } + if (!research || !research.keyword_analysis) { + return 'Please complete research first to get keyword data for SEO metadata generation. Use the research features to gather keyword insights.'; + } + openSEOMetadata(); + return 'Opening SEO metadata generator! This will create optimized titles, descriptions, Open Graph tags, Twitter cards, and structured data for your blog post.'; + }, + }); +} + +export default useBlogWriterCopilotActions; + + diff --git a/frontend/src/components/BlogWriter/ContinuityBadge.tsx b/frontend/src/components/BlogWriter/ContinuityBadge.tsx index 2efaa4dd..751e4769 100644 --- a/frontend/src/components/BlogWriter/ContinuityBadge.tsx +++ b/frontend/src/components/BlogWriter/ContinuityBadge.tsx @@ -1,5 +1,6 @@ import React, { useEffect, useState } from 'react'; import { blogWriterApi } from '../../services/blogWriterApi'; +import { debug } from '../../utils/debug'; interface Props { sectionId: string; @@ -17,36 +18,27 @@ export const ContinuityBadge: React.FC = ({ sectionId, refreshToken, disa // If we have flow analysis results, use them instead of API call if (flowAnalysisResults && flowAnalysisResults.sections) { - console.log('πŸ” [ContinuityBadge] Flow analysis results available:', flowAnalysisResults); - console.log('πŸ” [ContinuityBadge] Looking for section ID:', sectionId); - console.log('πŸ” [ContinuityBadge] Available section IDs:', flowAnalysisResults.sections.map((s: any) => s.section_id)); - const sectionAnalysis = flowAnalysisResults.sections.find((s: any) => s.section_id === sectionId); if (sectionAnalysis) { - console.log('πŸ” [ContinuityBadge] Found section analysis:', sectionAnalysis); if (mounted) { setMetrics({ - flow: sectionAnalysis.flow_score, // Already in decimal format (0.0-1.0) + flow: sectionAnalysis.flow_score, consistency: sectionAnalysis.consistency_score, progression: sectionAnalysis.progression_score }); } return; - } else { - console.log('πŸ” [ContinuityBadge] No matching section found for ID:', sectionId); } } // Fallback to API call if no flow analysis results - console.log('πŸ” [ContinuityBadge] Fetching continuity for section:', sectionId); + debug.log('[ContinuityBadge] fetching', { sectionId }); blogWriterApi.getContinuity(sectionId) .then(res => { - console.log('πŸ” [ContinuityBadge] Received continuity data:', res); if (mounted) setMetrics(res.continuity_metrics || null); }) .catch((error) => { - console.log('πŸ” [ContinuityBadge] Error fetching continuity:', error); - /* ignore */ + debug.error('[ContinuityBadge] fetch error', error); }); return () => { mounted = false; }; }, [sectionId, refreshToken, flowAnalysisResults]); diff --git a/frontend/src/components/BlogWriter/EnhancedOutlineEditor.tsx b/frontend/src/components/BlogWriter/EnhancedOutlineEditor.tsx index 37ae958c..5ff66b4b 100644 --- a/frontend/src/components/BlogWriter/EnhancedOutlineEditor.tsx +++ b/frontend/src/components/BlogWriter/EnhancedOutlineEditor.tsx @@ -2,6 +2,7 @@ import React, { useState } from 'react'; import { BlogOutlineSection, SourceMappingStats, GroundingInsights, OptimizationResults, ResearchCoverage } from '../../services/blogWriterApi'; import EnhancedOutlineInsights from './EnhancedOutlineInsights'; import OutlineIntelligenceChips from './OutlineIntelligenceChips'; +import ImageGeneratorModal from '../ImageGen/ImageGeneratorModal'; interface Props { outline: BlogOutlineSection[]; @@ -24,7 +25,10 @@ const EnhancedOutlineEditor: React.FC = ({ }) => { const [editingSection, setEditingSection] = useState(null); const [expandedSections, setExpandedSections] = useState>(new Set()); + const [hoveredSection, setHoveredSection] = useState(null); const [showAddSection, setShowAddSection] = useState(false); + const [imageModalState, setImageModalState] = useState<{ open: boolean; sectionId?: string }>(() => ({ open: false })); + const [sectionImages, setSectionImages] = useState>({}); const [newSectionData, setNewSectionData] = useState({ heading: '', subheadings: '', @@ -94,6 +98,31 @@ const EnhancedOutlineEditor: React.FC = ({ border: '1px solid #e0e0e0', overflow: 'hidden' }}> + {imageModalState.open && ( + setImageModalState({ open: false })} + defaultPrompt={(() => { + const sec = outline.find(s => s.id === imageModalState.sectionId); + return sec?.heading || ''; + })()} + context={(() => { + const sec = outline.find(s => s.id === imageModalState.sectionId); + return { + title: sec?.heading, + section: sec, + outline, + research, + sectionId: imageModalState.sectionId + }; + })()} + onImageGenerated={(imageBase64, sectionId) => { + if (sectionId) { + setSectionImages(prev => ({ ...prev, [sectionId]: imageBase64 })); + } + }} + /> + )} {/* Header */}
= ({ {/* Section Header */}
setHoveredSection(section.id)} + onMouseLeave={() => setHoveredSection(null)} onClick={() => toggleExpanded(section.id)}>
= ({ > ✏️ +
{/* Expanded Section Content */} - {expandedSections.has(section.id) && ( + {(expandedSections.has(section.id) || hoveredSection === section.id) && (
{/* Subheadings */} {section.subheadings && section.subheadings.length > 0 && ( @@ -533,6 +583,53 @@ const EnhancedOutlineEditor: React.FC = ({
)} + + {/* Generated Image Display */} + {sectionImages[section.id] && ( +
+

+ πŸ–ΌοΈ Generated Image +

+
+ {`Generated +
+
+ )} + +
+ +
)}
diff --git a/frontend/src/components/BlogWriter/KeywordInputForm.tsx b/frontend/src/components/BlogWriter/KeywordInputForm.tsx index 5b1a6a88..19083320 100644 --- a/frontend/src/components/BlogWriter/KeywordInputForm.tsx +++ b/frontend/src/components/BlogWriter/KeywordInputForm.tsx @@ -12,269 +12,11 @@ interface KeywordInputFormProps { onTaskStart?: (taskId: string) => void; } -// Separate component to manage form state -const ResearchForm: React.FC<{ - prompt?: string; - onSubmit: (data: { keywords: string; blogLength: string }) => void; - onCancel: () => void; -}> = ({ prompt, onSubmit, onCancel }) => { - const [keywords, setKeywords] = useState(''); - const [blogLength, setBlogLength] = useState('1000'); - const hasValidInput = keywords.trim().length > 0; - - const handleSubmit = (e: React.FormEvent) => { - e.preventDefault(); - if (hasValidInput) { - onSubmit({ keywords: keywords.trim(), blogLength }); - } else { - window.alert('Please enter keywords or a topic to start research.'); - } - }; - - return ( -
-

- πŸ” Let's Research Your Blog Topic -

-

- {prompt || 'Please provide the keywords or topic you want to research for your blog:'} -

- -
-
- - setKeywords(e.target.value)} - onFocus={(e) => e.target.select()} - placeholder="e.g., artificial intelligence, machine learning, AI trends" - style={{ - width: '100%', - padding: '10px 12px', - border: '2px solid #1976d2', - borderRadius: '6px', - fontSize: '14px', - outline: 'none', - backgroundColor: 'white', - boxSizing: 'border-box' - }} - autoFocus - autoComplete="off" - spellCheck="false" - /> -
- -
- - -
-
- -
- - - -
-
- ); -}; - export const KeywordInputForm: React.FC = ({ onKeywordsReceived, onResearchComplete, onTaskStart }) => { const [currentTaskId, setCurrentTaskId] = useState(null); - // Keyword input action with Human-in-the-Loop - useCopilotActionTyped({ - name: 'getResearchKeywords', - description: 'Get keywords from user for blog research', - parameters: [ - { name: 'prompt', type: 'string', description: 'Prompt to show user', required: false } - ], - renderAndWaitForResponse: ({ respond, args, status }: { respond?: (value: string) => void; args: { prompt?: string }; status: string }) => { - if (status === 'complete') { - return ( -
-

- βœ… Research keywords received! Starting research... -

-
- ); - } - - return ( - { - onKeywordsReceived?.(formData); - respond?.(JSON.stringify(formData)); - }} - onCancel={() => respond?.('CANCEL')} - /> - ); - } - }); - - // Research action that actually performs the research - useCopilotActionTyped({ - name: 'performResearch', - description: 'Perform research with collected keywords and blog length', - parameters: [ - { name: 'formData', type: 'string', description: 'JSON string with keywords and blogLength', required: true } - ], - handler: async ({ formData }: { formData: string }) => { - try { - const data = JSON.parse(formData); - const { keywords, blogLength } = data; - - const keywordList = keywords.includes(',') - ? keywords.split(',').map((k: string) => k.trim()) - : [keywords.trim()]; // Preserve single phrases as-is - - // Check frontend cache first - const cachedResult = researchCache.getCachedResult(keywordList, 'General', 'General'); - if (cachedResult) { - console.log('Frontend cache hit - returning cached result instantly'); - onResearchComplete?.(cachedResult); - return { - success: true, - message: `βœ… Found cached research for "${keywords}"! Results loaded instantly.`, - cached: true - }; - } - - const payload: BlogResearchRequest = { - keywords: keywordList, - industry: 'General', - target_audience: 'General', - word_count_target: parseInt(blogLength) - }; - - // Store the blog length in localStorage for later use - localStorage.setItem('blog_length_target', blogLength); - - // Start async research - const { task_id } = await blogWriterApi.startResearch(payload); - setCurrentTaskId(task_id); - onTaskStart?.(task_id); // Notify parent component to start polling - - return { - success: true, - message: `πŸ” Research started for "${keywords}"! Task ID: ${task_id}. Progress will be shown below.`, - task_id: task_id - }; - } catch (error) { - console.error(`Research failed: ${error}`); - return { - success: false, - message: `❌ Research failed: ${error}. Please try again with different keywords.` - }; - } - }, - render: ({ status }: any) => { - if (status === 'inProgress' || status === 'executing') { - return ( -
-
-
-

πŸ” Researching Your Topic

-
-
-

β€’ Connecting to Google Search grounding...

-

β€’ Analyzing keywords and search intent...

-

β€’ Gathering relevant sources and statistics...

-

β€’ Generating content angles and search queries...

-
- -
- ); - } - return null; - } - }); + // This component now only provides polling functionality + // The keyword input form is handled by ResearchAction component return ( <> @@ -294,4 +36,4 @@ export const KeywordInputForm: React.FC = ({ onKeywordsRe ); }; -export default KeywordInputForm; +export default KeywordInputForm; \ No newline at end of file diff --git a/frontend/src/components/BlogWriter/OutlineGenerator.tsx b/frontend/src/components/BlogWriter/OutlineGenerator.tsx index 1a74cf5a..5de43603 100644 --- a/frontend/src/components/BlogWriter/OutlineGenerator.tsx +++ b/frontend/src/components/BlogWriter/OutlineGenerator.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { forwardRef, useImperativeHandle } from 'react'; import { useCopilotAction } from '@copilotkit/react-core'; import { blogWriterApi, BlogResearchResponse } from '../../services/blogWriterApi'; @@ -11,18 +11,38 @@ interface OutlineGeneratorProps { const useCopilotActionTyped = useCopilotAction as any; -export const OutlineGenerator: React.FC = ({ +export const OutlineGenerator = forwardRef(({ research, onTaskStart, onPollingStart, onModalShow -}) => { +}, ref) => { + // Expose an imperative method to trigger outline generation directly (bypass LLM) + useImperativeHandle(ref, () => ({ + generateNow: async () => { + if (!research) { + return { success: false, message: 'No research yet. Please research a topic first.' }; + } + try { + onModalShow?.(); + const { task_id } = await blogWriterApi.startOutlineGeneration({ research }); + onTaskStart(task_id); + onPollingStart(task_id); + return { success: true, task_id }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Unknown error'; + return { success: false, message: errorMessage }; + } + } + })); useCopilotActionTyped({ name: 'generateOutline', description: 'Generate outline from research results using AI analysis', parameters: [], handler: async () => { - if (!research) return { success: false, message: 'No research yet. Please research a topic first.' }; + if (!research) { + return { success: false, message: 'No research yet. Please research a topic first.' }; + } try { // Show progress modal immediately when user clicks "Create outline" @@ -64,7 +84,6 @@ export const OutlineGenerator: React.FC = ({ } }, render: ({ status }: any) => { - console.log('generateOutline render called with status:', status); if (status === 'inProgress' || status === 'executing') { return (
= ({ }); return null; // This component only provides the copilot action -}; +}); export default OutlineGenerator; diff --git a/frontend/src/components/BlogWriter/PhaseNavigation.tsx b/frontend/src/components/BlogWriter/PhaseNavigation.tsx new file mode 100644 index 00000000..0ea7c412 --- /dev/null +++ b/frontend/src/components/BlogWriter/PhaseNavigation.tsx @@ -0,0 +1,89 @@ +import React from 'react'; + +export interface Phase { + id: string; + name: string; + icon: string; + description: string; + completed: boolean; + current: boolean; + disabled: boolean; +} + +interface PhaseNavigationProps { + phases: Phase[]; + onPhaseClick: (phaseId: string) => void; + currentPhase: string; +} + +export const PhaseNavigation: React.FC = ({ + phases, + onPhaseClick, + currentPhase +}) => { + return ( +
+ {phases.map((phase) => { + const isCurrent = phase.current; + const isCompleted = phase.completed; + const isDisabled = phase.disabled; + + return ( + + ); + })} +
+ ); +}; + +export default PhaseNavigation; diff --git a/frontend/src/components/BlogWriter/PhaseNavigationTest.tsx b/frontend/src/components/BlogWriter/PhaseNavigationTest.tsx new file mode 100644 index 00000000..e4c772a4 --- /dev/null +++ b/frontend/src/components/BlogWriter/PhaseNavigationTest.tsx @@ -0,0 +1,89 @@ +import React, { useState } from 'react'; +import PhaseNavigation from './PhaseNavigation'; +import { Phase } from './PhaseNavigation'; + +// Test component to verify phase navigation functionality +export const PhaseNavigationTest: React.FC = () => { + const [currentPhase, setCurrentPhase] = useState('research'); + + const testPhases: Phase[] = [ + { + id: 'research', + name: 'Research', + icon: 'πŸ”', + description: 'Research your topic and gather data', + completed: true, + current: currentPhase === 'research', + disabled: false + }, + { + id: 'outline', + name: 'Outline', + icon: 'πŸ“', + description: 'Create and refine your blog outline', + completed: true, + current: currentPhase === 'outline', + disabled: false + }, + { + id: 'content', + name: 'Content', + icon: '✍️', + description: 'Generate and edit your blog content', + completed: false, + current: currentPhase === 'content', + disabled: false + }, + { + id: 'seo', + name: 'SEO', + icon: 'πŸ“ˆ', + description: 'Optimize for search engines', + completed: false, + current: currentPhase === 'seo', + disabled: true + }, + { + id: 'publish', + name: 'Publish', + icon: 'πŸš€', + description: 'Publish your blog post', + completed: false, + current: currentPhase === 'publish', + disabled: true + } + ]; + + const handlePhaseClick = (phaseId: string) => { + setCurrentPhase(phaseId); + }; + + return ( +
+

Phase Navigation Test

+

Current Phase: {currentPhase}

+ + + +
+

Phase Status:

+
    + {testPhases.map(phase => ( +
  • + {phase.name}: + {phase.completed ? ' βœ… Completed' : ' ⏳ Pending'} | + {phase.current ? ' 🎯 Current' : ''} | + {phase.disabled ? ' 🚫 Disabled' : ' βœ… Enabled'} +
  • + ))} +
+
+
+ ); +}; + +export default PhaseNavigationTest; diff --git a/frontend/src/components/BlogWriter/ResearchAction.tsx b/frontend/src/components/BlogWriter/ResearchAction.tsx index 6d946d65..6add8c5e 100644 --- a/frontend/src/components/BlogWriter/ResearchAction.tsx +++ b/frontend/src/components/BlogWriter/ResearchAction.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useState, useRef } from 'react'; import { useCopilotAction } from '@copilotkit/react-core'; import { blogWriterApi, BlogResearchRequest, BlogResearchResponse } from '../../services/blogWriterApi'; import { useResearchPolling } from '../../hooks/usePolling'; @@ -15,13 +15,18 @@ export const ResearchAction: React.FC = ({ onResearchComple const [currentTaskId, setCurrentTaskId] = useState(null); const [currentMessage, setCurrentMessage] = useState(''); const [showProgressModal, setShowProgressModal] = useState(false); + const [forceUpdate, setForceUpdate] = useState(0); + + // Refs for form inputs (uncontrolled, avoids typing issues inside Copilot render) + const keywordsRef = useRef(null); + const blogLengthRef = useRef(null); const polling = useResearchPolling({ onProgress: (message) => { setCurrentMessage(message); + setForceUpdate(prev => prev + 1); // Force re-render }, onComplete: (result) => { - // Cache the result for future use if (result && result.keywords) { researchCache.cacheResult( result.keywords, @@ -35,84 +40,170 @@ export const ResearchAction: React.FC = ({ onResearchComple setCurrentTaskId(null); setCurrentMessage(''); setShowProgressModal(false); + setForceUpdate(prev => prev + 1); }, onError: (error) => { console.error('Research polling error:', error); setCurrentTaskId(null); setCurrentMessage(''); setShowProgressModal(false); + setForceUpdate(prev => prev + 1); } }); + useCopilotActionTyped({ + name: 'showResearchForm', + description: 'Show keyword input form for blog research', + parameters: [], + handler: async () => ({ + success: true, + message: "πŸ” Let's Research Your Blog Topic\n\nWhat keywords and information would you like to use for your research? Please also specify the desired length of the blog post.\n\nKeywords or Topic *\ne.g., artificial intelligence, machine learning, AI trends\n\nBlog Length (words)\n\n1000 words (Medium blog)\n\nπŸš€ Start Research", + showForm: true + }), + render: ({ status }: any) => { + const _ = forceUpdate; + + if (polling.currentStatus === 'completed' && polling.progressMessages.length > 0) { + const latestMessage = polling.progressMessages[polling.progressMessages.length - 1]; + return ( +
+

βœ… Research completed successfully!

+

{latestMessage?.message || 'Research data is now available for your blog.'}

+
+ ); + } + + if (polling.currentStatus === 'in_progress' || polling.currentStatus === 'running') { + return ( +
+

πŸ”„ Research in progress...

+

{currentMessage || 'Gathering research data...'}

+
+ ); + } + + return ( +
+

πŸ” Let's Research Your Blog Topic

+

+ What keywords and information would you like to use for your research? Please also specify the desired length of the blog post. +

+ +
+ + +
+ +
+ + +
+ +
+ +
+
+ ); + } + }); + + // Additional action to catch the specific suggestion message useCopilotActionTyped({ name: 'researchTopic', description: 'Research topic with keywords and persona context using Google Search grounding', parameters: [ - { name: 'keywords', type: 'string', description: 'Comma-separated keywords or topic description', required: true }, + { name: 'keywords', type: 'string', description: 'Comma-separated keywords or topic description', required: false }, { name: 'industry', type: 'string', description: 'Industry', required: false }, { name: 'target_audience', type: 'string', description: 'Target audience', required: false }, { name: 'blogLength', type: 'string', description: 'Target blog length in words', required: false } ], - handler: async ({ keywords, industry, target_audience, blogLength }: { keywords: string; industry?: string; target_audience?: string; blogLength?: string }) => { + handler: async ({ keywords = '', industry = 'General', target_audience = 'General', blogLength = '1000' }: any) => { try { - // If keywords is a topic description, preserve as single phrase unless comma-separated - const keywordList = keywords.includes(',') - ? keywords.split(',').map(k => k.trim()) - : [keywords.trim()]; // Preserve single phrases as-is - - const industryValue = industry || 'General'; - const audienceValue = target_audience || 'General'; - - // Check frontend cache first - const cachedResult = researchCache.getCachedResult(keywordList, industryValue, audienceValue); - if (cachedResult) { - console.log('Frontend cache hit - returning cached result instantly'); - onResearchComplete?.(cachedResult); - return { - success: true, - message: `βœ… Found cached research for "${keywords}"! Results loaded instantly.`, - cached: true - }; + const trimmed = keywords.trim(); + if (!trimmed) { + return "Please provide keywords or a topic for research."; } - + const keywordList = trimmed.includes(',') + ? trimmed.split(',').map((k: string) => k.trim()).filter(Boolean) + : [trimmed]; const payload: BlogResearchRequest = { keywords: keywordList, - industry: industryValue, - target_audience: audienceValue, - word_count_target: blogLength ? parseInt(blogLength) : 1000 + industry, + target_audience, + word_count_target: parseInt(blogLength) }; - - // Start async research const { task_id } = await blogWriterApi.startResearch(payload); setCurrentTaskId(task_id); setShowProgressModal(true); polling.startPolling(task_id); - - return { - success: true, - message: `πŸ” Research started for "${keywords}"! Task ID: ${task_id}. Progress will be shown below.`, - task_id: task_id - }; + return "Starting research with your provided keywords."; } catch (error) { - console.error(`Research failed: ${error}`); - return { - success: false, - message: `❌ Research failed: ${error}. The AI research system encountered an issue. Please try again with different keywords or contact support if the problem persists.` - }; + console.error('Failed to start research:', error); + return "Failed to start research. Please try again."; } - }, - render: () => null + } }); return ( - setShowProgressModal(false)} - /> + <> + {showProgressModal && ( + setShowProgressModal(false)} + /> + )} + ); }; diff --git a/frontend/src/components/BlogWriter/ResearchPollingHandler.tsx b/frontend/src/components/BlogWriter/ResearchPollingHandler.tsx index b07f7521..69c5843d 100644 --- a/frontend/src/components/BlogWriter/ResearchPollingHandler.tsx +++ b/frontend/src/components/BlogWriter/ResearchPollingHandler.tsx @@ -3,6 +3,7 @@ import { useResearchPolling } from '../../hooks/usePolling'; import ResearchProgressModal from './ResearchProgressModal'; import { BlogResearchResponse } from '../../services/blogWriterApi'; import { researchCache } from '../../services/researchCache'; +import { debug } from '../../utils/debug'; interface ResearchPollingHandlerProps { taskId: string | null; @@ -19,11 +20,11 @@ export const ResearchPollingHandler: React.FC = ({ const polling = useResearchPolling({ onProgress: (message) => { - console.log('ResearchPollingHandler - Progress message received:', message); + debug.log('[ResearchPollingHandler] progress', { message }); setCurrentMessage(message); }, onComplete: (result) => { - console.log('ResearchPollingHandler - Research completed:', result); + debug.log('[ResearchPollingHandler] complete'); // Cache the result for future use if (result && result.keywords) { @@ -39,7 +40,7 @@ export const ResearchPollingHandler: React.FC = ({ setCurrentMessage(''); }, onError: (error) => { - console.error('Research polling error:', error); + debug.error('[ResearchPollingHandler] error', error); onError?.(error); setCurrentMessage(''); } @@ -61,14 +62,14 @@ export const ResearchPollingHandler: React.FC = ({ }; }, [polling]); - console.log('ResearchPollingHandler render:', { - taskId, - isPolling: polling.isPolling, - status: polling.currentStatus, - progressMessages: polling.progressMessages?.length, - currentMessage, - error: polling.error - }); + // Only log on meaningful changes + useEffect(() => { + debug.log('[ResearchPollingHandler] state', { + isPolling: polling.isPolling, + status: polling.currentStatus, + progressCount: polling.progressMessages?.length || 0 + }); + }, [polling.isPolling, polling.currentStatus, polling.progressMessages?.length]); // Render the unified research progress modal when a task is present return ( diff --git a/frontend/src/components/BlogWriter/SEO/KeywordAnalysis.tsx b/frontend/src/components/BlogWriter/SEO/KeywordAnalysis.tsx index dd920241..a7d6b67b 100644 --- a/frontend/src/components/BlogWriter/SEO/KeywordAnalysis.tsx +++ b/frontend/src/components/BlogWriter/SEO/KeywordAnalysis.tsx @@ -1,6 +1,6 @@ /** * Keyword Analysis Component - * + * * Displays comprehensive keyword analysis including keyword types, densities, * missing keywords, over-optimization, and distribution analysis. */ @@ -15,7 +15,7 @@ import { IconButton, Tooltip } from '@mui/material'; -import { +import { GpsFixed, Search, Warning @@ -36,86 +36,140 @@ interface KeywordAnalysisProps { }; } +const baseCardSx = { + p: 3, + backgroundColor: '#ffffff', + border: '1px solid #e2e8f0', + borderRadius: 2, + boxShadow: '0 12px 28px rgba(15,23,42,0.08)', + color: '#0f172a', + minHeight: '100%' +} as const; + +const subCard = (color: string) => ({ + p: 2, + borderRadius: 2, + border: `1px solid ${color}`, + background: `linear-gradient(145deg, ${color}14, ${color}1f)` +}); + export const KeywordAnalysis: React.FC = ({ detailedAnalysis }) => { + const keywordData = detailedAnalysis?.keyword_analysis; + + const renderDensityRow = (keyword: string, density: number) => { + const status = density > 3 ? 'Over-optimized' : density < 1 ? 'Under-optimized' : 'Optimal'; + const chipColor = density > 3 ? 'error' : density < 1 ? 'warning' : 'success'; + + return ( + + + {keyword} + + + + {status} + + + + + ); + }; + return ( - + Keyword Analysis {/* Keyword Types Overview */} - - + + Keyword Types Found - - + + Primary Keywords - - {detailedAnalysis?.keyword_analysis?.primary_keywords?.length || 0} found + + {keywordData?.primary_keywords?.length || 0} found - {detailedAnalysis?.keyword_analysis?.primary_keywords?.slice(0, 3).map((keyword: string) => ( - - ))} + + {keywordData?.primary_keywords?.slice(0, 3).map((keyword) => ( + + ))} + - - + + Long-tail Keywords - - {detailedAnalysis?.keyword_analysis?.long_tail_keywords?.length || 0} found + + {keywordData?.long_tail_keywords?.length || 0} found - {detailedAnalysis?.keyword_analysis?.long_tail_keywords?.slice(0, 2).map((keyword: string) => ( - - ))} + + {keywordData?.long_tail_keywords?.slice(0, 3).map((keyword) => ( + + ))} + - - + + Semantic Keywords - - {detailedAnalysis?.keyword_analysis?.semantic_keywords?.length || 0} found + + {keywordData?.semantic_keywords?.length || 0} found - {detailedAnalysis?.keyword_analysis?.semantic_keywords?.slice(0, 2).map((keyword: string) => ( - - ))} + + {keywordData?.semantic_keywords?.slice(0, 3).map((keyword) => ( + + ))} + {/* Keyword Densities */} - + - + Keyword Densities - + Keyword Density Analysis - + Shows how frequently each keyword appears in your content as a percentage of total words. - + Optimal Range: 1-3% for primary keywords - + Too Low (<1%): Keyword may not be prominent enough - + Too High (>3%): Risk of keyword stuffing @@ -123,108 +177,96 @@ export const KeywordAnalysis: React.FC = ({ detailedAnalys arrow > - + - - {detailedAnalysis?.keyword_analysis?.keyword_density && Object.keys(detailedAnalysis.keyword_analysis.keyword_density).length > 0 ? ( - Object.entries(detailedAnalysis.keyword_analysis.keyword_density).map(([keyword, density]) => ( - - {keyword} - - - {density > 3 ? 'Over-optimized' : density < 1 ? 'Under-optimized' : 'Optimal'} - - 3 ? 'error' : density < 1 ? 'warning' : 'success'} - size="small" - /> - - - )) + + {keywordData?.keyword_density && Object.keys(keywordData.keyword_density).length > 0 ? ( + Object.entries(keywordData.keyword_density).map(([keyword, density]) => renderDensityRow(keyword, density)) ) : ( - + No keyword density data available. Make sure your research data includes target keywords. )} - + {/* Missing Keywords */} - {detailedAnalysis?.keyword_analysis?.missing_keywords && detailedAnalysis.keyword_analysis.missing_keywords.length > 0 && ( - + {keywordData?.missing_keywords && keywordData.missing_keywords.length > 0 && ( + - + Missing Keywords - - - + + + - {detailedAnalysis.keyword_analysis.missing_keywords.map((keyword: string) => ( - + {keywordData.missing_keywords.map((keyword) => ( + ))} )} - + {/* Over-Optimized Keywords */} - {detailedAnalysis?.keyword_analysis?.over_optimization && detailedAnalysis.keyword_analysis.over_optimization.length > 0 && ( - + {keywordData?.over_optimization && keywordData.over_optimization.length > 0 && ( + - + Over-Optimized Keywords - - - + + + - {detailedAnalysis.keyword_analysis.over_optimization.map((keyword: string) => ( - + {keywordData.over_optimization.map((keyword) => ( + ))} )} {/* Keyword Distribution Analysis */} - {detailedAnalysis?.keyword_analysis?.keyword_distribution && Object.keys(detailedAnalysis.keyword_analysis.keyword_distribution).length > 0 && ( - - + {keywordData?.keyword_distribution && Object.keys(keywordData.keyword_distribution).length > 0 && ( + + Keyword Distribution Analysis - {Object.entries(detailedAnalysis.keyword_analysis.keyword_distribution).map(([keyword, data]: [string, any]) => ( - - - "{keyword}" + {Object.entries(keywordData.keyword_distribution).map(([keyword, data]: [string, any]) => ( + + + β€œ{keyword}” - + Density: {data.density?.toFixed(1)}% - + In Headings: {data.in_headings ? 'Yes' : 'No'} - + First Occurrence: Character {data.first_occurrence || 'Not found'} diff --git a/frontend/src/components/BlogWriter/SEO/MetadataDisplay/CoreMetadataTab.tsx b/frontend/src/components/BlogWriter/SEO/MetadataDisplay/CoreMetadataTab.tsx index 92c39bf6..d6ee3e25 100644 --- a/frontend/src/components/BlogWriter/SEO/MetadataDisplay/CoreMetadataTab.tsx +++ b/frontend/src/components/BlogWriter/SEO/MetadataDisplay/CoreMetadataTab.tsx @@ -75,9 +75,22 @@ export const CoreMetadataTab: React.FC = ({ return `${current}/${max}`; }; + // Consistent text input styling for better contrast + const textInputSx = { + '& .MuiInputBase-input': { + color: '#202124' + }, + '& .MuiInputLabel-root': { + color: '#5f6368' + }, + '& .MuiOutlinedInput-notchedOutline': { + borderColor: '#dadce0' + } + } as const; + return ( - + Core SEO Metadata @@ -85,10 +98,10 @@ export const CoreMetadataTab: React.FC = ({ {/* SEO Title */} - + - - + + SEO Title @@ -107,6 +120,7 @@ export const CoreMetadataTab: React.FC = ({ value={metadata.seo_title || ''} onChange={handleTextFieldChange('seo_title')} placeholder="Enter SEO-optimized title (50-60 characters)" + sx={textInputSx} InputProps={{ endAdornment: ( @@ -120,18 +134,18 @@ export const CoreMetadataTab: React.FC = ({ ) }} /> - - Include your primary keyword and make it compelling for clicks - + + Include your primary keyword and keep between 50–60 characters + {/* Meta Description */} - + - - + + Meta Description @@ -150,6 +164,7 @@ export const CoreMetadataTab: React.FC = ({ value={metadata.meta_description || ''} onChange={handleTextFieldChange('meta_description')} placeholder="Enter compelling meta description (150-160 characters)" + sx={textInputSx} InputProps={{ endAdornment: ( @@ -163,18 +178,18 @@ export const CoreMetadataTab: React.FC = ({ ) }} /> - - Include a call-to-action and your primary keyword - + + Aim for 150–160 characters with a clear value proposition + {/* URL Slug */} - + - - + + URL Slug @@ -192,16 +207,18 @@ export const CoreMetadataTab: React.FC = ({ onChange={handleTextFieldChange('url_slug')} placeholder="seo-friendly-url-slug" helperText="Use lowercase letters, numbers, and hyphens only" + sx={textInputSx} + FormHelperTextProps={{ sx: { color: '#5f6368' } }} /> {/* Focus Keyword */} - + - - + + Focus Keyword @@ -219,16 +236,18 @@ export const CoreMetadataTab: React.FC = ({ onChange={handleTextFieldChange('focus_keyword')} placeholder="primary-keyword" helperText="Your main SEO keyword for this post" + sx={textInputSx} + FormHelperTextProps={{ sx: { color: '#5f6368' } }} /> {/* Blog Tags */} - + - - + + Blog Tags @@ -241,12 +260,12 @@ export const CoreMetadataTab: React.FC = ({ - Tags + Tags - - Add relevant tags for better categorization and discoverability - + + Add 3–6 relevant tags for better categorization and discoverability + {/* Blog Categories */} - + - - + + Blog Categories @@ -286,12 +305,12 @@ export const CoreMetadataTab: React.FC = ({ - Categories + Categories - - Select 2-3 primary categories for your content - + + Select 1–3 primary categories for your content + {/* Social Hashtags */} - + - - + + Social Hashtags @@ -331,12 +350,12 @@ export const CoreMetadataTab: React.FC = ({ - Hashtags + Hashtags - - Include # symbol for social media platforms - + + Include # symbol (e.g., #multimodalAI). 3–5 hashtags recommended. + {/* Reading Time */} - + - - + + Reading Time @@ -385,6 +404,8 @@ export const CoreMetadataTab: React.FC = ({ endAdornment: minutes }} helperText="Estimated reading time for your content" + sx={textInputSx} + FormHelperTextProps={{ sx: { color: '#5f6368' } }} /> diff --git a/frontend/src/components/BlogWriter/SEO/MetadataDisplay/PreviewCard.tsx b/frontend/src/components/BlogWriter/SEO/MetadataDisplay/PreviewCard.tsx index 6ac707d1..67df96ff 100644 --- a/frontend/src/components/BlogWriter/SEO/MetadataDisplay/PreviewCard.tsx +++ b/frontend/src/components/BlogWriter/SEO/MetadataDisplay/PreviewCard.tsx @@ -12,28 +12,35 @@ import { Box, Typography, Paper, - Grid, Card, CardContent, Chip, - Alert + Tabs, + Tab, + Tooltip, + IconButton } from '@mui/material'; import { Search as SearchIcon, Code as CodeIcon, Facebook as FacebookIcon, Twitter as TwitterIcon, - Google as GoogleIcon + Google as GoogleIcon, + Info as InfoIcon } from '@mui/icons-material'; interface PreviewCardProps { metadata: any; blogTitle: string; + previewTabValue: string; + onPreviewTabChange: (value: string) => void; } export const PreviewCard: React.FC = ({ metadata, - blogTitle + blogTitle, + previewTabValue, + onPreviewTabChange }) => { const getCurrentDate = () => { return new Date().toLocaleDateString('en-US', { @@ -45,320 +52,491 @@ export const PreviewCard: React.FC = ({ return ( - + {/* Title with Tooltip */} + - Live Preview - + + Live Preview + + + + + + + - - {/* Google Search Results Preview */} - - - - - - Google Search Results + {/* Platform Sub-Tabs */} + + onPreviewTabChange(newValue)} + variant="scrollable" + scrollButtons="auto" + sx={{ + '& .MuiTab-root': { + textTransform: 'none', + fontWeight: 500, + minHeight: 48 + }, + '& .Mui-selected': { + fontWeight: 600 + } + }} + > + } + iconPosition="start" + label="Google Search Results" + value="google" + /> + } + iconPosition="start" + label="Facebook Preview" + value="facebook" + /> + } + iconPosition="start" + label="Twitter Preview" + value="twitter" + /> + } + iconPosition="start" + label="Rich Snippets Preview" + value="richsnippets" + /> + + + + {/* Google Search Results Preview */} + {previewTabValue === 'google' && ( + + + + + Google Search Results + + + + {/* Google SERP Preview - Light Theme (matches actual Google) */} + + + {/* URL - Google Blue */} + + {metadata.canonical_url || 'https://yourwebsite.com/blog-post'} - - + + {/* Title - Google Blue, hover underline */} + + {metadata.seo_title || blogTitle} + + + {/* Description - Google Gray */} + + {metadata.meta_description || 'Your meta description will appear here in Google search results...'} + + + {/* Additional Info */} + + + {getCurrentDate()} + + + β€’ {metadata.reading_time || 5} min read + + {metadata.blog_tags && metadata.blog_tags.length > 0 && ( + <> + + β€’ {metadata.blog_tags.slice(0, 2).join(', ')} + + + )} + + + + + )} - - + {/* Facebook Preview */} + {previewTabValue === 'facebook' && ( + + + + + Facebook Preview + + + + + {/* Facebook Card Preview */} + + + {/* Image placeholder */} + + {metadata.open_graph?.image ? ( + + Image loaded + + ) : ( + + No image set + + )} + + + {/* URL */} - - {metadata.canonical_url || 'https://yourwebsite.com/blog-post'} + + {metadata.canonical_url ? new URL(metadata.canonical_url).hostname : 'yourwebsite.com'} {/* Title */} - {metadata.seo_title || blogTitle} + {metadata.open_graph?.title || metadata.seo_title || blogTitle} {/* Description */} - - {metadata.meta_description || 'Your meta description will appear here in Google search results...'} + + {metadata.open_graph?.description || metadata.meta_description || 'Your description will appear here...'} + + + + + + )} + + {/* Twitter Preview */} + {previewTabValue === 'twitter' && ( + + + + + Twitter Preview + + + + + {/* Twitter Card Preview */} + + + {/* Image placeholder */} + + {metadata.twitter_card?.image ? ( + + Image loaded + + ) : ( + + No image set + + )} + + + + {/* URL */} + + {metadata.canonical_url ? new URL(metadata.canonical_url).hostname : 'yourwebsite.com'} - {/* Additional Info */} - - - {getCurrentDate()} - - - β€’ - - - {metadata.reading_time || 5} min read - - {metadata.blog_tags && metadata.blog_tags.length > 0 && ( - <> - - β€’ - - - {metadata.blog_tags.slice(0, 2).join(', ')} - - - )} - - - - - - This is how your blog post will appear in Google search results - - - - - {/* Social Media Previews */} - - - - - - Facebook Preview - - - - - - - {/* Image placeholder */} - - - {metadata.open_graph?.image ? 'Image loaded' : 'No image set'} - - - - - {/* URL */} - - {metadata.canonical_url || 'yourwebsite.com'} - - - {/* Title */} - - {metadata.open_graph?.title || metadata.seo_title || blogTitle} - - - {/* Description */} - - {metadata.open_graph?.description || metadata.meta_description || 'Your description will appear here...'} - - - - - - - - - - - - - Twitter Preview - - - - - - - {/* Image placeholder */} - - - {metadata.twitter_card?.image ? 'Image loaded' : 'No image set'} - - - - - {/* URL */} - - {metadata.canonical_url || 'yourwebsite.com'} - - - {/* Title */} - - {metadata.twitter_card?.title || metadata.seo_title || blogTitle} - - - {/* Description */} - - {metadata.twitter_card?.description || metadata.meta_description || 'Your description will appear here...'} - - - {/* Twitter handle */} - {metadata.twitter_card?.site && ( - - {metadata.twitter_card.site} - - )} - - - - - - - {/* Rich Snippets Preview */} - - - - - - Rich Snippets Preview - - - - - - - {/* Article Schema Preview */} - - - {metadata.json_ld_schema?.headline || metadata.seo_title || blogTitle} - - - - - - {metadata.json_ld_schema?.description || metadata.meta_description || 'Article description...'} + {/* Title */} + + {metadata.twitter_card?.title || metadata.seo_title || blogTitle} - - {metadata.json_ld_schema?.author?.name && ( - - - By {metadata.json_ld_schema.author.name} - - - )} - - {metadata.json_ld_schema?.datePublished && ( - - - {new Date(metadata.json_ld_schema.datePublished).toLocaleDateString()} - - - )} - - {metadata.reading_time && ( - - - {metadata.reading_time} min read - - - )} - - {metadata.json_ld_schema?.wordCount && ( - - - {metadata.json_ld_schema.wordCount} words - - - )} - + {/* Description */} + + {metadata.twitter_card?.description || metadata.meta_description || 'Your description will appear here...'} + - {metadata.json_ld_schema?.keywords && metadata.json_ld_schema.keywords.length > 0 && ( - - - Keywords: + {/* Twitter handle */} + {metadata.twitter_card?.site && ( + + {metadata.twitter_card.site} + + )} + + + + + )} + + {/* Rich Snippets Preview */} + {previewTabValue === 'richsnippets' && ( + + + + + Rich Snippets Preview + + + + + {/* Rich Snippets Card */} + + + {/* Article Schema Preview */} + + + {metadata.json_ld_schema?.headline || metadata.seo_title || blogTitle} + + + + + + {metadata.json_ld_schema?.description || metadata.meta_description || 'Article description...'} + + + + {metadata.json_ld_schema?.author?.name && ( + + + By {metadata.json_ld_schema.author.name} - - {metadata.json_ld_schema.keywords.slice(0, 5).map((keyword: string, index: number) => ( - - ))} - )} - - - - - Rich snippets help search engines understand your content and may display additional information in search results - - - - - {/* Metadata Summary */} - - - - - Metadata Summary - - - - - - - {metadata.optimization_score || 0}% - - - Optimization Score + + {metadata.json_ld_schema?.datePublished && ( + + + {new Date(metadata.json_ld_schema.datePublished).toLocaleDateString()} + + + )} + + {metadata.reading_time && ( + + + {metadata.reading_time} min read + + + )} + + {metadata.json_ld_schema?.wordCount && ( + + + {metadata.json_ld_schema.wordCount} words + + + )} + + + {metadata.json_ld_schema?.keywords && metadata.json_ld_schema.keywords.length > 0 && ( + + + Keywords: + + {metadata.json_ld_schema.keywords.slice(0, 5).map((keyword: string, index: number) => ( + + ))} + - - - - - - {metadata.reading_time || 0} - - - Reading Time (min) - - - - - - - - {metadata.blog_tags?.length || 0} - - - Tags - - - - - - - - {metadata.blog_categories?.length || 0} - - - Categories - - - - - - - + )} + + + + )} ); }; diff --git a/frontend/src/components/BlogWriter/SEO/MetadataDisplay/SocialMediaTab.tsx b/frontend/src/components/BlogWriter/SEO/MetadataDisplay/SocialMediaTab.tsx index 5225a9de..ec65a93d 100644 --- a/frontend/src/components/BlogWriter/SEO/MetadataDisplay/SocialMediaTab.tsx +++ b/frontend/src/components/BlogWriter/SEO/MetadataDisplay/SocialMediaTab.tsx @@ -71,12 +71,25 @@ export const SocialMediaTab: React.FC = ({ return `${current}/${max}`; }; + // Consistent text input styling for better contrast + const textInputSx = { + '& .MuiInputBase-input': { + color: '#202124' + }, + '& .MuiInputLabel-root': { + color: '#5f6368' + }, + '& .MuiOutlinedInput-notchedOutline': { + borderColor: '#dadce0' + } + } as const; + const openGraph = metadata.open_graph || {}; const twitterCard = metadata.twitter_card || {}; return ( - + Social Media Metadata @@ -84,11 +97,11 @@ export const SocialMediaTab: React.FC = ({ {/* Open Graph Section */} - + - + Open Graph Tags @@ -97,7 +110,7 @@ export const SocialMediaTab: React.FC = ({ - + OG Title @@ -114,6 +127,7 @@ export const SocialMediaTab: React.FC = ({ value={openGraph.title || ''} onChange={handleNestedFieldChange('open_graph', 'title')} placeholder="Open Graph title (60 characters max)" + sx={textInputSx} InputProps={{ endAdornment: ( @@ -131,7 +145,7 @@ export const SocialMediaTab: React.FC = ({ - + OG Description @@ -150,6 +164,7 @@ export const SocialMediaTab: React.FC = ({ value={openGraph.description || ''} onChange={handleNestedFieldChange('open_graph', 'description')} placeholder="Open Graph description (160 characters max)" + sx={textInputSx} InputProps={{ endAdornment: ( @@ -167,7 +182,7 @@ export const SocialMediaTab: React.FC = ({ - + OG Image URL @@ -184,6 +199,7 @@ export const SocialMediaTab: React.FC = ({ value={openGraph.image || ''} onChange={handleNestedFieldChange('open_graph', 'image')} placeholder="https://example.com/image.jpg" + sx={textInputSx} InputProps={{ startAdornment: ( @@ -196,7 +212,7 @@ export const SocialMediaTab: React.FC = ({ - + OG URL @@ -213,6 +229,7 @@ export const SocialMediaTab: React.FC = ({ value={openGraph.url || ''} onChange={handleNestedFieldChange('open_graph', 'url')} placeholder="https://example.com/blog-post" + sx={textInputSx} InputProps={{ startAdornment: ( @@ -224,18 +241,18 @@ export const SocialMediaTab: React.FC = ({ - - Open Graph tags are used by Facebook, LinkedIn, and other social platforms to display rich previews - + + Open Graph tags are used by Facebook, LinkedIn, and others to display rich previews. + {/* Twitter Card Section */} - + - + Twitter Card Tags @@ -244,7 +261,7 @@ export const SocialMediaTab: React.FC = ({ - + Twitter Title @@ -261,6 +278,7 @@ export const SocialMediaTab: React.FC = ({ value={twitterCard.title || ''} onChange={handleNestedFieldChange('twitter_card', 'title')} placeholder="Twitter card title (70 characters max)" + sx={textInputSx} InputProps={{ endAdornment: ( @@ -278,7 +296,7 @@ export const SocialMediaTab: React.FC = ({ - + Twitter Description @@ -297,6 +315,7 @@ export const SocialMediaTab: React.FC = ({ value={twitterCard.description || ''} onChange={handleNestedFieldChange('twitter_card', 'description')} placeholder="Twitter card description (200 characters max)" + sx={textInputSx} InputProps={{ endAdornment: ( @@ -314,7 +333,7 @@ export const SocialMediaTab: React.FC = ({ - + Twitter Image URL @@ -331,6 +350,7 @@ export const SocialMediaTab: React.FC = ({ value={twitterCard.image || ''} onChange={handleNestedFieldChange('twitter_card', 'image')} placeholder="https://example.com/twitter-image.jpg" + sx={textInputSx} InputProps={{ startAdornment: ( @@ -343,7 +363,7 @@ export const SocialMediaTab: React.FC = ({ - + Twitter Site Handle @@ -360,6 +380,7 @@ export const SocialMediaTab: React.FC = ({ value={twitterCard.site || ''} onChange={handleNestedFieldChange('twitter_card', 'site')} placeholder="@yourwebsite" + sx={textInputSx} InputProps={{ startAdornment: ( @@ -371,16 +392,16 @@ export const SocialMediaTab: React.FC = ({ - - Twitter cards provide rich previews when your content is shared on Twitter/X - + + Twitter cards provide rich previews when your content is shared on Twitter/X. + {/* Social Media Preview */} - - + + Social Media Preview @@ -388,22 +409,22 @@ export const SocialMediaTab: React.FC = ({ {/* Facebook Preview */} - + - + Facebook Preview - - + + {openGraph.title || 'Your Blog Title'} - + {openGraph.url || 'yourwebsite.com'} - + {openGraph.description || 'Your meta description will appear here...'} @@ -413,22 +434,22 @@ export const SocialMediaTab: React.FC = ({ {/* Twitter Preview */} - + - + Twitter Preview - - + + {twitterCard.title || 'Your Blog Title'} - + {twitterCard.site || '@yourwebsite'} - + {twitterCard.description || 'Your Twitter description will appear here...'} diff --git a/frontend/src/components/BlogWriter/SEO/MetadataDisplay/StructuredDataTab.tsx b/frontend/src/components/BlogWriter/SEO/MetadataDisplay/StructuredDataTab.tsx index 770c51c0..387ec410 100644 --- a/frontend/src/components/BlogWriter/SEO/MetadataDisplay/StructuredDataTab.tsx +++ b/frontend/src/components/BlogWriter/SEO/MetadataDisplay/StructuredDataTab.tsx @@ -56,6 +56,28 @@ export const StructuredDataTab: React.FC = ({ }) => { const [showRawJson, setShowRawJson] = useState(false); + // Helpers for counters and consistent input styling + const getCharacterCountColor = (current: number, max: number) => { + if (current > max) return 'error'; + if (current > max * 0.9) return 'warning'; + return 'success'; + }; + + const getCharacterCountText = (current: number, max: number) => { + if (current > max) return `${current}/${max} (Too long)`; + if (current > max * 0.9) return `${current}/${max} (Near limit)`; + return `${current}/${max}`; + }; + + const textInputSx = { + '& .MuiInputBase-input': { + color: '#202124' + }, + '& .MuiInputLabel-root': { + color: '#5f6368' + } + } as const; + const handleTextFieldChange = (field: string) => (event: React.ChangeEvent) => { onMetadataEdit(field, event.target.value); }; @@ -123,7 +145,7 @@ export const StructuredDataTab: React.FC = ({ {/* Article Information */} - + Article Schema @@ -149,6 +171,19 @@ export const StructuredDataTab: React.FC = ({ value={jsonLdSchema.headline || ''} onChange={handleSchemaFieldChange('headline')} placeholder="Article headline" + sx={textInputSx} + InputProps={{ + endAdornment: ( + + + {getCharacterCountText((jsonLdSchema.headline || '').length, 110)} + + + ) + }} /> @@ -173,6 +208,19 @@ export const StructuredDataTab: React.FC = ({ value={jsonLdSchema.description || ''} onChange={handleSchemaFieldChange('description')} placeholder="Article description" + sx={textInputSx} + InputProps={{ + endAdornment: ( + + + {getCharacterCountText((jsonLdSchema.description || '').length, 200)} + + + ) + }} /> @@ -202,6 +250,7 @@ export const StructuredDataTab: React.FC = ({ ) }} + sx={textInputSx} /> @@ -228,6 +277,7 @@ export const StructuredDataTab: React.FC = ({ InputProps={{ endAdornment: words }} + sx={textInputSx} /> @@ -236,7 +286,7 @@ export const StructuredDataTab: React.FC = ({ {/* Author Information */} - + Author Information @@ -262,6 +312,7 @@ export const StructuredDataTab: React.FC = ({ value={author.name || ''} onChange={handleAuthorFieldChange('name')} placeholder="Author Name" + sx={textInputSx} /> @@ -284,6 +335,7 @@ export const StructuredDataTab: React.FC = ({ value={author['@type'] || ''} onChange={handleAuthorFieldChange('@type')} placeholder="Person" + sx={textInputSx} /> @@ -292,7 +344,7 @@ export const StructuredDataTab: React.FC = ({ {/* Publisher Information */} - + Publisher Information @@ -318,6 +370,7 @@ export const StructuredDataTab: React.FC = ({ value={publisher.name || ''} onChange={handlePublisherFieldChange('name')} placeholder="Publisher Name" + sx={textInputSx} /> @@ -340,6 +393,7 @@ export const StructuredDataTab: React.FC = ({ value={publisher.logo || ''} onChange={handlePublisherFieldChange('logo')} placeholder="https://example.com/logo.png" + sx={textInputSx} /> @@ -348,7 +402,7 @@ export const StructuredDataTab: React.FC = ({ {/* Publication Dates */} - + Publication Dates @@ -375,6 +429,7 @@ export const StructuredDataTab: React.FC = ({ value={jsonLdSchema.datePublished || ''} onChange={handleSchemaFieldChange('datePublished')} InputLabelProps={{ shrink: true }} + sx={textInputSx} /> @@ -398,6 +453,7 @@ export const StructuredDataTab: React.FC = ({ value={jsonLdSchema.dateModified || ''} onChange={handleSchemaFieldChange('dateModified')} InputLabelProps={{ shrink: true }} + sx={textInputSx} /> @@ -406,7 +462,7 @@ export const StructuredDataTab: React.FC = ({ {/* Keywords */} - + Keywords & Categories @@ -438,6 +494,7 @@ export const StructuredDataTab: React.FC = ({ }} placeholder="keyword1, keyword2, keyword3" helperText="Separate keywords with commas" + sx={textInputSx} /> @@ -479,7 +536,9 @@ export const StructuredDataTab: React.FC = ({ readOnly: true, sx: { fontFamily: 'monospace', - fontSize: '0.875rem' + fontSize: '0.875rem', + background: '#0f172a', + color: '#e2e8f0' } }} sx={{ diff --git a/frontend/src/components/BlogWriter/SEO/OverallScoreCard.tsx b/frontend/src/components/BlogWriter/SEO/OverallScoreCard.tsx new file mode 100644 index 00000000..21b392e7 --- /dev/null +++ b/frontend/src/components/BlogWriter/SEO/OverallScoreCard.tsx @@ -0,0 +1,264 @@ +/** + * OverallScoreCard Component + * + * Renders the compact overall SEO score summary with grade chip and + * category score tiles. + */ + +import React from 'react'; +import { + Card, + CardHeader, + CardContent, + Typography, + Box, + Tooltip, + Paper, + Chip, + Avatar +} from '@mui/material'; + +interface MetricTooltip { + title: string; + description: string; + methodology: string; + score_meaning: string; + examples: string; +} + +interface OverallScoreCardProps { + overallScore: number; + overallGrade: string; + statusLabel: string; + categoryScores: Record; + getMetricTooltip: (category: string) => MetricTooltip; + getScoreColor: (score: number) => string; +} + +const getGradeMeta = (grade: string) => { + switch (grade) { + case 'A': + return { + color: '#16a34a', + background: 'linear-gradient(135deg, rgba(34,197,94,0.12), rgba(22,163,74,0.18))', + tooltip: 'Grade A: Outstanding SEO health with only minor optimizations needed.' + }; + case 'B': + return { + color: '#0ea5e9', + background: 'linear-gradient(135deg, rgba(14,165,233,0.12), rgba(2,132,199,0.18))', + tooltip: 'Grade B: Strong SEO foundation with several opportunities to optimize further.' + }; + case 'C': + return { + color: '#d97706', + background: 'linear-gradient(135deg, rgba(251,191,36,0.14), rgba(217,119,6,0.2))', + tooltip: 'Grade C: Moderate SEO performance. Prioritize improvements in weaker categories.' + }; + case 'D': + return { + color: '#ea580c', + background: 'linear-gradient(135deg, rgba(251,113,133,0.14), rgba(249,115,22,0.2))', + tooltip: 'Grade D: Significant SEO gaps detected. Address critical issues promptly.' + }; + default: + return { + color: '#475569', + background: 'linear-gradient(135deg, rgba(148,163,184,0.14), rgba(100,116,139,0.2))', + tooltip: 'SEO grade unavailable. Review analysis details for more information.' + }; + } +}; + +export const OverallScoreCard: React.FC = ({ + overallScore, + overallGrade, + statusLabel, + categoryScores, + getMetricTooltip, + getScoreColor +}) => { + const gradeMeta = getGradeMeta(overallGrade); + + return ( + + + + + Overall SEO Performance Snapshot + + + + + + + + + + {overallScore} + + /100 + + + + Overall Score + + + + + {overallGrade} + + } + sx={{ + fontWeight: 600, + px: 2.2, + py: 0.5, + letterSpacing: 0.3, + color: gradeMeta.color, + background: gradeMeta.background + }} + /> + + + + + + {Object.entries(categoryScores).map(([category, score]) => { + const tooltip = getMetricTooltip(category); + return ( + + + {tooltip.title} + + + {tooltip.description} + + + Methodology: {tooltip.methodology} + + + Score Meaning: {tooltip.score_meaning} + + + Examples: {tooltip.examples} + + + } + arrow + placement="top" + > + + + {score} + + + {category.replace('_', ' ')} + + + + ); + })} + + + + + + ); +}; + +export default OverallScoreCard; diff --git a/frontend/src/components/BlogWriter/SEO/ReadabilityAnalysis.tsx b/frontend/src/components/BlogWriter/SEO/ReadabilityAnalysis.tsx index d6b08b90..e6d0db7e 100644 --- a/frontend/src/components/BlogWriter/SEO/ReadabilityAnalysis.tsx +++ b/frontend/src/components/BlogWriter/SEO/ReadabilityAnalysis.tsx @@ -1,6 +1,6 @@ /** * Readability Analysis Component - * + * * Displays comprehensive readability analysis including readability metrics, * content statistics, sentence/paragraph analysis, and target audience information. */ @@ -14,7 +14,7 @@ import { IconButton, Tooltip } from '@mui/material'; -import { +import { MenuBook } from '@mui/icons-material'; @@ -57,109 +57,186 @@ interface ReadabilityAnalysisProps { }; } -export const ReadabilityAnalysis: React.FC = ({ - detailedAnalysis, - visualizationData +const cardStyles = { + p: 3, + backgroundColor: '#ffffff', + border: '1px solid #e2e8f0', + borderRadius: 2, + boxShadow: '0 12px 30px rgba(15,23,42,0.08)', + color: '#0f172a', + minHeight: '100%' +} as const; + +const sectionTitleSx = { + fontWeight: 700, + letterSpacing: 0.2, + color: '#0f172a', + mb: 2 +} as const; + +const statRowSx = { + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + py: 0.5 +} as const; + +const statLabelSx = { + color: '#475569', + fontWeight: 500 +} as const; + +const statValueSx = { + color: '#0f172a', + fontWeight: 700 +} as const; + +const metricRowSx = { + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + padding: '0.65rem 0.85rem', + borderRadius: 12, + backgroundColor: '#f1f5f9', + cursor: 'help', + transition: 'transform 0.2s ease, box-shadow 0.2s ease', + '&:hover': { + transform: 'translateY(-2px)', + boxShadow: '0 10px 20px rgba(15,23,42,0.08)' + } +} as const; + +export const ReadabilityAnalysis: React.FC = ({ + detailedAnalysis, + visualizationData }) => { + const readabilityMetrics = detailedAnalysis?.readability_analysis?.metrics ?? {}; + + const getMetricDetails = (metric: string, value: number) => { + const tooltips: Record = { + flesch_reading_ease: { + description: 'Measures how easy text is to read (0-100 scale).', + interpretation: value >= 80 ? 'Very Easy' : value >= 60 ? 'Standard' : 'Challenging' + }, + flesch_kincaid_grade: { + description: 'U.S. grade level required to understand the text.', + interpretation: value <= 8 ? 'Easy' : value <= 12 ? 'Moderate' : 'Advanced' + }, + gunning_fog: { + description: 'Years of formal education needed for comprehension.', + interpretation: value <= 12 ? 'Easy' : value <= 16 ? 'Moderate' : 'Advanced' + }, + smog_index: { + description: 'Estimates the years of education needed to understand the text.', + interpretation: value <= 8 ? 'Easy' : value <= 12 ? 'Moderate' : 'Advanced' + }, + automated_readability: { + description: 'Automated readability score based on characters per word.', + interpretation: value <= 8 ? 'Easy' : value <= 12 ? 'Moderate' : 'Advanced' + }, + coleman_liau: { + description: 'Readability based on characters per word and sentence length.', + interpretation: value <= 8 ? 'Easy' : value <= 12 ? 'Moderate' : 'Advanced' + } + }; + + return ( + tooltips[metric] || { + description: 'Readability metric', + interpretation: 'No interpretation available' + } + ); + }; + + const renderStatRow = (label: React.ReactNode, value: React.ReactNode) => ( + + + {label} + + + {value} + + + ); + return ( - + Readability Analysis + - + - + Readability Metrics - + Readability Analysis - + Measures how easy your content is to read and understand. - + Flesch Reading Ease: 90-100 (Very Easy), 80-89 (Easy), 70-79 (Fairly Easy), 60-69 (Standard) - - Average Sentence Length: 15-20 words is optimal + + Sentence Length: 15-20 words is optimal - - Average Syllables per Word: 1.5-1.7 is ideal + + Syllables per Word: 1.5-1.7 keeps content approachable } arrow > - + - - {detailedAnalysis?.readability_analysis?.metrics && Object.keys(detailedAnalysis.readability_analysis.metrics).length > 0 ? ( - Object.entries(detailedAnalysis.readability_analysis.metrics).map(([metric, value]) => { - const getReadabilityTooltip = (metric: string, value: number) => { - const tooltips = { - flesch_reading_ease: { - description: "Measures how easy text is to read (0-100 scale)", - interpretation: value >= 80 ? "Very Easy" : value >= 60 ? "Standard" : "Difficult" - }, - flesch_kincaid_grade: { - description: "U.S. grade level needed to understand the text", - interpretation: value <= 8 ? "Easy" : value <= 12 ? "Moderate" : "Difficult" - }, - gunning_fog: { - description: "Years of formal education needed to understand the text", - interpretation: value <= 12 ? "Easy" : value <= 16 ? "Moderate" : "Difficult" - }, - smog_index: { - description: "Simple Measure of Gobbledygook - readability formula", - interpretation: value <= 8 ? "Easy" : value <= 12 ? "Moderate" : "Difficult" - }, - automated_readability: { - description: "Automated Readability Index based on character count", - interpretation: value <= 8 ? "Easy" : value <= 12 ? "Moderate" : "Difficult" - }, - coleman_liau: { - description: "Readability test based on average sentence length and characters per word", - interpretation: value <= 8 ? "Easy" : value <= 12 ? "Moderate" : "Difficult" - } - }; - return tooltips[metric as keyof typeof tooltips] || { description: "Readability metric", interpretation: "N/A" }; - }; - - const tooltip = getReadabilityTooltip(metric, value); + + + {Object.keys(readabilityMetrics).length > 0 ? ( + Object.entries(readabilityMetrics).map(([metric, value]) => { + const { description, interpretation } = getMetricDetails(metric, value); + const label = metric.replace('_', ' ').replace(/\b\w/g, (l) => l.toUpperCase()); + return ( - - {metric.replace('_', ' ').replace(/\b\w/g, l => l.toUpperCase())} + + {label} - - {tooltip.description} + + {description} - - Interpretation: {tooltip.interpretation} + + Interpretation: {interpretation} } arrow + placement="top" > - - + + {metric.replace('_', ' ')} - + {value.toFixed(1)} @@ -167,116 +244,72 @@ export const ReadabilityAnalysis: React.FC = ({ ); }) ) : ( - + No readability metrics available. This may indicate an issue with the content analysis. )} - + - - + + Content Statistics - - - Word Count - - {detailedAnalysis?.content_quality?.word_count || visualizationData?.content_stats.word_count} - - - - Sections - - {detailedAnalysis?.content_structure?.total_sections || visualizationData?.content_stats.sections} - - - - Paragraphs - - {detailedAnalysis?.content_structure?.total_paragraphs || visualizationData?.content_stats.paragraphs} - - - - Sentences - - {detailedAnalysis?.content_structure?.total_sentences || 'N/A'} - - - - Unique Words - - {detailedAnalysis?.content_quality?.unique_words || 'N/A'} - - - - Vocabulary Diversity - - {detailedAnalysis?.content_quality?.vocabulary_diversity ? - (detailedAnalysis.content_quality.vocabulary_diversity * 100).toFixed(1) + '%' : 'N/A'} - - + + {renderStatRow('Word Count', detailedAnalysis?.content_quality?.word_count || visualizationData?.content_stats.word_count || 'N/A')} + {renderStatRow('Sections', detailedAnalysis?.content_structure?.total_sections || visualizationData?.content_stats.sections || 'N/A')} + {renderStatRow('Paragraphs', detailedAnalysis?.content_structure?.total_paragraphs || visualizationData?.content_stats.paragraphs || 'N/A')} + {renderStatRow('Sentences', detailedAnalysis?.content_structure?.total_sentences || 'N/A')} + {renderStatRow('Unique Words', detailedAnalysis?.content_quality?.unique_words || 'N/A')} + {renderStatRow( + 'Vocabulary Diversity', + detailedAnalysis?.content_quality?.vocabulary_diversity !== undefined + ? `${(detailedAnalysis.content_quality.vocabulary_diversity * 100).toFixed(1)}%` + : 'N/A' + )} - {/* Additional Readability Metrics */} - - + + Sentence & Paragraph Analysis - - - Avg Sentence Length - - {detailedAnalysis?.readability_analysis?.avg_sentence_length?.toFixed(1) || 'N/A'} words - - - - Avg Paragraph Length - - {detailedAnalysis?.readability_analysis?.avg_paragraph_length?.toFixed(1) || 'N/A'} words - - - - Transition Words - - {detailedAnalysis?.content_quality?.transition_words_used || 'N/A'} - - + + {renderStatRow( + 'Average Sentence Length', + detailedAnalysis?.readability_analysis?.avg_sentence_length !== undefined + ? `${detailedAnalysis.readability_analysis.avg_sentence_length.toFixed(1)} words` + : 'N/A' + )} + {renderStatRow( + 'Average Paragraph Length', + detailedAnalysis?.readability_analysis?.avg_paragraph_length !== undefined + ? `${detailedAnalysis.readability_analysis.avg_paragraph_length.toFixed(1)} words` + : 'N/A' + )} + {renderStatRow( + 'Transition Words Used', + detailedAnalysis?.content_quality?.transition_words_used || 'N/A' + )} - + - - + + Target Audience - - - Reading Level - - {detailedAnalysis?.readability_analysis?.target_audience || 'N/A'} - - - - Content Depth Score - - {detailedAnalysis?.content_quality?.content_depth_score || 'N/A'} - - - - Flow Score - - {detailedAnalysis?.content_quality?.flow_score || 'N/A'} - - + + {renderStatRow('Reading Level', detailedAnalysis?.readability_analysis?.target_audience || 'N/A')} + {renderStatRow('Content Depth Score', detailedAnalysis?.content_quality?.content_depth_score || 'N/A')} + {renderStatRow('Flow Score', detailedAnalysis?.content_quality?.flow_score || 'N/A')} diff --git a/frontend/src/components/BlogWriter/SEO/Recommendations.tsx b/frontend/src/components/BlogWriter/SEO/Recommendations.tsx index 7e096c20..a4d1e592 100644 --- a/frontend/src/components/BlogWriter/SEO/Recommendations.tsx +++ b/frontend/src/components/BlogWriter/SEO/Recommendations.tsx @@ -1,6 +1,6 @@ /** * Recommendations Component - * + * * Displays actionable SEO recommendations with priority indicators, * category tags, and impact descriptions. */ @@ -12,7 +12,7 @@ import { Paper, Chip } from '@mui/material'; -import { +import { Lightbulb, CheckCircle, Cancel, @@ -30,78 +30,107 @@ interface RecommendationsProps { recommendations: Recommendation[]; } +const priorityStyles: Record = { + High: { color: '#dc2626', gradient: 'linear-gradient(135deg, rgba(248,113,113,0.12), rgba(239,68,68,0.18))' }, + Medium: { color: '#d97706', gradient: 'linear-gradient(135deg, rgba(251,191,36,0.12), rgba(217,119,6,0.16))' }, + Low: { color: '#16a34a', gradient: 'linear-gradient(135deg, rgba(74,222,128,0.12), rgba(22,163,74,0.16))' }, + default: { color: '#475569', gradient: 'linear-gradient(135deg, rgba(148,163,184,0.1), rgba(100,116,139,0.14))' } +}; + export const Recommendations: React.FC = ({ recommendations }) => { - const getPriorityColor = (priority: string) => { - switch (priority) { - case 'High': return 'error.main'; - case 'Medium': return 'warning.main'; - case 'Low': return 'success.main'; - default: return 'text.secondary'; - } - }; + const getPriorityColor = (priority: string) => priorityStyles[priority]?.color || priorityStyles.default.color; const getPriorityIcon = (priority: string) => { switch (priority) { - case 'High': return ; - case 'Medium': return ; - case 'Low': return ; - default: return ; + case 'High': + return ; + case 'Medium': + return ; + case 'Low': + return ; + default: + return ; } }; - const getScoreBadgeVariant = (score: number) => { - if (score >= 80) return 'success'; - if (score >= 60) return 'warning'; - return 'error'; + const getChipColor = (priority: string) => { + switch (priority) { + case 'High': + return 'error'; + case 'Medium': + return 'warning'; + case 'Low': + return 'success'; + default: + return 'default'; + } }; return ( - + Actionable Recommendations - - {recommendations.map((rec, index) => ( - - - - {getPriorityIcon(rec.priority)} - - - - - + + {recommendations.map((rec, index) => { + const styles = priorityStyles[rec.priority] || priorityStyles.default; + return ( + + + + {getPriorityIcon(rec.priority)} + + + + + + + + {rec.recommendation} + + + {rec.impact} + - - {rec.recommendation} - - - {rec.impact} - - - - ))} + + ); + })} ); diff --git a/frontend/src/components/BlogWriter/SEO/StructureAnalysis.tsx b/frontend/src/components/BlogWriter/SEO/StructureAnalysis.tsx index 82356711..bdc33771 100644 --- a/frontend/src/components/BlogWriter/SEO/StructureAnalysis.tsx +++ b/frontend/src/components/BlogWriter/SEO/StructureAnalysis.tsx @@ -1,6 +1,6 @@ /** * Structure Analysis Component - * + * * Displays comprehensive content structure analysis including structure overview, * content elements detection, and heading structure analysis. */ @@ -14,7 +14,7 @@ import { Chip, Tooltip } from '@mui/material'; -import { +import { BarChart } from '@mui/icons-material'; @@ -52,127 +52,157 @@ interface StructureAnalysisProps { }; } +const baseCard = { + p: 3, + backgroundColor: '#ffffff', + border: '1px solid #e2e8f0', + borderRadius: 2, + boxShadow: '0 12px 28px rgba(15,23,42,0.08)', + color: '#0f172a', + minHeight: '100%' +} as const; + +const infoRow = { + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + padding: '0.75rem 0', + cursor: 'help' +} as const; + +const statLabel = { + color: '#475569', + fontWeight: 500 +} as const; + +const statValue = { + color: '#0f172a', + fontWeight: 700 +} as const; + +const highlightCard = (borderColor: string) => ({ + p: 2, + borderRadius: 2, + border: `1px solid ${borderColor}`, + background: `linear-gradient(140deg, ${borderColor}15, ${borderColor}22)` +}); + export const StructureAnalysis: React.FC = ({ detailedAnalysis }) => { + const structure = detailedAnalysis?.content_structure; + const quality = detailedAnalysis?.content_quality; + const headings = detailedAnalysis?.heading_structure; + return ( - + Content Structure Analysis {/* Content Structure Overview */} - - + + Structure Overview - + - + Total Sections - + Number of main content sections (H2 headings) in your blog post. - + Optimal Range: 3-8 sections for most blog posts - - Why it matters: Good sectioning improves readability and helps search engines understand your content structure. + + Why it matters: Good sectioning improves readability and structure. } arrow > - - Total Sections - - {detailedAnalysis?.content_structure?.total_sections || 'N/A'} + + Total Sections + + {structure?.total_sections || 'N/A'} - + - + Total Paragraphs - + Number of paragraphs in your content (excluding headings). - + Optimal Range: 8-20 paragraphs for most blog posts - - Why it matters: Appropriate paragraph count indicates good content depth and organization. - } arrow > - - Total Paragraphs - - {detailedAnalysis?.content_structure?.total_paragraphs || 'N/A'} + + Total Paragraphs + + {structure?.total_paragraphs || 'N/A'} - + - + Total Sentences - + Total number of sentences in your content. - + Optimal Range: 40-100 sentences for most blog posts - - Why it matters: Sentence count affects readability and content comprehensiveness. - } arrow > - - Total Sentences - - {detailedAnalysis?.content_structure?.total_sentences || 'N/A'} + + Total Sentences + + {structure?.total_sentences || 'N/A'} - + - + Structure Score - + Overall score (0-100) for your content's structural organization. - - Scoring Factors: Section count, paragraph count, introduction/conclusion presence - - - Why it matters: Well-structured content ranks better and provides better user experience. + + Scoring Factors: Section count, paragraph count, intro/conclusion presence. } arrow > - - Structure Score - - {detailedAnalysis?.content_structure?.structure_score || 'N/A'} + + Structure Score + + {structure?.structure_score || 'N/A'} @@ -182,94 +212,52 @@ export const StructureAnalysis: React.FC = ({ detailedAn {/* Content Elements */} - - + + Content Elements - + - - Introduction Section - - - Whether your content has a clear introduction that sets context and expectations. - - - Why it matters: Introductions help readers understand what they'll learn and improve engagement. - - - SEO Impact: Clear introductions help search engines understand your content's purpose. - - - } + title="Whether your content has a clear introduction that sets context and expectations." arrow > - - Has Introduction - + Has Introduction + - + - - Conclusion Section - - - Whether your content has a clear conclusion that summarizes key points. - - - Why it matters: Conclusions help readers retain information and provide closure. - - - SEO Impact: Good conclusions can improve time on page and reduce bounce rate. - - - } + title="Whether your content ends with a clear conclusion summarizing key points." arrow > - - Has Conclusion - + Has Conclusion + - + - - Call to Action - - - Whether your content includes a clear call to action for readers. - - - Why it matters: CTAs guide readers to take desired actions and improve conversion rates. - - - SEO Impact: Strong CTAs can improve user engagement metrics. - - - } + title="Whether your content includes a clear call to action for readers." arrow > - - Has Call to Action - + Has Call to Action + @@ -281,193 +269,104 @@ export const StructureAnalysis: React.FC = ({ detailedAn {/* Content Quality Metrics */} - - + + Content Quality Metrics - - Word Count - - - Total number of words in your content. - - - Optimal Range: 800-2000 words for most blog posts - - - Why it matters: Longer content typically ranks better and provides more value to readers. - - - } + title="Total number of words in your content. Longer content typically ranks better." arrow > - - + + Word Count - - {detailedAnalysis?.content_quality?.word_count || 'N/A'} + + {quality?.word_count || 'N/A'} - + - - Vocabulary Diversity - - - Ratio of unique words to total words, indicating content variety. - - - Optimal Range: 0.4-0.7 (40-70% unique words) - - - Why it matters: Higher diversity indicates richer, more engaging content. - - - } + title="Ratio of unique words to total words, indicating content variety and richness." arrow > - - + + Vocabulary Diversity - - {detailedAnalysis?.content_quality?.vocabulary_diversity ? - (detailedAnalysis.content_quality.vocabulary_diversity * 100).toFixed(1) + '%' : 'N/A'} + + {quality?.vocabulary_diversity !== undefined + ? `${(quality.vocabulary_diversity * 100).toFixed(1)}%` + : 'N/A'} - + - - Content Depth Score - - - Score (0-100) indicating how comprehensive and detailed your content is. - - - Scoring Factors: Word count, section depth, information density - - - Why it matters: Deeper content provides more value and ranks better in search results. - - - } + title="Score (0-100) indicating how comprehensive and detailed your content is." arrow > - - + + Content Depth Score - - {detailedAnalysis?.content_quality?.content_depth_score || 'N/A'} + + {quality?.content_depth_score || 'N/A'} - + - - Flow Score - - - Score (0-100) indicating how well your content flows from one idea to the next. - - - Scoring Factors: Transition words, sentence variety, logical progression - - - Why it matters: Good flow improves readability and keeps readers engaged. - - - } + title="Score (0-100) indicating how well your content flows from one idea to the next." arrow > - - + + Flow Score - - {detailedAnalysis?.content_quality?.flow_score || 'N/A'} + + {quality?.flow_score || 'N/A'} - + - - Transition Words - - - Number of transition words used to connect ideas and improve flow. - - - Optimal Range: 5-15 transition words for most blog posts - - - Why it matters: Transition words improve readability and help readers follow your logic. - - - } + title="Number of transition words used – higher values suggest smoother narrative flow." arrow > - - - Transition Words + + + Transition Words Used - - {detailedAnalysis?.content_quality?.transition_words_used || 'N/A'} + + {quality?.transition_words_used || 'N/A'} - + - - Unique Words - - - Number of unique words used in your content. - - - Why it matters: More unique words indicate richer vocabulary and better content variety. - - - SEO Impact: Diverse vocabulary can help with semantic SEO and topic coverage. - - - } + title="Average unique words used throughout the article. Indicates lexical richness." arrow > - - + + Unique Words - - {detailedAnalysis?.content_quality?.unique_words || 'N/A'} + + {quality?.unique_words || 'N/A'} @@ -478,136 +377,58 @@ export const StructureAnalysis: React.FC = ({ detailedAn {/* Heading Structure */} - - - - - Heading Structure Analysis - - - - - - H1 Headings ({detailedAnalysis?.heading_structure?.h1_count || 0}) - - {detailedAnalysis?.heading_structure?.h1_headings?.map((heading: string, index: number) => ( - - β€’ {heading} + {headings && ( + + + + + Heading Structure + + + + + + H1 Headings - ))} - - - - - - H2 Headings ({detailedAnalysis?.heading_structure?.h2_count || 0}) - - {detailedAnalysis?.heading_structure?.h2_headings?.slice(0, 3).map((heading: string, index: number) => ( - - β€’ {heading} + + {headings.h1_count} - ))} - {detailedAnalysis?.heading_structure?.h2_headings && detailedAnalysis.heading_structure.h2_headings.length > 3 && ( - - ... and {detailedAnalysis.heading_structure.h2_headings.length - 3} more - - )} - - - - - - H3 Headings ({detailedAnalysis?.heading_structure?.h3_count || 0}) - - {detailedAnalysis?.heading_structure?.h3_headings?.slice(0, 3).map((heading: string, index: number) => ( - - β€’ {heading} - - ))} - {detailedAnalysis?.heading_structure?.h3_headings && detailedAnalysis.heading_structure.h3_headings.length > 3 && ( - - ... and {detailedAnalysis.heading_structure.h3_headings.length - 3} more - - )} - - - - - - - Heading Hierarchy Score - - - Score (0-100) indicating how well your heading structure follows SEO best practices. - - - Scoring Factors: H1 presence, logical hierarchy, keyword usage in headings - - - Why it matters: Good heading structure helps search engines understand your content and improves readability. + + {headings.h1_headings?.[0] ? `Primary: ${headings.h1_headings[0]}` : 'Primary heading analysis'} - } - arrow - > - - Heading Hierarchy Score: {detailedAnalysis?.heading_structure?.heading_hierarchy_score || 'N/A'} - - - - - {/* Structure Recommendations */} - {detailedAnalysis?.content_structure?.recommendations && detailedAnalysis.content_structure.recommendations.length > 0 && ( - - - Structure Recommendations - - - {detailedAnalysis.content_structure.recommendations.map((recommendation: string, index: number) => ( - - β€’ {recommendation} + + + + + H2 Headings - ))} - - - )} - - {/* Heading Recommendations */} - {detailedAnalysis?.heading_structure?.recommendations && detailedAnalysis.heading_structure.recommendations.length > 0 && ( - - - Heading Recommendations - - - {detailedAnalysis.heading_structure.recommendations.map((recommendation: string, index: number) => ( - - β€’ {recommendation} + + {headings.h2_count} - ))} - - - )} - - {/* Content Quality Recommendations */} - {detailedAnalysis?.content_quality?.recommendations && detailedAnalysis.content_quality.recommendations.length > 0 && ( - - - Content Quality Recommendations - - - {detailedAnalysis.content_quality.recommendations.map((recommendation: string, index: number) => ( - - β€’ {recommendation} + + {headings.h2_headings?.slice(0, 2).join(', ') || 'Summary of subtopics'} - ))} - - - )} - + + + + + + H3 Headings + + + {headings.h3_count} + + + {headings.h3_headings?.slice(0, 2).join(', ') || 'Supportive outline points'} + + + + + + - + )} ); }; diff --git a/frontend/src/components/BlogWriter/SEOAnalysisModal.tsx b/frontend/src/components/BlogWriter/SEOAnalysisModal.tsx index 3534febe..f6cf64c4 100644 --- a/frontend/src/components/BlogWriter/SEOAnalysisModal.tsx +++ b/frontend/src/components/BlogWriter/SEOAnalysisModal.tsx @@ -23,7 +23,9 @@ import { Grid, Paper, IconButton, - Tooltip + Tooltip, + Avatar, + CircularProgress } from '@mui/material'; import { apiClient } from '../../api/client'; import { @@ -32,11 +34,11 @@ import { Warning, TrendingUp, Search, - BarChart, Refresh, Close } from '@mui/icons-material'; import { KeywordAnalysis, ReadabilityAnalysis, StructureAnalysis, Recommendations } from './SEO'; +import OverallScoreCard from './SEO/OverallScoreCard'; interface SEOAnalysisResult { overall_score: number; @@ -139,7 +141,27 @@ interface SEOAnalysisModalProps { blogContent: string; blogTitle?: string; researchData: any; - onApplyRecommendations?: (recommendations: any[]) => void; + onApplyRecommendations?: (recommendations: SEOAnalysisResult['actionable_recommendations']) => Promise; + onAnalysisComplete?: (analysis: SEOAnalysisResult) => void; +} + +// Simple content hashing helper (SHA-256) +async function hashContent(text: string): Promise { + try { + const enc = new TextEncoder().encode(text); + const digest = await crypto.subtle.digest('SHA-256', enc); + const bytes = Array.from(new Uint8Array(digest)); + return bytes.map(b => b.toString(16).padStart(2, '0')).join(''); + } catch { + // Fallback hash + let h = 0; + for (let i = 0; i < text.length; i++) h = (h * 31 + text.charCodeAt(i)) | 0; + return String(h); + } +} + +function getSeoCacheKey(contentHash: string, title?: string) { + return `seo_cache:${contentHash}:${title || ''}`; } export const SEOAnalysisModal: React.FC = ({ @@ -148,7 +170,8 @@ export const SEOAnalysisModal: React.FC = ({ blogContent, blogTitle, researchData, - onApplyRecommendations + onApplyRecommendations, + onAnalysisComplete }) => { const [isAnalyzing, setIsAnalyzing] = useState(false); const [analysisResult, setAnalysisResult] = useState(null); @@ -156,18 +179,37 @@ export const SEOAnalysisModal: React.FC = ({ const [progressMessage, setProgressMessage] = useState(''); const [error, setError] = useState(null); const [tabValue, setTabValue] = useState('recommendations'); + const [contentHash, setContentHash] = useState(''); + const [isApplying, setIsApplying] = useState(false); + const [applyError, setApplyError] = useState(null); - // Debug logging console.log('SEOAnalysisModal render:', { isOpen, blogContent: blogContent?.length, researchData: !!researchData }); - const runSEOAnalysis = useCallback(async () => { + const runSEOAnalysis = useCallback(async (forceRefresh = false) => { try { setIsAnalyzing(true); setError(null); setProgress(0); setProgressMessage('Starting SEO analysis...'); - // Simulate progress updates (in real implementation, this would be SSE) + // Cache check + const hash = contentHash || (await hashContent(`${blogTitle || ''}\n${blogContent}`)); + const cacheKey = getSeoCacheKey(hash, blogTitle); + if (!forceRefresh) { + const cached = typeof window !== 'undefined' ? window.localStorage.getItem(cacheKey) : null; + if (cached) { + const parsed = JSON.parse(cached); + setAnalysisResult(parsed as SEOAnalysisResult); + setIsAnalyzing(false); + // Notify parent that analysis is complete (from cache) + if (onAnalysisComplete) { + onAnalysisComplete(parsed as SEOAnalysisResult); + } + return; + } + } + + // Simulated progress const progressStages = [ { progress: 20, message: 'Extracting keywords from research data...' }, { progress: 40, message: 'Analyzing content structure and readability...' }, @@ -182,7 +224,7 @@ export const SEOAnalysisModal: React.FC = ({ await new Promise(resolve => setTimeout(resolve, 1000)); } - // Make API call to analyze blog content + // Backend call const response = await apiClient.post('/api/blog-writer/seo/analyze', { blog_content: blogContent, blog_title: blogTitle, @@ -191,15 +233,8 @@ export const SEOAnalysisModal: React.FC = ({ const result = response.data; console.log('πŸ” Backend SEO Analysis Response:', result); - - // Convert API response to frontend format - fail fast if data is missing - if (!result.success) { - throw new Error(result.recommendations?.[0] || 'SEO analysis failed'); - } - - if (!result.overall_score && result.overall_score !== 0) { - throw new Error('Invalid SEO score received from API'); - } + if (!result.success) throw new Error(result.recommendations?.[0] || 'SEO analysis failed'); + if (!result.overall_score && result.overall_score !== 0) throw new Error('Invalid SEO score received from API'); const convertedResult: SEOAnalysisResult = { overall_score: result.overall_score, @@ -256,13 +291,44 @@ export const SEOAnalysisModal: React.FC = ({ }; setAnalysisResult(convertedResult); + + // Save to cache + try { + const h = hash || (await hashContent(`${blogTitle || ''}\n${blogContent}`)); + const key = getSeoCacheKey(h, blogTitle); + if (typeof window !== 'undefined') { + window.localStorage.setItem(key, JSON.stringify(convertedResult)); + } + } catch {} + setIsAnalyzing(false); + // Notify parent that analysis is complete (fresh analysis) + if (onAnalysisComplete) { + onAnalysisComplete(convertedResult); + } + } catch (err) { setError(err instanceof Error ? err.message : 'Analysis failed'); setIsAnalyzing(false); } - }, [blogContent, blogTitle, researchData]); + }, [blogContent, blogTitle, researchData, contentHash, onAnalysisComplete]); + + // Precompute hash when modal opens + useEffect(() => { + if (isOpen) { + (async () => { + const h = await hashContent(`${blogTitle || ''}\n${blogContent}`); + setContentHash(h); + })(); + } + }, [isOpen, blogContent, blogTitle]); + + useEffect(() => { + if (isOpen && !analysisResult) { + runSEOAnalysis(); + } + }, [isOpen, analysisResult, runSEOAnalysis]); const getScoreColor = (score: number) => { if (score >= 80) return 'success.main'; @@ -270,13 +336,6 @@ export const SEOAnalysisModal: React.FC = ({ return 'error.main'; }; - const getScoreBadgeVariant = (score: number) => { - if (score >= 80) return 'success'; - if (score >= 60) return 'warning'; - return 'error'; - }; - - // Tooltip content for each metric const getMetricTooltip = (category: string) => { const tooltips = { @@ -326,12 +385,6 @@ export const SEOAnalysisModal: React.FC = ({ return tooltips[category as keyof typeof tooltips] || tooltips.structure; }; - useEffect(() => { - if (isOpen && !analysisResult) { - runSEOAnalysis(); - } - }, [isOpen, analysisResult, runSEOAnalysis]); - return ( = ({ sx: { maxHeight: '90vh', borderRadius: 3, - background: 'rgba(255, 255, 255, 0.98)', + backgroundColor: '#f8fafc', backdropFilter: 'blur(20px)', - border: '1px solid rgba(0,0,0,0.1)', - color: 'text.primary' + border: '1px solid rgba(148,163,184,0.25)', + color: '#0f172a' } }} > - + @@ -358,9 +411,22 @@ export const SEOAnalysisModal: React.FC = ({ SEO Analysis Results - - - + + + + + + Comprehensive analysis of your blog content's SEO optimization @@ -410,138 +476,14 @@ export const SEOAnalysisModal: React.FC = ({ {analysisResult && ( {/* Overall Score Section */} - - - - - - Overall SEO Score - - - - - - - - - {analysisResult.overall_score} - - - Overall Score - - - - - - - {analysisResult.analysis_summary.overall_grade} - - - Grade - - - - - - - - - - - - - {/* Category Scores */} - - - - Category Breakdown - - - - - {Object.entries(analysisResult.category_scores).map(([category, score]) => { - const tooltip = getMetricTooltip(category); - return ( - - - - {tooltip.title} - - - {tooltip.description} - - - Methodology: {tooltip.methodology} - - - Score Meaning: {tooltip.score_meaning} - - - Examples: {tooltip.examples} - - - } - arrow - placement="top" - > - - - {score} - - - {category.replace('_', ' ')} - - - - - ); - })} - - - + {/* Detailed Analysis Tabs */} @@ -603,43 +545,41 @@ export const SEOAnalysisModal: React.FC = ({ - + AI-Powered Insights - - + + Content Summary - + {analysisResult.analysis_summary.ai_summary} - - - + + Key Strengths {analysisResult.analysis_summary.key_strengths.map((strength, index) => ( - {strength} + {strength} ))} - - - + + Areas for Improvement {analysisResult.analysis_summary.key_weaknesses.map((weakness, index) => ( - {weakness} + {weakness} ))} @@ -652,19 +592,35 @@ export const SEOAnalysisModal: React.FC = ({ {/* Action Buttons */} + {applyError && ( + + + {applyError} + + )} - diff --git a/frontend/src/components/BlogWriter/SEOMetadataModal.tsx b/frontend/src/components/BlogWriter/SEOMetadataModal.tsx index 2d8a5c4d..028d89ad 100644 --- a/frontend/src/components/BlogWriter/SEOMetadataModal.tsx +++ b/frontend/src/components/BlogWriter/SEOMetadataModal.tsx @@ -9,7 +9,7 @@ * - Integration with backend metadata generation */ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useCallback } from 'react'; import { Dialog, DialogTitle, @@ -23,7 +23,8 @@ import { CircularProgress, Alert, IconButton, - Chip + Chip, + Tooltip } from '@mui/material'; import { Close as CloseIcon, @@ -42,6 +43,7 @@ import { CoreMetadataTab } from './SEO/MetadataDisplay/CoreMetadataTab'; import { SocialMediaTab } from './SEO/MetadataDisplay/SocialMediaTab'; import { StructuredDataTab } from './SEO/MetadataDisplay/StructuredDataTab'; import { PreviewCard } from './SEO/MetadataDisplay/PreviewCard'; +import { subscribeImage } from '../../utils/imageBus'; interface SEOMetadataModalProps { isOpen: boolean; @@ -49,6 +51,8 @@ interface SEOMetadataModalProps { blogContent: string; blogTitle: string; researchData: any; + outline?: any[]; // Add outline structure + seoAnalysis?: any; // Add SEO analysis results onMetadataGenerated: (metadata: any) => void; } @@ -71,20 +75,55 @@ interface SEOMetadataResult { error?: string; } +// Cache helper functions (similar to SEOAnalysisModal) +async function hashContent(text: string): Promise { + try { + const enc = new TextEncoder().encode(text); + const digest = await crypto.subtle.digest('SHA-256', enc); + const bytes = Array.from(new Uint8Array(digest)); + return bytes.map(b => b.toString(16).padStart(2, '0')).join(''); + } catch { + // Fallback hash + let h = 0; + for (let i = 0; i < text.length; i++) h = (h * 31 + text.charCodeAt(i)) | 0; + return String(h); + } +} + +function getMetadataCacheKey(contentHash: string, title?: string): string { + return `seo_metadata_cache:${contentHash}:${title || ''}`; +} + export const SEOMetadataModal: React.FC = ({ isOpen, onClose, blogContent, blogTitle, researchData, + outline, + seoAnalysis, onMetadataGenerated }) => { const [isGenerating, setIsGenerating] = useState(false); const [metadataResult, setMetadataResult] = useState(null); const [error, setError] = useState(null); - const [tabValue, setTabValue] = useState('core'); + const [tabValue, setTabValue] = useState('preview'); // Start with preview tab first + const [previewTabValue, setPreviewTabValue] = useState('google'); // Sub-tab for preview platforms const [copiedItems, setCopiedItems] = useState>(new Set()); const [editableMetadata, setEditableMetadata] = useState(null); + const [contentHash, setContentHash] = useState(''); + // Subscribe to image generation bus to auto-fill OG/Twitter image fields + useEffect(() => { + const unsub = subscribeImage(({ base64 }: { base64: string }) => { + setEditableMetadata(prev => { + const next = { ...(prev || metadataResult || {}) } as any; + next.open_graph = { ...(next.open_graph || {}), image: `data:image/png;base64,${base64}` }; + next.twitter_card = { ...(next.twitter_card || {}), image: `data:image/png;base64,${base64}` }; + return next; + }); + }); + return unsub; + }, [metadataResult]); // Debug logging useEffect(() => { @@ -96,19 +135,67 @@ export const SEOMetadataModal: React.FC = ({ }); }, [isOpen, blogContent, blogTitle, researchData]); - const generateMetadata = async () => { + // Reset state when modal closes + useEffect(() => { + if (!isOpen) { + // Reset state when modal closes (but keep result for next time) + setError(null); + setIsGenerating(false); + } + }, [isOpen]); + + // Auto-generate metadata when modal opens (only once) + const hasAutoGeneratedRef = React.useRef(false); + useEffect(() => { + if (isOpen && blogContent && !hasAutoGeneratedRef.current) { + hasAutoGeneratedRef.current = true; + generateMetadata(false); // Auto-generate from cache or API + } + if (!isOpen) { + hasAutoGeneratedRef.current = false; // Reset when modal closes + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isOpen]); // Only trigger when modal opens + + const generateMetadata = useCallback(async (forceRefresh = false) => { try { setIsGenerating(true); setError(null); - setMetadataResult(null); + if (forceRefresh) { + setMetadataResult(null); + } - console.log('πŸš€ Starting SEO metadata generation...'); + console.log('πŸš€ Starting SEO metadata generation...', { forceRefresh }); + + // Calculate content hash for caching + const hash = await hashContent(`${blogTitle || ''}\n${blogContent}`); + setContentHash(hash); + const cacheKey = getMetadataCacheKey(hash, blogTitle); + + // Check cache first (unless force refresh) + if (!forceRefresh && typeof window !== 'undefined') { + const cached = window.localStorage.getItem(cacheKey); + if (cached) { + try { + const parsed = JSON.parse(cached) as SEOMetadataResult; + console.log('βœ… Using cached SEO metadata'); + setMetadataResult(parsed); + setEditableMetadata(parsed); + setIsGenerating(false); + return; + } catch (e) { + console.warn('Failed to parse cached metadata:', e); + } + } + } // Make API call to generate metadata const response = await apiClient.post('/api/blog/seo/metadata', { content: blogContent, title: blogTitle, - research_data: researchData + research_data: researchData, + outline: outline || null, + seo_analysis: seoAnalysis || null }); const result = response.data; @@ -118,6 +205,16 @@ export const SEOMetadataModal: React.FC = ({ throw new Error(result.error || 'Metadata generation failed'); } + // Cache the result + if (typeof window !== 'undefined') { + try { + window.localStorage.setItem(cacheKey, JSON.stringify(result)); + console.log('πŸ’Ύ SEO metadata cached'); + } catch (e) { + console.warn('Failed to cache metadata:', e); + } + } + setMetadataResult(result); setEditableMetadata(result); console.log('πŸ“Š Metadata result set:', result); @@ -128,7 +225,7 @@ export const SEOMetadataModal: React.FC = ({ } finally { setIsGenerating(false); } - }; + }, [blogContent, blogTitle, researchData, outline, seoAnalysis]); const handleTabChange = (event: React.SyntheticEvent, newValue: string) => { setTabValue(newValue); @@ -159,6 +256,23 @@ export const SEOMetadataModal: React.FC = ({ } }; + /** + * Handle Apply Metadata button click + * + * This saves the generated/edited metadata to the parent component's state. + * The metadata is then used when publishing to platforms: + * - WordPress: Requires SEO metadata for proper post creation with SEO fields + * - Wix: Currently doesn't require metadata, but could be added in future + * + * The metadata includes: + * - SEO title, meta description, URL slug + * - Blog tags, categories, focus keyword + * - Open Graph tags (Facebook/LinkedIn) + * - Twitter Card tags + * - JSON-LD structured data (Schema.org Article) + * + * All of these will be passed to the platform's API when publishing. + */ const handleApplyMetadata = () => { if (editableMetadata) { onMetadataGenerated(editableMetadata); @@ -222,32 +336,26 @@ export const SEOMetadataModal: React.FC = ({ /> )} - - - + + {metadataResult && ( + + generateMetadata(true)} + size="small" + disabled={isGenerating} + color="primary" + > + + + + )} + + + + - {!metadataResult && !isGenerating && ( - - - Generate Comprehensive SEO Metadata - - - Create optimized titles, descriptions, Open Graph tags, Twitter cards, and structured data for your blog post. - - - - )} - {isGenerating && ( @@ -267,7 +375,7 @@ export const SEOMetadataModal: React.FC = ({
)} + {/* Continue Writing Prompt */} + {showContinueWritingPrompt && !isGeneratingSuggestion && !smartSuggestion && ( +
+
+ ✨ AI Writing Assistant +
+
+ ALwrity can contextually continue writing your blog. Click below to get AI-powered suggestions. +
+
+ + +
+
+ )} + {/* CSS for spinner animation */}