diff --git a/backend/BILLING_SYSTEM_INTEGRATION.md b/backend/BILLING_SYSTEM_INTEGRATION.md
new file mode 100644
index 00000000..ff9a9a08
--- /dev/null
+++ b/backend/BILLING_SYSTEM_INTEGRATION.md
@@ -0,0 +1,256 @@
+# ALwrity Billing & Subscription System Integration
+
+## Overview
+
+The ALwrity backend now includes a comprehensive billing and subscription system that automatically tracks API usage, calculates costs, and manages subscription limits. This system is fully integrated into the startup process and provides real-time monitoring capabilities.
+
+## ๐ Quick Start
+
+### 1. Start the Backend with Billing System
+
+```bash
+# From the backend directory
+python start_alwrity_backend.py
+```
+
+The startup script will automatically:
+- โ
Create billing and subscription database tables
+- โ
Initialize default pricing and subscription plans
+- โ
Set up usage tracking middleware
+- โ
Verify all billing components are working
+- โ
Start the server with billing endpoints enabled
+
+### 2. Verify Installation
+
+```bash
+# Run the comprehensive verification script
+python verify_billing_setup.py
+```
+
+### 3. Test API Endpoints
+
+```bash
+# Get subscription plans
+curl http://localhost:8000/api/subscription/plans
+
+# Get user usage (replace 'demo' with actual user ID)
+curl http://localhost:8000/api/subscription/usage/demo
+
+# Get billing dashboard data
+curl http://localhost:8000/api/subscription/dashboard/demo
+
+# Get API pricing information
+curl http://localhost:8000/api/subscription/pricing
+```
+
+## ๐ Database Tables
+
+The billing system creates the following tables:
+
+| Table Name | Purpose |
+|------------|---------|
+| `subscription_plans` | Available subscription tiers and pricing |
+| `user_subscriptions` | User subscription assignments |
+| `api_usage_logs` | Detailed API usage tracking |
+| `usage_summaries` | Aggregated usage statistics |
+| `api_provider_pricing` | Cost per token for each AI provider |
+| `usage_alerts` | Usage limit warnings and notifications |
+| `billing_history` | Historical billing records |
+
+## ๐ง System Components
+
+### 1. Database Models (`models/subscription_models.py`)
+- **SubscriptionPlan**: Subscription tiers and pricing
+- **UserSubscription**: User subscription assignments
+- **APIUsageLog**: Detailed usage tracking
+- **UsageSummary**: Aggregated statistics
+- **APIProviderPricing**: Cost calculations
+- **UsageAlert**: Limit notifications
+
+### 2. Services
+- **PricingService** (`services/pricing_service.py`): Cost calculations and plan management
+- **UsageTrackingService** (`services/usage_tracking_service.py`): Usage monitoring and limits
+- **SubscriptionExceptionHandler** (`services/subscription_exception_handler.py`): Error handling
+
+### 3. API Endpoints (`api/subscription_api.py`)
+- `GET /api/subscription/plans` - Available subscription plans
+- `GET /api/subscription/usage/{user_id}` - User usage statistics
+- `GET /api/subscription/dashboard/{user_id}` - Dashboard data
+- `GET /api/subscription/pricing` - API pricing information
+- `GET /api/subscription/trends/{user_id}` - Usage trends
+
+### 4. Middleware Integration
+- **Monitoring Middleware** (`middleware/monitoring_middleware.py`): Automatic usage tracking
+- **Exception Handling**: Graceful error handling for billing issues
+
+## ๐ฏ Frontend Integration
+
+The billing system is fully integrated with the frontend dashboard:
+
+### CompactBillingDashboard
+- Real-time usage metrics
+- Cost tracking
+- System health monitoring
+- Interactive tooltips and help text
+
+### EnhancedBillingDashboard
+- Detailed usage breakdowns
+- Provider-specific costs
+- Usage trends and analytics
+- Alert management
+
+## ๐ Usage Tracking
+
+The system automatically tracks:
+
+- **API Calls**: Number of requests to each provider
+- **Token Usage**: Input and output tokens for each request
+- **Costs**: Real-time cost calculations
+- **Response Times**: Performance monitoring
+- **Error Rates**: Failed request tracking
+- **User Activity**: Per-user usage patterns
+
+## ๐ฐ Pricing Configuration
+
+### Default AI Provider Pricing (per token)
+
+| Provider | Model | Input Cost | Output Cost |
+|----------|-------|------------|-------------|
+| OpenAI | GPT-4 | $0.00003 | $0.00006 |
+| OpenAI | GPT-3.5-turbo | $0.0000015 | $0.000002 |
+| Gemini | Gemini Pro | $0.0000005 | $0.0000015 |
+| Anthropic | Claude-3 | $0.000008 | $0.000024 |
+| Mistral | Mistral-7B | $0.0000002 | $0.0000006 |
+
+### Subscription Plans
+
+| Plan | Monthly Price | Yearly Price | API Limits |
+|------|---------------|--------------|------------|
+| Free | $0 | $0 | 1,000 calls/month |
+| Starter | $29 | $290 | 10,000 calls/month |
+| Professional | $99 | $990 | 100,000 calls/month |
+| Enterprise | $299 | $2,990 | Unlimited |
+
+## ๐ Monitoring & Alerts
+
+### Real-time Monitoring
+- Usage tracking for all API calls
+- Cost calculations in real-time
+- Performance metrics
+- Error rate monitoring
+
+### Alert System
+- Usage approaching limits (80% threshold)
+- Cost overruns
+- System health issues
+- Provider-specific problems
+
+## ๐ ๏ธ Development Mode
+
+For development with auto-reload:
+
+```bash
+# Development mode with auto-reload
+python start_alwrity_backend.py --dev
+
+# Or with explicit reload flag
+python start_alwrity_backend.py --reload
+```
+
+## ๐ Configuration
+
+### Environment Variables
+
+The system uses the following environment variables:
+
+```bash
+# Database
+DATABASE_URL=sqlite:///./alwrity.db
+
+# API Keys (configured through onboarding)
+OPENAI_API_KEY=your_key_here
+GEMINI_API_KEY=your_key_here
+ANTHROPIC_API_KEY=your_key_here
+MISTRAL_API_KEY=your_key_here
+
+# Server Configuration
+HOST=0.0.0.0
+PORT=8000
+DEBUG=true
+```
+
+### Custom Pricing
+
+To modify pricing, update the `PricingService.initialize_default_pricing()` method in `services/pricing_service.py`.
+
+## ๐งช Testing
+
+### Run Verification Script
+```bash
+python verify_billing_setup.py
+```
+
+### Test Individual Components
+```bash
+# Test subscription system
+python test_subscription_system.py
+
+# Test billing tables creation
+python scripts/create_billing_tables.py
+```
+
+## ๐จ Troubleshooting
+
+### Common Issues
+
+1. **Tables not created**: Run `python scripts/create_billing_tables.py`
+2. **Missing dependencies**: Run `pip install -r requirements.txt`
+3. **Database errors**: Check `DATABASE_URL` in environment
+4. **API key issues**: Verify API keys are configured
+
+### Debug Mode
+
+Enable debug logging by setting `DEBUG=true` in your environment.
+
+## ๐ API Documentation
+
+Once the server is running, access the interactive API documentation:
+
+- **Swagger UI**: http://localhost:8000/api/docs
+- **ReDoc**: http://localhost:8000/api/redoc
+
+## ๐ Updates and Maintenance
+
+### Adding New Providers
+
+1. Add provider to `APIProvider` enum in `models/subscription_models.py`
+2. Update pricing in `PricingService.initialize_default_pricing()`
+3. Add provider detection in middleware
+4. Update frontend provider chips
+
+### Modifying Plans
+
+1. Update `PricingService.initialize_default_plans()`
+2. Modify plan limits and pricing
+3. Test with verification script
+
+## ๐ Support
+
+For issues or questions:
+
+1. Check the verification script output
+2. Review the startup logs
+3. Test individual components
+4. Check database table creation
+
+## ๐ Success Indicators
+
+You'll know the billing system is working when:
+
+- โ
Startup script shows "Billing and subscription tables created successfully"
+- โ
Verification script passes all checks
+- โ
API endpoints return data
+- โ
Frontend dashboard shows usage metrics
+- โ
Usage tracking middleware is active
+
+The billing system is now fully integrated and ready for production use!
diff --git a/backend/api/blog_writer/__init__.py b/backend/api/blog_writer/__init__.py
new file mode 100644
index 00000000..6b4c2118
--- /dev/null
+++ b/backend/api/blog_writer/__init__.py
@@ -0,0 +1,2 @@
+# Package init for AI Blog Writer API
+
diff --git a/backend/api/blog_writer/router.py b/backend/api/blog_writer/router.py
new file mode 100644
index 00000000..4a5da1c2
--- /dev/null
+++ b/backend/api/blog_writer/router.py
@@ -0,0 +1,107 @@
+from fastapi import APIRouter, HTTPException
+from typing import Any, Dict
+
+from models.blog_models import (
+ BlogResearchRequest,
+ BlogResearchResponse,
+ BlogOutlineRequest,
+ BlogOutlineResponse,
+ BlogOutlineRefineRequest,
+ BlogSectionRequest,
+ BlogSectionResponse,
+ BlogOptimizeRequest,
+ BlogOptimizeResponse,
+ BlogSEOAnalyzeRequest,
+ BlogSEOAnalyzeResponse,
+ BlogSEOMetadataRequest,
+ BlogSEOMetadataResponse,
+ BlogPublishRequest,
+ BlogPublishResponse,
+ HallucinationCheckRequest,
+ HallucinationCheckResponse,
+)
+from services.blog_writer.blog_service import BlogWriterService
+
+
+router = APIRouter(prefix="/api/blog", tags=["AI Blog Writer"])
+
+service = BlogWriterService()
+
+
+@router.get("/health")
+async def health() -> Dict[str, Any]:
+ return {"status": "ok", "service": "ai_blog_writer"}
+
+
+@router.post("/research", response_model=BlogResearchResponse)
+async def research(request: BlogResearchRequest) -> BlogResearchResponse:
+ try:
+ return await service.research(request)
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=str(e))
+
+
+@router.post("/outline/generate", response_model=BlogOutlineResponse)
+async def generate_outline(request: BlogOutlineRequest) -> BlogOutlineResponse:
+ try:
+ return await service.generate_outline(request)
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=str(e))
+
+
+@router.post("/outline/refine", response_model=BlogOutlineResponse)
+async def refine_outline(request: BlogOutlineRefineRequest) -> BlogOutlineResponse:
+ try:
+ return await service.refine_outline(request)
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=str(e))
+
+
+@router.post("/section/generate", response_model=BlogSectionResponse)
+async def generate_section(request: BlogSectionRequest) -> BlogSectionResponse:
+ try:
+ return await service.generate_section(request)
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=str(e))
+
+
+@router.post("/section/optimize", response_model=BlogOptimizeResponse)
+async def optimize_section(request: BlogOptimizeRequest) -> BlogOptimizeResponse:
+ try:
+ return await service.optimize_section(request)
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=str(e))
+
+
+@router.post("/quality/hallucination-check", response_model=HallucinationCheckResponse)
+async def hallucination_check(request: HallucinationCheckRequest) -> HallucinationCheckResponse:
+ try:
+ return await service.hallucination_check(request)
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=str(e))
+
+
+@router.post("/seo/analyze", response_model=BlogSEOAnalyzeResponse)
+async def seo_analyze(request: BlogSEOAnalyzeRequest) -> BlogSEOAnalyzeResponse:
+ try:
+ return await service.seo_analyze(request)
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=str(e))
+
+
+@router.post("/seo/metadata", response_model=BlogSEOMetadataResponse)
+async def seo_metadata(request: BlogSEOMetadataRequest) -> BlogSEOMetadataResponse:
+ try:
+ return await service.seo_metadata(request)
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=str(e))
+
+
+@router.post("/publish", response_model=BlogPublishResponse)
+async def publish(request: BlogPublishRequest) -> BlogPublishResponse:
+ try:
+ return await service.publish(request)
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=str(e))
+
+
diff --git a/backend/app.py b/backend/app.py
index 6a543303..b40b0ed1 100644
--- a/backend/app.py
+++ b/backend/app.py
@@ -457,6 +457,13 @@ app.include_router(content_planning_router)
app.include_router(user_data_router)
app.include_router(strategy_copilot_router)
+# Include AI Blog Writer router
+try:
+ from api.blog_writer.router import router as blog_writer_router
+ app.include_router(blog_writer_router)
+except Exception as e:
+ logger.warning(f"AI Blog Writer router not mounted: {e}")
+
# Include persona router
from api.persona_routes import router as persona_router
app.include_router(persona_router)
diff --git a/backend/models/blog_models.py b/backend/models/blog_models.py
new file mode 100644
index 00000000..8cf1a080
--- /dev/null
+++ b/backend/models/blog_models.py
@@ -0,0 +1,147 @@
+from pydantic import BaseModel, Field
+from typing import List, Optional, Dict, Any
+
+
+class PersonaInfo(BaseModel):
+ persona_id: Optional[str] = None
+ tone: Optional[str] = None
+ audience: Optional[str] = None
+ industry: Optional[str] = None
+
+
+class ResearchSource(BaseModel):
+ title: str
+ url: str
+ excerpt: Optional[str] = None
+ credibility_score: Optional[float] = None
+ published_at: Optional[str] = None
+
+
+class BlogResearchRequest(BaseModel):
+ keywords: List[str]
+ topic: Optional[str] = None
+ industry: Optional[str] = None
+ target_audience: Optional[str] = None
+ tone: Optional[str] = None
+ word_count_target: Optional[int] = 1500
+ persona: Optional[PersonaInfo] = None
+
+
+class BlogResearchResponse(BaseModel):
+ success: bool = True
+ sources: List[ResearchSource] = []
+ keyword_analysis: Dict[str, Any] = {}
+ competitor_analysis: Dict[str, Any] = {}
+ suggested_angles: List[str] = []
+ search_widget: Optional[str] = None # HTML content for search widget
+ search_queries: List[str] = [] # Search queries generated by Gemini
+
+
+class BlogOutlineSection(BaseModel):
+ id: str
+ heading: str
+ subheadings: List[str] = []
+ key_points: List[str] = []
+ references: List[ResearchSource] = []
+ target_words: Optional[int] = None
+ keywords: List[str] = []
+
+
+class BlogOutlineRequest(BaseModel):
+ research: BlogResearchResponse
+ persona: Optional[PersonaInfo] = None
+ word_count: Optional[int] = 1500
+
+
+class BlogOutlineResponse(BaseModel):
+ success: bool = True
+ title_options: List[str] = []
+ outline: List[BlogOutlineSection] = []
+
+
+class BlogOutlineRefineRequest(BaseModel):
+ outline: List[BlogOutlineSection]
+ operation: str
+ section_id: Optional[str] = None
+ payload: Optional[Dict[str, Any]] = None
+
+
+class BlogSectionRequest(BaseModel):
+ section: BlogOutlineSection
+ keywords: List[str] = []
+ tone: Optional[str] = None
+ persona: Optional[PersonaInfo] = None
+
+
+class BlogSectionResponse(BaseModel):
+ success: bool = True
+ markdown: str
+ citations: List[ResearchSource] = []
+
+
+class BlogOptimizeRequest(BaseModel):
+ content: str
+ goals: List[str] = []
+
+
+class BlogOptimizeResponse(BaseModel):
+ success: bool = True
+ optimized: str
+ diff_preview: Optional[str] = None
+
+
+class BlogSEOAnalyzeRequest(BaseModel):
+ content: str
+ keywords: List[str] = []
+
+
+class BlogSEOAnalyzeResponse(BaseModel):
+ success: bool = True
+ seo_score: float
+ density: Dict[str, Any] = {}
+ structure: Dict[str, Any] = {}
+ readability: Dict[str, Any] = {}
+ link_suggestions: List[Dict[str, Any]] = []
+ image_alt_status: Dict[str, Any] = {}
+ recommendations: List[str] = []
+
+
+class BlogSEOMetadataRequest(BaseModel):
+ content: str
+ title: Optional[str] = None
+ keywords: List[str] = []
+
+
+class BlogSEOMetadataResponse(BaseModel):
+ success: bool = True
+ title_options: List[str]
+ meta_descriptions: List[str]
+ open_graph: Dict[str, Any]
+ twitter_card: Dict[str, Any]
+ schema_data: Dict[str, Any]
+
+
+class BlogPublishRequest(BaseModel):
+ platform: str = Field(pattern="^(wix|wordpress)$")
+ html: str
+ metadata: BlogSEOMetadataResponse
+ schedule_time: Optional[str] = None
+
+
+class BlogPublishResponse(BaseModel):
+ success: bool = True
+ platform: str
+ url: Optional[str] = None
+ post_id: Optional[str] = None
+
+
+class HallucinationCheckRequest(BaseModel):
+ content: str
+ sources: List[str] = []
+
+
+class HallucinationCheckResponse(BaseModel):
+ success: bool = True
+ claims: List[Dict[str, Any]] = []
+ suggestions: List[Dict[str, Any]] = []
+
diff --git a/backend/research_analysis_20250911_173832.json b/backend/research_analysis_20250911_173832.json
new file mode 100644
index 00000000..102f6169
--- /dev/null
+++ b/backend/research_analysis_20250911_173832.json
@@ -0,0 +1,84 @@
+{
+ "success": true,
+ "sources": [
+ {
+ "title": "Search: AI content generation trends 2024 2025 technology industry",
+ "url": "https://www.google.com/search?q=AI+content+generation+trends+2024+2025+technology+industry",
+ "excerpt": "Source from Search: AI content generation trends 2024 2025 technology industry",
+ "credibility_score": 0.8,
+ "published_at": "2024-01-01"
+ },
+ {
+ "title": "Search: AI content generation market statistics 2024 2025",
+ "url": "https://www.google.com/search?q=AI+content+generation+market+statistics+2024+2025",
+ "excerpt": "Source from Search: AI content generation market statistics 2024 2025",
+ "credibility_score": 0.8,
+ "published_at": "2024-01-01"
+ },
+ {
+ "title": "Search: AI content generation expert opinions 2024 2025",
+ "url": "https://www.google.com/search?q=AI+content+generation+expert+opinions+2024+2025",
+ "excerpt": "Source from Search: AI content generation expert opinions 2024 2025",
+ "credibility_score": 0.8,
+ "published_at": "2024-01-01"
+ },
+ {
+ "title": "Search: recent developments AI content generation 2024 2025",
+ "url": "https://www.google.com/search?q=recent+developments+AI+content+generation+2024+2025",
+ "excerpt": "Source from Search: recent developments AI content generation 2024 2025",
+ "credibility_score": 0.8,
+ "published_at": "2024-01-01"
+ },
+ {
+ "title": "Search: AI content generation market analysis forecast 2024 2025",
+ "url": "https://www.google.com/search?q=AI+content+generation+market+analysis+forecast+2024+2025",
+ "excerpt": "Source from Search: AI content generation market analysis forecast 2024 2025",
+ "credibility_score": 0.8,
+ "published_at": "2024-01-01"
+ }
+ ],
+ "keyword_analysis": {
+ "primary": [
+ "AI content generation"
+ ],
+ "secondary": [
+ "blog writing"
+ ],
+ "long_tail": [
+ "AI content generation guide",
+ "blog writing guide"
+ ],
+ "search_intent": "informational",
+ "difficulty": 6,
+ "content_gaps": [
+ "AI content generation best practices",
+ "blog writing best practices"
+ ],
+ "analysis_content": "## The AI Content Generation Revolution: Navigating 2024-2025 Trends for Strategic Advantage\n\nThe landscape of content creation is undergoing a seismic shift, powered by the rapid advancements in Arti"
+ },
+ "competitor_analysis": {
+ "top_competitors": [],
+ "content_gaps": [],
+ "opportunities": [],
+ "analysis_notes": "Competitor analysis from research"
+ },
+ "suggested_angles": [
+ "How ALwrity content generation is Transforming Technology",
+ "Latest ALwrity content generation Trends: What You Need to Know",
+ "ALwrity content generation Best Practices for Technology",
+ "Case Study: ALwrity content generation Success Stories",
+ "The Future of ALwrity content generation in Technology"
+ ],
+ "search_widget": "\n
\n
\n
\n \n \n \n \n \n
\n \n \n \n \n \n \n
\n
\n
\n
\n",
+ "search_queries": [
+ "AI content generation trends 2024 2025 technology industry",
+ "AI content generation market statistics 2024 2025",
+ "AI content generation expert opinions 2024 2025",
+ "recent developments AI content generation 2024 2025",
+ "AI content generation market analysis forecast 2024 2025",
+ "best practices AI content generation case studies 2024 2025",
+ "keywords for AI content generation technology",
+ "top AI content generation competitors market share 2024",
+ "AI content generation content gaps"
+ ]
+}
\ No newline at end of file
diff --git a/backend/research_analysis_20250911_174238.json b/backend/research_analysis_20250911_174238.json
new file mode 100644
index 00000000..8379d320
--- /dev/null
+++ b/backend/research_analysis_20250911_174238.json
@@ -0,0 +1,206 @@
+{
+ "success": true,
+ "sources": [
+ {
+ "title": "alwrity.com",
+ "url": "https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQEbkhlVuzpjaGypNYMsWxvHBbX3AmqYaaOrbGeSKtT03vLSAx9LQlACNs0ulmX0oFvX610EvDErz6lWd9TTh7pSvrbWM-L6WaKh8HiAEucGVHXbKQ==",
+ "excerpt": "Source from alwrity.com",
+ "credibility_score": 0.8,
+ "published_at": "2024-01-01"
+ },
+ {
+ "title": "alwrity.com",
+ "url": "https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQHI_UttMuqLpNEDZnP22X5mhTqjrqObIjM66Ks8UoMHPN_SlF5XenKZlUiOPGQRuYPoNDqRctDEeheEZOZMm3SqM5OkrActXWVNXaqsdk4WwPcI4Jhep8vo1cm27c4=",
+ "excerpt": "Source from alwrity.com",
+ "credibility_score": 0.8,
+ "published_at": "2024-01-01"
+ },
+ {
+ "title": "youtube.com",
+ "url": "https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQGkWsMQ3Tjq8DewK8EikdNign9gcrLv11xR30xrMx-vq1sSfdmGk5w8s6vuDtIL4-YxqSyDcpb4ifwmwF9lq9tsKF_fd0Zenzd19PHrQ0-65vgeEO5CV6oGdO1TN7eZ0SCkLHsmFZ8=",
+ "excerpt": "Source from youtube.com",
+ "credibility_score": 0.8,
+ "published_at": "2024-01-01"
+ },
+ {
+ "title": "youtube.com",
+ "url": "https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQEBvGGtIXARTEe4XR1DH-6yFwzEC8HkG7GXEz6OpKTwznIJPC3V2VWbU5BqZpEzdt5U82qbuAdo4iZ006PzcanX-OQWA8EVy0R5-Yz43snYK-kt9JfvniBxLGDHXdz70J4creDZoSY=",
+ "excerpt": "Source from youtube.com",
+ "credibility_score": 0.8,
+ "published_at": "2024-01-01"
+ },
+ {
+ "title": "beyondkey.com",
+ "url": "https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQGRwVUwjgBma_K_ceWgs1ha6f5zat1idtf7AAXTnGW94yvWxNSSCA4615crchD8y4aSxXbVcyxj-mxDQP685ZvKevkDxPUTKdlTYvRWkT5Ewbc5vMo17qqvcw2MPF3SEvLjODMUT7hzUFJXb5gIehVh1Pq0g1MFz_3uW8jxdcHMend_PjiR6VE=",
+ "excerpt": "Source from beyondkey.com",
+ "credibility_score": 0.8,
+ "published_at": "2024-01-01"
+ },
+ {
+ "title": "medium.com",
+ "url": "https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQG3nG07PWqQhD9bLlXOBM9fBHVxgkZM_DEnKsUeHl-te61BWZcKL0fQbe8_oZUZWRFggLi4CgZxO__3Sfw5mfaYHDU6394PWEQ7qTa0UUwaTuKnLWBA2RX4A5LjGY0LglG01higs7_fetcOWwPTSvg-Y06d8pSZWUHHuiunoklPoQuR1tAQKvhKDvZbmJMIHPMkoLyovsag6rjmN_Chn0hqPf4=",
+ "excerpt": "Source from medium.com",
+ "credibility_score": 0.8,
+ "published_at": "2024-01-01"
+ },
+ {
+ "title": "quickcreator.io",
+ "url": "https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQHedj-KoY0QaU_0xyYVSyIb6NJWQjYPx5OpFxdRefde4fxALVZZ_uCcDyODW8UOeCTEJl_ynXnE5yKlFd_6jgHND9koGUnCPfwLDeZ-XzVjHlgY5V2GnmH6B8bH-lvdzVYUBZlvI0iDtq5hoNxqfz10jLh834ZrIc6NjTtG7kXLR7ddMg7FjEQ57Cu49Ug=",
+ "excerpt": "Source from quickcreator.io",
+ "credibility_score": 0.8,
+ "published_at": "2024-01-01"
+ },
+ {
+ "title": "eimt.edu.eu",
+ "url": "https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQFEyrobre2Q5LbCgLAYSxH1C8BCNy3Ln4iOIQ6_NGAD63_lp6EqMYourvzgiSOXtane5aLyRCzc0erIbMe0TIsO406sZ7jULZ8AbkaD93TTn34wRbNuUGOg9sCf9PcTAJSWMfX05cle8BfK6mhQU2t9EslhcSTEZBRTTGgPu_NmfMUVjZJ_9_6fT4qLUZtAuqpe",
+ "excerpt": "Source from eimt.edu.eu",
+ "credibility_score": 0.8,
+ "published_at": "2024-01-01"
+ },
+ {
+ "title": "youtube.com",
+ "url": "https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQEgK2_pZbhzW_wihG2wmD383PgA3awfJLNUBdMoevHvDou8oDoI24xQeKAaJsDpHJ-LxtRIdhOlg6RZCwLSRsjKQlL3_mFddH30OLZU-_IXAZ8sLmMCGgM8J7isNuhWQZTNrkoT2x0=",
+ "excerpt": "Source from youtube.com",
+ "credibility_score": 0.8,
+ "published_at": "2024-01-01"
+ },
+ {
+ "title": "synthesia.io",
+ "url": "https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQHMYC4-O4vRXhpwIZZ49Y7sJVJQ9pSJ6rDEN_dayRuoct0Q2ECmVH26wsyqdQAYiLPZLkQTlIgGZBIocrhqkZDj97UmGF66dGzvSGFUJXvOA127mFWYKXJ8TUYIr8Tjn9HQ",
+ "excerpt": "Source from synthesia.io",
+ "credibility_score": 0.8,
+ "published_at": "2024-01-01"
+ },
+ {
+ "title": "tryleap.ai",
+ "url": "https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQGZKtSSrxOYpXnBKsFhUoAtpw01oVKooJkUIpyVYqEguhSDWcX7svylTkuFy_8RrxKmvKBkNJjRn6KE6OMCTHV4AOET0fUaPJhxfwcnyvY1BB8GHQeDa_B-VfchBp_qeAdeS2m-_IQuLOUI-jV7wFpaA9Gg3-rcK_uqwtmrprxZCztkyJM=",
+ "excerpt": "Source from tryleap.ai",
+ "credibility_score": 0.8,
+ "published_at": "2024-01-01"
+ },
+ {
+ "title": "hulkapps.com",
+ "url": "https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQGcKDCewhOKAICoIWfiwqPDerQO3P6Ipm6vsTz-ujH7G-Vg9eo3LgACMbsTxk9IUCg7-fn_bV_QKah9N8VW0YEXBfyWZRqdTMbBbg07fswpACpIn1znpRmiJU3o_G3w99AgLNBtenIzIPpTnBj2ascMGNgEmrgDDznNFaX4LP5E0F6ToIWt3A3SrwJWpJwKyfyJ",
+ "excerpt": "Source from hulkapps.com",
+ "credibility_score": 0.8,
+ "published_at": "2024-01-01"
+ },
+ {
+ "title": "marketful.com",
+ "url": "https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQGFSpjwPUVCdlTrnxsqCp-O7T2-kUk4_hZrVYYZDrfFxvDLp5knmTd8PSTcoDybNfq8Iiod5DLhYaFIrL5G8yl2oU3UCl4KNqkRb95U6alKy2LumA1iHzYE4xtXDlvPdoP31pWPt5Pi2P_1aMTt921v_0oE1s4=",
+ "excerpt": "Source from marketful.com",
+ "credibility_score": 0.8,
+ "published_at": "2024-01-01"
+ },
+ {
+ "title": "ics-digital.com",
+ "url": "https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQHf0mDC8n9_pdUthEbF2f_FbRkiwcyc0I4GcRqa1dnbRzSU50Y9LiSWmWDpOM3-GYNmB3YmNZT7p0QBxbtKuju0ol7jA58RYphVUHtmJXwhea4lEwkKCAf8ONvIHtcGqpY4QdbsixE4rV2en6d9qoPER7i1QkPyknYsPw0QSJI=",
+ "excerpt": "Source from ics-digital.com",
+ "credibility_score": 0.8,
+ "published_at": "2024-01-01"
+ },
+ {
+ "title": "frase.io",
+ "url": "https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQG7h5MlbO5qp0jD2NFIyH1HUQff6CMreopPd0MExSZ00saW_ikR9QJTABCBmdR8CSdkAphaMJYOJG9i3qQQyoOlg_Seh_9JF8zQqdPj",
+ "excerpt": "Source from frase.io",
+ "credibility_score": 0.8,
+ "published_at": "2024-01-01"
+ },
+ {
+ "title": "thebusinessresearchcompany.com",
+ "url": "https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQEEvY4sZPVx7eA9v_j8qaQ_Tj7ljjwZ0hIdrxERYykbSkTId97TlSY74VYyu9IlOmcRtXpwNSgp6Q35BUKB3gvb6o0o6-PNfoYoEPHx6N6sXZs6lrtvqqpcTLqc7sOskZ-jxmZ_IEKX_VeIRxp41XBTY603RDiU-1CqrNUUkjwbxNorilqmV-3Oa0bbi5TvqoSNS98GHQrV0VUN9d21jMLPSpj9xRcB",
+ "excerpt": "Source from thebusinessresearchcompany.com",
+ "credibility_score": 0.8,
+ "published_at": "2024-01-01"
+ },
+ {
+ "title": "grandviewresearch.com",
+ "url": "https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQE-1--CbRoZLTs6PrEs7_b5Q4D2eJViu2pZrVlDTS8QfRf1DSDepwb2KO0ozfq04iLJRHXeBjLnu8tV6bOD9mXTv8paG_3Irgow9cpOw5jsPrvvqXmAJA-IvebcDdISuIUk7rZsfn95_BpnMsm4cWR4HTlYwTLWfGR5zUOtuupKG0Od2E5HpnYIH3TmlZ45y2fvvhw0wk8qjA==",
+ "excerpt": "Source from grandviewresearch.com",
+ "credibility_score": 0.8,
+ "published_at": "2024-01-01"
+ },
+ {
+ "title": "verifiedmarketresearch.com",
+ "url": "https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQEAPa_eY2-XR46ROJMDfq4b4YbZivGtdu2F0z2I3tHk10ZRr_j4HIPFZkmwFuvFm4iSBh7efJBbsvvp8TjelRmgskKKZSJw52f4oNsp4b-ewlmv5daOrwcH-to89YjZVvzpj-fs_hvCoKIDnmSQubcS62jqs_EVbWAomX-8cr7_gfg=",
+ "excerpt": "Source from verifiedmarketresearch.com",
+ "credibility_score": 0.8,
+ "published_at": "2024-01-01"
+ },
+ {
+ "title": "siegemedia.com",
+ "url": "https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQHkqpyuKPcPnTPdEI0qp6aR_gS7FOAgmJMjST58kZQtmApd9g1eynllR3vtkjVg1Y4IjLifjICqueLx-32ugvMPISwXlqq8D8LgC5j_YLvvyTAaq9drZM56OpgaRscGt4cuD-YoEhMs53xX18h798fs7gbc3g==",
+ "excerpt": "Source from siegemedia.com",
+ "credibility_score": 0.8,
+ "published_at": "2024-01-01"
+ },
+ {
+ "title": "synthesia.io",
+ "url": "https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQEtHmkgnrc-O2jG90jSY2OL_SvNN2ToZFl2CYmA7eSpPuDGvMWDpbg6NuTsAB8fFBwP4eRcRvqtoP74lJINPvxHw6uARdRuGIZmlBcldYRYiBvWLOz4oMcRzfIDNXSzzQUM57cmQwc=",
+ "excerpt": "Source from synthesia.io",
+ "credibility_score": 0.8,
+ "published_at": "2024-01-01"
+ },
+ {
+ "title": "makebot.ai",
+ "url": "https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQGNko08ftgaMVm9H1Jh-4vsgHjfFRriAeloVI_KWg2Ee-1MIs1N6o7X3MC0n98C4YgVzeXWM8W_3R-pFze-6R6uJsu1tdZms_1MI3NPGi4oKdSVRULy2vaY_SQVOiEDyqM3JSkJ3z1QZos-lUEeuG46iLr43KX3bJHRR5dzTh7eNFDv43DjEupuGR2KYX60j7ui929PXdP-e92yNcQuOu18740to6Gto2FdPT0=",
+ "excerpt": "Source from makebot.ai",
+ "credibility_score": 0.8,
+ "published_at": "2024-01-01"
+ },
+ {
+ "title": "medium.com",
+ "url": "https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQE2oEe411ttOlvnVvTUNZI60cD5iLva5mD7SDy0q1bGMM-DTyxXsNeAnegRIGY--svZZXEoqZrWnnA0K-j9T7KKZgZcqSodRYS8I5Qx_xKyuZ6OwJhKMUo4PFPhbvcid2mq1m9oS65rgHVM8iNFUBOtxyvYYOQGGf0tOsYGYevF68eQ5PaGJ-6iEbA=",
+ "excerpt": "Source from medium.com",
+ "credibility_score": 0.8,
+ "published_at": "2024-01-01"
+ }
+ ],
+ "keyword_analysis": {
+ "primary": [
+ "AI content generation"
+ ],
+ "secondary": [
+ "blog writing"
+ ],
+ "long_tail": [
+ "AI content generation guide",
+ "blog writing guide"
+ ],
+ "search_intent": "informational",
+ "difficulty": 6,
+ "content_gaps": [
+ "AI content generation best practices",
+ "blog writing best practices"
+ ],
+ "analysis_content": "## The Future of Content Creation: A Deep Dive into ALwrity and AI-Powered Content Generation (2024-2025)\n\nThe landscape of content creation is undergoing a profound transformation, largely driven by "
+ },
+ "competitor_analysis": {
+ "top_competitors": [],
+ "content_gaps": [],
+ "opportunities": [],
+ "analysis_notes": "Competitor analysis from research"
+ },
+ "suggested_angles": [
+ "How ALwrity content generation is Transforming Technology",
+ "Latest ALwrity content generation Trends: What You Need to Know",
+ "ALwrity content generation Best Practices for Technology",
+ "Case Study: ALwrity content generation Success Stories",
+ "The Future of ALwrity content generation in Technology"
+ ],
+ "search_widget": "\n\n
\n
\n \n \n \n \n \n
\n \n \n \n \n \n \n
\n
\n
\n
\n",
+ "search_queries": [
+ "ALwrity content generation",
+ "AI content generation trends 2024 2025",
+ "AI content writing market size forecast 2024 2025",
+ "expert opinions on AI content creation 2024 2025",
+ "recent developments in generative AI for content 2024 2025",
+ "best practices for AI-powered content strategy",
+ "AI content generation tools comparison 2024",
+ "AI content writing software",
+ "top AI content generators 2024",
+ "keywords for AI content generation",
+ "long tail keywords AI content writing",
+ "AI content creation case studies 2024"
+ ]
+}
\ No newline at end of file
diff --git a/backend/scripts/create_billing_tables.py b/backend/scripts/create_billing_tables.py
new file mode 100644
index 00000000..60b36726
--- /dev/null
+++ b/backend/scripts/create_billing_tables.py
@@ -0,0 +1,217 @@
+"""
+Database Migration Script for Billing System
+Creates all tables needed for billing, usage tracking, and subscription management.
+"""
+
+import sys
+import os
+from pathlib import Path
+
+# Add the backend directory to Python path
+backend_dir = Path(__file__).parent.parent
+sys.path.insert(0, str(backend_dir))
+
+from sqlalchemy import create_engine, text
+from sqlalchemy.orm import sessionmaker
+from loguru import logger
+import traceback
+
+# Import models
+from models.subscription_models import Base as SubscriptionBase
+from services.database import DATABASE_URL
+from services.pricing_service import PricingService
+
+def create_billing_tables():
+ """Create all billing and subscription-related tables."""
+
+ try:
+ # Create engine
+ engine = create_engine(DATABASE_URL, echo=True)
+
+ # Create all tables
+ logger.info("Creating billing and subscription system tables...")
+ SubscriptionBase.metadata.create_all(bind=engine)
+ logger.info("โ
Billing and subscription tables created successfully")
+
+ # Create session for data initialization
+ SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
+ db = SessionLocal()
+
+ try:
+ # Initialize pricing and plans
+ pricing_service = PricingService(db)
+
+ logger.info("Initializing default API pricing...")
+ pricing_service.initialize_default_pricing()
+ logger.info("โ
Default API pricing initialized")
+
+ logger.info("Initializing default subscription plans...")
+ pricing_service.initialize_default_plans()
+ logger.info("โ
Default subscription plans initialized")
+
+ except Exception as e:
+ logger.error(f"Error initializing default data: {e}")
+ logger.error(traceback.format_exc())
+ db.rollback()
+ raise
+ finally:
+ db.close()
+
+ logger.info("๐ Billing system setup completed successfully!")
+
+ # Display summary
+ display_setup_summary(engine)
+
+ except Exception as e:
+ logger.error(f"โ Error creating billing tables: {e}")
+ logger.error(traceback.format_exc())
+ raise
+
+def display_setup_summary(engine):
+ """Display a summary of the created tables and data."""
+
+ try:
+ with engine.connect() as conn:
+ logger.info("\n" + "="*60)
+ logger.info("BILLING SYSTEM SETUP SUMMARY")
+ logger.info("="*60)
+
+ # Check tables
+ tables_query = text("""
+ SELECT name FROM sqlite_master
+ WHERE type='table' AND (
+ name LIKE '%subscription%' OR
+ name LIKE '%usage%' OR
+ name LIKE '%billing%' OR
+ name LIKE '%pricing%' OR
+ name LIKE '%alert%'
+ )
+ ORDER BY name
+ """)
+
+ result = conn.execute(tables_query)
+ tables = result.fetchall()
+
+ logger.info(f"\n๐ Created Tables ({len(tables)}):")
+ for table in tables:
+ logger.info(f" โข {table[0]}")
+
+ # Check subscription plans
+ try:
+ plans_query = text("SELECT COUNT(*) FROM subscription_plans")
+ result = conn.execute(plans_query)
+ plan_count = result.fetchone()[0]
+ logger.info(f"\n๐ณ Subscription Plans: {plan_count}")
+
+ if plan_count > 0:
+ plans_detail_query = text("""
+ SELECT name, tier, price_monthly, price_yearly
+ FROM subscription_plans
+ ORDER BY price_monthly
+ """)
+ result = conn.execute(plans_detail_query)
+ plans = result.fetchall()
+
+ for plan in plans:
+ name, tier, monthly, yearly = plan
+ logger.info(f" โข {name} ({tier}): ${monthly}/month, ${yearly}/year")
+ except Exception as e:
+ logger.warning(f"Could not check subscription plans: {e}")
+
+ # Check API pricing
+ try:
+ pricing_query = text("SELECT COUNT(*) FROM api_provider_pricing")
+ result = conn.execute(pricing_query)
+ pricing_count = result.fetchone()[0]
+ logger.info(f"\n๐ฐ API Pricing Entries: {pricing_count}")
+
+ if pricing_count > 0:
+ pricing_detail_query = text("""
+ SELECT provider, model_name, cost_per_input_token, cost_per_output_token
+ FROM api_provider_pricing
+ WHERE cost_per_input_token > 0 OR cost_per_output_token > 0
+ ORDER BY provider, model_name
+ LIMIT 10
+ """)
+ result = conn.execute(pricing_detail_query)
+ pricing_entries = result.fetchall()
+
+ logger.info("\n LLM Pricing (per token) - Top 10:")
+ for entry in pricing_entries:
+ provider, model, input_cost, output_cost = entry
+ logger.info(f" โข {provider}/{model}: ${input_cost:.8f} in, ${output_cost:.8f} out")
+ except Exception as e:
+ logger.warning(f"Could not check API pricing: {e}")
+
+ logger.info("\n" + "="*60)
+ logger.info("NEXT STEPS:")
+ logger.info("="*60)
+ logger.info("1. Billing system is ready for use")
+ logger.info("2. API endpoints are available at:")
+ logger.info(" GET /api/subscription/plans")
+ logger.info(" GET /api/subscription/usage/{user_id}")
+ logger.info(" GET /api/subscription/dashboard/{user_id}")
+ logger.info(" GET /api/subscription/pricing")
+ logger.info("\n3. Frontend billing dashboard is integrated")
+ logger.info("4. Usage tracking middleware is active")
+ logger.info("5. Real-time cost monitoring is enabled")
+ logger.info("="*60)
+
+ except Exception as e:
+ logger.error(f"Error displaying summary: {e}")
+
+def check_existing_tables(engine):
+ """Check if billing tables already exist."""
+
+ try:
+ with engine.connect() as conn:
+ # Check for billing tables
+ check_query = text("""
+ SELECT name FROM sqlite_master
+ WHERE type='table' AND (
+ name = 'subscription_plans' OR
+ name = 'user_subscriptions' OR
+ name = 'api_usage_logs' OR
+ name = 'usage_summaries' OR
+ name = 'api_provider_pricing' OR
+ name = 'usage_alerts'
+ )
+ """)
+
+ result = conn.execute(check_query)
+ existing_tables = result.fetchall()
+
+ if existing_tables:
+ logger.warning(f"Found existing billing tables: {[t[0] for t in existing_tables]}")
+ logger.info("Tables already exist. Skipping creation to preserve data.")
+ return False
+
+ return True
+
+ except Exception as e:
+ logger.error(f"Error checking existing tables: {e}")
+ return True # Proceed anyway
+
+if __name__ == "__main__":
+ logger.info("๐ Starting billing system database migration...")
+
+ try:
+ # Create engine to check existing tables
+ engine = create_engine(DATABASE_URL, echo=False)
+
+ # Check existing tables
+ if not check_existing_tables(engine):
+ logger.info("โ
Billing tables already exist, skipping creation")
+ sys.exit(0)
+
+ # Create tables and initialize data
+ create_billing_tables()
+
+ logger.info("โ
Billing system migration completed successfully!")
+
+ except KeyboardInterrupt:
+ logger.info("Migration cancelled by user")
+ sys.exit(0)
+ except Exception as e:
+ logger.error(f"โ Migration failed: {e}")
+ sys.exit(1)
diff --git a/backend/services/blog_writer/blog_service.py b/backend/services/blog_writer/blog_service.py
new file mode 100644
index 00000000..7bad6cfa
--- /dev/null
+++ b/backend/services/blog_writer/blog_service.py
@@ -0,0 +1,649 @@
+from typing import Any, Dict, List
+from loguru import logger
+from services.llm_providers.gemini_provider import gemini_structured_json_response
+
+from models.blog_models import (
+ BlogResearchRequest,
+ BlogResearchResponse,
+ BlogOutlineRequest,
+ BlogOutlineResponse,
+ BlogOutlineRefineRequest,
+ BlogSectionRequest,
+ BlogSectionResponse,
+ BlogOptimizeRequest,
+ BlogOptimizeResponse,
+ BlogSEOAnalyzeRequest,
+ BlogSEOAnalyzeResponse,
+ BlogSEOMetadataRequest,
+ BlogSEOMetadataResponse,
+ BlogPublishRequest,
+ BlogPublishResponse,
+ ResearchSource,
+ BlogOutlineSection,
+)
+
+
+class BlogWriterService:
+ """Service layer for AI Blog Writer (stub implementations for scaffolding)."""
+
+ async def research(self, request: BlogResearchRequest) -> BlogResearchResponse:
+ """
+ Stage 1: Research & Strategy (AI Orchestration)
+ Uses ONLY Gemini's native Google Search grounding - ONE API call for everything.
+ Follows LinkedIn service pattern for efficiency and cost optimization.
+ """
+ from services.llm_providers.gemini_grounded_provider import GeminiGroundedProvider
+
+ gemini = GeminiGroundedProvider()
+
+ topic = request.topic or ", ".join(request.keywords)
+ industry = request.industry or (request.persona.industry if request.persona and request.persona.industry else "General")
+ target_audience = getattr(request.persona, 'target_audience', 'General') if request.persona else 'General'
+
+ # Single comprehensive research prompt - Gemini handles Google Search automatically
+ research_prompt = f"""
+ Research the topic "{topic}" in the {industry} industry for {target_audience} audience. Provide a 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
+
+ Focus on factual, up-to-date information from credible sources.
+ Include specific data points, percentages, and recent developments.
+ Structure your response with clear sections for each analysis area.
+ """
+
+ # Single Gemini call with native Google Search grounding - no fallbacks
+ gemini_result = await gemini.generate_grounded_content(
+ prompt=research_prompt,
+ content_type="research",
+ max_tokens=2000
+ )
+
+ # Extract sources from grounding metadata
+ sources = self._extract_sources_from_grounding(gemini_result)
+
+ # Extract search widget and queries for UI display
+ search_widget = gemini_result.get("search_widget", "") or ""
+ search_queries = gemini_result.get("search_queries", []) or []
+
+ # Parse the comprehensive response for different analysis components
+ content = gemini_result.get("content", "")
+ keyword_analysis = self._parse_keyword_analysis(content, request.keywords)
+ competitor_analysis = self._parse_competitor_analysis(content)
+ suggested_angles = self._parse_content_angles(content, topic, industry)
+
+ logger.info(f"Research completed successfully with {len(sources)} sources and {len(search_queries)} search queries")
+
+ return BlogResearchResponse(
+ success=True,
+ sources=sources,
+ keyword_analysis=keyword_analysis,
+ competitor_analysis=competitor_analysis,
+ suggested_angles=suggested_angles,
+ # Add search widget and queries for UI display
+ search_widget=search_widget if 'search_widget' in locals() else "",
+ search_queries=search_queries if 'search_queries' in locals() else [],
+ )
+
+ def _extract_sources_from_grounding(self, gemini_result: Dict[str, Any]) -> List[ResearchSource]:
+ """Extract sources from Gemini grounding metadata."""
+ sources = []
+
+ # The Gemini grounded provider already extracts sources and puts them in the 'sources' field
+ raw_sources = gemini_result.get("sources", [])
+ for src in raw_sources:
+ source = ResearchSource(
+ title=src.get("title", "Untitled"),
+ url=src.get("url", ""),
+ excerpt=src.get("content", "")[:500] if src.get("content") else f"Source from {src.get('title', 'web')}",
+ credibility_score=float(src.get("credibility_score", 0.8)),
+ published_at=str(src.get("publication_date", "2024-01-01"))
+ )
+ sources.append(source)
+
+ return sources
+
+ def _parse_keyword_analysis(self, content: str, original_keywords: List[str]) -> Dict[str, Any]:
+ """Parse keyword analysis from the research content."""
+ # Extract keywords from content sections
+ lines = content.split('\n')
+ keyword_section = []
+ in_keyword_section = False
+
+ for line in lines:
+ if 'keyword' in line.lower() and ('analysis' in line.lower() or 'primary' in line.lower()):
+ in_keyword_section = True
+ continue
+ if in_keyword_section and line.strip():
+ if line.startswith(('1.', '2.', '3.', '4.', '5.', '6.', '7.', '8.', '9.')):
+ break
+ keyword_section.append(line.strip())
+
+ return {
+ "primary": original_keywords[:1] if original_keywords else [],
+ "secondary": original_keywords[1:] if len(original_keywords) > 1 else [],
+ "long_tail": [f"{kw} guide" for kw in original_keywords[:2]] if original_keywords else [],
+ "search_intent": "informational",
+ "difficulty": 6,
+ "content_gaps": [f"{kw} best practices" for kw in original_keywords[:2]] if original_keywords else [],
+ "analysis_content": "\n".join(keyword_section) if keyword_section else content[:200]
+ }
+
+ def _parse_competitor_analysis(self, content: str) -> Dict[str, Any]:
+ """Parse competitor analysis from the research content."""
+ lines = content.split('\n')
+ competitor_section = []
+ in_competitor_section = False
+
+ for line in lines:
+ if 'competitor' in line.lower() and ('analysis' in line.lower() or 'top' in line.lower()):
+ in_competitor_section = True
+ continue
+ if in_competitor_section and line.strip():
+ if line.startswith(('1.', '2.', '3.', '4.', '5.', '6.', '7.', '8.', '9.')):
+ break
+ competitor_section.append(line.strip())
+
+ return {
+ "top_competitors": [],
+ "content_gaps": [],
+ "opportunities": [],
+ "analysis_notes": "\n".join(competitor_section) if competitor_section else "Competitor analysis from research"
+ }
+
+ def _parse_content_angles(self, content: str, topic: str, industry: str) -> List[str]:
+ """Parse content angles from the research content."""
+ lines = content.split('\n')
+ angles_section = []
+ in_angles_section = False
+
+ for line in lines:
+ if 'angle' in line.lower() and ('suggest' in line.lower() or 'content' in line.lower()):
+ in_angles_section = True
+ continue
+ if in_angles_section and line.strip():
+ if line.startswith(('1.', '2.', '3.', '4.', '5.', '6.', '7.', '8.', '9.')):
+ break
+ if line.strip() and not line.startswith(('โข', '-', '*')):
+ angles_section.append(line.strip())
+
+ # If no angles found in content, use fallback
+ if not angles_section:
+ angles_section = [
+ f"How {topic} is Transforming {industry}",
+ f"Latest {topic} Trends: What You Need to Know",
+ f"{topic} Best Practices for {industry}",
+ f"Case Study: {topic} Success Stories",
+ f"The Future of {topic} in {industry}"
+ ]
+
+ return angles_section[:5] # Return top 5 angles
+
+
+ async def generate_outline(self, request: BlogOutlineRequest) -> BlogOutlineResponse:
+ """
+ Stage 2: Content Planning with AI-generated outline using research results
+ Uses Gemini with research data to create comprehensive, SEO-optimized outline
+ """
+ # Extract research insights
+ research = request.research
+ primary_keywords = research.keyword_analysis.get('primary', [])
+ secondary_keywords = research.keyword_analysis.get('secondary', [])
+ content_angles = research.suggested_angles
+ sources = research.sources
+ search_intent = research.keyword_analysis.get('search_intent', 'informational')
+
+ # Build sophisticated outline generation prompt with advanced content strategy
+ outline_prompt = f"""
+ You are a world-class content strategist and SEO expert with 15+ years of experience creating viral, high-converting blog content. Your outlines have generated millions of views and driven significant business results.
+
+ CONTENT STRATEGY BRIEF:
+ Topic: {', '.join(primary_keywords)}
+ Search Intent: {search_intent}
+ Target Word Count: {request.word_count or 1500} words
+ Industry Context: {getattr(request.persona, 'industry', 'General') if request.persona else 'General'}
+ Audience: {getattr(request.persona, 'target_audience', 'General') if request.persona else 'General'}
+
+ RESEARCH INTELLIGENCE:
+ Primary Keywords: {', '.join(primary_keywords)}
+ Secondary Keywords: {', '.join(secondary_keywords)}
+ Long-tail Opportunities: {', '.join(research.keyword_analysis.get('long_tail', [])[:5])}
+
+ Content Angles Discovered:
+ {chr(10).join([f"โข {angle}" for angle in content_angles[:6]])}
+
+ Research Sources Available: {len(sources)} authoritative sources with current data
+
+ STRATEGIC OUTLINE REQUIREMENTS:
+
+ 1. CONTENT ARCHITECTURE:
+ - Create 5-7 sections that follow a logical progression
+ - Each section must have a clear purpose and value proposition
+ - Build a narrative arc that keeps readers engaged throughout
+ - Include strategic content gaps that competitors miss
+
+ 2. SEO OPTIMIZATION:
+ - Naturally integrate primary keywords in H2 headings (not forced)
+ - Use secondary keywords in subheadings and key points
+ - Include long-tail keywords in natural language
+ - Optimize for featured snippets and voice search
+ - Create semantic keyword clusters
+
+ 3. READER ENGAGEMENT:
+ - Start with a compelling hook that addresses pain points
+ - Use storytelling elements and real-world examples
+ - Include actionable insights readers can implement immediately
+ - Create sections that encourage social sharing
+ - End with a strong call-to-action
+
+ 4. CONTENT DEPTH:
+ - Each section: 2-4 specific, actionable subheadings
+ - Each section: 4-6 key points with research-backed insights
+ - Include data points, statistics, and case studies where relevant
+ - Address common objections and questions
+ - Provide unique angles not covered by competitors
+
+ 5. WORD COUNT DISTRIBUTION:
+ - Introduction: 10-15% of total words
+ - Main sections: 70-80% of total words (distributed strategically)
+ - Conclusion: 10-15% of total words
+ - Total target: {request.word_count or 1500} words
+
+ 6. COMPETITIVE ADVANTAGE:
+ - Include fresh perspectives from recent research
+ - Address emerging trends and future implications
+ - Provide deeper insights than surface-level content
+ - Include practical tools, frameworks, or templates
+ - Reference authoritative sources and data
+
+ TITLE STRATEGY:
+ Create 3 distinct title options that:
+ - Include primary keywords naturally
+ - Promise clear value to readers
+ - Create curiosity and urgency
+ - Are optimized for click-through rates
+ - Work well for social media sharing
+
+ CRITICAL: Respond ONLY with valid JSON. No additional text or explanations.
+
+ JSON FORMAT:
+ {{
+ "title_options": [
+ "Compelling title with primary keyword and benefit",
+ "Question-based title that creates curiosity",
+ "How-to title with specific outcome promise"
+ ],
+ "outline": [
+ {{
+ "heading": "Strategic section title with primary keyword",
+ "subheadings": [
+ "Specific, actionable subheading 1",
+ "Data-driven subheading 2",
+ "Case study or example subheading 3"
+ ],
+ "key_points": [
+ "Research-backed insight with specific data",
+ "Actionable step readers can take immediately",
+ "Common mistake to avoid with explanation",
+ "Advanced tip that provides competitive advantage",
+ "Real-world example or case study"
+ ],
+ "target_words": 300,
+ "keywords": ["primary keyword", "secondary keyword", "long-tail phrase"]
+ }}
+ ]
+ }}
+ """
+
+ logger.info("Generating AI-powered outline using research results")
+
+ # Define the schema for structured JSON response
+ outline_schema = {
+ "type": "object",
+ "properties": {
+ "title_options": {
+ "type": "array",
+ "items": {"type": "string"},
+ "description": "3 SEO-optimized title options"
+ },
+ "outline": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "id": {"type": "string"},
+ "heading": {"type": "string"},
+ "subheadings": {
+ "type": "array",
+ "items": {"type": "string"}
+ },
+ "key_points": {
+ "type": "array",
+ "items": {"type": "string"}
+ },
+ "word_count": {"type": "integer"},
+ "keywords": {
+ "type": "array",
+ "items": {"type": "string"},
+ "description": "Keywords to focus on in this section"
+ }
+ },
+ "required": ["id", "heading", "subheadings", "key_points", "word_count", "keywords"]
+ }
+ }
+ },
+ "required": ["title_options", "outline"]
+ }
+
+ # Generate outline using structured JSON response (no grounding needed)
+ outline_data = gemini_structured_json_response(
+ prompt=outline_prompt,
+ schema=outline_schema,
+ temperature=0.3,
+ max_tokens=3000
+ )
+
+ # Check for errors in the response
+ if isinstance(outline_data, dict) and 'error' in outline_data:
+ logger.error(f"Gemini structured response error: {outline_data['error']}")
+ raise ValueError(f"AI outline generation failed: {outline_data['error']}")
+
+ # Validate required fields
+ if not isinstance(outline_data, dict) or 'outline' not in outline_data or not isinstance(outline_data['outline'], list):
+ logger.error(f"Invalid outline structure: {outline_data}")
+ raise ValueError("Invalid outline structure in Gemini response")
+
+ # Convert to BlogOutlineSection objects
+ outline_sections = []
+ for i, section_data in enumerate(outline_data.get('outline', [])):
+ if not isinstance(section_data, dict) or 'heading' not in section_data:
+ logger.warning(f"Skipping invalid section data at index {i}")
+ continue
+
+ section = BlogOutlineSection(
+ id=f"s{i+1}",
+ heading=section_data.get('heading', f'Section {i+1}'),
+ subheadings=section_data.get('subheadings', []),
+ key_points=section_data.get('key_points', []),
+ references=sources[:2] if i < 2 else [], # Assign sources to first 2 sections
+ target_words=section_data.get('target_words', 300),
+ keywords=section_data.get('keywords', [])
+ )
+ outline_sections.append(section)
+
+ title_options = outline_data.get('title_options', [])
+ if not title_options:
+ raise ValueError("No title options provided in Gemini response")
+
+ logger.info(f"Generated outline with {len(outline_sections)} sections and {len(title_options)} title options")
+
+ return BlogOutlineResponse(
+ success=True,
+ title_options=title_options,
+ outline=outline_sections
+ )
+
+
+ async def refine_outline(self, request: BlogOutlineRefineRequest) -> BlogOutlineResponse:
+ """
+ Refine outline with HITL (Human-in-the-Loop) operations
+ Supports add, remove, move, merge, rename operations
+ """
+ outline = request.outline.copy()
+ operation = request.operation.lower()
+ section_id = request.section_id
+ payload = request.payload or {}
+
+ try:
+ if operation == 'add':
+ # Add new section
+ new_section = BlogOutlineSection(
+ id=f"s{len(outline) + 1}",
+ heading=payload.get('heading', 'New Section'),
+ subheadings=payload.get('subheadings', []),
+ key_points=payload.get('key_points', []),
+ references=[],
+ target_words=payload.get('target_words', 300)
+ )
+ outline.append(new_section)
+ logger.info(f"Added new section: {new_section.heading}")
+
+ elif operation == 'remove' and section_id:
+ # Remove section
+ outline = [s for s in outline if s.id != section_id]
+ logger.info(f"Removed section: {section_id}")
+
+ elif operation == 'rename' and section_id:
+ # Rename section
+ for section in outline:
+ if section.id == section_id:
+ section.heading = payload.get('heading', section.heading)
+ break
+ logger.info(f"Renamed section {section_id} to: {payload.get('heading')}")
+
+ elif operation == 'move' and section_id:
+ # Move section (reorder)
+ direction = payload.get('direction', 'down') # 'up' or 'down'
+ current_index = next((i for i, s in enumerate(outline) if s.id == section_id), -1)
+
+ if current_index != -1:
+ if direction == 'up' and current_index > 0:
+ outline[current_index], outline[current_index - 1] = outline[current_index - 1], outline[current_index]
+ elif direction == 'down' and current_index < len(outline) - 1:
+ outline[current_index], outline[current_index + 1] = outline[current_index + 1], outline[current_index]
+ logger.info(f"Moved section {section_id} {direction}")
+
+ elif operation == 'merge' and section_id:
+ # Merge with next section
+ current_index = next((i for i, s in enumerate(outline) if s.id == section_id), -1)
+ if current_index != -1 and current_index < len(outline) - 1:
+ current_section = outline[current_index]
+ next_section = outline[current_index + 1]
+
+ # Merge sections
+ current_section.heading = f"{current_section.heading} & {next_section.heading}"
+ current_section.subheadings.extend(next_section.subheadings)
+ current_section.key_points.extend(next_section.key_points)
+ current_section.references.extend(next_section.references)
+ current_section.target_words = (current_section.target_words or 0) + (next_section.target_words or 0)
+
+ # Remove the next section
+ outline.pop(current_index + 1)
+ logger.info(f"Merged section {section_id} with next section")
+
+ elif operation == 'update' and section_id:
+ # Update section details
+ for section in outline:
+ if section.id == section_id:
+ if 'heading' in payload:
+ section.heading = payload['heading']
+ if 'subheadings' in payload:
+ section.subheadings = payload['subheadings']
+ if 'key_points' in payload:
+ section.key_points = payload['key_points']
+ if 'target_words' in payload:
+ section.target_words = payload['target_words']
+ break
+ logger.info(f"Updated section {section_id}")
+
+ # Reassign IDs to maintain order
+ for i, section in enumerate(outline):
+ section.id = f"s{i+1}"
+
+ return BlogOutlineResponse(
+ success=True,
+ title_options=["Refined Outline"],
+ outline=outline
+ )
+
+ except Exception as e:
+ logger.error(f"Outline refinement failed: {e}")
+ return BlogOutlineResponse(
+ success=False,
+ title_options=["Error"],
+ outline=request.outline
+ )
+
+ async def generate_section(self, request: BlogSectionRequest) -> BlogSectionResponse:
+ # TODO: Generate section markdown incorporating references and persona/tone
+ md = f"## {request.section.heading}\n\nThis section content will be generated here.\n"
+ return BlogSectionResponse(success=True, markdown=md, citations=request.section.references)
+
+ async def optimize_section(self, request: BlogOptimizeRequest) -> BlogOptimizeResponse:
+ # TODO: Run readability/EEAT optimization and return diff
+ return BlogOptimizeResponse(success=True, optimized=request.content, diff_preview=None)
+
+ async def hallucination_check(self, payload: Dict[str, Any]) -> Dict[str, Any]:
+ """Run hallucination detection on provided text using existing detector service."""
+ text = str(payload.get("text", "") or "").strip()
+ if not text:
+ return {"success": False, "error": "No text provided"}
+
+ # Prefer direct service use over HTTP proxy
+ try:
+ from services.hallucination_detector import HallucinationDetector
+ detector = HallucinationDetector()
+ result = await detector.detect_hallucinations(text)
+
+ # Serialize dataclass-like result to dict
+ claims = []
+ for c in result.claims:
+ claims.append({
+ "text": c.text,
+ "confidence": c.confidence,
+ "assessment": c.assessment,
+ "supporting_sources": c.supporting_sources,
+ "refuting_sources": c.refuting_sources,
+ "reasoning": c.reasoning,
+ })
+
+ return {
+ "success": True,
+ "overall_confidence": result.overall_confidence,
+ "total_claims": result.total_claims,
+ "supported_claims": result.supported_claims,
+ "refuted_claims": result.refuted_claims,
+ "insufficient_claims": result.insufficient_claims,
+ "timestamp": result.timestamp,
+ "claims": claims,
+ }
+ except Exception as e:
+ return {"success": False, "error": str(e)}
+
+ async def seo_analyze(self, request: BlogSEOAnalyzeRequest) -> BlogSEOAnalyzeResponse:
+ """Wrap existing SEO tools to produce unified analysis for blog content."""
+ from services.seo_tools.on_page_seo_service import OnPageSEOService
+ from services.seo_tools.image_alt_service import ImageAltService
+ from services.seo_tools.content_strategy_service import ContentStrategyService
+
+ content = request.content or ""
+ target_keywords = request.keywords or []
+
+ # On-page analysis (treat content as a virtual URL/document for now)
+ on_page = OnPageSEOService()
+ on_page_result = await on_page.analyze_on_page_seo(url="about:blank", target_keywords=target_keywords)
+
+ # Image alt coverage (placeholder: no images in raw content yet)
+ try:
+ image_alt_service = ImageAltService()
+ image_alt_status = {"total_images": 0, "missing_alt": 0}
+ except Exception:
+ image_alt_status = {"total_images": 0, "missing_alt": 0}
+
+ # Strategy hints (keywords/topics)
+ try:
+ strategy = ContentStrategyService()
+ strategy_hints = await strategy.analyze_content_topics(content=content)
+ except Exception:
+ strategy_hints = {"topics": [], "gaps": []}
+
+ # Lightweight markdown parsing for headings/links/keywords
+ import re
+ content_text = content or ""
+ words = re.findall(r"[A-Za-z0-9']+", content_text)
+ total_words = max(len(words), 1)
+ heading_lines = content_text.splitlines()
+ h1 = sum(1 for ln in heading_lines if ln.startswith('# '))
+ h2 = sum(1 for ln in heading_lines if ln.startswith('## '))
+ h3 = sum(1 for ln in heading_lines if ln.startswith('### '))
+ md_links = re.findall(r"\[([^\]]+)\]\(([^)]+)\)", content_text)
+ external_links = [u for (_t, u) in md_links if u.startswith('http')]
+
+ # Keyword density
+ density_map: Dict[str, Any] = {"target_keywords": target_keywords}
+ for kw in target_keywords:
+ try:
+ occurrences = len(re.findall(re.escape(kw), content_text, flags=re.IGNORECASE))
+ except re.error:
+ occurrences = 0
+ density_map[kw] = {
+ "occurrences": occurrences,
+ "density": round(occurrences / total_words, 4)
+ }
+
+ # Build unified response
+ recommendations: List[str] = []
+ if isinstance(on_page_result.get("recommendations"), list):
+ recommendations.extend(on_page_result["recommendations"])
+ if strategy_hints.get("gaps"):
+ recommendations.append("Cover missing topics: " + ", ".join(strategy_hints["gaps"]))
+ if not external_links:
+ recommendations.append("Add at least one credible external link to authoritative sources.")
+ if h2 < 2:
+ recommendations.append("Increase number of H2 sections for better structure.")
+
+ # Internal link suggestions: generate anchors for H2s and propose cross-links
+ def to_anchor(h: str) -> str:
+ import re
+ a = re.sub(r"[^a-z0-9\s-]", "", h.lower())
+ a = re.sub(r"\s+", "-", a).strip('-')
+ return a
+ h2_headings = [ln[3:].strip() for ln in heading_lines if ln.startswith('## ')]
+ anchors = [to_anchor(h) for h in h2_headings]
+ internal_link_suggestions = []
+ for i in range(len(anchors)-1):
+ internal_link_suggestions.append({
+ "from": h2_headings[i],
+ "to": h2_headings[i+1],
+ "anchor": f"#{anchors[i+1]}",
+ "suggestion": f"Add internal link from '{h2_headings[i]}' to '{h2_headings[i+1]}'"
+ })
+
+ return BlogSEOAnalyzeResponse(
+ success=True,
+ seo_score=float(on_page_result.get("overall_score", 75)),
+ density=density_map,
+ structure={
+ **on_page_result.get("heading_structure", {}),
+ "markdown_headings": {"h1": h1, "h2": h2, "h3": h3},
+ "links": {"total": len(md_links), "external": len(external_links)}
+ },
+ readability=on_page_result.get("content_analysis", {}),
+ link_suggestions=([{"suggestion": "Add external citation links for key claims."}] if not external_links else []) + internal_link_suggestions,
+ image_alt_status=image_alt_status,
+ recommendations=recommendations,
+ )
+
+ async def seo_metadata(self, request: BlogSEOMetadataRequest) -> BlogSEOMetadataResponse:
+ # TODO: Generate SEO metadata using existing services
+ return BlogSEOMetadataResponse(
+ success=True,
+ title_options=[request.title or "Generated SEO Title"],
+ meta_descriptions=["Compelling meta description..."],
+ open_graph={"title": request.title or "OG Title", "image": ""},
+ twitter_card={"card": "summary_large_image"},
+ schema={"@type": "Article"},
+ )
+
+ async def publish(self, request: BlogPublishRequest) -> BlogPublishResponse:
+ # TODO: Call Wix/WordPress adapters to publish
+ return BlogPublishResponse(success=True, platform=request.platform, url="https://example.com/post")
+
+
diff --git a/backend/services/llm_providers/__pycache__/gemini_provider.cpython-313.pyc b/backend/services/llm_providers/__pycache__/gemini_provider.cpython-313.pyc
index 3974b088..1433b72e 100644
Binary files a/backend/services/llm_providers/__pycache__/gemini_provider.cpython-313.pyc and b/backend/services/llm_providers/__pycache__/gemini_provider.cpython-313.pyc differ
diff --git a/backend/services/llm_providers/gemini_grounded_provider.py b/backend/services/llm_providers/gemini_grounded_provider.py
index 2e627201..f0266de1 100644
--- a/backend/services/llm_providers/gemini_grounded_provider.py
+++ b/backend/services/llm_providers/gemini_grounded_provider.py
@@ -43,7 +43,7 @@ class GeminiGroundedProvider:
# Initialize the Gemini client with timeout configuration
self.client = genai.Client(api_key=self.api_key)
- self.timeout = 30 # 30 second timeout for API calls
+ self.timeout = 60 # 60 second timeout for API calls (increased for research)
logger.info("โ
Gemini Grounded Provider initialized with native Google Search grounding")
async def generate_grounded_content(
@@ -239,8 +239,8 @@ class GeminiGroundedProvider:
logger.info(f"Search queries: {grounding_metadata.web_search_queries}")
# Extract sources from grounding chunks
+ sources = [] # Initialize sources list
if hasattr(grounding_metadata, 'grounding_chunks') and grounding_metadata.grounding_chunks:
- sources = []
for i, chunk in enumerate(grounding_metadata.grounding_chunks):
logger.info(f"Chunk {i} attributes: {dir(chunk)}")
if hasattr(chunk, 'web'):
@@ -251,15 +251,29 @@ class GeminiGroundedProvider:
'type': 'web'
}
sources.append(source)
- result['sources'] = sources
- logger.info(f"Extracted {len(sources)} sources")
+ logger.info(f"Extracted {len(sources)} sources from grounding chunks")
else:
- logger.error("โ CRITICAL: No grounding chunks found in response")
- logger.error(f"Grounding metadata structure: {dir(grounding_metadata)}")
- if hasattr(grounding_metadata, 'grounding_chunks'):
- logger.error(f"Grounding chunks type: {type(grounding_metadata.grounding_chunks)}")
- logger.error(f"Grounding chunks value: {grounding_metadata.grounding_chunks}")
- raise ValueError("No grounding chunks found - grounding is not working properly")
+ logger.warning("โ ๏ธ No grounding chunks found - this is normal for some queries")
+ logger.info(f"Grounding metadata available fields: {[attr for attr in dir(grounding_metadata) if not attr.startswith('_')]}")
+
+ # Check if we have search queries - this means Google Search was triggered
+ if hasattr(grounding_metadata, 'web_search_queries') and grounding_metadata.web_search_queries:
+ logger.info(f"โ
Google Search was triggered with {len(grounding_metadata.web_search_queries)} queries")
+ # Create sources based on search queries
+ for i, query in enumerate(grounding_metadata.web_search_queries[:5]): # Limit to 5 sources
+ source = {
+ 'index': i,
+ 'title': f"Search: {query}",
+ 'url': f"https://www.google.com/search?q={query.replace(' ', '+')}",
+ 'type': 'search_query',
+ 'query': query
+ }
+ sources.append(source)
+ logger.info(f"Created {len(sources)} sources from search queries")
+ else:
+ logger.warning("โ ๏ธ No search queries found either - grounding may not have been triggered")
+
+ result['sources'] = sources
# Extract citations from grounding supports
if hasattr(grounding_metadata, 'grounding_supports') and grounding_metadata.grounding_supports:
@@ -278,12 +292,37 @@ class GeminiGroundedProvider:
result['citations'] = citations
logger.info(f"Extracted {len(citations)} citations")
else:
- logger.error("โ CRITICAL: No grounding supports found in response")
- logger.error(f"Grounding metadata structure: {dir(grounding_metadata)}")
- if hasattr(grounding_metadata, 'grounding_supports'):
- logger.error(f"Grounding supports type: {type(grounding_metadata.grounding_supports)}")
- logger.error(f"Grounding supports value: {grounding_metadata.grounding_supports}")
- raise ValueError("No grounding supports found - grounding is not working properly")
+ logger.warning("โ ๏ธ No grounding supports found - this is normal when no web sources are retrieved")
+ # Create basic citations from the content if we have sources
+ if sources:
+ citations = []
+ for i, source in enumerate(sources[:3]): # Limit to 3 citations
+ citation = {
+ 'type': 'reference',
+ 'start_index': 0,
+ 'end_index': 0,
+ 'text': f"Source {i+1}",
+ 'source_indices': [i],
+ 'reference': f"Source {i+1}",
+ 'source': source
+ }
+ citations.append(citation)
+ result['citations'] = citations
+ logger.info(f"Created {len(citations)} basic citations from sources")
+ else:
+ result['citations'] = []
+ logger.info("No citations created - no sources available")
+
+ # Extract search entry point for UI display
+ if hasattr(grounding_metadata, 'search_entry_point') and grounding_metadata.search_entry_point:
+ if hasattr(grounding_metadata.search_entry_point, 'rendered_content'):
+ result['search_widget'] = grounding_metadata.search_entry_point.rendered_content
+ logger.info("โ
Extracted search widget HTML for UI display")
+
+ # Extract search queries for reference
+ if hasattr(grounding_metadata, 'web_search_queries') and grounding_metadata.web_search_queries:
+ result['search_queries'] = grounding_metadata.web_search_queries
+ logger.info(f"โ
Extracted {len(grounding_metadata.web_search_queries)} search queries")
logger.info(f"โ
Successfully extracted {len(result['sources'])} sources and {len(result['citations'])} citations from grounding metadata")
logger.info(f"Sources: {result['sources']}")
diff --git a/backend/services/llm_providers/gemini_provider.py b/backend/services/llm_providers/gemini_provider.py
index 4a1e7a14..0480e68e 100644
--- a/backend/services/llm_providers/gemini_provider.py
+++ b/backend/services/llm_providers/gemini_provider.py
@@ -389,43 +389,13 @@ def gemini_structured_json_response(prompt, schema, temperature=0.7, top_p=0.9,
config=generation_config,
)
- # Add debugging for response
- logger.info("Gemini response | type=%s | has_text=%s | has_parsed=%s",
- type(response), hasattr(response, 'text'), hasattr(response, 'parsed'))
-
- if hasattr(response, 'text'):
- logger.info(f"Gemini response.text: {repr(response.text)}")
- if hasattr(response, 'parsed'):
- logger.info(f"Gemini response.parsed: {repr(response.parsed)}")
-
# According to the documentation, we should use response.parsed for structured output
if hasattr(response, 'parsed') and response.parsed is not None:
logger.info("Using response.parsed for structured output")
return response.parsed
- # Fallback to text if parsed is not available
- if hasattr(response, 'text') and response.text:
- logger.info("Falling back to response.text parsing")
- text = response.text.strip()
-
- # Strip markdown code fences if present
- if text.startswith('```'):
- if text.lower().startswith('```json'):
- text = text[7:]
- else:
- text = text[3:]
- if text.endswith('```'):
- text = text[:-3]
- text = text.strip()
-
- try:
- return json.loads(text)
- except json.JSONDecodeError as e:
- logger.error(f"Failed to parse response.text as JSON: {e}")
- return {"error": f"Failed to parse JSON response: {e}", "raw_response": text[:500]}
-
- logger.error("No valid response content found")
- return {"error": "No valid response content found", "raw_response": ""}
+ logger.error("No valid structured response content found")
+ return {"error": "No valid structured response content found"}
except ValueError as e:
# API key related errors
diff --git a/backend/services/research/google_search_service.py b/backend/services/research/google_search_service.py
index 1112c3d6..50fcea15 100644
--- a/backend/services/research/google_search_service.py
+++ b/backend/services/research/google_search_service.py
@@ -45,8 +45,7 @@ class GoogleSearchService:
self.base_url = "https://www.googleapis.com/customsearch/v1"
if not self.api_key or not self.search_engine_id:
- logger.warning("Google Search API credentials not configured. Service will use fallback methods.")
- self.enabled = False
+ raise ValueError("Google Search API credentials not configured. Please set GOOGLE_SEARCH_API_KEY and GOOGLE_SEARCH_ENGINE_ID environment variables.")
else:
self.enabled = True
logger.info("Google Search Service initialized successfully")
@@ -69,8 +68,7 @@ class GoogleSearchService:
List of search results with credibility scoring
"""
if not self.enabled:
- logger.warning("Google Search Service not enabled, using fallback research")
- return await self._fallback_research(topic, industry)
+ raise RuntimeError("Google Search Service is not enabled. Please configure API credentials.")
try:
# Construct industry-specific search query
@@ -99,7 +97,7 @@ class GoogleSearchService:
except Exception as e:
logger.error(f"Google search failed: {str(e)}")
- return await self._fallback_research(topic, industry)
+ raise RuntimeError(f"Google search failed: {str(e)}")
def _build_search_query(self, topic: str, industry: str) -> str:
"""
@@ -465,45 +463,6 @@ class GoogleSearchService:
"statistics": statistics
}
- async def _fallback_research(self, topic: str, industry: str) -> Dict[str, Any]:
- """
- Fallback research method when Google Search is not available.
-
- Args:
- topic: The research topic
- industry: The industry context
-
- Returns:
- Fallback research data
- """
- logger.info(f"Using fallback research for {topic} in {industry}")
-
- return {
- "sources": [
- {
- "title": f"Industry insights on {topic} in {industry}",
- "url": f"https://example.com/{topic.lower().replace(' ', '-')}",
- "content": f"Professional insights and trends related to {topic} in the {industry} sector...",
- "relevance_score": 0.8,
- "credibility_score": 0.6,
- "domain_authority": 0.5,
- "source_type": "general",
- "grounding_enabled": False
- }
- ],
- "key_insights": [
- f"{topic} is transforming {industry} operations",
- f"Industry leaders are investing in {topic}",
- f"Expected growth in {topic} adoption within {industry}"
- ],
- "statistics": [
- f"85% of {industry} companies are exploring {topic}",
- f"Investment in {topic} increased by 40% this year"
- ],
- "grounding_enabled": False,
- "search_query": f"{topic} {industry} trends",
- "timestamp": datetime.utcnow().isoformat()
- }
async def test_api_connection(self) -> Dict[str, Any]:
"""
@@ -513,11 +472,7 @@ class GoogleSearchService:
Test results and status information
"""
if not self.enabled:
- return {
- "status": "disabled",
- "message": "Google Search API credentials not configured",
- "enabled": False
- }
+ raise RuntimeError("Google Search Service is not enabled. Please configure API credentials.")
try:
# Perform a simple test search
diff --git a/backend/start_alwrity_backend.py b/backend/start_alwrity_backend.py
index 1156e5e6..5c344a4b 100644
--- a/backend/start_alwrity_backend.py
+++ b/backend/start_alwrity_backend.py
@@ -91,6 +91,37 @@ def setup_monitoring_tables():
print(" Monitoring will be disabled. Continuing startup...")
return True # Don't fail startup for monitoring issues
+def setup_billing_tables():
+ """Set up billing and subscription database tables."""
+ print("๐ณ Setting up billing and subscription tables...")
+
+ try:
+ # Import and run the billing table creation
+ sys.path.append(str(Path(__file__).parent))
+ from scripts.create_billing_tables import create_billing_tables, check_existing_tables
+ from services.database import DATABASE_URL
+ from sqlalchemy import create_engine
+
+ # Create engine to check existing tables
+ engine = create_engine(DATABASE_URL, echo=False)
+
+ # Check existing tables
+ if not check_existing_tables(engine):
+ print("โ
Billing tables already exist, skipping creation")
+ return True
+
+ if create_billing_tables():
+ print("โ
Billing and subscription tables created successfully!")
+ return True
+ else:
+ print("โ ๏ธ Warning: Failed to create billing tables, continuing anyway...")
+ return True # Don't fail startup for billing issues
+
+ except Exception as e:
+ print(f"โ ๏ธ Warning: Could not set up billing tables: {e}")
+ print(" Billing system will be disabled. Continuing startup...")
+ return True # Don't fail startup for billing issues
+
def setup_monitoring_middleware():
"""Set up monitoring middleware in app.py if not already present."""
print("๐ Setting up API monitoring middleware...")
@@ -168,7 +199,8 @@ def check_dependencies():
'openai',
'google.generativeai',
'anthropic',
- 'mistralai'
+ 'mistralai',
+ 'sqlalchemy'
]
missing_packages = []
@@ -212,6 +244,9 @@ def setup_environment():
setup_monitoring_tables()
setup_monitoring_middleware()
+ # Set up billing and subscription system
+ setup_billing_tables()
+
print("โ
Environment setup complete")
def verify_persona_tables():
@@ -238,6 +273,35 @@ def verify_persona_tables():
print(f"โ ๏ธ Warning: Could not verify persona tables: {e}")
return False
+def verify_billing_tables():
+ """Verify that billing and subscription tables exist and are accessible."""
+ print("๐ Verifying billing and subscription tables...")
+ try:
+ from services.database import get_db_session
+ from models.subscription_models import (
+ SubscriptionPlan, UserSubscription, APIUsageLog,
+ UsageSummary, APIProviderPricing, UsageAlert
+ )
+
+ session = get_db_session()
+ if session:
+ # Try to query all billing tables to verify they exist
+ session.query(SubscriptionPlan).first()
+ session.query(UserSubscription).first()
+ session.query(APIUsageLog).first()
+ session.query(UsageSummary).first()
+ session.query(APIProviderPricing).first()
+ session.query(UsageAlert).first()
+ session.close()
+ print("โ
All billing and subscription tables verified successfully")
+ return True
+ else:
+ print("โ ๏ธ Warning: Could not get database session")
+ return False
+ except Exception as e:
+ print(f"โ ๏ธ Warning: Could not verify billing tables: {e}")
+ return False
+
def start_backend(enable_reload=False):
"""Start the backend server."""
print("๐ Starting ALwrity Backend...")
@@ -276,11 +340,16 @@ def start_backend(enable_reload=False):
# Verify persona tables exist
verify_persona_tables()
+ # Verify billing tables exist
+ verify_billing_tables()
+
print("\n๐ Backend is starting...")
print(" ๐ API Documentation: http://localhost:8000/api/docs")
print(" ๐ Health Check: http://localhost:8000/health")
print(" ๐ ReDoc: http://localhost:8000/api/redoc")
print(" ๐ API Monitoring: http://localhost:8000/api/content-planning/monitoring/health")
+ print(" ๐ณ Billing Dashboard: http://localhost:8000/api/subscription/plans")
+ print(" ๐ Usage Tracking: http://localhost:8000/api/subscription/usage/demo")
print("\nโน๏ธ Press Ctrl+C to stop the server")
print("=" * 60)
print("\n๐ก Usage:")
diff --git a/backend/test_detailed.py b/backend/test_detailed.py
new file mode 100644
index 00000000..9426ff62
--- /dev/null
+++ b/backend/test_detailed.py
@@ -0,0 +1,43 @@
+import requests
+import json
+
+# Test the research endpoint with more detailed output
+url = "http://localhost:8000/api/blog/research"
+payload = {
+ "keywords": ["AI content generation", "blog writing"],
+ "topic": "ALwrity content generation",
+ "industry": "Technology",
+ "target_audience": "content creators"
+}
+
+try:
+ print("Sending request to research endpoint...")
+ response = requests.post(url, json=payload, timeout=60)
+ print(f"Status Code: {response.status_code}")
+
+ if response.status_code == 200:
+ data = response.json()
+ print("\n=== FULL RESPONSE ===")
+ print(json.dumps(data, indent=2))
+
+ # Check if we got the expected fields
+ expected_fields = ['success', 'sources', 'keyword_analysis', 'competitor_analysis', 'suggested_angles', 'search_widget', 'search_queries']
+ print(f"\n=== FIELD ANALYSIS ===")
+ for field in expected_fields:
+ value = data.get(field)
+ if field == 'sources':
+ print(f"{field}: {len(value) if value else 0} items")
+ elif field == 'search_queries':
+ print(f"{field}: {len(value) if value else 0} items")
+ elif field == 'search_widget':
+ print(f"{field}: {'Present' if value else 'Missing'}")
+ else:
+ print(f"{field}: {type(value).__name__} - {str(value)[:100]}...")
+
+ else:
+ print(f"Error Response: {response.text}")
+
+except Exception as e:
+ print(f"Request failed: {e}")
+ import traceback
+ traceback.print_exc()
diff --git a/backend/test_gemini_direct.py b/backend/test_gemini_direct.py
new file mode 100644
index 00000000..7084f5e4
--- /dev/null
+++ b/backend/test_gemini_direct.py
@@ -0,0 +1,60 @@
+import asyncio
+from services.llm_providers.gemini_grounded_provider import GeminiGroundedProvider
+
+async def test_gemini_direct():
+ gemini = GeminiGroundedProvider()
+
+ prompt = """
+ Research the topic "AI content generation" in the Technology industry for content creators audience. Provide a 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
+
+ Focus on factual, up-to-date information from credible sources.
+ Include specific data points, percentages, and recent developments.
+ Structure your response with clear sections for each analysis area.
+ """
+
+ try:
+ result = await gemini.generate_grounded_content(
+ prompt=prompt,
+ content_type="research",
+ max_tokens=2000
+ )
+
+ print("=== GEMINI RESULT ===")
+ print(f"Type: {type(result)}")
+ print(f"Keys: {list(result.keys()) if isinstance(result, dict) else 'Not a dict'}")
+
+ if isinstance(result, dict):
+ print(f"Sources count: {len(result.get('sources', []))}")
+ print(f"Search queries count: {len(result.get('search_queries', []))}")
+ print(f"Has search widget: {bool(result.get('search_widget'))}")
+ print(f"Content length: {len(result.get('content', ''))}")
+
+ print("\n=== FIRST SOURCE ===")
+ sources = result.get('sources', [])
+ if sources:
+ print(f"Source: {sources[0]}")
+
+ print("\n=== SEARCH QUERIES (First 3) ===")
+ queries = result.get('search_queries', [])
+ for i, query in enumerate(queries[:3]):
+ print(f"{i+1}. {query}")
+ else:
+ print(f"Result is not a dict: {result}")
+
+ except Exception as e:
+ print(f"Error: {e}")
+ import traceback
+ traceback.print_exc()
+
+if __name__ == "__main__":
+ asyncio.run(test_gemini_direct())
diff --git a/backend/test_research.py b/backend/test_research.py
new file mode 100644
index 00000000..e2f18301
--- /dev/null
+++ b/backend/test_research.py
@@ -0,0 +1,58 @@
+import requests
+import json
+
+# Test the research endpoint
+url = "http://localhost:8000/api/blog/research"
+payload = {
+ "keywords": ["AI content generation", "blog writing"],
+ "topic": "ALwrity content generation",
+ "industry": "Technology",
+ "target_audience": "content creators"
+}
+
+try:
+ response = requests.post(url, json=payload)
+ print(f"Status Code: {response.status_code}")
+ print(f"Response Headers: {dict(response.headers)}")
+
+ if response.status_code == 200:
+ data = response.json()
+ print("\n=== RESEARCH RESPONSE ===")
+ print(f"Success: {data.get('success')}")
+ print(f"Sources Count: {len(data.get('sources', []))}")
+ print(f"Search Queries Count: {len(data.get('search_queries', []))}")
+ print(f"Has Search Widget: {bool(data.get('search_widget'))}")
+ print(f"Suggested Angles Count: {len(data.get('suggested_angles', []))}")
+
+ print("\n=== SOURCES ===")
+ for i, source in enumerate(data.get('sources', [])[:3]):
+ print(f"Source {i+1}: {source.get('title', 'No title')}")
+ print(f" URL: {source.get('url', 'No URL')}")
+ print(f" Type: {source.get('type', 'Unknown')}")
+
+ print("\n=== SEARCH QUERIES (First 5) ===")
+ for i, query in enumerate(data.get('search_queries', [])[:5]):
+ print(f"{i+1}. {query}")
+
+ print("\n=== SUGGESTED ANGLES ===")
+ for i, angle in enumerate(data.get('suggested_angles', [])[:3]):
+ print(f"{i+1}. {angle}")
+
+ print("\n=== KEYWORD ANALYSIS ===")
+ kw_analysis = data.get('keyword_analysis', {})
+ print(f"Primary: {kw_analysis.get('primary', [])}")
+ print(f"Secondary: {kw_analysis.get('secondary', [])}")
+ print(f"Search Intent: {kw_analysis.get('search_intent', 'Unknown')}")
+
+ print("\n=== SEARCH WIDGET (First 200 chars) ===")
+ widget = data.get('search_widget', '')
+ if widget:
+ print(widget[:200] + "..." if len(widget) > 200 else widget)
+ else:
+ print("No search widget provided")
+
+ else:
+ print(f"Error: {response.text}")
+
+except Exception as e:
+ print(f"Request failed: {e}")
diff --git a/backend/test_research_analysis.py b/backend/test_research_analysis.py
new file mode 100644
index 00000000..df76c75c
--- /dev/null
+++ b/backend/test_research_analysis.py
@@ -0,0 +1,115 @@
+import requests
+import json
+from datetime import datetime
+
+# Test the research endpoint and capture full response
+url = "http://localhost:8000/api/blog/research"
+payload = {
+ "keywords": ["AI content generation", "blog writing"],
+ "topic": "ALwrity content generation",
+ "industry": "Technology",
+ "target_audience": "content creators"
+}
+
+try:
+ print("Sending request to research endpoint...")
+ response = requests.post(url, json=payload, timeout=120)
+ print(f"Status Code: {response.status_code}")
+
+ if response.status_code == 200:
+ data = response.json()
+
+ # Create analysis file with timestamp
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
+ filename = f"research_analysis_{timestamp}.json"
+
+ # Save full response to file
+ with open(filename, 'w', encoding='utf-8') as f:
+ json.dump(data, f, indent=2, ensure_ascii=False)
+
+ print(f"\n=== RESEARCH RESPONSE ANALYSIS ===")
+ print(f"โ
Full response saved to: {filename}")
+ print(f"Success: {data.get('success')}")
+ print(f"Sources Count: {len(data.get('sources', []))}")
+ print(f"Search Queries Count: {len(data.get('search_queries', []))}")
+ print(f"Has Search Widget: {bool(data.get('search_widget'))}")
+ print(f"Suggested Angles Count: {len(data.get('suggested_angles', []))}")
+
+ print(f"\n=== SOURCES ANALYSIS ===")
+ sources = data.get('sources', [])
+ for i, source in enumerate(sources[:5]): # Show first 5
+ print(f"Source {i+1}: {source.get('title', 'No title')}")
+ print(f" URL: {source.get('url', 'No URL')[:100]}...")
+ print(f" Type: {source.get('type', 'Unknown')}")
+ print(f" Credibility: {source.get('credibility_score', 'N/A')}")
+
+ print(f"\n=== SEARCH QUERIES ANALYSIS ===")
+ queries = data.get('search_queries', [])
+ print(f"Total queries: {len(queries)}")
+ for i, query in enumerate(queries[:10]): # Show first 10
+ print(f"{i+1:2d}. {query}")
+
+ print(f"\n=== SEARCH WIDGET ANALYSIS ===")
+ widget = data.get('search_widget', '')
+ if widget:
+ print(f"Widget HTML length: {len(widget)} characters")
+ print(f"Contains Google branding: {'Google' in widget}")
+ print(f"Contains search chips: {'chip' in widget}")
+ print(f"Contains carousel: {'carousel' in widget}")
+ print(f"First 200 chars: {widget[:200]}...")
+ else:
+ print("No search widget provided")
+
+ print(f"\n=== KEYWORD ANALYSIS ===")
+ kw_analysis = data.get('keyword_analysis', {})
+ print(f"Primary keywords: {kw_analysis.get('primary', [])}")
+ print(f"Secondary keywords: {kw_analysis.get('secondary', [])}")
+ print(f"Long-tail keywords: {kw_analysis.get('long_tail', [])}")
+ print(f"Search intent: {kw_analysis.get('search_intent', 'Unknown')}")
+ print(f"Difficulty score: {kw_analysis.get('difficulty', 'N/A')}")
+
+ print(f"\n=== SUGGESTED ANGLES ===")
+ angles = data.get('suggested_angles', [])
+ for i, angle in enumerate(angles):
+ print(f"{i+1}. {angle}")
+
+ print(f"\n=== UI REPRESENTATION RECOMMENDATIONS ===")
+ print("Based on the response, here's what should be displayed in the Editor UI:")
+ print(f"1. Research Sources Panel: {len(sources)} real web sources")
+ print(f"2. Search Widget: Interactive Google search chips ({len(queries)} queries)")
+ print(f"3. Keyword Analysis: Primary/Secondary/Long-tail breakdown")
+ print(f"4. Content Angles: {len(angles)} suggested blog post angles")
+ print(f"5. Search Queries: {len(queries)} research queries for reference")
+
+ # Additional analysis for UI components
+ print(f"\n=== UI COMPONENT BREAKDOWN ===")
+
+ # Sources for UI
+ print("SOURCES FOR UI:")
+ for i, source in enumerate(sources[:3]):
+ print(f" - {source.get('title')} (Credibility: {source.get('credibility_score')})")
+
+ # Search widget for UI
+ print(f"\nSEARCH WIDGET FOR UI:")
+ print(f" - HTML length: {len(widget)} chars")
+ print(f" - Can be embedded directly in UI")
+ print(f" - Contains {len(queries)} search suggestions")
+
+ # Keywords for UI
+ print(f"\nKEYWORDS FOR UI:")
+ print(f" - Primary: {', '.join(kw_analysis.get('primary', []))}")
+ print(f" - Secondary: {', '.join(kw_analysis.get('secondary', []))}")
+ print(f" - Long-tail: {', '.join(kw_analysis.get('long_tail', []))}")
+
+ # Angles for UI
+ print(f"\nCONTENT ANGLES FOR UI:")
+ for i, angle in enumerate(angles[:3]):
+ print(f" - {angle}")
+
+ else:
+ print(f"Error: {response.text}")
+
+except Exception as e:
+ print(f"Request failed: {e}")
+ import traceback
+ traceback.print_exc()
diff --git a/backend/verify_billing_setup.py b/backend/verify_billing_setup.py
new file mode 100644
index 00000000..d9647b42
--- /dev/null
+++ b/backend/verify_billing_setup.py
@@ -0,0 +1,280 @@
+"""
+Comprehensive verification script for billing and subscription system setup.
+Checks that all files are created, tables exist, and the system is properly integrated.
+"""
+
+import os
+import sys
+from pathlib import Path
+
+def check_file_exists(file_path, description):
+ """Check if a file exists and report status."""
+ if os.path.exists(file_path):
+ print(f"โ
{description}: {file_path}")
+ return True
+ else:
+ print(f"โ {description}: {file_path} - NOT FOUND")
+ return False
+
+def check_file_content(file_path, search_terms, description):
+ """Check if file contains expected content."""
+ try:
+ with open(file_path, 'r', encoding='utf-8') as f:
+ content = f.read()
+
+ missing_terms = []
+ for term in search_terms:
+ if term not in content:
+ missing_terms.append(term)
+
+ if not missing_terms:
+ print(f"โ
{description}: All expected content found")
+ return True
+ else:
+ print(f"โ {description}: Missing content - {missing_terms}")
+ return False
+ except Exception as e:
+ print(f"โ {description}: Error reading file - {e}")
+ return False
+
+def check_database_tables():
+ """Check if billing database tables exist."""
+ print("\n๐๏ธ Checking Database Tables:")
+ print("-" * 30)
+
+ try:
+ # Add backend to path
+ backend_dir = Path(__file__).parent
+ sys.path.insert(0, str(backend_dir))
+
+ from services.database import get_db_session, DATABASE_URL
+ from sqlalchemy import text
+
+ session = get_db_session()
+ if not session:
+ print("โ Could not get database session")
+ return False
+
+ # Check for billing tables
+ tables_query = text("""
+ SELECT name FROM sqlite_master
+ WHERE type='table' AND (
+ name LIKE '%subscription%' OR
+ name LIKE '%usage%' OR
+ name LIKE '%billing%' OR
+ name LIKE '%pricing%' OR
+ name LIKE '%alert%'
+ )
+ ORDER BY name
+ """)
+
+ result = session.execute(tables_query)
+ tables = result.fetchall()
+
+ expected_tables = [
+ 'api_provider_pricing',
+ 'api_usage_logs',
+ 'subscription_plans',
+ 'usage_alerts',
+ 'usage_summaries',
+ 'user_subscriptions'
+ ]
+
+ found_tables = [t[0] for t in tables]
+ print(f"Found tables: {found_tables}")
+
+ missing_tables = [t for t in expected_tables if t not in found_tables]
+ if missing_tables:
+ print(f"โ Missing tables: {missing_tables}")
+ return False
+
+ # Check table data
+ for table in ['subscription_plans', 'api_provider_pricing']:
+ count_query = text(f"SELECT COUNT(*) FROM {table}")
+ result = session.execute(count_query)
+ count = result.fetchone()[0]
+ print(f"โ
{table}: {count} records")
+
+ session.close()
+ return True
+
+ except Exception as e:
+ print(f"โ Database check failed: {e}")
+ return False
+
+def main():
+ """Main verification function."""
+
+ print("๐ ALwrity Billing & Subscription System Setup Verification")
+ print("=" * 70)
+
+ backend_dir = Path(__file__).parent
+
+ # Files to check
+ files_to_check = [
+ (backend_dir / "models" / "subscription_models.py", "Subscription Models"),
+ (backend_dir / "services" / "pricing_service.py", "Pricing Service"),
+ (backend_dir / "services" / "usage_tracking_service.py", "Usage Tracking Service"),
+ (backend_dir / "services" / "subscription_exception_handler.py", "Exception Handler"),
+ (backend_dir / "api" / "subscription_api.py", "Subscription API"),
+ (backend_dir / "scripts" / "create_billing_tables.py", "Billing Migration Script"),
+ (backend_dir / "scripts" / "create_subscription_tables.py", "Subscription Migration Script"),
+ (backend_dir / "start_alwrity_backend.py", "Backend Startup Script"),
+ ]
+
+ # Check file existence
+ print("\n๐ Checking File Existence:")
+ print("-" * 30)
+ files_exist = 0
+ for file_path, description in files_to_check:
+ if check_file_exists(file_path, description):
+ files_exist += 1
+
+ # Check content of key files
+ print("\n๐ Checking File Content:")
+ print("-" * 30)
+
+ content_checks = [
+ (
+ backend_dir / "models" / "subscription_models.py",
+ ["SubscriptionPlan", "APIUsageLog", "UsageSummary", "APIProviderPricing"],
+ "Subscription Models Content"
+ ),
+ (
+ backend_dir / "services" / "pricing_service.py",
+ ["calculate_api_cost", "check_usage_limits", "initialize_default_pricing"],
+ "Pricing Service Content"
+ ),
+ (
+ backend_dir / "services" / "usage_tracking_service.py",
+ ["track_api_usage", "get_user_usage_stats", "enforce_usage_limits"],
+ "Usage Tracking Content"
+ ),
+ (
+ backend_dir / "api" / "subscription_api.py",
+ ["get_user_usage", "get_subscription_plans", "get_dashboard_data"],
+ "API Endpoints Content"
+ ),
+ (
+ backend_dir / "start_alwrity_backend.py",
+ ["setup_billing_tables", "verify_billing_tables"],
+ "Backend Startup Integration"
+ )
+ ]
+
+ content_valid = 0
+ for file_path, search_terms, description in content_checks:
+ if os.path.exists(file_path):
+ if check_file_content(file_path, search_terms, description):
+ content_valid += 1
+ else:
+ print(f"โ {description}: File not found")
+
+ # Check database tables
+ database_ok = check_database_tables()
+
+ # Check middleware integration
+ print("\n๐ง Checking Middleware Integration:")
+ print("-" * 30)
+
+ middleware_file = backend_dir / "middleware" / "monitoring_middleware.py"
+ middleware_terms = [
+ "UsageTrackingService",
+ "detect_api_provider",
+ "track_api_usage",
+ "check_usage_limits_middleware"
+ ]
+
+ middleware_ok = check_file_content(
+ middleware_file,
+ middleware_terms,
+ "Middleware Integration"
+ )
+
+ # Check app.py integration
+ print("\n๐ Checking FastAPI Integration:")
+ print("-" * 30)
+
+ app_file = backend_dir / "app.py"
+ app_terms = [
+ "from api.subscription_api import router as subscription_router",
+ "app.include_router(subscription_router)"
+ ]
+
+ app_ok = check_file_content(
+ app_file,
+ app_terms,
+ "FastAPI App Integration"
+ )
+
+ # Check database service integration
+ print("\n๐พ Checking Database Integration:")
+ print("-" * 30)
+
+ db_file = backend_dir / "services" / "database.py"
+ db_terms = [
+ "from models.subscription_models import Base as SubscriptionBase",
+ "SubscriptionBase.metadata.create_all(bind=engine)"
+ ]
+
+ db_ok = check_file_content(
+ db_file,
+ db_terms,
+ "Database Service Integration"
+ )
+
+ # Summary
+ print("\n" + "=" * 70)
+ print("๐ VERIFICATION SUMMARY")
+ print("=" * 70)
+
+ total_files = len(files_to_check)
+ total_content = len(content_checks)
+
+ print(f"Files Created: {files_exist}/{total_files}")
+ print(f"Content Valid: {content_valid}/{total_content}")
+ print(f"Database Tables: {'โ
' if database_ok else 'โ'}")
+ print(f"Middleware Integration: {'โ
' if middleware_ok else 'โ'}")
+ print(f"FastAPI Integration: {'โ
' if app_ok else 'โ'}")
+ print(f"Database Integration: {'โ
' if db_ok else 'โ'}")
+
+ # Overall status
+ all_checks = [
+ files_exist == total_files,
+ content_valid == total_content,
+ database_ok,
+ middleware_ok,
+ app_ok,
+ db_ok
+ ]
+
+ if all(all_checks):
+ print("\n๐ ALL CHECKS PASSED!")
+ print("โ
Billing and subscription system setup is complete and ready to use.")
+
+ print("\n" + "=" * 70)
+ print("๐ NEXT STEPS:")
+ print("=" * 70)
+ print("1. Start the backend server:")
+ print(" python start_alwrity_backend.py")
+ print("\n2. Test the API endpoints:")
+ print(" GET http://localhost:8000/api/subscription/plans")
+ print(" GET http://localhost:8000/api/subscription/usage/demo")
+ print(" GET http://localhost:8000/api/subscription/dashboard/demo")
+ print(" GET http://localhost:8000/api/subscription/pricing")
+ print("\n3. Access the frontend billing dashboard")
+ print("4. Monitor usage through the API monitoring middleware")
+ print("5. Set up user identification for production use")
+ print("=" * 70)
+
+ else:
+ print("\nโ SOME CHECKS FAILED!")
+ print("Please review the errors above and fix any issues.")
+ return False
+
+ return True
+
+if __name__ == "__main__":
+ success = main()
+ if not success:
+ sys.exit(1)
diff --git a/docs/AI_BLOG_WRITER_IMPLEMENTATION_SPEC.md b/docs/AI_BLOG_WRITER_IMPLEMENTATION_SPEC.md
new file mode 100644
index 00000000..1ebb5ac0
--- /dev/null
+++ b/docs/AI_BLOG_WRITER_IMPLEMENTATION_SPEC.md
@@ -0,0 +1,237 @@
+## AI Blog Writer โ Implementation Specification (Copilot-first, Research-led)
+
+### Overview
+- **Goal**: Build a SOTA AI blog writer that guides non-technical users end-to-end: research โ outline โ section generation โ quality/SEO โ publishing.
+- **Approach**: Copilot-first UX using CopilotKit. Reuse LinkedIn assistive writing patterns: Google Search grounding, Exa research, hallucination detector, quality analysis, citations.
+- **User Interaction Model**: The user only talks to the Copilot; the editor reflects all state and changes via generative UI and HITL confirmations.
+
+### Key Principles
+- **AI-first, HITL**: The assistant leads with intelligent suggestions; the user approves via render-and-wait HITL components where appropriate.
+- **Research fidelity**: Google grounding + Exa researcher; hallucination detection with claim verification; pervasive citations.
+- **Persona-aware**: Import blog writing persona from DB and apply it across planning/generation/optimizations.
+- **SEO-excellent**: Real-time SEO analysis, metadata generation, schema, and image alt handling.
+- **Publish-ready**: Smooth handoff to Wix/WordPress; preview and scheduling.
+
+---
+
+## 1) Workflow (4 Stages)
+
+### Stage 1: Research & Strategy (AI Orchestration)
+Inputs
+- `keywords: string[]`, `industry: string`, `targetAudience: string`, `tone: string`, `wordCountTarget: number`, `userId`
+- Persona is fetched from DB and persisted in session
+
+Backend/Services
+- Reuse LinkedIn research handler patterns: Google native grounding (Gemini provider), optional Exa research.
+- Reuse hallucination detector service and models: `/api/hallucination-detector/*` for claim extraction and verification.
+
+CopilotKit Actions
+- `getPersonaFromDB(userId)` โ persona constraints and style.
+- `analyzeKeywords(keywords, industry, audience)` โ search intent, primary/secondary/long-tail, difficulty, volume.
+- `researchTopic(topic, depth, sources=['google','exa'])` โ aggregated research sources (with credibility + timestamps).
+- `analyzeCompetitors(keywords, industry)` โ top pages, headings used, gaps/opportunities.
+
+Generative UI (render-only)
+- Research Summary card: sources, credibility score, proposed angles.
+- Suggested Keywords: chip list; add/remove HITL.
+
+Suggestions (programmatic)
+- โConfirm researchโ, โRefine keywordsโ, โAdd competitorโ, โProceed to outlineโ.
+
+---
+
+### Stage 2: Content Planning (AI + Human)
+Deliverables
+- Structured outline (H1/H2/H3), per-section key points, citations to use, target word counts.
+
+CopilotKit Actions
+- `generateOutline(research, persona, wordCount)` โ full outline with per-section targets and suggested refs.
+- `refineOutline(operation, sectionId, payload?)` โ add/remove/move/merge sections (HITL diff in UI).
+- `attachReferences(sectionId, sourceIds[])` โ associate sources to sections.
+
+Generative UI (HITL)
+- Outline Editor: draggable sections/subsections, per-section references and target words, persona style hints.
+
+Suggestions
+- โGenerate [Section 1]โ, โRegenerate [Section 2]โ, โAttach sources to [Section]โ, โGenerate All Sectionsโ.
+
+---
+
+### Stage 3: Content Generation (CopilotKit-only, no multi-agent)
+Deliverables
+- Long-form markdown content with inline citations, persona-aligned tone, and sectioned structure.
+
+CopilotKit Actions
+- `generateSection(sectionPlan, keywords, tone, persona, refs[])` โ returns markdown + inline cites.
+- `generateAllSections(outline)` โ sequential section generation with progress render.
+- `optimizeSection(content, goals[])` โ readability/EEAT/examples/data improvements; UI shows diff preview (HITL confirm).
+- `runHallucinationCheck(content)` โ uses `/api/hallucination-detector/detect` to flag claims + propose fixes.
+
+Editor/UI Updates
+- Per-section markdown tabs; word count; inline citation chips; section mini-SEO score.
+- DiffPreview component for any AI edit prior to apply.
+
+Suggestions
+- โAdd table/figureโ, โInsert case study with sourceโ, โStrengthen introductionโ, โTighten conclusion CTAโ.
+
+---
+
+### Stage 4: Optimization & Publishing (AI + Human)
+SEO Optimization
+- `analyzeSEO(content, keywords)` โ density, heading structure, links, readability, image alt coverage, overall SEO score.
+- `generateSEOMetadata(content, title, keywords)` โ title options, meta description, OG/Twitter cards, schema Article/FAQ.
+- `applySEOFixes(suggestions[])` โ diff preview + HITL apply.
+
+Publishing
+- `prepareForPublish(platform: 'wix' | 'wordpress')` โ HTML + images + metadata packaging.
+- `publishToPlatform(platform, schedule?)` โ uses Wix/WordPress clients (ToBeMigrated integrations). Shows URL/status.
+
+Suggestions
+- โRun SEO analysisโ, โApply recommended fixesโ, โGenerate metadataโ, โPublish to WordPressโ, โSchedule on Wixโ.
+
+---
+
+## 2) SEO Tools Integration & Metadata
+
+Existing Services to Wrap
+- Meta Description, OpenGraph, Image Alt, On-Page SEO, Technical SEO, Content Strategy (see `backend/services/seo_tools/*` and docs).
+
+Unified Endpoints
+- `POST /api/blog/seo/analyze` โ { seoScore, density, structure, readability, link suggestions, image alt status, recs }
+- `POST /api/blog/seo/metadata` โ { titleOptions, metaDescriptionOptions, openGraph, twitterCard, schema: { Article, FAQ?, Breadcrumb, Org/Person } }
+
+Editor SEO Panel
+- Live density and distribution, readability (Flesch-Kincaid), heading hierarchy, internal/external link suggestions.
+- One-click โApply Fixโ with diff preview.
+
+Schema
+- Default Article schema; optional FAQ when Q&A snippets exist; Breadcrumb, Organization/Person as applicable.
+
+---
+
+## 3) Dedicated Blog Editor Design (Copilot-first)
+
+Layout
+- Left: Markdown Editor (per-section tabs), word count, persona cues, inline citation chips.
+- Right: Live Preview (desktop/mobile), SEO SERP snippet preview, social preview (OG/Twitter).
+- Sidebar Panels: Research (sources, claims), SEO (scores/fixes), Media (AI images + alt text), History (versions).
+
+Core Components
+- `BlogResearchCard` (render-only): sources, credibility scores, add-to-outline.
+- `OutlineEditor` (HITL): drag-drop H2/H3, per-section refs and target words.
+- `SectionEditor`: markdown area with persona/tone badges; per-section SEO mini-score.
+- `DiffPreview` (HITL): apply/reject AI edits.
+- `SEOPanel`: density/structure/readability + apply fix.
+- `MediaPanel`: AI images, compression, automatic alt-text.
+
+CopilotKit Integrations
+- Suggestions: set programmatically (`useCopilotChatHeadless_c`) or via `CopilotSidebar` props.
+- Generative UI: `useCopilotAction({ render })` for research cards, outline editor, diff preview, publish dialog.
+- HITL: `renderAndWaitForResponse` for approvals at outline, diff apply, and publish steps.
+- References: CopilotKit docs โ Frontend Actions, Generative UI, Suggestions, HITL.
+
+Persistence
+- Persist outline, per-section content, references, persona snapshot, SEO state, metadata drafts.
+- Auto-save every 30s; version history for undo.
+
+---
+
+## 4) Backend APIs
+
+New Blog Endpoints
+- `POST /api/blog/research` โ inputs: keywords/industry/audience/tone/wordCount, personaId?; returns research bundle.
+- `POST /api/blog/outline/generate` โ returns structured outline with targets and ref suggestions.
+- `POST /api/blog/outline/refine` โ returns updated outline (operation-based).
+- `POST /api/blog/section/generate` โ returns markdown + inline citations.
+- `POST /api/blog/section/optimize` โ returns optimized content + rationale.
+- `POST /api/blog/quality/hallucination-check` โ proxies hallucination detector results for blog.
+- `POST /api/blog/seo/analyze` โ wraps SEO analyzers; returns scores/suggestions.
+- `POST /api/blog/seo/metadata` โ returns title/meta/OG/Twitter/schema.
+- `POST /api/blog/publish` โ platform: wix|wordpress, schedule?; returns URL/status.
+
+Reuse
+- `/api/hallucination-detector/detect|extract-claims|verify-claim|health` (already implemented).
+
+Models (indicative)
+- `BlogResearchRequest`, `BlogResearchResponse`
+- `BlogOutline`, `BlogOutlineRefinement`
+- `BlogSectionRequest`, `BlogSectionResponse`
+- `BlogSEOAnalysisRequest`, `BlogSEOMetadataResponse`
+
+---
+
+## 5) CopilotKit Action Inventory
+
+Research
+- `getPersonaFromDB`, `analyzeKeywords`, `researchTopic`, `analyzeCompetitors`
+
+Planning
+- `generateOutline`, `refineOutline`, `attachReferences`
+
+Generation
+- `generateSection`, `generateAllSections`, `optimizeSection`, `runHallucinationCheck`
+
+SEO
+- `analyzeSEO`, `generateSEOMetadata`, `applySEOFixes`
+
+Publishing
+- `prepareForPublish`, `publishToPlatform`
+
+UX/Render-only/HITL
+- `showResearchCard`, `showOutlineEditor`, `showDiffPreview`, `showSEOPanel`, `showPublishDialog`
+
+---
+
+## 6) Intelligent Suggestions (states)
+
+Before research
+- โLoad personaโ, โAnalyze keywordsโ, โResearch topicโ
+
+After research
+- โGenerate outlineโ, โAdd competitor H2sโ, โAttach sourcesโ
+
+Outline ready
+- โGenerate [Section 1]โ, โโฆโ, โGenerate all sectionsโ
+
+Draft ready
+- โRun fact-checkโ, โRun SEO analysisโ, โGenerate metadataโ
+
+Final
+- โPublish to WordPressโ, โSchedule on Wixโ
+
+---
+
+## 7) Delivery Plan / Milestones
+
+Milestone 1: Research + Outline
+- Actions: persona load, analyze keywords, research topic, generate outline, outline editor (HITL)
+
+Milestone 2: Section Generation + Quality
+- generateSection/generateAllSections, optimizeSection with diff preview, hallucination check + fixes
+
+Milestone 3: SEO & Metadata
+- analyzeSEO panel, generateSEOMetadata (title/meta/OG/Twitter/schema), apply fixes
+
+Milestone 4: Publishing
+- prepareForPublish, publishToPlatform (Wix/WordPress), schedule, success URL
+
+Milestone 5: Polish
+- Readability aids, version history, performance, accessibility
+
+---
+
+## 8) References
+- CopilotKit Quickstart, Frontend Actions, Generative UI, HITL, Suggestions
+ - Quickstart: https://docs.copilotkit.ai/direct-to-llm/guides/quickstart
+ - Frontend Actions: https://docs.copilotkit.ai/frontend-actions
+ - Generative UI: https://docs.copilotkit.ai/direct-to-llm/guides/generative-ui
+ - Headless + Suggestions + HITL: https://docs.copilotkit.ai/premium/headless-ui
+
+---
+
+## 9) Notes on Reuse from LinkedIn Writer
+- Research handler; Gemini grounded provider; citation manager; quality analyzer.
+- Hallucination detector + Exa verification endpoints.
+- CopilotKit integration patterns: actions, suggestions, render/HITL, state persistence.
+
+
diff --git a/BILLING_FRONTEND_INTEGRATION_PLAN.md b/docs/Billing_Subscription/BILLING_FRONTEND_INTEGRATION_PLAN.md
similarity index 100%
rename from BILLING_FRONTEND_INTEGRATION_PLAN.md
rename to docs/Billing_Subscription/BILLING_FRONTEND_INTEGRATION_PLAN.md
diff --git a/BILLING_IMPLEMENTATION_ROADMAP.md b/docs/Billing_Subscription/BILLING_IMPLEMENTATION_ROADMAP.md
similarity index 100%
rename from BILLING_IMPLEMENTATION_ROADMAP.md
rename to docs/Billing_Subscription/BILLING_IMPLEMENTATION_ROADMAP.md
diff --git a/BILLING_IMPLEMENTATION_STATUS.md b/docs/Billing_Subscription/BILLING_IMPLEMENTATION_STATUS.md
similarity index 100%
rename from BILLING_IMPLEMENTATION_STATUS.md
rename to docs/Billing_Subscription/BILLING_IMPLEMENTATION_STATUS.md
diff --git a/BILLING_TECHNICAL_SPECIFICATION.md b/docs/Billing_Subscription/BILLING_TECHNICAL_SPECIFICATION.md
similarity index 100%
rename from BILLING_TECHNICAL_SPECIFICATION.md
rename to docs/Billing_Subscription/BILLING_TECHNICAL_SPECIFICATION.md
diff --git a/SUBSCRIPTION_IMPLEMENTATION_SUMMARY.md b/docs/Billing_Subscription/SUBSCRIPTION_IMPLEMENTATION_SUMMARY.md
similarity index 100%
rename from SUBSCRIPTION_IMPLEMENTATION_SUMMARY.md
rename to docs/Billing_Subscription/SUBSCRIPTION_IMPLEMENTATION_SUMMARY.md
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 446fd4f2..7d3b353f 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -9,6 +9,7 @@ import SEODashboard from './components/SEODashboard/SEODashboard';
import ContentPlanningDashboard from './components/ContentPlanningDashboard/ContentPlanningDashboard';
import FacebookWriter from './components/FacebookWriter/FacebookWriter';
import LinkedInWriter from './components/LinkedInWriter/LinkedInWriter';
+import BlogWriter from './components/BlogWriter/BlogWriter';
import { apiClient } from './api/client';
@@ -187,6 +188,7 @@ const App: React.FC = () => {
} />
} />
} />
+ } />
diff --git a/frontend/src/components/BlogWriter/BlogWriter.tsx b/frontend/src/components/BlogWriter/BlogWriter.tsx
new file mode 100644
index 00000000..e1ec8b90
--- /dev/null
+++ b/frontend/src/components/BlogWriter/BlogWriter.tsx
@@ -0,0 +1,581 @@
+import React, { useMemo, useState } from 'react';
+import { CopilotSidebar } from '@copilotkit/react-ui';
+import { useCopilotAction } from '@copilotkit/react-core';
+import '@copilotkit/react-ui/styles.css';
+import { blogWriterApi, BlogOutlineSection, BlogResearchResponse, BlogSEOMetadataResponse, BlogSEOAnalyzeResponse } from '../../services/blogWriterApi';
+import EnhancedOutlineEditor from './EnhancedOutlineEditor';
+import TitleSelector from './TitleSelector';
+import DiffPreview from './DiffPreview';
+import SEOMiniPanel from './SEOMiniPanel';
+import ResearchResults from './ResearchResults';
+import KeywordInputForm from './KeywordInputForm';
+import ResearchAction from './ResearchAction';
+
+const useCopilotActionTyped = useCopilotAction as any;
+
+export const BlogWriter: React.FC = () => {
+ const [research, setResearch] = useState(null);
+ const [outline, setOutline] = useState([]);
+ const [titleOptions, setTitleOptions] = useState([]);
+ const [selectedTitle, setSelectedTitle] = useState('');
+ const [sections, setSections] = useState>({});
+ const [seoAnalysis, setSeoAnalysis] = useState(null);
+ const [seoMetadata, setSeoMetadata] = useState(null);
+ const [hallucinationResult, setHallucinationResult] = useState(null);
+
+ const buildFullMarkdown = () => {
+ if (!outline.length) return '';
+ return outline.map(s => `## ${s.heading}\n\n${sections[s.id] || ''}`).join('\n\n');
+ };
+
+ // Sentence-level claim mapping and patching helpers
+ const normalized = (s: string) => s.toLowerCase().replace(/\s+/g, ' ').trim();
+
+ const fuzzyScore = (a: string, b: string) => {
+ // Dice's coefficient over word bigrams for robustness (no deps)
+ const bigrams = (s: string) => {
+ const t = s.split(/\W+/).filter(Boolean);
+ const grams: string[] = [];
+ for (let i = 0; i < t.length - 1; i++) grams.push(`${t[i]} ${t[i+1]}`);
+ return grams;
+ };
+ const A = new Set(bigrams(a));
+ const B = new Set(bigrams(b));
+ if (!A.size || !B.size) return 0;
+ let overlap = 0;
+ A.forEach(g => { if (B.has(g)) overlap++; });
+ return (2 * overlap) / (A.size + B.size);
+ };
+
+ const findSentenceForClaim = (md: string, claimText: string) => {
+ const text = md || '';
+ // Split by sentence enders; keep delimiters
+ const sentences = text.split(/(?<=[.!?])\s+/);
+ const normalizedClaim = claimText.trim().toLowerCase();
+ // Direct includes first
+ let bestIndex = sentences.findIndex(s => s.toLowerCase().includes(normalizedClaim));
+ if (bestIndex >= 0) return { sentence: sentences[bestIndex], index: bestIndex, sentences };
+ // Fallback: overlap ratio by words
+ const claimWords = normalizedClaim.split(/\W+/).filter(Boolean);
+ let bestScore = 0; bestIndex = -1;
+ sentences.forEach((s, i) => {
+ const sw = s.toLowerCase().split(/\W+/).filter(Boolean);
+ const overlap = claimWords.filter(w => sw.includes(w)).length;
+ const score = overlap / Math.max(claimWords.length, 1);
+ if (score > bestScore) { bestScore = score; bestIndex = i; }
+ });
+ // Second fallback: Dice coefficient on normalized strings
+ if (bestIndex < 0) {
+ let diceBest = 0; let diceIdx = -1;
+ sentences.forEach((s, i) => {
+ const sc = fuzzyScore(normalized(s), normalized(claimText));
+ if (sc > diceBest) { diceBest = sc; diceIdx = i; }
+ });
+ if (diceIdx >= 0) return { sentence: sentences[diceIdx], index: diceIdx, sentences };
+ }
+ if (bestIndex >= 0) return { sentence: sentences[bestIndex], index: bestIndex, sentences };
+ return { sentence: '', index: -1, sentences };
+ };
+
+ const buildUpdatedMarkdownForClaim = (claimText: string, supportingUrl?: string) => {
+ const md = buildFullMarkdown();
+ const { sentence, index, sentences } = findSentenceForClaim(md, claimText);
+ if (!sentence || index < 0) return { original: '', updated: '', updatedMarkdown: md };
+ const alreadyHasLink = /\[[^\]]+\]\(([^)]+)\)/.test(sentence);
+ const fix = supportingUrl && !alreadyHasLink ? `${sentence} [source](${supportingUrl})` : sentence;
+ const updatedSentences = [...sentences];
+ updatedSentences[index] = fix;
+ const updatedMarkdown = updatedSentences.join(' ');
+ return { original: sentence, updated: fix, updatedMarkdown };
+ };
+
+ const applyClaimFix = (claimText: string, supportingUrl?: string) => {
+ // Naive fix: append citation footnote to the first occurrence of claim text
+ const { updatedMarkdown } = buildUpdatedMarkdownForClaim(claimText, supportingUrl);
+ const updated = updatedMarkdown;
+ // Re-split content back to per-section, by headings
+ const parts = updated.split(/^## /gm).filter(Boolean);
+ const newSections: Record = {};
+ outline.forEach((s, idx) => {
+ const body = parts[idx] ? parts[idx].replace(new RegExp(`^${s.heading}\n\n?`), '') : (sections[s.id] || '');
+ newSections[s.id] = body;
+ });
+ setSections(newSections);
+ };
+
+ // Handle research completion
+ const handleResearchComplete = (researchData: BlogResearchResponse) => {
+ setResearch(researchData);
+ };
+
+ 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.' };
+
+ try {
+ const res = await blogWriterApi.generateOutline({ research });
+ if (res?.outline) {
+ setOutline(res.outline);
+ setTitleOptions(res.title_options || []);
+ if (res.title_options && res.title_options.length > 0) {
+ setSelectedTitle(res.title_options[0]); // Auto-select first title
+ }
+
+ const outlineCount = res.outline.length;
+ const primaryKeywords = research.keyword_analysis?.primary || [];
+
+ return {
+ success: true,
+ message: `๐งฉ Outline generated successfully! Created ${outlineCount} sections based on your research. The outline incorporates your primary keywords (${primaryKeywords.join(', ')}) and follows the content angles we discovered. You can now review the outline structure, choose a title, and generate content for individual sections.`,
+ outline_summary: {
+ sections: outlineCount,
+ primary_keywords: primaryKeywords,
+ research_sources: research.sources?.length || 0,
+ title_options: res.title_options?.length || 0
+ }
+ };
+ }
+ } catch (error) {
+ console.error('Outline generation failed:', error);
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
+ return {
+ success: false,
+ message: `โ Outline generation failed: ${errorMessage}. The AI system encountered an issue while creating your outline. Please try again or contact support if the problem persists.`
+ };
+ }
+ return {
+ success: false,
+ message: 'Failed to generate outline. The AI outline generation system encountered an issue. Please try again or contact support if the problem persists.'
+ };
+ },
+ render: ({ status }: any) => {
+ console.log('generateOutline render called with status:', status);
+ if (status === 'inProgress' || status === 'executing') {
+ return (
+
+
+
+
๐งฉ Generating Outline
+
+
+
โข Analyzing research results and content angles...
+
โข Structuring content based on keyword analysis...
+
โข Creating logical flow and section hierarchy...
+
โข Optimizing for SEO and reader engagement...
+
+
+
+ );
+ }
+ return null;
+ }
+ });
+
+ useCopilotActionTyped({
+ name: 'generateSection',
+ description: 'Generate content for a specific section using research and outline',
+ parameters: [ { name: 'sectionId', type: 'string', description: 'Section ID', required: true } ],
+ handler: async ({ sectionId }: { sectionId: string }) => {
+ const section = outline.find(s => s.id === sectionId);
+ if (!section) return { success: false, message: 'Section not found. Please generate an outline first.' };
+
+ try {
+ const res = await blogWriterApi.generateSection({ section });
+ if (res?.markdown) {
+ setSections(prev => ({ ...prev, [sectionId]: res.markdown }));
+
+ return {
+ success: true,
+ message: `โ๏ธ Content generated for "${section.heading}"! The section incorporates your research findings and primary keywords. You can now review the content, run SEO analysis, or generate more sections.`,
+ section_summary: {
+ heading: section.heading,
+ content_length: res.markdown.length,
+ primary_keywords: research?.keyword_analysis?.primary || []
+ }
+ };
+ }
+ } catch (error) {
+ console.error('Section generation failed:', error);
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
+ return {
+ success: false,
+ message: `โ Content generation failed for "${section.heading}": ${errorMessage}. Please try again or contact support if the problem persists.`
+ };
+ }
+ return { success: false, message: 'Failed to generate section content. Please try again.' };
+ },
+ render: ({ status }: any) => {
+ if (status === 'inProgress' || status === 'executing') {
+ return (
+
+
+
+
โ๏ธ Generating Section Content
+
+
+
โข Analyzing section requirements and research data...
+
โข Incorporating primary keywords and SEO best practices...
+
โข Writing engaging content with proper structure...
+
โข Ensuring factual accuracy and readability...
+
+
+
+ );
+ }
+ return null;
+ }
+ });
+
+ useCopilotActionTyped({
+ name: 'generateAllSections',
+ description: 'Generate content for every section in the outline',
+ parameters: [],
+ handler: async () => {
+ for (const s of outline) {
+ const res = await blogWriterApi.generateSection({ section: s });
+ setSections(prev => ({ ...prev, [s.id]: res.markdown }));
+ }
+ return { success: true };
+ },
+ render: ({ status }: any) => (status === 'inProgress' || status === 'executing') ? Generating all sectionsโฆ
: null
+ });
+
+ // Outline refinement (basic op pass-through)
+ useCopilotActionTyped({
+ name: 'refineOutline',
+ description: 'Refine the outline (add/remove/move/merge)',
+ parameters: [
+ { name: 'operation', type: 'string', description: 'add|remove|move|merge|rename', required: true },
+ { name: 'sectionId', type: 'string', description: 'Target section ID', required: false },
+ { name: 'payload', type: 'string', description: 'JSON payload for operation', required: false },
+ ],
+ handler: async ({ operation, sectionId, payload }: { operation: string; sectionId?: string; payload?: string }) => {
+ const payloadObj = payload ? (() => { try { return JSON.parse(payload); } catch { return {}; } })() : undefined;
+ const res = await blogWriterApi.refineOutline({ outline, operation, section_id: sectionId, payload: payloadObj });
+ if (res?.outline) setOutline(res.outline);
+ return { success: true };
+ }
+ });
+
+ // Optimize section with HITL diff preview
+ useCopilotActionTyped({
+ name: 'optimizeSection',
+ description: 'Optimize a section for readability/EEAT/examples/data with HITL diff',
+ parameters: [
+ { name: 'sectionId', type: 'string', description: 'Section ID', required: true },
+ { name: 'goals', type: 'string', description: 'Comma-separated goals', required: false },
+ ],
+ handler: async ({ sectionId, goals }: { sectionId: string; goals?: string }) => {
+ const current = sections[sectionId] || '';
+ if (!current) return { success: false, message: 'No content yet for this section' };
+ const res = await blogWriterApi.seoAnalyze({ content: current, keywords: [] });
+ setSeoAnalysis(res);
+ return { success: true, message: 'Analysis ready' };
+ },
+ renderAndWaitForResponse: ({ respond, args, status }: any) => {
+ if (status === 'complete') return Optimization applied.
;
+ return (
+
+
Optimization preview
+
Goals: {args.goals || 'readability, EEAT'}
+
respond?.('apply')}>Apply Changes
+
+ );
+ }
+ });
+
+ // SEO analyze full draft
+ useCopilotActionTyped({
+ name: 'runSEOAnalyze',
+ description: 'Analyze SEO for the full draft',
+ parameters: [ { name: 'keywords', type: 'string', description: 'Comma-separated keywords', required: false } ],
+ handler: async ({ keywords }: { keywords?: string }) => {
+ const content = buildFullMarkdown();
+ const res = await blogWriterApi.seoAnalyze({ content, keywords: keywords ? keywords.split(',').map(k => k.trim()) : [] });
+ setSeoAnalysis(res);
+ return { success: true, seo_score: res.seo_score };
+ },
+ render: ({ status, result }: any) => status === 'complete' ? (
+
+
SEO Score: {result?.seo_score ?? 'โ'}
+
+ ) : null
+ });
+
+ // SEO metadata generate + HITL accept
+ useCopilotActionTyped({
+ name: 'generateSEOMetadata',
+ description: 'Generate SEO metadata for the full draft',
+ parameters: [ { name: 'title', type: 'string', description: 'Preferred title', required: false } ],
+ handler: async ({ title }: { title?: string }) => {
+ const content = buildFullMarkdown();
+ const res = await blogWriterApi.seoMetadata({ content, title, keywords: [] });
+ setSeoMetadata(res);
+ return { success: true };
+ },
+ renderAndWaitForResponse: ({ respond }: any) => (
+
+
SEO Metadata Ready
+
Review the generated title, meta description, and OG/Twitter tags in the editor.
+
respond?.('accept')}>Accept Metadata
+
+ )
+ });
+
+ // Hallucination check with HITL apply-fix
+ useCopilotActionTyped({
+ name: 'runHallucinationCheck',
+ description: 'Run hallucination detector on full draft and view claims',
+ parameters: [],
+ handler: async () => {
+ const content = buildFullMarkdown();
+ const res = await fetch('/api/blog/quality/hallucination-check', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ text: content })
+ });
+ const data = await res.json();
+ setHallucinationResult(data);
+ return { success: true, total_claims: data?.total_claims };
+ },
+ renderAndWaitForResponse: ({ respond, result }: any) => {
+ if (!result) return null;
+ const claims = hallucinationResult?.claims || [];
+ return (
+
+
Hallucination Check
+
Total claims: {hallucinationResult?.total_claims ?? 0}
+
+
respond?.('ack')}>Close
+
+ );
+ }
+ });
+
+ // Publish (convert markdown -> HTML rudimentary; TODO: replace with proper converter like marked)
+ useCopilotActionTyped({
+ name: 'publishToPlatform',
+ description: 'Publish the blog to Wix or WordPress',
+ parameters: [
+ { name: 'platform', type: 'string', description: 'wix|wordpress', required: true },
+ { name: 'schedule_time', type: 'string', description: 'Optional ISO datetime', required: false }
+ ],
+ handler: async ({ platform, schedule_time }: { platform: 'wix' | 'wordpress'; schedule_time?: string }) => {
+ const md = buildFullMarkdown();
+ const html = md
+ .replace(/^### (.*$)/gim, '$1 ')
+ .replace(/^## (.*$)/gim, '$1 ')
+ .replace(/^# (.*$)/gim, '$1 ')
+ .replace(/\*\*(.*)\*\*/gim, '$1 ')
+ .replace(/\*(.*)\*/gim, '$1 ')
+ .replace(/\n\n/g, ' ');
+ if (!seoMetadata) return { success: false, message: 'Generate SEO metadata first' };
+ const res = await blogWriterApi.publish({ platform, html, metadata: seoMetadata, schedule_time });
+ return { success: true, url: res.url };
+ },
+ render: ({ status, result }: any) => status === 'complete' ? (
+ Published: {result?.url || 'Success'}
+ ) : null
+ });
+
+ const suggestions = useMemo(() => {
+ const items = [] as { title: string; message: string }[];
+ if (!research) items.push({ title: '๐ Start research', message: "I want to research a topic for my blog" });
+ if (research && outline.length === 0) items.push({ title: '๐งฉ Create Outline', message: 'Let\'s proceed to create an outline based on the research results' });
+ if (outline.length > 0) {
+ items.push({ title: '๐ Generate all sections', message: 'Generate all sections of my blog post' });
+ outline.forEach(s => items.push({ title: `โ๏ธ Generate ${s.heading}`, message: `Generate the section: ${s.heading}` }));
+ items.push({ title: '๐ง Refine outline', message: 'Help me refine the outline structure' });
+ items.push({ title: '๐ Run SEO analysis', message: 'Analyze SEO for my blog post' });
+ items.push({ title: '๐งพ Generate SEO metadata', message: 'Generate SEO metadata and title' });
+ items.push({ title: '๐งช Hallucination check', message: 'Check for any false claims in my content' });
+ items.push({ title: '๐ Publish to WordPress', message: 'Publish my blog to WordPress' });
+ }
+ return items;
+ }, [research, outline]);
+
+ return (
+
+ {/* Extracted Components */}
+
+
+
+
+
AI Blog Writer
+
+
+
+ {!research && (
+
+
๐
+
Ready to Research Your Blog Topic
+
Start by asking the copilot to research your topic.
+
+ )}
+ {research && outline.length === 0 &&
}
+ {outline.length > 0 && (
+
+ {/* Title Selection */}
+ {titleOptions.length > 0 && (
+
{
+ setTitleOptions(prev => [...prev, title]);
+ setSelectedTitle(title);
+ }}
+ />
+ )}
+
+ {/* Enhanced Outline Editor */}
+ blogWriterApi.refineOutline({ outline, operation: op, section_id: id, payload }).then(res => setOutline(res.outline))}
+ />
+
+ {outline.map(s => (
+
+
{s.heading}
+ {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 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 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` : 'โ No outline generated yet'}
+
+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)
+- generateOutline()
+- generateSection(sectionId: string)
+- generateAllSections()
+- refineOutline(operation: add|remove|move|merge|rename, sectionId?: string, payload?: object)
+- runSEOAnalyze(keywords?: string)
+- generateSEOMetadata(title?: string)
+- runHallucinationCheck()
+- publishToPlatform(platform: 'wix'|'wordpress', schedule_time?: string)
+
+ CRITICAL BEHAVIOR:
+ - 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
+ - When user asks for outline, call generateOutline()
+ - When user asks to generate content, call generateSection or generateAllSections
+ - 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 โ Content โ SEO โ Publish
+`;
+ return [toolGuide, additional].filter(Boolean).join('\n\n');
+ }}
+ />
+
+ );
+};
+
+export default BlogWriter;
\ No newline at end of file
diff --git a/frontend/src/components/BlogWriter/DiffPreview.tsx b/frontend/src/components/BlogWriter/DiffPreview.tsx
new file mode 100644
index 00000000..798098fb
--- /dev/null
+++ b/frontend/src/components/BlogWriter/DiffPreview.tsx
@@ -0,0 +1,51 @@
+import React from 'react';
+
+interface Props {
+ original: string;
+ updated: string;
+ onApply: () => void;
+ onDiscard: () => void;
+}
+
+function highlightDiff(a: string, b: string) {
+ // Simple common prefix/suffix highlighting
+ let i = 0;
+ while (i < a.length && i < b.length && a[i] === b[i]) i++;
+ let j = 0;
+ while (j < a.length - i && j < b.length - i && a[a.length - 1 - j] === b[b.length - 1 - j]) j++;
+ const aMid = a.substring(i, a.length - j);
+ const bMid = b.substring(i, b.length - j);
+ const aHtml = `${escapeHtml(a.substring(0, i))}${escapeHtml(aMid)} ${escapeHtml(a.substring(a.length - j))}`;
+ const bHtml = `${escapeHtml(b.substring(0, i))}${escapeHtml(bMid)} ${escapeHtml(b.substring(b.length - j))}`;
+ return { aHtml, bHtml };
+}
+
+function escapeHtml(s: string) {
+ return s
+ .replace(/&/g, '&')
+ .replace(//g, '>')
+ .replace(/"/g, '"')
+ .replace(/'/g, ''');
+}
+
+const DiffPreview: React.FC = ({ original, updated, onApply, onDiscard }) => {
+ const { aHtml, bHtml } = highlightDiff(original, updated);
+ return (
+
+
Preview Changes
+
+
+ Apply
+ Discard
+
+
+ );
+};
+
+export default DiffPreview;
+
+
diff --git a/frontend/src/components/BlogWriter/EnhancedOutlineEditor.tsx b/frontend/src/components/BlogWriter/EnhancedOutlineEditor.tsx
new file mode 100644
index 00000000..20c1ed4f
--- /dev/null
+++ b/frontend/src/components/BlogWriter/EnhancedOutlineEditor.tsx
@@ -0,0 +1,535 @@
+import React, { useState } from 'react';
+import { BlogOutlineSection } from '../../services/blogWriterApi';
+
+interface Props {
+ outline: BlogOutlineSection[];
+ onRefine: (operation: string, sectionId?: string, payload?: any) => void;
+ research?: any; // Research data for context
+}
+
+const EnhancedOutlineEditor: React.FC = ({ outline, onRefine, research }) => {
+ const [editingSection, setEditingSection] = useState(null);
+ const [expandedSections, setExpandedSections] = useState>(new Set());
+ const [showAddSection, setShowAddSection] = useState(false);
+ const [newSectionData, setNewSectionData] = useState({
+ heading: '',
+ subheadings: '',
+ key_points: '',
+ target_words: 300
+ });
+
+ const toggleExpanded = (sectionId: string) => {
+ const newExpanded = new Set(expandedSections);
+ if (newExpanded.has(sectionId)) {
+ newExpanded.delete(sectionId);
+ } else {
+ newExpanded.add(sectionId);
+ }
+ setExpandedSections(newExpanded);
+ };
+
+ const handleRename = (sectionId: string, newHeading: string) => {
+ if (newHeading.trim()) {
+ onRefine('rename', sectionId, { heading: newHeading.trim() });
+ }
+ setEditingSection(null);
+ };
+
+ const handleMove = (sectionId: string, direction: 'up' | 'down') => {
+ onRefine('move', sectionId, { direction });
+ };
+
+ const handleAddSection = () => {
+ if (newSectionData.heading.trim()) {
+ const subheadings = newSectionData.subheadings
+ .split('\n')
+ .map(s => s.trim())
+ .filter(s => s.length > 0);
+
+ const keyPoints = newSectionData.key_points
+ .split('\n')
+ .map(s => s.trim())
+ .filter(s => s.length > 0);
+
+ onRefine('add', undefined, {
+ heading: newSectionData.heading.trim(),
+ subheadings,
+ key_points: keyPoints,
+ target_words: newSectionData.target_words
+ });
+
+ setNewSectionData({
+ heading: '',
+ subheadings: '',
+ key_points: '',
+ target_words: 300
+ });
+ setShowAddSection(false);
+ }
+ };
+
+ const getTotalWords = () => {
+ return outline.reduce((total, section) => total + (section.target_words || 0), 0);
+ };
+
+
+ return (
+
+ {/* Header */}
+
+
+
+
+ ๐ Blog Outline
+
+
+ {outline.length} sections โข {getTotalWords()} words total
+
+
+
setShowAddSection(!showAddSection)}
+ style={{
+ backgroundColor: '#1976d2',
+ color: 'white',
+ border: 'none',
+ padding: '10px 20px',
+ borderRadius: '8px',
+ cursor: 'pointer',
+ fontSize: '14px',
+ fontWeight: '500',
+ display: 'flex',
+ alignItems: 'center',
+ gap: '8px'
+ }}
+ >
+ โ Add Section
+
+
+
+
+ {/* Add Section Form */}
+ {showAddSection && (
+
+
Add New Section
+
+
+
+ Section Title
+
+ setNewSectionData({...newSectionData, heading: e.target.value})}
+ placeholder="Enter section title..."
+ style={{
+ width: '100%',
+ padding: '8px 12px',
+ border: '1px solid #ddd',
+ borderRadius: '6px',
+ fontSize: '14px'
+ }}
+ />
+
+
+
+
+ Subheadings (one per line)
+
+
+
+
+ Key Points (one per line)
+
+
+
+
+
+ Target Words
+
+ setNewSectionData({...newSectionData, target_words: parseInt(e.target.value) || 300})}
+ min="100"
+ max="2000"
+ style={{
+ width: '120px',
+ padding: '8px 12px',
+ border: '1px solid #ddd',
+ borderRadius: '6px',
+ fontSize: '14px'
+ }}
+ />
+
+
+
+ Add Section
+
+ setShowAddSection(false)}
+ style={{
+ backgroundColor: '#f5f5f5',
+ color: '#666',
+ border: 'none',
+ padding: '10px 20px',
+ borderRadius: '6px',
+ cursor: 'pointer',
+ fontSize: '14px',
+ fontWeight: '500'
+ }}
+ >
+ Cancel
+
+
+
+
+ )}
+
+ {/* Outline Sections */}
+
+ {outline.map((section, index) => (
+
+ {/* Section Header */}
+
toggleExpanded(section.id)}>
+
+
+ {index + 1}
+
+
+ {editingSection === section.id ? (
+
handleRename(section.id, e.target.value)}
+ onKeyPress={(e) => {
+ if (e.key === 'Enter') {
+ handleRename(section.id, e.currentTarget.value);
+ }
+ }}
+ autoFocus
+ style={{
+ fontSize: '16px',
+ fontWeight: '600',
+ border: '1px solid #1976d2',
+ borderRadius: '4px',
+ padding: '4px 8px',
+ backgroundColor: 'white'
+ }}
+ />
+ ) : (
+
+ {section.heading}
+
+ )}
+
+
+
+ {section.target_words || 300} words
+
+
+ {section.references && section.references.length > 0 && (
+
+ {section.references.length} sources
+
+ )}
+
+
+
+
+
{
+ e.stopPropagation();
+ setEditingSection(section.id);
+ }}
+ style={{
+ backgroundColor: 'transparent',
+ border: '1px solid #ddd',
+ borderRadius: '4px',
+ padding: '4px 8px',
+ cursor: 'pointer',
+ fontSize: '12px',
+ color: '#666'
+ }}
+ >
+ โ๏ธ
+
+
+
{
+ e.stopPropagation();
+ handleMove(section.id, 'up');
+ }}
+ disabled={index === 0}
+ style={{
+ backgroundColor: 'transparent',
+ border: '1px solid #ddd',
+ borderRadius: '4px',
+ padding: '4px 8px',
+ cursor: index === 0 ? 'not-allowed' : 'pointer',
+ fontSize: '12px',
+ color: index === 0 ? '#ccc' : '#666',
+ opacity: index === 0 ? 0.5 : 1
+ }}
+ >
+ โฌ๏ธ
+
+
+
{
+ e.stopPropagation();
+ handleMove(section.id, 'down');
+ }}
+ disabled={index === outline.length - 1}
+ style={{
+ backgroundColor: 'transparent',
+ border: '1px solid #ddd',
+ borderRadius: '4px',
+ padding: '4px 8px',
+ cursor: index === outline.length - 1 ? 'not-allowed' : 'pointer',
+ fontSize: '12px',
+ color: index === outline.length - 1 ? '#ccc' : '#666',
+ opacity: index === outline.length - 1 ? 0.5 : 1
+ }}
+ >
+ โฌ๏ธ
+
+
+
{
+ e.stopPropagation();
+ if (window.confirm(`Are you sure you want to remove "${section.heading}"?`)) {
+ onRefine('remove', section.id);
+ }
+ }}
+ style={{
+ backgroundColor: 'transparent',
+ border: '1px solid #f44336',
+ borderRadius: '4px',
+ padding: '4px 8px',
+ cursor: 'pointer',
+ fontSize: '12px',
+ color: '#f44336'
+ }}
+ >
+ ๐๏ธ
+
+
+
+ โผ
+
+
+
+
+ {/* Expanded Section Content */}
+ {expandedSections.has(section.id) && (
+
+ {/* Subheadings */}
+ {section.subheadings && section.subheadings.length > 0 && (
+
+
+ ๐ Subheadings
+
+
+ {section.subheadings.map((subheading, i) => (
+
+ {subheading}
+
+ ))}
+
+
+ )}
+
+ {/* Key Points */}
+ {section.key_points && section.key_points.length > 0 && (
+
+
+ ๐ฏ Key Points
+
+
+ {section.key_points.map((point, i) => (
+
+ {point}
+
+ ))}
+
+
+ )}
+
+ {/* Keywords */}
+ {section.keywords && section.keywords.length > 0 && (
+
+
+ ๐ฏ SEO Keywords
+
+
+ {section.keywords.map((keyword, i) => (
+
+ {keyword}
+
+ ))}
+
+
+ )}
+
+ {/* References */}
+ {section.references && section.references.length > 0 && (
+
+
+ ๐ Sources ({section.references.length})
+
+
+ {section.references.map((ref, i) => (
+
+
+ {ref.title}
+
+
+ Credibility: {Math.round((ref.credibility_score || 0.8) * 100)}%
+
+
+ ))}
+
+
+ )}
+
+ )}
+
+ ))}
+
+
+ {/* Footer */}
+
+
+ ๐ก Tip: Click on any section to expand and see details. Use the controls to reorder, edit, or remove sections.
+
+
+ Total: {getTotalWords()} words
+
+
+
+ );
+};
+
+export default EnhancedOutlineEditor;
diff --git a/frontend/src/components/BlogWriter/KeywordInputForm.tsx b/frontend/src/components/BlogWriter/KeywordInputForm.tsx
new file mode 100644
index 00000000..559e3aca
--- /dev/null
+++ b/frontend/src/components/BlogWriter/KeywordInputForm.tsx
@@ -0,0 +1,292 @@
+import React, { useState, useRef, useEffect } from 'react';
+import { useCopilotAction } from '@copilotkit/react-core';
+import { blogWriterApi, BlogResearchRequest, BlogResearchResponse } from '../../services/blogWriterApi';
+
+const useCopilotActionTyped = useCopilotAction as any;
+
+interface KeywordInputFormProps {
+ onKeywordsReceived?: (data: { keywords: string; blogLength: string }) => void;
+ onResearchComplete?: (researchData: BlogResearchResponse) => void;
+}
+
+export const KeywordInputForm: React.FC = ({ onKeywordsReceived, onResearchComplete }) => {
+ // State for button enable/disable only
+ const [hasInput, setHasInput] = useState(false);
+
+ const inputRef = useRef(null);
+ const selectRef = useRef(null);
+
+ // Focus input when form appears
+ useEffect(() => {
+ if (inputRef.current) {
+ setTimeout(() => {
+ inputRef.current?.focus();
+ inputRef.current?.select();
+ }, 100);
+ }
+ }, []);
+
+
+ // 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 (
+
+ );
+ }
+ });
+
+ // 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;
+
+ // If keywords is a topic description, extract keywords from it
+ const keywordList = keywords.includes(',')
+ ? keywords.split(',').map((k: string) => k.trim())
+ : keywords.split(' ').filter((k: string) => k.length > 2).slice(0, 5);
+
+ const payload: BlogResearchRequest = {
+ keywords: keywordList,
+ industry: 'General',
+ target_audience: 'General',
+ word_count_target: parseInt(blogLength)
+ };
+
+ const res = await blogWriterApi.research(payload);
+
+ // Notify parent component
+ onResearchComplete?.(res);
+
+ const sourcesCount = res.sources?.length || 0;
+ const queriesCount = res.search_queries?.length || 0;
+ const anglesCount = res.suggested_angles?.length || 0;
+
+ return {
+ success: true,
+ message: `๐ Research completed for "${keywords}"! Found ${sourcesCount} sources, ${queriesCount} search queries, and ${anglesCount} content angles. The research results are now displayed in the UI.`,
+ research_summary: {
+ topic: keywords,
+ sources: sourcesCount,
+ queries: queriesCount,
+ angles: anglesCount,
+ primary_keywords: res.keyword_analysis?.primary || [],
+ search_intent: res.keyword_analysis?.search_intent || 'informational'
+ }
+ };
+ } catch (error) {
+ console.error(`Research failed: ${error}`);
+ return {
+ success: false,
+ message: `โ Research failed: ${error}. Please try again with different keywords.`
+ };
+ }
+ },
+ render: ({ status }: any) => {
+ console.log('performResearch render called with status:', status);
+ 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;
+ }
+ });
+
+ return null; // This component only provides the CopilotKit action, no UI
+};
+
+export default KeywordInputForm;
diff --git a/frontend/src/components/BlogWriter/RegisterBlogWriterActions.tsx b/frontend/src/components/BlogWriter/RegisterBlogWriterActions.tsx
new file mode 100644
index 00000000..a6b8ff52
--- /dev/null
+++ b/frontend/src/components/BlogWriter/RegisterBlogWriterActions.tsx
@@ -0,0 +1,21 @@
+import { useCopilotAction } from '@copilotkit/react-core';
+
+const useCopilotActionTyped = useCopilotAction as any;
+
+export const RegisterBlogWriterActions: React.FC = () => {
+ useCopilotActionTyped({
+ name: 'Generate All Sections of Outline',
+ description: 'Generate content for every section in the current outline',
+ parameters: [],
+ handler: async () => {
+ // Frontend-only placeholder; generation handled via individual actions in UI for now
+ return { success: true };
+ },
+ });
+
+ return null;
+};
+
+export default RegisterBlogWriterActions;
+
+
diff --git a/frontend/src/components/BlogWriter/ResearchAction.tsx b/frontend/src/components/BlogWriter/ResearchAction.tsx
new file mode 100644
index 00000000..8100a72e
--- /dev/null
+++ b/frontend/src/components/BlogWriter/ResearchAction.tsx
@@ -0,0 +1,108 @@
+import React from 'react';
+import { useCopilotAction } from '@copilotkit/react-core';
+import { blogWriterApi, BlogResearchRequest, BlogResearchResponse } from '../../services/blogWriterApi';
+
+const useCopilotActionTyped = useCopilotAction as any;
+
+interface ResearchActionProps {
+ onResearchComplete?: (research: BlogResearchResponse) => void;
+}
+
+export const ResearchAction: React.FC = ({ onResearchComplete }) => {
+ 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: '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 }) => {
+ try {
+ // If keywords is a topic description, extract keywords from it
+ const keywordList = keywords.includes(',')
+ ? keywords.split(',').map(k => k.trim())
+ : keywords.split(' ').filter(k => k.length > 2).slice(0, 5); // Extract up to 5 meaningful words
+
+ const payload: BlogResearchRequest = {
+ keywords: keywordList,
+ industry: industry || 'General',
+ target_audience: target_audience || 'General',
+ word_count_target: blogLength ? parseInt(blogLength) : 1000
+ };
+
+ const res = await blogWriterApi.research(payload);
+
+ // Notify parent component
+ onResearchComplete?.(res);
+
+ // Create detailed success message with research insights
+ const sourcesCount = res.sources?.length || 0;
+ const queriesCount = res.search_queries?.length || 0;
+ const anglesCount = res.suggested_angles?.length || 0;
+
+ return {
+ success: true,
+ message: `๐ Research completed for "${keywords}"! Found ${sourcesCount} sources, ${queriesCount} search queries, and ${anglesCount} content angles. The research results are now displayed in the UI. You can explore the sources, keywords, and content angles to understand the topic better before we create an outline.`,
+ research_summary: {
+ topic: keywords,
+ sources: sourcesCount,
+ queries: queriesCount,
+ angles: anglesCount,
+ primary_keywords: res.keyword_analysis?.primary || [],
+ search_intent: res.keyword_analysis?.search_intent || 'informational'
+ }
+ };
+ } 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.`
+ };
+ }
+ },
+ render: ({ status }: any) => {
+ if (status === 'inProgress') {
+ 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;
+ }
+ });
+
+ return null; // This component only provides the CopilotKit action, no UI
+};
+
+export default ResearchAction;
diff --git a/frontend/src/components/BlogWriter/ResearchResults.tsx b/frontend/src/components/BlogWriter/ResearchResults.tsx
new file mode 100644
index 00000000..4ecb6625
--- /dev/null
+++ b/frontend/src/components/BlogWriter/ResearchResults.tsx
@@ -0,0 +1,351 @@
+import React, { useState } from 'react';
+import { BlogResearchResponse } from '../../services/blogWriterApi';
+
+interface ResearchResultsProps {
+ research: BlogResearchResponse;
+}
+
+export const ResearchResults: React.FC = ({ research }) => {
+ const [activeTab, setActiveTab] = useState<'sources' | 'keywords' | 'angles' | 'queries'>('sources');
+ const [showSearchWidget, setShowSearchWidget] = useState(false);
+
+ const renderCredibilityScore = (score: number | undefined) => {
+ const safeScore = score ?? 0.8; // Default to 0.8 if undefined
+ const percentage = Math.round(safeScore * 100);
+ const color = safeScore >= 0.8 ? '#4CAF50' : safeScore >= 0.6 ? '#FF9800' : '#F44336';
+ return (
+
+ );
+ };
+
+ const renderSources = () => (
+
+
๐ Research Sources ({research.sources.length})
+
+ {research.sources.map((source, index) => (
+
+
+
+ {source.title}
+
+ {renderCredibilityScore(source.credibility_score)}
+
+
+ {source.excerpt}
+
+
+ {source.url}
+
+
+ Published: {source.published_at}
+
+
+ ))}
+
+
+ );
+
+ const renderKeywordAnalysis = () => (
+
+
๐ฏ Keyword Analysis
+
+
+
+
Primary Keywords
+
+ {research.keyword_analysis.primary?.map((keyword: string, index: number) => (
+
+ {keyword}
+
+ ))}
+
+
+
+
+
Secondary Keywords
+
+ {research.keyword_analysis.secondary?.map((keyword: string, index: number) => (
+
+ {keyword}
+
+ ))}
+
+
+
+
+
Long-tail Keywords
+
+ {research.keyword_analysis.long_tail?.map((keyword: string, index: number) => (
+
+ {keyword}
+
+ ))}
+
+
+
+
+
+
Search Intent
+
+ {research.keyword_analysis.search_intent || 'Informational'}
+
+
+
+
Difficulty Score
+
+ {research.keyword_analysis.difficulty || 'N/A'}/10
+
+
+
+
+
+ );
+
+ const renderContentAngles = () => (
+
+
๐ก Content Angles ({research.suggested_angles.length})
+
+ {research.suggested_angles.map((angle, index) => (
+
{
+ e.currentTarget.style.backgroundColor = '#f0f0f0';
+ e.currentTarget.style.borderColor = '#1976d2';
+ }}
+ onMouseLeave={(e) => {
+ e.currentTarget.style.backgroundColor = '#fafafa';
+ e.currentTarget.style.borderColor = '#e0e0e0';
+ }}>
+
+
+ {index + 1}
+
+
+ {angle}
+
+
+
+ ))}
+
+
+ );
+
+ const renderSearchQueries = () => {
+ const queries = research.search_queries || [];
+ return (
+
+
๐ Search Queries ({queries.length})
+
+ {queries.map((query: string, index: number) => (
+
+ {index + 1}.
+ {query}
+
+ ))}
+
+
+ );
+ };
+
+ const renderSearchWidget = () => {
+ if (!research.search_widget) return null;
+
+ return (
+
+
+
๐ฏ Interactive Search Widget
+ setShowSearchWidget(!showSearchWidget)}
+ style={{
+ backgroundColor: showSearchWidget ? '#1976d2' : '#f5f5f5',
+ color: showSearchWidget ? 'white' : '#333',
+ border: 'none',
+ padding: '8px 16px',
+ borderRadius: '6px',
+ cursor: 'pointer',
+ fontSize: '12px',
+ fontWeight: '500'
+ }}
+ >
+ {showSearchWidget ? 'Hide Widget' : 'Show Widget'}
+
+
+
+ {showSearchWidget && (
+
+ )}
+
+ );
+ };
+
+ return (
+
+ {/* Header */}
+
+
+ ๐ Research Results
+
+
+ Google Search grounding analysis completed with {research.sources.length} sources and {research.search_queries?.length || 0} search queries
+
+
+
+ {/* Tabs */}
+
+ {[
+ { id: 'sources', label: 'Sources', icon: '๐' },
+ { id: 'keywords', label: 'Keywords', icon: '๐ฏ' },
+ { id: 'angles', label: 'Angles', icon: '๐ก' },
+ { id: 'queries', label: 'Queries', icon: '๐' }
+ ].map(tab => (
+ setActiveTab(tab.id as any)}
+ style={{
+ flex: 1,
+ padding: '12px 16px',
+ border: 'none',
+ backgroundColor: activeTab === tab.id ? 'white' : 'transparent',
+ color: activeTab === tab.id ? '#1976d2' : '#666',
+ cursor: 'pointer',
+ fontSize: '14px',
+ fontWeight: activeTab === tab.id ? '600' : '400',
+ borderBottom: activeTab === tab.id ? '2px solid #1976d2' : '2px solid transparent',
+ transition: 'all 0.2s ease'
+ }}
+ >
+ {tab.icon} {tab.label}
+
+ ))}
+
+
+ {/* Content */}
+ {activeTab === 'sources' && renderSources()}
+ {activeTab === 'keywords' && renderKeywordAnalysis()}
+ {activeTab === 'angles' && renderContentAngles()}
+ {activeTab === 'queries' && renderSearchQueries()}
+
+ {/* Search Widget */}
+ {renderSearchWidget()}
+
+ );
+};
+
+export default ResearchResults;
diff --git a/frontend/src/components/BlogWriter/SEOMiniPanel.tsx b/frontend/src/components/BlogWriter/SEOMiniPanel.tsx
new file mode 100644
index 00000000..e691069a
--- /dev/null
+++ b/frontend/src/components/BlogWriter/SEOMiniPanel.tsx
@@ -0,0 +1,25 @@
+import React from 'react';
+import { BlogSEOAnalyzeResponse } from '../../services/blogWriterApi';
+
+interface Props {
+ analysis?: BlogSEOAnalyzeResponse | null;
+}
+
+const SEOMiniPanel: React.FC = ({ analysis }) => {
+ if (!analysis) return null;
+ return (
+
+
SEO Mini Panel
+
Score: {analysis.seo_score}
+ {!!analysis.recommendations?.length && (
+
+ {analysis.recommendations.slice(0, 3).map((r, i) => ({r} ))}
+
+ )}
+
+ );
+};
+
+export default SEOMiniPanel;
+
+
diff --git a/frontend/src/components/BlogWriter/TitleSelector.tsx b/frontend/src/components/BlogWriter/TitleSelector.tsx
new file mode 100644
index 00000000..8102d34e
--- /dev/null
+++ b/frontend/src/components/BlogWriter/TitleSelector.tsx
@@ -0,0 +1,195 @@
+import React, { useState } from 'react';
+
+interface TitleSelectorProps {
+ titleOptions: string[];
+ selectedTitle?: string;
+ onTitleSelect: (title: string) => void;
+ onCustomTitle?: (title: string) => void;
+}
+
+const TitleSelector: React.FC = ({
+ titleOptions,
+ selectedTitle,
+ onTitleSelect,
+ onCustomTitle
+}) => {
+ const [showCustomInput, setShowCustomInput] = useState(false);
+ const [customTitle, setCustomTitle] = useState('');
+
+ const handleCustomTitleSubmit = () => {
+ if (customTitle.trim() && onCustomTitle) {
+ onCustomTitle(customTitle.trim());
+ setCustomTitle('');
+ setShowCustomInput(false);
+ }
+ };
+
+ return (
+
+
+ ๐ Choose Your Blog Title
+
+
+ Select from AI-generated options or create your own custom title.
+
+
+ {/* AI-Generated Title Options */}
+
+
+ AI-Generated Options
+
+
+ {titleOptions.map((title, index) => (
+
onTitleSelect(title)}
+ style={{
+ padding: '12px 16px',
+ border: selectedTitle === title ? '2px solid #1976d2' : '1px solid #e0e0e0',
+ borderRadius: '8px',
+ cursor: 'pointer',
+ backgroundColor: selectedTitle === title ? '#f0f8ff' : 'white',
+ transition: 'all 0.2s ease',
+ fontSize: '14px',
+ color: '#333'
+ }}
+ onMouseEnter={(e) => {
+ if (selectedTitle !== title) {
+ e.currentTarget.style.backgroundColor = '#f8f9fa';
+ e.currentTarget.style.borderColor = '#1976d2';
+ }
+ }}
+ onMouseLeave={(e) => {
+ if (selectedTitle !== title) {
+ e.currentTarget.style.backgroundColor = 'white';
+ e.currentTarget.style.borderColor = '#e0e0e0';
+ }
+ }}
+ >
+
+ {selectedTitle === title && (
+ โ
+ )}
+
+ {title}
+
+
+
+ ))}
+
+
+
+ {/* Custom Title Input */}
+
+
+ Custom Title
+
+
+ {!showCustomInput ? (
+
setShowCustomInput(true)}
+ style={{
+ backgroundColor: 'transparent',
+ border: '1px dashed #1976d2',
+ borderRadius: '8px',
+ padding: '12px 16px',
+ cursor: 'pointer',
+ fontSize: '14px',
+ color: '#1976d2',
+ width: '100%',
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ gap: '8px'
+ }}
+ >
+ โ๏ธ Create Custom Title
+
+ ) : (
+
+ setCustomTitle(e.target.value)}
+ placeholder="Enter your custom title..."
+ style={{
+ flex: 1,
+ padding: '12px 16px',
+ border: '1px solid #e0e0e0',
+ borderRadius: '8px',
+ fontSize: '14px',
+ outline: 'none'
+ }}
+ onKeyPress={(e) => {
+ if (e.key === 'Enter') {
+ handleCustomTitleSubmit();
+ }
+ }}
+ autoFocus
+ />
+
+ Add
+
+ {
+ setShowCustomInput(false);
+ setCustomTitle('');
+ }}
+ style={{
+ backgroundColor: 'transparent',
+ border: '1px solid #e0e0e0',
+ borderRadius: '8px',
+ padding: '12px 16px',
+ cursor: 'pointer',
+ fontSize: '14px',
+ color: '#666'
+ }}
+ >
+ Cancel
+
+
+ )}
+
+
+ {/* Title Tips */}
+
+
+ ๐ก Title Tips
+
+
+ Keep it under 60 characters for better SEO
+ Include your primary keyword naturally
+ Make it compelling and click-worthy
+ Consider your target audience
+
+
+
+ );
+};
+
+export default TitleSelector;
diff --git a/frontend/src/services/blogWriterApi.ts b/frontend/src/services/blogWriterApi.ts
new file mode 100644
index 00000000..735c13b8
--- /dev/null
+++ b/frontend/src/services/blogWriterApi.ts
@@ -0,0 +1,124 @@
+import { apiClient } from "../api/client";
+
+export interface PersonaInfo {
+ persona_id?: string;
+ tone?: string;
+ audience?: string;
+ industry?: string;
+}
+
+export interface ResearchSource {
+ title: string;
+ url: string;
+ excerpt?: string;
+ credibility_score?: number;
+ published_at?: string;
+}
+
+export interface BlogResearchRequest {
+ keywords: string[];
+ topic?: string;
+ industry?: string;
+ target_audience?: string;
+ tone?: string;
+ word_count_target?: number;
+ persona?: PersonaInfo;
+}
+
+export interface BlogResearchResponse {
+ success: boolean;
+ sources: ResearchSource[];
+ keyword_analysis: Record;
+ competitor_analysis: Record;
+ suggested_angles: string[];
+ search_widget?: string;
+ search_queries?: string[];
+}
+
+export interface BlogOutlineSection {
+ id: string;
+ heading: string;
+ subheadings: string[];
+ key_points: string[];
+ references: ResearchSource[];
+ target_words?: number;
+ keywords: string[];
+}
+
+export interface BlogOutlineResponse {
+ success: boolean;
+ title_options: string[];
+ outline: BlogOutlineSection[];
+}
+
+export interface BlogSectionResponse {
+ success: boolean;
+ markdown: string;
+ citations: ResearchSource[];
+}
+
+export interface BlogSEOAnalyzeResponse {
+ success: boolean;
+ seo_score: number;
+ density: Record;
+ structure: Record;
+ readability: Record;
+ link_suggestions: any[];
+ image_alt_status: Record;
+ recommendations: string[];
+}
+
+export interface BlogSEOMetadataResponse {
+ success: boolean;
+ title_options: string[];
+ meta_descriptions: string[];
+ open_graph: Record;
+ twitter_card: Record;
+ schema: Record;
+}
+
+export interface BlogPublishResponse {
+ success: boolean;
+ platform: string;
+ url?: string;
+ post_id?: string;
+}
+
+export const blogWriterApi = {
+ async research(payload: BlogResearchRequest): Promise {
+ const { data } = await apiClient.post("/api/blog/research", payload);
+ return data;
+ },
+
+ async generateOutline(payload: { research: BlogResearchResponse; persona?: PersonaInfo; word_count?: number }): Promise {
+ const { data } = await apiClient.post("/api/blog/outline/generate", payload);
+ return data;
+ },
+
+ async refineOutline(payload: { outline: BlogOutlineSection[]; operation: string; section_id?: string; payload?: any }): Promise {
+ const { data } = await apiClient.post("/api/blog/outline/refine", payload);
+ return data;
+ },
+
+ async generateSection(payload: { section: BlogOutlineSection; keywords?: string[]; tone?: string; persona?: PersonaInfo }): Promise {
+ const { data } = await apiClient.post("/api/blog/section/generate", payload);
+ return data;
+ },
+
+ async seoAnalyze(payload: { content: string; keywords?: string[] }): Promise {
+ const { data } = await apiClient.post("/api/blog/seo/analyze", payload);
+ return data;
+ },
+
+ async seoMetadata(payload: { content: string; title?: string; keywords?: string[] }): Promise {
+ const { data } = await apiClient.post("/api/blog/seo/metadata", payload);
+ return data;
+ },
+
+ async publish(payload: { platform: 'wix' | 'wordpress'; html: string; metadata: BlogSEOMetadataResponse; schedule_time?: string }): Promise {
+ const { data } = await apiClient.post("/api/blog/publish", payload);
+ return data;
+ }
+};
+
+
diff --git a/test_research.json b/test_research.json
new file mode 100644
index 00000000..5cd3223a
--- /dev/null
+++ b/test_research.json
@@ -0,0 +1,6 @@
+{
+ "keywords": ["AI content generation", "blog writing"],
+ "topic": "ALwrity content generation",
+ "industry": "Technology",
+ "target_audience": "content creators"
+}
diff --git a/test_research.py b/test_research.py
new file mode 100644
index 00000000..e2f18301
--- /dev/null
+++ b/test_research.py
@@ -0,0 +1,58 @@
+import requests
+import json
+
+# Test the research endpoint
+url = "http://localhost:8000/api/blog/research"
+payload = {
+ "keywords": ["AI content generation", "blog writing"],
+ "topic": "ALwrity content generation",
+ "industry": "Technology",
+ "target_audience": "content creators"
+}
+
+try:
+ response = requests.post(url, json=payload)
+ print(f"Status Code: {response.status_code}")
+ print(f"Response Headers: {dict(response.headers)}")
+
+ if response.status_code == 200:
+ data = response.json()
+ print("\n=== RESEARCH RESPONSE ===")
+ print(f"Success: {data.get('success')}")
+ print(f"Sources Count: {len(data.get('sources', []))}")
+ print(f"Search Queries Count: {len(data.get('search_queries', []))}")
+ print(f"Has Search Widget: {bool(data.get('search_widget'))}")
+ print(f"Suggested Angles Count: {len(data.get('suggested_angles', []))}")
+
+ print("\n=== SOURCES ===")
+ for i, source in enumerate(data.get('sources', [])[:3]):
+ print(f"Source {i+1}: {source.get('title', 'No title')}")
+ print(f" URL: {source.get('url', 'No URL')}")
+ print(f" Type: {source.get('type', 'Unknown')}")
+
+ print("\n=== SEARCH QUERIES (First 5) ===")
+ for i, query in enumerate(data.get('search_queries', [])[:5]):
+ print(f"{i+1}. {query}")
+
+ print("\n=== SUGGESTED ANGLES ===")
+ for i, angle in enumerate(data.get('suggested_angles', [])[:3]):
+ print(f"{i+1}. {angle}")
+
+ print("\n=== KEYWORD ANALYSIS ===")
+ kw_analysis = data.get('keyword_analysis', {})
+ print(f"Primary: {kw_analysis.get('primary', [])}")
+ print(f"Secondary: {kw_analysis.get('secondary', [])}")
+ print(f"Search Intent: {kw_analysis.get('search_intent', 'Unknown')}")
+
+ print("\n=== SEARCH WIDGET (First 200 chars) ===")
+ widget = data.get('search_widget', '')
+ if widget:
+ print(widget[:200] + "..." if len(widget) > 200 else widget)
+ else:
+ print("No search widget provided")
+
+ else:
+ print(f"Error: {response.text}")
+
+except Exception as e:
+ print(f"Request failed: {e}")