sushilideaclan01 commited on
Commit
f201243
·
1 Parent(s): 3ea0e86

Initial commit of the Ad Generator Lite project, including backend services, frontend components, and configuration files. Added core functionalities for ad generation, user management, and image processing, along with a structured matrix system for ad testing.

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitignore +129 -0
  2. Dockerfile +32 -0
  3. README.md +154 -10
  4. config.py +70 -0
  5. create_user.py +185 -0
  6. data/__init__.py +2 -0
  7. data/angles.py +297 -0
  8. data/concepts.py +282 -0
  9. data/containers.py +434 -0
  10. data/frameworks.py +393 -0
  11. data/glp1.py +683 -0
  12. data/home_insurance.py +741 -0
  13. data/hooks.py +156 -0
  14. data/triggers.py +141 -0
  15. data/visuals.py +126 -0
  16. frontend/.gitignore +41 -0
  17. frontend/README.md +144 -0
  18. frontend/app/favicon.ico +0 -0
  19. frontend/app/gallery/[id]/page.tsx +512 -0
  20. frontend/app/gallery/page.tsx +190 -0
  21. frontend/app/generate/batch/page.tsx +151 -0
  22. frontend/app/generate/matrix/page.tsx +198 -0
  23. frontend/app/generate/page.tsx +563 -0
  24. frontend/app/globals.css +244 -0
  25. frontend/app/layout.tsx +33 -0
  26. frontend/app/login/page.tsx +128 -0
  27. frontend/app/matrix/angles/page.tsx +134 -0
  28. frontend/app/matrix/concepts/page.tsx +132 -0
  29. frontend/app/matrix/page.tsx +107 -0
  30. frontend/app/matrix/testing/page.tsx +30 -0
  31. frontend/app/page.tsx +270 -0
  32. frontend/components/gallery/AdCard.tsx +108 -0
  33. frontend/components/gallery/FilterBar.tsx +80 -0
  34. frontend/components/gallery/GalleryGrid.tsx +50 -0
  35. frontend/components/generation/AdPreview.tsx +357 -0
  36. frontend/components/generation/BatchForm.tsx +139 -0
  37. frontend/components/generation/CorrectionModal.tsx +352 -0
  38. frontend/components/generation/ExtensiveForm.tsx +166 -0
  39. frontend/components/generation/GenerationForm.tsx +105 -0
  40. frontend/components/generation/GenerationProgress.tsx +217 -0
  41. frontend/components/layout/ConditionalHeader.tsx +16 -0
  42. frontend/components/layout/Header.tsx +100 -0
  43. frontend/components/matrix/AngleSelector.tsx +133 -0
  44. frontend/components/matrix/ConceptSelector.tsx +208 -0
  45. frontend/components/matrix/TestingMatrixBuilder.tsx +436 -0
  46. frontend/components/ui/Button.tsx +57 -0
  47. frontend/components/ui/Card.tsx +79 -0
  48. frontend/components/ui/Input.tsx +51 -0
  49. frontend/components/ui/LoadingSkeleton.tsx +36 -0
  50. frontend/components/ui/LoadingSpinner.tsx +54 -0
.gitignore ADDED
@@ -0,0 +1,129 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ .Python
7
+ build/
8
+ develop-eggs/
9
+ dist/
10
+ downloads/
11
+ eggs/
12
+ .eggs/
13
+ lib/
14
+ lib64/
15
+ parts/
16
+ sdist/
17
+ var/
18
+ wheels/
19
+ share/python-wheels/
20
+ *.egg-info/
21
+ .installed.cfg
22
+ *.egg
23
+ MANIFEST
24
+
25
+ # Virtual Environment
26
+ venv/
27
+ env/
28
+ ENV/
29
+ env.bak/
30
+ venv.bak/
31
+ .venv
32
+
33
+ # Environment variables
34
+ .env
35
+ .env.local
36
+ .env.*.local
37
+ .env.production
38
+ .env.development
39
+
40
+ # IDE / Editors
41
+ .vscode/
42
+ .idea/
43
+ *.swp
44
+ *.swo
45
+ *~
46
+ .DS_Store
47
+ *.sublime-project
48
+ *.sublime-workspace
49
+
50
+ # Jupyter Notebook
51
+ .ipynb_checkpoints
52
+
53
+ # pytest
54
+ .pytest_cache/
55
+ .coverage
56
+ htmlcov/
57
+ .tox/
58
+ .hypothesis/
59
+
60
+ # mypy
61
+ .mypy_cache/
62
+ .dmypy.json
63
+ dmypy.json
64
+
65
+ # Pyre type checker
66
+ .pyre/
67
+
68
+ # pytype static type analyzer
69
+ .pytype/
70
+
71
+ # Node.js / Next.js (root level)
72
+ node_modules/
73
+ npm-debug.log*
74
+ yarn-debug.log*
75
+ yarn-error.log*
76
+ .pnpm-debug.log*
77
+
78
+ # Next.js (if built at root)
79
+ .next/
80
+ out/
81
+ build/
82
+ .vercel
83
+
84
+ # Generated assets
85
+ assets/generated/*
86
+ !assets/generated/.gitkeep
87
+
88
+ # Database
89
+ *.db
90
+ *.sqlite
91
+ *.sqlite3
92
+
93
+ # Logs
94
+ *.log
95
+ logs/
96
+ *.log.*
97
+
98
+ # OS
99
+ .DS_Store
100
+ .DS_Store?
101
+ ._*
102
+ .Spotlight-V100
103
+ .Trashes
104
+ ehthumbs.db
105
+ Thumbs.db
106
+
107
+ # Temporary files
108
+ *.tmp
109
+ *.temp
110
+ *.bak
111
+ *.swp
112
+ *~
113
+
114
+ # Docker
115
+ .dockerignore
116
+
117
+ # Testing
118
+ .pytest_cache/
119
+ .coverage
120
+ htmlcov/
121
+ .tox/
122
+
123
+ # TypeScript
124
+ *.tsbuildinfo
125
+
126
+ # Misc
127
+ *.pem
128
+ .cache/
129
+ .temp/
Dockerfile ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Use Python 3.11 slim image
2
+ FROM python:3.11-slim
3
+
4
+ # Set working directory
5
+ WORKDIR /app
6
+
7
+ # Install system dependencies
8
+ RUN apt-get update && apt-get install -y \
9
+ gcc \
10
+ && rm -rf /var/lib/apt/lists/*
11
+
12
+ # Copy requirements first for better caching
13
+ COPY requirements.txt .
14
+
15
+ # Install Python dependencies
16
+ RUN pip install --no-cache-dir -r requirements.txt
17
+
18
+ # Copy application code
19
+ COPY . .
20
+
21
+ # Create output directory for generated images
22
+ RUN mkdir -p assets/generated
23
+
24
+ # Expose port (Hugging Face Spaces uses port 7860 by default, but we'll use 8000)
25
+ EXPOSE 8000
26
+
27
+ # Set environment variables
28
+ ENV PYTHONUNBUFFERED=1
29
+ ENV PORT=8000
30
+
31
+ # Run the application
32
+ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
README.md CHANGED
@@ -1,10 +1,154 @@
1
- ---
2
- title: Creative Breakthrough
3
- emoji: 😻
4
- colorFrom: green
5
- colorTo: blue
6
- sdk: docker
7
- pinned: false
8
- ---
9
-
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Ad Generator Lite
2
+
3
+ Generate high-converting ad creatives for Home Insurance and GLP-1 niches using psychological triggers and AI-powered image generation.
4
+
5
+ ## Features
6
+
7
+ - **Multiple Generation Modes**:
8
+ - Standard generation with randomization
9
+ - Batch generation for multiple ads
10
+ - Angle × Concept matrix system (100 angles × 100 concepts)
11
+ - Extensive generation with researcher → creative director → designer → copywriter flow
12
+
13
+ - **Image Generation**: Supports multiple models (z-image-turbo, nano-banana, nano-banana-pro, imagen-4-ultra, recraft-v3, ideogram-v3, photon, seedream-3)
14
+
15
+ - **Image Correction**: AI-powered image correction for spelling mistakes and visual issues
16
+
17
+ - **Database Storage**: MongoDB integration for storing generated ads
18
+
19
+ - **Authentication**: JWT-based authentication system
20
+
21
+ ## Setup
22
+
23
+ ### Environment Variables
24
+
25
+ Copy `.env.example` to `.env` and fill in your API keys:
26
+
27
+ ```bash
28
+ cp .env.example .env
29
+ ```
30
+
31
+ Required variables:
32
+ - `OPENAI_API_KEY`: Your OpenAI API key
33
+ - `REPLICATE_API_TOKEN`: Your Replicate API token
34
+ - `JWT_SECRET_KEY`: Secret key for JWT tokens (change in production!)
35
+
36
+ Optional variables:
37
+ - `MONGODB_URL`: MongoDB connection string (for database features)
38
+ - R2 Storage credentials (for cloud image storage)
39
+
40
+ ### Running Locally
41
+
42
+ 1. Install dependencies:
43
+ ```bash
44
+ pip install -r requirements.txt
45
+ ```
46
+
47
+ 2. Run the server:
48
+ ```bash
49
+ uvicorn main:app --reload
50
+ ```
51
+
52
+ 3. Access the API at `http://localhost:8000`
53
+
54
+ ### API Documentation
55
+
56
+ Once running, visit:
57
+ - API docs: `http://localhost:8000/docs`
58
+ - Alternative docs: `http://localhost:8000/redoc`
59
+
60
+ ## Deployment on Hugging Face Spaces
61
+
62
+ This app is configured for deployment on Hugging Face Spaces using Docker.
63
+
64
+ ### Steps to Deploy
65
+
66
+ 1. **Create a new Space** on Hugging Face:
67
+ - Go to https://huggingface.co/spaces
68
+ - Click "Create new Space"
69
+ - Choose "Docker" as the SDK
70
+ - Name your space (e.g., `your-username/ad-generator-lite`)
71
+
72
+ 2. **Push your code** to the Space:
73
+ ```bash
74
+ git clone https://huggingface.co/spaces/your-username/ad-generator-lite
75
+ cd ad-generator-lite
76
+ # Copy your files here
77
+ git add .
78
+ git commit -m "Initial commit"
79
+ git push
80
+ ```
81
+
82
+ 3. **Set Environment Variables**:
83
+ - Go to your Space settings
84
+ - Navigate to "Variables and secrets"
85
+ - Add all required environment variables from `.env.example`
86
+ - **Important**: Set `JWT_SECRET_KEY` to a secure random string
87
+
88
+ 4. **Wait for Build**:
89
+ - Hugging Face will automatically build and deploy your Docker container
90
+ - Check the "Logs" tab for build progress
91
+
92
+ ### Space Configuration
93
+
94
+ The `huggingface.yml` file configures:
95
+ - Docker-based deployment
96
+ - Port 8000 for the FastAPI app
97
+ - Health check endpoint
98
+
99
+ ### Accessing Your Deployed App
100
+
101
+ Once deployed, your app will be available at:
102
+ ```
103
+ https://your-username-ad-generator-lite.hf.space
104
+ ```
105
+
106
+ ## API Endpoints
107
+
108
+ ### Authentication
109
+ - `POST /auth/login` - Login and get JWT token
110
+
111
+ ### Generation
112
+ - `POST /generate` - Generate single ad (requires auth)
113
+ - `POST /generate/batch` - Generate multiple ads (requires auth)
114
+ - `POST /matrix/generate` - Generate using Angle × Concept matrix (requires auth)
115
+ - `POST /matrix/testing` - Generate testing matrix
116
+ - `POST /extensive/generate` - Extensive generation flow (requires auth)
117
+
118
+ ### Matrix System
119
+ - `GET /matrix/angles` - List all 100 angles
120
+ - `GET /matrix/concepts` - List all 100 concepts
121
+ - `GET /matrix/angle/{key}` - Get specific angle details
122
+ - `GET /matrix/concept/{key}` - Get specific concept details
123
+ - `GET /matrix/compatible/{angle_key}` - Get compatible concepts
124
+
125
+ ### Image Correction
126
+ - `POST /api/correct` - Correct image for spelling/visual issues (requires auth)
127
+
128
+ ### Database
129
+ - `GET /db/stats` - Get database statistics (requires auth)
130
+ - `GET /db/ads` - List stored ads (requires auth)
131
+ - `GET /db/ad/{ad_id}` - Get specific ad
132
+ - `DELETE /db/ad/{ad_id}` - Delete ad (requires auth)
133
+
134
+ ### Health
135
+ - `GET /health` - Health check
136
+ - `GET /` - API information
137
+
138
+ ## Supported Niches
139
+
140
+ - `home_insurance`: Fear, urgency, savings, authority, guilt strategies
141
+ - `glp1`: Shame, transformation, FOMO, authority, simplicity strategies
142
+
143
+ ## Matrix System
144
+
145
+ The app includes a systematic Angle × Concept matrix:
146
+ - **100 Angles**: Psychological triggers (10 categories)
147
+ - **100 Concepts**: Visual approaches (10 categories)
148
+ - **10,000 possible combinations**
149
+
150
+ Formula: 1 Offer → 5-8 Angles → 3-5 Concepts per angle
151
+
152
+ ## License
153
+
154
+ [Add your license here]
config.py ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Configuration settings for the ad generator."""
2
+
3
+ from pydantic_settings import BaseSettings, SettingsConfigDict
4
+ from typing import Optional
5
+
6
+
7
+ class Settings(BaseSettings):
8
+ """Application settings loaded from environment variables."""
9
+
10
+ model_config = SettingsConfigDict(
11
+ env_file=".env",
12
+ env_file_encoding="utf-8",
13
+ case_sensitive=False,
14
+ extra="ignore",
15
+ )
16
+
17
+ # OpenAI API Key (required for copy generation)
18
+ openai_api_key: str
19
+
20
+ # Replicate API Token (required for image generation)
21
+ replicate_api_token: str
22
+
23
+ # Database (MongoDB)
24
+ mongodb_url: Optional[str] = None
25
+ mongodb_db_name: str = "creative_breakthrough"
26
+
27
+ # R2 Storage (Cloudflare R2)
28
+ r2_endpoint: Optional[str] = None
29
+ r2_bucket_name: Optional[str] = None
30
+ r2_access_key: Optional[str] = None
31
+ r2_secret_key: Optional[str] = None
32
+ r2_public_domain: Optional[str] = None # Optional: Custom domain for public URLs (e.g., "cdn.example.com")
33
+
34
+ # LLM Settings
35
+ llm_model: str = "gpt-4o-mini" # Cost-effective model
36
+ llm_temperature: float = 0.95 # High for variety
37
+
38
+ # Vision API Settings
39
+ vision_model: str = "gpt-4o" # Vision-capable model for image analysis
40
+
41
+ # Third Flow (Extensive) GPT Model Settings
42
+ third_flow_model: str = "gpt-4o" # Model for researcher, creative_director, designer, copywriter
43
+ # Options: "gpt-4o", "gpt-4o-mini", "gpt-4-turbo", "gpt-4"
44
+
45
+ # Image Generation Settings (same models as creative-breakthrough project)
46
+ image_model: str = "z-image-turbo" # Z-Image Turbo - fast and high quality
47
+ # Alternative models: "nano-banana", "nano-banana-pro", "imagen-4-ultra", "recraft-v3", "ideogram-v3", "photon", "seedream-3", "gpt-image-1.5"
48
+ image_width: int = 1024
49
+ image_height: int = 1024
50
+
51
+ # Output Settings
52
+ output_dir: str = "assets/generated"
53
+
54
+ # Production & Storage Settings
55
+ environment: str = "development" # "development" or "production"
56
+ save_images_locally: bool = True # Whether to save images locally
57
+ local_image_retention_hours: int = 24 # Hours to keep images locally before cleanup (only in production)
58
+
59
+ # Auth Settings
60
+ jwt_secret_key: str = "your-secret-key-change-in-production" # Change this in production!
61
+ jwt_algorithm: str = "HS256"
62
+ jwt_expiration_hours: int = 24
63
+
64
+ # Debug
65
+ debug: bool = False
66
+
67
+
68
+ # Global settings instance
69
+ settings = Settings()
70
+
create_user.py ADDED
@@ -0,0 +1,185 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ CLI script to create users manually.
4
+ This script is for backend-only user creation (not exposed in frontend).
5
+
6
+ Usage:
7
+ python create_user.py <username> <password>
8
+
9
+ Example:
10
+ python create_user.py admin mypassword123
11
+ """
12
+
13
+ import asyncio
14
+ import sys
15
+ import os
16
+
17
+ # Add current directory to path
18
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
19
+
20
+ from services.database import db_service
21
+ from services.auth import auth_service
22
+
23
+
24
+ async def create_user(username: str, password: str):
25
+ """Create a new user."""
26
+ # Connect to database
27
+ print("Connecting to database...")
28
+ connected = await db_service.connect()
29
+ if not connected:
30
+ print("❌ Failed to connect to database. Please check your MONGODB_URL configuration.")
31
+ return False
32
+
33
+ try:
34
+ # Check if user already exists
35
+ existing_user = await db_service.get_user(username)
36
+ if existing_user:
37
+ print(f"❌ User '{username}' already exists!")
38
+ return False
39
+
40
+ # Hash password
41
+ print(f"Creating user '{username}'...")
42
+ hashed_password = auth_service.hash_password(password)
43
+
44
+ # Create user
45
+ user_id = await db_service.create_user(username, hashed_password)
46
+
47
+ if user_id:
48
+ print(f"✅ User '{username}' created successfully!")
49
+ print(f" User ID: {user_id}")
50
+ print(f"\nYou can now login with:")
51
+ print(f" Username: {username}")
52
+ print(f" Password: {password}")
53
+ return True
54
+ else:
55
+ print(f"❌ Failed to create user '{username}'")
56
+ return False
57
+
58
+ except Exception as e:
59
+ print(f"❌ Error creating user: {e}")
60
+ return False
61
+
62
+ finally:
63
+ await db_service.disconnect()
64
+
65
+
66
+ async def list_users():
67
+ """List all users."""
68
+ print("Connecting to database...")
69
+ connected = await db_service.connect()
70
+ if not connected:
71
+ print("❌ Failed to connect to database. Please check your MONGODB_URL configuration.")
72
+ return
73
+
74
+ try:
75
+ users = await db_service.list_users()
76
+ if not users:
77
+ print("No users found.")
78
+ else:
79
+ print(f"\nFound {len(users)} user(s):\n")
80
+ for user in users:
81
+ print(f" - {user['username']}")
82
+ print(f" Created: {user.get('created_at', 'N/A')}")
83
+ print()
84
+
85
+ except Exception as e:
86
+ print(f"❌ Error listing users: {e}")
87
+
88
+ finally:
89
+ await db_service.disconnect()
90
+
91
+
92
+ async def delete_user(username: str):
93
+ """Delete a user."""
94
+ print("Connecting to database...")
95
+ connected = await db_service.connect()
96
+ if not connected:
97
+ print("❌ Failed to connect to database. Please check your MONGODB_URL configuration.")
98
+ return False
99
+
100
+ try:
101
+ # Confirm deletion
102
+ confirm = input(f"Are you sure you want to delete user '{username}'? (yes/no): ")
103
+ if confirm.lower() != "yes":
104
+ print("Cancelled.")
105
+ return False
106
+
107
+ deleted = await db_service.delete_user(username)
108
+ if deleted:
109
+ print(f"✅ User '{username}' deleted successfully!")
110
+ return True
111
+ else:
112
+ print(f"❌ User '{username}' not found or could not be deleted.")
113
+ return False
114
+
115
+ except Exception as e:
116
+ print(f"❌ Error deleting user: {e}")
117
+ return False
118
+
119
+ finally:
120
+ await db_service.disconnect()
121
+
122
+
123
+ def print_usage():
124
+ """Print usage instructions."""
125
+ print("""
126
+ User Management Script
127
+
128
+ Usage:
129
+ python create_user.py create <username> <password> - Create a new user
130
+ python create_user.py list - List all users
131
+ python create_user.py delete <username> - Delete a user
132
+
133
+ Examples:
134
+ python create_user.py create admin mypassword123
135
+ python create_user.py list
136
+ python create_user.py delete admin
137
+ """)
138
+
139
+
140
+ async def main():
141
+ """Main entry point."""
142
+ if len(sys.argv) < 2:
143
+ print_usage()
144
+ return
145
+
146
+ command = sys.argv[1].lower()
147
+
148
+ if command == "create":
149
+ if len(sys.argv) != 4:
150
+ print("❌ Error: Username and password required")
151
+ print("Usage: python create_user.py create <username> <password>")
152
+ return
153
+
154
+ username = sys.argv[2]
155
+ password = sys.argv[3]
156
+
157
+ if len(username) < 3:
158
+ print("❌ Error: Username must be at least 3 characters")
159
+ return
160
+
161
+ if len(password) < 6:
162
+ print("❌ Error: Password must be at least 6 characters")
163
+ return
164
+
165
+ await create_user(username, password)
166
+
167
+ elif command == "list":
168
+ await list_users()
169
+
170
+ elif command == "delete":
171
+ if len(sys.argv) != 3:
172
+ print("❌ Error: Username required")
173
+ print("Usage: python create_user.py delete <username>")
174
+ return
175
+
176
+ username = sys.argv[2]
177
+ await delete_user(username)
178
+
179
+ else:
180
+ print(f"❌ Unknown command: {command}")
181
+ print_usage()
182
+
183
+
184
+ if __name__ == "__main__":
185
+ asyncio.run(main())
data/__init__.py ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ # Data module - psychological triggers and patterns
2
+
data/angles.py ADDED
@@ -0,0 +1,297 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Affiliate Marketing Angles Framework (Lite Version)
3
+ 100 angles organized into 10 categories.
4
+ Angles answer: "Why should I care?" (Psychological WHY)
5
+ """
6
+
7
+ from typing import Dict, List, Any, Optional
8
+ from enum import Enum
9
+ import random
10
+
11
+
12
+ class AngleCategory(str, Enum):
13
+ """Angle categories."""
14
+ EMOTIONAL = "emotional"
15
+ FINANCIAL = "financial"
16
+ CONVENIENCE = "convenience"
17
+ IDENTITY = "identity"
18
+ AUTHORITY = "authority"
19
+ SOCIAL_PROOF = "social_proof"
20
+ URGENCY = "urgency"
21
+ INTELLECTUAL = "intellectual"
22
+ CURIOSITY = "curiosity"
23
+ PROBLEM_SOLUTION = "problem_solution"
24
+
25
+
26
+ # Complete angles framework - 100 angles (10 per category)
27
+ ANGLES = {
28
+ AngleCategory.EMOTIONAL: {
29
+ "name": "Emotional & Psychological",
30
+ "angles": [
31
+ {"key": "fear_loss", "name": "Fear / Loss Prevention", "trigger": "Fear", "example": "Don't lose your home to disaster"},
32
+ {"key": "anxiety_reduction", "name": "Anxiety Reduction", "trigger": "Relief", "example": "Sleep better knowing you're protected"},
33
+ {"key": "security_safety", "name": "Security / Safety", "trigger": "Security", "example": "Your family's safety is our priority"},
34
+ {"key": "peace_of_mind", "name": "Peace of Mind", "trigger": "Relief", "example": "Peace of mind for just $X/month"},
35
+ {"key": "stress_relief", "name": "Stress Relief", "trigger": "Relief", "example": "End the stress of [problem]"},
36
+ {"key": "relief_escape", "name": "Relief / Escape", "trigger": "Relief", "example": "Finally escape [problem]"},
37
+ {"key": "confidence_boost", "name": "Confidence Boost", "trigger": "Pride", "example": "Feel confident about your future"},
38
+ {"key": "hope_optimism", "name": "Hope / Optimism", "trigger": "Hope", "example": "A brighter future starts today"},
39
+ {"key": "guilt_responsibility", "name": "Guilt (Family)", "trigger": "Guilt", "example": "Do it for your family"},
40
+ {"key": "pride_self_worth", "name": "Pride / Self-worth", "trigger": "Pride", "example": "You deserve the best"},
41
+ {"key": "emotional_connection", "name": "Emotional Connection", "trigger": "Belonging", "example": "Feel connected to what matters"},
42
+ {"key": "nostalgia", "name": "Nostalgia", "trigger": "Emotion", "example": "Remember when things were simpler"},
43
+ {"key": "empowerment", "name": "Empowerment", "trigger": "Pride", "example": "Take control of your future"},
44
+ ]
45
+ },
46
+ AngleCategory.FINANCIAL: {
47
+ "name": "Financial",
48
+ "angles": [
49
+ {"key": "save_money", "name": "Save Money", "trigger": "Greed", "example": "Save $600/year"},
50
+ {"key": "cut_hidden_costs", "name": "Cut Hidden Costs", "trigger": "Anger", "example": "Stop paying hidden fees"},
51
+ {"key": "avoid_overpaying", "name": "Avoid Overpaying", "trigger": "Anger", "example": "Stop overpaying for [service]"},
52
+ {"key": "financial_freedom", "name": "Financial Freedom", "trigger": "Desire", "example": "Achieve financial freedom"},
53
+ {"key": "budget_control", "name": "Budget Control", "trigger": "Control", "example": "Take control of your budget"},
54
+ {"key": "price_comparison", "name": "Price Comparison", "trigger": "Greed", "example": "Compare prices in 30 seconds"},
55
+ {"key": "smart_spending", "name": "Smart Spending", "trigger": "Pride", "example": "Make smart financial choices"},
56
+ {"key": "long_term_value", "name": "Long-Term Value", "trigger": "Greed", "example": "Invest in your future"},
57
+ {"key": "roi_investment", "name": "ROI / Investment", "trigger": "Greed", "example": "Get 3x return on investment"},
58
+ {"key": "cost_transparency", "name": "Cost Transparency", "trigger": "Trust", "example": "100% transparent pricing"},
59
+ {"key": "hidden_fees_exposed", "name": "Hidden Fees Exposed", "trigger": "Anger", "example": "No hidden fees, ever"},
60
+ {"key": "money_back", "name": "Money-Back Guarantee", "trigger": "Security", "example": "Get your money back if not satisfied"},
61
+ {"key": "payment_flexibility", "name": "Payment Flexibility", "trigger": "Convenience", "example": "Pay your way, when you want"},
62
+ ]
63
+ },
64
+ AngleCategory.CONVENIENCE: {
65
+ "name": "Convenience & Ease",
66
+ "angles": [
67
+ {"key": "fast_instant", "name": "Fast / Instant", "trigger": "Convenience", "example": "Get a quote in 30 seconds"},
68
+ {"key": "simple_easy", "name": "Simple / Easy", "trigger": "Convenience", "example": "It's that simple"},
69
+ {"key": "no_paperwork", "name": "No Paperwork", "trigger": "Convenience", "example": "No paperwork required"},
70
+ {"key": "no_phone_calls", "name": "No Phone Calls", "trigger": "Convenience", "example": "No phone calls needed"},
71
+ {"key": "one_click", "name": "One-Click / Few Steps", "trigger": "Convenience", "example": "Get started in 3 steps"},
72
+ {"key": "beginner_friendly", "name": "Beginner-Friendly", "trigger": "Security", "example": "Perfect for beginners"},
73
+ {"key": "done_for_you", "name": "Done-For-You", "trigger": "Convenience", "example": "We handle everything"},
74
+ {"key": "hassle_free", "name": "Hassle-Free", "trigger": "Convenience", "example": "100% hassle-free"},
75
+ {"key": "low_effort", "name": "Low Effort", "trigger": "Convenience", "example": "Minimal effort, maximum results"},
76
+ {"key": "time_saving", "name": "Time Saving", "trigger": "Convenience", "example": "Save 10 hours per week"},
77
+ {"key": "automated_process", "name": "Automated Process", "trigger": "Convenience", "example": "Fully automated, zero work"},
78
+ {"key": "instant_access", "name": "Instant Access", "trigger": "Convenience", "example": "Get instant access now"},
79
+ {"key": "no_waiting", "name": "No Waiting", "trigger": "Convenience", "example": "No waiting, start immediately"},
80
+ ]
81
+ },
82
+ AngleCategory.IDENTITY: {
83
+ "name": "Identity & Personalization",
84
+ "angles": [
85
+ {"key": "age_based", "name": "Age-Based", "trigger": "Personalization", "example": "Special rates for seniors 65+"},
86
+ {"key": "location_based", "name": "Location-Based", "trigger": "Personalization", "example": "Best rates in [location]"},
87
+ {"key": "profession_specific", "name": "Profession-Specific", "trigger": "Personalization", "example": "Special rates for teachers"},
88
+ {"key": "life_stage", "name": "Life Stage", "trigger": "Personalization", "example": "Perfect for new homeowners"},
89
+ {"key": "lifestyle_match", "name": "Lifestyle Match", "trigger": "Personalization", "example": "Fits your lifestyle"},
90
+ {"key": "people_like_you", "name": "People Like You", "trigger": "Social Proof", "example": "Join thousands like you"},
91
+ {"key": "custom_fit", "name": "Custom Fit", "trigger": "Personalization", "example": "Customized just for you"},
92
+ {"key": "personal_relevance", "name": "Personal Relevance", "trigger": "Personalization", "example": "Built specifically for you"},
93
+ {"key": "niche_targeting", "name": "Niche Targeting", "trigger": "Personalization", "example": "Designed for [niche]"},
94
+ {"key": "localized_offer", "name": "Localized Offer", "trigger": "Personalization", "example": "Best deals in [city]"},
95
+ {"key": "behavioral_match", "name": "Behavioral Match", "trigger": "Personalization", "example": "Fits your lifestyle perfectly"},
96
+ {"key": "preference_based", "name": "Preference-Based", "trigger": "Personalization", "example": "Tailored to your preferences"},
97
+ {"key": "demographic_specific", "name": "Demographic-Specific", "trigger": "Personalization", "example": "Made for people like you"},
98
+ ]
99
+ },
100
+ AngleCategory.AUTHORITY: {
101
+ "name": "Authority & Trust",
102
+ "angles": [
103
+ {"key": "expert_backed", "name": "Expert-Backed", "trigger": "Authority", "example": "Recommended by experts"},
104
+ {"key": "industry_standard", "name": "Industry Standard", "trigger": "Authority", "example": "The industry standard"},
105
+ {"key": "government_related", "name": "Government-Related", "trigger": "Authority", "example": "Government-approved program"},
106
+ {"key": "trusted_millions", "name": "Trusted by Millions", "trigger": "Social Proof", "example": "Trusted by 2M+ customers"},
107
+ {"key": "years_experience", "name": "Years of Experience", "trigger": "Authority", "example": "20+ years of experience"},
108
+ {"key": "certified_verified", "name": "Certified / Verified", "trigger": "Authority", "example": "Certified and verified"},
109
+ {"key": "brand_reputation", "name": "Brand Reputation", "trigger": "Authority", "example": "Trusted brand name"},
110
+ {"key": "compliance", "name": "Compliance / Regulation", "trigger": "Authority", "example": "Fully compliant and regulated"},
111
+ {"key": "awards_recognition", "name": "Awards / Recognition", "trigger": "Authority", "example": "Award-winning service"},
112
+ {"key": "risk_free", "name": "Risk-Free", "trigger": "Security", "example": "100% risk-free guarantee"},
113
+ {"key": "industry_leader", "name": "Industry Leader", "trigger": "Authority", "example": "The industry leader"},
114
+ {"key": "proven_track_record", "name": "Proven Track Record", "trigger": "Authority", "example": "Proven results over decades"},
115
+ ]
116
+ },
117
+ AngleCategory.SOCIAL_PROOF: {
118
+ "name": "Social Proof & Validation",
119
+ "angles": [
120
+ {"key": "testimonials", "name": "Testimonials", "trigger": "Social Proof", "example": "See what customers say"},
121
+ {"key": "reviews_ratings", "name": "Reviews / Ratings", "trigger": "Social Proof", "example": "4.8/5 stars from 10K+ reviews"},
122
+ {"key": "mass_adoption", "name": "Mass Adoption", "trigger": "Social Proof", "example": "Join 2M+ users"},
123
+ {"key": "case_studies", "name": "Case Studies", "trigger": "Social Proof", "example": "See real success stories"},
124
+ {"key": "word_of_mouth", "name": "Word of Mouth", "trigger": "Social Proof", "example": "Recommended by friends"},
125
+ {"key": "community_trust", "name": "Community Trust", "trigger": "Social Proof", "example": "Trusted by our community"},
126
+ {"key": "real_stories", "name": "Real Stories", "trigger": "Social Proof", "example": "Real stories from real customers"},
127
+ {"key": "viral_popularity", "name": "Viral Popularity", "trigger": "FOMO", "example": "Going viral on [platform]"},
128
+ {"key": "trending_now", "name": "Trending Now", "trigger": "FOMO", "example": "Trending now"},
129
+ {"key": "most_chosen", "name": "Most Chosen", "trigger": "Social Proof", "example": "The most chosen option"},
130
+ {"key": "peer_recommendation", "name": "Peer Recommendation", "trigger": "Social Proof", "example": "Recommended by your peers"},
131
+ {"key": "success_rate", "name": "High Success Rate", "trigger": "Social Proof", "example": "95% success rate"},
132
+ ]
133
+ },
134
+ AngleCategory.URGENCY: {
135
+ "name": "Urgency & Scarcity",
136
+ "angles": [
137
+ {"key": "limited_time", "name": "Limited Time", "trigger": "FOMO", "example": "Limited time offer - ends Friday"},
138
+ {"key": "ending_soon", "name": "Ending Soon", "trigger": "FOMO", "example": "Offer ending in 48 hours"},
139
+ {"key": "price_increase", "name": "Price Increase Warning", "trigger": "FOMO", "example": "Prices increasing on [date]"},
140
+ {"key": "seasonal_change", "name": "Seasonal Change", "trigger": "FOMO", "example": "Spring special - ends soon"},
141
+ {"key": "renewal_reminder", "name": "Policy Renewal", "trigger": "FOMO", "example": "Renew before [date] to save"},
142
+ {"key": "countdown", "name": "Countdown", "trigger": "FOMO", "example": "Only 24 hours left"},
143
+ {"key": "last_chance", "name": "Last Chance", "trigger": "FOMO", "example": "Last chance to save"},
144
+ {"key": "market_shift", "name": "Market Shift", "trigger": "FOMO", "example": "Market rates changing soon"},
145
+ {"key": "deadline_pressure", "name": "Deadline Pressure", "trigger": "FOMO", "example": "Deadline: [date]"},
146
+ {"key": "miss_out_avoidance", "name": "Miss-Out Avoidance", "trigger": "FOMO", "example": "Don't miss this opportunity"},
147
+ {"key": "early_bird", "name": "Early Bird Special", "trigger": "FOMO", "example": "Early bird pricing ends soon"},
148
+ {"key": "flash_sale", "name": "Flash Sale", "trigger": "FOMO", "example": "Flash sale - 24 hours only"},
149
+ ]
150
+ },
151
+ AngleCategory.INTELLECTUAL: {
152
+ "name": "Intellectual / Smart Choice",
153
+ "angles": [
154
+ {"key": "insider_knowledge", "name": "Insider Knowledge", "trigger": "Curiosity", "example": "Insider tip: [secret]"},
155
+ {"key": "avoid_mistakes", "name": "Avoid Common Mistakes", "trigger": "Fear", "example": "Avoid these 5 common mistakes"},
156
+ {"key": "educated_decision", "name": "Educated Decision", "trigger": "Pride", "example": "Make an educated decision"},
157
+ {"key": "comparison_logic", "name": "Comparison Logic", "trigger": "Pride", "example": "Compare and choose wisely"},
158
+ {"key": "transparency", "name": "Transparency", "trigger": "Trust", "example": "100% transparent pricing"},
159
+ {"key": "informed_buyer", "name": "Informed Buyer", "trigger": "Pride", "example": "For informed buyers"},
160
+ {"key": "data_driven", "name": "Data-Driven", "trigger": "Authority", "example": "Backed by data"},
161
+ {"key": "rational_choice", "name": "Rational Choice", "trigger": "Pride", "example": "The rational choice"},
162
+ {"key": "what_experts_do", "name": "What Experts Do", "trigger": "Authority", "example": "What experts do"},
163
+ {"key": "optimization", "name": "Optimization", "trigger": "Pride", "example": "Optimize your [thing]"},
164
+ {"key": "smart_investment", "name": "Smart Investment", "trigger": "Pride", "example": "The smart investment choice"},
165
+ {"key": "evidence_based", "name": "Evidence-Based", "trigger": "Authority", "example": "Backed by research and data"},
166
+ ]
167
+ },
168
+ AngleCategory.CURIOSITY: {
169
+ "name": "Curiosity & Pattern Interrupt",
170
+ "angles": [
171
+ {"key": "shocking_stats", "name": "Shocking Stats", "trigger": "Curiosity", "example": "Shocking stat: [number]"},
172
+ {"key": "did_you_know", "name": "Did You Know?", "trigger": "Curiosity", "example": "Did you know [fact]?"},
173
+ {"key": "open_loops", "name": "Open Loops", "trigger": "Curiosity", "example": "Thousands doing THIS instead"},
174
+ {"key": "contrarian", "name": "Contrarian Claims", "trigger": "Curiosity", "example": "Why everyone is wrong about [thing]"},
175
+ {"key": "myth_busting", "name": "Myth Busting", "trigger": "Curiosity", "example": "Myth busted: [myth]"},
176
+ {"key": "unexpected_truth", "name": "Unexpected Truth", "trigger": "Curiosity", "example": "The truth about [thing]"},
177
+ {"key": "hidden_secrets", "name": "Hidden Secrets", "trigger": "Curiosity", "example": "The hidden secret to [thing]"},
178
+ {"key": "scroll_stopper", "name": "Scroll Stopper", "trigger": "Curiosity", "example": "Stop scrolling - read this"},
179
+ {"key": "pattern_break", "name": "Pattern Break", "trigger": "Curiosity", "example": "This breaks all patterns"},
180
+ {"key": "curiosity_gap", "name": "Curiosity Gap", "trigger": "Curiosity", "example": "What is THIS?"},
181
+ {"key": "reveal_secret", "name": "Reveal Secret", "trigger": "Curiosity", "example": "The secret they don't want you to know"},
182
+ {"key": "unexpected_benefit", "name": "Unexpected Benefit", "trigger": "Curiosity", "example": "The benefit nobody talks about"},
183
+ ]
184
+ },
185
+ AngleCategory.PROBLEM_SOLUTION: {
186
+ "name": "Problem–Solution",
187
+ "angles": [
188
+ {"key": "pain_point", "name": "Pain Point Highlight", "trigger": "Anger", "example": "Tired of [problem]?"},
189
+ {"key": "frustration_relief", "name": "Frustration Relief", "trigger": "Relief", "example": "End your frustration with [problem]"},
190
+ {"key": "complexity_simplified", "name": "Complexity Simplified", "trigger": "Convenience", "example": "We make [thing] simple"},
191
+ {"key": "confusion_clarity", "name": "Confusion → Clarity", "trigger": "Relief", "example": "From confusion to clarity"},
192
+ {"key": "overwhelm_reduction", "name": "Overwhelm Reduction", "trigger": "Relief", "example": "Stop feeling overwhelmed"},
193
+ {"key": "direct_fix", "name": "Direct Fix", "trigger": "Relief", "example": "The direct fix for [problem]"},
194
+ {"key": "shortcut", "name": "Shortcut", "trigger": "Convenience", "example": "The shortcut to [goal]"},
195
+ {"key": "better_alternative", "name": "Better Alternative", "trigger": "Desire", "example": "A better alternative to [current]"},
196
+ {"key": "replace_old_way", "name": "Replace Old Way", "trigger": "Desire", "example": "Replace the old way"},
197
+ {"key": "modern_solution", "name": "Modern Solution", "trigger": "Desire", "example": "The modern solution to [problem]"},
198
+ {"key": "pain_elimination", "name": "Pain Elimination", "trigger": "Relief", "example": "Eliminate [problem] forever"},
199
+ {"key": "simplified_complexity", "name": "Simplified Complexity", "trigger": "Convenience", "example": "We simplified the complex"},
200
+ {"key": "one_click_solution", "name": "One-Click Solution", "trigger": "Convenience", "example": "Solve it in one click"},
201
+ ]
202
+ },
203
+ }
204
+
205
+
206
+ def get_all_angles() -> List[Dict[str, Any]]:
207
+ """Get all angles as a flat list."""
208
+ all_angles = []
209
+ for category, data in ANGLES.items():
210
+ for angle in data["angles"]:
211
+ angle_copy = angle.copy()
212
+ angle_copy["category"] = data["name"]
213
+ angle_copy["category_key"] = category
214
+ all_angles.append(angle_copy)
215
+ return all_angles
216
+
217
+
218
+ def get_angles_by_category(category: AngleCategory) -> List[Dict[str, Any]]:
219
+ """Get angles for a specific category."""
220
+ return ANGLES.get(category, {}).get("angles", [])
221
+
222
+
223
+ def get_angle_by_key(key: str) -> Optional[Dict[str, Any]]:
224
+ """Get a specific angle by key."""
225
+ for category, data in ANGLES.items():
226
+ for angle in data["angles"]:
227
+ if angle["key"] == key:
228
+ angle_copy = angle.copy()
229
+ angle_copy["category"] = data["name"]
230
+ angle_copy["category_key"] = category
231
+ return angle_copy
232
+ return None
233
+
234
+
235
+ def get_random_angles(count: int = 6, diverse: bool = True) -> List[Dict[str, Any]]:
236
+ """Get random angles, optionally ensuring diversity across categories."""
237
+ if diverse:
238
+ # Select one from each category first
239
+ selected = []
240
+ categories = list(ANGLES.keys())
241
+ random.shuffle(categories)
242
+
243
+ for category in categories[:count]:
244
+ angles = ANGLES[category]["angles"]
245
+ angle = random.choice(angles).copy()
246
+ angle["category"] = ANGLES[category]["name"]
247
+ angle["category_key"] = category
248
+ selected.append(angle)
249
+
250
+ return selected[:count]
251
+ else:
252
+ all_angles = get_all_angles()
253
+ return random.sample(all_angles, min(count, len(all_angles)))
254
+
255
+
256
+ def get_angles_for_niche(niche: str) -> List[Dict[str, Any]]:
257
+ """Get angles best suited for a niche."""
258
+ niche_lower = niche.lower()
259
+
260
+ # Niche-specific angle recommendations
261
+ if "insurance" in niche_lower:
262
+ recommended_keys = [
263
+ "fear_loss", "peace_of_mind", "save_money", "price_comparison",
264
+ "trusted_millions", "limited_time", "avoid_mistakes", "pain_point"
265
+ ]
266
+ elif "glp" in niche_lower or "weight" in niche_lower:
267
+ recommended_keys = [
268
+ "confidence_boost", "pride_self_worth", "before_after_shock",
269
+ "testimonials", "trending_now", "modern_solution", "shortcut"
270
+ ]
271
+ else:
272
+ # Default mix
273
+ recommended_keys = [
274
+ "save_money", "fast_instant", "testimonials", "limited_time",
275
+ "shocking_stats", "pain_point"
276
+ ]
277
+
278
+ angles = []
279
+ for key in recommended_keys:
280
+ angle = get_angle_by_key(key)
281
+ if angle:
282
+ angles.append(angle)
283
+
284
+ return angles
285
+
286
+
287
+ # Top performing angles for initial testing
288
+ TOP_ANGLES = [
289
+ "save_money", "fear_loss", "fast_instant", "expert_backed",
290
+ "testimonials", "limited_time", "age_based", "pain_point"
291
+ ]
292
+
293
+
294
+ def get_top_angles() -> List[Dict[str, Any]]:
295
+ """Get top performing angles for initial testing."""
296
+ return [get_angle_by_key(key) for key in TOP_ANGLES if get_angle_by_key(key)]
297
+
data/concepts.py ADDED
@@ -0,0 +1,282 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Affiliate Marketing Concepts Framework (Lite Version)
3
+ 100 concepts organized into 10 categories.
4
+ Concepts answer: "How do I show it visually?" (Creative HOW)
5
+ """
6
+
7
+ from typing import Dict, List, Any, Optional
8
+ from enum import Enum
9
+ import random
10
+
11
+
12
+ class ConceptCategory(str, Enum):
13
+ """Concept categories."""
14
+ VISUAL_STRUCTURE = "visual_structure"
15
+ UGC_NATIVE = "ugc_native"
16
+ STORYTELLING = "storytelling"
17
+ COMPARISON = "comparison"
18
+ AUTHORITY = "authority"
19
+ SOCIAL_PROOF = "social_proof"
20
+ SCROLL_STOPPING = "scroll_stopping"
21
+ EDUCATIONAL = "educational"
22
+ PERSONALIZATION = "personalization"
23
+ CTA_FOCUSED = "cta_focused"
24
+
25
+
26
+ # Complete concepts framework - 100 concepts (10 per category)
27
+ CONCEPTS = {
28
+ ConceptCategory.VISUAL_STRUCTURE: {
29
+ "name": "Visual Structure",
30
+ "concepts": [
31
+ {"key": "before_after", "name": "Before vs After", "structure": "Left=pain, Right=solution", "visual": "Split-screen transformation"},
32
+ {"key": "split_screen", "name": "Split Screen", "structure": "Two halves comparison", "visual": "Vertical or horizontal split"},
33
+ {"key": "checklist", "name": "Checklist / Tick Marks", "structure": "List with checkmarks", "visual": "Clean list, prominent checks"},
34
+ {"key": "bold_headline", "name": "Bold Headline Image", "structure": "Headline dominates", "visual": "Large bold typography"},
35
+ {"key": "text_first", "name": "Text-First Image", "structure": "Text primary, image secondary", "visual": "Clear typography hierarchy"},
36
+ {"key": "minimalist", "name": "Minimalist Design", "structure": "White space, minimal elements", "visual": "Clean lines, focused"},
37
+ {"key": "big_numbers", "name": "Big Numbers Visual", "structure": "Numbers dominate", "visual": "Huge numbers, bold type"},
38
+ {"key": "highlight_circle", "name": "Highlight / Circle Focus", "structure": "Circle on key element", "visual": "Red circles, arrows"},
39
+ {"key": "step_by_step", "name": "Step-by-Step Visual", "structure": "Step 1 → 2 → 3", "visual": "Clear flow, numbered"},
40
+ {"key": "icon_based", "name": "Icon-Based Layout", "structure": "Icons represent features", "visual": "Clear icons, organized"},
41
+ {"key": "grid_layout", "name": "Grid Layout", "structure": "Organized grid format", "visual": "Clean grid, easy to scan"},
42
+ {"key": "timeline_visual", "name": "Timeline Visual", "structure": "Time-based progression", "visual": "Clear timeline flow"},
43
+ {"key": "infographic_style", "name": "Infographic Style", "structure": "Information graphics", "visual": "Data visualization"},
44
+ ]
45
+ },
46
+ ConceptCategory.UGC_NATIVE: {
47
+ "name": "UGC & Native",
48
+ "concepts": [
49
+ {"key": "selfie_style", "name": "Selfie-Style Image", "structure": "Phone camera angle", "visual": "Casual, authentic feel"},
50
+ {"key": "casual_phone", "name": "Casual Phone Shot", "structure": "Mobile perspective", "visual": "Unpolished, authentic"},
51
+ {"key": "pov", "name": "POV Perspective", "structure": "First-person view", "visual": "Immersive feel"},
52
+ {"key": "just_found", "name": "Just Found This", "structure": "Discovery moment", "visual": "Excited, sharing"},
53
+ {"key": "organic_feed", "name": "Organic Feed Style", "structure": "Native to platform", "visual": "Doesn't look like ad"},
54
+ {"key": "influencer", "name": "Influencer-Style", "structure": "Polished but authentic", "visual": "Influencer aesthetic"},
55
+ {"key": "low_production", "name": "Low-Production Authentic", "structure": "Raw, unpolished", "visual": "Authentic feel"},
56
+ {"key": "reaction", "name": "Reaction Shot", "structure": "Emotional reaction", "visual": "Clear emotion visible"},
57
+ {"key": "screenshot", "name": "Screenshot-Style", "structure": "Device screenshot", "visual": "Realistic screenshot"},
58
+ {"key": "story_frame", "name": "Story Frame", "structure": "Vertical story format", "visual": "Story-style layout"},
59
+ {"key": "behind_scenes", "name": "Behind the Scenes", "structure": "Raw, unedited look", "visual": "Authentic, unpolished"},
60
+ {"key": "day_in_life", "name": "Day in the Life", "structure": "Daily routine snapshot", "visual": "Relatable daily moments"},
61
+ {"key": "unboxing_style", "name": "Unboxing Style", "structure": "Product reveal moment", "visual": "Discovery excitement"},
62
+ ]
63
+ },
64
+ ConceptCategory.STORYTELLING: {
65
+ "name": "Storytelling",
66
+ "concepts": [
67
+ {"key": "emotional_snapshot", "name": "Emotional Snapshot", "structure": "Single emotional moment", "visual": "Authentic expression"},
68
+ {"key": "relatable_moment", "name": "Relatable Daily Moment", "structure": "Everyday scene", "visual": "Relatable situation"},
69
+ {"key": "micro_story", "name": "Micro-Story Scene", "structure": "Small story moment", "visual": "Narrative feel"},
70
+ {"key": "life_situation", "name": "Life Situation", "structure": "Real-life context", "visual": "Authentic situation"},
71
+ {"key": "decision_moment", "name": "Decision Moment", "structure": "Decision point", "visual": "Contemplative"},
72
+ {"key": "problem_awareness", "name": "Problem Awareness Scene", "structure": "Problem visible", "visual": "Awareness moment"},
73
+ {"key": "turning_point", "name": "Turning Point Frame", "structure": "Transformation moment", "visual": "Change visible"},
74
+ {"key": "relief_moment", "name": "Relief Moment", "structure": "Relief visible", "visual": "Peace visible"},
75
+ {"key": "success_moment", "name": "Success Moment", "structure": "Success visible", "visual": "Achievement visible"},
76
+ {"key": "future_projection", "name": "Future Projection", "structure": "Future vision", "visual": "Aspirational"},
77
+ {"key": "journey_arc", "name": "Journey Arc", "structure": "Complete transformation story", "visual": "Full journey visible"},
78
+ {"key": "milestone_moment", "name": "Milestone Moment", "structure": "Key achievement moment", "visual": "Celebration visible"},
79
+ ]
80
+ },
81
+ ConceptCategory.COMPARISON: {
82
+ "name": "Comparison & Logic",
83
+ "concepts": [
84
+ {"key": "side_by_side_table", "name": "Side-by-Side Table", "structure": "Comparison table", "visual": "Checkmarks/crosses"},
85
+ {"key": "old_vs_new", "name": "Old Way vs New Way", "structure": "Old vs new comparison", "visual": "Modern highlighted"},
86
+ {"key": "choice_elimination", "name": "Choice Elimination", "structure": "Others crossed out", "visual": "Winner clear"},
87
+ {"key": "feature_breakdown", "name": "Feature Breakdown", "structure": "Features compared", "visual": "Easy to compare"},
88
+ {"key": "pros_cons", "name": "Pros vs Cons", "structure": "Pros on one side, cons other", "visual": "Balanced view"},
89
+ {"key": "ranking", "name": "Ranking Visual", "structure": "Ranked list", "visual": "Top highlighted"},
90
+ {"key": "winner_highlight", "name": "Winner Highlight", "structure": "Winner highlighted", "visual": "Others dimmed"},
91
+ {"key": "bar_scale", "name": "Bar / Scale Visual", "structure": "Visual bars/scales", "visual": "Easy to compare"},
92
+ {"key": "price_stack", "name": "Price Stack", "structure": "Prices stacked", "visual": "Savings visible"},
93
+ {"key": "value_breakdown", "name": "Value Breakdown", "structure": "Value components", "visual": "Easy to understand"},
94
+ {"key": "versus_comparison", "name": "Versus Comparison", "structure": "Head-to-head comparison", "visual": "Clear winner"},
95
+ {"key": "feature_matrix", "name": "Feature Matrix", "structure": "Feature comparison grid", "visual": "Easy comparison"},
96
+ ]
97
+ },
98
+ ConceptCategory.AUTHORITY: {
99
+ "name": "Authority & Trust",
100
+ "concepts": [
101
+ {"key": "expert_portrait", "name": "Expert Portrait", "structure": "Professional portrait", "visual": "Trustworthy appearance"},
102
+ {"key": "badge_seal", "name": "Badge / Seal Visual", "structure": "Badges prominent", "visual": "Official look"},
103
+ {"key": "certification", "name": "Certification Overlay", "structure": "Cert overlaid", "visual": "Official"},
104
+ {"key": "media_mention", "name": "Media Mention Style", "structure": "Media logos/quotes", "visual": "Credible"},
105
+ {"key": "professional_setting", "name": "Professional Setting", "structure": "Office/professional space", "visual": "Clean, organized"},
106
+ {"key": "office_scene", "name": "Office / Desk Scene", "structure": "Office environment", "visual": "Professional"},
107
+ {"key": "institutional", "name": "Institutional Look", "structure": "Official design", "visual": "Institutional"},
108
+ {"key": "data_backed", "name": "Data-Backed Visual", "structure": "Charts, graphs", "visual": "Data visible"},
109
+ {"key": "chart_graph", "name": "Chart / Graph Snapshot", "structure": "Chart dominates", "visual": "Easy to read"},
110
+ {"key": "trust_signals", "name": "Trust Signal Stack", "structure": "Multiple trust elements", "visual": "Organized signals"},
111
+ {"key": "credentials_display", "name": "Credentials Display", "structure": "Certifications shown", "visual": "Official credentials"},
112
+ {"key": "partnership_logos", "name": "Partnership Logos", "structure": "Partner brands visible", "visual": "Trusted partnerships"},
113
+ ]
114
+ },
115
+ ConceptCategory.SOCIAL_PROOF: {
116
+ "name": "Social Proof",
117
+ "concepts": [
118
+ {"key": "testimonial_screenshot", "name": "Testimonial Screenshot", "structure": "Screenshot format", "visual": "Authentic testimonial"},
119
+ {"key": "review_stars", "name": "Review Stars Visual", "structure": "Stars dominate", "visual": "Large stars, rating"},
120
+ {"key": "quote_card", "name": "Quote Card", "structure": "Quote as main visual", "visual": "Clear, readable"},
121
+ {"key": "case_study_frame", "name": "Case Study Frame", "structure": "Case study format", "visual": "Results visible"},
122
+ {"key": "crowd", "name": "Crowd Visual", "structure": "Many people visible", "visual": "Popular feel"},
123
+ {"key": "others_like_you", "name": "Others Like You", "structure": "Similar people", "visual": "Relatable, authentic"},
124
+ {"key": "real_customer", "name": "Real Customer Photo", "structure": "Real customer shown", "visual": "Authentic, relatable"},
125
+ {"key": "comment_screenshot", "name": "Comment Screenshot", "structure": "Comments visible", "visual": "Realistic comments"},
126
+ {"key": "ugc_collage", "name": "UGC Collage", "structure": "Multiple UGC pieces", "visual": "Collage format"},
127
+ {"key": "community", "name": "Community Visual", "structure": "Community visible", "visual": "Belonging feel"},
128
+ {"key": "user_spotlight", "name": "User Spotlight", "structure": "Individual user featured", "visual": "Personal success story"},
129
+ {"key": "group_success", "name": "Group Success", "structure": "Multiple success stories", "visual": "Collective achievement"},
130
+ ]
131
+ },
132
+ ConceptCategory.SCROLL_STOPPING: {
133
+ "name": "Scroll-Stopping",
134
+ "concepts": [
135
+ {"key": "shock_headline", "name": "Shock Headline", "structure": "Shocking headline dominates", "visual": "Bold, high contrast"},
136
+ {"key": "red_warning", "name": "Big Red Warning Text", "structure": "Red warning prominent", "visual": "Large red text"},
137
+ {"key": "unusual_contrast", "name": "Unusual Contrast", "structure": "High contrast, unusual colors", "visual": "Stands out"},
138
+ {"key": "pattern_break", "name": "Pattern Break Design", "structure": "Different from expected", "visual": "Unexpected"},
139
+ {"key": "unexpected_image", "name": "Unexpected Image", "structure": "Surprising visual", "visual": "Attention-grabbing"},
140
+ {"key": "bold_claim", "name": "Bold Claim Card", "structure": "Bold claim prominent", "visual": "Large text"},
141
+ {"key": "glitch_highlight", "name": "Glitch / Highlight", "structure": "Glitch effect", "visual": "Modern"},
142
+ {"key": "disruptive_color", "name": "Disruptive Color Use", "structure": "Unexpected colors", "visual": "Disruptive"},
143
+ {"key": "meme_style", "name": "Meme-Style Image", "structure": "Meme format", "visual": "Shareable"},
144
+ {"key": "visual_tension", "name": "Visual Tension", "structure": "Tension in visual", "visual": "Engaging"},
145
+ {"key": "bold_typography", "name": "Bold Typography Focus", "structure": "Typography dominates", "visual": "Text-first impact"},
146
+ {"key": "motion_blur", "name": "Motion Blur Effect", "structure": "Dynamic movement feel", "visual": "Energy and action"},
147
+ ]
148
+ },
149
+ ConceptCategory.EDUCATIONAL: {
150
+ "name": "Educational & Explainer",
151
+ "concepts": [
152
+ {"key": "how_it_works", "name": "How It Works", "structure": "Process explanation", "visual": "Step-by-step"},
153
+ {"key": "three_step", "name": "3-Step Process", "structure": "Step 1→2→3", "visual": "Simple flow"},
154
+ {"key": "flow_diagram", "name": "Flow Diagram", "structure": "Flowchart format", "visual": "Easy to follow"},
155
+ {"key": "simple_explainer", "name": "Simple Explainer", "structure": "Simple explanation", "visual": "Easy to understand"},
156
+ {"key": "faq", "name": "FAQ Visual", "structure": "FAQ format", "visual": "Clear Q&A"},
157
+ {"key": "mistake_callout", "name": "Mistake Callout", "structure": "Mistakes highlighted", "visual": "Avoid them"},
158
+ {"key": "do_dont", "name": "Do / Don't Visual", "structure": "Do vs Don't", "visual": "Easy to understand"},
159
+ {"key": "learning_card", "name": "Learning Card", "structure": "Card with content", "visual": "Educational"},
160
+ {"key": "visual_guide", "name": "Visual Guide", "structure": "Guide format", "visual": "Instructional"},
161
+ {"key": "instructional", "name": "Instructional Frame", "structure": "Instruction format", "visual": "Easy to follow"},
162
+ {"key": "tip_card", "name": "Tip Card", "structure": "Quick tip format", "visual": "Actionable advice"},
163
+ {"key": "checklist_visual", "name": "Checklist Visual", "structure": "Checklist format", "visual": "Easy to follow"},
164
+ ]
165
+ },
166
+ ConceptCategory.PERSONALIZATION: {
167
+ "name": "Call-Out & Personalization",
168
+ "concepts": [
169
+ {"key": "if_you_are", "name": "If You Are X…", "structure": "Direct callout", "visual": "Specific group"},
170
+ {"key": "age_specific", "name": "Age-Specific Visual", "structure": "Age-specific imagery", "visual": "Relatable"},
171
+ {"key": "location_specific", "name": "Location-Specific", "structure": "Location visible", "visual": "Local feel"},
172
+ {"key": "role_based", "name": "Role-Based Scene", "structure": "Role-specific scene", "visual": "Relatable"},
173
+ {"key": "lifestyle_scene", "name": "Lifestyle Match", "structure": "Lifestyle visible", "visual": "Match"},
174
+ {"key": "identity_mirror", "name": "Identity Mirror", "structure": "Identity reflected", "visual": "Connection"},
175
+ {"key": "targeted_headline", "name": "Targeted Headline", "structure": "Targeted headline", "visual": "Relevant"},
176
+ {"key": "personalized_hook", "name": "Personalized Hook", "structure": "Personalized opening", "visual": "Connection"},
177
+ {"key": "segment_callout", "name": "Segment Callout", "structure": "Segment-specific", "visual": "Targeted"},
178
+ {"key": "direct_address", "name": "Direct Address", "structure": "Direct address format", "visual": "Personal"},
179
+ {"key": "custom_message", "name": "Custom Message", "structure": "Personalized message", "visual": "One-on-one feel"},
180
+ {"key": "targeted_audience", "name": "Targeted Audience Visual", "structure": "Specific audience shown", "visual": "Relatable group"},
181
+ ]
182
+ },
183
+ ConceptCategory.CTA_FOCUSED: {
184
+ "name": "CTA-Focused",
185
+ "concepts": [
186
+ {"key": "button_focused", "name": "Button-Focused Image", "structure": "Button prominent", "visual": "Action-oriented"},
187
+ {"key": "arrow_flow", "name": "Arrow-Directed Flow", "structure": "Arrows point to CTA", "visual": "Directional"},
188
+ {"key": "highlighted_cta", "name": "Highlighted CTA Box", "structure": "CTA box stands out", "visual": "Clear"},
189
+ {"key": "action_words", "name": "Action Words Overlay", "structure": "Action words prominent", "visual": "Urgent"},
190
+ {"key": "click_prompt", "name": "Click Prompt Visual", "structure": "Click prompt visible", "visual": "Action-oriented"},
191
+ {"key": "tap_here", "name": "Tap-Here Cue", "structure": "Tap here visible", "visual": "Mobile-friendly"},
192
+ {"key": "next_step", "name": "Next Step Highlight", "structure": "Next step prominent", "visual": "Action-oriented"},
193
+ {"key": "simple_cta", "name": "Simple CTA Card", "structure": "Simple card with CTA", "visual": "Minimal"},
194
+ {"key": "countdown_cta", "name": "Countdown CTA", "structure": "Countdown + CTA", "visual": "Urgent"},
195
+ {"key": "final_push", "name": "Final Push Frame", "structure": "Final push format", "visual": "Urgent, action"},
196
+ {"key": "pulsing_cta", "name": "Pulsing CTA", "structure": "Animated attention", "visual": "Eye-catching action"},
197
+ {"key": "multi_cta", "name": "Multiple CTAs", "structure": "Multiple action options", "visual": "Flexible engagement"},
198
+ ]
199
+ },
200
+ }
201
+
202
+
203
+ def get_all_concepts() -> List[Dict[str, Any]]:
204
+ """Get all concepts as a flat list."""
205
+ all_concepts = []
206
+ for category, data in CONCEPTS.items():
207
+ for concept in data["concepts"]:
208
+ concept_copy = concept.copy()
209
+ concept_copy["category"] = data["name"]
210
+ concept_copy["category_key"] = category
211
+ all_concepts.append(concept_copy)
212
+ return all_concepts
213
+
214
+
215
+ def get_concepts_by_category(category: ConceptCategory) -> List[Dict[str, Any]]:
216
+ """Get concepts for a specific category."""
217
+ return CONCEPTS.get(category, {}).get("concepts", [])
218
+
219
+
220
+ def get_concept_by_key(key: str) -> Optional[Dict[str, Any]]:
221
+ """Get a specific concept by key."""
222
+ for category, data in CONCEPTS.items():
223
+ for concept in data["concepts"]:
224
+ if concept["key"] == key:
225
+ concept_copy = concept.copy()
226
+ concept_copy["category"] = data["name"]
227
+ concept_copy["category_key"] = category
228
+ return concept_copy
229
+ return None
230
+
231
+
232
+ def get_random_concepts(count: int = 5, diverse: bool = True) -> List[Dict[str, Any]]:
233
+ """Get random concepts, optionally ensuring diversity across categories."""
234
+ if diverse:
235
+ selected = []
236
+ categories = list(CONCEPTS.keys())
237
+ random.shuffle(categories)
238
+
239
+ for category in categories[:count]:
240
+ concepts = CONCEPTS[category]["concepts"]
241
+ concept = random.choice(concepts).copy()
242
+ concept["category"] = CONCEPTS[category]["name"]
243
+ concept["category_key"] = category
244
+ selected.append(concept)
245
+
246
+ return selected[:count]
247
+ else:
248
+ all_concepts = get_all_concepts()
249
+ return random.sample(all_concepts, min(count, len(all_concepts)))
250
+
251
+
252
+ # Top performing concepts for initial testing
253
+ TOP_CONCEPTS = [
254
+ "before_after", "selfie_style", "problem_awareness",
255
+ "side_by_side_table", "relatable_moment"
256
+ ]
257
+
258
+
259
+ def get_top_concepts() -> List[Dict[str, Any]]:
260
+ """Get top performing concepts for initial testing."""
261
+ return [get_concept_by_key(key) for key in TOP_CONCEPTS if get_concept_by_key(key)]
262
+
263
+
264
+ def get_compatible_concepts(angle_trigger: str) -> List[Dict[str, Any]]:
265
+ """Get concepts compatible with a psychological trigger."""
266
+ # Compatibility mapping
267
+ compatibility = {
268
+ "Fear": ["before_after", "shock_headline", "red_warning", "problem_awareness"],
269
+ "Relief": ["before_after", "relief_moment", "success_moment", "turning_point"],
270
+ "Greed": ["big_numbers", "price_stack", "value_breakdown", "side_by_side_table"],
271
+ "FOMO": ["countdown_cta", "red_warning", "crowd", "trending"],
272
+ "Social Proof": ["testimonial_screenshot", "review_stars", "real_customer", "crowd"],
273
+ "Authority": ["expert_portrait", "badge_seal", "data_backed", "certification"],
274
+ "Curiosity": ["shock_headline", "unexpected_image", "pattern_break", "bold_claim"],
275
+ "Pride": ["success_moment", "before_after", "winner_highlight"],
276
+ "Convenience": ["three_step", "how_it_works", "simple_explainer"],
277
+ "Trust": ["trust_signals", "badge_seal", "real_customer", "testimonial_screenshot"],
278
+ }
279
+
280
+ concept_keys = compatibility.get(angle_trigger, [])
281
+ return [get_concept_by_key(key) for key in concept_keys if get_concept_by_key(key)]
282
+
data/containers.py ADDED
@@ -0,0 +1,434 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Container Types - Visual container styles for ad creatives.
3
+ These simulate different interface styles to make ads feel native and authentic.
4
+ """
5
+
6
+ from typing import Dict, Any, List, Optional
7
+ import random
8
+
9
+
10
+ # Complete list of container types with visual guidance
11
+ CONTAINER_TYPES: Dict[str, Dict[str, Any]] = {
12
+ "imessage": {
13
+ "name": "iMessage",
14
+ "description": "iOS iMessage screenshot style",
15
+ "visual_guidance": "iOS iMessage screenshot style, blue/green message bubbles, iPhone interface, authentic conversation look",
16
+ "font_style": "San Francisco, system font",
17
+ "colors": {
18
+ "primary": "#007AFF", # iOS blue
19
+ "secondary": "#34C759", # iOS green
20
+ "background": "#FFFFFF",
21
+ "text": "#000000",
22
+ },
23
+ "best_for": ["personal connection", "conversation", "informal offers"],
24
+ "authenticity_tips": [
25
+ "Include battery %, time, signal bars",
26
+ "Use realistic conversation format",
27
+ "Keep messages short (2-4 messages)",
28
+ ],
29
+ },
30
+ "whatsapp": {
31
+ "name": "WhatsApp",
32
+ "description": "WhatsApp chat screenshot style",
33
+ "visual_guidance": "WhatsApp chat interface, green bubbles, checkmarks, authentic conversation feel",
34
+ "font_style": "Helvetica Neue, system font",
35
+ "colors": {
36
+ "primary": "#25D366", # WhatsApp green
37
+ "secondary": "#128C7E",
38
+ "background": "#ECE5DD",
39
+ "text": "#000000",
40
+ },
41
+ "best_for": ["personal recommendations", "peer-to-peer", "urgent messages"],
42
+ "authenticity_tips": [
43
+ "Include double checkmarks (read receipts)",
44
+ "Add timestamps",
45
+ "Use typical WhatsApp formatting",
46
+ ],
47
+ },
48
+ "sms": {
49
+ "name": "SMS/Text",
50
+ "description": "Standard SMS text message style",
51
+ "visual_guidance": "Android/iOS SMS interface, simple text bubbles, notification style",
52
+ "font_style": "Roboto or San Francisco",
53
+ "colors": {
54
+ "primary": "#2196F3",
55
+ "secondary": "#4CAF50",
56
+ "background": "#FFFFFF",
57
+ "text": "#000000",
58
+ },
59
+ "best_for": ["urgent alerts", "personal messages", "time-sensitive offers"],
60
+ "authenticity_tips": [
61
+ "Keep messages very short",
62
+ "Use typical SMS abbreviations",
63
+ "Include carrier/time info",
64
+ ],
65
+ },
66
+ "bank_alert": {
67
+ "name": "Bank Alert",
68
+ "description": "Bank transaction notification style",
69
+ "visual_guidance": "Bank transaction notification style, red alert box, bank app UI, urgent notification aesthetic",
70
+ "font_style": "Arial, Helvetica, system font",
71
+ "colors": {
72
+ "primary": "#D32F2F", # Alert red
73
+ "secondary": "#1976D2", # Bank blue
74
+ "background": "#FFFFFF",
75
+ "text": "#212121",
76
+ },
77
+ "best_for": ["financial urgency", "savings alerts", "money-related offers"],
78
+ "authenticity_tips": [
79
+ "Use official-looking format",
80
+ "Include dollar amounts",
81
+ "Add bank-style icons",
82
+ ],
83
+ },
84
+ "news_chyron": {
85
+ "name": "News Chyron",
86
+ "description": "Breaking news ticker style",
87
+ "visual_guidance": "Breaking news ticker style, red/white scrolling text bar, news channel aesthetic, urgent feel",
88
+ "font_style": "Impact, Arial Black, bold sans-serif",
89
+ "colors": {
90
+ "primary": "#FF0000", # Breaking news red
91
+ "secondary": "#FFFFFF",
92
+ "background": "#000000",
93
+ "text": "#FFFFFF",
94
+ },
95
+ "best_for": ["breaking announcements", "urgent news", "time-sensitive offers"],
96
+ "authenticity_tips": [
97
+ "Use BREAKING/ALERT prefix",
98
+ "Include news channel logo area",
99
+ "Add scrolling ticker effect",
100
+ ],
101
+ },
102
+ "email_notification": {
103
+ "name": "Email Notification",
104
+ "description": "Email notification/preview style",
105
+ "visual_guidance": "Email notification style, email client interface, notification badge, real email app look",
106
+ "font_style": "System font, Segoe UI, Roboto",
107
+ "colors": {
108
+ "primary": "#1A73E8", # Gmail blue
109
+ "secondary": "#EA4335", # Notification badge
110
+ "background": "#FFFFFF",
111
+ "text": "#202124",
112
+ },
113
+ "best_for": ["official communications", "professional offers", "formal announcements"],
114
+ "authenticity_tips": [
115
+ "Include sender, subject, preview",
116
+ "Add unread badge if relevant",
117
+ "Use email timestamp format",
118
+ ],
119
+ },
120
+ "reddit_post": {
121
+ "name": "Reddit Post",
122
+ "description": "Reddit post/comment style",
123
+ "visual_guidance": "Reddit discussion style, Reddit UI, comment thread appearance, authentic forum look",
124
+ "font_style": "Noto Sans, Arial",
125
+ "colors": {
126
+ "primary": "#FF4500", # Reddit orange
127
+ "secondary": "#0079D3", # Reddit blue
128
+ "background": "#DAE0E6",
129
+ "text": "#1A1A1B",
130
+ },
131
+ "best_for": ["social proof", "user discussions", "authentic testimonials"],
132
+ "authenticity_tips": [
133
+ "Include upvote counts",
134
+ "Add username (anonymous style)",
135
+ "Use subreddit reference",
136
+ ],
137
+ },
138
+ "system_notification": {
139
+ "name": "System Notification",
140
+ "description": "iOS/Android system notification popup",
141
+ "visual_guidance": "iOS/Android system notification style - plain white or gray rounded rectangle box, NO gradients, NO emojis, NO decorative elements, simple system font, minimal text, authentic OS notification appearance",
142
+ "font_style": "San Francisco, Roboto, system font only",
143
+ "colors": {
144
+ "primary": "#000000",
145
+ "secondary": "#666666",
146
+ "background": "#F2F2F2",
147
+ "text": "#000000",
148
+ },
149
+ "best_for": ["urgent alerts", "app notifications", "system messages"],
150
+ "authenticity_tips": [
151
+ "NO emojis or decorative elements",
152
+ "Keep to 1-2 short lines",
153
+ "Use app icon if relevant",
154
+ ],
155
+ "avoid": ["emojis", "decorative elements", "gradients", "colorful backgrounds"],
156
+ },
157
+ "push_notification": {
158
+ "name": "Push Notification",
159
+ "description": "Mobile app push notification style",
160
+ "visual_guidance": "Mobile push notification banner, app icon, brief text, swipe-to-view format",
161
+ "font_style": "System font",
162
+ "colors": {
163
+ "primary": "#007AFF",
164
+ "secondary": "#8E8E93",
165
+ "background": "#FFFFFF",
166
+ "text": "#000000",
167
+ },
168
+ "best_for": ["app alerts", "time-sensitive messages", "quick updates"],
169
+ "authenticity_tips": [
170
+ "Include app icon",
171
+ "Very short headline (5-7 words)",
172
+ "Add timestamp (e.g., 'now', '2m ago')",
173
+ ],
174
+ },
175
+ "sticky_note": {
176
+ "name": "Sticky Note",
177
+ "description": "Handwritten sticky note overlay",
178
+ "visual_guidance": "Yellow sticky note overlay on image, handwritten-style text, authentic note appearance, slightly wrinkled paper texture",
179
+ "font_style": "Handwriting fonts, marker style",
180
+ "colors": {
181
+ "primary": "#FFEB3B", # Sticky note yellow
182
+ "secondary": "#FFC107",
183
+ "background": "#FFEB3B",
184
+ "text": "#000000",
185
+ },
186
+ "best_for": ["personal reminders", "quick tips", "informal notes"],
187
+ "authenticity_tips": [
188
+ "Slight angle/tilt",
189
+ "Handwritten font style",
190
+ "Paper texture/wrinkles",
191
+ ],
192
+ },
193
+ "memo": {
194
+ "name": "Internal Memo",
195
+ "description": "Office memo/document style",
196
+ "visual_guidance": "Internal memo document style, typewriter font, yellow/white paper, authentic document appearance",
197
+ "font_style": "Courier, typewriter fonts",
198
+ "colors": {
199
+ "primary": "#000000",
200
+ "secondary": "#333333",
201
+ "background": "#FFFFCC", # Paper yellow
202
+ "text": "#000000",
203
+ },
204
+ "best_for": ["official announcements", "leaked documents", "internal secrets"],
205
+ "authenticity_tips": [
206
+ "Add CONFIDENTIAL stamp if relevant",
207
+ "Include date, to/from fields",
208
+ "Paper texture/fold marks",
209
+ ],
210
+ },
211
+ "browser_alert": {
212
+ "name": "Browser Alert",
213
+ "description": "Browser popup/alert dialog",
214
+ "visual_guidance": "Browser dialog box, alert icons, OK/Cancel buttons, system alert aesthetic",
215
+ "font_style": "System font, Segoe UI",
216
+ "colors": {
217
+ "primary": "#0078D4", # Windows blue
218
+ "secondary": "#D83B01", # Warning orange
219
+ "background": "#FFFFFF",
220
+ "text": "#000000",
221
+ },
222
+ "best_for": ["urgent warnings", "system alerts", "confirmation messages"],
223
+ "authenticity_tips": [
224
+ "Include browser chrome",
225
+ "Add alert icon",
226
+ "Button styling",
227
+ ],
228
+ },
229
+ "social_post": {
230
+ "name": "Social Media Post",
231
+ "description": "Facebook/Instagram post style",
232
+ "visual_guidance": "Social media feed post, user profile, likes/comments, authentic social feel",
233
+ "font_style": "Helvetica, system font",
234
+ "colors": {
235
+ "primary": "#1877F2", # Facebook blue
236
+ "secondary": "#E4405F", # Instagram pink
237
+ "background": "#FFFFFF",
238
+ "text": "#1C1E21",
239
+ },
240
+ "best_for": ["social proof", "user content", "organic feel"],
241
+ "authenticity_tips": [
242
+ "Include profile picture",
243
+ "Add like/comment counts",
244
+ "Use platform-specific formatting",
245
+ ],
246
+ },
247
+ "standard": {
248
+ "name": "Standard Ad",
249
+ "description": "Clean, professional ad format",
250
+ "visual_guidance": "Clean ad layout, professional design, clear headline and CTA",
251
+ "font_style": "Clean sans-serif fonts",
252
+ "colors": {
253
+ "primary": "#2196F3",
254
+ "secondary": "#FF9800",
255
+ "background": "#FFFFFF",
256
+ "text": "#212121",
257
+ },
258
+ "best_for": ["professional campaigns", "brand awareness", "general advertising"],
259
+ "authenticity_tips": [
260
+ "Clear visual hierarchy",
261
+ "Prominent CTA",
262
+ "Brand-consistent design",
263
+ ],
264
+ },
265
+ "telegram": {
266
+ "name": "Telegram",
267
+ "description": "Telegram chat message style",
268
+ "visual_guidance": "Telegram chat interface, blue message bubbles, Telegram UI, authentic messaging look",
269
+ "font_style": "Roboto, system font",
270
+ "colors": {
271
+ "primary": "#3390EC", # Telegram blue
272
+ "secondary": "#0088CC",
273
+ "background": "#FFFFFF",
274
+ "text": "#000000",
275
+ },
276
+ "best_for": ["personal messages", "group chats", "informal communication"],
277
+ "authenticity_tips": [
278
+ "Include Telegram UI elements",
279
+ "Use typical Telegram formatting",
280
+ "Add read receipts if relevant",
281
+ ],
282
+ },
283
+ "slack": {
284
+ "name": "Slack",
285
+ "description": "Slack workspace message style",
286
+ "visual_guidance": "Slack workspace interface, channel messages, Slack UI, professional team communication",
287
+ "font_style": "Lato, Slack font",
288
+ "colors": {
289
+ "primary": "#4A154B", # Slack purple
290
+ "secondary": "#36C5F0",
291
+ "background": "#FFFFFF",
292
+ "text": "#1D1C1D",
293
+ },
294
+ "best_for": ["team communication", "workplace announcements", "professional updates"],
295
+ "authenticity_tips": [
296
+ "Include channel name",
297
+ "Add user avatar",
298
+ "Use Slack message formatting",
299
+ ],
300
+ },
301
+ "instagram_story": {
302
+ "name": "Instagram Story",
303
+ "description": "Instagram story frame style",
304
+ "visual_guidance": "Instagram story format, vertical 9:16 aspect ratio, story UI elements, authentic Instagram look",
305
+ "font_style": "Instagram font, system font",
306
+ "colors": {
307
+ "primary": "#E4405F", # Instagram pink
308
+ "secondary": "#833AB4",
309
+ "background": "#000000",
310
+ "text": "#FFFFFF",
311
+ },
312
+ "best_for": ["social media engagement", "story-style content", "mobile-first ads"],
313
+ "authenticity_tips": [
314
+ "Vertical format (9:16)",
315
+ "Include story UI elements",
316
+ "Use Instagram-style fonts",
317
+ ],
318
+ },
319
+ "tiktok_style": {
320
+ "name": "TikTok Style",
321
+ "description": "TikTok video frame style",
322
+ "visual_guidance": "TikTok video frame, vertical format, TikTok UI overlay, authentic TikTok appearance",
323
+ "font_style": "TikTok font, bold sans-serif",
324
+ "colors": {
325
+ "primary": "#000000",
326
+ "secondary": "#FE2C55", # TikTok red
327
+ "background": "#000000",
328
+ "text": "#FFFFFF",
329
+ },
330
+ "best_for": ["youth engagement", "viral content", "trending topics"],
331
+ "authenticity_tips": [
332
+ "Vertical video format",
333
+ "Bold text overlays",
334
+ "Trending style elements",
335
+ ],
336
+ },
337
+ "linkedin_post": {
338
+ "name": "LinkedIn Post",
339
+ "description": "LinkedIn feed post style",
340
+ "visual_guidance": "LinkedIn feed post, professional network UI, LinkedIn interface, authentic professional look",
341
+ "font_style": "LinkedIn font, professional sans-serif",
342
+ "colors": {
343
+ "primary": "#0077B5", # LinkedIn blue
344
+ "secondary": "#000000",
345
+ "background": "#FFFFFF",
346
+ "text": "#000000",
347
+ },
348
+ "best_for": ["professional networking", "B2B marketing", "career-focused content"],
349
+ "authenticity_tips": [
350
+ "Include profile elements",
351
+ "Professional tone",
352
+ "LinkedIn-style formatting",
353
+ ],
354
+ },
355
+ "app_store_listing": {
356
+ "name": "App Store Listing",
357
+ "description": "App store screenshot style",
358
+ "visual_guidance": "App store listing screenshot, app icon, ratings, screenshots, authentic app store appearance",
359
+ "font_style": "San Francisco, system font",
360
+ "colors": {
361
+ "primary": "#007AFF", # iOS blue
362
+ "secondary": "#FF9500",
363
+ "background": "#FFFFFF",
364
+ "text": "#000000",
365
+ },
366
+ "best_for": ["app promotion", "mobile apps", "app features"],
367
+ "authenticity_tips": [
368
+ "Include app icon",
369
+ "Star ratings",
370
+ "App store UI elements",
371
+ ],
372
+ },
373
+ "email_signature": {
374
+ "name": "Email Signature",
375
+ "description": "Professional email signature style",
376
+ "visual_guidance": "Professional email signature, contact info, logo, authentic email signature appearance",
377
+ "font_style": "Arial, Helvetica, professional fonts",
378
+ "colors": {
379
+ "primary": "#000000",
380
+ "secondary": "#666666",
381
+ "background": "#FFFFFF",
382
+ "text": "#000000",
383
+ },
384
+ "best_for": ["professional communication", "B2B outreach", "formal announcements"],
385
+ "authenticity_tips": [
386
+ "Include contact information",
387
+ "Professional formatting",
388
+ "Company logo if relevant",
389
+ ],
390
+ },
391
+ }
392
+
393
+
394
+ def get_all_containers() -> Dict[str, Dict[str, Any]]:
395
+ """Get all available container types."""
396
+ return CONTAINER_TYPES
397
+
398
+
399
+ def get_container(key: str) -> Optional[Dict[str, Any]]:
400
+ """Get a specific container type by key."""
401
+ return CONTAINER_TYPES.get(key.lower().replace(" ", "_"))
402
+
403
+
404
+ def get_random_container() -> Dict[str, Any]:
405
+ """Get a random container type."""
406
+ key = random.choice(list(CONTAINER_TYPES.keys()))
407
+ return {"key": key, **CONTAINER_TYPES[key]}
408
+
409
+
410
+ def get_container_visual_guidance(container_key: str) -> str:
411
+ """Get visual guidance for a container type."""
412
+ container = get_container(container_key)
413
+ if container:
414
+ return container.get("visual_guidance", "")
415
+ return f"Container type: {container_key}, authentic appearance"
416
+
417
+
418
+ def get_native_containers() -> List[str]:
419
+ """Get container types that look like native app interfaces."""
420
+ native = ["imessage", "whatsapp", "sms", "system_notification", "push_notification", "email_notification"]
421
+ return native
422
+
423
+
424
+ def get_ugc_containers() -> List[str]:
425
+ """Get container types that look like user-generated content."""
426
+ ugc = ["reddit_post", "social_post", "sticky_note", "memo"]
427
+ return ugc
428
+
429
+
430
+ def get_alert_containers() -> List[str]:
431
+ """Get container types that create urgency."""
432
+ alerts = ["bank_alert", "news_chyron", "browser_alert", "system_notification"]
433
+ return alerts
434
+
data/frameworks.py ADDED
@@ -0,0 +1,393 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Ad Frameworks - Different structural approaches for ad creatives.
3
+ Each framework has a unique style and is suited for different marketing goals.
4
+ """
5
+
6
+ from typing import Dict, Any, List, Optional
7
+ import random
8
+
9
+
10
+ # Complete list of 10 ad frameworks with detailed configurations
11
+ FRAMEWORKS: Dict[str, Dict[str, Any]] = {
12
+ "breaking_news": {
13
+ "name": "Breaking News",
14
+ "description": "Creates urgency through news-style presentation, feels like breaking news",
15
+ "best_for": ["announcements", "limited offers", "new discoveries", "price drops"],
16
+ "visual_style": "News channel aesthetic, red/white ticker, urgent feel",
17
+ "hook_examples": [
18
+ "BREAKING: Home Insurance Rates Drop 40%",
19
+ "URGENT: Limited Time Savings Available Now",
20
+ "ALERT: New Insurance Savings Discovered",
21
+ "FLASH: This Week Only - Save $500",
22
+ "EXCLUSIVE: Secret Savings Method Revealed",
23
+ ],
24
+ "headline_style": "ALL CAPS with news prefix (BREAKING:, URGENT:, ALERT:)",
25
+ "tone": "Urgent, newsworthy, time-sensitive",
26
+ "psychological_triggers": ["FOMO", "Urgency", "Curiosity"],
27
+ },
28
+ "mobile_post": {
29
+ "name": "Mobile Post",
30
+ "description": "Optimized for mobile scrolling, short and punchy",
31
+ "best_for": ["quick engagement", "thumb-stopping content", "social feeds"],
32
+ "visual_style": "Mobile-first design, clean, easy to read on small screens",
33
+ "hook_examples": [
34
+ "Save $500/Year in 2 Minutes",
35
+ "Get Your Quote - Tap to Start",
36
+ "See Your Savings Instantly",
37
+ "One tap. Big savings.",
38
+ "30 seconds to lower rates",
39
+ ],
40
+ "headline_style": "Short, punchy, action-oriented (under 8 words)",
41
+ "tone": "Quick, casual, conversational",
42
+ "psychological_triggers": ["Convenience", "Speed", "Ease"],
43
+ },
44
+ "before_after": {
45
+ "name": "Before/After",
46
+ "description": "Shows transformation, contrast between old and new state",
47
+ "best_for": ["transformations", "improvements", "results showcase"],
48
+ "visual_style": "Split-screen comparison, clear before/after contrast",
49
+ "hook_examples": [
50
+ "From $200/Month to $50/Month",
51
+ "Before: High Rates. After: Big Savings",
52
+ "Transform Your Insurance Costs Today",
53
+ "See What Changed Everything",
54
+ "The Difference One Call Made",
55
+ ],
56
+ "headline_style": "Comparison format, specific numbers, transformation language",
57
+ "tone": "Results-focused, proof-based, transformation",
58
+ "psychological_triggers": ["Transformation", "Proof", "Results"],
59
+ },
60
+ "testimonial": {
61
+ "name": "Testimonial",
62
+ "description": "Social proof through customer stories and reviews",
63
+ "best_for": ["trust building", "social proof", "credibility"],
64
+ "visual_style": "Quote cards, star ratings, real customer photos",
65
+ "hook_examples": [
66
+ "Join 50,000+ Happy Customers",
67
+ "Rated 5 Stars by Thousands",
68
+ "See Why Customers Love Us",
69
+ '"Best decision I ever made"',
70
+ "Real people. Real savings.",
71
+ ],
72
+ "headline_style": "Quote format, numbers, social proof indicators",
73
+ "tone": "Trustworthy, relatable, authentic",
74
+ "psychological_triggers": ["Social Proof", "Trust", "Belonging"],
75
+ },
76
+ "lifestyle": {
77
+ "name": "Lifestyle",
78
+ "description": "Aspirational imagery showing the desired lifestyle",
79
+ "best_for": ["aspiration", "emotional connection", "dream selling"],
80
+ "visual_style": "Aspirational imagery, happy people, dream scenarios",
81
+ "hook_examples": [
82
+ "Live Life Fully Protected",
83
+ "Drive with Complete Confidence",
84
+ "Peace of Mind on Every Journey",
85
+ "The Life You Deserve",
86
+ "Freedom Starts Here",
87
+ ],
88
+ "headline_style": "Aspirational, emotional, lifestyle-focused",
89
+ "tone": "Aspirational, emotional, inspiring",
90
+ "psychological_triggers": ["Aspiration", "Desire", "Freedom"],
91
+ },
92
+ "educational": {
93
+ "name": "Educational",
94
+ "description": "Provides value through information and tips",
95
+ "best_for": ["authority building", "value-first approach", "complex topics"],
96
+ "visual_style": "Infographic style, lists, clear information hierarchy",
97
+ "hook_examples": [
98
+ "3 Things Your Insurance Agent Won't Tell You",
99
+ "The Hidden Costs of Cheap Insurance",
100
+ "What Every Homeowner Must Know",
101
+ "Learn the Secret to Lower Rates",
102
+ "5 Mistakes That Cost You Money",
103
+ ],
104
+ "headline_style": "Numbers, curiosity gaps, valuable information promise",
105
+ "tone": "Informative, helpful, authoritative",
106
+ "psychological_triggers": ["Curiosity", "Knowledge", "Fear of Missing Out"],
107
+ },
108
+ "comparison": {
109
+ "name": "Comparison",
110
+ "description": "Compares to competitors or alternatives",
111
+ "best_for": ["differentiation", "competitive positioning", "value demonstration"],
112
+ "visual_style": "Side-by-side comparison, checkmarks vs X marks",
113
+ "hook_examples": [
114
+ "Why Customers Are Switching",
115
+ "The Difference Is Clear",
116
+ "Compare and Save Hundreds",
117
+ "What Others Charge vs Our Price",
118
+ "See the Better Option",
119
+ ],
120
+ "headline_style": "Comparison language, competitive positioning",
121
+ "tone": "Confident, comparative, fact-based",
122
+ "psychological_triggers": ["Comparison", "Value", "Smart Choice"],
123
+ },
124
+ "storytelling": {
125
+ "name": "Storytelling",
126
+ "description": "Narrative-driven content that tells a story",
127
+ "best_for": ["emotional connection", "memorable content", "brand building"],
128
+ "visual_style": "Narrative imagery, sequential scenes, story elements",
129
+ "hook_examples": [
130
+ "When Sarah Lost Everything...",
131
+ "The Day That Changed My Life",
132
+ "I Never Thought It Would Happen to Me",
133
+ "Here's What Happened Next",
134
+ "The Story Nobody Tells You",
135
+ ],
136
+ "headline_style": "Narrative hooks, story beginnings, curiosity builders",
137
+ "tone": "Narrative, emotional, personal",
138
+ "psychological_triggers": ["Empathy", "Curiosity", "Emotion"],
139
+ },
140
+ "problem_solution": {
141
+ "name": "Problem/Solution",
142
+ "description": "Identifies a problem and presents the solution",
143
+ "best_for": ["pain point targeting", "solution selling", "problem awareness"],
144
+ "visual_style": "Problem visualization, solution presentation, relief imagery",
145
+ "hook_examples": [
146
+ "Tired of High Insurance Rates?",
147
+ "Finally, a Solution That Works",
148
+ "The Problem Nobody Talks About",
149
+ "End Your Insurance Frustration",
150
+ "There's a Better Way",
151
+ ],
152
+ "headline_style": "Problem questions, solution statements, relief language",
153
+ "tone": "Empathetic, problem-aware, solution-focused",
154
+ "psychological_triggers": ["Pain Relief", "Problem Awareness", "Hope"],
155
+ },
156
+ "authority": {
157
+ "name": "Authority",
158
+ "description": "Establishes expertise and credibility",
159
+ "best_for": ["credibility building", "expert positioning", "trust establishment"],
160
+ "visual_style": "Professional imagery, credentials, expert endorsements",
161
+ "hook_examples": [
162
+ "Expert-Recommended Insurance",
163
+ "Backed by 50 Years of Experience",
164
+ "What the Pros Know About Insurance",
165
+ "Industry-Leading Protection",
166
+ "Trusted by Professionals",
167
+ ],
168
+ "headline_style": "Authority indicators, credentials, expert language",
169
+ "tone": "Professional, authoritative, trustworthy",
170
+ "psychological_triggers": ["Authority", "Trust", "Expertise"],
171
+ },
172
+ "scarcity": {
173
+ "name": "Scarcity",
174
+ "description": "Creates urgency through limited availability",
175
+ "best_for": ["limited offers", "exclusive deals", "time-sensitive promotions"],
176
+ "visual_style": "Countdown timers, limited stock indicators, exclusive badges",
177
+ "hook_examples": [
178
+ "Only 50 Spots Left This Month",
179
+ "Limited Availability - Act Fast",
180
+ "Exclusive Offer for First 100",
181
+ "While Supplies Last",
182
+ "Don't Miss Your Chance",
183
+ ],
184
+ "headline_style": "Numbers, time limits, availability language",
185
+ "tone": "Urgent, exclusive, time-sensitive",
186
+ "psychological_triggers": ["FOMO", "Urgency", "Exclusivity"],
187
+ },
188
+ "benefit_stack": {
189
+ "name": "Benefit Stack",
190
+ "description": "Lists multiple benefits in quick succession",
191
+ "best_for": ["value demonstration", "feature highlights", "quick scanning"],
192
+ "visual_style": "Bullet points, checkmarks, organized list format",
193
+ "hook_examples": [
194
+ "Save Money, Save Time, Save Stress",
195
+ "3 Benefits in One Solution",
196
+ "Everything You Need, All in One",
197
+ "More Coverage, Lower Cost, Better Service",
198
+ "Protection + Savings + Peace of Mind",
199
+ ],
200
+ "headline_style": "Multiple benefits, parallel structure, value stacking",
201
+ "tone": "Value-focused, comprehensive, efficient",
202
+ "psychological_triggers": ["Value", "Convenience", "Completeness"],
203
+ },
204
+ "risk_reversal": {
205
+ "name": "Risk Reversal",
206
+ "description": "Removes risk and uncertainty from the decision",
207
+ "best_for": ["overcoming objections", "building confidence", "reducing hesitation"],
208
+ "visual_style": "Guarantee badges, risk-free indicators, confidence builders",
209
+ "hook_examples": [
210
+ "100% Risk-Free Guarantee",
211
+ "Try It Free - No Commitment",
212
+ "Cancel Anytime, No Questions",
213
+ "Money-Back Guarantee",
214
+ "No Risk, All Reward",
215
+ ],
216
+ "headline_style": "Guarantee language, risk removal, confidence builders",
217
+ "tone": "Reassuring, confident, risk-free",
218
+ "psychological_triggers": ["Security", "Trust", "Risk Reduction"],
219
+ },
220
+ "contrarian": {
221
+ "name": "Contrarian",
222
+ "description": "Challenges conventional wisdom or expectations",
223
+ "best_for": ["differentiation", "attention-grabbing", "thought leadership"],
224
+ "visual_style": "Bold statements, unexpected visuals, pattern breaks",
225
+ "hook_examples": [
226
+ "Why Everything You Know About Insurance Is Wrong",
227
+ "The Unpopular Truth About Rates",
228
+ "Stop Following the Crowd",
229
+ "The Counter-Intuitive Way to Save",
230
+ "What They Don't Want You to Know",
231
+ ],
232
+ "headline_style": "Contrarian statements, pattern breaks, unexpected angles",
233
+ "tone": "Bold, provocative, thought-provoking",
234
+ "psychological_triggers": ["Curiosity", "Differentiation", "Intellectual"],
235
+ },
236
+ "case_study": {
237
+ "name": "Case Study",
238
+ "description": "Shows real results and specific outcomes",
239
+ "best_for": ["proof demonstration", "results showcase", "credibility"],
240
+ "visual_style": "Before/after data, specific numbers, real examples",
241
+ "hook_examples": [
242
+ "How Sarah Saved $1,200 in 6 Months",
243
+ "Real Results from Real Customers",
244
+ "The Exact Steps That Worked",
245
+ "See the Numbers That Matter",
246
+ "From Problem to Solution: The Journey",
247
+ ],
248
+ "headline_style": "Specific examples, real names, concrete results",
249
+ "tone": "Proof-based, specific, results-focused",
250
+ "psychological_triggers": ["Proof", "Social Proof", "Results"],
251
+ },
252
+ "interactive": {
253
+ "name": "Interactive",
254
+ "description": "Engages through questions, quizzes, or participation",
255
+ "best_for": ["engagement", "personalization", "interaction"],
256
+ "visual_style": "Interactive elements, questions, quiz formats",
257
+ "hook_examples": [
258
+ "Take Our 30-Second Quiz",
259
+ "Answer 3 Questions, Get Your Rate",
260
+ "See If You Qualify in 60 Seconds",
261
+ "What's Your Insurance Personality?",
262
+ "Find Your Perfect Match",
263
+ ],
264
+ "headline_style": "Questions, interactive prompts, participation language",
265
+ "tone": "Engaging, interactive, personalized",
266
+ "psychological_triggers": ["Engagement", "Personalization", "Curiosity"],
267
+ },
268
+ }
269
+
270
+ # Framework examples by niche
271
+ NICHE_FRAMEWORK_EXAMPLES: Dict[str, Dict[str, List[str]]] = {
272
+ "home_insurance": {
273
+ "breaking_news": [
274
+ "BREAKING: Home Insurance Rates Drop 40%",
275
+ "ALERT: New Homeowner Discounts Available",
276
+ "URGENT: Rate Freeze Ends Friday",
277
+ ],
278
+ "mobile_post": [
279
+ "Protect Your Home in 3 Minutes",
280
+ "One Quote. Big Savings.",
281
+ "Tap to See Your Rate",
282
+ ],
283
+ "before_after": [
284
+ "Before: $2,400/year. After: $1,200/year",
285
+ "Old policy vs New savings",
286
+ "What switching saved me",
287
+ ],
288
+ "testimonial": [
289
+ '"I saved $1,200 on my first year"',
290
+ "Join 100,000+ protected homeowners",
291
+ "Rated #1 by customers like you",
292
+ ],
293
+ "problem_solution": [
294
+ "Worried your home isn't covered?",
295
+ "Stop overpaying for insurance",
296
+ "End the coverage gaps",
297
+ ],
298
+ },
299
+ "glp1": {
300
+ "breaking_news": [
301
+ "NEW: FDA-Approved Weight Loss Solution",
302
+ "ALERT: Limited Appointments Available",
303
+ "EXCLUSIVE: Online Consultations Now Open",
304
+ ],
305
+ "before_after": [
306
+ "Her 60-Day Transformation",
307
+ "What Changed in 90 Days",
308
+ "The Results Speak for Themselves",
309
+ ],
310
+ "testimonial": [
311
+ '"I finally found what works"',
312
+ "Thousands have transformed",
313
+ "Real patients. Real results.",
314
+ ],
315
+ "lifestyle": [
316
+ "Feel Confident Again",
317
+ "The Energy to Live Fully",
318
+ "Your New Chapter Starts Here",
319
+ ],
320
+ "authority": [
321
+ "Doctor-Recommended Solution",
322
+ "Clinically Proven Results",
323
+ "Backed by Medical Research",
324
+ ],
325
+ "scarcity": [
326
+ "Only 20 Appointments This Week",
327
+ "Limited Spots Available",
328
+ "Join the Waitlist Now",
329
+ ],
330
+ "risk_reversal": [
331
+ "100% Satisfaction Guarantee",
332
+ "Try Risk-Free for 30 Days",
333
+ "No Commitment Required",
334
+ ],
335
+ "case_study": [
336
+ "How Maria Lost 30 Pounds in 3 Months",
337
+ "Real Patient Results",
338
+ "The Transformation Journey",
339
+ ],
340
+ },
341
+ }
342
+
343
+
344
+ def get_all_frameworks() -> Dict[str, Dict[str, Any]]:
345
+ """Get all available frameworks."""
346
+ return FRAMEWORKS
347
+
348
+
349
+ def get_framework(key: str) -> Optional[Dict[str, Any]]:
350
+ """Get a specific framework by key."""
351
+ return FRAMEWORKS.get(key)
352
+
353
+
354
+ def get_random_framework() -> Dict[str, Any]:
355
+ """Get a random framework."""
356
+ key = random.choice(list(FRAMEWORKS.keys()))
357
+ return {"key": key, **FRAMEWORKS[key]}
358
+
359
+
360
+ def get_frameworks_for_niche(niche: str, count: int = 3) -> List[Dict[str, Any]]:
361
+ """Get recommended frameworks for a niche."""
362
+ niche_lower = niche.lower().replace(" ", "_").replace("-", "_")
363
+
364
+ # Niche-specific framework preferences
365
+ niche_preferences = {
366
+ "home_insurance": ["testimonial", "problem_solution", "authority", "before_after", "lifestyle"],
367
+ "glp1": ["before_after", "testimonial", "lifestyle", "authority", "problem_solution"],
368
+ }
369
+
370
+ # Get preferred frameworks or use all
371
+ preferred_keys = niche_preferences.get(niche_lower, list(FRAMEWORKS.keys()))
372
+
373
+ # Add remaining frameworks for variety
374
+ all_keys = preferred_keys + [k for k in FRAMEWORKS.keys() if k not in preferred_keys]
375
+
376
+ # Select count frameworks with some randomization
377
+ selected = all_keys[:count]
378
+ random.shuffle(selected)
379
+
380
+ return [{"key": k, **FRAMEWORKS[k]} for k in selected]
381
+
382
+
383
+ def get_framework_hook_examples(framework_key: str, niche: Optional[str] = None) -> List[str]:
384
+ """Get hook examples for a framework, optionally niche-specific."""
385
+ if niche:
386
+ niche_key = niche.lower().replace(" ", "_").replace("-", "_")
387
+ niche_examples = NICHE_FRAMEWORK_EXAMPLES.get(niche_key, {}).get(framework_key, [])
388
+ if niche_examples:
389
+ return niche_examples
390
+
391
+ framework = FRAMEWORKS.get(framework_key)
392
+ return framework.get("hook_examples", []) if framework else []
393
+
data/glp1.py ADDED
@@ -0,0 +1,683 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ GLP-1 (Weight Loss) - Complete Psychological Arsenal
3
+ All strategies: shame, transformation, FOMO, authority, simplicity, etc.
4
+ Updated with winning ad patterns from high-converting creative analysis.
5
+ """
6
+
7
+ # Strategy categories with hooks and descriptions
8
+ STRATEGIES = {
9
+ # ==========================================================================
10
+ # NEW: WINNING STRATEGIES FROM HIGH-CONVERTING AD ANALYSIS
11
+ # ==========================================================================
12
+
13
+ "accusation_opener": {
14
+ "name": "Accusation Opener",
15
+ "description": "Direct accusation triggers immediate emotional response",
16
+ "hooks": [
17
+ "Still Overweight?",
18
+ "Another Failed Diet?",
19
+ "Still Hiding Your Body?",
20
+ "Tired Of The Lies?",
21
+ "How Many More Diets Will Fail?",
22
+ "Still Making Excuses?",
23
+ "Wasted Money On Diets That Don't Work?",
24
+ "Why Are You Still Struggling?",
25
+ "Avoiding The Mirror Again?",
26
+ "How Long Will You Wait?",
27
+ "Still Wearing Baggy Clothes To Hide?",
28
+ "Done Lying To Yourself?",
29
+ ],
30
+ "visual_styles": [
31
+ "person on scale looking frustrated, candid documentary",
32
+ "oversized clothes on person, accusatory framing",
33
+ "mirror reflection with accusatory text overlay",
34
+ "pile of diet books and unused gym equipment",
35
+ "frustrated person looking at reflection, vintage feel",
36
+ "before photo style, raw and honest",
37
+ ],
38
+ },
39
+
40
+ "curiosity_gap": {
41
+ "name": "Curiosity Gap",
42
+ "description": "Open loop with 'THIS' or 'Secret' demands click",
43
+ "hooks": [
44
+ "Thousands Are Losing Weight After Discovering THIS",
45
+ "Doctors Are Prescribing THIS Instead Of Diets",
46
+ "What Celebrities Have Used For Years",
47
+ "The Weight Loss Secret They Tried To Hide",
48
+ "Everyone's Losing Weight Except You. Here's Why.",
49
+ "What Hollywood Knew For Years Is Now Available",
50
+ "After THIS, Dieting Becomes Obsolete",
51
+ "The ONE Thing That Actually Works",
52
+ "What Your Doctor Isn't Telling You",
53
+ "People Are Ditching Diets For THIS",
54
+ "The Method That's Changing Everything",
55
+ "Discover What Everyone's Talking About",
56
+ ],
57
+ "visual_styles": [
58
+ "transformation collage with 'THIS' callout, documentary style",
59
+ "person revealing secret, candid testimonial",
60
+ "before/after with mysterious 'secret' overlay",
61
+ "celebrity transformation reference, tabloid aesthetic",
62
+ "dramatic reveal moment, old footage feel",
63
+ "person sharing discovery, authentic UGC style",
64
+ ],
65
+ },
66
+
67
+ "specific_numbers": {
68
+ "name": "Specific Numbers",
69
+ "description": "Oddly specific numbers create instant believability",
70
+ "hooks": [
71
+ "Lost 47 lbs In 90 Days",
72
+ "Down 23 lbs In 6 Weeks",
73
+ "Dropped 4 Dress Sizes In 8 Weeks",
74
+ "Lost 67 lbs Without Exercise",
75
+ "From 247 lbs to 168 lbs",
76
+ "12 lbs Gone In The First Month",
77
+ "Lost 38 lbs Before Her Reunion",
78
+ "Dropped 52 lbs After Starting THIS",
79
+ "91 Days: Lost 41 lbs",
80
+ "From Size 18 to Size 8",
81
+ "Lost 1.2 lbs Per Day",
82
+ "Down 29 lbs In 10 Weeks",
83
+ ],
84
+ "visual_styles": [
85
+ "scale showing specific number, close-up documentary",
86
+ "before/after with specific weight numbers visible",
87
+ "measuring tape showing inches lost",
88
+ "old jeans that are now too big, specific sizes shown",
89
+ "calendar tracking progress with numbers",
90
+ "weight loss chart with specific data points",
91
+ ],
92
+ },
93
+
94
+ "before_after_proof": {
95
+ "name": "Before/After Proof",
96
+ "description": "Transformation proof creates immediate desire",
97
+ "hooks": [
98
+ "Same Person. 90 Days Apart.",
99
+ "Is This Even The Same Person?",
100
+ "The Transformation That Shocked Everyone",
101
+ "From THIS to THIS In 12 Weeks",
102
+ "Unrecognizable In 90 Days",
103
+ "Her Friends Didn't Recognize Her",
104
+ "The Picture That Went Viral",
105
+ "From Hiding To Showing Off",
106
+ "Watch The Transformation",
107
+ "Before Anyone Asks: Yes, It's The Same Person",
108
+ "Jaw-Dropping 90-Day Results",
109
+ "The Change Everyone's Talking About",
110
+ ],
111
+ "visual_styles": [
112
+ "dramatic split-screen before/after, same outfit",
113
+ "timeline showing 30/60/90 day progress",
114
+ "same location before/after comparison",
115
+ "loose pants/old clothes proof shot",
116
+ "side-by-side full body transformation",
117
+ "progress photos documentary style",
118
+ ],
119
+ },
120
+
121
+ "quiz_interactive": {
122
+ "name": "Quiz/Interactive",
123
+ "description": "Quiz format drives engagement and self-selection",
124
+ "hooks": [
125
+ "How Much Weight Could You Lose?",
126
+ "Take The 30-Second Quiz",
127
+ "What's Your Weight Loss Type?",
128
+ "See If You Qualify",
129
+ "Answer 3 Questions. See Your Results.",
130
+ "Calculate Your Potential Weight Loss",
131
+ "Find Your Starting Point",
132
+ "Check Your Eligibility Now",
133
+ "Tap Your Age To See Results",
134
+ "What's Your Goal Weight?",
135
+ "Select Your Starting Weight",
136
+ "How Much Do You Want To Lose?",
137
+ ],
138
+ "visual_styles": [
139
+ "quiz interface with weight options, app screenshot",
140
+ "goal weight selector, clean UI",
141
+ "Notes app style checklist: 10-20 lbs, 20-40 lbs, 40+ lbs",
142
+ "interactive calculator showing potential results",
143
+ "age/weight selector buttons",
144
+ "eligibility checker interface mockup",
145
+ ],
146
+ },
147
+
148
+ "authority_transfer": {
149
+ "name": "Authority Transfer",
150
+ "description": "Transfer trust from medical/celebrity authority",
151
+ "hooks": [
152
+ "FDA-Approved Weight Loss",
153
+ "Doctor-Prescribed. Clinically Proven.",
154
+ "Used By 15 Million Patients",
155
+ "What Doctors Prescribe Their Own Families",
156
+ "The Medical Breakthrough Of The Decade",
157
+ "Backed By Harvard Research",
158
+ "Endorsed By Leading Endocrinologists",
159
+ "The Prescription Celebrities Pay Thousands For",
160
+ "Clinically Proven 20%+ Weight Loss",
161
+ "What Oprah, Elon And Millions Know",
162
+ "Peer-Reviewed Science. Real Results.",
163
+ "The Treatment Doctors Trust",
164
+ ],
165
+ "visual_styles": [
166
+ "FDA approval badge, medical document aesthetic",
167
+ "doctor in white coat, professional clinical setting",
168
+ "scientific study graphics, research paper style",
169
+ "celebrity transformation reference, tabloid feel",
170
+ "medical prescription pad aesthetic",
171
+ "clinical trial results visualization",
172
+ ],
173
+ },
174
+
175
+ "identity_targeting": {
176
+ "name": "Identity Targeting",
177
+ "description": "Direct demographic callout creates self-selection",
178
+ "hooks": [
179
+ "Women Over 40: This Changes Everything",
180
+ "If You've Tried Every Diet And Failed...",
181
+ "For People Who've Struggled For Years",
182
+ "Busy Moms Are Losing Weight Without Dieting",
183
+ "Over 50 And Struggling To Lose Weight?",
184
+ "If Nothing Has Worked For You Before...",
185
+ "For Anyone Who's Given Up On Diets",
186
+ "People With 30+ lbs To Lose",
187
+ "If You've Been Overweight For 10+ Years",
188
+ "Yo-Yo Dieters: This Is Different",
189
+ "For Those Who Hate The Gym",
190
+ "If Willpower Has Never Been Enough",
191
+ ],
192
+ "visual_styles": [
193
+ "relatable person matching demographic, candid shot",
194
+ "real woman 40-60 in transformation moment",
195
+ "busy mom in everyday setting, authentic",
196
+ "person in regular clothes, not fitness model",
197
+ "testimonial portrait, trustworthy face",
198
+ "before/after of relatable person",
199
+ ],
200
+ },
201
+
202
+ "insider_secret": {
203
+ "name": "Insider Secret",
204
+ "description": "Exclusivity and hidden knowledge framing",
205
+ "hooks": [
206
+ "Hollywood's Best-Kept Secret",
207
+ "What Big Pharma Doesn't Want You To Know",
208
+ "The Industry Secret Finally Revealed",
209
+ "How Celebrities Really Lose Weight",
210
+ "The Method They Tried To Keep Hidden",
211
+ "What Doctors Know But Won't Tell You",
212
+ "The Weight Loss Trick No One Talks About",
213
+ "Finally: The Truth About Weight Loss",
214
+ "What The Diet Industry Hides",
215
+ "The Secret Behind Celebrity Transformations",
216
+ "Insider Knowledge Now Available To Everyone",
217
+ "The Method They Don't Advertise",
218
+ ],
219
+ "visual_styles": [
220
+ "reveal/exposed documentary style",
221
+ "person sharing secret, intimate UGC feel",
222
+ "tabloid expose aesthetic, revealing truth",
223
+ "hidden document being shown",
224
+ "before/after celebrity transformation style",
225
+ "insider testimonial, candid shot",
226
+ ],
227
+ },
228
+
229
+ # ==========================================================================
230
+ # ORIGINAL STRATEGIES (UPDATED WITH VINTAGE VISUAL STYLES)
231
+ # ==========================================================================
232
+
233
+ "shame_insecurity": {
234
+ "name": "Shame & Insecurity",
235
+ "description": "Trigger personal shame and body insecurity",
236
+ "hooks": [
237
+ "Tired of hiding your body?",
238
+ "Still wearing baggy clothes?",
239
+ "How long will you keep lying to yourself?",
240
+ "The mirror doesn't lie",
241
+ "Everyone notices. They just don't say it.",
242
+ "Summer is coming...",
243
+ "When's the last time you felt confident?",
244
+ "Still avoiding photos?",
245
+ "How many more events will you skip?",
246
+ "Your body is holding you back",
247
+ "Stop making excuses",
248
+ "The scale doesn't lie. Neither does your reflection.",
249
+ "Are you embarrassed to undress?",
250
+ "How many diets have failed you?",
251
+ ],
252
+ "visual_styles": [
253
+ "person looking in mirror, disappointed, vintage film grain",
254
+ "oversized clothes, hiding body, documentary candid",
255
+ "scale showing high number, old footage aesthetic",
256
+ "person avoiding camera, raw authentic moment",
257
+ "closet full of clothes that don't fit, nostalgic feel",
258
+ "person looking at old photos, sepia tones",
259
+ ],
260
+ },
261
+
262
+ "transformation_desire": {
263
+ "name": "Transformation & Desire",
264
+ "description": "Appeal to transformation and aspiration",
265
+ "hooks": [
266
+ "Imagine fitting into your old jeans",
267
+ "Get your confidence back",
268
+ "Become the person you were meant to be",
269
+ "Your best self is waiting",
270
+ "Turn heads again",
271
+ "Feel sexy for the first time in years",
272
+ "Reclaim your body",
273
+ "The glow-up you deserve",
274
+ "Finally love what you see in the mirror",
275
+ "Your transformation starts today",
276
+ "Become unrecognizable in 90 days",
277
+ "The body you've always wanted",
278
+ "Unlock the best version of yourself",
279
+ "From invisible to irresistible",
280
+ ],
281
+ "visual_styles": [
282
+ "dramatic before/after transformation",
283
+ "confident person in fitted clothes",
284
+ "person checking out reflection, smiling",
285
+ "beach body, confidence pose",
286
+ "person getting compliments",
287
+ "transformation timeline photos",
288
+ ],
289
+ },
290
+
291
+ "fomo": {
292
+ "name": "Fear of Missing Out",
293
+ "description": "Create fear of being left behind",
294
+ "hooks": [
295
+ "Hollywood's secret is finally available",
296
+ "Celebrities have used this for years",
297
+ "Everyone is losing weight except you",
298
+ "Don't be the last to know",
299
+ "Limited supply - high demand",
300
+ "Waitlist growing daily",
301
+ "The weight loss revolution you're missing",
302
+ "While you're reading this, someone is transforming",
303
+ "Why does everyone look better except you?",
304
+ "The secret they tried to hide from you",
305
+ "TikTok's viral weight loss trend",
306
+ "The A-list solution now available to you",
307
+ "What Oprah, Elon, and millions know",
308
+ "The shortage is real. Act now.",
309
+ ],
310
+ "visual_styles": [
311
+ "celebrity transformation reference",
312
+ "trending hashtag graphics",
313
+ "viral social media aesthetic",
314
+ "before/after celebrity style",
315
+ "exclusive access imagery",
316
+ "sold out, limited availability badges",
317
+ ],
318
+ },
319
+
320
+ "social_rejection_acceptance": {
321
+ "name": "Social Rejection & Acceptance",
322
+ "description": "Leverage social dynamics and dating",
323
+ "hooks": [
324
+ "They'll finally see the REAL you",
325
+ "Stop being invisible",
326
+ "Dating becomes easier",
327
+ "People treat you differently when you're thin",
328
+ "Your ex will regret leaving",
329
+ "Make them jealous",
330
+ "Get the attention you deserve",
331
+ "Stop being overlooked",
332
+ "First impressions matter. Yours is failing.",
333
+ "They're judging you. Change the verdict.",
334
+ "Imagine being the hot one in your friend group",
335
+ "Turn rejection into attraction",
336
+ "From friend-zoned to desired",
337
+ "Become someone people want to be around",
338
+ ],
339
+ "visual_styles": [
340
+ "confident person at social event",
341
+ "person getting attention, admired",
342
+ "dating app success aesthetic",
343
+ "group of friends, confident person",
344
+ "romantic couple, attractive partners",
345
+ "person commanding room, confident",
346
+ ],
347
+ },
348
+
349
+ "medical_authority": {
350
+ "name": "Medical Authority",
351
+ "description": "Leverage medical credibility and science",
352
+ "hooks": [
353
+ "FDA-approved",
354
+ "Doctor-prescribed",
355
+ "Clinically proven",
356
+ "Used by 15 million patients",
357
+ "Backed by Harvard research",
358
+ "Your doctor won't tell you about this",
359
+ "Big Pharma's best-kept secret",
360
+ "The science is undeniable",
361
+ "Medical breakthrough of the decade",
362
+ "Doctors are prescribing this to their own families",
363
+ "Clinical trials prove 20%+ weight loss",
364
+ "The medication celebrities pay thousands for",
365
+ "Peer-reviewed. Science-backed. Life-changing.",
366
+ "What every endocrinologist knows",
367
+ ],
368
+ "visual_styles": [
369
+ "doctor in white coat, professional",
370
+ "FDA approval badge",
371
+ "clinical study graphics",
372
+ "medical office setting",
373
+ "scientific research aesthetic",
374
+ "prescription medication style",
375
+ ],
376
+ },
377
+
378
+ "urgency_scarcity": {
379
+ "name": "Urgency & Scarcity",
380
+ "description": "Create time pressure and limited availability",
381
+ "hooks": [
382
+ "Shortage alert - limited doses available",
383
+ "Insurance may stop covering soon",
384
+ "Prices increasing next month",
385
+ "Get in before the waitlist",
386
+ "Supply chain issues - act now",
387
+ "Only accepting 50 new patients this month",
388
+ "The shortage is getting worse",
389
+ "Pharmacy stock running low",
390
+ "Manufacturer limiting supply",
391
+ "Price hike coming in 30 days",
392
+ "Don't miss the enrollment window",
393
+ "Available for a limited time",
394
+ ],
395
+ "visual_styles": [
396
+ "countdown timer, urgent",
397
+ "low stock warning",
398
+ "sold out badges",
399
+ "limited availability graphics",
400
+ "pharmacy shelves nearly empty",
401
+ "urgent notification style",
402
+ ],
403
+ },
404
+
405
+ "guilt": {
406
+ "name": "Guilt",
407
+ "description": "Trigger guilt about health and family",
408
+ "hooks": [
409
+ "Your kids need a healthy parent",
410
+ "Don't miss their graduation",
411
+ "How many more years will you lose?",
412
+ "Life is passing you by",
413
+ "You deserve to feel good",
414
+ "Stop punishing yourself",
415
+ "Your family needs you around",
416
+ "What example are you setting?",
417
+ "They're worried about you",
418
+ "You're not just hurting yourself",
419
+ "Your grandkids deserve to know you",
420
+ "Every pound is a year off your life",
421
+ "You owe it to yourself",
422
+ "Stop letting yourself down",
423
+ ],
424
+ "visual_styles": [
425
+ "parent with children, health focus",
426
+ "family moments, active lifestyle",
427
+ "grandparent playing with grandkids",
428
+ "health warning, medical imagery",
429
+ "funeral aesthetic, mortality reminder",
430
+ "empty chair at family gathering",
431
+ ],
432
+ },
433
+
434
+ "simplicity_laziness": {
435
+ "name": "Simplicity & Laziness",
436
+ "description": "Emphasize ease and minimal effort",
437
+ "hooks": [
438
+ "No diet. No exercise. Just results.",
439
+ "One injection, that's it",
440
+ "Lose weight while you sleep",
441
+ "The lazy way to get thin",
442
+ "Stop torturing yourself with diets",
443
+ "Finally, something that actually works",
444
+ "No gym required",
445
+ "Eat what you want and still lose weight",
446
+ "The effortless weight loss solution",
447
+ "Works while you Netflix",
448
+ "No willpower needed",
449
+ "The anti-diet diet",
450
+ "Stop counting calories forever",
451
+ "The 10-second daily routine that melts fat",
452
+ ],
453
+ "visual_styles": [
454
+ "person relaxing, effortless",
455
+ "simple injection graphic",
456
+ "no gym, no diet icons",
457
+ "person eating favorite foods",
458
+ "couch to confidence transformation",
459
+ "simple 1-2-3 step process",
460
+ ],
461
+ },
462
+
463
+ "comparison_envy": {
464
+ "name": "Comparison & Envy",
465
+ "description": "Compare to others who succeeded",
466
+ "hooks": [
467
+ "She lost 47 lbs. What's your excuse?",
468
+ "Same age. Different body.",
469
+ "While you're reading this, someone is getting results",
470
+ "Your coworker's secret weapon",
471
+ "Why does she look 10 years younger?",
472
+ "They started where you are now",
473
+ "Same starting point. Different ending.",
474
+ "She was bigger than you. Look at her now.",
475
+ "What's her secret? (Now it's yours)",
476
+ "Your high school reunion is coming...",
477
+ "Everyone's getting results except you",
478
+ "They found the answer. Why haven't you?",
479
+ ],
480
+ "visual_styles": [
481
+ "side by side comparison",
482
+ "success story testimonial",
483
+ "before/after same person",
484
+ "two people comparison",
485
+ "results showcase gallery",
486
+ "transformation collage",
487
+ ],
488
+ },
489
+
490
+ "before_after_shock": {
491
+ "name": "Before/After Shock",
492
+ "description": "Dramatic visual transformations",
493
+ "hooks": [
494
+ "90 days. 50 lbs. Real person.",
495
+ "The same person. Unbelievable.",
496
+ "Is this even the same person?",
497
+ "Wait until you see the after...",
498
+ "Jaw-dropping transformation",
499
+ "You won't believe your eyes",
500
+ "The most incredible transformation ever",
501
+ "From XL to S in 4 months",
502
+ "Lost half her body weight",
503
+ "From plus-size to model",
504
+ "The picture that broke the internet",
505
+ "This can be you",
506
+ ],
507
+ "visual_styles": [
508
+ "dramatic before/after split screen",
509
+ "timeline transformation 30/60/90 days",
510
+ "same outfit, different body",
511
+ "scale showing massive loss",
512
+ "measuring tape victory",
513
+ "loose pants, weight loss proof",
514
+ ],
515
+ },
516
+
517
+ "future_pacing": {
518
+ "name": "Future Pacing",
519
+ "description": "Help them visualize their future self",
520
+ "hooks": [
521
+ "Picture yourself at your goal weight",
522
+ "6 months from now...",
523
+ "Your future self will thank you",
524
+ "This time next year...",
525
+ "Imagine summer with your dream body",
526
+ "What will you do when you hit your goal?",
527
+ "The vacation you've been putting off",
528
+ "The dress you've been saving",
529
+ "Finally, that beach trip",
530
+ "Your wedding, your reunion, your moment",
531
+ "Close your eyes. See yourself thin.",
532
+ "The life that's waiting for you",
533
+ ],
534
+ "visual_styles": [
535
+ "aspirational lifestyle imagery",
536
+ "beach vacation, confident person",
537
+ "dream wedding, fitting dress",
538
+ "vision board aesthetic",
539
+ "future self visualization",
540
+ "bucket list experiences",
541
+ ],
542
+ },
543
+
544
+ "loss_aversion": {
545
+ "name": "Loss Aversion",
546
+ "description": "Emphasize what they're losing by not acting",
547
+ "hooks": [
548
+ "Every day you wait is another day wasted",
549
+ "How many more years will you lose to this?",
550
+ "You're not getting younger",
551
+ "Time is running out on your metabolism",
552
+ "Each year makes it harder",
553
+ "Your 20s are gone. Don't waste your 30s too.",
554
+ "The best years of your life - spent overweight?",
555
+ "Missed opportunities don't come back",
556
+ "Your youth is slipping away",
557
+ "Obesity is stealing your life",
558
+ "The experiences you're missing",
559
+ "Life is passing you by while you stay stuck",
560
+ ],
561
+ "visual_styles": [
562
+ "hourglass, time running out",
563
+ "calendar pages flipping",
564
+ "missed opportunities montage",
565
+ "aging comparison, health decline",
566
+ "before it's too late imagery",
567
+ "now vs never contrast",
568
+ ],
569
+ },
570
+ }
571
+
572
+ # All hooks flattened for random selection
573
+ ALL_HOOKS = []
574
+ for strategy in STRATEGIES.values():
575
+ ALL_HOOKS.extend(strategy["hooks"])
576
+
577
+ # All visual styles flattened
578
+ ALL_VISUAL_STYLES = []
579
+ for strategy in STRATEGIES.values():
580
+ ALL_VISUAL_STYLES.extend(strategy["visual_styles"])
581
+
582
+ # Strategy names for random selection
583
+ STRATEGY_NAMES = list(STRATEGIES.keys())
584
+
585
+ # Creative directions for variety - Updated with winning patterns
586
+ # Note: These are TONE/APPROACH, not structural formats (use frameworks.py for structure)
587
+ CREATIVE_DIRECTIONS = [
588
+ "accusatory", # Still overweight? style
589
+ "curiosity-driven", # THIS/Secret style
590
+ "numbers-focused", # 47 lbs in 90 days style
591
+ "proof-transformation", # Before/after evidence (use "before_after" framework for structure)
592
+ "quiz-interactive", # How much could you lose?
593
+ "authority-medical", # FDA/Doctor style (use "authority" framework for structure)
594
+ "identity-targeted", # Women over 40...
595
+ "insider-reveal", # Hollywood secret
596
+ "urgent",
597
+ "aspirational",
598
+ ]
599
+
600
+ # Visual aesthetic styles (documentary/authentic formats for ad images)
601
+ # Note: These are aesthetic styles, not emotional moods (see data/visuals.py for emotional moods)
602
+ VISUAL_MOODS = [
603
+ "documentary-candid", # Real people, unposed
604
+ "vintage-authentic", # Old footage feel
605
+ "transformation-proof", # Before/after evidence
606
+ "ui-screenshot", # Quiz, calculator
607
+ "medical-clinical", # Doctor/FDA aesthetic
608
+ "celebrity-tabloid", # Reveal/transformation
609
+ "raw-testimonial", # UGC feel
610
+ "warm-nostalgic", # Amber, sepia tones
611
+ ]
612
+
613
+ # Ad copy templates
614
+ COPY_TEMPLATES = [
615
+ {
616
+ "structure": "hook_then_cta",
617
+ "format": "{hook}\n\n{supporting_text}\n\n👉 {cta}",
618
+ },
619
+ {
620
+ "structure": "question_answer",
621
+ "format": "{question}\n\n{answer}\n\n{cta}",
622
+ },
623
+ {
624
+ "structure": "before_after",
625
+ "format": "BEFORE: {before}\n\nAFTER: {after}\n\n{cta}",
626
+ },
627
+ {
628
+ "structure": "stat_hook",
629
+ "format": "⚠️ {statistic}\n\n{explanation}\n\n{cta}",
630
+ },
631
+ {
632
+ "structure": "story_hook",
633
+ "format": "{story_opening}\n\n{story_middle}\n\n{cta}",
634
+ },
635
+ {
636
+ "structure": "testimonial",
637
+ "format": '"{testimonial}"\n\n- {name}, lost {weight}\n\n{cta}',
638
+ },
639
+ ]
640
+
641
+ # CTAs for variety - Updated with high-converting patterns
642
+ CTAS = [
643
+ # Discovery/Eligibility CTAs (highest conversion)
644
+ "See If You Qualify",
645
+ "Check Your Eligibility",
646
+ "Take The Quiz",
647
+ "Calculate Your Results",
648
+ "See How Much You Could Lose",
649
+
650
+ # Action CTAs
651
+ "Start Your Transformation",
652
+ "Get Your Personalized Plan",
653
+ "Claim Your Consultation",
654
+ "Learn More",
655
+ "See Your Options",
656
+
657
+ # Urgency CTAs
658
+ "Start Now",
659
+ "Don't Wait Another Day",
660
+ "Begin Today",
661
+ "Get Started",
662
+
663
+ # Specific CTAs
664
+ "Get Your Prescription",
665
+ "Join Thousands Who Transformed",
666
+ "See Real Results",
667
+ ]
668
+
669
+
670
+ def get_niche_data():
671
+ """Return all GLP-1 data for the generator."""
672
+ return {
673
+ "niche": "glp1",
674
+ "strategies": STRATEGIES,
675
+ "all_hooks": ALL_HOOKS,
676
+ "all_visual_styles": ALL_VISUAL_STYLES,
677
+ "strategy_names": STRATEGY_NAMES,
678
+ "creative_directions": CREATIVE_DIRECTIONS,
679
+ "visual_moods": VISUAL_MOODS,
680
+ "copy_templates": COPY_TEMPLATES,
681
+ "ctas": CTAS,
682
+ }
683
+
data/home_insurance.py ADDED
@@ -0,0 +1,741 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Home Insurance - Complete Psychological Arsenal
3
+ All strategies: fear, shame, urgency, greed, authority, loss aversion, etc.
4
+ Updated with winning ad patterns from high-converting creative analysis.
5
+ """
6
+
7
+ # ============================================================================
8
+ # SECTION 1: PSYCHOLOGICAL STRATEGIES
9
+ # ============================================================================
10
+
11
+ STRATEGIES = {
12
+ # ------------------------------------------------------------------------
13
+ # Winning Strategies (High-Converting Patterns)
14
+ # ------------------------------------------------------------------------
15
+
16
+ "accusation_opener": {
17
+ "name": "Accusation Opener",
18
+ "description": "Direct accusation triggers immediate loss aversion - 'OVERPAYING?' style",
19
+ "hooks": [
20
+ "OVERPAYING?",
21
+ "Still Overpaying For Home Insurance?",
22
+ "Wasting $1,247/year On Insurance?",
23
+ "Are You Being Overcharged?",
24
+ "Paying Too Much? Most Homeowners Are.",
25
+ "Your Insurance Company Is Overcharging You",
26
+ "Stop Getting Ripped Off",
27
+ "You're Probably Paying Double",
28
+ "Why Are You Still Paying Full Price?",
29
+ "Being Overcharged Without Knowing It?",
30
+ "Throwing Money Away Every Month?",
31
+ "Your Premium Is Too High. Here's Proof.",
32
+ ],
33
+ "visual_styles": [
34
+ "person holding fan of $100 bills, hiding face, accusatory",
35
+ "frustrated senior looking at insurance bill, red 'OVERPAYING?' text",
36
+ "money flying away from house, loss visualization",
37
+ "comparison of high vs low price with red X on high price",
38
+ "person counting money with worried expression",
39
+ "wallet being emptied, coins and bills falling out",
40
+ ],
41
+ },
42
+
43
+ "curiosity_gap": {
44
+ "name": "Curiosity Gap",
45
+ "description": "Open loop with 'THIS' or 'Instead' demands click - highest CTR pattern",
46
+ "hooks": [
47
+ "Seniors Are Ditching Their Home Insurance & Doing This Instead",
48
+ "Thousands of homeowners are dropping their home insurance after THIS",
49
+ "Homeowners Over 50 Are Switching To THIS",
50
+ "What Smart Seniors Know About Home Insurance",
51
+ "The Secret Insurance Companies Don't Want You To Know",
52
+ "Everyone's Switching. Here's Why.",
53
+ "This Is Why Your Neighbors Pay Less",
54
+ "What 847 Homeowners In Your Area Just Discovered",
55
+ "The Loophole Seniors Are Using",
56
+ "They Found Something Better. Have You?",
57
+ "After Seeing THIS, You'll Never Overpay Again",
58
+ "One Simple Change Saves Thousands",
59
+ ],
60
+ "visual_styles": [
61
+ "senior man looking at utility meter or electric box, candid documentary",
62
+ "collage with Social Security card and government building",
63
+ "person reading document with surprised expression",
64
+ "group of seniors with knowing expressions",
65
+ "person pointing at hidden information, reveal moment",
66
+ "before/after document comparison with circled numbers",
67
+ ],
68
+ },
69
+
70
+ "specific_price_anchor": {
71
+ "name": "Specific Price Anchor",
72
+ "description": "Oddly specific prices ($97.33 not $100) create instant believability",
73
+ "hooks": [
74
+ "Home Insurance for as low as $43/month",
75
+ "Home Insurance with coverage as low as $43/month",
76
+ "$97.33/month For Full Coverage",
77
+ "Pay Just $47/month Instead of $200+",
78
+ "Locked In At $52/month",
79
+ "Full Protection For $1.50/day",
80
+ "Coverage From $39/month",
81
+ "$67/month Beats Your Current Rate",
82
+ "Most Seniors Qualify For $49/month",
83
+ "Switch And Pay Just $83/month",
84
+ "Rates Starting At $37.50/month",
85
+ ],
86
+ "visual_styles": [
87
+ "giant price number $43 in teal/bold color, age buttons below",
88
+ "clean white background, price dominant, age selector: 21-40, 41-64, 65+",
89
+ "BLACK FRIDAY style with specific price and gold balloons",
90
+ "price comparison showing crossed-out high price, new low price",
91
+ "calculator interface showing specific savings number",
92
+ "receipt or bill showing exact monthly amount",
93
+ ],
94
+ },
95
+
96
+ "before_after_proof": {
97
+ "name": "Before/After Proof",
98
+ "description": "Specific savings numbers with visual proof creates social proof",
99
+ "hooks": [
100
+ "WAS: $1,701 → NOW: $583",
101
+ "How I Dropped My Premium By $1,118/year",
102
+ "Paid $2,400. Now Pay $876.",
103
+ "From $189/month To $67/month",
104
+ "Cut My Bill In Half. Here's How.",
105
+ "Saved $1,247 In 5 Minutes",
106
+ "Before: $2,100/year After: $720/year",
107
+ "My Neighbor Showed Me How To Save $800",
108
+ "Real Savings: $1,456 Less Per Year",
109
+ "Went From Overpaying To Saving Big",
110
+ "The 5-Minute Switch That Saved Me $1,100",
111
+ ],
112
+ "visual_styles": [
113
+ "real woman 50-60 holding document with circled numbers, testimonial",
114
+ "before/after price bar: red 'WAS' vs green 'NOW'",
115
+ "split screen: old bill vs new bill with circles",
116
+ "person giving thumbs up with savings numbers visible",
117
+ "calculator or phone showing savings calculation",
118
+ "happy couple reviewing lower insurance bill together",
119
+ ],
120
+ },
121
+
122
+ "quiz_interactive": {
123
+ "name": "Quiz/Interactive",
124
+ "description": "Quiz format drives engagement and self-selection",
125
+ "hooks": [
126
+ "What Year Was Your House Built?",
127
+ "Tap Your Age To Calculate Your New Payment",
128
+ "Answer 3 Questions. See Your Rate.",
129
+ "How Old Is Your Home?",
130
+ "Check Your Eligibility In 60 Seconds",
131
+ "Take The 30-Second Quiz",
132
+ "Select Your Age Bracket",
133
+ "What's Your Home Worth?",
134
+ "Find Your New Rate - 2 Questions",
135
+ "See If You Qualify",
136
+ "Quick Quiz: How Much Can You Save?",
137
+ "Select Your State To See Rates",
138
+ ],
139
+ "visual_styles": [
140
+ "iPhone Notes app dark mode with checkboxes: Before 1970, 1970-1999, etc.",
141
+ "age selector buttons: 21-40 (yellow), 41-64 (blue), 65+ (red)",
142
+ "quiz interface with multiple choice options",
143
+ "clean UI with 'Tap below to see your rate'",
144
+ "interactive calculator mockup",
145
+ "state selection dropdown or map interface",
146
+ ],
147
+ },
148
+
149
+ "authority_transfer": {
150
+ "name": "Authority Transfer",
151
+ "description": "Transfer trust from government/institutions - highest trust pattern",
152
+ "hooks": [
153
+ "State Farm Brings Welfare!",
154
+ "Sponsored by the US Government and State Farm",
155
+ "Government Program For Senior Homeowners",
156
+ "New State Program Cuts Insurance Costs",
157
+ "Federal Assistance For Homeowners Over 50",
158
+ "Official: Seniors Qualify For Reduced Rates",
159
+ "State-Approved Savings Program",
160
+ "Government-Backed Insurance Savings",
161
+ "Medicare-Age Homeowners: New Benefit Available",
162
+ "Social Security Recipients: Check Eligibility",
163
+ "Official Notice: Rate Reduction Program",
164
+ "State Insurance Commission Announces Savings",
165
+ ],
166
+ "visual_styles": [
167
+ "government eagle seal, official document aesthetic",
168
+ "Social Security card with government building background",
169
+ "official letterhead style with seal and formal typography",
170
+ "state capitol building with official banner",
171
+ "government form aesthetic with checkboxes",
172
+ "presidential seal or state seal visible",
173
+ ],
174
+ },
175
+
176
+ "identity_targeting": {
177
+ "name": "Identity Targeting",
178
+ "description": "Direct demographic callout creates instant self-selection",
179
+ "hooks": [
180
+ "Seniors Won't Have To Pay More Than $49 A Month",
181
+ "Homeowners Over 50: Check Your Eligibility",
182
+ "Senior homeowners over the age of 50...",
183
+ "If You're 50+ And Own A Home, Read This",
184
+ "Attention: Homeowners Born Before 1975",
185
+ "For Homeowners 55 And Older",
186
+ "65+ Homeowners: New Rate Available",
187
+ "Baby Boomers: Insurance Relief Is Here",
188
+ "Retired Homeowners: Special Program",
189
+ "If You Own A Home And You're Over 50...",
190
+ "Senior Citizen Home Insurance Rates",
191
+ "Homeowners Turning 65 This Year",
192
+ ],
193
+ "visual_styles": [
194
+ "four senior faces in portrait style, dignified, relatable",
195
+ "real senior couple in front of their home",
196
+ "senior looking at camera, trustworthy expression",
197
+ "multiple seniors of different ethnicities, inclusive",
198
+ "senior holding document, testimonial style",
199
+ "elderly hands holding house keys or insurance papers",
200
+ ],
201
+ },
202
+
203
+ "insider_secret": {
204
+ "name": "Insider Secret",
205
+ "description": "Exclusivity and hidden knowledge framing",
206
+ "hooks": [
207
+ "The Easiest Way To Cut Home Insurance Bills",
208
+ "What Insurance Companies Don't Want You To Know",
209
+ "The Loophole That Saves Thousands",
210
+ "Former Agent Reveals Industry Secret",
211
+ "The Trick Your Insurance Company Hides",
212
+ "Why Insiders Pay 40% Less",
213
+ "The One Thing That Cuts Your Premium In Half",
214
+ "What They Don't Tell You About Home Insurance",
215
+ "Insurance Industry Insider Speaks Out",
216
+ "The Secret Smart Homeowners Use",
217
+ "Hidden Discount Most People Miss",
218
+ "The Backdoor To Lower Rates",
219
+ ],
220
+ "visual_styles": [
221
+ "person whispering or revealing secret, documentary candid",
222
+ "document being unveiled or revealed",
223
+ "insider/whistleblower aesthetic, anonymous feel",
224
+ "magnifying glass over insurance document",
225
+ "hidden text being exposed or highlighted",
226
+ "person looking over shoulder, sharing secret",
227
+ ],
228
+ },
229
+
230
+ # ------------------------------------------------------------------------
231
+ # Core Psychological Strategies
232
+ # ------------------------------------------------------------------------
233
+
234
+ "fear_based": {
235
+ "name": "Fear-Based",
236
+ "description": "Trigger fear of loss, disaster, and worst-case scenarios",
237
+ "hooks": [
238
+ "Your home could be gone tomorrow",
239
+ "One spark. Everything lost.",
240
+ "93% of homeowners are UNDERINSURED",
241
+ "Will your family be homeless?",
242
+ "Storm season is HERE - are you ready?",
243
+ "Your neighbor's house burned down last week",
244
+ "What if you can't afford to rebuild?",
245
+ "Bankruptcy from one disaster",
246
+ "Fire doesn't wait. Neither should you.",
247
+ "8 minutes. That's how fast you can lose everything.",
248
+ "The average house fire costs $287,000",
249
+ "Are you gambling with your family's safety?",
250
+ ],
251
+ "visual_styles": [
252
+ "burning house at night with vintage film grain, documentary footage",
253
+ "flooded living room with old VHS quality, damage visible",
254
+ "storm damage to roof, aged documentary photography",
255
+ "worried family looking at damaged home, candid shot",
256
+ "fire truck responding, old news footage aesthetic",
257
+ "tornado approaching, vintage weather broadcast style",
258
+ ],
259
+ },
260
+
261
+ "urgency_scarcity": {
262
+ "name": "Urgency & Scarcity",
263
+ "description": "Create time pressure and limited availability",
264
+ "hooks": [
265
+ "Rates increasing in 48 hours",
266
+ "Last chance for 2024 pricing",
267
+ "Only 23 spots left at this rate",
268
+ "Offer expires midnight",
269
+ "Insurers are DROPPING coverage in your area",
270
+ "Lock in before it's too late",
271
+ "Price hike coming January 1st",
272
+ "Limited-time discount ending soon",
273
+ "Your quote expires in 24 hours",
274
+ "Enrollment window closing",
275
+ "Act now or pay 30% more next month",
276
+ "Final warning: rates going up",
277
+ ],
278
+ "visual_styles": [
279
+ "countdown timer graphic, urgent red colors",
280
+ "calendar with deadline circled",
281
+ "clock showing almost midnight",
282
+ "red URGENT stamp on documents",
283
+ "limited time banner, expiring offer",
284
+ ],
285
+ },
286
+
287
+ "social_proof_fomo": {
288
+ "name": "Social Proof & FOMO",
289
+ "description": "Show others are doing it, create fear of missing out",
290
+ "hooks": [
291
+ "847 homeowners in your area switched THIS WEEK",
292
+ "Your neighbors are protected. Are you?",
293
+ "Join 2.3 million smart homeowners",
294
+ "Why is everyone switching?",
295
+ "The #1 choice for homeowners in 2024",
296
+ "9 out of 10 homeowners recommend this",
297
+ "Everyone on your street has coverage. Except you?",
298
+ "Don't be the last one unprotected",
299
+ "Over 500,000 claims paid this year",
300
+ "Rated #1 by Consumer Reports",
301
+ "The insurance your neighbors trust",
302
+ "Smart homeowners are making the switch",
303
+ ],
304
+ "visual_styles": [
305
+ "happy neighborhood, protected homes",
306
+ "map showing covered homes in area",
307
+ "crowd of satisfied customers",
308
+ "testimonial collage of happy families",
309
+ "5-star rating badges, trust indicators",
310
+ ],
311
+ },
312
+
313
+ "guilt_shame": {
314
+ "name": "Guilt & Shame",
315
+ "description": "Trigger guilt about family responsibility",
316
+ "hooks": [
317
+ "Can you look your family in the eye without protection?",
318
+ "Your kids are counting on you",
319
+ "Don't let them down",
320
+ "Responsible homeowners don't gamble with their family's future",
321
+ "What will you tell your kids when there's nothing left?",
322
+ "A real parent protects their family",
323
+ "Your spouse trusts you to keep them safe",
324
+ "Failure to protect is a choice",
325
+ "They're depending on you. Don't fail them.",
326
+ "Would your family forgive you?",
327
+ "Every night without coverage is a risk to your family",
328
+ "What kind of homeowner are you?",
329
+ ],
330
+ "visual_styles": [
331
+ "parent looking worried at sleeping children",
332
+ "family photo with protective imagery",
333
+ "father looking at burned home, regret",
334
+ "mother holding child, concerned expression",
335
+ "empty picture frame, lost memories",
336
+ ],
337
+ },
338
+
339
+ "greed_savings": {
340
+ "name": "Greed & Savings",
341
+ "description": "Appeal to desire to save money and get more",
342
+ "hooks": [
343
+ "You're overpaying by $1,247/year",
344
+ "Stop throwing money away",
345
+ "Get $500 back instantly",
346
+ "Why pay more for less?",
347
+ "Save up to 40% on your premium",
348
+ "Free quote reveals your savings",
349
+ "Most homeowners can save $800+",
350
+ "You're leaving money on the table",
351
+ "Switch and save in 5 minutes",
352
+ "Same coverage. Half the price.",
353
+ "Get more coverage for less money",
354
+ "Stop wasting money on overpriced insurance",
355
+ ],
356
+ "visual_styles": [
357
+ "stack of cash, money savings",
358
+ "piggy bank overflowing with coins",
359
+ "comparison chart showing savings",
360
+ "happy couple reviewing lower bills",
361
+ "calculator showing big savings number",
362
+ "wallet with money, financial freedom",
363
+ ],
364
+ },
365
+
366
+ "authority_trust": {
367
+ "name": "Authority & Trust",
368
+ "description": "Leverage expert credibility and insider knowledge",
369
+ "hooks": [
370
+ "Former agent reveals the truth",
371
+ "Industry insider secret exposed",
372
+ "A+ rated, 50 years trusted",
373
+ "Backed by Warren Buffett",
374
+ "Exposed: The coverage gap trap",
375
+ "Insurance agent confessions",
376
+ "The dirty secret of cheap policies",
377
+ "What your agent isn't telling you",
378
+ "BBB accredited with zero complaints",
379
+ "Trusted by Fortune 500 companies",
380
+ "Licensed in all 50 states",
381
+ ],
382
+ "visual_styles": [
383
+ "professional insurance agent, trustworthy",
384
+ "A+ rating badge, gold seal",
385
+ "official documents, certificates",
386
+ "expert in business attire",
387
+ "trust badges, security icons",
388
+ "newspaper headline style, exposed",
389
+ ],
390
+ },
391
+
392
+ "loss_aversion": {
393
+ "name": "Loss Aversion",
394
+ "description": "Emphasize what they stand to lose",
395
+ "hooks": [
396
+ "Everything you've worked for - GONE in 8 minutes",
397
+ "Average fire destroys $287,000 in belongings",
398
+ "You can't get back what's already ash",
399
+ "Your memories. Your savings. Your future. Gone.",
400
+ "One lawsuit could take everything",
401
+ "Imagine losing it all tomorrow",
402
+ "The average flood destroys 20 years of memories",
403
+ "Your life's work, wiped out in an hour",
404
+ "What would you save if you only had 2 minutes?",
405
+ "Some things can never be replaced",
406
+ "Everything you own could be gone by morning",
407
+ "Your equity, your memories, your peace of mind",
408
+ ],
409
+ "visual_styles": [
410
+ "before/after disaster comparison",
411
+ "pile of ash where home used to be",
412
+ "family photo partially burned",
413
+ "empty lot where house stood",
414
+ "destroyed personal belongings",
415
+ "wallet with nothing inside",
416
+ ],
417
+ },
418
+
419
+ "anchoring": {
420
+ "name": "Anchoring",
421
+ "description": "Compare high value to low cost",
422
+ "hooks": [
423
+ "Coverage worth $500,000 for just $47/month",
424
+ "Compared to losing everything, $1.50/day is nothing",
425
+ "Full protection for less than your Netflix subscription",
426
+ "Your home is worth $400K. Protection is $39/month.",
427
+ "Rebuild cost: $350,000. Coverage cost: $52/month.",
428
+ "Insurance: $40/month. Disaster recovery: $500,000.",
429
+ "Skip one coffee a day. Protect everything you own.",
430
+ "The cost of not having insurance: everything",
431
+ "$1.25/day protects $500,000 in assets",
432
+ "Cheaper than your daily coffee habit",
433
+ ],
434
+ "visual_styles": [
435
+ "scale comparing cost vs value",
436
+ "coffee cup vs house comparison",
437
+ "small price tag vs big house",
438
+ "simple math equation graphic",
439
+ "price comparison infographic",
440
+ ],
441
+ },
442
+
443
+ "simplicity": {
444
+ "name": "Simplicity & Ease",
445
+ "description": "Emphasize how easy it is to get covered",
446
+ "hooks": [
447
+ "Get covered in 3 minutes",
448
+ "One click. Full protection.",
449
+ "No paperwork. No hassle.",
450
+ "Quote in 60 seconds",
451
+ "The easiest insurance you'll ever buy",
452
+ "Set it and forget it protection",
453
+ "Online in minutes, protected for years",
454
+ "Skip the agent. Save time and money.",
455
+ "Apply from your couch",
456
+ "Instant quote, instant coverage",
457
+ "The lazy homeowner's insurance solution",
458
+ "Why is getting insurance still this hard? (It isn't anymore)",
459
+ ],
460
+ "visual_styles": [
461
+ "person on phone, relaxed",
462
+ "simple 3-step process graphic",
463
+ "checkmark, done, complete icons",
464
+ "happy person on couch with laptop",
465
+ "clean, minimal interface screenshot",
466
+ ],
467
+ },
468
+
469
+ "comparison_envy": {
470
+ "name": "Comparison & Envy",
471
+ "description": "Compare to others who are better protected",
472
+ "hooks": [
473
+ "Your neighbor pays less and gets more coverage",
474
+ "Why are smart homeowners switching?",
475
+ "They're protected. Why aren't you?",
476
+ "Same house. Same street. Half the premium.",
477
+ "What do they know that you don't?",
478
+ "The Jones family just saved $800. Your turn.",
479
+ "Your colleague's home is protected. Is yours?",
480
+ "Everyone's switching. What are you waiting for?",
481
+ "Don't be the only unprotected house on the block",
482
+ "Your neighbor's claim was covered. Would yours be?",
483
+ ],
484
+ "visual_styles": [
485
+ "two houses side by side, one protected",
486
+ "neighbor comparison graphic",
487
+ "protected house vs exposed house",
488
+ "happy neighbor vs worried neighbor",
489
+ "community map showing coverage",
490
+ ],
491
+ },
492
+
493
+ "transformation": {
494
+ "name": "Transformation & Peace",
495
+ "description": "Show the transformation from worry to peace",
496
+ "hooks": [
497
+ "From worried to worry-free in 5 minutes",
498
+ "Sleep soundly knowing you're covered",
499
+ "Finally, peace of mind for your family",
500
+ "Stop worrying. Start living.",
501
+ "Imagine never worrying about disasters again",
502
+ "The weight off your shoulders",
503
+ "From stressed to blessed",
504
+ "Live your life. We'll protect your home.",
505
+ "Worry-free homeownership starts here",
506
+ "Breathe easy. You're protected.",
507
+ ],
508
+ "visual_styles": [
509
+ "relaxed family in protected home",
510
+ "person sleeping peacefully",
511
+ "before/after: worried vs happy homeowner",
512
+ "sunny day, secure home",
513
+ "family enjoying life, not worrying",
514
+ ],
515
+ },
516
+ }
517
+
518
+ # ============================================================================
519
+ # SECTION 2: HIGH-CONVERTING VISUAL LIBRARY
520
+ # ============================================================================
521
+
522
+ PROTECTION_SAFETY_VISUALS = [
523
+ "family inside home, warm lights on, night outside",
524
+ "house surrounded by subtle glowing shield",
525
+ "parent locking the front door while kids inside",
526
+ "calm home while storm clouds gather in distance",
527
+ "hands holding a small house icon",
528
+ "roof + checkmark overlay",
529
+ ]
530
+
531
+ DISASTER_FEAR_VISUALS = [
532
+ "half image: safe home / damaged neighborhood",
533
+ "flood water stopping at doorstep",
534
+ "fire smoke behind intact house",
535
+ "fallen tree near but not on the house",
536
+ "cracked wall close-up",
537
+ "burnt house blurred in background, intact one sharp",
538
+ ]
539
+
540
+ FAMILY_EMOTIONAL_VISUALS = [
541
+ "parents hugging kids inside living room",
542
+ "child doing homework at dining table",
543
+ "family movie night at home",
544
+ "newborn in nursery",
545
+ "elderly parents sitting peacefully at home",
546
+ ]
547
+
548
+ FIRST_TIME_HOMEBUYER_VISUALS = [
549
+ "couple holding house keys",
550
+ "empty living room with boxes",
551
+ "first night sleeping on mattress on floor",
552
+ "SOLD sign outside house",
553
+ "smiling couple + nervous body language",
554
+ ]
555
+
556
+ ASSET_INVESTMENT_VISUALS = [
557
+ "beautiful home at sunset",
558
+ "clean driveway + car parked",
559
+ "home with subtle price tag icon",
560
+ "blueprint / house plan overlay",
561
+ "before/after renovation shots",
562
+ ]
563
+
564
+ PROBLEM_RISK_VISUALS = [
565
+ "leaking ceiling",
566
+ "broken window",
567
+ "short-circuit sparks (safe depiction)",
568
+ "burst pipe under sink",
569
+ "mold on wall",
570
+ "roof damage after storm",
571
+ ]
572
+
573
+ RELIEF_VISUALS = [
574
+ "sunlight after rain over house",
575
+ "rainbow behind neighborhood",
576
+ "family relaxing on couch",
577
+ "home with covered tag",
578
+ "coffee mug + window rain outside",
579
+ ]
580
+
581
+ MORTGAGE_BANK_VISUALS = [
582
+ "official-looking documents",
583
+ "laptop with insurance form open",
584
+ "house + bank icon",
585
+ "EMI letter on table",
586
+ "calculator + paperwork",
587
+ ]
588
+
589
+ COMPARISON_CHOICE_VISUALS = [
590
+ "covered vs uncovered home split",
591
+ "cheap insurance vs proper coverage",
592
+ "umbrella over house vs rain",
593
+ "shield vs lightning bolt",
594
+ ]
595
+
596
+ MINIMAL_SYMBOLIC_VISUALS = [
597
+ "simple house icon + shield",
598
+ "line-art house with lock",
599
+ "home inside heart shape",
600
+ "roof outline with checkmark",
601
+ "key + house silhouette",
602
+ ]
603
+
604
+ LIFESTYLE_VISUALS = [
605
+ "quiet suburban morning",
606
+ "weekend BBQ in backyard",
607
+ "dog running in yard",
608
+ "home decorated for festivals",
609
+ "neighborhood aerial shot",
610
+ ]
611
+
612
+ TEXT_FIRST_VISUALS = [
613
+ "text overlay: 'This home is insured. Is yours?'",
614
+ "text overlay: 'Most homeowners are underinsured.'",
615
+ "text overlay: 'Hope is not a plan.'",
616
+ "text overlay: 'One storm can change everything.'",
617
+ ]
618
+
619
+ SEASONAL_VISUALS = [
620
+ "monsoon rain (flood angle)",
621
+ "summer heat (fire risk)",
622
+ "winter storms",
623
+ "festival decorations (emotional peak)",
624
+ ]
625
+
626
+ HIGH_CONVERTING_VISUAL_LIBRARY = (
627
+ PROTECTION_SAFETY_VISUALS +
628
+ DISASTER_FEAR_VISUALS +
629
+ FAMILY_EMOTIONAL_VISUALS +
630
+ FIRST_TIME_HOMEBUYER_VISUALS +
631
+ ASSET_INVESTMENT_VISUALS +
632
+ PROBLEM_RISK_VISUALS +
633
+ RELIEF_VISUALS +
634
+ MORTGAGE_BANK_VISUALS +
635
+ COMPARISON_CHOICE_VISUALS +
636
+ MINIMAL_SYMBOLIC_VISUALS +
637
+ LIFESTYLE_VISUALS +
638
+ TEXT_FIRST_VISUALS +
639
+ SEASONAL_VISUALS
640
+ )
641
+
642
+ # ============================================================================
643
+ # SECTION 3: CREATIVE ELEMENTS
644
+ # ============================================================================
645
+
646
+ CREATIVE_DIRECTIONS = [
647
+ "accusatory", # Direct accusation style
648
+ "curiosity-driven", # Open loop/secret style
649
+ "price-focused", # Price anchor emphasis
650
+ "proof-based", # Evidence/testimonial style (note: use testimonial framework for structure)
651
+ "quiz-interactive", # Interactive/engagement style
652
+ "authority-backed", # Authority/trust transfer
653
+ "identity-targeted", # Demographic callout
654
+ "insider-reveal", # Exclusive/hidden knowledge
655
+ "urgent", # Time-sensitive urgency
656
+ # Note: "testimonial" removed - use "testimonial" framework instead for structure
657
+ ]
658
+
659
+ # Visual aesthetic styles (documentary/authentic formats for ad images)
660
+ # Note: These are aesthetic styles, not emotional moods (see data/visuals.py for emotional moods)
661
+ VISUAL_MOODS = [
662
+ "documentary-candid", # Documentary photography style
663
+ "vintage-authentic", # Vintage/retro aesthetic
664
+ "proof-testimonial", # Testimonial/evidence style
665
+ "ui-screenshot", # Native app interface style
666
+ "official-institutional", # Official/document style
667
+ "warm-nostalgic", # Warm, nostalgic tones
668
+ "raw-unpolished", # Raw, unpolished UGC feel
669
+ "news-expose", # News/editorial style
670
+ ]
671
+
672
+ COPY_TEMPLATES = [
673
+ {
674
+ "structure": "hook_then_cta",
675
+ "format": "{hook}\n\n{supporting_text}\n\n👉 {cta}",
676
+ },
677
+ {
678
+ "structure": "question_answer",
679
+ "format": "{question}\n\n{answer}\n\n{cta}",
680
+ },
681
+ {
682
+ "structure": "stat_hook",
683
+ "format": "⚠️ {statistic}\n\n{explanation}\n\n{cta}",
684
+ },
685
+ {
686
+ "structure": "story_hook",
687
+ "format": "{story_opening}\n\n{story_middle}\n\n{cta}",
688
+ },
689
+ ]
690
+
691
+ CTAS = [
692
+ "Check Your Eligibility",
693
+ "See If You Qualify",
694
+ "Check Eligibility Now",
695
+ "Tap To See Your Rate",
696
+ "Calculate Your Savings",
697
+ "Get Your Free Quote",
698
+ "See Your New Rate",
699
+ "Find Out How Much You Can Save",
700
+ "Click To See Your Savings",
701
+ "Get Protected Now",
702
+ "Start Saving Today",
703
+ "Don't Miss This",
704
+ "Claim Your Rate",
705
+ "Seniors: Check Your Rate",
706
+ "See Senior Rates",
707
+ "50+: Get Your Quote",
708
+ ]
709
+
710
+ # ============================================================================
711
+ # SECTION 4: AGGREGATED DATA
712
+ # ============================================================================
713
+
714
+ STRATEGY_NAMES = list(STRATEGIES.keys())
715
+
716
+ ALL_HOOKS = []
717
+ for strategy in STRATEGIES.values():
718
+ ALL_HOOKS.extend(strategy["hooks"])
719
+
720
+ ALL_VISUAL_STYLES = []
721
+ for strategy in STRATEGIES.values():
722
+ ALL_VISUAL_STYLES.extend(strategy["visual_styles"])
723
+ ALL_VISUAL_STYLES.extend(HIGH_CONVERTING_VISUAL_LIBRARY)
724
+
725
+ # ============================================================================
726
+ # SECTION 5: DATA EXPORT
727
+ # ============================================================================
728
+
729
+ def get_niche_data():
730
+ """Return all home insurance data for the generator."""
731
+ return {
732
+ "niche": "home_insurance",
733
+ "strategies": STRATEGIES,
734
+ "all_hooks": ALL_HOOKS,
735
+ "all_visual_styles": ALL_VISUAL_STYLES,
736
+ "strategy_names": STRATEGY_NAMES,
737
+ "creative_directions": CREATIVE_DIRECTIONS,
738
+ "visual_moods": VISUAL_MOODS,
739
+ "copy_templates": COPY_TEMPLATES,
740
+ "ctas": CTAS,
741
+ }
data/hooks.py ADDED
@@ -0,0 +1,156 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Hook Styles and Power Words - Language elements for compelling ad copy.
3
+ """
4
+
5
+ from typing import Dict, Any, List, Optional
6
+ import random
7
+
8
+ HOOK_STYLES: Dict[str, Dict[str, Any]] = {
9
+ "question": {
10
+ "name": "Question Hook",
11
+ "description": "Starts with a question to engage curiosity",
12
+ "examples": ["What if you could save $500/year?", "Are you overpaying for insurance?", "Tired of high premiums?"],
13
+ "trigger": "Curiosity",
14
+ },
15
+ "statement": {
16
+ "name": "Statement Hook",
17
+ "description": "Bold declaration that demands attention",
18
+ "examples": ["The secret to affordable insurance", "Most people don't know this trick", "The truth about insurance rates"],
19
+ "trigger": "Curiosity",
20
+ },
21
+ "number": {
22
+ "name": "Number Hook",
23
+ "description": "Uses specific numbers for credibility",
24
+ "examples": ["3 ways to cut insurance costs", "Save $847 in 5 minutes", "97% of users see results"],
25
+ "trigger": "Logic",
26
+ },
27
+ "transformation": {
28
+ "name": "Transformation Hook",
29
+ "description": "Shows before/after change",
30
+ "examples": ["From $200/month to $50/month", "Before: worried. After: protected.", "The change that saved everything"],
31
+ "trigger": "Transformation",
32
+ },
33
+ "urgency": {
34
+ "name": "Urgency Hook",
35
+ "description": "Creates time pressure",
36
+ "examples": ["Limited time: Save 40% today", "Offer expires Friday", "Last chance for this rate"],
37
+ "trigger": "FOMO",
38
+ },
39
+ "notification": {
40
+ "name": "Notification-Style Hook",
41
+ "description": "Mimics system notifications",
42
+ "examples": ["ALERT: Rates dropping now", "New message: Your savings are waiting", "Notice: Rate change detected"],
43
+ "trigger": "Urgency",
44
+ },
45
+ "curiosity_gap": {
46
+ "name": "Curiosity Gap Hook",
47
+ "description": "Creates information gap that needs closing",
48
+ "examples": ["The reason your rates are so high", "What they don't want you to know", "The hidden trick insurance companies hate"],
49
+ "trigger": "Curiosity",
50
+ },
51
+ "pattern_interrupt": {
52
+ "name": "Pattern Interrupt Hook",
53
+ "description": "Breaks expected patterns to grab attention",
54
+ "examples": ["Stop saving money (hear me out)", "I was wrong about insurance", "The worst advice that actually works"],
55
+ "trigger": "Surprise",
56
+ },
57
+ "social_proof": {
58
+ "name": "Social Proof Hook",
59
+ "description": "Leverages others' actions or opinions",
60
+ "examples": ["Join 50,000+ satisfied customers", "Why thousands are switching", "Rated 5 stars by 10,000+ users"],
61
+ "trigger": "Social Proof",
62
+ },
63
+ "benefit": {
64
+ "name": "Benefit-First Hook",
65
+ "description": "Leads with the primary benefit",
66
+ "examples": ["Save $500/year on home insurance", "Get coverage in 5 minutes", "Lower rates, better coverage"],
67
+ "trigger": "Greed",
68
+ },
69
+ "shocking_revelation": {
70
+ "name": "Shocking Revelation Hook",
71
+ "description": "Reveals surprising or shocking information",
72
+ "examples": ["The insurance trick that saves you $1,000", "What insurance companies don't want you to know", "The hidden cost of cheap insurance"],
73
+ "trigger": "Curiosity",
74
+ },
75
+ "direct_command": {
76
+ "name": "Direct Command Hook",
77
+ "description": "Tells the reader exactly what to do",
78
+ "examples": ["Stop overpaying today", "Get your quote now", "Compare rates in 60 seconds"],
79
+ "trigger": "Action",
80
+ },
81
+ "statistic_driven": {
82
+ "name": "Statistic-Driven Hook",
83
+ "description": "Uses compelling statistics to grab attention",
84
+ "examples": ["9 out of 10 people overpay", "Save an average of $847 per year", "97% see results in 30 days"],
85
+ "trigger": "Logic",
86
+ },
87
+ "story_opener": {
88
+ "name": "Story Opener Hook",
89
+ "description": "Begins a narrative that draws the reader in",
90
+ "examples": ["Last month, Sarah discovered something that changed everything", "Three years ago, I made a mistake that cost me thousands", "The day I found out I was overpaying..."],
91
+ "trigger": "Curiosity",
92
+ },
93
+ "contrarian_statement": {
94
+ "name": "Contrarian Statement Hook",
95
+ "description": "Challenges conventional wisdom",
96
+ "examples": ["Everything you know about insurance is wrong", "The worst advice that actually works", "Why the popular choice is the wrong choice"],
97
+ "trigger": "Curiosity",
98
+ },
99
+ "empathy_first": {
100
+ "name": "Empathy-First Hook",
101
+ "description": "Shows understanding of the reader's situation",
102
+ "examples": ["Tired of confusing insurance policies?", "Frustrated with rising rates?", "Wish insurance was simpler?"],
103
+ "trigger": "Empathy",
104
+ },
105
+ "exclusive_access": {
106
+ "name": "Exclusive Access Hook",
107
+ "description": "Offers special or limited access",
108
+ "examples": ["Exclusive offer for new customers only", "VIP access to special rates", "Invitation-only pricing"],
109
+ "trigger": "Exclusivity",
110
+ },
111
+ }
112
+
113
+ POWER_WORDS: Dict[str, List[str]] = {
114
+ "urgency": ["Now", "Today", "Hurry", "Instant", "Fast", "Quick", "Limited", "Deadline", "Expires", "Final", "Last chance", "Act now"],
115
+ "exclusivity": ["Exclusive", "Secret", "Hidden", "Private", "VIP", "Insider", "Confidential", "Elite", "Special", "Rare"],
116
+ "savings": ["Free", "Save", "Discount", "Deal", "Bargain", "Value", "Affordable", "Bonus", "Extra", "Reward", "Cashback"],
117
+ "trust": ["Guaranteed", "Proven", "Certified", "Verified", "Official", "Trusted", "Reliable", "Secure", "Safe", "Protected"],
118
+ "transformation": ["Transform", "Change", "Unlock", "Discover", "Reveal", "Breakthrough", "Revolutionary", "New", "Improved", "Ultimate"],
119
+ "emotion": ["Love", "Happy", "Joy", "Peace", "Confident", "Proud", "Relieved", "Excited", "Amazing", "Incredible"],
120
+ "fear": ["Warning", "Danger", "Risk", "Threat", "Avoid", "Mistake", "Problem", "Crisis", "Emergency", "Alert"],
121
+ "curiosity": ["Discover", "Learn", "Find out", "See why", "Uncover", "Secret", "Mystery", "Surprising", "Shocking", "Revealed"],
122
+ "social_proof": ["Popular", "Trending", "Best-selling", "Top-rated", "Award-winning", "Recommended", "Thousands", "Millions", "Join"],
123
+ "action": ["Get", "Start", "Try", "Claim", "Grab", "Take", "Join", "Sign up", "Subscribe", "Download", "Order", "Apply"],
124
+ }
125
+
126
+ CTA_TEMPLATES: Dict[str, List[str]] = {
127
+ "action": ["Get Started", "Get Your Quote", "Start Saving", "Claim Your Discount", "See Your Rate", "Get Protected"],
128
+ "urgency": ["Get It Now", "Claim Today", "Don't Miss Out", "Act Now", "Limited Time", "Last Chance"],
129
+ "curiosity": ["Learn More", "See How", "Find Out", "Discover More", "See If You Qualify", "Check Eligibility"],
130
+ "value": ["Save Now", "Get Free Quote", "Compare & Save", "See Your Savings", "Unlock Savings", "Get Best Rate"],
131
+ "low_commitment": ["Try Free", "No Obligation", "See for Yourself", "Take a Look", "Explore Options", "Check It Out"],
132
+ }
133
+
134
+ def get_all_hook_styles() -> Dict[str, Dict[str, Any]]:
135
+ return HOOK_STYLES
136
+
137
+ def get_hook_style(key: str) -> Optional[Dict[str, Any]]:
138
+ return HOOK_STYLES.get(key)
139
+
140
+ def get_random_hook_style() -> Dict[str, Any]:
141
+ key = random.choice(list(HOOK_STYLES.keys()))
142
+ return {"key": key, **HOOK_STYLES[key]}
143
+
144
+ def get_power_words(category: Optional[str] = None, count: int = 5) -> List[str]:
145
+ if category and category in POWER_WORDS:
146
+ words = POWER_WORDS[category]
147
+ else:
148
+ words = [w for cat_words in POWER_WORDS.values() for w in cat_words]
149
+ return random.sample(words, min(count, len(words)))
150
+
151
+ def get_random_cta(style: Optional[str] = None) -> str:
152
+ if style and style in CTA_TEMPLATES:
153
+ ctas = CTA_TEMPLATES[style]
154
+ else:
155
+ ctas = [c for style_ctas in CTA_TEMPLATES.values() for c in style_ctas]
156
+ return random.choice(ctas)
data/triggers.py ADDED
@@ -0,0 +1,141 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Psychological Triggers - Core emotional drivers that make ads convert.
3
+ """
4
+
5
+ from typing import Dict, Any, List, Optional
6
+ import random
7
+
8
+ PSYCHOLOGICAL_TRIGGERS: Dict[str, Dict[str, Any]] = {
9
+ "fear": {
10
+ "name": "Fear / Loss Aversion",
11
+ "description": "Fear of losing what you have or missing out on protection",
12
+ "copy_angles": ["Don't risk losing your home", "Protect what matters most", "Before disaster strikes"],
13
+ "visual_cues": ["warning signs", "protection imagery", "security elements"],
14
+ "color_palette": "warning",
15
+ "best_for": ["insurance", "security", "protection services"],
16
+ },
17
+ "greed": {
18
+ "name": "Greed / Savings",
19
+ "description": "Desire to save money, get more value, maximize gain",
20
+ "copy_angles": ["Save $500+ per year", "Get more for less", "Maximize your savings"],
21
+ "visual_cues": ["money imagery", "savings charts", "discount badges"],
22
+ "color_palette": "savings",
23
+ "best_for": ["financial products", "deals", "discounts"],
24
+ },
25
+ "fomo": {
26
+ "name": "FOMO (Fear of Missing Out)",
27
+ "description": "Anxiety about missing a limited opportunity",
28
+ "copy_angles": ["Limited time offer", "Only 50 spots left", "Offer expires soon"],
29
+ "visual_cues": ["countdown timers", "urgency badges", "scarcity indicators"],
30
+ "color_palette": "urgency",
31
+ "best_for": ["sales", "launches", "limited offers"],
32
+ },
33
+ "authority": {
34
+ "name": "Authority / Expertise",
35
+ "description": "Trust in experts, credentials, and established institutions",
36
+ "copy_angles": ["Doctor recommended", "Expert approved", "Industry leading"],
37
+ "visual_cues": ["credentials", "certifications", "professional imagery"],
38
+ "color_palette": "trust",
39
+ "best_for": ["health", "professional services", "premium products"],
40
+ },
41
+ "social_proof": {
42
+ "name": "Social Proof",
43
+ "description": "Following what others are doing, trust in numbers",
44
+ "copy_angles": ["Join 50,000+ customers", "5-star rated", "Most popular choice"],
45
+ "visual_cues": ["star ratings", "customer counts", "testimonials"],
46
+ "color_palette": "trust",
47
+ "best_for": ["consumer products", "services", "subscriptions"],
48
+ },
49
+ "curiosity": {
50
+ "name": "Curiosity",
51
+ "description": "Desire to learn, discover, and fill knowledge gaps",
52
+ "copy_angles": ["The secret they don't want you to know", "Discover how", "What they won't tell you"],
53
+ "visual_cues": ["mystery elements", "reveal moments", "hidden information"],
54
+ "color_palette": "premium",
55
+ "best_for": ["educational content", "reveals", "discoveries"],
56
+ },
57
+ "belonging": {
58
+ "name": "Belonging / Community",
59
+ "description": "Desire to be part of a group, tribe, or community",
60
+ "copy_angles": ["Join the community", "Be part of something", "Welcome to the family"],
61
+ "visual_cues": ["group imagery", "community symbols", "togetherness"],
62
+ "color_palette": "calm",
63
+ "best_for": ["memberships", "communities", "brands"],
64
+ },
65
+ "transformation": {
66
+ "name": "Transformation",
67
+ "description": "Desire for change, improvement, and becoming better",
68
+ "copy_angles": ["Transform your life", "Become the best version", "Change everything"],
69
+ "visual_cues": ["before/after", "progression imagery", "improvement indicators"],
70
+ "color_palette": "energy",
71
+ "best_for": ["fitness", "personal development", "lifestyle"],
72
+ },
73
+ "relief": {
74
+ "name": "Relief / Pain Relief",
75
+ "description": "Escaping pain, stress, or uncomfortable situations",
76
+ "copy_angles": ["End the stress", "Finally get relief", "Stop the pain"],
77
+ "visual_cues": ["relaxation imagery", "relief moments", "calm scenes"],
78
+ "color_palette": "calm",
79
+ "best_for": ["health", "stress relief", "solutions"],
80
+ },
81
+ "pride": {
82
+ "name": "Pride / Self-Worth",
83
+ "description": "Feeling good about oneself, achievements, and status",
84
+ "copy_angles": ["You deserve the best", "Treat yourself", "You've earned it"],
85
+ "visual_cues": ["success imagery", "achievement symbols", "premium elements"],
86
+ "color_palette": "premium",
87
+ "best_for": ["luxury", "premium products", "self-care"],
88
+ },
89
+ "urgency": {
90
+ "name": "Urgency / Time Pressure",
91
+ "description": "Need to act quickly before time runs out",
92
+ "copy_angles": ["Act now", "Today only", "Time is running out"],
93
+ "visual_cues": ["clocks", "timers", "urgent badges"],
94
+ "color_palette": "urgency",
95
+ "best_for": ["sales", "deadlines", "limited offers"],
96
+ },
97
+ "exclusivity": {
98
+ "name": "Exclusivity",
99
+ "description": "Desire for special access, VIP treatment, insider status",
100
+ "copy_angles": ["Exclusive offer", "VIP access", "By invitation only"],
101
+ "visual_cues": ["VIP badges", "exclusive tags", "premium styling"],
102
+ "color_palette": "premium",
103
+ "best_for": ["premium products", "memberships", "exclusive deals"],
104
+ },
105
+ }
106
+
107
+ # Trigger combinations that work well together
108
+ TRIGGER_COMBINATIONS: List[Dict[str, Any]] = [
109
+ {"primary": "fear", "secondary": "urgency", "name": "Fear + Urgency", "description": "Creates immediate action through fear of loss"},
110
+ {"primary": "greed", "secondary": "fomo", "name": "Savings + FOMO", "description": "Limited-time savings opportunity"},
111
+ {"primary": "social_proof", "secondary": "authority", "name": "Proof + Authority", "description": "Expert-backed with customer validation"},
112
+ {"primary": "transformation", "secondary": "curiosity", "name": "Transform + Curiosity", "description": "Discover the secret to transformation"},
113
+ {"primary": "relief", "secondary": "social_proof", "name": "Relief + Proof", "description": "Join thousands who found relief"},
114
+ {"primary": "exclusivity", "secondary": "urgency", "name": "Exclusive + Urgent", "description": "Limited VIP opportunity"},
115
+ ]
116
+
117
+ def get_all_triggers() -> Dict[str, Dict[str, Any]]:
118
+ return PSYCHOLOGICAL_TRIGGERS
119
+
120
+ def get_trigger(key: str) -> Optional[Dict[str, Any]]:
121
+ return PSYCHOLOGICAL_TRIGGERS.get(key)
122
+
123
+ def get_random_trigger() -> Dict[str, Any]:
124
+ key = random.choice(list(PSYCHOLOGICAL_TRIGGERS.keys()))
125
+ return {"key": key, **PSYCHOLOGICAL_TRIGGERS[key]}
126
+
127
+ def get_trigger_combination() -> Dict[str, Any]:
128
+ return random.choice(TRIGGER_COMBINATIONS)
129
+
130
+ def get_triggers_for_niche(niche: str) -> List[Dict[str, Any]]:
131
+ niche_lower = niche.lower().replace(" ", "_").replace("-", "_")
132
+ niche_triggers = {
133
+ "home_insurance": ["fear", "greed", "social_proof", "authority", "relief"],
134
+ "glp1": ["transformation", "pride", "social_proof", "authority", "relief"],
135
+ }
136
+ trigger_keys = niche_triggers.get(niche_lower, list(PSYCHOLOGICAL_TRIGGERS.keys())[:5])
137
+ return [{"key": k, **PSYCHOLOGICAL_TRIGGERS[k]} for k in trigger_keys if k in PSYCHOLOGICAL_TRIGGERS]
138
+
139
+ def get_copy_angles_for_trigger(trigger_key: str) -> List[str]:
140
+ trigger = get_trigger(trigger_key)
141
+ return trigger.get("copy_angles", []) if trigger else []
data/visuals.py ADDED
@@ -0,0 +1,126 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Visual Elements - Colors, typography, styles, and image generation guidance.
3
+ """
4
+
5
+ from typing import Dict, Any, List, Optional
6
+ import random
7
+
8
+ COLOR_PALETTES: Dict[str, Dict[str, Any]] = {
9
+ "urgency": {"primary": "#FF0000", "secondary": "#FF4500", "accent": "#FFD700", "background": "#000000", "text": "#FFFFFF"},
10
+ "trust": {"primary": "#1976D2", "secondary": "#0D47A1", "accent": "#4CAF50", "background": "#FFFFFF", "text": "#212121"},
11
+ "savings": {"primary": "#4CAF50", "secondary": "#2E7D32", "accent": "#FFD700", "background": "#FFFFFF", "text": "#212121"},
12
+ "energy": {"primary": "#FF9800", "secondary": "#F57C00", "accent": "#FFEB3B", "background": "#FFFFFF", "text": "#212121"},
13
+ "premium": {"primary": "#212121", "secondary": "#424242", "accent": "#FFD700", "background": "#FFFFFF", "text": "#212121"},
14
+ "calm": {"primary": "#00BCD4", "secondary": "#0097A7", "accent": "#B2EBF2", "background": "#FFFFFF", "text": "#212121"},
15
+ "health": {"primary": "#4CAF50", "secondary": "#81C784", "accent": "#2196F3", "background": "#E8F5E9", "text": "#212121"},
16
+ "warning": {"primary": "#F44336", "secondary": "#D32F2F", "accent": "#FFEB3B", "background": "#FFF3E0", "text": "#212121"},
17
+ }
18
+
19
+ TYPOGRAPHY_STYLES: Dict[str, Dict[str, Any]] = {
20
+ "system": {"name": "System/Native", "fonts": ["Arial", "Helvetica", "San Francisco", "Roboto"], "best_for": ["notifications", "system alerts"]},
21
+ "bold_impact": {"name": "Bold Impact", "fonts": ["Impact", "Arial Black", "Bebas Neue", "Anton"], "best_for": ["headlines", "breaking news"]},
22
+ "modern_clean": {"name": "Modern Clean", "fonts": ["Montserrat", "Lato", "Open Sans", "Poppins"], "best_for": ["professional ads", "lifestyle"]},
23
+ "handwritten": {"name": "Handwritten", "fonts": ["Indie Flower", "Dancing Script", "Pacifico", "Caveat"], "best_for": ["testimonials", "personal notes"]},
24
+ "typewriter": {"name": "Typewriter", "fonts": ["Courier", "Courier New", "American Typewriter"], "best_for": ["memos", "documents"]},
25
+ "newspaper": {"name": "Newspaper", "fonts": ["Times New Roman", "Georgia", "Playfair Display"], "best_for": ["news style", "editorial"]},
26
+ }
27
+
28
+ VISUAL_STYLES: List[Dict[str, Any]] = [
29
+ {"key": "ugc_authentic", "name": "UGC Authentic", "prompt_guidance": "Casual phone camera snapshot, authentic feel, slightly imperfect, natural lighting"},
30
+ {"key": "clean_minimal", "name": "Clean Minimal", "prompt_guidance": "Clean minimal design, white space, simple composition, modern aesthetic"},
31
+ {"key": "bold_graphic", "name": "Bold Graphic", "prompt_guidance": "Bold graphic design, high contrast, strong colors, impactful visual"},
32
+ {"key": "lifestyle_aspirational", "name": "Lifestyle Aspirational", "prompt_guidance": "Aspirational lifestyle photography, happy people, warm lighting"},
33
+ {"key": "documentary_real", "name": "Documentary Real", "prompt_guidance": "Documentary style, candid shots, real moments, natural light"},
34
+ {"key": "screenshot_native", "name": "Screenshot Native", "prompt_guidance": "Mobile app screenshot, native UI elements, authentic interface"},
35
+ {"key": "news_editorial", "name": "News Editorial", "prompt_guidance": "News publication style, editorial photography, professional"},
36
+ {"key": "retro_nostalgic", "name": "Retro Nostalgic", "prompt_guidance": "Retro aesthetic, vintage colors, nostalgic feel"},
37
+ {"key": "dark_dramatic", "name": "Dark Dramatic", "prompt_guidance": "Dark dramatic lighting, moody atmosphere, cinematic"},
38
+ {"key": "bright_optimistic", "name": "Bright Optimistic", "prompt_guidance": "Bright cheerful photography, optimistic feel, positive energy"},
39
+ {"key": "corporate_professional", "name": "Corporate Professional", "prompt_guidance": "Corporate photography, business setting, professional attire, clean office environment"},
40
+ {"key": "street_style", "name": "Street Style", "prompt_guidance": "Urban street photography, candid moments, city backdrop, authentic urban feel"},
41
+ {"key": "studio_clean", "name": "Studio Clean", "prompt_guidance": "Studio photography, controlled lighting, clean background, product-focused"},
42
+ {"key": "cinematic_epic", "name": "Cinematic Epic", "prompt_guidance": "Cinematic wide shots, epic scale, dramatic composition, film-like quality"},
43
+ {"key": "handheld_casual", "name": "Handheld Casual", "prompt_guidance": "Handheld camera feel, slight motion blur, casual framing, everyday moments"},
44
+ ]
45
+
46
+ CAMERA_ANGLES: List[str] = [
47
+ "eye level shot", "slightly low angle (empowering)", "slightly high angle (overview)", "close-up portrait",
48
+ "medium shot", "wide establishing shot", "over-the-shoulder shot", "POV first-person perspective", "candid angle",
49
+ "dutch angle (tilted)", "bird's eye view", "worm's eye view", "extreme close-up", "two-shot (two people)",
50
+ "group shot", "profile shot", "three-quarter angle", "top-down view", "oblique angle",
51
+ ]
52
+
53
+ LIGHTING_STYLES: List[str] = [
54
+ "natural daylight", "golden hour warm light", "soft diffused light", "harsh direct sunlight",
55
+ "indoor ambient lighting", "dramatic side lighting", "backlit silhouette", "overcast soft light",
56
+ "rim lighting", "softbox lighting", "window light", "neon lighting", "candlelight warm",
57
+ "studio lighting", "natural window light", "dramatic chiaroscuro", "soft ambient glow",
58
+ ]
59
+
60
+ COMPOSITIONS: List[str] = [
61
+ "rule of thirds", "centered subject", "leading lines to subject", "frame within frame",
62
+ "negative space emphasis", "symmetrical balance", "asymmetrical dynamic", "diagonal composition",
63
+ "golden ratio", "triangular composition", "circular composition", "layered depth",
64
+ "foreground/background separation", "depth of field focus", "repetitive patterns", "contrasting elements",
65
+ ]
66
+
67
+ VISUAL_MOODS: List[str] = [
68
+ "urgent and alarming", "calm and reassuring", "exciting and energetic", "trustworthy and professional",
69
+ "warm and friendly", "bold and confident", "subtle and sophisticated", "raw and authentic",
70
+ "hopeful and optimistic", "serious and authoritative", "playful and fun", "mysterious and intriguing",
71
+ "inspiring and motivational", "comfortable and cozy", "dynamic and action-packed", "peaceful and serene",
72
+ ]
73
+
74
+ NEGATIVE_PROMPTS: List[str] = [
75
+ "no watermarks", "no logos", "no brand marks", "no stock photo aesthetic", "no overly polished professional photography",
76
+ "no perfect studio lighting", "no corporate photography style", "no artificial perfection", "no text errors",
77
+ "no distorted faces", "no extra limbs", "no AI artifacts", "no blurry main subjects", "no over-processed HDR look",
78
+ ]
79
+
80
+ NICHE_VISUAL_GUIDANCE: Dict[str, Dict[str, Any]] = {
81
+ "home_insurance": {
82
+ "subjects": ["family in front of home", "house exterior", "homeowner looking confident", "couple reviewing papers"],
83
+ "props": ["insurance documents", "house keys", "tablet showing coverage", "family photos"],
84
+ "avoid": ["disasters", "fire or floods", "stressed expressions", "dark settings"],
85
+ "color_preference": "trust",
86
+ },
87
+ "glp1": {
88
+ "subjects": ["confident person smiling", "active lifestyle scenes", "healthy meal preparation", "doctor consultation"],
89
+ "props": ["fitness equipment", "healthy food", "comfortable clothing"],
90
+ "avoid": ["before/after weight comparisons", "measuring tapes", "scales prominently", "needle close-ups"],
91
+ "color_preference": "health",
92
+ },
93
+ }
94
+
95
+ def get_color_palette(trigger: str) -> Dict[str, str]:
96
+ return COLOR_PALETTES.get(trigger, COLOR_PALETTES["trust"])
97
+
98
+ def get_random_visual_style() -> Dict[str, Any]:
99
+ return random.choice(VISUAL_STYLES)
100
+
101
+ def get_random_camera_angle() -> str:
102
+ return random.choice(CAMERA_ANGLES)
103
+
104
+ def get_random_lighting() -> str:
105
+ return random.choice(LIGHTING_STYLES)
106
+
107
+ def get_random_composition() -> str:
108
+ return random.choice(COMPOSITIONS)
109
+
110
+ def get_random_mood() -> str:
111
+ return random.choice(VISUAL_MOODS)
112
+
113
+ def get_niche_visual_guidance(niche: str) -> Optional[Dict[str, Any]]:
114
+ return NICHE_VISUAL_GUIDANCE.get(niche.lower().replace(" ", "_").replace("-", "_"))
115
+
116
+ def build_negative_prompt() -> str:
117
+ return ", ".join(NEGATIVE_PROMPTS)
118
+
119
+ def get_random_visual_elements() -> Dict[str, Any]:
120
+ return {
121
+ "style": get_random_visual_style(),
122
+ "camera_angle": get_random_camera_angle(),
123
+ "lighting": get_random_lighting(),
124
+ "composition": get_random_composition(),
125
+ "mood": get_random_mood(),
126
+ }
frontend/.gitignore ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2
+
3
+ # dependencies
4
+ /node_modules
5
+ /.pnp
6
+ .pnp.*
7
+ .yarn/*
8
+ !.yarn/patches
9
+ !.yarn/plugins
10
+ !.yarn/releases
11
+ !.yarn/versions
12
+
13
+ # testing
14
+ /coverage
15
+
16
+ # next.js
17
+ /.next/
18
+ /out/
19
+
20
+ # production
21
+ /build
22
+
23
+ # misc
24
+ .DS_Store
25
+ *.pem
26
+
27
+ # debug
28
+ npm-debug.log*
29
+ yarn-debug.log*
30
+ yarn-error.log*
31
+ .pnpm-debug.log*
32
+
33
+ # env files (can opt-in for committing if needed)
34
+ .env*
35
+
36
+ # vercel
37
+ .vercel
38
+
39
+ # typescript
40
+ *.tsbuildinfo
41
+ next-env.d.ts
frontend/README.md ADDED
@@ -0,0 +1,144 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Ad Generator Lite - Frontend
2
+
3
+ Modern Next.js dashboard for generating and managing ad creatives for Home Insurance and GLP-1 niches.
4
+
5
+ ## Features
6
+
7
+ - **Single Ad Generation**: Generate individual ads with randomized strategies
8
+ - **Batch Generation**: Create multiple ads at once for testing
9
+ - **Matrix System**: Generate ads using specific angle × concept combinations
10
+ - **Gallery**: Browse, filter, and manage all generated ads
11
+ - **Testing Matrix Builder**: Create systematic testing matrices
12
+ - **Export**: Download images, export JSON/CSV, copy ad copy
13
+
14
+ ## Tech Stack
15
+
16
+ - **Next.js 16** with App Router
17
+ - **TypeScript** for type safety
18
+ - **Tailwind CSS** for styling
19
+ - **Zustand** for state management
20
+ - **React Hook Form** + **Zod** for form validation
21
+ - **Axios** for API calls
22
+ - **React Hot Toast** for notifications
23
+
24
+ ## Getting Started
25
+
26
+ ### Prerequisites
27
+
28
+ - Node.js 18+ and npm
29
+ - Backend API running on `http://localhost:8000` (or configure `NEXT_PUBLIC_API_URL`)
30
+
31
+ ### Installation
32
+
33
+ ```bash
34
+ # Install dependencies
35
+ npm install
36
+
37
+ # Run development server
38
+ npm run dev
39
+ ```
40
+
41
+ The app will be available at `http://localhost:3000`
42
+
43
+ ### Environment Variables
44
+
45
+ Create a `.env.local` file:
46
+
47
+ ```env
48
+ NEXT_PUBLIC_API_URL=http://localhost:8000
49
+ ```
50
+
51
+ ## Project Structure
52
+
53
+ ```
54
+ frontend/
55
+ ├── app/ # Next.js pages (App Router)
56
+ │ ├── page.tsx # Dashboard
57
+ │ ├── generate/ # Generation pages
58
+ │ ├── gallery/ # Gallery pages
59
+ │ └── matrix/ # Matrix system pages
60
+ ├── components/ # React components
61
+ │ ├── ui/ # Base UI components
62
+ │ ├── generation/ # Generation components
63
+ │ ├── gallery/ # Gallery components
64
+ │ └── matrix/ # Matrix components
65
+ ├── lib/ # Utilities
66
+ │ ├── api/ # API client
67
+ │ ├── hooks/ # Custom hooks
68
+ │ └── utils/ # Utility functions
69
+ ├── store/ # Zustand stores
70
+ ├── types/ # TypeScript types
71
+ └── styles/ # Global styles
72
+ ```
73
+
74
+ ## Available Scripts
75
+
76
+ - `npm run dev` - Start development server
77
+ - `npm run build` - Build for production
78
+ - `npm run start` - Start production server
79
+ - `npm run lint` - Run ESLint
80
+
81
+ ## API Integration
82
+
83
+ The frontend integrates with the FastAPI backend. All API endpoints are defined in `lib/api/endpoints.ts` and use the axios client in `lib/api/client.ts`.
84
+
85
+ ## Features Overview
86
+
87
+ ### Dashboard
88
+ - Quick stats (total ads, by niche, system status)
89
+ - Recent ads preview
90
+ - Quick action buttons
91
+
92
+ ### Generation
93
+ - **Single**: Generate one ad with configurable image count
94
+ - **Batch**: Generate multiple ads (1-20) with 1-3 images each
95
+ - **Matrix**: Select specific angle and concept combinations
96
+
97
+ ### Gallery
98
+ - Grid view of all ads
99
+ - Filter by niche, method, search
100
+ - Pagination
101
+ - Bulk actions (select, delete)
102
+ - Ad detail view with full copy and metadata
103
+
104
+ ### Matrix System
105
+ - Browse all 100 angles and 100 concepts
106
+ - View compatibility between angles and concepts
107
+ - Generate testing matrices for systematic optimization
108
+ - Export matrices as JSON/CSV
109
+
110
+ ## Development
111
+
112
+ ### Adding New Components
113
+
114
+ 1. Create component in appropriate `components/` subdirectory
115
+ 2. Use TypeScript for type safety
116
+ 3. Follow existing patterns for styling (Tailwind CSS)
117
+ 4. Use UI components from `components/ui/`
118
+
119
+ ### State Management
120
+
121
+ Use Zustand stores in `store/`:
122
+ - `generationStore` - Current generation state
123
+ - `galleryStore` - Gallery filters, pagination, selection
124
+ - `matrixStore` - Matrix angles, concepts, selections
125
+
126
+ ### API Calls
127
+
128
+ All API calls should use functions from `lib/api/endpoints.ts` which provide:
129
+ - Type safety
130
+ - Error handling
131
+ - Automatic toast notifications
132
+
133
+ ## Building for Production
134
+
135
+ ```bash
136
+ npm run build
137
+ npm start
138
+ ```
139
+
140
+ The production build will be optimized and ready for deployment.
141
+
142
+ ## License
143
+
144
+ Same as the main project.
frontend/app/favicon.ico ADDED
frontend/app/gallery/[id]/page.tsx ADDED
@@ -0,0 +1,512 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import React, { useEffect, useState, useCallback } from "react";
4
+ import { useParams, useRouter } from "next/navigation";
5
+ import Link from "next/link";
6
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/Card";
7
+ import { Button } from "@/components/ui/Button";
8
+ import { LoadingSpinner } from "@/components/ui/LoadingSpinner";
9
+ import { getAd, deleteAd, listAds } from "@/lib/api/endpoints";
10
+ import { formatDate, formatNiche, getImageUrl, getImageUrlFallback } from "@/lib/utils/formatters";
11
+ import { downloadImage, copyToClipboard, exportAsJSON } from "@/lib/utils/export";
12
+ import { toast } from "react-hot-toast";
13
+ import { ArrowLeft, ArrowRight, Download, Copy, Trash2, FileJson, Wand2 } from "lucide-react";
14
+ import type { AdCreativeDB, ImageCorrectResponse } from "@/types/api";
15
+ import { CorrectionModal } from "@/components/generation/CorrectionModal";
16
+
17
+ export default function AdDetailPage() {
18
+ const params = useParams();
19
+ const router = useRouter();
20
+ const adId = params.id as string;
21
+
22
+ const [ad, setAd] = useState<AdCreativeDB | null>(null);
23
+ const [isLoading, setIsLoading] = useState(true);
24
+ const [imageSrc, setImageSrc] = useState<string | null>(null);
25
+ const [imageError, setImageError] = useState(false);
26
+ const [allAds, setAllAds] = useState<AdCreativeDB[]>([]);
27
+ const [currentIndex, setCurrentIndex] = useState<number>(-1);
28
+ const [showCorrectionModal, setShowCorrectionModal] = useState(false);
29
+
30
+ useEffect(() => {
31
+ loadAd();
32
+ loadAllAds();
33
+ }, [adId]);
34
+
35
+ useEffect(() => {
36
+ if (ad) {
37
+ const { primary, fallback } = getImageUrlFallback(ad.image_url, ad.image_filename);
38
+ setImageSrc(primary || fallback);
39
+ setImageError(false);
40
+ const index = allAds.findIndex((a) => a.id === ad.id);
41
+ setCurrentIndex(index);
42
+ } else {
43
+ setImageSrc(null);
44
+ setImageError(false);
45
+ }
46
+ }, [ad, allAds]);
47
+
48
+ const navigateToPrevious = useCallback(() => {
49
+ if (currentIndex > 0 && allAds.length > 0) {
50
+ const previousAd = allAds[currentIndex - 1];
51
+ router.push(`/gallery/${previousAd.id}`);
52
+ }
53
+ }, [currentIndex, allAds, router]);
54
+
55
+ const navigateToNext = useCallback(() => {
56
+ if (currentIndex >= 0 && currentIndex < allAds.length - 1) {
57
+ const nextAd = allAds[currentIndex + 1];
58
+ router.push(`/gallery/${nextAd.id}`);
59
+ }
60
+ }, [currentIndex, allAds, router]);
61
+
62
+ const hasPrevious = currentIndex > 0;
63
+ const hasNext = currentIndex >= 0 && currentIndex < allAds.length - 1;
64
+
65
+ useEffect(() => {
66
+ const handleKeyPress = (e: KeyboardEvent) => {
67
+ if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) {
68
+ return;
69
+ }
70
+ if (e.key === "ArrowLeft" && hasPrevious) {
71
+ e.preventDefault();
72
+ navigateToPrevious();
73
+ } else if (e.key === "ArrowRight" && hasNext) {
74
+ e.preventDefault();
75
+ navigateToNext();
76
+ }
77
+ };
78
+ window.addEventListener("keydown", handleKeyPress);
79
+ return () => window.removeEventListener("keydown", handleKeyPress);
80
+ }, [hasPrevious, hasNext, navigateToPrevious, navigateToNext]);
81
+
82
+ const loadAd = async () => {
83
+ setIsLoading(true);
84
+ try {
85
+ const data = await getAd(adId);
86
+ setAd(data);
87
+ } catch (error: any) {
88
+ toast.error("Failed to load ad");
89
+ router.push("/gallery");
90
+ } finally {
91
+ setIsLoading(false);
92
+ }
93
+ };
94
+
95
+ const loadAllAds = async () => {
96
+ try {
97
+ const response = await listAds({ limit: 1000, offset: 0 });
98
+ setAllAds(response.ads);
99
+ } catch (error: any) {
100
+ console.error("Failed to load ads list:", error);
101
+ }
102
+ };
103
+
104
+ const handleDelete = async () => {
105
+ if (!confirm("Are you sure you want to delete this ad?")) return;
106
+ try {
107
+ await deleteAd(adId);
108
+ toast.success("Ad deleted");
109
+ router.push("/gallery");
110
+ } catch (error: any) {
111
+ toast.error("Failed to delete ad");
112
+ }
113
+ };
114
+
115
+ const handleDownloadImage = async () => {
116
+ if (!ad?.image_url && !ad?.image_filename) {
117
+ toast.error("No image available");
118
+ return;
119
+ }
120
+ try {
121
+ const imageUrl = getImageUrl(ad.image_url, ad.image_filename);
122
+ if (imageUrl) {
123
+ await downloadImage(imageUrl, ad.image_filename || `ad-${ad.id}.png`);
124
+ toast.success("Image downloaded");
125
+ }
126
+ } catch (error) {
127
+ toast.error("Failed to download image");
128
+ }
129
+ };
130
+
131
+ const handleCopyText = async (text: string, label: string) => {
132
+ try {
133
+ await copyToClipboard(text);
134
+ toast.success(`${label} copied`);
135
+ } catch (error) {
136
+ toast.error("Failed to copy");
137
+ }
138
+ };
139
+
140
+ const handleExportJSON = () => {
141
+ if (!ad) return;
142
+ exportAsJSON(ad, `ad-${ad.id}.json`);
143
+ toast.success("JSON exported");
144
+ };
145
+
146
+ const handleImageError = () => {
147
+ if (!ad) return;
148
+ const { fallback } = getImageUrlFallback(ad.image_url, ad.image_filename);
149
+ if (!imageError && fallback && imageSrc) {
150
+ setImageSrc(fallback);
151
+ setImageError(true);
152
+ } else {
153
+ setImageSrc(null);
154
+ }
155
+ };
156
+
157
+ if (isLoading) {
158
+ return (
159
+ <div className="min-h-screen flex items-center justify-center">
160
+ <LoadingSpinner size="lg" />
161
+ </div>
162
+ );
163
+ }
164
+
165
+ if (!ad) {
166
+ return (
167
+ <div className="min-h-screen flex items-center justify-center">
168
+ <div className="text-center">
169
+ <p className="text-gray-500 mb-4">Ad not found</p>
170
+ <Link href="/gallery">
171
+ <Button variant="primary">Back to Gallery</Button>
172
+ </Link>
173
+ </div>
174
+ </div>
175
+ );
176
+ }
177
+
178
+ return (
179
+ <div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50/30 to-cyan-50/30">
180
+ {/* Header */}
181
+ <div className="sticky top-0 z-50 bg-white/90 backdrop-blur-md border-b border-blue-100 shadow-sm">
182
+ <div className="max-w-7xl mx-auto px-4 py-3 flex items-center justify-between">
183
+ <Link href="/gallery">
184
+ <Button variant="ghost" size="sm" className="text-blue-600 hover:text-blue-700 hover:bg-blue-50">
185
+ <ArrowLeft className="h-4 w-4 mr-2" />
186
+ Gallery
187
+ </Button>
188
+ </Link>
189
+
190
+ {allAds.length > 0 && currentIndex >= 0 && (
191
+ <div className="flex items-center gap-2">
192
+ <div className="relative group">
193
+ <Button
194
+ variant="ghost"
195
+ size="sm"
196
+ onClick={navigateToPrevious}
197
+ disabled={!hasPrevious}
198
+ className="text-blue-600 hover:bg-blue-50 disabled:text-gray-300"
199
+ >
200
+ <ArrowLeft className="h-4 w-4" />
201
+ </Button>
202
+ {hasPrevious && (
203
+ <span className="absolute -bottom-8 left-1/2 -translate-x-1/2 px-2 py-1 bg-gray-800 text-white text-xs rounded opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap pointer-events-none">
204
+ Previous (←)
205
+ </span>
206
+ )}
207
+ </div>
208
+ <span className="text-sm font-bold bg-gradient-to-r from-blue-600 to-cyan-600 bg-clip-text text-transparent min-w-[60px] text-center">
209
+ {currentIndex + 1} / {allAds.length}
210
+ </span>
211
+ <div className="relative group">
212
+ <Button
213
+ variant="ghost"
214
+ size="sm"
215
+ onClick={navigateToNext}
216
+ disabled={!hasNext}
217
+ className="text-blue-600 hover:bg-blue-50 disabled:text-gray-300"
218
+ >
219
+ <ArrowRight className="h-4 w-4" />
220
+ </Button>
221
+ {hasNext && (
222
+ <span className="absolute -bottom-8 left-1/2 -translate-x-1/2 px-2 py-1 bg-gray-800 text-white text-xs rounded opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap pointer-events-none">
223
+ Next (→)
224
+ </span>
225
+ )}
226
+ </div>
227
+ </div>
228
+ )}
229
+
230
+ <div className="flex items-center gap-1">
231
+ <div className="relative group">
232
+ <Button
233
+ variant="ghost"
234
+ size="sm"
235
+ onClick={() => setShowCorrectionModal(true)}
236
+ className="text-amber-600 hover:bg-amber-50"
237
+ >
238
+ <Wand2 className="h-4 w-4" />
239
+ </Button>
240
+ <span className="absolute -bottom-8 left-1/2 -translate-x-1/2 px-2 py-1 bg-gray-800 text-white text-xs rounded opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap pointer-events-none">
241
+ Correct Image
242
+ </span>
243
+ </div>
244
+ <div className="relative group">
245
+ <Button variant="ghost" size="sm" onClick={handleDownloadImage} className="text-emerald-600 hover:bg-emerald-50">
246
+ <Download className="h-4 w-4" />
247
+ </Button>
248
+ <span className="absolute -bottom-8 left-1/2 -translate-x-1/2 px-2 py-1 bg-gray-800 text-white text-xs rounded opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap pointer-events-none">
249
+ Download Image
250
+ </span>
251
+ </div>
252
+ <div className="relative group">
253
+ <Button variant="ghost" size="sm" onClick={handleExportJSON} className="text-violet-600 hover:bg-violet-50">
254
+ <FileJson className="h-4 w-4" />
255
+ </Button>
256
+ <span className="absolute -bottom-8 left-1/2 -translate-x-1/2 px-2 py-1 bg-gray-800 text-white text-xs rounded opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap pointer-events-none">
257
+ Export JSON
258
+ </span>
259
+ </div>
260
+ <div className="relative group">
261
+ <Button variant="ghost" size="sm" onClick={handleDelete} className="text-red-500 hover:text-red-600 hover:bg-red-50">
262
+ <Trash2 className="h-4 w-4" />
263
+ </Button>
264
+ <span className="absolute -bottom-8 left-1/2 -translate-x-1/2 px-2 py-1 bg-red-600 text-white text-xs rounded opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap pointer-events-none">
265
+ Delete Ad
266
+ </span>
267
+ </div>
268
+ </div>
269
+ </div>
270
+ </div>
271
+
272
+ {/* Main Content */}
273
+ <div className="max-w-7xl mx-auto px-4 py-8">
274
+ <div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
275
+ {/* Left - Image */}
276
+ <div className="space-y-4">
277
+ {imageSrc ? (
278
+ <div className="bg-white rounded-2xl shadow-lg shadow-blue-100/50 overflow-hidden ring-1 ring-blue-100">
279
+ <img
280
+ src={imageSrc}
281
+ alt={ad.headline}
282
+ className="w-full h-auto"
283
+ onError={handleImageError}
284
+ />
285
+ </div>
286
+ ) : (
287
+ <div className="bg-gradient-to-br from-blue-50 to-cyan-50 rounded-2xl shadow-lg aspect-square flex items-center justify-center ring-1 ring-blue-100">
288
+ <div className="text-blue-300 text-center">
289
+ <svg className="w-16 h-16 mx-auto mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
290
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
291
+ </svg>
292
+ <p className="text-sm">No image</p>
293
+ </div>
294
+ </div>
295
+ )}
296
+
297
+ {/* Metadata */}
298
+ <div className="bg-white rounded-2xl shadow-lg p-6 border border-gray-100">
299
+ <h3 className="text-xs font-bold text-blue-600 uppercase tracking-wider mb-4">Details</h3>
300
+ <div className="grid grid-cols-2 gap-3 text-sm">
301
+ <div className="bg-gradient-to-br from-blue-50 to-cyan-50 rounded-xl p-3 border border-blue-100">
302
+ <p className="text-blue-500 text-xs font-medium mb-1">Niche</p>
303
+ <p className="font-semibold text-gray-800">{formatNiche(ad.niche)}</p>
304
+ </div>
305
+ {ad.generation_method && (
306
+ <div className="bg-gradient-to-br from-cyan-50 to-teal-50 rounded-xl p-3 border border-cyan-100">
307
+ <p className="text-cyan-600 text-xs font-medium mb-1">Method</p>
308
+ <p className="font-semibold text-gray-800">{ad.generation_method}</p>
309
+ </div>
310
+ )}
311
+ {ad.angle_name && (
312
+ <div className="bg-gradient-to-br from-pink-50 to-rose-50 rounded-xl p-3 border border-pink-100">
313
+ <p className="text-pink-500 text-xs font-medium mb-1">Angle</p>
314
+ <p className="font-semibold text-gray-800">{ad.angle_name}</p>
315
+ </div>
316
+ )}
317
+ {ad.concept_name && (
318
+ <div className="bg-gradient-to-br from-violet-50 to-purple-50 rounded-xl p-3 border border-violet-100">
319
+ <p className="text-violet-500 text-xs font-medium mb-1">Concept</p>
320
+ <p className="font-semibold text-gray-800">{ad.concept_name}</p>
321
+ </div>
322
+ )}
323
+ {ad.image_model && (
324
+ <div className="bg-gradient-to-br from-emerald-50 to-green-50 rounded-xl p-3 border border-emerald-100">
325
+ <p className="text-emerald-600 text-xs font-medium mb-1">Image Model</p>
326
+ <p className="font-semibold text-gray-800">{ad.image_model}</p>
327
+ </div>
328
+ )}
329
+ {ad.created_at && (
330
+ <div className="bg-gradient-to-br from-amber-50 to-orange-50 rounded-xl p-3 border border-amber-100">
331
+ <p className="text-amber-600 text-xs font-medium mb-1">Created</p>
332
+ <p className="font-semibold text-gray-800">{formatDate(ad.created_at)}</p>
333
+ </div>
334
+ )}
335
+ </div>
336
+ </div>
337
+ </div>
338
+
339
+ {/* Right - Ad Copy */}
340
+ <div className="space-y-5">
341
+ {/* Headline */}
342
+ <div className="bg-white rounded-2xl shadow-lg shadow-blue-100/30 p-6 border-l-4 border-blue-500">
343
+ <div className="flex items-start justify-between gap-4">
344
+ <div className="flex-1">
345
+ {ad.title && (
346
+ <p className="text-sm text-blue-600 font-medium mb-1">{ad.title}</p>
347
+ )}
348
+ <h1 className="text-2xl font-bold bg-gradient-to-r from-gray-900 to-gray-700 bg-clip-text text-transparent leading-tight">
349
+ {ad.headline}
350
+ </h1>
351
+ </div>
352
+ <div className="relative group shrink-0">
353
+ <Button
354
+ variant="ghost"
355
+ size="sm"
356
+ onClick={() => handleCopyText(ad.headline, "Headline")}
357
+ className="text-blue-500 hover:bg-blue-50"
358
+ >
359
+ <Copy className="h-4 w-4" />
360
+ </Button>
361
+ <span className="absolute -bottom-8 left-1/2 -translate-x-1/2 px-2 py-1 bg-blue-600 text-white text-xs rounded opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap pointer-events-none z-10">
362
+ Copy Headline
363
+ </span>
364
+ </div>
365
+ </div>
366
+ </div>
367
+
368
+ {/* Primary Text */}
369
+ {ad.primary_text && (
370
+ <div className="bg-white rounded-2xl shadow-md p-6 border-l-4 border-cyan-500">
371
+ <div className="flex items-start justify-between gap-4 mb-3">
372
+ <h3 className="text-xs font-bold text-cyan-600 uppercase tracking-wider">Primary Text</h3>
373
+ <div className="relative group">
374
+ <Button
375
+ variant="ghost"
376
+ size="sm"
377
+ onClick={() => handleCopyText(ad.primary_text!, "Primary Text")}
378
+ className="text-cyan-500 hover:bg-cyan-50"
379
+ >
380
+ <Copy className="h-4 w-4" />
381
+ </Button>
382
+ <span className="absolute -bottom-8 left-1/2 -translate-x-1/2 px-2 py-1 bg-cyan-600 text-white text-xs rounded opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap pointer-events-none z-10">
383
+ Copy Text
384
+ </span>
385
+ </div>
386
+ </div>
387
+ <p className="text-gray-700 whitespace-pre-line leading-relaxed">{ad.primary_text}</p>
388
+ </div>
389
+ )}
390
+
391
+ {/* Description */}
392
+ {ad.description && (
393
+ <div className="bg-white rounded-2xl shadow-md p-6 border-l-4 border-violet-500">
394
+ <div className="flex items-start justify-between gap-4 mb-3">
395
+ <h3 className="text-xs font-bold text-violet-600 uppercase tracking-wider">Description</h3>
396
+ <div className="relative group">
397
+ <Button
398
+ variant="ghost"
399
+ size="sm"
400
+ onClick={() => handleCopyText(ad.description!, "Description")}
401
+ className="text-violet-500 hover:bg-violet-50"
402
+ >
403
+ <Copy className="h-4 w-4" />
404
+ </Button>
405
+ <span className="absolute -bottom-8 left-1/2 -translate-x-1/2 px-2 py-1 bg-violet-600 text-white text-xs rounded opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap pointer-events-none z-10">
406
+ Copy Description
407
+ </span>
408
+ </div>
409
+ </div>
410
+ <p className="text-gray-700 leading-relaxed">{ad.description}</p>
411
+ </div>
412
+ )}
413
+
414
+ {/* Body Story */}
415
+ {ad.body_story && (
416
+ <div className="bg-white rounded-2xl shadow-md p-6 border-l-4 border-amber-500">
417
+ <div className="flex items-start justify-between gap-4 mb-3">
418
+ <h3 className="text-xs font-bold text-amber-600 uppercase tracking-wider">Body Story</h3>
419
+ <div className="relative group">
420
+ <Button
421
+ variant="ghost"
422
+ size="sm"
423
+ onClick={() => handleCopyText(ad.body_story!, "Body Story")}
424
+ className="text-amber-500 hover:bg-amber-50"
425
+ >
426
+ <Copy className="h-4 w-4" />
427
+ </Button>
428
+ <span className="absolute -bottom-8 left-1/2 -translate-x-1/2 px-2 py-1 bg-amber-600 text-white text-xs rounded opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap pointer-events-none z-10">
429
+ Copy Story
430
+ </span>
431
+ </div>
432
+ </div>
433
+ <p className="text-gray-700 whitespace-pre-line leading-relaxed">{ad.body_story}</p>
434
+ </div>
435
+ )}
436
+
437
+ {/* CTA */}
438
+ {ad.cta && (
439
+ <div className="bg-gradient-to-r from-emerald-50 to-teal-50 rounded-2xl shadow-md p-6 border border-emerald-200">
440
+ <div className="flex items-start justify-between gap-4 mb-3">
441
+ <h3 className="text-xs font-bold text-emerald-600 uppercase tracking-wider">Call to Action</h3>
442
+ <div className="relative group">
443
+ <Button
444
+ variant="ghost"
445
+ size="sm"
446
+ onClick={() => handleCopyText(ad.cta!, "CTA")}
447
+ className="text-emerald-600 hover:bg-emerald-100"
448
+ >
449
+ <Copy className="h-4 w-4" />
450
+ </Button>
451
+ <span className="absolute -bottom-8 left-1/2 -translate-x-1/2 px-2 py-1 bg-emerald-600 text-white text-xs rounded opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap pointer-events-none z-10">
452
+ Copy CTA
453
+ </span>
454
+ </div>
455
+ </div>
456
+ <p className="text-xl font-bold bg-gradient-to-r from-emerald-600 to-teal-600 bg-clip-text text-transparent">{ad.cta}</p>
457
+ </div>
458
+ )}
459
+
460
+ {/* Psychological Angle */}
461
+ {ad.psychological_angle && (
462
+ <div className="bg-gradient-to-br from-pink-50 via-purple-50 to-blue-50 rounded-2xl p-6 border border-purple-200 shadow-md">
463
+ <div className="flex items-start justify-between gap-4 mb-3">
464
+ <h3 className="text-xs font-bold text-purple-600 uppercase tracking-wider">🧠 Psychological Angle</h3>
465
+ <div className="relative group">
466
+ <Button
467
+ variant="ghost"
468
+ size="sm"
469
+ onClick={() => handleCopyText(ad.psychological_angle!, "Angle")}
470
+ className="text-purple-500 hover:bg-purple-100"
471
+ >
472
+ <Copy className="h-4 w-4" />
473
+ </Button>
474
+ <span className="absolute -bottom-8 left-1/2 -translate-x-1/2 px-2 py-1 bg-purple-600 text-white text-xs rounded opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap pointer-events-none z-10">
475
+ Copy Angle
476
+ </span>
477
+ </div>
478
+ </div>
479
+ <p className="text-gray-700 leading-relaxed">{ad.psychological_angle}</p>
480
+ {ad.why_it_works && (
481
+ <div className="mt-4 pt-4 border-t border-purple-200">
482
+ <p className="text-xs font-bold text-purple-600 uppercase tracking-wider mb-2">💡 Why It Works</p>
483
+ <p className="text-gray-600 text-sm leading-relaxed">{ad.why_it_works}</p>
484
+ </div>
485
+ )}
486
+ </div>
487
+ )}
488
+ </div>
489
+ </div>
490
+ </div>
491
+
492
+ {/* Correction Modal */}
493
+ <CorrectionModal
494
+ isOpen={showCorrectionModal}
495
+ onClose={() => setShowCorrectionModal(false)}
496
+ adId={adId}
497
+ ad={ad}
498
+ onSuccess={(result: ImageCorrectResponse) => {
499
+ // Update the displayed image if correction was successful
500
+ if (result.corrected_image?.image_url) {
501
+ setImageSrc(result.corrected_image.image_url);
502
+ }
503
+ // If a new ad was created, optionally navigate to it or reload the gallery
504
+ if (result.corrected_image?.ad_id) {
505
+ toast.success("Corrected image saved to gallery!");
506
+ // Optionally: router.push(`/gallery/${result.corrected_image.ad_id}`);
507
+ }
508
+ }}
509
+ />
510
+ </div>
511
+ );
512
+ }
frontend/app/gallery/page.tsx ADDED
@@ -0,0 +1,190 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import React, { useEffect, useState, useCallback } from "react";
4
+ import { GalleryGrid } from "@/components/gallery/GalleryGrid";
5
+ import { FilterBar } from "@/components/gallery/FilterBar";
6
+ import { Button } from "@/components/ui/Button";
7
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/Card";
8
+ import { listAds, deleteAd } from "@/lib/api/endpoints";
9
+ import { useGalleryStore } from "@/store/galleryStore";
10
+ import { toast } from "react-hot-toast";
11
+ import { Download, Trash2, CheckSquare, Square } from "lucide-react";
12
+ import type { AdFilters } from "@/types";
13
+
14
+ export default function GalleryPage() {
15
+ const {
16
+ ads,
17
+ total,
18
+ limit,
19
+ offset,
20
+ filters,
21
+ selectedAds,
22
+ isLoading,
23
+ setAds,
24
+ setFilters,
25
+ setOffset,
26
+ toggleAdSelection,
27
+ clearSelection,
28
+ selectAll,
29
+ setIsLoading,
30
+ removeAd,
31
+ } = useGalleryStore();
32
+
33
+ const [viewMode, setViewMode] = useState<"grid" | "list">("grid");
34
+
35
+ const loadAds = useCallback(async () => {
36
+ setIsLoading(true);
37
+ try {
38
+ // Map generation_method filter values to backend values
39
+ let generationMethod: string | null | undefined = filters.generation_method;
40
+ if (generationMethod === "original") {
41
+ generationMethod = "standard"; // Backend uses "standard" for original method
42
+ }
43
+ // "angle_concept_matrix" and "extensive" map directly
44
+
45
+ const response = await listAds({
46
+ niche: filters.niche || undefined,
47
+ generation_method: generationMethod || undefined,
48
+ limit,
49
+ offset,
50
+ });
51
+
52
+ // Filter client-side for search only (text search is better done client-side)
53
+ let filteredAds = response.ads;
54
+
55
+ if (filters.search) {
56
+ const searchLower = filters.search.toLowerCase();
57
+ filteredAds = filteredAds.filter(
58
+ (ad) =>
59
+ ad.headline?.toLowerCase().includes(searchLower) ||
60
+ ad.title?.toLowerCase().includes(searchLower) ||
61
+ ad.primary_text?.toLowerCase().includes(searchLower) ||
62
+ ad.description?.toLowerCase().includes(searchLower)
63
+ );
64
+ }
65
+
66
+ // Use the total from backend (it's already filtered by niche and generation_method)
67
+ // For search, we show the filtered count but this is only for current page
68
+ // In a production app, you'd want server-side search for accurate totals
69
+ const total = filters.search ? filteredAds.length : response.total;
70
+ setAds(filteredAds, total);
71
+ } catch (error: any) {
72
+ toast.error("Failed to load ads");
73
+ console.error(error);
74
+ } finally {
75
+ setIsLoading(false);
76
+ }
77
+ }, [filters, limit, offset, setAds, setIsLoading]);
78
+
79
+ useEffect(() => {
80
+ loadAds();
81
+ }, [loadAds]);
82
+
83
+ const handleBulkDelete = async () => {
84
+ if (selectedAds.length === 0) return;
85
+
86
+ if (!confirm(`Are you sure you want to delete ${selectedAds.length} ad(s)?`)) {
87
+ return;
88
+ }
89
+
90
+ try {
91
+ await Promise.all(selectedAds.map((id) => deleteAd(id)));
92
+ selectedAds.forEach((id) => removeAd(id));
93
+ toast.success(`Deleted ${selectedAds.length} ad(s)`);
94
+ clearSelection();
95
+ loadAds();
96
+ } catch (error: any) {
97
+ toast.error("Failed to delete some ads");
98
+ }
99
+ };
100
+
101
+ const handlePageChange = (newOffset: number) => {
102
+ setOffset(newOffset);
103
+ window.scrollTo({ top: 0, behavior: "smooth" });
104
+ };
105
+
106
+ const totalPages = Math.ceil(total / limit);
107
+ const currentPage = Math.floor(offset / limit) + 1;
108
+
109
+ return (
110
+ <div className="min-h-screen pb-12">
111
+ {/* Hero Section */}
112
+ <div className="relative overflow-hidden bg-gradient-to-br from-blue-50 via-cyan-50 to-pink-50 py-12 mb-8">
113
+ <div className="absolute inset-0 bg-grid-pattern opacity-5"></div>
114
+ <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
115
+ <div className="text-center animate-fade-in">
116
+ <h1 className="text-4xl md:text-5xl font-extrabold mb-4">
117
+ <span className="gradient-text">Gallery</span>
118
+ </h1>
119
+ <p className="text-lg text-gray-600">
120
+ {total} {total === 1 ? "ad" : "ads"} total
121
+ </p>
122
+ </div>
123
+ </div>
124
+ </div>
125
+
126
+ <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
127
+ <div className="mb-8">
128
+ <div className="flex items-center justify-between">
129
+ <div className="flex items-center space-x-2">
130
+ {selectedAds.length > 0 && (
131
+ <>
132
+ <Button variant="outline" size="sm" onClick={clearSelection}>
133
+ <Square className="h-4 w-4 mr-1" />
134
+ Deselect ({selectedAds.length})
135
+ </Button>
136
+ <Button variant="danger" size="sm" onClick={handleBulkDelete}>
137
+ <Trash2 className="h-4 w-4 mr-1" />
138
+ Delete Selected
139
+ </Button>
140
+ </>
141
+ )}
142
+ {selectedAds.length === 0 && ads.length > 0 && (
143
+ <Button variant="outline" size="sm" onClick={selectAll}>
144
+ <CheckSquare className="h-4 w-4 mr-1" />
145
+ Select All
146
+ </Button>
147
+ )}
148
+ </div>
149
+ </div>
150
+ </div>
151
+
152
+ <FilterBar filters={filters} onFiltersChange={setFilters} />
153
+
154
+ <div className="mt-6">
155
+ <GalleryGrid
156
+ ads={ads}
157
+ selectedAds={selectedAds}
158
+ onAdSelect={toggleAdSelection}
159
+ isLoading={isLoading}
160
+ />
161
+ </div>
162
+
163
+ {/* Pagination */}
164
+ {totalPages > 1 && (
165
+ <div className="mt-8 flex items-center justify-center space-x-2">
166
+ <Button
167
+ variant="outline"
168
+ size="sm"
169
+ onClick={() => handlePageChange(Math.max(0, offset - limit))}
170
+ disabled={offset === 0}
171
+ >
172
+ Previous
173
+ </Button>
174
+ <span className="text-sm text-gray-600">
175
+ Page {currentPage} of {totalPages}
176
+ </span>
177
+ <Button
178
+ variant="outline"
179
+ size="sm"
180
+ onClick={() => handlePageChange(offset + limit)}
181
+ disabled={offset + limit >= total}
182
+ >
183
+ Next
184
+ </Button>
185
+ </div>
186
+ )}
187
+ </div>
188
+ </div>
189
+ );
190
+ }
frontend/app/generate/batch/page.tsx ADDED
@@ -0,0 +1,151 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import React, { useState } from "react";
4
+ import { BatchForm } from "@/components/generation/BatchForm";
5
+ import { ProgressBar } from "@/components/ui/ProgressBar";
6
+ import { Card, CardContent } from "@/components/ui/Card";
7
+ import { generateBatch } from "@/lib/api/endpoints";
8
+ import { toast } from "react-hot-toast";
9
+ import { AdPreview } from "@/components/generation/AdPreview";
10
+ import { Sparkles } from "lucide-react";
11
+ import type { Niche, GenerateResponse } from "@/types/api";
12
+
13
+ export default function BatchGeneratePage() {
14
+ const [results, setResults] = useState<GenerateResponse[]>([]);
15
+ const [isGenerating, setIsGenerating] = useState(false);
16
+ const [progress, setProgress] = useState(0);
17
+ const [currentIndex, setCurrentIndex] = useState(0);
18
+
19
+ const handleGenerate = async (data: { niche: Niche; count: number; images_per_ad: number; image_model?: string | null }) => {
20
+ setResults([]);
21
+ setIsGenerating(true);
22
+ setProgress(0);
23
+ setCurrentIndex(0);
24
+
25
+ // Estimate time per ad (roughly 30-60 seconds per ad)
26
+ const estimatedTimePerAd = 45; // seconds
27
+ const totalEstimatedTime = data.count * estimatedTimePerAd;
28
+ let elapsedTime = 0;
29
+ const progressInterval = 500; // Update every 500ms
30
+
31
+ // Start progress simulation
32
+ const progressIntervalId = setInterval(() => {
33
+ elapsedTime += progressInterval / 1000; // Convert to seconds
34
+ // Calculate progress: start at 5%, reach 90% by estimated time
35
+ const progress = Math.min(90, 5 + (elapsedTime / totalEstimatedTime) * 85);
36
+ setProgress(progress);
37
+ }, progressInterval);
38
+
39
+ try {
40
+ const result = await generateBatch(data);
41
+ clearInterval(progressIntervalId);
42
+ setResults(result.ads);
43
+ setProgress(100);
44
+ toast.success(`Successfully generated ${result.count} ads!`);
45
+ } catch (error: any) {
46
+ clearInterval(progressIntervalId);
47
+ setProgress(0);
48
+ toast.error(error.message || "Failed to generate batch");
49
+ } finally {
50
+ setIsGenerating(false);
51
+ }
52
+ };
53
+
54
+ return (
55
+ <div className="min-h-screen pb-12">
56
+ {/* Hero Section */}
57
+ <div className="relative overflow-hidden bg-gradient-to-br from-blue-50 via-cyan-50 to-pink-50 py-12 mb-8">
58
+ <div className="absolute inset-0 bg-grid-pattern opacity-5"></div>
59
+ <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
60
+ <div className="text-center animate-fade-in">
61
+ <h1 className="text-4xl md:text-5xl font-extrabold mb-4">
62
+ <span className="gradient-text">Batch</span>
63
+ <span className="text-gray-900"> Generation</span>
64
+ </h1>
65
+ <p className="text-lg text-gray-600 max-w-2xl mx-auto">
66
+ Generate multiple ads at once for testing
67
+ </p>
68
+ </div>
69
+ </div>
70
+ </div>
71
+
72
+ <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
73
+ <div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
74
+ {/* Left Column - Form */}
75
+ <div className="lg:col-span-1">
76
+ <BatchForm onSubmit={handleGenerate} isLoading={isGenerating} />
77
+
78
+ {isGenerating && (
79
+ <Card variant="glass" className="mt-6 animate-scale-in">
80
+ <CardContent className="pt-6">
81
+ <div className="space-y-4">
82
+ <div className="flex items-center justify-between">
83
+ <div className="flex items-center space-x-3">
84
+ <div className="relative">
85
+ <div className="absolute inset-0 bg-gradient-to-r from-blue-500 to-cyan-500 rounded-full animate-ping opacity-20"></div>
86
+ <div className="relative bg-gradient-to-r from-blue-500 to-cyan-500 rounded-full p-2">
87
+ <Sparkles className="h-5 w-5 text-white animate-pulse" />
88
+ </div>
89
+ </div>
90
+ <div>
91
+ <p className="font-semibold text-gray-900">Generating Batch Ads</p>
92
+ <p className="text-sm text-gray-600">Creating multiple ad variations...</p>
93
+ </div>
94
+ </div>
95
+ </div>
96
+ <ProgressBar
97
+ progress={progress}
98
+ label="Batch Generation Progress"
99
+ showPercentage={true}
100
+ />
101
+ </div>
102
+ </CardContent>
103
+ </Card>
104
+ )}
105
+ </div>
106
+
107
+ {/* Right Column - Results */}
108
+ <div className="lg:col-span-2">
109
+ {results.length > 0 && (
110
+ <div className="space-y-6">
111
+ <div className="flex items-center justify-between">
112
+ <h2 className="text-xl font-semibold text-gray-900">
113
+ Generated Ads ({results.length})
114
+ </h2>
115
+ <div className="flex space-x-2">
116
+ {results.map((_, index) => (
117
+ <button
118
+ key={index}
119
+ onClick={() => setCurrentIndex(index)}
120
+ className={`px-3 py-1 rounded text-sm ${
121
+ currentIndex === index
122
+ ? "bg-blue-600 text-white"
123
+ : "bg-gray-200 text-gray-700 hover:bg-gray-300"
124
+ }`}
125
+ >
126
+ {index + 1}
127
+ </button>
128
+ ))}
129
+ </div>
130
+ </div>
131
+
132
+ {results[currentIndex] && (
133
+ <AdPreview ad={results[currentIndex]} />
134
+ )}
135
+ </div>
136
+ )}
137
+
138
+ {!isGenerating && results.length === 0 && (
139
+ <Card variant="glass" className="text-center py-16">
140
+ <div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-gradient-to-br from-blue-500 to-cyan-500 mb-4">
141
+ <Sparkles className="h-8 w-8 text-white" />
142
+ </div>
143
+ <p className="text-gray-500 text-lg">Fill out the form and click "Generate Batch" to create multiple ads</p>
144
+ </Card>
145
+ )}
146
+ </div>
147
+ </div>
148
+ </div>
149
+ </div>
150
+ );
151
+ }
frontend/app/generate/matrix/page.tsx ADDED
@@ -0,0 +1,198 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import React, { useState } from "react";
4
+ import { AngleSelector } from "@/components/matrix/AngleSelector";
5
+ import { ConceptSelector } from "@/components/matrix/ConceptSelector";
6
+ import { GenerationForm } from "@/components/generation/GenerationForm";
7
+ import { GenerationProgressComponent } from "@/components/generation/GenerationProgress";
8
+ import { AdPreview } from "@/components/generation/AdPreview";
9
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/Card";
10
+ import { Button } from "@/components/ui/Button";
11
+ import { generateMatrixAd } from "@/lib/api/endpoints";
12
+ import { Sparkles } from "lucide-react";
13
+ import { useGenerationStore } from "@/store/generationStore";
14
+ import { useMatrixStore } from "@/store/matrixStore";
15
+ import { toast } from "react-hot-toast";
16
+ import { IMAGE_MODELS } from "@/lib/constants/models";
17
+ import type { Niche, MatrixGenerateResponse, AngleInfo, ConceptInfo } from "@/types/api";
18
+ import type { GenerationProgress } from "@/types";
19
+
20
+ export default function MatrixGeneratePage() {
21
+ const {
22
+ currentGeneration,
23
+ progress,
24
+ isGenerating,
25
+ setCurrentGeneration,
26
+ setProgress,
27
+ setIsGenerating,
28
+ setError,
29
+ reset,
30
+ } = useGenerationStore();
31
+
32
+ const { selectedAngle, selectedConcept, setSelectedAngle, setSelectedConcept } = useMatrixStore();
33
+
34
+ const [niche, setNiche] = useState<Niche>("home_insurance");
35
+ const [numImages, setNumImages] = useState(1);
36
+ const [imageModel, setImageModel] = useState<string | null>(null);
37
+
38
+ const handleGenerate = async () => {
39
+ if (!selectedAngle || !selectedConcept) {
40
+ toast.error("Please select both an angle and a concept");
41
+ return;
42
+ }
43
+
44
+ reset();
45
+ setIsGenerating(true);
46
+ setProgress({
47
+ step: "copy",
48
+ progress: 10,
49
+ message: "Generating ad with selected angle and concept...",
50
+ });
51
+
52
+ try {
53
+ const result = await generateMatrixAd({
54
+ niche,
55
+ angle_key: selectedAngle.key,
56
+ concept_key: selectedConcept.key,
57
+ num_images: numImages,
58
+ image_model: imageModel,
59
+ });
60
+
61
+ setCurrentGeneration(result);
62
+ setProgress({
63
+ step: "complete",
64
+ progress: 100,
65
+ message: "Ad generated successfully!",
66
+ });
67
+
68
+ toast.success("Ad generated successfully!");
69
+ } catch (error: any) {
70
+ setError(error.message || "Failed to generate ad");
71
+ setProgress({
72
+ step: "error",
73
+ progress: 0,
74
+ message: error.message || "An error occurred",
75
+ });
76
+ toast.error(error.message || "Failed to generate ad");
77
+ } finally {
78
+ setIsGenerating(false);
79
+ }
80
+ };
81
+
82
+ return (
83
+ <div className="min-h-screen pb-12">
84
+ {/* Hero Section */}
85
+ <div className="relative overflow-hidden bg-gradient-to-br from-blue-50 via-cyan-50 to-pink-50 py-12 mb-8">
86
+ <div className="absolute inset-0 bg-grid-pattern opacity-5"></div>
87
+ <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
88
+ <div className="text-center animate-fade-in">
89
+ <h1 className="text-4xl md:text-5xl font-extrabold mb-4">
90
+ <span className="gradient-text">Matrix</span>
91
+ <span className="text-gray-900"> Generation</span>
92
+ </h1>
93
+ <p className="text-lg text-gray-600 max-w-2xl mx-auto">
94
+ Generate ads using specific angle × concept combinations
95
+ </p>
96
+ </div>
97
+ </div>
98
+ </div>
99
+
100
+ <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
101
+ <div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
102
+ {/* Left Column - Selection */}
103
+ <div className="lg:col-span-1 space-y-6">
104
+ <Card variant="glass" className="animate-slide-in">
105
+ <CardHeader>
106
+ <CardTitle>Configuration</CardTitle>
107
+ </CardHeader>
108
+ <CardContent className="space-y-4">
109
+ <div>
110
+ <label className="block text-sm font-semibold text-gray-700 mb-2">Niche</label>
111
+ <select
112
+ className="w-full px-4 py-3 rounded-xl border-2 border-gray-300 bg-white/80 backdrop-blur-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all duration-250"
113
+ value={niche}
114
+ onChange={(e) => setNiche(e.target.value as Niche)}
115
+ >
116
+ <option value="home_insurance">Home Insurance</option>
117
+ <option value="glp1">GLP-1</option>
118
+ </select>
119
+ </div>
120
+
121
+ <div>
122
+ <label className="block text-sm font-semibold text-gray-700 mb-2">Image Model</label>
123
+ <select
124
+ className="w-full px-4 py-3 rounded-xl border-2 border-gray-300 bg-white/80 backdrop-blur-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all duration-250"
125
+ value={imageModel || ""}
126
+ onChange={(e) => setImageModel(e.target.value || null)}
127
+ >
128
+ {IMAGE_MODELS.map((model) => (
129
+ <option key={model.value} value={model.value}>
130
+ {model.label}
131
+ </option>
132
+ ))}
133
+ </select>
134
+ </div>
135
+
136
+ <div>
137
+ <label className="block text-sm font-semibold text-gray-700 mb-2">
138
+ Number of Images: <span className="text-blue-600 font-bold">{numImages}</span>
139
+ </label>
140
+ <input
141
+ type="range"
142
+ min="1"
143
+ max="5"
144
+ step="1"
145
+ className="w-full accent-blue-500"
146
+ value={numImages}
147
+ onChange={(e) => setNumImages(Number(e.target.value))}
148
+ />
149
+ <div className="flex justify-between text-xs text-gray-500 mt-1 font-medium">
150
+ <span>1</span>
151
+ <span>5</span>
152
+ </div>
153
+ </div>
154
+ </CardContent>
155
+ </Card>
156
+
157
+ <AngleSelector
158
+ onSelect={setSelectedAngle}
159
+ selectedAngle={selectedAngle}
160
+ />
161
+
162
+ <ConceptSelector
163
+ onSelect={setSelectedConcept}
164
+ selectedConcept={selectedConcept}
165
+ angleKey={selectedAngle?.key}
166
+ />
167
+
168
+ <Button
169
+ variant="primary"
170
+ size="lg"
171
+ className="w-full"
172
+ onClick={handleGenerate}
173
+ isLoading={isGenerating}
174
+ disabled={!selectedAngle || !selectedConcept}
175
+ >
176
+ Generate Ad
177
+ </Button>
178
+
179
+ {isGenerating && (
180
+ <GenerationProgressComponent progress={progress} />
181
+ )}
182
+ </div>
183
+
184
+ {/* Right Column - Preview */}
185
+ <div className="lg:col-span-2">
186
+ {currentGeneration ? (
187
+ <AdPreview ad={currentGeneration} />
188
+ ) : (
189
+ <div className="text-center py-12 text-gray-500">
190
+ <p>Select an angle and concept, then click "Generate Ad"</p>
191
+ </div>
192
+ )}
193
+ </div>
194
+ </div>
195
+ </div>
196
+ </div>
197
+ );
198
+ }
frontend/app/generate/page.tsx ADDED
@@ -0,0 +1,563 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import React, { useState } from "react";
4
+ import { GenerationForm } from "@/components/generation/GenerationForm";
5
+ import { BatchForm } from "@/components/generation/BatchForm";
6
+ import { ExtensiveForm } from "@/components/generation/ExtensiveForm";
7
+ import { GenerationProgressComponent } from "@/components/generation/GenerationProgress";
8
+ import { AdPreview } from "@/components/generation/AdPreview";
9
+ import { AngleSelector } from "@/components/matrix/AngleSelector";
10
+ import { ConceptSelector } from "@/components/matrix/ConceptSelector";
11
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/Card";
12
+ import { Button } from "@/components/ui/Button";
13
+ import { ProgressBar } from "@/components/ui/ProgressBar";
14
+ import { generateAd, generateMatrixAd, generateBatch, generateExtensiveAd } from "@/lib/api/endpoints";
15
+ import { useGenerationStore } from "@/store/generationStore";
16
+ import { useMatrixStore } from "@/store/matrixStore";
17
+ import { toast } from "react-hot-toast";
18
+ import { Sparkles, Zap, Layers, Package, Workflow } from "lucide-react";
19
+ import { IMAGE_MODELS } from "@/lib/constants/models";
20
+ import type { Niche, GenerateResponse } from "@/types/api";
21
+
22
+ type GenerationMode = "standard" | "matrix" | "batch" | "extensive";
23
+
24
+ export default function GeneratePage() {
25
+ const [mode, setMode] = useState<GenerationMode>("standard");
26
+ const [niche, setNiche] = useState<Niche>("home_insurance");
27
+ const [numImages, setNumImages] = useState(1);
28
+ const [imageModel, setImageModel] = useState<string | null>(null);
29
+ const [batchResults, setBatchResults] = useState<GenerateResponse[]>([]);
30
+ const [currentBatchIndex, setCurrentBatchIndex] = useState(0);
31
+ const [batchProgress, setBatchProgress] = useState(0);
32
+
33
+ const {
34
+ currentGeneration,
35
+ progress,
36
+ isGenerating,
37
+ setCurrentGeneration,
38
+ setProgress,
39
+ setIsGenerating,
40
+ setError,
41
+ reset,
42
+ } = useGenerationStore();
43
+
44
+ const { selectedAngle, selectedConcept, setSelectedAngle, setSelectedConcept } = useMatrixStore();
45
+
46
+ const handleStandardGenerate = async (data: { niche: Niche; num_images: number; image_model?: string | null }) => {
47
+ reset();
48
+ setIsGenerating(true);
49
+ setProgress({
50
+ step: "copy",
51
+ progress: 10,
52
+ message: "Generating ad copy...",
53
+ });
54
+
55
+ try {
56
+ // Simulate progress updates
57
+ let currentProgress = 20;
58
+ const progressInterval = setInterval(() => {
59
+ currentProgress = Math.min(90, currentProgress + 5);
60
+ setProgress({
61
+ step: currentProgress < 50 ? "copy" : "image",
62
+ progress: currentProgress,
63
+ message: currentProgress < 50 ? "Generating ad copy..." : "Generating images...",
64
+ });
65
+ }, 1000);
66
+
67
+ // Generate ad
68
+ const result = await generateAd(data);
69
+
70
+ clearInterval(progressInterval);
71
+
72
+ setProgress({
73
+ step: "saving",
74
+ progress: 90,
75
+ message: "Saving to database...",
76
+ });
77
+
78
+ setCurrentGeneration(result);
79
+ setProgress({
80
+ step: "complete",
81
+ progress: 100,
82
+ message: "Ad generated successfully!",
83
+ });
84
+
85
+ toast.success("Ad generated successfully!");
86
+ } catch (error: any) {
87
+ setError(error.message || "Failed to generate ad");
88
+ setProgress({
89
+ step: "error",
90
+ progress: 0,
91
+ message: error.message || "An error occurred",
92
+ });
93
+ toast.error(error.message || "Failed to generate ad");
94
+ } finally {
95
+ setIsGenerating(false);
96
+ }
97
+ };
98
+
99
+ const handleMatrixGenerate = async () => {
100
+ if (!selectedAngle || !selectedConcept) {
101
+ toast.error("Please select both an angle and a concept");
102
+ return;
103
+ }
104
+
105
+ reset();
106
+ setIsGenerating(true);
107
+ setProgress({
108
+ step: "copy",
109
+ progress: 10,
110
+ message: "Generating ad with selected angle and concept...",
111
+ });
112
+
113
+ try {
114
+ const result = await generateMatrixAd({
115
+ niche,
116
+ angle_key: selectedAngle.key,
117
+ concept_key: selectedConcept.key,
118
+ num_images: numImages,
119
+ image_model: imageModel,
120
+ });
121
+
122
+ setCurrentGeneration(result);
123
+ setProgress({
124
+ step: "complete",
125
+ progress: 100,
126
+ message: "Ad generated successfully!",
127
+ });
128
+
129
+ toast.success("Ad generated successfully!");
130
+ } catch (error: any) {
131
+ setError(error.message || "Failed to generate ad");
132
+ setProgress({
133
+ step: "error",
134
+ progress: 0,
135
+ message: error.message || "An error occurred",
136
+ });
137
+ toast.error(error.message || "Failed to generate ad");
138
+ } finally {
139
+ setIsGenerating(false);
140
+ }
141
+ };
142
+
143
+ const handleBatchGenerate = async (data: { niche: Niche; count: number; images_per_ad: number; image_model?: string | null }) => {
144
+ setBatchResults([]);
145
+ setIsGenerating(true);
146
+ setBatchProgress(0);
147
+ setCurrentBatchIndex(0);
148
+
149
+ // Estimate time per ad (roughly 30-60 seconds per ad)
150
+ const estimatedTimePerAd = 45; // seconds
151
+ const totalEstimatedTime = data.count * estimatedTimePerAd;
152
+ let elapsedTime = 0;
153
+ const progressInterval = 500; // Update every 500ms
154
+
155
+ // Start progress simulation
156
+ const progressIntervalId = setInterval(() => {
157
+ elapsedTime += progressInterval / 1000; // Convert to seconds
158
+ // Calculate progress: start at 5%, reach 90% by estimated time
159
+ const progress = Math.min(90, 5 + (elapsedTime / totalEstimatedTime) * 85);
160
+ setBatchProgress(progress);
161
+ }, progressInterval);
162
+
163
+ try {
164
+ const result = await generateBatch(data);
165
+ clearInterval(progressIntervalId);
166
+ setBatchResults(result.ads);
167
+ setBatchProgress(100);
168
+ toast.success(`Successfully generated ${result.count} ads!`);
169
+ } catch (error: any) {
170
+ clearInterval(progressIntervalId);
171
+ setBatchProgress(0);
172
+ toast.error(error.message || "Failed to generate batch");
173
+ } finally {
174
+ setIsGenerating(false);
175
+ }
176
+ };
177
+
178
+ const handleExtensiveGenerate = async (data: {
179
+ niche: Niche;
180
+ target_audience: string;
181
+ offer: string;
182
+ num_images: number;
183
+ num_strategies: number;
184
+ image_model?: string | null;
185
+ }) => {
186
+ reset();
187
+ setIsGenerating(true);
188
+ setProgress({
189
+ step: "copy",
190
+ progress: 10,
191
+ message: "Researching psychology triggers, angles, and concepts...",
192
+ });
193
+
194
+ try {
195
+ // Simulate progress updates for extensive
196
+ let currentProgress = 20;
197
+ const progressSteps = [
198
+ { step: "copy" as const, progress: 20, message: "Researching psychology triggers..." },
199
+ { step: "copy" as const, progress: 35, message: "Retrieving marketing knowledge..." },
200
+ { step: "copy" as const, progress: 50, message: "Creating creative strategies..." },
201
+ { step: "copy" as const, progress: 70, message: "Generating image prompts and copy..." },
202
+ { step: "image" as const, progress: 85, message: "Generating images..." },
203
+ ];
204
+ let stepIndex = 0;
205
+
206
+ const progressInterval = setInterval(() => {
207
+ if (stepIndex < progressSteps.length) {
208
+ setProgress(progressSteps[stepIndex]);
209
+ stepIndex++;
210
+ } else {
211
+ currentProgress = Math.min(95, currentProgress + 2);
212
+ setProgress({
213
+ step: "image",
214
+ progress: currentProgress,
215
+ message: "Generating images...",
216
+ });
217
+ }
218
+ }, 2000);
219
+
220
+ // Generate ad using extensive
221
+ const result = await generateExtensiveAd(data);
222
+
223
+ clearInterval(progressInterval);
224
+
225
+ setProgress({
226
+ step: "saving",
227
+ progress: 90,
228
+ message: "Saving to database...",
229
+ });
230
+
231
+ setCurrentGeneration(result);
232
+ setProgress({
233
+ step: "complete",
234
+ progress: 100,
235
+ message: "Ad generated successfully!",
236
+ });
237
+
238
+ toast.success("Ad generated successfully using Extensive!");
239
+ } catch (error: any) {
240
+ setError(error.message || "Failed to generate ad");
241
+ setProgress({
242
+ step: "error",
243
+ progress: 0,
244
+ message: error.message || "An error occurred",
245
+ });
246
+ toast.error(error.message || "Failed to generate ad");
247
+ } finally {
248
+ setIsGenerating(false);
249
+ }
250
+ };
251
+
252
+ return (
253
+ <div className="min-h-screen pb-12">
254
+ {/* Hero Section */}
255
+ <div className="relative overflow-hidden bg-gradient-to-br from-blue-50 via-cyan-50 to-pink-50 py-12 mb-8">
256
+ <div className="absolute inset-0 bg-grid-pattern opacity-5"></div>
257
+ <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
258
+ <div className="text-center animate-fade-in">
259
+ <h1 className="text-4xl md:text-5xl font-extrabold mb-4">
260
+ <span className="gradient-text">Generate</span>
261
+ <span className="text-gray-900"> Ad</span>
262
+ </h1>
263
+ <p className="text-lg text-gray-600 max-w-2xl mx-auto mb-6">
264
+ Create high-converting ad creatives using standard, matrix, batch, or Extensive generation
265
+ </p>
266
+
267
+ {/* Mode Toggle */}
268
+ <div className="flex items-center justify-center gap-3 flex-wrap">
269
+ <button
270
+ onClick={() => setMode("standard")}
271
+ className={`flex items-center gap-2 px-6 py-3 rounded-xl font-semibold transition-all duration-300 ${
272
+ mode === "standard"
273
+ ? "bg-gradient-to-r from-blue-500 to-cyan-500 text-white shadow-lg scale-105"
274
+ : "bg-white/80 backdrop-blur-sm text-gray-700 hover:bg-white/90 hover:scale-105 border border-gray-200"
275
+ }`}
276
+ >
277
+ <Zap className="h-5 w-5" />
278
+ Standard
279
+ </button>
280
+ <button
281
+ onClick={() => setMode("matrix")}
282
+ className={`flex items-center gap-2 px-6 py-3 rounded-xl font-semibold transition-all duration-300 ${
283
+ mode === "matrix"
284
+ ? "bg-gradient-to-r from-blue-500 to-cyan-500 text-white shadow-lg scale-105"
285
+ : "bg-white/80 backdrop-blur-sm text-gray-700 hover:bg-white/90 hover:scale-105 border border-gray-200"
286
+ }`}
287
+ >
288
+ <Layers className="h-5 w-5" />
289
+ Matrix
290
+ </button>
291
+ <button
292
+ onClick={() => setMode("batch")}
293
+ className={`flex items-center gap-2 px-6 py-3 rounded-xl font-semibold transition-all duration-300 ${
294
+ mode === "batch"
295
+ ? "bg-gradient-to-r from-blue-500 to-cyan-500 text-white shadow-lg scale-105"
296
+ : "bg-white/80 backdrop-blur-sm text-gray-700 hover:bg-white/90 hover:scale-105 border border-gray-200"
297
+ }`}
298
+ >
299
+ <Package className="h-5 w-5" />
300
+ Batch
301
+ </button>
302
+ <button
303
+ onClick={() => setMode("extensive")}
304
+ className={`flex items-center gap-2 px-6 py-3 rounded-xl font-semibold transition-all duration-300 ${
305
+ mode === "extensive"
306
+ ? "bg-gradient-to-r from-blue-500 to-cyan-500 text-white shadow-lg scale-105"
307
+ : "bg-white/80 backdrop-blur-sm text-gray-700 hover:bg-white/90 hover:scale-105 border border-gray-200"
308
+ }`}
309
+ >
310
+ <Workflow className="h-5 w-5" />
311
+ Extensive
312
+ </button>
313
+ </div>
314
+ </div>
315
+ </div>
316
+ </div>
317
+
318
+ <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
319
+ <div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
320
+ {/* Left Column - Form and Configuration */}
321
+ <div className="lg:col-span-1 space-y-6">
322
+ {mode === "standard" ? (
323
+ <>
324
+ <div className="animate-slide-in">
325
+ <GenerationForm
326
+ onSubmit={handleStandardGenerate}
327
+ isLoading={isGenerating}
328
+ />
329
+ </div>
330
+
331
+ {isGenerating && (
332
+ <div className="animate-scale-in">
333
+ <GenerationProgressComponent progress={progress} />
334
+ </div>
335
+ )}
336
+ </>
337
+ ) : mode === "matrix" ? (
338
+ <>
339
+ <Card variant="glass" className="animate-slide-in">
340
+ <CardHeader>
341
+ <CardTitle>Configuration</CardTitle>
342
+ </CardHeader>
343
+ <CardContent className="space-y-4">
344
+ <div>
345
+ <label className="block text-sm font-semibold text-gray-700 mb-2">Niche</label>
346
+ <select
347
+ className="w-full px-4 py-3 rounded-xl border-2 border-gray-300 bg-white/80 backdrop-blur-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all duration-250"
348
+ value={niche}
349
+ onChange={(e) => setNiche(e.target.value as Niche)}
350
+ >
351
+ <option value="home_insurance">Home Insurance</option>
352
+ <option value="glp1">GLP-1</option>
353
+ </select>
354
+ </div>
355
+
356
+ <div>
357
+ <label className="block text-sm font-semibold text-gray-700 mb-2">Image Model</label>
358
+ <select
359
+ className="w-full px-4 py-3 rounded-xl border-2 border-gray-300 bg-white/80 backdrop-blur-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all duration-250"
360
+ value={imageModel || ""}
361
+ onChange={(e) => setImageModel(e.target.value || null)}
362
+ >
363
+ {IMAGE_MODELS.map((model) => (
364
+ <option key={model.value} value={model.value}>
365
+ {model.label}
366
+ </option>
367
+ ))}
368
+ </select>
369
+ </div>
370
+
371
+ <div>
372
+ <label className="block text-sm font-semibold text-gray-700 mb-2">
373
+ Number of Variations: <span className="text-blue-600 font-bold">{numImages}</span>
374
+ </label>
375
+ <input
376
+ type="range"
377
+ min="1"
378
+ max="5"
379
+ step="1"
380
+ className="w-full accent-blue-500"
381
+ value={numImages}
382
+ onChange={(e) => setNumImages(Number(e.target.value))}
383
+ />
384
+ <div className="flex justify-between text-xs text-gray-500 mt-1 font-medium">
385
+ <span>1</span>
386
+ <span>5</span>
387
+ </div>
388
+ <p className="text-xs text-gray-500 mt-1">
389
+ Each variation will have a unique image and slight copy variations
390
+ </p>
391
+ </div>
392
+ </CardContent>
393
+ </Card>
394
+
395
+ <AngleSelector
396
+ onSelect={setSelectedAngle}
397
+ selectedAngle={selectedAngle}
398
+ />
399
+
400
+ <ConceptSelector
401
+ onSelect={setSelectedConcept}
402
+ selectedConcept={selectedConcept}
403
+ angleKey={selectedAngle?.key}
404
+ />
405
+
406
+ <Button
407
+ variant="primary"
408
+ size="lg"
409
+ className="w-full"
410
+ onClick={handleMatrixGenerate}
411
+ isLoading={isGenerating}
412
+ disabled={!selectedAngle || !selectedConcept}
413
+ >
414
+ Generate Ad
415
+ </Button>
416
+
417
+ {isGenerating && (
418
+ <GenerationProgressComponent progress={progress} />
419
+ )}
420
+ </>
421
+ ) : mode === "batch" ? (
422
+ <>
423
+ <div className="animate-slide-in">
424
+ <BatchForm
425
+ onSubmit={handleBatchGenerate}
426
+ isLoading={isGenerating}
427
+ />
428
+ </div>
429
+
430
+ {isGenerating && (
431
+ <Card variant="glass" className="mt-6 animate-scale-in">
432
+ <CardContent className="pt-6">
433
+ <div className="space-y-4">
434
+ <div className="flex items-center justify-between">
435
+ <div className="flex items-center space-x-3">
436
+ <div className="relative">
437
+ <div className="absolute inset-0 bg-gradient-to-r from-blue-500 to-cyan-500 rounded-full animate-ping opacity-20"></div>
438
+ <div className="relative bg-gradient-to-r from-blue-500 to-cyan-500 rounded-full p-2">
439
+ <Sparkles className="h-5 w-5 text-white animate-pulse" />
440
+ </div>
441
+ </div>
442
+ <div>
443
+ <p className="font-semibold text-gray-900">Generating Batch Ads</p>
444
+ <p className="text-sm text-gray-600">Creating multiple ad variations...</p>
445
+ </div>
446
+ </div>
447
+ </div>
448
+ <ProgressBar
449
+ progress={batchProgress}
450
+ label="Batch Generation Progress"
451
+ showPercentage={true}
452
+ />
453
+ </div>
454
+ </CardContent>
455
+ </Card>
456
+ )}
457
+ </>
458
+ ) : (
459
+ <>
460
+ <div className="animate-slide-in">
461
+ <ExtensiveForm
462
+ onSubmit={handleExtensiveGenerate}
463
+ isLoading={isGenerating}
464
+ />
465
+ </div>
466
+
467
+ {isGenerating && (
468
+ <div className="animate-scale-in">
469
+ <GenerationProgressComponent progress={progress} />
470
+ </div>
471
+ )}
472
+ </>
473
+ )}
474
+ </div>
475
+
476
+ {/* Right Column - Preview */}
477
+ <div className="lg:col-span-2">
478
+ {mode === "batch" && batchResults.length > 0 ? (
479
+ <div className="space-y-6 animate-fade-in">
480
+ <div className="bg-white/80 backdrop-blur-sm rounded-2xl shadow-lg p-6 border border-gray-200">
481
+ <div className="flex items-center justify-between flex-wrap gap-4">
482
+ <div>
483
+ <h2 className="text-2xl font-bold bg-gradient-to-r from-blue-600 to-cyan-600 bg-clip-text text-transparent">
484
+ Generated Ads
485
+ </h2>
486
+ <p className="text-sm text-gray-600 mt-1">
487
+ {batchResults.length} {batchResults.length === 1 ? "ad" : "ads"} created
488
+ </p>
489
+ </div>
490
+ <div className="flex space-x-2 flex-wrap gap-2">
491
+ {batchResults.map((_, index) => (
492
+ <button
493
+ key={index}
494
+ onClick={() => setCurrentBatchIndex(index)}
495
+ className={`px-4 py-2 rounded-xl text-sm font-semibold transition-all duration-300 ${
496
+ currentBatchIndex === index
497
+ ? "bg-gradient-to-r from-blue-500 to-cyan-500 text-white shadow-lg scale-105"
498
+ : "bg-white/80 backdrop-blur-sm text-gray-700 hover:bg-white hover:scale-105 border border-gray-200 shadow-sm"
499
+ }`}
500
+ >
501
+ {index + 1}
502
+ </button>
503
+ ))}
504
+ </div>
505
+ </div>
506
+ </div>
507
+
508
+ {batchResults[currentBatchIndex] && (
509
+ <div className="bg-gradient-to-br from-slate-50 via-blue-50/30 to-cyan-50/30 rounded-2xl p-6">
510
+ <AdPreview ad={batchResults[currentBatchIndex]} />
511
+ </div>
512
+ )}
513
+ </div>
514
+ ) : currentGeneration ? (
515
+ <div className="animate-fade-in bg-gradient-to-br from-slate-50 via-blue-50/30 to-cyan-50/30 rounded-2xl p-6">
516
+ <AdPreview ad={currentGeneration} />
517
+ </div>
518
+ ) : (
519
+ <Card variant="glass" className="text-center py-20 border-2 border-dashed border-gray-300">
520
+ <div className="inline-flex items-center justify-center w-20 h-20 rounded-full bg-gradient-to-br from-blue-500 to-cyan-500 mb-6 shadow-lg">
521
+ <Sparkles className="h-10 w-10 text-white" />
522
+ </div>
523
+ <h3 className="text-xl font-bold text-gray-800 mb-3">
524
+ {mode === "standard"
525
+ ? "Ready to Generate"
526
+ : mode === "matrix"
527
+ ? "Select Your Combination"
528
+ : mode === "batch"
529
+ ? "Batch Generation Ready"
530
+ : "Extensive Generation Ready"
531
+ }
532
+ </h3>
533
+ <p className="text-gray-600 text-base mb-4 max-w-md mx-auto leading-relaxed">
534
+ {mode === "standard"
535
+ ? "Fill out the form and click 'Generate Ad' to create your ad creative"
536
+ : mode === "matrix"
537
+ ? "Select an angle and concept, then click 'Generate Ad' to create your ad creative"
538
+ : mode === "batch"
539
+ ? "Fill out the form and click 'Generate Batch' to create multiple ads at once"
540
+ : "Fill out the form with target audience and offer, then click 'Generate with Extensive'"
541
+ }
542
+ </p>
543
+ <div className="inline-flex items-center gap-2 px-4 py-2 bg-gradient-to-r from-blue-50 to-cyan-50 rounded-xl border border-blue-200">
544
+ <div className="w-2 h-2 bg-blue-500 rounded-full animate-pulse"></div>
545
+ <p className="text-sm text-gray-600 font-medium">
546
+ {mode === "standard"
547
+ ? "Uses randomized strategies for variety"
548
+ : mode === "matrix"
549
+ ? "Uses specific angle × concept combinations for targeted testing"
550
+ : mode === "batch"
551
+ ? "Generate 1-20 ads at once for comprehensive testing and variety"
552
+ : "Researcher → Creative Director → Designer → Copywriter flow with knowledge retrieval"
553
+ }
554
+ </p>
555
+ </div>
556
+ </Card>
557
+ )}
558
+ </div>
559
+ </div>
560
+ </div>
561
+ </div>
562
+ );
563
+ }
frontend/app/globals.css ADDED
@@ -0,0 +1,244 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @import "tailwindcss";
2
+
3
+ :root {
4
+ /* Color System - Vibrant Gradients */
5
+ --color-primary-start: #3b82f6; /* blue-500 */
6
+ --color-primary-end: #06b6d4; /* cyan-500 */
7
+ --color-secondary-start: #fb923c; /* orange-400 */
8
+ --color-secondary-end: #ec4899; /* pink-500 */
9
+ --color-success: #10b981;
10
+ --color-warning: #f59e0b;
11
+ --color-error: #ef4444;
12
+
13
+ /* Background */
14
+ --background: linear-gradient(135deg, #f9fafb 0%, #f3f4f6 100%);
15
+ --foreground: #111827;
16
+
17
+ /* Glassmorphism */
18
+ --glass-bg: rgba(255, 255, 255, 0.7);
19
+ --glass-border: rgba(255, 255, 255, 0.18);
20
+ --glass-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.37);
21
+
22
+ /* Shadows */
23
+ --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
24
+ --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
25
+ --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
26
+ --shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
27
+ --shadow-glow: 0 0 20px rgba(59, 130, 246, 0.3);
28
+
29
+ /* Spacing */
30
+ --spacing-xs: 0.25rem; /* 4px */
31
+ --spacing-sm: 0.5rem; /* 8px */
32
+ --spacing-md: 1rem; /* 16px */
33
+ --spacing-lg: 1.5rem; /* 24px */
34
+ --spacing-xl: 2rem; /* 32px */
35
+ --spacing-2xl: 3rem; /* 48px */
36
+ --spacing-3xl: 4rem; /* 64px */
37
+
38
+ /* Border Radius */
39
+ --radius-sm: 0.5rem; /* 8px */
40
+ --radius-md: 0.75rem; /* 12px */
41
+ --radius-lg: 1rem; /* 16px */
42
+ --radius-xl: 1.5rem; /* 24px */
43
+
44
+ /* Transitions */
45
+ --transition-fast: 150ms ease-out;
46
+ --transition-base: 250ms ease-out;
47
+ --transition-slow: 350ms ease-out;
48
+ }
49
+
50
+ body {
51
+ background: var(--background);
52
+ background-attachment: fixed;
53
+ color: var(--foreground);
54
+ font-family: var(--font-inter), system-ui, -apple-system, sans-serif;
55
+ min-height: 100vh;
56
+ }
57
+
58
+ /* Gradient Background Animation */
59
+ @keyframes gradient-shift {
60
+ 0%, 100% {
61
+ background-position: 0% 50%;
62
+ }
63
+ 50% {
64
+ background-position: 100% 50%;
65
+ }
66
+ }
67
+
68
+ .gradient-bg {
69
+ background: linear-gradient(-45deg, #3b82f6, #06b6d4, #fb923c, #ec4899);
70
+ background-size: 400% 400%;
71
+ animation: gradient-shift 15s ease infinite;
72
+ }
73
+
74
+ /* Glassmorphism Effect */
75
+ .glass {
76
+ background: var(--glass-bg);
77
+ backdrop-filter: blur(10px);
78
+ -webkit-backdrop-filter: blur(10px);
79
+ border: 1px solid var(--glass-border);
80
+ box-shadow: var(--glass-shadow);
81
+ }
82
+
83
+ /* Smooth Animations */
84
+ @keyframes fadeIn {
85
+ from {
86
+ opacity: 0;
87
+ transform: translateY(10px);
88
+ }
89
+ to {
90
+ opacity: 1;
91
+ transform: translateY(0);
92
+ }
93
+ }
94
+
95
+ @keyframes slideIn {
96
+ from {
97
+ opacity: 0;
98
+ transform: translateX(-20px);
99
+ }
100
+ to {
101
+ opacity: 1;
102
+ transform: translateX(0);
103
+ }
104
+ }
105
+
106
+ @keyframes scaleIn {
107
+ from {
108
+ opacity: 0;
109
+ transform: scale(0.95);
110
+ }
111
+ to {
112
+ opacity: 1;
113
+ transform: scale(1);
114
+ }
115
+ }
116
+
117
+ @keyframes pulse-glow {
118
+ 0%, 100% {
119
+ box-shadow: 0 0 20px rgba(59, 130, 246, 0.3);
120
+ }
121
+ 50% {
122
+ box-shadow: 0 0 30px rgba(59, 130, 246, 0.5);
123
+ }
124
+ }
125
+
126
+ .animate-fade-in {
127
+ animation: fadeIn 0.5s ease-out;
128
+ }
129
+
130
+ .animate-slide-in {
131
+ animation: slideIn 0.4s ease-out;
132
+ }
133
+
134
+ .animate-scale-in {
135
+ animation: scaleIn 0.3s ease-out;
136
+ }
137
+
138
+ .animate-pulse-glow {
139
+ animation: pulse-glow 2s ease-in-out infinite;
140
+ }
141
+
142
+ /* Hover Effects */
143
+ .hover-lift {
144
+ transition: transform var(--transition-base), box-shadow var(--transition-base);
145
+ }
146
+
147
+ .hover-lift:hover {
148
+ transform: translateY(-4px);
149
+ box-shadow: var(--shadow-xl);
150
+ }
151
+
152
+ .hover-glow {
153
+ transition: box-shadow var(--transition-base);
154
+ }
155
+
156
+ .hover-glow:hover {
157
+ box-shadow: var(--shadow-glow);
158
+ }
159
+
160
+ /* Gradient Text */
161
+ .gradient-text {
162
+ background: linear-gradient(135deg, var(--color-primary-start), var(--color-primary-end));
163
+ -webkit-background-clip: text;
164
+ -webkit-text-fill-color: transparent;
165
+ background-clip: text;
166
+ }
167
+
168
+ .gradient-text-secondary {
169
+ background: linear-gradient(135deg, var(--color-secondary-start), var(--color-secondary-end));
170
+ -webkit-background-clip: text;
171
+ -webkit-text-fill-color: transparent;
172
+ background-clip: text;
173
+ }
174
+
175
+ /* Scrollbar Styling */
176
+ ::-webkit-scrollbar {
177
+ width: 10px;
178
+ }
179
+
180
+ ::-webkit-scrollbar-track {
181
+ background: #f1f1f1;
182
+ border-radius: 10px;
183
+ }
184
+
185
+ ::-webkit-scrollbar-thumb {
186
+ background: linear-gradient(135deg, var(--color-primary-start), var(--color-primary-end));
187
+ border-radius: 10px;
188
+ }
189
+
190
+ ::-webkit-scrollbar-thumb:hover {
191
+ background: linear-gradient(135deg, var(--color-primary-end), var(--color-primary-start));
192
+ }
193
+
194
+ /* Selection */
195
+ ::selection {
196
+ background: rgba(59, 130, 246, 0.3);
197
+ color: inherit;
198
+ }
199
+
200
+ /* Shimmer animation for skeleton loaders */
201
+ @keyframes shimmer {
202
+ 0% {
203
+ background-position: -200% 0;
204
+ }
205
+ 100% {
206
+ background-position: 200% 0;
207
+ }
208
+ }
209
+
210
+ .animate-shimmer {
211
+ animation: shimmer 2s linear infinite;
212
+ }
213
+
214
+ /* Grid pattern background */
215
+ .bg-grid-pattern {
216
+ background-image:
217
+ linear-gradient(to right, rgba(0,0,0,0.1) 1px, transparent 1px),
218
+ linear-gradient(to bottom, rgba(0,0,0,0.1) 1px, transparent 1px);
219
+ background-size: 20px 20px;
220
+ }
221
+
222
+ /* Slow spin animation for icons */
223
+ @keyframes spin-slow {
224
+ from {
225
+ transform: rotate(0deg);
226
+ }
227
+ to {
228
+ transform: rotate(360deg);
229
+ }
230
+ }
231
+
232
+ .animate-spin-slow {
233
+ animation: spin-slow 3s linear infinite;
234
+ }
235
+
236
+ /* Enhanced gradient shift for progress bar */
237
+ @keyframes gradient-shift {
238
+ 0%, 100% {
239
+ background-position: 0% 50%;
240
+ }
241
+ 50% {
242
+ background-position: 100% 50%;
243
+ }
244
+ }
frontend/app/layout.tsx ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { Metadata } from "next";
2
+ import { Inter } from "next/font/google";
3
+ import "./globals.css";
4
+ import { Toaster } from "react-hot-toast";
5
+ import { ConditionalHeader } from "@/components/layout/ConditionalHeader";
6
+
7
+ const inter = Inter({
8
+ subsets: ["latin"],
9
+ variable: "--font-inter",
10
+ });
11
+
12
+ export const metadata: Metadata = {
13
+ title: "Creative Breakthrough",
14
+ description: "Generate high-converting ad creatives for Home Insurance and GLP-1 niches",
15
+ };
16
+
17
+ export default function RootLayout({
18
+ children,
19
+ }: Readonly<{
20
+ children: React.ReactNode;
21
+ }>) {
22
+ return (
23
+ <html lang="en">
24
+ <body className={`${inter.variable} font-sans antialiased bg-gray-50`}>
25
+ <ConditionalHeader />
26
+ <main className="min-h-screen">
27
+ {children}
28
+ </main>
29
+ <Toaster position="top-right" />
30
+ </body>
31
+ </html>
32
+ );
33
+ }
frontend/app/login/page.tsx ADDED
@@ -0,0 +1,128 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import React, { useState, useEffect } from "react";
4
+ import { useRouter } from "next/navigation";
5
+ import { useForm } from "react-hook-form";
6
+ import { zodResolver } from "@hookform/resolvers/zod";
7
+ import { z } from "zod";
8
+ import { Input } from "@/components/ui/Input";
9
+ import { Button } from "@/components/ui/Button";
10
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/Card";
11
+ import { useAuthStore } from "@/store/authStore";
12
+ import { login } from "@/lib/api/endpoints";
13
+ import { Lock, User, Rocket, AlertCircle } from "lucide-react";
14
+ import { toast } from "react-hot-toast";
15
+
16
+ const loginSchema = z.object({
17
+ username: z.string().min(1, "Username is required"),
18
+ password: z.string().min(1, "Password is required"),
19
+ });
20
+
21
+ type LoginFormData = z.infer<typeof loginSchema>;
22
+
23
+ export default function LoginPage() {
24
+ const router = useRouter();
25
+ const { isAuthenticated, login: setAuth } = useAuthStore();
26
+ const [isLoading, setIsLoading] = useState(false);
27
+ const {
28
+ register,
29
+ handleSubmit,
30
+ formState: { errors },
31
+ } = useForm<LoginFormData>({
32
+ resolver: zodResolver(loginSchema),
33
+ });
34
+
35
+ // Redirect if already authenticated
36
+ useEffect(() => {
37
+ if (isAuthenticated) {
38
+ router.push("/");
39
+ }
40
+ }, [isAuthenticated, router]);
41
+
42
+ const onSubmit = async (data: LoginFormData) => {
43
+ setIsLoading(true);
44
+ try {
45
+ const response = await login(data.username, data.password);
46
+ setAuth(response.token, response.username);
47
+ toast.success("Login successful!");
48
+ router.push("/");
49
+ } catch (error: any) {
50
+ const errorMessage =
51
+ error.response?.data?.detail || error.message || "Login failed. Please check your credentials.";
52
+ toast.error(errorMessage);
53
+ } finally {
54
+ setIsLoading(false);
55
+ }
56
+ };
57
+
58
+ return (
59
+ <div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-blue-50 via-cyan-50 to-pink-50 py-12 px-4 sm:px-6 lg:px-8">
60
+ <div className="max-w-md w-full space-y-8">
61
+ {/* Logo and Title */}
62
+ <div className="text-center animate-fade-in">
63
+ <div className="flex justify-center mb-4">
64
+ <div className="relative">
65
+ <Rocket className="h-16 w-16 text-blue-500 animate-bounce" />
66
+ <div className="absolute inset-0 bg-blue-500/20 rounded-full blur-xl"></div>
67
+ </div>
68
+ </div>
69
+ <h1 className="text-4xl font-extrabold mb-2">
70
+ <span className="gradient-text">Creative</span>
71
+ <span className="text-gray-900"> Breakthrough</span>
72
+ </h1>
73
+ <p className="text-gray-600 text-lg">Sign in to your account</p>
74
+ </div>
75
+
76
+ {/* Login Card */}
77
+ <Card variant="glass" className="animate-scale-in" style={{ animationDelay: "0.2s" }}>
78
+ <CardHeader>
79
+ <CardTitle className="text-2xl text-center">Login</CardTitle>
80
+ </CardHeader>
81
+ <CardContent>
82
+ <form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
83
+ <div>
84
+ <Input
85
+ label="Username"
86
+ type="text"
87
+ placeholder="Enter your username"
88
+ error={errors.username?.message}
89
+ {...register("username")}
90
+ />
91
+ </div>
92
+
93
+ <div>
94
+ <Input
95
+ label="Password"
96
+ type="password"
97
+ placeholder="Enter your password"
98
+ error={errors.password?.message}
99
+ {...register("password")}
100
+ />
101
+ </div>
102
+
103
+ <Button
104
+ type="submit"
105
+ variant="primary"
106
+ size="lg"
107
+ className="w-full"
108
+ isLoading={isLoading}
109
+ >
110
+ Sign In
111
+ </Button>
112
+ </form>
113
+
114
+ <div className="mt-6 p-4 bg-blue-50/50 rounded-xl border border-blue-200/50">
115
+ <div className="flex items-start space-x-3">
116
+ <AlertCircle className="h-5 w-5 text-blue-600 mt-0.5 flex-shrink-0" />
117
+ <div className="text-sm text-blue-800">
118
+ <p className="font-semibold mb-1">Note:</p>
119
+ <p>Credentials are managed manually. Please contact your administrator for access.</p>
120
+ </div>
121
+ </div>
122
+ </div>
123
+ </CardContent>
124
+ </Card>
125
+ </div>
126
+ </div>
127
+ );
128
+ }
frontend/app/matrix/angles/page.tsx ADDED
@@ -0,0 +1,134 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import React, { useEffect, useState } from "react";
4
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/Card";
5
+ import { Input } from "@/components/ui/Input";
6
+ import { Select } from "@/components/ui/Select";
7
+ import { LoadingSpinner } from "@/components/ui/LoadingSpinner";
8
+ import { getAllAngles } from "@/lib/api/endpoints";
9
+ import type { AnglesResponse } from "@/types/api";
10
+
11
+ export default function AnglesPage() {
12
+ const [angles, setAngles] = useState<AnglesResponse | null>(null);
13
+ const [isLoading, setIsLoading] = useState(true);
14
+ const [searchTerm, setSearchTerm] = useState("");
15
+ const [selectedCategory, setSelectedCategory] = useState<string>("");
16
+
17
+ useEffect(() => {
18
+ loadAngles();
19
+ }, []);
20
+
21
+ const loadAngles = async () => {
22
+ try {
23
+ const data = await getAllAngles();
24
+ setAngles(data);
25
+ } catch (error) {
26
+ console.error("Failed to load angles:", error);
27
+ } finally {
28
+ setIsLoading(false);
29
+ }
30
+ };
31
+
32
+ if (isLoading) {
33
+ return (
34
+ <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
35
+ <div className="flex items-center justify-center h-64">
36
+ <LoadingSpinner size="lg" />
37
+ </div>
38
+ </div>
39
+ );
40
+ }
41
+
42
+ if (!angles) {
43
+ return (
44
+ <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
45
+ <div className="text-center py-12">
46
+ <p className="text-gray-500">Failed to load angles</p>
47
+ </div>
48
+ </div>
49
+ );
50
+ }
51
+
52
+ // Filter angles
53
+ let filteredAngles: Array<{ category: string; angle: any }> = [];
54
+
55
+ Object.entries(angles.categories).forEach(([catKey, catData]) => {
56
+ if (selectedCategory && catKey !== selectedCategory) return;
57
+
58
+ catData.angles.forEach((angle) => {
59
+ if (
60
+ !searchTerm ||
61
+ angle.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
62
+ angle.trigger.toLowerCase().includes(searchTerm.toLowerCase()) ||
63
+ angle.key.toLowerCase().includes(searchTerm.toLowerCase())
64
+ ) {
65
+ filteredAngles.push({ category: catData.name, angle });
66
+ }
67
+ });
68
+ });
69
+
70
+ const categories = Object.entries(angles.categories).map(([key, data]) => ({
71
+ value: key,
72
+ label: `${data.name} (${data.angle_count})`,
73
+ }));
74
+
75
+ return (
76
+ <div className="min-h-screen pb-12">
77
+ {/* Hero Section */}
78
+ <div className="relative overflow-hidden bg-gradient-to-br from-blue-50 via-cyan-50 to-pink-50 py-12 mb-8">
79
+ <div className="absolute inset-0 bg-grid-pattern opacity-5"></div>
80
+ <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
81
+ <div className="text-center animate-fade-in">
82
+ <h1 className="text-4xl md:text-5xl font-extrabold mb-4">
83
+ <span className="gradient-text">Angles</span>
84
+ </h1>
85
+ <p className="text-lg text-gray-600">
86
+ Browse all {angles.total_angles} available angles
87
+ </p>
88
+ </div>
89
+ </div>
90
+ </div>
91
+
92
+ <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
93
+ <div className="mb-6 grid grid-cols-1 md:grid-cols-2 gap-4">
94
+ <Input
95
+ placeholder="Search angles..."
96
+ value={searchTerm}
97
+ onChange={(e) => setSearchTerm(e.target.value)}
98
+ />
99
+ <Select
100
+ options={[{ value: "", label: "All Categories" }, ...categories]}
101
+ value={selectedCategory}
102
+ onChange={(e) => setSelectedCategory(e.target.value)}
103
+ />
104
+ </div>
105
+
106
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
107
+ {filteredAngles.map(({ category, angle }, index) => (
108
+ <Card key={angle.key} variant="glass" className="animate-scale-in hover-lift" style={{ animationDelay: `${index * 0.05}s` }}>
109
+ <CardContent className="pt-6">
110
+ <div className="mb-3">
111
+ <span className="text-xs font-bold px-3 py-1 bg-gradient-to-r from-blue-500 to-cyan-500 text-white rounded-full">
112
+ {category}
113
+ </span>
114
+ </div>
115
+ <h3 className="font-bold text-gray-900 mb-2 text-lg">{angle.name}</h3>
116
+ <p className="text-sm text-gray-600 mb-3">{angle.trigger}</p>
117
+ {angle.example && (
118
+ <p className="text-xs text-gray-500 italic mb-3">"{angle.example}"</p>
119
+ )}
120
+ <p className="text-xs text-gray-400 font-mono">{angle.key}</p>
121
+ </CardContent>
122
+ </Card>
123
+ ))}
124
+ </div>
125
+
126
+ {filteredAngles.length === 0 && (
127
+ <div className="text-center py-12">
128
+ <p className="text-gray-500">No angles found matching your filters</p>
129
+ </div>
130
+ )}
131
+ </div>
132
+ </div>
133
+ );
134
+ }
frontend/app/matrix/concepts/page.tsx ADDED
@@ -0,0 +1,132 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import React, { useEffect, useState } from "react";
4
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/Card";
5
+ import { Input } from "@/components/ui/Input";
6
+ import { Select } from "@/components/ui/Select";
7
+ import { LoadingSpinner } from "@/components/ui/LoadingSpinner";
8
+ import { getAllConcepts } from "@/lib/api/endpoints";
9
+ import type { ConceptsResponse } from "@/types/api";
10
+
11
+ export default function ConceptsPage() {
12
+ const [concepts, setConcepts] = useState<ConceptsResponse | null>(null);
13
+ const [isLoading, setIsLoading] = useState(true);
14
+ const [searchTerm, setSearchTerm] = useState("");
15
+ const [selectedCategory, setSelectedCategory] = useState<string>("");
16
+
17
+ useEffect(() => {
18
+ loadConcepts();
19
+ }, []);
20
+
21
+ const loadConcepts = async () => {
22
+ try {
23
+ const data = await getAllConcepts();
24
+ setConcepts(data);
25
+ } catch (error) {
26
+ console.error("Failed to load concepts:", error);
27
+ } finally {
28
+ setIsLoading(false);
29
+ }
30
+ };
31
+
32
+ if (isLoading) {
33
+ return (
34
+ <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
35
+ <div className="flex items-center justify-center h-64">
36
+ <LoadingSpinner size="lg" />
37
+ </div>
38
+ </div>
39
+ );
40
+ }
41
+
42
+ if (!concepts) {
43
+ return (
44
+ <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
45
+ <div className="text-center py-12">
46
+ <p className="text-gray-500">Failed to load concepts</p>
47
+ </div>
48
+ </div>
49
+ );
50
+ }
51
+
52
+ // Filter concepts
53
+ let filteredConcepts: Array<{ category: string; concept: any }> = [];
54
+
55
+ Object.entries(concepts.categories).forEach(([catKey, catData]) => {
56
+ if (selectedCategory && catKey !== selectedCategory) return;
57
+
58
+ catData.concepts.forEach((concept) => {
59
+ if (
60
+ !searchTerm ||
61
+ concept.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
62
+ concept.structure.toLowerCase().includes(searchTerm.toLowerCase()) ||
63
+ concept.key.toLowerCase().includes(searchTerm.toLowerCase())
64
+ ) {
65
+ filteredConcepts.push({ category: catData.name, concept });
66
+ }
67
+ });
68
+ });
69
+
70
+ const categories = Object.entries(concepts.categories).map(([key, data]) => ({
71
+ value: key,
72
+ label: `${data.name} (${data.concept_count})`,
73
+ }));
74
+
75
+ return (
76
+ <div className="min-h-screen pb-12">
77
+ {/* Hero Section */}
78
+ <div className="relative overflow-hidden bg-gradient-to-br from-blue-50 via-cyan-50 to-pink-50 py-12 mb-8">
79
+ <div className="absolute inset-0 bg-grid-pattern opacity-5"></div>
80
+ <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
81
+ <div className="text-center animate-fade-in">
82
+ <h1 className="text-4xl md:text-5xl font-extrabold mb-4">
83
+ <span className="gradient-text">Concepts</span>
84
+ </h1>
85
+ <p className="text-lg text-gray-600">
86
+ Browse all {concepts.total_concepts} available concepts
87
+ </p>
88
+ </div>
89
+ </div>
90
+ </div>
91
+
92
+ <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
93
+ <div className="mb-6 grid grid-cols-1 md:grid-cols-2 gap-4">
94
+ <Input
95
+ placeholder="Search concepts..."
96
+ value={searchTerm}
97
+ onChange={(e) => setSearchTerm(e.target.value)}
98
+ />
99
+ <Select
100
+ options={[{ value: "", label: "All Categories" }, ...categories]}
101
+ value={selectedCategory}
102
+ onChange={(e) => setSelectedCategory(e.target.value)}
103
+ />
104
+ </div>
105
+
106
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
107
+ {filteredConcepts.map(({ category, concept }, index) => (
108
+ <Card key={concept.key} variant="glass" className="animate-scale-in hover-lift" style={{ animationDelay: `${index * 0.05}s` }}>
109
+ <CardContent className="pt-6">
110
+ <div className="mb-3">
111
+ <span className="text-xs font-bold px-3 py-1 bg-gradient-to-r from-cyan-500 to-pink-600 text-white rounded-full">
112
+ {category}
113
+ </span>
114
+ </div>
115
+ <h3 className="font-bold text-gray-900 mb-2 text-lg">{concept.name}</h3>
116
+ <p className="text-sm text-gray-600 mb-2">{concept.structure}</p>
117
+ <p className="text-xs text-gray-500 mb-3">{concept.visual}</p>
118
+ <p className="text-xs text-gray-400 font-mono">{concept.key}</p>
119
+ </CardContent>
120
+ </Card>
121
+ ))}
122
+ </div>
123
+
124
+ {filteredConcepts.length === 0 && (
125
+ <div className="text-center py-12">
126
+ <p className="text-gray-500">No concepts found matching your filters</p>
127
+ </div>
128
+ )}
129
+ </div>
130
+ </div>
131
+ );
132
+ }
frontend/app/matrix/page.tsx ADDED
@@ -0,0 +1,107 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import React from "react";
4
+ import Link from "next/link";
5
+ import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/Card";
6
+ import { Button } from "@/components/ui/Button";
7
+ import { Layers, Search, TestTube, Sparkles } from "lucide-react";
8
+
9
+ export default function MatrixPage() {
10
+ return (
11
+ <div className="min-h-screen pb-12">
12
+ {/* Hero Section */}
13
+ <div className="relative overflow-hidden bg-gradient-to-br from-blue-50 via-cyan-50 to-pink-50 py-12 mb-8">
14
+ <div className="absolute inset-0 bg-grid-pattern opacity-5"></div>
15
+ <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
16
+ <div className="text-center animate-fade-in">
17
+ <h1 className="text-4xl md:text-5xl font-extrabold mb-4">
18
+ <span className="gradient-text">Matrix</span>
19
+ <span className="text-gray-900"> System</span>
20
+ </h1>
21
+ <p className="text-lg text-gray-600 max-w-2xl mx-auto">
22
+ Explore the Angle × Concept matrix for systematic ad generation
23
+ </p>
24
+ </div>
25
+ </div>
26
+ </div>
27
+
28
+ <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
29
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
30
+ <Card variant="glass" className="animate-scale-in" style={{ animationDelay: "0.1s" }}>
31
+ <CardHeader>
32
+ <div className="p-3 rounded-xl bg-gradient-to-br from-blue-500 to-cyan-500 w-fit mb-3">
33
+ <Layers className="h-6 w-6 text-white" />
34
+ </div>
35
+ <CardTitle>Generate with Matrix</CardTitle>
36
+ <CardDescription>
37
+ Select specific angle and concept combinations
38
+ </CardDescription>
39
+ </CardHeader>
40
+ <CardContent>
41
+ <Link href="/generate/matrix">
42
+ <Button variant="primary" className="w-full group">
43
+ <Sparkles className="h-4 w-4 mr-2 group-hover:rotate-12 transition-transform duration-300" />
44
+ Generate Ad
45
+ </Button>
46
+ </Link>
47
+ </CardContent>
48
+ </Card>
49
+
50
+ <Card variant="glass" className="animate-scale-in" style={{ animationDelay: "0.2s" }}>
51
+ <CardHeader>
52
+ <div className="p-3 rounded-xl bg-gradient-to-br from-cyan-500 to-pink-600 w-fit mb-3">
53
+ <Search className="h-6 w-6 text-white" />
54
+ </div>
55
+ <CardTitle>Browse Angles</CardTitle>
56
+ <CardDescription>
57
+ Explore all 100 available angles
58
+ </CardDescription>
59
+ </CardHeader>
60
+ <CardContent>
61
+ <Link href="/matrix/angles">
62
+ <Button variant="outline" className="w-full">View Angles</Button>
63
+ </Link>
64
+ </CardContent>
65
+ </Card>
66
+
67
+ <Card variant="glass" className="animate-scale-in" style={{ animationDelay: "0.3s" }}>
68
+ <CardHeader>
69
+ <div className="p-3 rounded-xl bg-gradient-to-br from-green-500 to-emerald-600 w-fit mb-3">
70
+ <Search className="h-6 w-6 text-white" />
71
+ </div>
72
+ <CardTitle>Browse Concepts</CardTitle>
73
+ <CardDescription>
74
+ Explore all 100 available concepts
75
+ </CardDescription>
76
+ </CardHeader>
77
+ <CardContent>
78
+ <Link href="/matrix/concepts">
79
+ <Button variant="outline" className="w-full">View Concepts</Button>
80
+ </Link>
81
+ </CardContent>
82
+ </Card>
83
+
84
+ <Card variant="glass" className="md:col-span-2 lg:col-span-3 animate-scale-in" style={{ animationDelay: "0.4s" }}>
85
+ <CardHeader>
86
+ <div className="p-3 rounded-xl bg-gradient-to-br from-orange-500 to-pink-600 w-fit mb-3">
87
+ <TestTube className="h-6 w-6 text-white" />
88
+ </div>
89
+ <CardTitle>Testing Matrix Builder</CardTitle>
90
+ <CardDescription>
91
+ Generate systematic testing matrices for optimization
92
+ </CardDescription>
93
+ </CardHeader>
94
+ <CardContent>
95
+ <Link href="/matrix/testing">
96
+ <Button variant="secondary" className="w-full group">
97
+ <TestTube className="h-4 w-4 mr-2 group-hover:rotate-12 transition-transform duration-300" />
98
+ Build Testing Matrix
99
+ </Button>
100
+ </Link>
101
+ </CardContent>
102
+ </Card>
103
+ </div>
104
+ </div>
105
+ </div>
106
+ );
107
+ }
frontend/app/matrix/testing/page.tsx ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import React from "react";
4
+ import { TestingMatrixBuilder } from "@/components/matrix/TestingMatrixBuilder";
5
+
6
+ export default function TestingMatrixPage() {
7
+ return (
8
+ <div className="min-h-screen pb-12">
9
+ {/* Hero Section */}
10
+ <div className="relative overflow-hidden bg-gradient-to-br from-blue-50 via-cyan-50 to-pink-50 py-12 mb-8">
11
+ <div className="absolute inset-0 bg-grid-pattern opacity-5"></div>
12
+ <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
13
+ <div className="text-center animate-fade-in">
14
+ <h1 className="text-4xl md:text-5xl font-extrabold mb-4">
15
+ <span className="gradient-text">Testing Matrix</span>
16
+ <span className="text-gray-900"> Builder</span>
17
+ </h1>
18
+ <p className="text-lg text-gray-600 max-w-2xl mx-auto">
19
+ Create systematic testing matrices for ad optimization
20
+ </p>
21
+ </div>
22
+ </div>
23
+ </div>
24
+
25
+ <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
26
+ <TestingMatrixBuilder />
27
+ </div>
28
+ </div>
29
+ );
30
+ }
frontend/app/page.tsx ADDED
@@ -0,0 +1,270 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import React, { useEffect, useState, memo, useCallback } from "react";
4
+ import Link from "next/link";
5
+ import Image from "next/image";
6
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/Card";
7
+ import { Button } from "@/components/ui/Button";
8
+ import { LoadingSpinner } from "@/components/ui/LoadingSpinner";
9
+ import { getDbStats, listAds } from "@/lib/api/endpoints";
10
+ import { formatRelativeDate, formatNiche, getImageUrl, getImageUrlFallback } from "@/lib/utils/formatters";
11
+ import { Home, Sparkles, Grid, TrendingUp, Database } from "lucide-react";
12
+ import type { DbStatsResponse, AdCreativeDB } from "@/types/api";
13
+
14
+ // Component for dashboard ad cards with image error handling
15
+ const DashboardAdCard = memo(function DashboardAdCard({
16
+ ad,
17
+ initialImageSrc,
18
+ fallback,
19
+ index
20
+ }: {
21
+ ad: AdCreativeDB;
22
+ initialImageSrc: string | null;
23
+ fallback: string | null;
24
+ index: number;
25
+ }) {
26
+ const [imageSrc, setImageSrc] = useState<string | null>(initialImageSrc);
27
+ const [imageError, setImageError] = useState(false);
28
+
29
+ const handleImageError = useCallback(() => {
30
+ if (!imageError && fallback && imageSrc === initialImageSrc) {
31
+ setImageSrc(fallback);
32
+ setImageError(true);
33
+ } else {
34
+ setImageSrc(null);
35
+ }
36
+ }, [imageError, fallback, imageSrc, initialImageSrc]);
37
+
38
+ return (
39
+ <Link
40
+ href={`/gallery/${ad.id}`}
41
+ className="block group animate-scale-in"
42
+ style={{ animationDelay: `${index * 0.1}s` }}
43
+ >
44
+ <Card variant="elevated" className="h-full overflow-hidden">
45
+ <CardContent className="p-0">
46
+ {(imageSrc || ad.image_filename || ad.image_url) && (
47
+ <div className="aspect-video bg-gradient-to-br from-gray-100 to-gray-200 rounded-t-2xl overflow-hidden relative flex items-center justify-center">
48
+ {imageSrc ? (
49
+ <Image
50
+ src={imageSrc}
51
+ alt={ad.headline}
52
+ fill
53
+ className="object-contain group-hover:scale-105 transition-transform duration-500"
54
+ onError={handleImageError}
55
+ loading="lazy"
56
+ sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
57
+ unoptimized={imageSrc.startsWith('http://') || imageSrc.includes('r2.cloudflarestorage.com') || imageSrc.includes('replicate.delivery')}
58
+ />
59
+ ) : (
60
+ <div className="w-full h-full flex items-center justify-center">
61
+ <div className="text-center text-gray-400">
62
+ <svg className="w-12 h-12 mx-auto mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
63
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
64
+ </svg>
65
+ <p className="text-xs">Image unavailable</p>
66
+ </div>
67
+ </div>
68
+ )}
69
+ <div className="absolute inset-0 bg-gradient-to-t from-black/20 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
70
+ </div>
71
+ )}
72
+ <div className="p-5">
73
+ <div className="flex items-center justify-between mb-3">
74
+ <span className="text-xs font-bold px-3 py-1 bg-gradient-to-r from-blue-500 to-cyan-500 text-white rounded-full">
75
+ {formatNiche(ad.niche)}
76
+ </span>
77
+ <span className="text-xs text-gray-500 font-medium">
78
+ {formatRelativeDate(ad.created_at)}
79
+ </span>
80
+ </div>
81
+ <h3 className="font-bold text-gray-900 line-clamp-2 mb-2 group-hover:text-blue-600 transition-colors">
82
+ {ad.headline}
83
+ </h3>
84
+ {ad.title && (
85
+ <p className="text-sm text-gray-600 line-clamp-1">
86
+ {ad.title}
87
+ </p>
88
+ )}
89
+ </div>
90
+ </CardContent>
91
+ </Card>
92
+ </Link>
93
+ );
94
+ });
95
+
96
+ export default function Dashboard() {
97
+ const [stats, setStats] = useState<DbStatsResponse | null>(null);
98
+ const [recentAds, setRecentAds] = useState<AdCreativeDB[]>([]);
99
+ const [isLoading, setIsLoading] = useState(true);
100
+
101
+ useEffect(() => {
102
+ const loadData = async () => {
103
+ try {
104
+ // Load stats
105
+ const statsData = await getDbStats();
106
+ setStats(statsData);
107
+
108
+ // Load recent ads
109
+ const adsData = await listAds({ limit: 6 });
110
+ setRecentAds(adsData.ads);
111
+ } catch (error) {
112
+ console.error("Error loading dashboard data:", error);
113
+ } finally {
114
+ setIsLoading(false);
115
+ }
116
+ };
117
+
118
+ loadData();
119
+ }, []);
120
+
121
+ if (isLoading) {
122
+ return (
123
+ <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
124
+ <div className="flex items-center justify-center h-64">
125
+ <LoadingSpinner size="lg" />
126
+ </div>
127
+ </div>
128
+ );
129
+ }
130
+
131
+ return (
132
+ <div className="min-h-screen pb-12">
133
+ {/* Hero Section */}
134
+ <div className="relative overflow-hidden bg-gradient-to-br from-blue-50 via-cyan-50 to-pink-50 py-16 mb-12">
135
+ <div className="absolute inset-0 bg-grid-pattern opacity-5"></div>
136
+ <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
137
+ <div className="text-center animate-fade-in">
138
+ <h1 className="text-5xl md:text-6xl font-extrabold mb-4">
139
+ <span className="gradient-text">Creative</span>
140
+ <span className="text-gray-900"> Breakthrough</span>
141
+ </h1>
142
+ <p className="text-xl text-gray-600 max-w-2xl mx-auto">
143
+ Create high-converting ad creatives for Home Insurance and GLP-1 niches with AI-powered generation
144
+ </p>
145
+ </div>
146
+ </div>
147
+ </div>
148
+
149
+ <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
150
+ {/* Stats Grid */}
151
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-8">
152
+ <Card variant="glass" className="animate-scale-in" style={{ animationDelay: "0.1s" }}>
153
+ <CardContent className="pt-6">
154
+ <div className="flex items-center justify-between">
155
+ <div>
156
+ <p className="text-sm font-semibold text-gray-600 mb-1">Total Ads</p>
157
+ <p className="text-3xl font-bold text-gray-900">
158
+ {stats?.total_ads ?? 0}
159
+ </p>
160
+ </div>
161
+ <div className="p-3 rounded-xl bg-gradient-to-br from-blue-500 to-cyan-500 shadow-lg">
162
+ <Database className="h-8 w-8 text-white" />
163
+ </div>
164
+ </div>
165
+ </CardContent>
166
+ </Card>
167
+
168
+ <Card variant="glass" className="animate-scale-in" style={{ animationDelay: "0.2s" }}>
169
+ <CardContent className="pt-6">
170
+ <div className="flex items-center justify-between">
171
+ <div>
172
+ <p className="text-sm font-semibold text-gray-600 mb-1">Home Insurance</p>
173
+ <p className="text-3xl font-bold text-gray-900">
174
+ {stats?.by_niche?.home_insurance ?? 0}
175
+ </p>
176
+ </div>
177
+ <div className="p-3 rounded-xl bg-gradient-to-br from-green-500 to-emerald-600 shadow-lg">
178
+ <Home className="h-8 w-8 text-white" />
179
+ </div>
180
+ </div>
181
+ </CardContent>
182
+ </Card>
183
+
184
+ <Card variant="glass" className="animate-scale-in" style={{ animationDelay: "0.3s" }}>
185
+ <CardContent className="pt-6">
186
+ <div className="flex items-center justify-between">
187
+ <div>
188
+ <p className="text-sm font-semibold text-gray-600 mb-1">GLP-1</p>
189
+ <p className="text-3xl font-bold text-gray-900">
190
+ {stats?.by_niche?.glp1 ?? 0}
191
+ </p>
192
+ </div>
193
+ <div className="p-3 rounded-xl bg-gradient-to-br from-cyan-500 to-pink-600 shadow-lg">
194
+ <TrendingUp className="h-8 w-8 text-white" />
195
+ </div>
196
+ </div>
197
+ </CardContent>
198
+ </Card>
199
+ </div>
200
+
201
+ {/* Quick Actions */}
202
+ <Card variant="glass" className="mb-8 animate-fade-in">
203
+ <CardHeader>
204
+ <CardTitle>Quick Actions</CardTitle>
205
+ </CardHeader>
206
+ <CardContent>
207
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
208
+ <Link href="/generate">
209
+ <Button variant="primary" size="lg" className="w-full group">
210
+ <Sparkles className="h-5 w-5 mr-2 group-hover:rotate-12 transition-transform duration-300" />
211
+ Generate New Ad
212
+ </Button>
213
+ </Link>
214
+ <Link href="/gallery">
215
+ <Button variant="secondary" size="lg" className="w-full group">
216
+ <Grid className="h-5 w-5 mr-2 group-hover:rotate-12 transition-transform duration-300" />
217
+ View Gallery
218
+ </Button>
219
+ </Link>
220
+ </div>
221
+ </CardContent>
222
+ </Card>
223
+
224
+ {/* Recent Ads */}
225
+ <Card variant="glass" className="animate-fade-in">
226
+ <CardHeader>
227
+ <div className="flex items-center justify-between">
228
+ <CardTitle>Recent Ads</CardTitle>
229
+ <Link href="/gallery">
230
+ <Button variant="ghost" size="sm">View All</Button>
231
+ </Link>
232
+ </div>
233
+ </CardHeader>
234
+ <CardContent>
235
+ {recentAds.length === 0 ? (
236
+ <div className="text-center py-16">
237
+ <div className="inline-flex items-center justify-center w-20 h-20 rounded-full bg-gradient-to-br from-blue-500 to-cyan-500 mb-4">
238
+ <Sparkles className="h-10 w-10 text-white" />
239
+ </div>
240
+ <p className="text-gray-500 text-lg mb-4">No ads generated yet</p>
241
+ <Link href="/generate">
242
+ <Button variant="primary">
243
+ Generate Your First Ad
244
+ </Button>
245
+ </Link>
246
+ </div>
247
+ ) : (
248
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
249
+ {recentAds.map((ad, index) => {
250
+ const { primary, fallback } = getImageUrlFallback(ad.image_url, ad.image_filename);
251
+ const initialSrc = primary || fallback;
252
+
253
+ return (
254
+ <DashboardAdCard
255
+ key={ad.id}
256
+ ad={ad}
257
+ initialImageSrc={initialSrc}
258
+ fallback={fallback}
259
+ index={index}
260
+ />
261
+ );
262
+ })}
263
+ </div>
264
+ )}
265
+ </CardContent>
266
+ </Card>
267
+ </div>
268
+ </div>
269
+ );
270
+ }
frontend/components/gallery/AdCard.tsx ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import React, { useState, memo } from "react";
4
+ import Link from "next/link";
5
+ import Image from "next/image";
6
+ import { Card, CardContent } from "@/components/ui/Card";
7
+ import { formatRelativeDate, formatNiche, getImageUrl, getImageUrlFallback, truncateText } from "@/lib/utils/formatters";
8
+ import type { AdCreativeDB } from "@/types/api";
9
+
10
+ interface AdCardProps {
11
+ ad: AdCreativeDB;
12
+ isSelected?: boolean;
13
+ onSelect?: (adId: string) => void;
14
+ }
15
+
16
+ export const AdCard: React.FC<AdCardProps> = memo(({
17
+ ad,
18
+ isSelected = false,
19
+ onSelect,
20
+ }) => {
21
+ const { primary, fallback } = getImageUrlFallback(ad.image_url, ad.image_filename);
22
+ const [imageSrc, setImageSrc] = useState<string | null>(primary || fallback);
23
+ const [imageError, setImageError] = useState(false);
24
+
25
+ const handleImageError = () => {
26
+ // Try fallback if primary failed
27
+ if (!imageError && fallback && imageSrc === primary) {
28
+ setImageSrc(fallback);
29
+ setImageError(true);
30
+ } else {
31
+ // Both failed, show placeholder
32
+ setImageSrc(null);
33
+ }
34
+ };
35
+
36
+ return (
37
+ <Card
38
+ variant={isSelected ? "glass" : "elevated"}
39
+ className={`cursor-pointer transition-all duration-300 group ${
40
+ isSelected ? "ring-4 ring-blue-500 ring-opacity-50 scale-105" : "hover:scale-105"
41
+ }`}
42
+ onClick={() => onSelect?.(ad.id)}
43
+ >
44
+ <Link href={`/gallery/${ad.id}`} onClick={(e) => e.stopPropagation()}>
45
+ <CardContent className="p-0">
46
+ {(imageSrc || ad.image_filename || ad.image_url) && (
47
+ <div className="aspect-video bg-gradient-to-br from-gray-100 to-gray-200 rounded-t-2xl overflow-hidden relative flex items-center justify-center">
48
+ {imageSrc ? (
49
+ <Image
50
+ src={imageSrc}
51
+ alt={ad.headline}
52
+ fill
53
+ className="object-contain group-hover:scale-105 transition-transform duration-500"
54
+ onError={handleImageError}
55
+ loading="lazy"
56
+ sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
57
+ unoptimized={imageSrc.startsWith('http://') || imageSrc.includes('r2.cloudflarestorage.com') || imageSrc.includes('replicate.delivery')}
58
+ />
59
+ ) : (
60
+ <div className="w-full h-full flex items-center justify-center">
61
+ <div className="text-center text-gray-400">
62
+ <svg className="w-12 h-12 mx-auto mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
63
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
64
+ </svg>
65
+ <p className="text-xs">Image unavailable</p>
66
+ </div>
67
+ </div>
68
+ )}
69
+ <div className="absolute inset-0 bg-gradient-to-t from-black/30 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
70
+ </div>
71
+ )}
72
+ <div className="p-5">
73
+ <div className="flex items-center justify-between mb-3">
74
+ <span className="text-xs font-bold px-3 py-1 bg-gradient-to-r from-blue-500 to-cyan-500 text-white rounded-full">
75
+ {formatNiche(ad.niche)}
76
+ </span>
77
+ <span className="text-xs text-gray-500 font-medium">
78
+ {formatRelativeDate(ad.created_at)}
79
+ </span>
80
+ </div>
81
+ <h3 className="font-bold text-gray-900 line-clamp-2 mb-2 group-hover:text-blue-600 transition-colors">
82
+ {truncateText(ad.headline, 60)}
83
+ </h3>
84
+ {ad.title && (
85
+ <p className="text-sm text-gray-600 line-clamp-1">
86
+ {ad.title}
87
+ </p>
88
+ )}
89
+ {ad.psychological_angle && (
90
+ <p className="text-xs text-gray-500 mt-2 line-clamp-1">
91
+ {truncateText(ad.psychological_angle, 50)}
92
+ </p>
93
+ )}
94
+ </div>
95
+ </CardContent>
96
+ </Link>
97
+ </Card>
98
+ );
99
+ }, (prevProps, nextProps) => {
100
+ // Only re-render if ad data or selection state changes
101
+ return (
102
+ prevProps.ad.id === nextProps.ad.id &&
103
+ prevProps.isSelected === nextProps.isSelected &&
104
+ prevProps.ad.image_url === nextProps.ad.image_url
105
+ );
106
+ });
107
+
108
+ AdCard.displayName = "AdCard";
frontend/components/gallery/FilterBar.tsx ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import React from "react";
4
+ import { Input } from "@/components/ui/Input";
5
+ import { Select } from "@/components/ui/Select";
6
+ import { Button } from "@/components/ui/Button";
7
+ import { Card } from "@/components/ui/Card";
8
+ import { X } from "lucide-react";
9
+ import type { AdFilters } from "@/types";
10
+
11
+ interface FilterBarProps {
12
+ filters: AdFilters;
13
+ onFiltersChange: (filters: Partial<AdFilters>) => void;
14
+ }
15
+
16
+ export const FilterBar: React.FC<FilterBarProps> = ({
17
+ filters,
18
+ onFiltersChange,
19
+ }) => {
20
+ const handleFilterChange = (key: keyof AdFilters, value: string | null) => {
21
+ onFiltersChange({ [key]: value || null });
22
+ };
23
+
24
+ const clearFilters = () => {
25
+ onFiltersChange({
26
+ niche: null,
27
+ generation_method: null,
28
+ search: null,
29
+ date_from: null,
30
+ date_to: null,
31
+ });
32
+ };
33
+
34
+ const hasActiveFilters = Object.values(filters).some((v) => v !== null && v !== undefined);
35
+
36
+ return (
37
+ <Card variant="glass">
38
+ <div className="p-6 space-y-4">
39
+ <div className="flex items-center justify-between">
40
+ <h3 className="text-lg font-bold gradient-text">Filters</h3>
41
+ {hasActiveFilters && (
42
+ <Button variant="ghost" size="sm" onClick={clearFilters} className="group">
43
+ <X className="h-4 w-4 mr-1 group-hover:rotate-90 transition-transform duration-300" />
44
+ Clear
45
+ </Button>
46
+ )}
47
+ </div>
48
+
49
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
50
+ <Input
51
+ placeholder="Search by headline or title..."
52
+ value={filters.search || ""}
53
+ onChange={(e) => handleFilterChange("search", e.target.value)}
54
+ />
55
+
56
+ <Select
57
+ options={[
58
+ { value: "", label: "All Niches" },
59
+ { value: "home_insurance", label: "Home Insurance" },
60
+ { value: "glp1", label: "GLP-1" },
61
+ ]}
62
+ value={filters.niche || ""}
63
+ onChange={(e) => handleFilterChange("niche", e.target.value || null)}
64
+ />
65
+
66
+ <Select
67
+ options={[
68
+ { value: "", label: "All Methods" },
69
+ { value: "original", label: "Original" },
70
+ { value: "angle_concept_matrix", label: "Matrix" },
71
+ { value: "extensive", label: "Extensive" },
72
+ ]}
73
+ value={filters.generation_method || ""}
74
+ onChange={(e) => handleFilterChange("generation_method", e.target.value || null)}
75
+ />
76
+ </div>
77
+ </div>
78
+ </Card>
79
+ );
80
+ };
frontend/components/gallery/GalleryGrid.tsx ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import React from "react";
4
+ import { AdCard } from "./AdCard";
5
+ import { LoadingSpinner } from "@/components/ui/LoadingSpinner";
6
+ import type { AdCreativeDB } from "@/types/api";
7
+
8
+ interface GalleryGridProps {
9
+ ads: AdCreativeDB[];
10
+ selectedAds: string[];
11
+ onAdSelect: (adId: string) => void;
12
+ isLoading?: boolean;
13
+ }
14
+
15
+ export const GalleryGrid: React.FC<GalleryGridProps> = ({
16
+ ads,
17
+ selectedAds,
18
+ onAdSelect,
19
+ isLoading = false,
20
+ }) => {
21
+ if (isLoading) {
22
+ return (
23
+ <div className="flex items-center justify-center py-12">
24
+ <LoadingSpinner size="lg" />
25
+ </div>
26
+ );
27
+ }
28
+
29
+ if (ads.length === 0) {
30
+ return (
31
+ <div className="text-center py-12">
32
+ <p className="text-gray-500 mb-4">No ads found</p>
33
+ <p className="text-sm text-gray-400">Try adjusting your filters or generate a new ad</p>
34
+ </div>
35
+ );
36
+ }
37
+
38
+ return (
39
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
40
+ {ads.map((ad) => (
41
+ <AdCard
42
+ key={ad.id}
43
+ ad={ad}
44
+ isSelected={selectedAds.includes(ad.id)}
45
+ onSelect={onAdSelect}
46
+ />
47
+ ))}
48
+ </div>
49
+ );
50
+ };
frontend/components/generation/AdPreview.tsx ADDED
@@ -0,0 +1,357 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import React, { useState } from "react";
4
+ import { Button } from "@/components/ui/Button";
5
+ import { Download, Copy } from "lucide-react";
6
+ import { downloadImage, copyToClipboard } from "@/lib/utils/export";
7
+ import { getImageUrl, getImageUrlFallback } from "@/lib/utils/formatters";
8
+ import { toast } from "react-hot-toast";
9
+ import type { GenerateResponse, MatrixGenerateResponse } from "@/types/api";
10
+
11
+ interface AdPreviewProps {
12
+ ad: GenerateResponse | MatrixGenerateResponse;
13
+ }
14
+
15
+ export const AdPreview: React.FC<AdPreviewProps> = ({ ad }) => {
16
+ const [imageErrors, setImageErrors] = useState<Record<number, boolean>>({});
17
+
18
+ const handleDownloadImage = async (imageUrl: string | null | undefined, filename: string | null | undefined) => {
19
+ if (!imageUrl && !filename) {
20
+ toast.error("No image URL available");
21
+ return;
22
+ }
23
+
24
+ try {
25
+ const url = getImageUrl(imageUrl, filename);
26
+ if (url) {
27
+ await downloadImage(url, filename || `ad-${ad.id}.png`);
28
+ toast.success("Image downloaded");
29
+ }
30
+ } catch (error) {
31
+ toast.error("Failed to download image");
32
+ }
33
+ };
34
+
35
+ const handleCopyText = async (text: string, label: string) => {
36
+ try {
37
+ await copyToClipboard(text);
38
+ toast.success(`${label} copied`);
39
+ } catch (error) {
40
+ toast.error("Failed to copy");
41
+ }
42
+ };
43
+
44
+ const handleImageError = (index: number, image: { image_url?: string | null; filename?: string | null }) => {
45
+ if (!imageErrors[index]) {
46
+ const { fallback } = getImageUrlFallback(image.image_url, image.filename);
47
+ if (fallback) {
48
+ setImageErrors((prev) => ({ ...prev, [index]: true }));
49
+ }
50
+ }
51
+ };
52
+
53
+ return (
54
+ <div className="space-y-6">
55
+ {/* Images */}
56
+ {ad.images && ad.images.length > 0 && (
57
+ <div className="space-y-4">
58
+ {ad.images.length === 1 ? (
59
+ // Single image - full width with better styling
60
+ <div className="bg-white rounded-2xl shadow-lg shadow-blue-100/50 overflow-hidden ring-1 ring-blue-100">
61
+ {(() => {
62
+ const image = ad.images[0];
63
+ const { primary, fallback } = getImageUrlFallback(image.image_url, image.filename);
64
+ const imageUrl = imageErrors[0] ? fallback : (primary || fallback);
65
+
66
+ return imageUrl ? (
67
+ <div className="relative group">
68
+ <img
69
+ src={imageUrl}
70
+ alt={ad.headline || "Ad image"}
71
+ className="w-full h-auto"
72
+ onError={() => handleImageError(0, image)}
73
+ />
74
+ <div className="absolute top-4 right-4 opacity-0 group-hover:opacity-100 transition-opacity">
75
+ <div className="relative group/btn">
76
+ <Button
77
+ variant="primary"
78
+ size="sm"
79
+ onClick={() => handleDownloadImage(image.image_url, image.filename)}
80
+ className="shadow-lg"
81
+ >
82
+ <Download className="h-4 w-4" />
83
+ </Button>
84
+ <span className="absolute -bottom-8 left-1/2 -translate-x-1/2 px-2 py-1 bg-gray-800 text-white text-xs rounded opacity-0 group-hover/btn:opacity-100 transition-opacity whitespace-nowrap pointer-events-none z-10">
85
+ Download Image
86
+ </span>
87
+ </div>
88
+ </div>
89
+ </div>
90
+ ) : (
91
+ <div className="bg-gradient-to-br from-blue-50 to-cyan-50 rounded-2xl aspect-square flex items-center justify-center ring-1 ring-blue-100">
92
+ <div className="text-blue-300 text-center">
93
+ <svg className="w-16 h-16 mx-auto mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
94
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
95
+ </svg>
96
+ <p className="text-sm">No image</p>
97
+ </div>
98
+ </div>
99
+ );
100
+ })()}
101
+ {ad.images[0].error && (
102
+ <div className="p-4 bg-red-50 border-t border-red-200">
103
+ <p className="text-sm text-red-800">Error: {ad.images[0].error}</p>
104
+ </div>
105
+ )}
106
+ </div>
107
+ ) : (
108
+ // Multiple images - grid layout
109
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
110
+ {ad.images.map((image, index) => {
111
+ const { primary, fallback } = getImageUrlFallback(image.image_url, image.filename);
112
+ const imageUrl = imageErrors[index] ? fallback : (primary || fallback);
113
+
114
+ return (
115
+ <div key={index} className="bg-white rounded-2xl shadow-lg shadow-blue-100/50 overflow-hidden ring-1 ring-blue-100">
116
+ {imageUrl ? (
117
+ <div className="relative group">
118
+ <img
119
+ src={imageUrl}
120
+ alt={`Ad image ${index + 1}`}
121
+ className="w-full h-auto"
122
+ onError={() => handleImageError(index, image)}
123
+ />
124
+ <div className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity">
125
+ <div className="relative group/btn">
126
+ <Button
127
+ variant="primary"
128
+ size="sm"
129
+ onClick={() => handleDownloadImage(image.image_url, image.filename)}
130
+ className="shadow-lg"
131
+ >
132
+ <Download className="h-4 w-4" />
133
+ </Button>
134
+ <span className="absolute -bottom-8 left-1/2 -translate-x-1/2 px-2 py-1 bg-gray-800 text-white text-xs rounded opacity-0 group-hover/btn:opacity-100 transition-opacity whitespace-nowrap pointer-events-none z-10">
135
+ Download
136
+ </span>
137
+ </div>
138
+ </div>
139
+ </div>
140
+ ) : (
141
+ <div className="bg-gradient-to-br from-blue-50 to-cyan-50 aspect-square flex items-center justify-center">
142
+ <div className="text-blue-300 text-center">
143
+ <svg className="w-12 h-12 mx-auto mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
144
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
145
+ </svg>
146
+ <p className="text-xs">No image</p>
147
+ </div>
148
+ </div>
149
+ )}
150
+ {image.error && (
151
+ <div className="p-3 bg-red-50 border-t border-red-200">
152
+ <p className="text-xs text-red-800">Error: {image.error}</p>
153
+ </div>
154
+ )}
155
+ </div>
156
+ );
157
+ })}
158
+ </div>
159
+ )}
160
+ </div>
161
+ )}
162
+
163
+ {/* Ad Copy Section */}
164
+ <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
165
+ {/* Left Column - Main Copy */}
166
+ <div className="space-y-5">
167
+ {/* Title */}
168
+ {ad.title && (
169
+ <div className="bg-white rounded-2xl shadow-md p-6 border-l-4 border-indigo-500">
170
+ <div className="flex items-start justify-between gap-4 mb-3">
171
+ <h3 className="text-xs font-bold text-indigo-600 uppercase tracking-wider">Title</h3>
172
+ <div className="relative group">
173
+ <Button
174
+ variant="ghost"
175
+ size="sm"
176
+ onClick={() => handleCopyText(ad.title!, "Title")}
177
+ className="text-indigo-500 hover:bg-indigo-50"
178
+ >
179
+ <Copy className="h-4 w-4" />
180
+ </Button>
181
+ <span className="absolute -bottom-8 left-1/2 -translate-x-1/2 px-2 py-1 bg-indigo-600 text-white text-xs rounded opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap pointer-events-none z-10">
182
+ Copy Title
183
+ </span>
184
+ </div>
185
+ </div>
186
+ <p className="text-lg font-semibold text-gray-800">{ad.title}</p>
187
+ </div>
188
+ )}
189
+
190
+ {/* Headline */}
191
+ <div className="bg-white rounded-2xl shadow-lg shadow-blue-100/30 p-6 border-l-4 border-blue-500">
192
+ <div className="flex items-start justify-between gap-4 mb-3">
193
+ <h3 className="text-xs font-bold text-blue-600 uppercase tracking-wider">Headline</h3>
194
+ <div className="relative group">
195
+ <Button
196
+ variant="ghost"
197
+ size="sm"
198
+ onClick={() => handleCopyText(ad.headline, "Headline")}
199
+ className="text-blue-500 hover:bg-blue-50"
200
+ >
201
+ <Copy className="h-4 w-4" />
202
+ </Button>
203
+ <span className="absolute -bottom-8 left-1/2 -translate-x-1/2 px-2 py-1 bg-blue-600 text-white text-xs rounded opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap pointer-events-none z-10">
204
+ Copy Headline
205
+ </span>
206
+ </div>
207
+ </div>
208
+ <h1 className="text-2xl font-bold bg-gradient-to-r from-gray-900 to-gray-700 bg-clip-text text-transparent leading-tight">
209
+ {ad.headline}
210
+ </h1>
211
+ </div>
212
+
213
+ {/* Primary Text */}
214
+ {ad.primary_text && (
215
+ <div className="bg-white rounded-2xl shadow-md p-6 border-l-4 border-cyan-500">
216
+ <div className="flex items-start justify-between gap-4 mb-3">
217
+ <h3 className="text-xs font-bold text-cyan-600 uppercase tracking-wider">Primary Text</h3>
218
+ <div className="relative group">
219
+ <Button
220
+ variant="ghost"
221
+ size="sm"
222
+ onClick={() => handleCopyText(ad.primary_text!, "Primary Text")}
223
+ className="text-cyan-500 hover:bg-cyan-50"
224
+ >
225
+ <Copy className="h-4 w-4" />
226
+ </Button>
227
+ <span className="absolute -bottom-8 left-1/2 -translate-x-1/2 px-2 py-1 bg-cyan-600 text-white text-xs rounded opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap pointer-events-none z-10">
228
+ Copy Text
229
+ </span>
230
+ </div>
231
+ </div>
232
+ <p className="text-gray-700 whitespace-pre-line leading-relaxed">{ad.primary_text}</p>
233
+ </div>
234
+ )}
235
+
236
+ {/* Description */}
237
+ {ad.description && (
238
+ <div className="bg-white rounded-2xl shadow-md p-6 border-l-4 border-violet-500">
239
+ <div className="flex items-start justify-between gap-4 mb-3">
240
+ <h3 className="text-xs font-bold text-violet-600 uppercase tracking-wider">Description</h3>
241
+ <div className="relative group">
242
+ <Button
243
+ variant="ghost"
244
+ size="sm"
245
+ onClick={() => handleCopyText(ad.description!, "Description")}
246
+ className="text-violet-500 hover:bg-violet-50"
247
+ >
248
+ <Copy className="h-4 w-4" />
249
+ </Button>
250
+ <span className="absolute -bottom-8 left-1/2 -translate-x-1/2 px-2 py-1 bg-violet-600 text-white text-xs rounded opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap pointer-events-none z-10">
251
+ Copy Description
252
+ </span>
253
+ </div>
254
+ </div>
255
+ <p className="text-gray-700 leading-relaxed">{ad.description}</p>
256
+ </div>
257
+ )}
258
+ </div>
259
+
260
+ {/* Right Column - Additional Info */}
261
+ <div className="space-y-5">
262
+ {/* Body Story */}
263
+ {ad.body_story && (
264
+ <div className="bg-white rounded-2xl shadow-md p-6 border-l-4 border-amber-500">
265
+ <div className="flex items-start justify-between gap-4 mb-3">
266
+ <h3 className="text-xs font-bold text-amber-600 uppercase tracking-wider">Body Story</h3>
267
+ <div className="relative group">
268
+ <Button
269
+ variant="ghost"
270
+ size="sm"
271
+ onClick={() => handleCopyText(ad.body_story!, "Body Story")}
272
+ className="text-amber-500 hover:bg-amber-50"
273
+ >
274
+ <Copy className="h-4 w-4" />
275
+ </Button>
276
+ <span className="absolute -bottom-8 left-1/2 -translate-x-1/2 px-2 py-1 bg-amber-600 text-white text-xs rounded opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap pointer-events-none z-10">
277
+ Copy Story
278
+ </span>
279
+ </div>
280
+ </div>
281
+ <p className="text-gray-700 whitespace-pre-line leading-relaxed">{ad.body_story}</p>
282
+ </div>
283
+ )}
284
+
285
+ {/* CTA */}
286
+ {ad.cta && (
287
+ <div className="bg-gradient-to-r from-emerald-50 to-teal-50 rounded-2xl shadow-md p-6 border border-emerald-200">
288
+ <div className="flex items-start justify-between gap-4 mb-3">
289
+ <h3 className="text-xs font-bold text-emerald-600 uppercase tracking-wider">Call to Action</h3>
290
+ <div className="relative group">
291
+ <Button
292
+ variant="ghost"
293
+ size="sm"
294
+ onClick={() => handleCopyText(ad.cta!, "CTA")}
295
+ className="text-emerald-600 hover:bg-emerald-100"
296
+ >
297
+ <Copy className="h-4 w-4" />
298
+ </Button>
299
+ <span className="absolute -bottom-8 left-1/2 -translate-x-1/2 px-2 py-1 bg-emerald-600 text-white text-xs rounded opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap pointer-events-none z-10">
300
+ Copy CTA
301
+ </span>
302
+ </div>
303
+ </div>
304
+ <p className="text-xl font-bold bg-gradient-to-r from-emerald-600 to-teal-600 bg-clip-text text-transparent">{ad.cta}</p>
305
+ </div>
306
+ )}
307
+
308
+ {/* Psychological Angle */}
309
+ <div className="bg-gradient-to-br from-pink-50 via-purple-50 to-blue-50 rounded-2xl p-6 border border-purple-200 shadow-md">
310
+ <div className="flex items-start justify-between gap-4 mb-3">
311
+ <h3 className="text-xs font-bold text-purple-600 uppercase tracking-wider">🧠 Psychological Angle</h3>
312
+ <div className="relative group">
313
+ <Button
314
+ variant="ghost"
315
+ size="sm"
316
+ onClick={() => handleCopyText(ad.psychological_angle, "Angle")}
317
+ className="text-purple-500 hover:bg-purple-100"
318
+ >
319
+ <Copy className="h-4 w-4" />
320
+ </Button>
321
+ <span className="absolute -bottom-8 left-1/2 -translate-x-1/2 px-2 py-1 bg-purple-600 text-white text-xs rounded opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap pointer-events-none z-10">
322
+ Copy Angle
323
+ </span>
324
+ </div>
325
+ </div>
326
+ <p className="text-gray-700 leading-relaxed">{ad.psychological_angle}</p>
327
+ {ad.why_it_works && (
328
+ <div className="mt-4 pt-4 border-t border-purple-200">
329
+ <p className="text-xs font-bold text-purple-600 uppercase tracking-wider mb-2">💡 Why It Works</p>
330
+ <p className="text-gray-600 text-sm leading-relaxed">{ad.why_it_works}</p>
331
+ </div>
332
+ )}
333
+ </div>
334
+
335
+ {/* Matrix Details */}
336
+ {"matrix" in ad && ad.matrix && (
337
+ <div className="bg-white rounded-2xl shadow-md p-6 border border-gray-200">
338
+ <h3 className="text-xs font-bold text-gray-600 uppercase tracking-wider mb-4">Matrix Details</h3>
339
+ <div className="space-y-4">
340
+ <div className="bg-gradient-to-br from-blue-50 to-cyan-50 rounded-xl p-4 border border-blue-100">
341
+ <p className="text-blue-500 text-xs font-medium mb-1">Angle</p>
342
+ <p className="font-semibold text-gray-800">{ad.matrix.angle.name}</p>
343
+ <p className="text-xs text-gray-500 mt-1">{ad.matrix.angle.trigger}</p>
344
+ </div>
345
+ <div className="bg-gradient-to-br from-violet-50 to-purple-50 rounded-xl p-4 border border-violet-100">
346
+ <p className="text-violet-500 text-xs font-medium mb-1">Concept</p>
347
+ <p className="font-semibold text-gray-800">{ad.matrix.concept.name}</p>
348
+ <p className="text-xs text-gray-500 mt-1">{ad.matrix.concept.structure}</p>
349
+ </div>
350
+ </div>
351
+ </div>
352
+ )}
353
+ </div>
354
+ </div>
355
+ </div>
356
+ );
357
+ };
frontend/components/generation/BatchForm.tsx ADDED
@@ -0,0 +1,139 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import React from "react";
4
+ import { useForm } from "react-hook-form";
5
+ import { zodResolver } from "@hookform/resolvers/zod";
6
+ import { generateBatchSchema } from "@/lib/utils/validators";
7
+ import { Input } from "@/components/ui/Input";
8
+ import { Select } from "@/components/ui/Select";
9
+ import { Button } from "@/components/ui/Button";
10
+ import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/Card";
11
+ import { IMAGE_MODELS } from "@/lib/constants/models";
12
+ import type { Niche } from "@/types/api";
13
+
14
+ interface BatchFormProps {
15
+ onSubmit: (data: { niche: Niche; count: number; images_per_ad: number; image_model?: string | null }) => Promise<void>;
16
+ isLoading: boolean;
17
+ }
18
+
19
+ export const BatchForm: React.FC<BatchFormProps> = ({
20
+ onSubmit,
21
+ isLoading,
22
+ }) => {
23
+ const {
24
+ register,
25
+ handleSubmit,
26
+ formState: { errors },
27
+ watch,
28
+ } = useForm({
29
+ resolver: zodResolver(generateBatchSchema),
30
+ defaultValues: {
31
+ niche: "home_insurance" as Niche,
32
+ count: 5,
33
+ images_per_ad: 1,
34
+ image_model: null,
35
+ },
36
+ });
37
+
38
+ const count = watch("count");
39
+ const imagesPerAd = watch("images_per_ad");
40
+
41
+ return (
42
+ <Card variant="glass">
43
+ <CardHeader>
44
+ <CardTitle>Batch Generation</CardTitle>
45
+ <CardDescription>
46
+ Generate multiple ads at once for testing and variety
47
+ </CardDescription>
48
+ </CardHeader>
49
+ <CardContent>
50
+ <form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
51
+ <Select
52
+ label="Niche"
53
+ options={[
54
+ { value: "home_insurance", label: "Home Insurance" },
55
+ { value: "glp1", label: "GLP-1" },
56
+ ]}
57
+ error={errors.niche?.message}
58
+ {...register("niche")}
59
+ />
60
+
61
+ <Select
62
+ label="Image Model"
63
+ options={IMAGE_MODELS.map(model => ({ value: model.value, label: model.label }))}
64
+ error={errors.image_model?.message}
65
+ {...register("image_model")}
66
+ />
67
+
68
+ <div>
69
+ <label className="block text-sm font-medium text-gray-700 mb-2">
70
+ Number of Ads: {count}
71
+ </label>
72
+ <input
73
+ type="range"
74
+ min="1"
75
+ max="20"
76
+ step="1"
77
+ className="w-full"
78
+ {...register("count", { valueAsNumber: true })}
79
+ />
80
+ <div className="flex justify-between text-xs text-gray-500 mt-1">
81
+ <span>1</span>
82
+ <span>20</span>
83
+ </div>
84
+ {errors.count && (
85
+ <p className="mt-1 text-sm text-red-600">
86
+ {errors.count.message}
87
+ </p>
88
+ )}
89
+ </div>
90
+
91
+ <div>
92
+ <label className="block text-sm font-semibold text-gray-700 mb-2">
93
+ Variations per Ad: <span className="text-blue-600 font-bold">{imagesPerAd}</span>
94
+ </label>
95
+ <input
96
+ type="range"
97
+ min="1"
98
+ max="3"
99
+ step="1"
100
+ className="w-full accent-blue-500"
101
+ {...register("images_per_ad", { valueAsNumber: true })}
102
+ />
103
+ <div className="flex justify-between text-xs text-gray-500 mt-1 font-medium">
104
+ <span>1</span>
105
+ <span>3</span>
106
+ </div>
107
+ <p className="text-xs text-gray-500 mt-1">
108
+ Each variation will have a unique image and slight copy variations
109
+ </p>
110
+ {errors.images_per_ad && (
111
+ <p className="mt-1 text-sm text-red-600">
112
+ {errors.images_per_ad.message}
113
+ </p>
114
+ )}
115
+ </div>
116
+
117
+ <div className="bg-gradient-to-r from-blue-50 to-cyan-50 border border-blue-200 rounded-xl p-4">
118
+ <p className="text-sm font-semibold text-gray-800">
119
+ <strong>Estimated:</strong> {count} ads × {imagesPerAd} variation(s) = {count * imagesPerAd} total variations
120
+ </p>
121
+ <p className="text-xs text-gray-600 mt-1">
122
+ This may take several minutes to complete
123
+ </p>
124
+ </div>
125
+
126
+ <Button
127
+ type="submit"
128
+ variant="primary"
129
+ size="lg"
130
+ isLoading={isLoading}
131
+ className="w-full"
132
+ >
133
+ Generate Batch
134
+ </Button>
135
+ </form>
136
+ </CardContent>
137
+ </Card>
138
+ );
139
+ };
frontend/components/generation/CorrectionModal.tsx ADDED
@@ -0,0 +1,352 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import React, { useState, useEffect } from "react";
4
+ import { X, Wand2, Image as ImageIcon, CheckCircle2, AlertCircle, Loader2, Sparkles } from "lucide-react";
5
+ import { correctImage } from "@/lib/api/endpoints";
6
+ import type { ImageCorrectResponse, AdCreativeDB } from "@/types/api";
7
+ import { ProgressBar } from "@/components/ui/ProgressBar";
8
+ import { Button } from "@/components/ui/Button";
9
+ import { Input } from "@/components/ui/Input";
10
+ import { Card, CardContent } from "@/components/ui/Card";
11
+
12
+ interface CorrectionModalProps {
13
+ isOpen: boolean;
14
+ onClose: () => void;
15
+ adId: string;
16
+ ad?: AdCreativeDB | null;
17
+ onSuccess?: (result: ImageCorrectResponse) => void;
18
+ }
19
+
20
+ type CorrectionStep = "idle" | "input" | "analyzing" | "correcting" | "regenerating" | "complete" | "error";
21
+
22
+ export const CorrectionModal: React.FC<CorrectionModalProps> = ({
23
+ isOpen,
24
+ onClose,
25
+ adId,
26
+ ad,
27
+ onSuccess,
28
+ }) => {
29
+ const [step, setStep] = useState<CorrectionStep>("idle");
30
+ const [progress, setProgress] = useState(0);
31
+ const [result, setResult] = useState<ImageCorrectResponse | null>(null);
32
+ const [error, setError] = useState<string | null>(null);
33
+ const [userInstructions, setUserInstructions] = useState("");
34
+ const [useAutoAnalyze, setUseAutoAnalyze] = useState(false);
35
+
36
+ useEffect(() => {
37
+ if (isOpen) {
38
+ setStep("input");
39
+ setProgress(0);
40
+ setResult(null);
41
+ setError(null);
42
+ setUserInstructions("");
43
+ setUseAutoAnalyze(false);
44
+ } else {
45
+ // Reset state when modal closes
46
+ setStep("idle");
47
+ setProgress(0);
48
+ setResult(null);
49
+ setError(null);
50
+ setUserInstructions("");
51
+ setUseAutoAnalyze(false);
52
+ }
53
+ }, [isOpen]);
54
+
55
+ const handleCorrection = async () => {
56
+ if (!userInstructions && !useAutoAnalyze) {
57
+ setError("Please specify what you want to correct or enable auto-analysis");
58
+ return;
59
+ }
60
+
61
+ setStep("analyzing");
62
+ setProgress(0);
63
+ setError(null);
64
+ setResult(null);
65
+
66
+ try {
67
+ // Simulate progress updates
68
+ const progressInterval = setInterval(() => {
69
+ setProgress((prev) => {
70
+ if (prev < 90) {
71
+ return prev + 5;
72
+ }
73
+ return prev;
74
+ });
75
+ }, 500);
76
+
77
+ setStep("analyzing");
78
+ await new Promise((resolve) => setTimeout(resolve, 1000));
79
+
80
+ setStep("correcting");
81
+ await new Promise((resolve) => setTimeout(resolve, 1000));
82
+
83
+ setStep("regenerating");
84
+ await new Promise((resolve) => setTimeout(resolve, 2000));
85
+
86
+ // Actually perform the correction
87
+ const response = await correctImage({
88
+ image_id: adId,
89
+ user_instructions: userInstructions || undefined,
90
+ auto_analyze: useAutoAnalyze,
91
+ });
92
+
93
+ clearInterval(progressInterval);
94
+ setProgress(100);
95
+
96
+ if (response.status === "success") {
97
+ setStep("complete");
98
+ setResult(response);
99
+ onSuccess?.(response);
100
+ } else {
101
+ setStep("error");
102
+ setError(response.error || "Correction failed");
103
+ }
104
+ } catch (err: any) {
105
+ setStep("error");
106
+ setError(err.response?.data?.detail || err.message || "Failed to correct image");
107
+ setProgress(0);
108
+ }
109
+ };
110
+
111
+ const getStepLabel = () => {
112
+ switch (step) {
113
+ case "input":
114
+ return "Specify Corrections";
115
+ case "analyzing":
116
+ return "Analyzing image...";
117
+ case "correcting":
118
+ return "Generating corrections...";
119
+ case "regenerating":
120
+ return "Regenerating with nano-banana-pro...";
121
+ case "complete":
122
+ return "Correction complete!";
123
+ case "error":
124
+ return "Error occurred";
125
+ default:
126
+ return "Starting correction...";
127
+ }
128
+ };
129
+
130
+ const getStepIcon = () => {
131
+ switch (step) {
132
+ case "complete":
133
+ return <CheckCircle2 className="h-6 w-6 text-green-500" />;
134
+ case "error":
135
+ return <AlertCircle className="h-6 w-6 text-red-500" />;
136
+ default:
137
+ return <Loader2 className="h-6 w-6 text-blue-500 animate-spin" />;
138
+ }
139
+ };
140
+
141
+ if (!isOpen) return null;
142
+
143
+ return (
144
+ <div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm">
145
+ <div className="bg-white rounded-2xl shadow-2xl max-w-2xl w-full max-h-[90vh] overflow-y-auto">
146
+ {/* Header */}
147
+ <div className="sticky top-0 bg-white border-b border-gray-200 px-6 py-4 rounded-t-2xl z-10">
148
+ <div className="flex items-center justify-between">
149
+ <div className="flex items-center gap-3">
150
+ <div className="p-2 bg-gradient-to-r from-blue-500 to-cyan-500 rounded-lg">
151
+ <Wand2 className="h-5 w-5 text-white" />
152
+ </div>
153
+ <div>
154
+ <h2 className="text-xl font-bold text-gray-900">Correct Image</h2>
155
+ <p className="text-sm text-gray-500">
156
+ {step === "input"
157
+ ? "Specify what you want to correct"
158
+ : "Analyzing and correcting your ad creative"}
159
+ </p>
160
+ </div>
161
+ </div>
162
+ <button
163
+ onClick={onClose}
164
+ className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
165
+ >
166
+ <X className="h-5 w-5 text-gray-500" />
167
+ </button>
168
+ </div>
169
+ </div>
170
+
171
+ {/* Content */}
172
+ <div className="p-6 space-y-6">
173
+ {/* Input Step */}
174
+ {step === "input" && (
175
+ <div className="space-y-4">
176
+ <Card variant="glass">
177
+ <CardContent className="pt-6">
178
+ <div className="space-y-4">
179
+ <div>
180
+ <label className="block text-sm font-semibold text-gray-700 mb-2">
181
+ What would you like to correct?
182
+ </label>
183
+ <Input
184
+ type="text"
185
+ placeholder="e.g., 'Fix spelling: change Save 50% to Save 60%' or 'Adjust colors to be brighter' or 'Change headline text to X'"
186
+ value={userInstructions}
187
+ onChange={(e) => setUserInstructions(e.target.value)}
188
+ className="w-full"
189
+ />
190
+ <p className="text-xs text-gray-500 mt-2">
191
+ Be specific about what you want to change. Only the specified changes will be made.
192
+ </p>
193
+ </div>
194
+
195
+ <div className="flex items-center gap-3 pt-2 border-t border-gray-200">
196
+ <input
197
+ type="checkbox"
198
+ id="auto-analyze"
199
+ checked={useAutoAnalyze}
200
+ onChange={(e) => setUseAutoAnalyze(e.target.checked)}
201
+ className="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
202
+ />
203
+ <label htmlFor="auto-analyze" className="text-sm text-gray-700 cursor-pointer">
204
+ Or let AI automatically analyze and suggest corrections
205
+ </label>
206
+ </div>
207
+
208
+ {useAutoAnalyze && (
209
+ <div className="bg-blue-50 border border-blue-200 rounded-lg p-3 text-sm text-blue-800">
210
+ <p className="font-semibold mb-1">Auto-Analysis Mode</p>
211
+ <p>AI will analyze the image for spelling mistakes and visual issues, then suggest corrections.</p>
212
+ </div>
213
+ )}
214
+
215
+ {userInstructions && (
216
+ <div className="bg-green-50 border border-green-200 rounded-lg p-3 text-sm text-green-800">
217
+ <p className="font-semibold mb-1">Custom Correction Mode</p>
218
+ <p>Only the changes you specified will be made. The rest of the image will be preserved.</p>
219
+ </div>
220
+ )}
221
+ </div>
222
+ </CardContent>
223
+ </Card>
224
+ </div>
225
+ )}
226
+
227
+ {/* Progress Section */}
228
+ {step !== "input" && step !== "complete" && step !== "error" && (
229
+ <div className="space-y-4">
230
+ <div className="flex items-center gap-4">
231
+ {getStepIcon()}
232
+ <div className="flex-1">
233
+ <p className="font-semibold text-gray-900">{getStepLabel()}</p>
234
+ <ProgressBar progress={progress} showPercentage={true} className="mt-2" />
235
+ </div>
236
+ </div>
237
+ </div>
238
+ )}
239
+
240
+ {/* Error State */}
241
+ {step === "error" && (
242
+ <div className="bg-red-50 border border-red-200 rounded-xl p-4">
243
+ <div className="flex items-start gap-3">
244
+ <AlertCircle className="h-5 w-5 text-red-500 mt-0.5" />
245
+ <div className="flex-1">
246
+ <h3 className="font-semibold text-red-900 mb-1">Correction Failed</h3>
247
+ <p className="text-sm text-red-700">{error}</p>
248
+ </div>
249
+ </div>
250
+ </div>
251
+ )}
252
+
253
+ {/* Success State */}
254
+ {step === "complete" && result && (
255
+ <div className="space-y-4">
256
+ <div className="bg-green-50 border border-green-200 rounded-xl p-4">
257
+ <div className="flex items-center gap-3">
258
+ <CheckCircle2 className="h-5 w-5 text-green-500" />
259
+ <div>
260
+ <h3 className="font-semibold text-green-900">Correction Complete!</h3>
261
+ <p className="text-sm text-green-700">Your image has been corrected successfully</p>
262
+ </div>
263
+ </div>
264
+ </div>
265
+
266
+ {result.corrected_image?.image_url && (
267
+ <div className="space-y-3">
268
+ <h3 className="font-semibold text-gray-900">Corrected Image</h3>
269
+ <img
270
+ src={result.corrected_image.image_url}
271
+ alt="Corrected"
272
+ className="w-full rounded-lg border border-gray-200"
273
+ />
274
+ </div>
275
+ )}
276
+
277
+ {result.corrections && (
278
+ <div className="space-y-3">
279
+ {result.corrections.spelling_corrections.length > 0 && (
280
+ <div>
281
+ <h3 className="font-semibold text-gray-900 mb-2">Spelling Corrections</h3>
282
+ <div className="space-y-2">
283
+ {result.corrections.spelling_corrections.map((correction, idx) => (
284
+ <div
285
+ key={idx}
286
+ className="bg-yellow-50 border border-yellow-200 rounded-lg p-3 text-sm"
287
+ >
288
+ <span className="font-medium text-red-600 line-through">
289
+ {correction.detected}
290
+ </span>{" "}
291
+ →{" "}
292
+ <span className="font-medium text-green-600">
293
+ {correction.corrected}
294
+ </span>
295
+ </div>
296
+ ))}
297
+ </div>
298
+ </div>
299
+ )}
300
+
301
+ {result.corrections.visual_corrections.length > 0 && (
302
+ <div>
303
+ <h3 className="font-semibold text-gray-900 mb-2">Visual Improvements</h3>
304
+ <div className="space-y-2">
305
+ {result.corrections.visual_corrections.map((correction, idx) => (
306
+ <div
307
+ key={idx}
308
+ className="bg-blue-50 border border-blue-200 rounded-lg p-3 text-sm"
309
+ >
310
+ <p className="font-medium text-gray-900">{correction.issue}</p>
311
+ <p className="text-gray-600 mt-1">{correction.suggestion}</p>
312
+ </div>
313
+ ))}
314
+ </div>
315
+ </div>
316
+ )}
317
+ </div>
318
+ )}
319
+ </div>
320
+ )}
321
+ </div>
322
+
323
+ {/* Footer */}
324
+ <div className="sticky bottom-0 bg-gray-50 border-t border-gray-200 px-6 py-4 rounded-b-2xl">
325
+ <div className="flex justify-end gap-3">
326
+ {step === "input" && (
327
+ <Button
328
+ onClick={handleCorrection}
329
+ variant="primary"
330
+ disabled={!userInstructions && !useAutoAnalyze}
331
+ >
332
+ <Sparkles className="h-4 w-4 mr-2" />
333
+ Start Correction
334
+ </Button>
335
+ )}
336
+ {step === "error" && (
337
+ <Button onClick={() => setStep("input")} variant="primary">
338
+ Try Again
339
+ </Button>
340
+ )}
341
+ <Button
342
+ onClick={onClose}
343
+ variant={step === "complete" ? "primary" : "secondary"}
344
+ >
345
+ {step === "complete" ? "Done" : "Close"}
346
+ </Button>
347
+ </div>
348
+ </div>
349
+ </div>
350
+ </div>
351
+ );
352
+ };
frontend/components/generation/ExtensiveForm.tsx ADDED
@@ -0,0 +1,166 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import React from "react";
4
+ import { useForm } from "react-hook-form";
5
+ import { zodResolver } from "@hookform/resolvers/zod";
6
+ import { z } from "zod";
7
+ import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/Card";
8
+ import { Select } from "@/components/ui/Select";
9
+ import { IMAGE_MODELS } from "@/lib/constants/models";
10
+ import type { Niche } from "@/types/api";
11
+
12
+ const extensiveSchema = z.object({
13
+ niche: z.enum(["home_insurance", "glp1"]),
14
+ target_audience: z.string().min(1, "Target audience is required"),
15
+ offer: z.string().min(1, "Offer is required"),
16
+ num_images: z.number().min(1).max(3),
17
+ num_strategies: z.number().min(1).max(10),
18
+ image_model: z.string().nullable().optional(),
19
+ });
20
+
21
+ type ExtensiveFormData = z.infer<typeof extensiveSchema>;
22
+
23
+ interface ExtensiveFormProps {
24
+ onSubmit: (data: {
25
+ niche: Niche;
26
+ target_audience: string;
27
+ offer: string;
28
+ num_images: number;
29
+ num_strategies: number;
30
+ image_model?: string | null;
31
+ }) => Promise<void>;
32
+ isLoading: boolean;
33
+ }
34
+
35
+ export const ExtensiveForm: React.FC<ExtensiveFormProps> = ({
36
+ onSubmit,
37
+ isLoading,
38
+ }) => {
39
+ const {
40
+ register,
41
+ handleSubmit,
42
+ formState: { errors },
43
+ watch,
44
+ } = useForm<ExtensiveFormData>({
45
+ resolver: zodResolver(extensiveSchema),
46
+ defaultValues: {
47
+ niche: "home_insurance" as Niche,
48
+ target_audience: "",
49
+ offer: "",
50
+ num_images: 1,
51
+ num_strategies: 5,
52
+ image_model: null,
53
+ },
54
+ });
55
+
56
+ const numImages = watch("num_images");
57
+ const numStrategies = watch("num_strategies");
58
+
59
+ return (
60
+ <Card variant="glass">
61
+ <CardHeader>
62
+ <CardTitle>Extensive Generation</CardTitle>
63
+ <CardDescription>
64
+ Researcher → Creative Director → Designer → Copywriter flow
65
+ </CardDescription>
66
+ </CardHeader>
67
+ <CardContent>
68
+ <form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
69
+ <Select
70
+ label="Niche"
71
+ options={[
72
+ { value: "home_insurance", label: "Home Insurance" },
73
+ { value: "glp1", label: "GLP-1" },
74
+ ]}
75
+ error={errors.niche?.message}
76
+ {...register("niche")}
77
+ />
78
+
79
+ <div>
80
+ <label className="block text-sm font-semibold text-gray-700 mb-2">
81
+ Target Audience <span className="text-red-500">*</span>
82
+ </label>
83
+ <input
84
+ type="text"
85
+ className="w-full px-4 py-3 rounded-xl border-2 border-gray-300 bg-white/80 backdrop-blur-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all duration-250"
86
+ placeholder="e.g., US people over 50+ age"
87
+ {...register("target_audience")}
88
+ />
89
+ {errors.target_audience && (
90
+ <p className="text-red-500 text-xs mt-1">{errors.target_audience.message}</p>
91
+ )}
92
+ </div>
93
+
94
+ <div>
95
+ <label className="block text-sm font-semibold text-gray-700 mb-2">
96
+ Offer <span className="text-red-500">*</span>
97
+ </label>
98
+ <input
99
+ type="text"
100
+ className="w-full px-4 py-3 rounded-xl border-2 border-gray-300 bg-white/80 backdrop-blur-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all duration-250"
101
+ placeholder="e.g., Don't overpay your insurance"
102
+ {...register("offer")}
103
+ />
104
+ {errors.offer && (
105
+ <p className="text-red-500 text-xs mt-1">{errors.offer.message}</p>
106
+ )}
107
+ </div>
108
+
109
+ <Select
110
+ label="Image Model"
111
+ options={IMAGE_MODELS.map(model => ({ value: model.value, label: model.label }))}
112
+ error={errors.image_model?.message}
113
+ {...register("image_model")}
114
+ />
115
+
116
+ <div>
117
+ <label className="block text-sm font-semibold text-gray-700 mb-2">
118
+ Images per Strategy: <span className="text-blue-600 font-bold">{numImages}</span>
119
+ </label>
120
+ <input
121
+ type="range"
122
+ min="1"
123
+ max="3"
124
+ step="1"
125
+ className="w-full accent-blue-500"
126
+ {...register("num_images", { valueAsNumber: true })}
127
+ />
128
+ <div className="flex justify-between text-xs text-gray-500 mt-1 font-medium">
129
+ <span>1</span>
130
+ <span>3</span>
131
+ </div>
132
+ </div>
133
+
134
+ <div>
135
+ <label className="block text-sm font-semibold text-gray-700 mb-2">
136
+ Number of Strategies: <span className="text-blue-600 font-bold">{numStrategies}</span>
137
+ </label>
138
+ <input
139
+ type="range"
140
+ min="1"
141
+ max="10"
142
+ step="1"
143
+ className="w-full accent-blue-500"
144
+ {...register("num_strategies", { valueAsNumber: true })}
145
+ />
146
+ <div className="flex justify-between text-xs text-gray-500 mt-1 font-medium">
147
+ <span>1</span>
148
+ <span>10</span>
149
+ </div>
150
+ <p className="text-xs text-gray-500 mt-1">
151
+ More strategies = more variety, but longer generation time
152
+ </p>
153
+ </div>
154
+
155
+ <button
156
+ type="submit"
157
+ disabled={isLoading}
158
+ className="w-full bg-gradient-to-r from-blue-500 to-cyan-500 text-white font-bold py-4 px-6 rounded-xl hover:from-blue-600 hover:to-cyan-600 transition-all duration-300 shadow-lg hover:shadow-xl disabled:opacity-50 disabled:cursor-not-allowed"
159
+ >
160
+ {isLoading ? "Generating..." : "Generate with Extensive"}
161
+ </button>
162
+ </form>
163
+ </CardContent>
164
+ </Card>
165
+ );
166
+ };
frontend/components/generation/GenerationForm.tsx ADDED
@@ -0,0 +1,105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import React from "react";
4
+ import { useForm } from "react-hook-form";
5
+ import { zodResolver } from "@hookform/resolvers/zod";
6
+ import { generateAdSchema } from "@/lib/utils/validators";
7
+ import { Input } from "@/components/ui/Input";
8
+ import { Select } from "@/components/ui/Select";
9
+ import { Button } from "@/components/ui/Button";
10
+ import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/Card";
11
+ import { IMAGE_MODELS } from "@/lib/constants/models";
12
+ import type { Niche } from "@/types/api";
13
+
14
+ interface GenerationFormProps {
15
+ onSubmit: (data: { niche: Niche; num_images: number; image_model?: string | null }) => Promise<void>;
16
+ isLoading: boolean;
17
+ }
18
+
19
+ export const GenerationForm: React.FC<GenerationFormProps> = ({
20
+ onSubmit,
21
+ isLoading,
22
+ }) => {
23
+ const {
24
+ register,
25
+ handleSubmit,
26
+ formState: { errors },
27
+ watch,
28
+ } = useForm({
29
+ resolver: zodResolver(generateAdSchema),
30
+ defaultValues: {
31
+ niche: "home_insurance" as Niche,
32
+ num_images: 1,
33
+ image_model: null,
34
+ },
35
+ });
36
+
37
+ const numImages = watch("num_images");
38
+
39
+ return (
40
+ <Card variant="glass">
41
+ <CardHeader>
42
+ <CardTitle>Generate Ad</CardTitle>
43
+ <CardDescription>
44
+ Create a new ad creative using randomized strategies
45
+ </CardDescription>
46
+ </CardHeader>
47
+ <CardContent>
48
+ <form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
49
+ <Select
50
+ label="Niche"
51
+ options={[
52
+ { value: "home_insurance", label: "Home Insurance" },
53
+ { value: "glp1", label: "GLP-1" },
54
+ ]}
55
+ error={errors.niche?.message}
56
+ {...register("niche")}
57
+ />
58
+
59
+ <Select
60
+ label="Image Model"
61
+ options={IMAGE_MODELS.map(model => ({ value: model.value, label: model.label }))}
62
+ error={errors.image_model?.message}
63
+ {...register("image_model")}
64
+ />
65
+
66
+ <div>
67
+ <label className="block text-sm font-semibold text-gray-700 mb-2">
68
+ Number of Variations: <span className="text-blue-600 font-bold">{numImages}</span>
69
+ </label>
70
+ <input
71
+ type="range"
72
+ min="1"
73
+ max="10"
74
+ step="1"
75
+ className="w-full accent-blue-500"
76
+ {...register("num_images", { valueAsNumber: true })}
77
+ />
78
+ <div className="flex justify-between text-xs text-gray-500 mt-1 font-medium">
79
+ <span>1</span>
80
+ <span>10</span>
81
+ </div>
82
+ <p className="text-xs text-gray-500 mt-1">
83
+ Each variation will have a unique image and slight copy variations
84
+ </p>
85
+ {errors.num_images && (
86
+ <p className="mt-1 text-sm text-red-600">
87
+ {errors.num_images.message}
88
+ </p>
89
+ )}
90
+ </div>
91
+
92
+ <Button
93
+ type="submit"
94
+ variant="primary"
95
+ size="lg"
96
+ isLoading={isLoading}
97
+ className="w-full"
98
+ >
99
+ Generate Ad
100
+ </Button>
101
+ </form>
102
+ </CardContent>
103
+ </Card>
104
+ );
105
+ };
frontend/components/generation/GenerationProgress.tsx ADDED
@@ -0,0 +1,217 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import React from "react";
4
+ import { Card, CardContent } from "@/components/ui/Card";
5
+ import { ProgressBar } from "@/components/ui/ProgressBar";
6
+ import { LoadingSpinner } from "@/components/ui/LoadingSpinner";
7
+ import {
8
+ Sparkles,
9
+ Image as ImageIcon,
10
+ Database,
11
+ CheckCircle2,
12
+ AlertCircle,
13
+ Wand2,
14
+ Zap
15
+ } from "lucide-react";
16
+ import type { GenerationProgress } from "@/types";
17
+
18
+ interface GenerationProgressProps {
19
+ progress: GenerationProgress;
20
+ }
21
+
22
+ const STEPS = [
23
+ { key: "copy", label: "Crafting Copy", icon: Sparkles, color: "from-blue-500 to-cyan-500", messages: [
24
+ "Brainstorming compelling headlines...",
25
+ "Writing persuasive ad copy...",
26
+ "Polishing the perfect message...",
27
+ "Adding psychological triggers...",
28
+ ]},
29
+ { key: "image", label: "Generating Images", icon: ImageIcon, color: "from-cyan-500 to-pink-500", messages: [
30
+ "Creating stunning visuals...",
31
+ "Bringing your vision to life...",
32
+ "Rendering high-quality images...",
33
+ "Adding creative flair...",
34
+ ]},
35
+ { key: "saving", label: "Saving", icon: Database, color: "from-pink-500 to-purple-500", messages: [
36
+ "Storing your creative...",
37
+ "Securing your masterpiece...",
38
+ "Almost done...",
39
+ ]},
40
+ ] as const;
41
+
42
+ export const GenerationProgressComponent: React.FC<GenerationProgressProps> = ({
43
+ progress,
44
+ }) => {
45
+ const stepProgress = {
46
+ idle: 0,
47
+ copy: 33,
48
+ image: 66,
49
+ saving: 90,
50
+ complete: 100,
51
+ error: 0,
52
+ };
53
+
54
+ const currentProgress = progress.progress || stepProgress[progress.step];
55
+ const currentStepIndex = STEPS.findIndex(s => s.key === progress.step);
56
+ const isComplete = progress.step === "complete";
57
+ const isError = progress.step === "error";
58
+
59
+ // Get random message for current step
60
+ const getStepMessage = () => {
61
+ if (progress.message) return progress.message;
62
+ const step = STEPS.find(s => s.key === progress.step);
63
+ if (step && step.messages.length > 0) {
64
+ const randomIndex = Math.floor(Math.random() * step.messages.length);
65
+ return step.messages[randomIndex];
66
+ }
67
+ return "Processing...";
68
+ };
69
+
70
+ return (
71
+ <Card variant="glass" className="overflow-hidden">
72
+ <CardContent className="pt-6">
73
+ <div className="space-y-6">
74
+ {/* Header with animated icon */}
75
+ <div className="flex items-center justify-between">
76
+ <div className="flex items-center space-x-4">
77
+ {isComplete ? (
78
+ <div className="relative">
79
+ <div className="absolute inset-0 bg-green-500 rounded-full animate-ping opacity-75"></div>
80
+ <CheckCircle2 className="h-8 w-8 text-green-500 relative z-10" />
81
+ </div>
82
+ ) : isError ? (
83
+ <AlertCircle className="h-8 w-8 text-red-500 animate-pulse" />
84
+ ) : (
85
+ <div className="relative">
86
+ <div className="absolute inset-0 bg-gradient-to-r from-blue-500 to-cyan-500 rounded-full animate-ping opacity-20"></div>
87
+ <div className="relative bg-gradient-to-r from-blue-500 to-cyan-500 rounded-full p-2">
88
+ <Wand2 className="h-5 w-5 text-white animate-spin-slow" />
89
+ </div>
90
+ </div>
91
+ )}
92
+ <div>
93
+ <h3 className="text-lg font-bold bg-gradient-to-r from-blue-600 to-cyan-600 bg-clip-text text-transparent">
94
+ {isComplete ? "Generation Complete!" : isError ? "Generation Failed" : "Creating Your Ad"}
95
+ </h3>
96
+ <p className="text-sm text-gray-600 mt-0.5">
97
+ {isComplete ? "Your ad is ready!" : isError ? "Something went wrong" : getStepMessage()}
98
+ </p>
99
+ </div>
100
+ </div>
101
+ {progress.estimatedTimeRemaining && !isComplete && !isError && (
102
+ <div className="text-right">
103
+ <div className="flex items-center space-x-1 text-sm font-semibold text-gray-700">
104
+ <Zap className="h-4 w-4 text-yellow-500 animate-pulse" />
105
+ <span>~{Math.ceil(progress.estimatedTimeRemaining)}s</span>
106
+ </div>
107
+ <p className="text-xs text-gray-500">remaining</p>
108
+ </div>
109
+ )}
110
+ </div>
111
+
112
+ {/* Step Indicators */}
113
+ {!isComplete && !isError && (
114
+ <div className="flex items-center justify-between relative">
115
+ {/* Progress line */}
116
+ <div className="absolute top-5 left-0 right-0 h-0.5 bg-gray-200 -z-10">
117
+ <div
118
+ className="h-full bg-gradient-to-r from-blue-500 via-cyan-500 to-pink-500 transition-all duration-500 ease-out"
119
+ style={{ width: `${currentProgress}%` }}
120
+ />
121
+ </div>
122
+
123
+ {STEPS.map((step, index) => {
124
+ const StepIcon = step.icon;
125
+ const isActive = progress.step === step.key;
126
+ const isCompleted = currentStepIndex > index;
127
+ const isUpcoming = currentStepIndex < index;
128
+
129
+ return (
130
+ <div key={step.key} className="flex flex-col items-center flex-1">
131
+ <div className={`relative w-10 h-10 rounded-full flex items-center justify-center transition-all duration-300 ${
132
+ isActive
133
+ ? `bg-gradient-to-r ${step.color} shadow-lg scale-110 animate-pulse`
134
+ : isCompleted
135
+ ? "bg-gradient-to-r from-green-500 to-emerald-500 shadow-md"
136
+ : "bg-gray-200"
137
+ }`}>
138
+ {isActive ? (
139
+ <div className="text-white">
140
+ <LoadingSpinner size="sm" />
141
+ </div>
142
+ ) : isCompleted ? (
143
+ <CheckCircle2 className="h-5 w-5 text-white" />
144
+ ) : (
145
+ <StepIcon className={`h-5 w-5 ${isUpcoming ? "text-gray-400" : "text-white"}`} />
146
+ )}
147
+ {isActive && (
148
+ <div className={`absolute inset-0 rounded-full bg-gradient-to-r ${step.color} animate-ping opacity-75`}></div>
149
+ )}
150
+ </div>
151
+ <p className={`text-xs font-medium mt-2 text-center ${
152
+ isActive
153
+ ? "text-gray-900 font-bold"
154
+ : isCompleted
155
+ ? "text-green-600"
156
+ : "text-gray-400"
157
+ }`}>
158
+ {step.label}
159
+ </p>
160
+ </div>
161
+ );
162
+ })}
163
+ </div>
164
+ )}
165
+
166
+ {/* Progress Bar */}
167
+ <div className="space-y-2">
168
+ <div className="flex justify-between items-center">
169
+ <span className="text-sm font-semibold text-gray-700">Overall Progress</span>
170
+ <span className="text-sm font-bold bg-gradient-to-r from-blue-600 to-cyan-600 bg-clip-text text-transparent">
171
+ {Math.round(currentProgress)}%
172
+ </span>
173
+ </div>
174
+ <ProgressBar
175
+ progress={currentProgress}
176
+ showPercentage={false}
177
+ />
178
+ </div>
179
+
180
+ {/* Success State */}
181
+ {isComplete && (
182
+ <div className="mt-4 p-4 bg-gradient-to-r from-green-50 to-emerald-50 border-2 border-green-200 rounded-xl animate-scale-in">
183
+ <div className="flex items-center space-x-3">
184
+ <CheckCircle2 className="h-6 w-6 text-green-600 flex-shrink-0" />
185
+ <div>
186
+ <p className="text-sm font-semibold text-green-900">
187
+ Ad generated successfully!
188
+ </p>
189
+ <p className="text-xs text-green-700 mt-0.5">
190
+ Your creative is ready to use
191
+ </p>
192
+ </div>
193
+ </div>
194
+ </div>
195
+ )}
196
+
197
+ {/* Error State */}
198
+ {isError && (
199
+ <div className="mt-4 p-4 bg-gradient-to-r from-red-50 to-pink-50 border-2 border-red-200 rounded-xl animate-scale-in">
200
+ <div className="flex items-center space-x-3">
201
+ <AlertCircle className="h-6 w-6 text-red-600 flex-shrink-0" />
202
+ <div>
203
+ <p className="text-sm font-semibold text-red-900">
204
+ Generation failed
205
+ </p>
206
+ <p className="text-xs text-red-700 mt-0.5">
207
+ {progress.message || "An error occurred. Please try again."}
208
+ </p>
209
+ </div>
210
+ </div>
211
+ </div>
212
+ )}
213
+ </div>
214
+ </CardContent>
215
+ </Card>
216
+ );
217
+ };
frontend/components/layout/ConditionalHeader.tsx ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import React from "react";
4
+ import { usePathname } from "next/navigation";
5
+ import { Header } from "./Header";
6
+
7
+ export const ConditionalHeader: React.FC = () => {
8
+ const pathname = usePathname();
9
+ const isLoginPage = pathname === "/login";
10
+
11
+ if (isLoginPage) {
12
+ return null;
13
+ }
14
+
15
+ return <Header />;
16
+ };
frontend/components/layout/Header.tsx ADDED
@@ -0,0 +1,100 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import React from "react";
4
+ import Link from "next/link";
5
+ import { usePathname, useRouter } from "next/navigation";
6
+ import { Home, Rocket, Grid, Layers, LogOut, User } from "lucide-react";
7
+ import { useAuthStore } from "@/store/authStore";
8
+ import { Button } from "@/components/ui/Button";
9
+
10
+ export const Header: React.FC = () => {
11
+ const pathname = usePathname();
12
+ const router = useRouter();
13
+ const { isAuthenticated, user, logout } = useAuthStore();
14
+
15
+ const navItems = [
16
+ { href: "/", label: "Dashboard", icon: Home },
17
+ { href: "/generate", label: "Generate", icon: Rocket },
18
+ { href: "/gallery", label: "Gallery", icon: Grid },
19
+ { href: "/matrix", label: "Matrix", icon: Layers },
20
+ ];
21
+
22
+ const handleLogout = () => {
23
+ logout();
24
+ router.push("/login");
25
+ };
26
+
27
+ return (
28
+ <header className="sticky top-0 z-50 glass border-b border-white/20 backdrop-blur-xl transition-all duration-300">
29
+ <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
30
+ <div className="flex justify-between items-center h-20">
31
+ <div className="flex items-center">
32
+ <Link href="/" className="flex items-center space-x-3 group">
33
+ <div className="relative">
34
+ <Rocket className="h-8 w-8 text-blue-500 group-hover:text-cyan-500 transition-all duration-300 group-hover:scale-110 group-hover:-translate-y-1" />
35
+ <div className="absolute inset-0 bg-blue-500/20 rounded-full blur-xl group-hover:bg-cyan-500/20 transition-colors duration-300"></div>
36
+ </div>
37
+ <span className="text-2xl font-bold gradient-text group-hover:scale-105 transition-transform duration-300">
38
+ Creative Breakthrough
39
+ </span>
40
+ </Link>
41
+ </div>
42
+
43
+ <div className="flex items-center space-x-4">
44
+ <nav className="hidden md:flex space-x-2">
45
+ {navItems.map((item) => {
46
+ const Icon = item.icon;
47
+ const isActive = pathname === item.href ||
48
+ (item.href !== "/" && pathname?.startsWith(item.href));
49
+
50
+ return (
51
+ <Link
52
+ key={item.href}
53
+ href={item.href}
54
+ className={`relative flex items-center space-x-2 px-5 py-2.5 rounded-xl text-sm font-semibold transition-all duration-300 ${
55
+ isActive
56
+ ? "bg-gradient-to-r from-blue-500 to-cyan-500 text-white shadow-lg scale-105"
57
+ : "text-gray-700 hover:bg-white/50 hover:text-gray-900 hover:scale-105"
58
+ }`}
59
+ >
60
+ <Icon className={`h-4 w-4 transition-transform duration-300 ${isActive ? "scale-110" : ""}`} />
61
+ <span>{item.label}</span>
62
+ {isActive && (
63
+ <div className="absolute inset-0 rounded-xl bg-gradient-to-r from-blue-500 to-cyan-500 opacity-50 blur-sm -z-10"></div>
64
+ )}
65
+ </Link>
66
+ );
67
+ })}
68
+ </nav>
69
+
70
+ {isAuthenticated ? (
71
+ <div className="flex items-center space-x-3">
72
+ <div className="hidden md:flex items-center space-x-2 px-4 py-2 rounded-xl bg-white/50 backdrop-blur-sm text-gray-700">
73
+ <User className="h-4 w-4" />
74
+ <span className="text-sm font-semibold">{user?.username}</span>
75
+ </div>
76
+ <Button
77
+ variant="outline"
78
+ size="sm"
79
+ onClick={handleLogout}
80
+ className="flex items-center space-x-2"
81
+ >
82
+ <LogOut className="h-4 w-4" />
83
+ <span className="hidden sm:inline">Logout</span>
84
+ </Button>
85
+ </div>
86
+ ) : (
87
+ pathname !== "/login" && (
88
+ <Link href="/login">
89
+ <Button variant="primary" size="sm">
90
+ Login
91
+ </Button>
92
+ </Link>
93
+ )
94
+ )}
95
+ </div>
96
+ </div>
97
+ </div>
98
+ </header>
99
+ );
100
+ };
frontend/components/matrix/AngleSelector.tsx ADDED
@@ -0,0 +1,133 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import React, { useState, useEffect } from "react";
4
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/Card";
5
+ import { Input } from "@/components/ui/Input";
6
+ import { Select } from "@/components/ui/Select";
7
+ import { LoadingSpinner } from "@/components/ui/LoadingSpinner";
8
+ import { getAllAngles } from "@/lib/api/endpoints";
9
+ import { useMatrixStore } from "@/store/matrixStore";
10
+ import type { AngleInfo, AnglesResponse } from "@/types/api";
11
+
12
+ interface AngleSelectorProps {
13
+ onSelect?: (angle: AngleInfo) => void;
14
+ selectedAngle?: AngleInfo | null;
15
+ }
16
+
17
+ export const AngleSelector: React.FC<AngleSelectorProps> = ({
18
+ onSelect,
19
+ selectedAngle,
20
+ }) => {
21
+ const { angles, isLoading, setAngles, setIsLoading } = useMatrixStore();
22
+ const [searchTerm, setSearchTerm] = useState("");
23
+ const [selectedCategory, setSelectedCategory] = useState<string>("");
24
+
25
+ useEffect(() => {
26
+ if (!angles) {
27
+ loadAngles();
28
+ }
29
+ }, []);
30
+
31
+ const loadAngles = async () => {
32
+ setIsLoading(true);
33
+ try {
34
+ const data = await getAllAngles();
35
+ setAngles(data);
36
+ } catch (error) {
37
+ console.error("Failed to load angles:", error);
38
+ } finally {
39
+ setIsLoading(false);
40
+ }
41
+ };
42
+
43
+ if (isLoading && !angles) {
44
+ return (
45
+ <Card>
46
+ <CardContent className="pt-6">
47
+ <LoadingSpinner size="lg" />
48
+ </CardContent>
49
+ </Card>
50
+ );
51
+ }
52
+
53
+ if (!angles) {
54
+ return (
55
+ <Card>
56
+ <CardContent className="pt-6">
57
+ <p className="text-gray-500">Failed to load angles</p>
58
+ </CardContent>
59
+ </Card>
60
+ );
61
+ }
62
+
63
+ // Filter angles
64
+ let filteredAngles: Array<{ category: string; angle: any }> = [];
65
+
66
+ Object.entries(angles.categories).forEach(([catKey, catData]) => {
67
+ if (selectedCategory && catKey !== selectedCategory) return;
68
+
69
+ catData.angles.forEach((angle) => {
70
+ if (
71
+ !searchTerm ||
72
+ angle.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
73
+ angle.trigger.toLowerCase().includes(searchTerm.toLowerCase()) ||
74
+ angle.key.toLowerCase().includes(searchTerm.toLowerCase())
75
+ ) {
76
+ filteredAngles.push({ category: catData.name, angle });
77
+ }
78
+ });
79
+ });
80
+
81
+ const categories = Object.entries(angles.categories).map(([key, data]) => ({
82
+ value: key,
83
+ label: data.name,
84
+ }));
85
+
86
+ return (
87
+ <Card variant="glass">
88
+ <CardHeader>
89
+ <CardTitle>Select Angle</CardTitle>
90
+ </CardHeader>
91
+ <CardContent className="space-y-4">
92
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
93
+ <Input
94
+ placeholder="Search angles..."
95
+ value={searchTerm}
96
+ onChange={(e) => setSearchTerm(e.target.value)}
97
+ />
98
+ <Select
99
+ options={[{ value: "", label: "All Categories" }, ...categories]}
100
+ value={selectedCategory}
101
+ onChange={(e) => setSelectedCategory(e.target.value)}
102
+ />
103
+ </div>
104
+
105
+ <div className="max-h-96 overflow-y-auto space-y-2">
106
+ {filteredAngles.length === 0 ? (
107
+ <p className="text-center text-gray-500 py-8">No angles found</p>
108
+ ) : (
109
+ filteredAngles.map(({ category, angle }) => (
110
+ <div
111
+ key={angle.key}
112
+ onClick={() => onSelect?.(angle)}
113
+ className={`p-3 rounded-lg border cursor-pointer transition-colors ${
114
+ selectedAngle?.key === angle.key
115
+ ? "border-blue-500 bg-blue-50"
116
+ : "border-gray-200 hover:border-gray-300 hover:bg-gray-50"
117
+ }`}
118
+ >
119
+ <div className="flex items-start justify-between">
120
+ <div className="flex-1">
121
+ <h4 className="font-semibold text-gray-900">{angle.name}</h4>
122
+ <p className="text-sm text-gray-600 mt-1">{angle.trigger}</p>
123
+ <p className="text-xs text-gray-500 mt-1">{category}</p>
124
+ </div>
125
+ </div>
126
+ </div>
127
+ ))
128
+ )}
129
+ </div>
130
+ </CardContent>
131
+ </Card>
132
+ );
133
+ };
frontend/components/matrix/ConceptSelector.tsx ADDED
@@ -0,0 +1,208 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import React, { useState, useEffect } from "react";
4
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/Card";
5
+ import { Input } from "@/components/ui/Input";
6
+ import { Select } from "@/components/ui/Select";
7
+ import { LoadingSpinner } from "@/components/ui/LoadingSpinner";
8
+ import { getAllConcepts, getCompatibleConcepts } from "@/lib/api/endpoints";
9
+ import { useMatrixStore } from "@/store/matrixStore";
10
+ import type { ConceptInfo, ConceptsResponse } from "@/types/api";
11
+
12
+ interface ConceptSelectorProps {
13
+ onSelect?: (concept: ConceptInfo) => void;
14
+ selectedConcept?: ConceptInfo | null;
15
+ angleKey?: string | null;
16
+ }
17
+
18
+ export const ConceptSelector: React.FC<ConceptSelectorProps> = ({
19
+ onSelect,
20
+ selectedConcept,
21
+ angleKey,
22
+ }) => {
23
+ const { concepts, compatibleConcepts, isLoading, setConcepts, setCompatibleConcepts, setIsLoading } = useMatrixStore();
24
+ const [searchTerm, setSearchTerm] = useState("");
25
+ const [selectedCategory, setSelectedCategory] = useState<string>("");
26
+ const [showCompatibleOnly, setShowCompatibleOnly] = useState(false);
27
+
28
+ useEffect(() => {
29
+ if (!concepts) {
30
+ loadConcepts();
31
+ }
32
+ }, []);
33
+
34
+ useEffect(() => {
35
+ if (angleKey && showCompatibleOnly) {
36
+ loadCompatibleConcepts(angleKey);
37
+ }
38
+ }, [angleKey, showCompatibleOnly]);
39
+
40
+ const loadConcepts = async () => {
41
+ setIsLoading(true);
42
+ try {
43
+ const data = await getAllConcepts();
44
+ setConcepts(data);
45
+ } catch (error) {
46
+ console.error("Failed to load concepts:", error);
47
+ } finally {
48
+ setIsLoading(false);
49
+ }
50
+ };
51
+
52
+ const loadCompatibleConcepts = async (key: string) => {
53
+ try {
54
+ const data = await getCompatibleConcepts(key);
55
+ // Convert compatible concepts to full ConceptInfo by fetching from all concepts
56
+ if (concepts) {
57
+ const fullConcepts: ConceptInfo[] = [];
58
+ for (const compat of data.compatible_concepts) {
59
+ // Find full concept info from all concepts
60
+ let found = false;
61
+ for (const [catKey, cat] of Object.entries(concepts.categories)) {
62
+ const fullConcept = cat.concepts.find((c) => c.key === compat.key);
63
+ if (fullConcept) {
64
+ // Add category to the concept
65
+ fullConcepts.push({
66
+ ...fullConcept,
67
+ category: cat.name,
68
+ });
69
+ found = true;
70
+ break;
71
+ }
72
+ }
73
+ // If not found, create a minimal version
74
+ if (!found) {
75
+ fullConcepts.push({
76
+ key: compat.key,
77
+ name: compat.name,
78
+ structure: compat.structure,
79
+ visual: "",
80
+ category: "",
81
+ });
82
+ }
83
+ }
84
+ setCompatibleConcepts(fullConcepts);
85
+ } else {
86
+ // Fallback to minimal concepts if full concepts not loaded
87
+ const minimalConcepts: ConceptInfo[] = data.compatible_concepts.map((c): ConceptInfo => ({
88
+ key: c.key,
89
+ name: c.name,
90
+ structure: c.structure,
91
+ visual: "",
92
+ category: "",
93
+ }));
94
+ setCompatibleConcepts(minimalConcepts);
95
+ }
96
+ } catch (error) {
97
+ console.error("Failed to load compatible concepts:", error);
98
+ }
99
+ };
100
+
101
+ if (isLoading && !concepts) {
102
+ return (
103
+ <Card>
104
+ <CardContent className="pt-6">
105
+ <LoadingSpinner size="lg" />
106
+ </CardContent>
107
+ </Card>
108
+ );
109
+ }
110
+
111
+ if (!concepts) {
112
+ return (
113
+ <Card>
114
+ <CardContent className="pt-6">
115
+ <p className="text-gray-500">Failed to load concepts</p>
116
+ </CardContent>
117
+ </Card>
118
+ );
119
+ }
120
+
121
+ // Filter concepts
122
+ let filteredConcepts: Array<{ category: string; concept: any }> = [];
123
+ const conceptsToUse = showCompatibleOnly && compatibleConcepts.length > 0
124
+ ? compatibleConcepts
125
+ : Object.values(concepts.categories).flatMap((cat) => cat.concepts);
126
+
127
+ Object.entries(concepts.categories).forEach(([catKey, catData]) => {
128
+ if (selectedCategory && catKey !== selectedCategory) return;
129
+
130
+ catData.concepts.forEach((concept) => {
131
+ if (
132
+ (!showCompatibleOnly || compatibleConcepts.some((c) => c.key === concept.key)) &&
133
+ (!searchTerm ||
134
+ concept.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
135
+ concept.structure.toLowerCase().includes(searchTerm.toLowerCase()) ||
136
+ concept.key.toLowerCase().includes(searchTerm.toLowerCase()))
137
+ ) {
138
+ filteredConcepts.push({ category: catData.name, concept });
139
+ }
140
+ });
141
+ });
142
+
143
+ const categories = Object.entries(concepts.categories).map(([key, data]) => ({
144
+ value: key,
145
+ label: data.name,
146
+ }));
147
+
148
+ return (
149
+ <Card variant="glass">
150
+ <CardHeader>
151
+ <CardTitle>Select Concept</CardTitle>
152
+ {angleKey && (
153
+ <div className="mt-2">
154
+ <label className="flex items-center space-x-2">
155
+ <input
156
+ type="checkbox"
157
+ checked={showCompatibleOnly}
158
+ onChange={(e) => setShowCompatibleOnly(e.target.checked)}
159
+ className="rounded"
160
+ />
161
+ <span className="text-sm text-gray-600">Show compatible concepts only</span>
162
+ </label>
163
+ </div>
164
+ )}
165
+ </CardHeader>
166
+ <CardContent className="space-y-4">
167
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
168
+ <Input
169
+ placeholder="Search concepts..."
170
+ value={searchTerm}
171
+ onChange={(e) => setSearchTerm(e.target.value)}
172
+ />
173
+ <Select
174
+ options={[{ value: "", label: "All Categories" }, ...categories]}
175
+ value={selectedCategory}
176
+ onChange={(e) => setSelectedCategory(e.target.value)}
177
+ />
178
+ </div>
179
+
180
+ <div className="max-h-96 overflow-y-auto space-y-2">
181
+ {filteredConcepts.length === 0 ? (
182
+ <p className="text-center text-gray-500 py-8">No concepts found</p>
183
+ ) : (
184
+ filteredConcepts.map(({ category, concept }) => (
185
+ <div
186
+ key={concept.key}
187
+ onClick={() => onSelect?.(concept)}
188
+ className={`p-3 rounded-lg border cursor-pointer transition-colors ${
189
+ selectedConcept?.key === concept.key
190
+ ? "border-blue-500 bg-blue-50"
191
+ : "border-gray-200 hover:border-gray-300 hover:bg-gray-50"
192
+ }`}
193
+ >
194
+ <div className="flex items-start justify-between">
195
+ <div className="flex-1">
196
+ <h4 className="font-semibold text-gray-900">{concept.name}</h4>
197
+ <p className="text-sm text-gray-600 mt-1">{concept.structure}</p>
198
+ <p className="text-xs text-gray-500 mt-1">{category}</p>
199
+ </div>
200
+ </div>
201
+ </div>
202
+ ))
203
+ )}
204
+ </div>
205
+ </CardContent>
206
+ </Card>
207
+ );
208
+ };
frontend/components/matrix/TestingMatrixBuilder.tsx ADDED
@@ -0,0 +1,436 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import React, { useState } from "react";
4
+ import { useRouter } from "next/navigation";
5
+ import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/Card";
6
+ import { Button } from "@/components/ui/Button";
7
+ import { Select } from "@/components/ui/Select";
8
+ import { ProgressBar } from "@/components/ui/ProgressBar";
9
+ import { generateTestingMatrix, generateMatrixAd } from "@/lib/api/endpoints";
10
+ import { exportAsJSON, exportAsCSV } from "@/lib/utils/export";
11
+ import { toast } from "react-hot-toast";
12
+ import { Download, FileJson, FileSpreadsheet, Rocket, CheckSquare, Square } from "lucide-react";
13
+ import { IMAGE_MODELS } from "@/lib/constants/models";
14
+ import type { TestingMatrixResponse, CombinationInfo } from "@/types/api";
15
+ import type { Niche } from "@/types/api";
16
+
17
+ interface TestingMatrixBuilderProps {
18
+ onMatrixGenerated?: (matrix: TestingMatrixResponse) => void;
19
+ }
20
+
21
+ export const TestingMatrixBuilder: React.FC<TestingMatrixBuilderProps> = ({
22
+ onMatrixGenerated,
23
+ }) => {
24
+ const router = useRouter();
25
+ const [niche, setNiche] = useState<Niche>("home_insurance");
26
+ const [strategy, setStrategy] = useState<"balanced" | "top_performers" | "diverse">("balanced");
27
+ const [angleCount, setAngleCount] = useState(6);
28
+ const [conceptCount, setConceptCount] = useState(5);
29
+ const [matrix, setMatrix] = useState<TestingMatrixResponse | null>(null);
30
+ const [isGenerating, setIsGenerating] = useState(false);
31
+ const [selectedCombinations, setSelectedCombinations] = useState<Set<string>>(new Set());
32
+ const [isGeneratingAds, setIsGeneratingAds] = useState(false);
33
+ const [generationProgress, setGenerationProgress] = useState({ current: 0, total: 0 });
34
+ const [numVariations, setNumVariations] = useState(1);
35
+ const [imageModel, setImageModel] = useState<string | null>(null);
36
+
37
+ const handleGenerate = async () => {
38
+ setIsGenerating(true);
39
+ try {
40
+ const result = await generateTestingMatrix({
41
+ niche,
42
+ angle_count: angleCount,
43
+ concept_count: conceptCount,
44
+ strategy,
45
+ });
46
+ setMatrix(result);
47
+ onMatrixGenerated?.(result);
48
+ toast.success(`Generated ${result.summary.total_combinations} combinations!`);
49
+ } catch (error: any) {
50
+ toast.error(error.message || "Failed to generate testing matrix");
51
+ } finally {
52
+ setIsGenerating(false);
53
+ }
54
+ };
55
+
56
+ const handleExportJSON = () => {
57
+ if (!matrix) return;
58
+ exportAsJSON(matrix, `testing-matrix-${niche}-${Date.now()}.json`);
59
+ toast.success("Matrix exported as JSON");
60
+ };
61
+
62
+ const handleExportCSV = () => {
63
+ if (!matrix) return;
64
+ const csvData = matrix.combinations.map((combo) => ({
65
+ combination_id: combo.combination_id,
66
+ angle_key: combo.angle.key,
67
+ angle_name: combo.angle.name,
68
+ angle_trigger: combo.angle.trigger,
69
+ concept_key: combo.concept.key,
70
+ concept_name: combo.concept.name,
71
+ compatibility_score: combo.compatibility_score,
72
+ }));
73
+ exportAsCSV(csvData, `testing-matrix-${niche}-${Date.now()}.csv`);
74
+ toast.success("Matrix exported as CSV");
75
+ };
76
+
77
+ const toggleCombination = (combinationId: string) => {
78
+ const newSelected = new Set(selectedCombinations);
79
+ if (newSelected.has(combinationId)) {
80
+ newSelected.delete(combinationId);
81
+ } else {
82
+ newSelected.add(combinationId);
83
+ }
84
+ setSelectedCombinations(newSelected);
85
+ };
86
+
87
+ const selectAll = () => {
88
+ if (!matrix) return;
89
+ if (selectedCombinations.size === matrix.combinations.length) {
90
+ setSelectedCombinations(new Set());
91
+ } else {
92
+ setSelectedCombinations(new Set(matrix.combinations.map(c => c.combination_id)));
93
+ }
94
+ };
95
+
96
+ const handleGenerateAds = async () => {
97
+ if (!matrix) return;
98
+
99
+ const combinationsToGenerate = selectedCombinations.size > 0
100
+ ? matrix.combinations.filter(c => selectedCombinations.has(c.combination_id))
101
+ : matrix.combinations;
102
+
103
+ if (combinationsToGenerate.length === 0) {
104
+ toast.error("Please select at least one combination");
105
+ return;
106
+ }
107
+
108
+ setIsGeneratingAds(true);
109
+ setGenerationProgress({ current: 0, total: combinationsToGenerate.length });
110
+
111
+ try {
112
+ const generatedAds = [];
113
+
114
+ for (let i = 0; i < combinationsToGenerate.length; i++) {
115
+ const combo = combinationsToGenerate[i];
116
+ setGenerationProgress({ current: i + 1, total: combinationsToGenerate.length });
117
+
118
+ try {
119
+ const result = await generateMatrixAd({
120
+ niche,
121
+ angle_key: combo.angle.key,
122
+ concept_key: combo.concept.key,
123
+ num_images: numVariations,
124
+ image_model: imageModel,
125
+ });
126
+ generatedAds.push(result);
127
+ toast.success(`Generated ad ${i + 1}/${combinationsToGenerate.length}`);
128
+ } catch (error: any) {
129
+ console.error(`Failed to generate ad for combination ${combo.combination_id}:`, error);
130
+ toast.error(`Failed to generate ad ${i + 1}/${combinationsToGenerate.length}`);
131
+ }
132
+ }
133
+
134
+ toast.success(`Successfully generated ${generatedAds.length} ads!`);
135
+
136
+ // Navigate to gallery to see the results
137
+ setTimeout(() => {
138
+ router.push("/gallery");
139
+ }, 1500);
140
+ } catch (error: any) {
141
+ toast.error(error.message || "Failed to generate ads");
142
+ } finally {
143
+ setIsGeneratingAds(false);
144
+ setGenerationProgress({ current: 0, total: 0 });
145
+ }
146
+ };
147
+
148
+ return (
149
+ <div className="space-y-6">
150
+ <Card variant="glass">
151
+ <CardHeader>
152
+ <CardTitle>Testing Matrix Builder</CardTitle>
153
+ <CardDescription>
154
+ Generate a systematic testing matrix for ad optimization
155
+ </CardDescription>
156
+ </CardHeader>
157
+ <CardContent className="space-y-6">
158
+ <Select
159
+ label="Niche"
160
+ options={[
161
+ { value: "home_insurance", label: "Home Insurance" },
162
+ { value: "glp1", label: "GLP-1" },
163
+ ]}
164
+ value={niche}
165
+ onChange={(e) => setNiche(e.target.value as Niche)}
166
+ />
167
+
168
+ <Select
169
+ label="Strategy"
170
+ options={[
171
+ { value: "balanced", label: "Balanced - Mix of top performers and diverse" },
172
+ { value: "top_performers", label: "Top Performers - Focus on proven winners" },
173
+ { value: "diverse", label: "Diverse - Maximum variety" },
174
+ ]}
175
+ value={strategy}
176
+ onChange={(e) => setStrategy(e.target.value as typeof strategy)}
177
+ />
178
+
179
+ <div>
180
+ <label className="block text-sm font-medium text-gray-700 mb-2">
181
+ Number of Angles: {angleCount}
182
+ </label>
183
+ <input
184
+ type="range"
185
+ min="1"
186
+ max="10"
187
+ step="1"
188
+ className="w-full"
189
+ value={angleCount}
190
+ onChange={(e) => setAngleCount(Number(e.target.value))}
191
+ />
192
+ <div className="flex justify-between text-xs text-gray-500 mt-1">
193
+ <span>1</span>
194
+ <span>10</span>
195
+ </div>
196
+ </div>
197
+
198
+ <div>
199
+ <label className="block text-sm font-medium text-gray-700 mb-2">
200
+ Concepts per Angle: {conceptCount}
201
+ </label>
202
+ <input
203
+ type="range"
204
+ min="1"
205
+ max="10"
206
+ step="1"
207
+ className="w-full"
208
+ value={conceptCount}
209
+ onChange={(e) => setConceptCount(Number(e.target.value))}
210
+ />
211
+ <div className="flex justify-between text-xs text-gray-500 mt-1">
212
+ <span>1</span>
213
+ <span>10</span>
214
+ </div>
215
+ </div>
216
+
217
+ <div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
218
+ <p className="text-sm text-blue-800">
219
+ <strong>Total Combinations:</strong> {angleCount} × {conceptCount} = {angleCount * conceptCount}
220
+ </p>
221
+ </div>
222
+
223
+ <Button
224
+ variant="primary"
225
+ size="lg"
226
+ className="w-full"
227
+ onClick={handleGenerate}
228
+ isLoading={isGenerating}
229
+ >
230
+ Generate Testing Matrix
231
+ </Button>
232
+ </CardContent>
233
+ </Card>
234
+
235
+ {matrix && (
236
+ <>
237
+ <Card variant="glass">
238
+ <CardHeader>
239
+ <div className="flex items-center justify-between">
240
+ <div>
241
+ <CardTitle>Matrix Summary</CardTitle>
242
+ <CardDescription>
243
+ {matrix.summary.total_combinations} combinations ready for testing
244
+ </CardDescription>
245
+ </div>
246
+ <div className="flex space-x-2">
247
+ <Button variant="outline" size="sm" onClick={handleExportJSON}>
248
+ <FileJson className="h-4 w-4 mr-1" />
249
+ JSON
250
+ </Button>
251
+ <Button variant="outline" size="sm" onClick={handleExportCSV}>
252
+ <FileSpreadsheet className="h-4 w-4 mr-1" />
253
+ CSV
254
+ </Button>
255
+ </div>
256
+ </div>
257
+ </CardHeader>
258
+ <CardContent>
259
+ <div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
260
+ <div>
261
+ <p className="text-sm text-gray-600">Total Combinations</p>
262
+ <p className="text-2xl font-bold text-gray-900">{matrix.summary.total_combinations}</p>
263
+ </div>
264
+ <div>
265
+ <p className="text-sm text-gray-600">Unique Angles</p>
266
+ <p className="text-2xl font-bold text-gray-900">{matrix.summary.unique_angles}</p>
267
+ </div>
268
+ <div>
269
+ <p className="text-sm text-gray-600">Unique Concepts</p>
270
+ <p className="text-2xl font-bold text-gray-900">{matrix.summary.unique_concepts}</p>
271
+ </div>
272
+ <div>
273
+ <p className="text-sm text-gray-600">Avg Compatibility</p>
274
+ <p className="text-2xl font-bold text-gray-900">
275
+ {(matrix.summary.average_compatibility * 100).toFixed(0)}%
276
+ </p>
277
+ </div>
278
+ </div>
279
+
280
+ {/* Generate Ads Section */}
281
+ <div className="bg-gradient-to-r from-blue-50 to-cyan-50 border border-blue-200 rounded-xl p-6 mb-6">
282
+ <div className="flex items-center justify-between mb-4">
283
+ <div>
284
+ <h3 className="text-lg font-bold text-gray-900 mb-1">Generate Ads from Matrix</h3>
285
+ <p className="text-sm text-gray-600">
286
+ Select combinations to generate ads, or generate all
287
+ </p>
288
+ </div>
289
+ </div>
290
+
291
+ <div className="mb-4">
292
+ <label className="block text-sm font-medium text-gray-700 mb-2">
293
+ Variations per Combination: {numVariations}
294
+ </label>
295
+ <input
296
+ type="range"
297
+ min="1"
298
+ max="3"
299
+ step="1"
300
+ className="w-full accent-blue-500"
301
+ value={numVariations}
302
+ onChange={(e) => setNumVariations(Number(e.target.value))}
303
+ />
304
+ <div className="flex justify-between text-xs text-gray-500 mt-1">
305
+ <span>1</span>
306
+ <span>3</span>
307
+ </div>
308
+ </div>
309
+
310
+ <div className="mb-4">
311
+ <Select
312
+ label="Image Model"
313
+ options={IMAGE_MODELS.map(model => ({ value: model.value, label: model.label }))}
314
+ value={imageModel || ""}
315
+ onChange={(e) => setImageModel(e.target.value || null)}
316
+ />
317
+ </div>
318
+
319
+ <div className="flex items-center justify-between mb-4">
320
+ <div className="flex items-center space-x-2">
321
+ <Button
322
+ variant="outline"
323
+ size="sm"
324
+ onClick={selectAll}
325
+ >
326
+ {selectedCombinations.size === matrix.combinations.length ? (
327
+ <>
328
+ <CheckSquare className="h-4 w-4 mr-1" />
329
+ Deselect All
330
+ </>
331
+ ) : (
332
+ <>
333
+ <Square className="h-4 w-4 mr-1" />
334
+ Select All
335
+ </>
336
+ )}
337
+ </Button>
338
+ <span className="text-sm text-gray-600">
339
+ {selectedCombinations.size > 0
340
+ ? `${selectedCombinations.size} selected`
341
+ : "All combinations will be generated"}
342
+ </span>
343
+ </div>
344
+ <Button
345
+ variant="primary"
346
+ size="lg"
347
+ onClick={handleGenerateAds}
348
+ isLoading={isGeneratingAds}
349
+ disabled={isGeneratingAds}
350
+ >
351
+ <Rocket className="h-5 w-5 mr-2" />
352
+ Generate Ads
353
+ </Button>
354
+ </div>
355
+
356
+ {isGeneratingAds && generationProgress.total > 0 && (
357
+ <div className="mt-4">
358
+ <div className="flex items-center justify-between mb-2">
359
+ <span className="text-sm font-medium text-gray-700">
360
+ Generating ads...
361
+ </span>
362
+ <span className="text-sm text-gray-600">
363
+ {generationProgress.current} / {generationProgress.total}
364
+ </span>
365
+ </div>
366
+ <ProgressBar
367
+ progress={(generationProgress.current / generationProgress.total) * 100}
368
+ />
369
+ </div>
370
+ )}
371
+
372
+ <div className="bg-blue-100 border border-blue-300 rounded-lg p-3 mt-4">
373
+ <p className="text-xs text-blue-800">
374
+ <strong>Note:</strong> This will generate{" "}
375
+ {selectedCombinations.size > 0
376
+ ? `${selectedCombinations.size} × ${numVariations} = ${selectedCombinations.size * numVariations}`
377
+ : `${matrix.combinations.length} × ${numVariations} = ${matrix.combinations.length * numVariations}`}{" "}
378
+ total ad variations. This may take several minutes.
379
+ </p>
380
+ </div>
381
+ </div>
382
+
383
+ {/* Combinations List */}
384
+ <div className="max-h-96 overflow-y-auto space-y-2">
385
+ {matrix.combinations.map((combo) => {
386
+ const isSelected = selectedCombinations.has(combo.combination_id);
387
+ return (
388
+ <div
389
+ key={combo.combination_id}
390
+ onClick={() => toggleCombination(combo.combination_id)}
391
+ className={`p-3 border rounded-lg cursor-pointer transition-all ${
392
+ isSelected
393
+ ? "border-blue-500 bg-blue-50 shadow-md"
394
+ : "border-gray-200 hover:bg-gray-50 hover:border-gray-300"
395
+ }`}
396
+ >
397
+ <div className="flex items-start justify-between">
398
+ <div className="flex items-start space-x-3 flex-1">
399
+ <div className="mt-0.5">
400
+ {isSelected ? (
401
+ <CheckSquare className="h-5 w-5 text-blue-600" />
402
+ ) : (
403
+ <Square className="h-5 w-5 text-gray-400" />
404
+ )}
405
+ </div>
406
+ <div className="flex-1">
407
+ <div className="flex items-center space-x-2 mb-1">
408
+ <span className="text-sm font-semibold text-gray-900">
409
+ {combo.angle.name}
410
+ </span>
411
+ <span className="text-gray-400">×</span>
412
+ <span className="text-sm font-semibold text-gray-900">
413
+ {combo.concept.name}
414
+ </span>
415
+ </div>
416
+ <p className="text-xs text-gray-500">{combo.angle.trigger}</p>
417
+ <p className="text-xs text-gray-500 mt-1">{combo.concept.structure}</p>
418
+ </div>
419
+ </div>
420
+ <div className="ml-4 text-right">
421
+ <span className="text-xs font-medium text-blue-600">
422
+ {(combo.compatibility_score * 100).toFixed(0)}%
423
+ </span>
424
+ </div>
425
+ </div>
426
+ </div>
427
+ );
428
+ })}
429
+ </div>
430
+ </CardContent>
431
+ </Card>
432
+ </>
433
+ )}
434
+ </div>
435
+ );
436
+ };
frontend/components/ui/Button.tsx ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react";
2
+ import { cn } from "../../lib/utils/cn";
3
+
4
+ interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
5
+ variant?: "primary" | "secondary" | "outline" | "ghost" | "danger";
6
+ size?: "sm" | "md" | "lg";
7
+ isLoading?: boolean;
8
+ }
9
+
10
+ export const Button: React.FC<ButtonProps> = ({
11
+ children,
12
+ variant = "primary",
13
+ size = "md",
14
+ isLoading = false,
15
+ disabled,
16
+ className,
17
+ ...props
18
+ }) => {
19
+ const baseStyles = "inline-flex items-center justify-center rounded-xl font-semibold transition-all duration-250 ease-out focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none relative overflow-hidden group";
20
+
21
+ const variants = {
22
+ primary: "bg-gradient-to-r from-blue-500 to-cyan-500 text-white hover:from-blue-600 hover:to-cyan-600 focus:ring-blue-500 shadow-lg hover:shadow-xl hover:scale-105 active:scale-100",
23
+ secondary: "bg-gradient-to-r from-orange-400 to-pink-500 text-white hover:from-orange-500 hover:to-pink-600 focus:ring-orange-500 shadow-lg hover:shadow-xl hover:scale-105 active:scale-100",
24
+ outline: "border-2 border-gray-300 text-gray-700 hover:bg-gray-50 focus:ring-gray-500 hover:border-gray-400 hover:scale-105 active:scale-100 bg-white/50 backdrop-blur-sm",
25
+ ghost: "text-gray-700 hover:bg-gray-100/80 focus:ring-gray-500 hover:scale-105 active:scale-100",
26
+ danger: "bg-gradient-to-r from-red-500 to-red-600 text-white hover:from-red-600 hover:to-red-700 focus:ring-red-500 shadow-lg hover:shadow-xl hover:scale-105 active:scale-100",
27
+ };
28
+
29
+ const sizes = {
30
+ sm: "px-4 py-2 text-sm",
31
+ md: "px-6 py-3 text-base",
32
+ lg: "px-8 py-4 text-lg",
33
+ };
34
+
35
+ return (
36
+ <button
37
+ className={cn(baseStyles, variants[variant], sizes[size], className)}
38
+ disabled={disabled || isLoading}
39
+ {...props}
40
+ >
41
+ {/* Shine effect on hover */}
42
+ <span className="absolute inset-0 -translate-x-full group-hover:translate-x-full transition-transform duration-700 bg-gradient-to-r from-transparent via-white/20 to-transparent"></span>
43
+
44
+ {isLoading ? (
45
+ <>
46
+ <svg className="animate-spin -ml-1 mr-2 h-4 w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
47
+ <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
48
+ <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
49
+ </svg>
50
+ Loading...
51
+ </>
52
+ ) : (
53
+ <span className="relative z-10">{children}</span>
54
+ )}
55
+ </button>
56
+ );
57
+ };
frontend/components/ui/Card.tsx ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react";
2
+ import { cn } from "../../lib/utils/cn";
3
+
4
+ interface CardProps extends React.HTMLAttributes<HTMLDivElement> {
5
+ variant?: "default" | "outline" | "elevated" | "glass";
6
+ }
7
+
8
+ export const Card: React.FC<CardProps> = ({
9
+ children,
10
+ variant = "default",
11
+ className,
12
+ ...props
13
+ }) => {
14
+ const variants = {
15
+ default: "bg-white/80 backdrop-blur-sm border border-gray-200/50 shadow-md hover:shadow-lg transition-all duration-300",
16
+ outline: "bg-transparent border-2 border-gray-300/50 hover:border-gray-400 transition-all duration-300",
17
+ elevated: "bg-white shadow-xl hover:shadow-2xl transition-all duration-300 hover:-translate-y-1",
18
+ glass: "glass border border-white/20 shadow-xl hover:shadow-2xl transition-all duration-300 hover:-translate-y-1",
19
+ };
20
+
21
+ return (
22
+ <div
23
+ className={cn("rounded-2xl p-6 hover-lift", variants[variant], className)}
24
+ {...props}
25
+ >
26
+ {children}
27
+ </div>
28
+ );
29
+ };
30
+
31
+ export const CardHeader: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({
32
+ children,
33
+ className,
34
+ ...props
35
+ }) => (
36
+ <div className={cn("mb-6", className)} {...props}>
37
+ {children}
38
+ </div>
39
+ );
40
+
41
+ export const CardTitle: React.FC<React.HTMLAttributes<HTMLHeadingElement>> = ({
42
+ children,
43
+ className,
44
+ ...props
45
+ }) => (
46
+ <h3 className={cn("text-2xl font-bold gradient-text", className)} {...props}>
47
+ {children}
48
+ </h3>
49
+ );
50
+
51
+ export const CardDescription: React.FC<React.HTMLAttributes<HTMLParagraphElement>> = ({
52
+ children,
53
+ className,
54
+ ...props
55
+ }) => (
56
+ <p className={cn("text-sm text-gray-600 mt-2", className)} {...props}>
57
+ {children}
58
+ </p>
59
+ );
60
+
61
+ export const CardContent: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({
62
+ children,
63
+ className,
64
+ ...props
65
+ }) => (
66
+ <div className={cn("", className)} {...props}>
67
+ {children}
68
+ </div>
69
+ );
70
+
71
+ export const CardFooter: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({
72
+ children,
73
+ className,
74
+ ...props
75
+ }) => (
76
+ <div className={cn("mt-6 flex items-center justify-between", className)} {...props}>
77
+ {children}
78
+ </div>
79
+ );
frontend/components/ui/Input.tsx ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState } from "react";
2
+ import { cn } from "../../lib/utils/cn";
3
+
4
+ interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
5
+ label?: string;
6
+ error?: string;
7
+ }
8
+
9
+ export const Input: React.FC<InputProps> = ({
10
+ label,
11
+ error,
12
+ className,
13
+ ...props
14
+ }) => {
15
+ const [focused, setFocused] = useState(false);
16
+ const hasValue = props.value !== undefined && props.value !== "";
17
+
18
+ return (
19
+ <div className="w-full">
20
+ {label && (
21
+ <label className="block text-sm font-semibold text-gray-700 mb-2">
22
+ {label}
23
+ </label>
24
+ )}
25
+ <div className="relative">
26
+ <input
27
+ className={cn(
28
+ "w-full px-4 py-3 rounded-xl border-2 transition-all duration-250 focus:outline-none focus:ring-2 focus:ring-offset-2",
29
+ "bg-white/80 backdrop-blur-sm",
30
+ error
31
+ ? "border-red-400 focus:border-red-500 focus:ring-red-500"
32
+ : focused || hasValue
33
+ ? "border-blue-400 focus:border-blue-500 focus:ring-blue-500"
34
+ : "border-gray-300 focus:border-blue-500 focus:ring-blue-500",
35
+ "hover:border-gray-400",
36
+ className
37
+ )}
38
+ onFocus={() => setFocused(true)}
39
+ onBlur={() => setFocused(false)}
40
+ {...props}
41
+ />
42
+ {(focused || hasValue) && !error && (
43
+ <div className="absolute inset-0 rounded-xl border-2 border-blue-500 pointer-events-none animate-pulse-glow opacity-50"></div>
44
+ )}
45
+ </div>
46
+ {error && (
47
+ <p className="mt-2 text-sm text-red-600 font-medium animate-slide-in">{error}</p>
48
+ )}
49
+ </div>
50
+ );
51
+ };
frontend/components/ui/LoadingSkeleton.tsx ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react";
2
+ import { cn } from "../../lib/utils/cn";
3
+
4
+ interface LoadingSkeletonProps {
5
+ className?: string;
6
+ variant?: "text" | "circular" | "rectangular";
7
+ }
8
+
9
+ export const LoadingSkeleton: React.FC<LoadingSkeletonProps> = ({
10
+ className,
11
+ variant = "rectangular",
12
+ }) => {
13
+ const baseStyles = "animate-pulse bg-gradient-to-r from-gray-200 via-gray-300 to-gray-200 bg-[length:200%_100%] animate-shimmer";
14
+
15
+ const variants = {
16
+ text: "h-4 rounded",
17
+ circular: "rounded-full aspect-square",
18
+ rectangular: "rounded-xl",
19
+ };
20
+
21
+ return (
22
+ <div className={cn(baseStyles, variants[variant], className)} />
23
+ );
24
+ };
25
+
26
+ // Add shimmer animation to globals.css
27
+ const shimmerKeyframes = `
28
+ @keyframes shimmer {
29
+ 0% {
30
+ background-position: -200% 0;
31
+ }
32
+ 100% {
33
+ background-position: 200% 0;
34
+ }
35
+ }
36
+ `;
frontend/components/ui/LoadingSpinner.tsx ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react";
2
+ import { cn } from "../../lib/utils/cn";
3
+
4
+ interface LoadingSpinnerProps {
5
+ size?: "sm" | "md" | "lg";
6
+ className?: string;
7
+ }
8
+
9
+ export const LoadingSpinner: React.FC<LoadingSpinnerProps> = ({
10
+ size = "md",
11
+ className,
12
+ }) => {
13
+ const sizes = {
14
+ sm: "h-4 w-4",
15
+ md: "h-8 w-8",
16
+ lg: "h-12 w-12",
17
+ };
18
+
19
+ return (
20
+ <div className={cn("flex items-center justify-center relative", className)}>
21
+ {/* Outer glow */}
22
+ <div className={cn("absolute rounded-full bg-gradient-to-r from-blue-500 to-cyan-500 opacity-20 animate-ping", sizes[size])}></div>
23
+
24
+ {/* Main spinner */}
25
+ <svg
26
+ className={cn("animate-spin relative z-10", sizes[size])}
27
+ xmlns="http://www.w3.org/2000/svg"
28
+ fill="none"
29
+ viewBox="0 0 24 24"
30
+ >
31
+ <defs>
32
+ <linearGradient id="spinner-gradient" x1="0%" y1="0%" x2="100%" y2="100%">
33
+ <stop offset="0%" stopColor="#3b82f6" />
34
+ <stop offset="50%" stopColor="#06b6d4" />
35
+ <stop offset="100%" stopColor="#ec4899" />
36
+ </linearGradient>
37
+ </defs>
38
+ <circle
39
+ className="opacity-25"
40
+ cx="12"
41
+ cy="12"
42
+ r="10"
43
+ stroke="currentColor"
44
+ strokeWidth="4"
45
+ ></circle>
46
+ <path
47
+ className="opacity-75"
48
+ fill="url(#spinner-gradient)"
49
+ d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
50
+ ></path>
51
+ </svg>
52
+ </div>
53
+ );
54
+ };