Commit
·
8ffe335
1
Parent(s):
5106651
- frontend/app/browse/angles/page.tsx +15 -3
- frontend/app/browse/concepts/page.tsx +21 -3
- frontend/app/creative/modify/page.tsx +28 -3
- frontend/app/generate/page.tsx +2 -0
- frontend/app/globals.css +15 -0
- frontend/app/matrix/page.tsx +24 -4
- frontend/components/generation/BatchForm.tsx +9 -1
- frontend/components/generation/ExtensiveForm.tsx +21 -3
- frontend/components/generation/GenerationForm.tsx +9 -1
- frontend/components/matrix/AngleSelector.tsx +15 -3
- frontend/components/matrix/ConceptSelector.tsx +20 -3
- frontend/components/ui/InfoButton.tsx +122 -0
- main.py +1 -1
- services/generator.py +5 -3
- services/third_flow.py +139 -52
frontend/app/browse/angles/page.tsx
CHANGED
|
@@ -7,6 +7,7 @@ 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);
|
|
@@ -79,9 +80,20 @@ export default function AnglesPage() {
|
|
| 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 |
-
<
|
| 83 |
-
<
|
| 84 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
<p className="text-lg text-gray-600">
|
| 86 |
Browse all {angles.total_angles} available angles
|
| 87 |
</p>
|
|
|
|
| 7 |
import { LoadingSpinner } from "@/components/ui/LoadingSpinner";
|
| 8 |
import { getAllAngles } from "@/lib/api/endpoints";
|
| 9 |
import type { AnglesResponse } from "@/types/api";
|
| 10 |
+
import { InfoButton } from "@/components/ui/InfoButton";
|
| 11 |
|
| 12 |
export default function AnglesPage() {
|
| 13 |
const [angles, setAngles] = useState<AnglesResponse | null>(null);
|
|
|
|
| 80 |
<div className="absolute inset-0 bg-grid-pattern opacity-5"></div>
|
| 81 |
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
|
| 82 |
<div className="text-center animate-fade-in">
|
| 83 |
+
<div className="flex items-center justify-center gap-3 mb-4">
|
| 84 |
+
<h1 className="text-4xl md:text-5xl font-extrabold">
|
| 85 |
+
<span className="gradient-text">Angles</span>
|
| 86 |
+
</h1>
|
| 87 |
+
<InfoButton
|
| 88 |
+
title="What are Angles?"
|
| 89 |
+
content="Angles are the reason someone should care right now. They're the specific hooks or approaches that make your ad relevant and compelling to your target audience.
|
| 90 |
+
|
| 91 |
+
Each angle targets a specific emotional trigger or pain point. The same product can have multiple angles - for example, insurance could use 'Save money' or 'Protect your family' as different angles.
|
| 92 |
+
|
| 93 |
+
Browse through all available angles to find ones that resonate with your target audience. You can use these in the Matrix flow to create targeted ads."
|
| 94 |
+
position="bottom"
|
| 95 |
+
/>
|
| 96 |
+
</div>
|
| 97 |
<p className="text-lg text-gray-600">
|
| 98 |
Browse all {angles.total_angles} available angles
|
| 99 |
</p>
|
frontend/app/browse/concepts/page.tsx
CHANGED
|
@@ -7,6 +7,7 @@ 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);
|
|
@@ -79,9 +80,26 @@ export default function ConceptsPage() {
|
|
| 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 |
-
<
|
| 83 |
-
<
|
| 84 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
<p className="text-lg text-gray-600">
|
| 86 |
Browse all {concepts.total_concepts} available concepts
|
| 87 |
</p>
|
|
|
|
| 7 |
import { LoadingSpinner } from "@/components/ui/LoadingSpinner";
|
| 8 |
import { getAllConcepts } from "@/lib/api/endpoints";
|
| 9 |
import type { ConceptsResponse } from "@/types/api";
|
| 10 |
+
import { InfoButton } from "@/components/ui/InfoButton";
|
| 11 |
|
| 12 |
export default function ConceptsPage() {
|
| 13 |
const [concepts, setConcepts] = useState<ConceptsResponse | null>(null);
|
|
|
|
| 80 |
<div className="absolute inset-0 bg-grid-pattern opacity-5"></div>
|
| 81 |
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
|
| 82 |
<div className="text-center animate-fade-in">
|
| 83 |
+
<div className="flex items-center justify-center gap-3 mb-4">
|
| 84 |
+
<h1 className="text-4xl md:text-5xl font-extrabold">
|
| 85 |
+
<span className="gradient-text">Concepts</span>
|
| 86 |
+
</h1>
|
| 87 |
+
<InfoButton
|
| 88 |
+
title="What are Concepts?"
|
| 89 |
+
content="Concepts are the creative execution styles or storylines you use to deliver your angle. They define how your ad will look and feel visually.
|
| 90 |
+
|
| 91 |
+
Examples include:
|
| 92 |
+
- Before/After comparisons
|
| 93 |
+
- Testimonials and reviews
|
| 94 |
+
- Problem/Solution narratives
|
| 95 |
+
- Visual metaphors
|
| 96 |
+
- Lifestyle imagery
|
| 97 |
+
- UGC (User Generated Content) style
|
| 98 |
+
|
| 99 |
+
Each concept has a specific structure and visual direction. When combined with an angle in the Matrix flow, they create powerful ads that both hook attention and drive action. Browse through all available concepts to find ones that match your creative vision."
|
| 100 |
+
position="bottom"
|
| 101 |
+
/>
|
| 102 |
+
</div>
|
| 103 |
<p className="text-lg text-gray-600">
|
| 104 |
Browse all {concepts.total_concepts} available concepts
|
| 105 |
</p>
|
frontend/app/creative/modify/page.tsx
CHANGED
|
@@ -18,6 +18,7 @@ import type {
|
|
| 18 |
ModifiedImageResult,
|
| 19 |
ImageCorrectResponse,
|
| 20 |
} from "@/types/api";
|
|
|
|
| 21 |
|
| 22 |
type WorkflowStep = "upload" | "analysis" | "modify" | "result";
|
| 23 |
|
|
@@ -213,9 +214,33 @@ export default function CreativeModifyPage() {
|
|
| 213 |
<div className="container mx-auto px-4 py-8">
|
| 214 |
{/* Header */}
|
| 215 |
<div className="text-center mb-8">
|
| 216 |
-
<
|
| 217 |
-
|
| 218 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 219 |
<p className="text-gray-600 max-w-2xl mx-auto">
|
| 220 |
Upload your existing creative, let AI analyze it, then apply new
|
| 221 |
angles or concepts to generate variations
|
|
|
|
| 18 |
ModifiedImageResult,
|
| 19 |
ImageCorrectResponse,
|
| 20 |
} from "@/types/api";
|
| 21 |
+
import { InfoButton } from "@/components/ui/InfoButton";
|
| 22 |
|
| 23 |
type WorkflowStep = "upload" | "analysis" | "modify" | "result";
|
| 24 |
|
|
|
|
| 214 |
<div className="container mx-auto px-4 py-8">
|
| 215 |
{/* Header */}
|
| 216 |
<div className="text-center mb-8">
|
| 217 |
+
<div className="flex items-center justify-center gap-3 mb-2">
|
| 218 |
+
<h1 className="text-3xl font-bold text-gray-900">
|
| 219 |
+
Creative Modifier
|
| 220 |
+
</h1>
|
| 221 |
+
<InfoButton
|
| 222 |
+
title="Creative Modification Flow"
|
| 223 |
+
content="This flow allows you to modify existing ad creatives:
|
| 224 |
+
|
| 225 |
+
1. UPLOAD: Upload an existing ad image (from URL or file)
|
| 226 |
+
|
| 227 |
+
2. ANALYSIS: AI analyzes your creative to understand:
|
| 228 |
+
- Current angle and concept
|
| 229 |
+
- Visual elements and composition
|
| 230 |
+
- Copy elements and messaging
|
| 231 |
+
- Overall strategy
|
| 232 |
+
|
| 233 |
+
3. MODIFY: Apply new angles or concepts to create variations:
|
| 234 |
+
- Modify: Change angle/concept while keeping similar structure
|
| 235 |
+
- Regenerate: Create completely new version
|
| 236 |
+
- Custom: Use your own prompt for specific changes
|
| 237 |
+
|
| 238 |
+
4. RESULT: Get your modified creative with new image and updated copy
|
| 239 |
+
|
| 240 |
+
Perfect for iterating on winning ads or testing new angles with proven visuals."
|
| 241 |
+
position="bottom"
|
| 242 |
+
/>
|
| 243 |
+
</div>
|
| 244 |
<p className="text-gray-600 max-w-2xl mx-auto">
|
| 245 |
Upload your existing creative, let AI analyze it, then apply new
|
| 246 |
angles or concepts to generate variations
|
frontend/app/generate/page.tsx
CHANGED
|
@@ -727,6 +727,7 @@ export default function GeneratePage() {
|
|
| 727 |
</p>
|
| 728 |
|
| 729 |
{/* Mode Toggle */}
|
|
|
|
| 730 |
<div className="flex items-center justify-center gap-3 flex-wrap">
|
| 731 |
<button
|
| 732 |
onClick={() => setMode("standard")}
|
|
@@ -773,6 +774,7 @@ export default function GeneratePage() {
|
|
| 773 |
Batch
|
| 774 |
</button>
|
| 775 |
</div>
|
|
|
|
| 776 |
</div>
|
| 777 |
</div>
|
| 778 |
</div>
|
|
|
|
| 727 |
</p>
|
| 728 |
|
| 729 |
{/* Mode Toggle */}
|
| 730 |
+
<div className="flex flex-col items-center gap-3">
|
| 731 |
<div className="flex items-center justify-center gap-3 flex-wrap">
|
| 732 |
<button
|
| 733 |
onClick={() => setMode("standard")}
|
|
|
|
| 774 |
Batch
|
| 775 |
</button>
|
| 776 |
</div>
|
| 777 |
+
</div>
|
| 778 |
</div>
|
| 779 |
</div>
|
| 780 |
</div>
|
frontend/app/globals.css
CHANGED
|
@@ -135,6 +135,21 @@ body {
|
|
| 135 |
animation: scaleIn 0.3s ease-out;
|
| 136 |
}
|
| 137 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 138 |
.animate-pulse-glow {
|
| 139 |
animation: pulse-glow 2s ease-in-out infinite;
|
| 140 |
}
|
|
|
|
| 135 |
animation: scaleIn 0.3s ease-out;
|
| 136 |
}
|
| 137 |
|
| 138 |
+
@keyframes infoTooltipEnter {
|
| 139 |
+
from {
|
| 140 |
+
opacity: 0;
|
| 141 |
+
transform: scale(0.96);
|
| 142 |
+
}
|
| 143 |
+
to {
|
| 144 |
+
opacity: 1;
|
| 145 |
+
transform: scale(1);
|
| 146 |
+
}
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
.info-tooltip-enter {
|
| 150 |
+
animation: infoTooltipEnter 0.15s ease-out;
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
.animate-pulse-glow {
|
| 154 |
animation: pulse-glow 2s ease-in-out infinite;
|
| 155 |
}
|
frontend/app/matrix/page.tsx
CHANGED
|
@@ -5,6 +5,7 @@ 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 { Search, TestTube } from "lucide-react";
|
|
|
|
| 8 |
|
| 9 |
export default function MatrixPage() {
|
| 10 |
return (
|
|
@@ -14,10 +15,29 @@ export default function MatrixPage() {
|
|
| 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 |
-
<
|
| 18 |
-
<
|
| 19 |
-
|
| 20 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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>
|
|
|
|
| 5 |
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/Card";
|
| 6 |
import { Button } from "@/components/ui/Button";
|
| 7 |
import { Search, TestTube } from "lucide-react";
|
| 8 |
+
import { InfoButton } from "@/components/ui/InfoButton";
|
| 9 |
|
| 10 |
export default function MatrixPage() {
|
| 11 |
return (
|
|
|
|
| 15 |
<div className="absolute inset-0 bg-grid-pattern opacity-5"></div>
|
| 16 |
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
|
| 17 |
<div className="text-center animate-fade-in">
|
| 18 |
+
<div className="flex items-center justify-center gap-3 mb-4">
|
| 19 |
+
<h1 className="text-4xl md:text-5xl font-extrabold">
|
| 20 |
+
<span className="gradient-text">Matrix</span>
|
| 21 |
+
<span className="text-gray-900"> System</span>
|
| 22 |
+
</h1>
|
| 23 |
+
<InfoButton
|
| 24 |
+
title="Matrix System Explained"
|
| 25 |
+
content="The Matrix System is a systematic approach to ad generation that combines Angles and Concepts.
|
| 26 |
+
|
| 27 |
+
ANGLES: The reason someone should care right now. They target specific emotional triggers or pain points.
|
| 28 |
+
|
| 29 |
+
CONCEPTS: The creative execution style - how your ad looks and feels visually.
|
| 30 |
+
|
| 31 |
+
By combining different angles with different concepts, you create a matrix of possibilities. This systematic approach helps you:
|
| 32 |
+
- Test multiple combinations efficiently
|
| 33 |
+
- Find winning angle/concept pairs
|
| 34 |
+
- Scale successful patterns
|
| 35 |
+
- Avoid creative fatigue
|
| 36 |
+
|
| 37 |
+
Use the Matrix flow in Generate to select specific combinations, or use the Testing Matrix Builder to create systematic test plans."
|
| 38 |
+
position="bottom"
|
| 39 |
+
/>
|
| 40 |
+
</div>
|
| 41 |
<p className="text-lg text-gray-600 max-w-2xl mx-auto">
|
| 42 |
Explore the Angle × Concept matrix for systematic ad generation
|
| 43 |
</p>
|
frontend/components/generation/BatchForm.tsx
CHANGED
|
@@ -10,6 +10,7 @@ import { Button } from "@/components/ui/Button";
|
|
| 10 |
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/Card";
|
| 11 |
import { IMAGE_MODELS, getModelCost, formatCost } 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; target_audience?: string | null; offer?: string | null }) => Promise<void>;
|
|
@@ -44,7 +45,14 @@ export const BatchForm: React.FC<BatchFormProps> = ({
|
|
| 44 |
return (
|
| 45 |
<Card variant="glass">
|
| 46 |
<CardHeader>
|
| 47 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
<CardDescription>
|
| 49 |
Generate multiple ads at once for testing and variety
|
| 50 |
</CardDescription>
|
|
|
|
| 10 |
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/Card";
|
| 11 |
import { IMAGE_MODELS, getModelCost, formatCost } from "@/lib/constants/models";
|
| 12 |
import type { Niche } from "@/types/api";
|
| 13 |
+
import { InfoButton } from "@/components/ui/InfoButton";
|
| 14 |
|
| 15 |
interface BatchFormProps {
|
| 16 |
onSubmit: (data: { niche: Niche; count: number; images_per_ad: number; image_model?: string | null; target_audience?: string | null; offer?: string | null }) => Promise<void>;
|
|
|
|
| 45 |
return (
|
| 46 |
<Card variant="glass">
|
| 47 |
<CardHeader>
|
| 48 |
+
<div className="flex items-center gap-2">
|
| 49 |
+
<CardTitle>Batch Generation</CardTitle>
|
| 50 |
+
<InfoButton
|
| 51 |
+
title="Batch Generation Flow"
|
| 52 |
+
content="Generate multiple ads simultaneously for A/B testing and variety. Each ad is created with randomized strategies, giving you diverse options to test. You can generate up to 100 ads with 1-3 variations per ad. Perfect for finding winning combinations through volume testing."
|
| 53 |
+
position="bottom"
|
| 54 |
+
/>
|
| 55 |
+
</div>
|
| 56 |
<CardDescription>
|
| 57 |
Generate multiple ads at once for testing and variety
|
| 58 |
</CardDescription>
|
frontend/components/generation/ExtensiveForm.tsx
CHANGED
|
@@ -8,6 +8,7 @@ import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/com
|
|
| 8 |
import { Select } from "@/components/ui/Select";
|
| 9 |
import { IMAGE_MODELS, getModelCost, formatCost } from "@/lib/constants/models";
|
| 10 |
import type { Niche } from "@/types/api";
|
|
|
|
| 11 |
|
| 12 |
const extensiveSchema = z.object({
|
| 13 |
niche: z.enum(["home_insurance", "glp1", "auto_insurance", "others"]),
|
|
@@ -57,7 +58,7 @@ export const ExtensiveForm: React.FC<ExtensiveFormProps> = ({
|
|
| 57 |
} = useForm<ExtensiveFormData>({
|
| 58 |
resolver: zodResolver(extensiveSchema),
|
| 59 |
defaultValues: {
|
| 60 |
-
niche: "home_insurance" as const,
|
| 61 |
custom_niche: "",
|
| 62 |
target_audience: "",
|
| 63 |
offer: "",
|
|
@@ -75,7 +76,24 @@ export const ExtensiveForm: React.FC<ExtensiveFormProps> = ({
|
|
| 75 |
return (
|
| 76 |
<Card variant="glass">
|
| 77 |
<CardHeader>
|
| 78 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
<CardDescription>
|
| 80 |
Researcher → Creative Director → Designer → Copywriter flow
|
| 81 |
</CardDescription>
|
|
@@ -133,7 +151,7 @@ export const ExtensiveForm: React.FC<ExtensiveFormProps> = ({
|
|
| 133 |
<input
|
| 134 |
type="text"
|
| 135 |
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"
|
| 136 |
-
placeholder="e.g.,
|
| 137 |
{...register("offer")}
|
| 138 |
/>
|
| 139 |
{errors.offer && (
|
|
|
|
| 8 |
import { Select } from "@/components/ui/Select";
|
| 9 |
import { IMAGE_MODELS, getModelCost, formatCost } from "@/lib/constants/models";
|
| 10 |
import type { Niche } from "@/types/api";
|
| 11 |
+
import { InfoButton } from "@/components/ui/InfoButton";
|
| 12 |
|
| 13 |
const extensiveSchema = z.object({
|
| 14 |
niche: z.enum(["home_insurance", "glp1", "auto_insurance", "others"]),
|
|
|
|
| 58 |
} = useForm<ExtensiveFormData>({
|
| 59 |
resolver: zodResolver(extensiveSchema),
|
| 60 |
defaultValues: {
|
| 61 |
+
niche: "home_insurance" as const, // first option; flow works for any niche
|
| 62 |
custom_niche: "",
|
| 63 |
target_audience: "",
|
| 64 |
offer: "",
|
|
|
|
| 76 |
return (
|
| 77 |
<Card variant="glass">
|
| 78 |
<CardHeader>
|
| 79 |
+
<div className="flex items-center gap-2">
|
| 80 |
+
<CardTitle>Extensive Generation</CardTitle>
|
| 81 |
+
<InfoButton
|
| 82 |
+
title="Extensive Generation Flow"
|
| 83 |
+
content="This flow works for any niche (insurance, GLP-1, auto, or custom). It uses a 4-stage process:
|
| 84 |
+
|
| 85 |
+
1. RESEARCHER: Analyzes your inputs and researches psychology triggers, angles, and concepts that work best for your niche and audience.
|
| 86 |
+
|
| 87 |
+
2. CREATIVE DIRECTOR: Uses marketing knowledge and successful ad patterns to create multiple creative strategies. Each strategy includes visual direction, text placement, CTA, and copy ideas.
|
| 88 |
+
|
| 89 |
+
3. DESIGNER: Converts each creative strategy into detailed image generation prompts optimized for affiliate marketing (authentic, low-production style).
|
| 90 |
+
|
| 91 |
+
4. COPYWRITER: Writes compelling ad copy (title, body, description) that matches each strategy's emotional tone and psychology trigger.
|
| 92 |
+
|
| 93 |
+
You can generate multiple strategies (1-10) and multiple variations per strategy (1-3) for comprehensive testing."
|
| 94 |
+
position="bottom"
|
| 95 |
+
/>
|
| 96 |
+
</div>
|
| 97 |
<CardDescription>
|
| 98 |
Researcher → Creative Director → Designer → Copywriter flow
|
| 99 |
</CardDescription>
|
|
|
|
| 151 |
<input
|
| 152 |
type="text"
|
| 153 |
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"
|
| 154 |
+
placeholder="e.g., Free quote, Save $500/year, Limited-time offer"
|
| 155 |
{...register("offer")}
|
| 156 |
/>
|
| 157 |
{errors.offer && (
|
frontend/components/generation/GenerationForm.tsx
CHANGED
|
@@ -12,6 +12,7 @@ import { IMAGE_MODELS, getModelCost, formatCost } from "@/lib/constants/models";
|
|
| 12 |
import type { Niche } from "@/types/api";
|
| 13 |
import { Loader2, TrendingUp, Check } from "lucide-react";
|
| 14 |
import apiClient from "@/lib/api/client";
|
|
|
|
| 15 |
|
| 16 |
interface GenerationFormProps {
|
| 17 |
onSubmit: (data: {
|
|
@@ -110,7 +111,14 @@ export const GenerationForm: React.FC<GenerationFormProps> = ({
|
|
| 110 |
return (
|
| 111 |
<Card variant="glass">
|
| 112 |
<CardHeader>
|
| 113 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 114 |
<CardDescription>
|
| 115 |
Create a new ad creative using randomized strategies
|
| 116 |
</CardDescription>
|
|
|
|
| 12 |
import type { Niche } from "@/types/api";
|
| 13 |
import { Loader2, TrendingUp, Check } from "lucide-react";
|
| 14 |
import apiClient from "@/lib/api/client";
|
| 15 |
+
import { InfoButton } from "@/components/ui/InfoButton";
|
| 16 |
|
| 17 |
interface GenerationFormProps {
|
| 18 |
onSubmit: (data: {
|
|
|
|
| 111 |
return (
|
| 112 |
<Card variant="glass">
|
| 113 |
<CardHeader>
|
| 114 |
+
<div className="flex items-center gap-2">
|
| 115 |
+
<CardTitle>Generate Ad</CardTitle>
|
| 116 |
+
<InfoButton
|
| 117 |
+
title="Standard Generation Flow"
|
| 118 |
+
content="This flow generates ads using randomized strategies from predefined angles and concepts. It's the fastest way to create ads with minimal configuration. The system automatically selects the best combinations based on your niche, target audience, and offer."
|
| 119 |
+
position="bottom"
|
| 120 |
+
/>
|
| 121 |
+
</div>
|
| 122 |
<CardDescription>
|
| 123 |
Create a new ad creative using randomized strategies
|
| 124 |
</CardDescription>
|
frontend/components/matrix/AngleSelector.tsx
CHANGED
|
@@ -8,6 +8,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;
|
|
@@ -86,9 +87,20 @@ export const AngleSelector: React.FC<AngleSelectorProps> = ({
|
|
| 86 |
return (
|
| 87 |
<Card variant="glass" className="border-2 border-transparent hover:border-blue-200/50 transition-all duration-300">
|
| 88 |
<CardHeader>
|
| 89 |
-
<
|
| 90 |
-
|
| 91 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 92 |
</CardHeader>
|
| 93 |
<CardContent className="space-y-4">
|
| 94 |
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
|
|
| 8 |
import { getAllAngles } from "@/lib/api/endpoints";
|
| 9 |
import { useMatrixStore } from "@/store/matrixStore";
|
| 10 |
import type { AngleInfo, AnglesResponse } from "@/types/api";
|
| 11 |
+
import { InfoButton } from "@/components/ui/InfoButton";
|
| 12 |
|
| 13 |
interface AngleSelectorProps {
|
| 14 |
onSelect?: (angle: AngleInfo) => void;
|
|
|
|
| 87 |
return (
|
| 88 |
<Card variant="glass" className="border-2 border-transparent hover:border-blue-200/50 transition-all duration-300">
|
| 89 |
<CardHeader>
|
| 90 |
+
<div className="flex items-center gap-2">
|
| 91 |
+
<CardTitle className="bg-gradient-to-r from-blue-600 to-cyan-600 bg-clip-text text-transparent">
|
| 92 |
+
Select Angle
|
| 93 |
+
</CardTitle>
|
| 94 |
+
<InfoButton
|
| 95 |
+
title="What is an Angle?"
|
| 96 |
+
content="An angle is the reason someone should care right now. It's the specific hook or approach that makes your ad relevant and compelling to your target audience.
|
| 97 |
+
|
| 98 |
+
The same product can have multiple angles - each targeting different emotional triggers or pain points. For example, 'Save money' vs 'Protect your family' are different angles for insurance.
|
| 99 |
+
|
| 100 |
+
Selecting the right angle helps ensure your ad resonates with your audience and drives action."
|
| 101 |
+
position="bottom"
|
| 102 |
+
/>
|
| 103 |
+
</div>
|
| 104 |
</CardHeader>
|
| 105 |
<CardContent className="space-y-4">
|
| 106 |
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
frontend/components/matrix/ConceptSelector.tsx
CHANGED
|
@@ -8,6 +8,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;
|
|
@@ -148,9 +149,25 @@ export const ConceptSelector: React.FC<ConceptSelectorProps> = ({
|
|
| 148 |
return (
|
| 149 |
<Card variant="glass" className="border-2 border-transparent hover:border-cyan-200/50 transition-all duration-300">
|
| 150 |
<CardHeader>
|
| 151 |
-
<
|
| 152 |
-
|
| 153 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 154 |
{angleKey && (
|
| 155 |
<div className="mt-2">
|
| 156 |
<label className="flex items-center space-x-2">
|
|
|
|
| 8 |
import { getAllConcepts, getCompatibleConcepts } from "@/lib/api/endpoints";
|
| 9 |
import { useMatrixStore } from "@/store/matrixStore";
|
| 10 |
import type { ConceptInfo, ConceptsResponse } from "@/types/api";
|
| 11 |
+
import { InfoButton } from "@/components/ui/InfoButton";
|
| 12 |
|
| 13 |
interface ConceptSelectorProps {
|
| 14 |
onSelect?: (concept: ConceptInfo) => void;
|
|
|
|
| 149 |
return (
|
| 150 |
<Card variant="glass" className="border-2 border-transparent hover:border-cyan-200/50 transition-all duration-300">
|
| 151 |
<CardHeader>
|
| 152 |
+
<div className="flex items-center gap-2">
|
| 153 |
+
<CardTitle className="bg-gradient-to-r from-cyan-600 to-pink-600 bg-clip-text text-transparent">
|
| 154 |
+
Select Concept
|
| 155 |
+
</CardTitle>
|
| 156 |
+
<InfoButton
|
| 157 |
+
title="What is a Concept?"
|
| 158 |
+
content="A concept is the creative execution style or storyline you use to deliver your angle. It defines how your ad will look and feel visually.
|
| 159 |
+
|
| 160 |
+
Concepts include things like:
|
| 161 |
+
- Before/After comparisons
|
| 162 |
+
- Testimonials
|
| 163 |
+
- Problem/Solution narratives
|
| 164 |
+
- Visual metaphors
|
| 165 |
+
- Lifestyle imagery
|
| 166 |
+
|
| 167 |
+
Each concept has a specific structure and visual direction. When combined with an angle, they create a powerful ad that both hooks attention and drives action. Some concepts work better with certain angles - use the 'Show compatible concepts only' option to see recommended pairings."
|
| 168 |
+
position="bottom"
|
| 169 |
+
/>
|
| 170 |
+
</div>
|
| 171 |
{angleKey && (
|
| 172 |
<div className="mt-2">
|
| 173 |
<label className="flex items-center space-x-2">
|
frontend/components/ui/InfoButton.tsx
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client";
|
| 2 |
+
|
| 3 |
+
import React, { useState, useRef, useEffect } from "react";
|
| 4 |
+
import { Info } from "lucide-react";
|
| 5 |
+
import { cn } from "@/lib/utils/cn";
|
| 6 |
+
|
| 7 |
+
interface InfoButtonProps {
|
| 8 |
+
content: string | React.ReactNode;
|
| 9 |
+
title?: string;
|
| 10 |
+
position?: "top" | "bottom" | "left" | "right";
|
| 11 |
+
className?: string;
|
| 12 |
+
/** Smaller, more subtle variant for inline use next to labels */
|
| 13 |
+
variant?: "default" | "subtle";
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
export const InfoButton: React.FC<InfoButtonProps> = ({
|
| 17 |
+
content,
|
| 18 |
+
title,
|
| 19 |
+
position = "top",
|
| 20 |
+
className = "",
|
| 21 |
+
variant = "subtle",
|
| 22 |
+
}) => {
|
| 23 |
+
const [isOpen, setIsOpen] = useState(false);
|
| 24 |
+
const buttonRef = useRef<HTMLButtonElement>(null);
|
| 25 |
+
const tooltipRef = useRef<HTMLDivElement>(null);
|
| 26 |
+
|
| 27 |
+
useEffect(() => {
|
| 28 |
+
const handleClickOutside = (event: MouseEvent) => {
|
| 29 |
+
if (
|
| 30 |
+
tooltipRef.current &&
|
| 31 |
+
buttonRef.current &&
|
| 32 |
+
!tooltipRef.current.contains(event.target as Node) &&
|
| 33 |
+
!buttonRef.current.contains(event.target as Node)
|
| 34 |
+
) {
|
| 35 |
+
setIsOpen(false);
|
| 36 |
+
}
|
| 37 |
+
};
|
| 38 |
+
|
| 39 |
+
if (isOpen) {
|
| 40 |
+
document.addEventListener("mousedown", handleClickOutside);
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
return () => {
|
| 44 |
+
document.removeEventListener("mousedown", handleClickOutside);
|
| 45 |
+
};
|
| 46 |
+
}, [isOpen]);
|
| 47 |
+
|
| 48 |
+
const positionClasses = {
|
| 49 |
+
top: "bottom-full left-1/2 -translate-x-1/2 mb-2",
|
| 50 |
+
bottom: "top-full left-1/2 -translate-x-1/2 mt-2",
|
| 51 |
+
left: "right-full top-1/2 -translate-y-1/2 mr-2",
|
| 52 |
+
right: "left-full top-1/2 -translate-y-1/2 ml-2",
|
| 53 |
+
};
|
| 54 |
+
|
| 55 |
+
const isBottom = position === "bottom";
|
| 56 |
+
const isTop = position === "top";
|
| 57 |
+
|
| 58 |
+
return (
|
| 59 |
+
<span className={cn("relative inline-flex", className)}>
|
| 60 |
+
<button
|
| 61 |
+
ref={buttonRef}
|
| 62 |
+
type="button"
|
| 63 |
+
onClick={(e) => {
|
| 64 |
+
e.preventDefault();
|
| 65 |
+
e.stopPropagation();
|
| 66 |
+
setIsOpen(!isOpen);
|
| 67 |
+
}}
|
| 68 |
+
className={cn(
|
| 69 |
+
"inline-flex items-center justify-center rounded-full transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-blue-400/50 focus:ring-offset-1",
|
| 70 |
+
variant === "subtle"
|
| 71 |
+
? "w-4 h-4 text-gray-400 hover:text-gray-600 hover:bg-gray-100"
|
| 72 |
+
: "w-5 h-5 bg-gray-100 text-gray-500 hover:bg-gray-200 hover:text-gray-700"
|
| 73 |
+
)}
|
| 74 |
+
aria-label="Show information"
|
| 75 |
+
>
|
| 76 |
+
<Info className="w-3 h-3" strokeWidth={2.25} />
|
| 77 |
+
</button>
|
| 78 |
+
|
| 79 |
+
{isOpen && (
|
| 80 |
+
<div
|
| 81 |
+
ref={tooltipRef}
|
| 82 |
+
className={cn(
|
| 83 |
+
"absolute z-[100] w-72 sm:w-80 max-w-[calc(100vw-2rem)]",
|
| 84 |
+
positionClasses[position]
|
| 85 |
+
)}
|
| 86 |
+
>
|
| 87 |
+
<div
|
| 88 |
+
className={cn(
|
| 89 |
+
"relative rounded-xl border border-gray-200/90 bg-white/95 shadow-lg backdrop-blur-sm",
|
| 90 |
+
"info-tooltip-enter"
|
| 91 |
+
)}
|
| 92 |
+
>
|
| 93 |
+
<div className="max-h-[70vh] overflow-y-auto rounded-xl p-4">
|
| 94 |
+
{title && (
|
| 95 |
+
<h3 className="text-sm font-semibold text-gray-900 mb-2 pr-6">
|
| 96 |
+
{title}
|
| 97 |
+
</h3>
|
| 98 |
+
)}
|
| 99 |
+
<div className="text-sm leading-relaxed text-gray-600">
|
| 100 |
+
{typeof content === "string" ? (
|
| 101 |
+
<p className="whitespace-pre-line">{content}</p>
|
| 102 |
+
) : (
|
| 103 |
+
content
|
| 104 |
+
)}
|
| 105 |
+
</div>
|
| 106 |
+
</div>
|
| 107 |
+
{/* Arrow */}
|
| 108 |
+
<div
|
| 109 |
+
className={cn(
|
| 110 |
+
"absolute w-2 h-2 rotate-45 border border-gray-200/90 bg-white/95",
|
| 111 |
+
isBottom && "top-0 left-1/2 -translate-x-1/2 -translate-y-px border-t-transparent border-l-transparent",
|
| 112 |
+
isTop && "bottom-0 left-1/2 -translate-x-1/2 translate-y-px border-b-transparent border-r-transparent",
|
| 113 |
+
position === "left" && "right-0 top-1/2 -translate-y-1/2 translate-x-px border-r-transparent border-b-transparent",
|
| 114 |
+
position === "right" && "left-0 top-1/2 -translate-y-1/2 -translate-x-px border-l-transparent border-t-transparent"
|
| 115 |
+
)}
|
| 116 |
+
/>
|
| 117 |
+
</div>
|
| 118 |
+
</div>
|
| 119 |
+
)}
|
| 120 |
+
</span>
|
| 121 |
+
);
|
| 122 |
+
}
|
main.py
CHANGED
|
@@ -1816,7 +1816,7 @@ async def motivator_generate_endpoint(
|
|
| 1816 |
class ExtensiveGenerateRequest(BaseModel):
|
| 1817 |
"""Request for extensive generation."""
|
| 1818 |
niche: str = Field(
|
| 1819 |
-
description="Target niche: home_insurance, glp1, or others"
|
| 1820 |
)
|
| 1821 |
custom_niche: Optional[str] = Field(
|
| 1822 |
default=None,
|
|
|
|
| 1816 |
class ExtensiveGenerateRequest(BaseModel):
|
| 1817 |
"""Request for extensive generation."""
|
| 1818 |
niche: str = Field(
|
| 1819 |
+
description="Target niche: home_insurance, glp1, auto_insurance, or others (use custom_niche when others)"
|
| 1820 |
)
|
| 1821 |
custom_niche: Optional[str] = Field(
|
| 1822 |
default=None,
|
services/generator.py
CHANGED
|
@@ -2087,9 +2087,10 @@ CONCEPT: {concept.get('name', 'Custom Concept')}
|
|
| 2087 |
) -> Dict[str, Any]:
|
| 2088 |
"""
|
| 2089 |
Generate ad using extensive: researcher → creative director → designer → copywriter.
|
|
|
|
| 2090 |
|
| 2091 |
Args:
|
| 2092 |
-
niche: Target niche (home_insurance or
|
| 2093 |
target_audience: Optional target audience description
|
| 2094 |
offer: Optional offer to run
|
| 2095 |
num_images: Number of images to generate per strategy
|
|
@@ -2102,12 +2103,13 @@ CONCEPT: {concept.get('name', 'Custom Concept')}
|
|
| 2102 |
if not third_flow_available:
|
| 2103 |
raise ValueError("Extensive service not available")
|
| 2104 |
|
| 2105 |
-
# Map niche names
|
| 2106 |
niche_map = {
|
| 2107 |
"home_insurance": "Home Insurance",
|
| 2108 |
"glp1": "GLP-1",
|
|
|
|
| 2109 |
}
|
| 2110 |
-
niche_display = niche_map.get(niche, niche.title())
|
| 2111 |
|
| 2112 |
# Provide defaults if target_audience or offer are not provided
|
| 2113 |
if not target_audience:
|
|
|
|
| 2087 |
) -> Dict[str, Any]:
|
| 2088 |
"""
|
| 2089 |
Generate ad using extensive: researcher → creative director → designer → copywriter.
|
| 2090 |
+
Works for any niche: home_insurance, glp1, auto_insurance, or custom (e.g. from 'others').
|
| 2091 |
|
| 2092 |
Args:
|
| 2093 |
+
niche: Target niche (home_insurance, glp1, auto_insurance, or custom display name when 'others')
|
| 2094 |
target_audience: Optional target audience description
|
| 2095 |
offer: Optional offer to run
|
| 2096 |
num_images: Number of images to generate per strategy
|
|
|
|
| 2103 |
if not third_flow_available:
|
| 2104 |
raise ValueError("Extensive service not available")
|
| 2105 |
|
| 2106 |
+
# Map known niche keys to display names; custom niches (e.g. from 'others') pass through as-is
|
| 2107 |
niche_map = {
|
| 2108 |
"home_insurance": "Home Insurance",
|
| 2109 |
"glp1": "GLP-1",
|
| 2110 |
+
"auto_insurance": "Auto Insurance",
|
| 2111 |
}
|
| 2112 |
+
niche_display = niche_map.get(niche, niche.replace("_", " ").title())
|
| 2113 |
|
| 2114 |
# Provide defaults if target_audience or offer are not provided
|
| 2115 |
if not target_audience:
|
services/third_flow.py
CHANGED
|
@@ -13,6 +13,7 @@ from pydantic import BaseModel
|
|
| 13 |
# Add parent directory to path for imports
|
| 14 |
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
| 15 |
|
|
|
|
| 16 |
from openai import OpenAI
|
| 17 |
from config import settings
|
| 18 |
|
|
@@ -44,6 +45,7 @@ class CreativeStrategies(BaseModel):
|
|
| 44 |
titleIdeas: str
|
| 45 |
captionIdeas: str
|
| 46 |
bodyIdeas: str
|
|
|
|
| 47 |
|
| 48 |
|
| 49 |
class CreativeStrategiesOutput(BaseModel):
|
|
@@ -78,12 +80,12 @@ class ThirdFlowService:
|
|
| 78 |
) -> List[ImageAdEssentials]:
|
| 79 |
"""
|
| 80 |
Research psychology triggers, angles, and concepts.
|
| 81 |
-
|
| 82 |
Args:
|
| 83 |
target_audience: Target audience description
|
| 84 |
offer: Offer to run
|
| 85 |
niche: Niche category
|
| 86 |
-
|
| 87 |
Returns:
|
| 88 |
List of ImageAdEssentials with psychology triggers, angles, and concepts
|
| 89 |
"""
|
|
@@ -94,14 +96,14 @@ class ThirdFlowService:
|
|
| 94 |
{
|
| 95 |
"type": "text",
|
| 96 |
"text": """You are the researcher for the affiliate marketing company which does research on trending angles, concepts and psychology triggers based on the user input.
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
}
|
| 106 |
]
|
| 107 |
},
|
|
@@ -111,23 +113,23 @@ class ThirdFlowService:
|
|
| 111 |
{
|
| 112 |
"type": "text",
|
| 113 |
"text": f"""Following are the inputs:
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
}
|
| 120 |
]
|
| 121 |
}
|
| 122 |
]
|
| 123 |
-
|
| 124 |
try:
|
| 125 |
completion = self.client.beta.chat.completions.parse(
|
| 126 |
model=self.gpt_model,
|
| 127 |
messages=messages,
|
| 128 |
response_format=ImageAdEssentialsOutput,
|
| 129 |
)
|
| 130 |
-
|
| 131 |
response = completion.choices[0].message
|
| 132 |
if response.parsed:
|
| 133 |
return response.parsed.output
|
|
@@ -138,7 +140,41 @@ class ThirdFlowService:
|
|
| 138 |
except Exception as e:
|
| 139 |
print(f"Error in researcher: {e}")
|
| 140 |
return []
|
| 141 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 142 |
def retrieve_search(
|
| 143 |
self,
|
| 144 |
target_audience: str,
|
|
@@ -446,8 +482,8 @@ class ThirdFlowService:
|
|
| 446 |
except Exception as e:
|
| 447 |
print(f"Error in creative_director: {e}")
|
| 448 |
return []
|
| 449 |
-
|
| 450 |
-
def creative_designer(self, creative_strategy: CreativeStrategies, niche: str = "Home Insurance") -> str:
|
| 451 |
"""
|
| 452 |
Generate image prompt from creative strategy.
|
| 453 |
|
|
@@ -474,37 +510,26 @@ PROPS TO INCLUDE: {', '.join(niche_guidance_data.get('props', []))}
|
|
| 474 |
AVOID: {', '.join(niche_guidance_data.get('avoid', []))}
|
| 475 |
COLOR PREFERENCE: {niche_guidance_data.get('color_preference', 'balanced')}
|
| 476 |
|
| 477 |
-
CRITICAL: The image MUST be appropriate for {niche} niche.
|
| 478 |
-
|
| 479 |
-
niche_guidance = """
|
| 480 |
-
NICHE-SPECIFIC REQUIREMENTS (Home Insurance):
|
| 481 |
-
SUBJECTS TO INCLUDE: family in front of home, house exterior, homeowner looking confident, couple reviewing papers
|
| 482 |
-
PROPS TO INCLUDE: insurance documents, house keys, tablet showing coverage, family photos
|
| 483 |
-
AVOID: disasters, fire or floods, stressed expressions, dark settings
|
| 484 |
-
COLOR PREFERENCE: trust
|
| 485 |
-
|
| 486 |
-
CRITICAL: The image MUST show home insurance-related content. Show REAL American suburban homes, homeowners, and insurance-related elements."""
|
| 487 |
-
elif niche_lower == "glp1":
|
| 488 |
-
niche_guidance = """
|
| 489 |
-
NICHE-SPECIFIC REQUIREMENTS (GLP-1):
|
| 490 |
-
SUBJECTS TO INCLUDE: confident person smiling (age 30-50), active lifestyle scenes with adults, healthy meal preparation, doctor consultation
|
| 491 |
-
PROPS TO INCLUDE: fitness equipment, healthy food, comfortable clothing
|
| 492 |
-
AVOID: before/after weight comparisons, measuring tapes, scales prominently, needle close-ups, elderly people over 65, senior citizens, very old looking people, gray-haired elderly groups
|
| 493 |
-
AGE GUIDANCE: Show people aged 30-50 primarily. DO NOT default to elderly/senior citizens. The target audience is middle-aged adults in their 30s-40s, NOT seniors or elderly people.
|
| 494 |
-
COLOR PREFERENCE: health
|
| 495 |
-
|
| 496 |
-
CRITICAL: The image MUST be appropriate for GLP-1/weight loss niche. Show lifestyle, health, and confidence-related content. People in images should look 30-50 years old, NOT elderly."""
|
| 497 |
else:
|
| 498 |
niche_guidance = f"""
|
| 499 |
-
NICHE-SPECIFIC REQUIREMENTS ({niche}):
|
| 500 |
-
CRITICAL: The image MUST be appropriate for {niche} niche.
|
| 501 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 502 |
strategy_str = f"""Psychology Trigger: {creative_strategy.phsychologyTrigger}
|
| 503 |
Angle: {creative_strategy.angle}
|
| 504 |
Concept: {creative_strategy.concept}
|
| 505 |
-
Text: {creative_strategy.text.textToBeWrittern if creative_strategy.text.textToBeWrittern not in [None, 'None', 'NA'] else 'No text overlay'}
|
| 506 |
CTA: {creative_strategy.cta}
|
| 507 |
Visual Direction: {creative_strategy.visualDirection}
|
|
|
|
| 508 |
"""
|
| 509 |
|
| 510 |
messages = [
|
|
@@ -614,8 +639,8 @@ CRITICAL: The image MUST be appropriate for {niche} niche."""
|
|
| 614 |
prompt += " Authentic, relatable style - not overly polished or commercial."
|
| 615 |
|
| 616 |
return prompt.strip()
|
| 617 |
-
|
| 618 |
-
def copy_writer(self, creative_strategy: CreativeStrategies) -> CopyWriterOutput:
|
| 619 |
"""
|
| 620 |
Generate ad copy from creative strategy.
|
| 621 |
|
|
@@ -625,13 +650,18 @@ CRITICAL: The image MUST be appropriate for {niche} niche."""
|
|
| 625 |
Returns:
|
| 626 |
CopyWriterOutput with title, body, and description
|
| 627 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 628 |
strategy_str = f"""Psychology Trigger: {creative_strategy.phsychologyTrigger}
|
| 629 |
Angle: {creative_strategy.angle}
|
| 630 |
Concept: {creative_strategy.concept}
|
| 631 |
CTA: {creative_strategy.cta}
|
| 632 |
-
|
| 633 |
-
Caption Ideas: {creative_strategy.captionIdeas}
|
| 634 |
-
Body Ideas: {creative_strategy.bodyIdeas}
|
| 635 |
"""
|
| 636 |
|
| 637 |
messages = [
|
|
@@ -710,7 +740,8 @@ CRITICAL: The image MUST be appropriate for {niche} niche."""
|
|
| 710 |
def process_strategy(
|
| 711 |
self,
|
| 712 |
creative_strategy: CreativeStrategies,
|
| 713 |
-
niche: str = "Home Insurance"
|
|
|
|
| 714 |
) -> tuple[str, str, str, str]:
|
| 715 |
"""
|
| 716 |
Process a single creative strategy to generate prompt and copy.
|
|
@@ -722,8 +753,9 @@ CRITICAL: The image MUST be appropriate for {niche} niche."""
|
|
| 722 |
Returns:
|
| 723 |
Tuple of (prompt, title, body, description)
|
| 724 |
"""
|
| 725 |
-
prompt = self.creative_designer(creative_strategy,
|
| 726 |
-
ad_copy = self.copy_writer(creative_strategy)
|
|
|
|
| 727 |
return (
|
| 728 |
prompt,
|
| 729 |
ad_copy.title,
|
|
@@ -734,3 +766,58 @@ CRITICAL: The image MUST be appropriate for {niche} niche."""
|
|
| 734 |
|
| 735 |
# Global service instance
|
| 736 |
third_flow_service = ThirdFlowService()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
# Add parent directory to path for imports
|
| 14 |
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
| 15 |
|
| 16 |
+
from services.motivator import generate_motivators
|
| 17 |
from openai import OpenAI
|
| 18 |
from config import settings
|
| 19 |
|
|
|
|
| 45 |
titleIdeas: str
|
| 46 |
captionIdeas: str
|
| 47 |
bodyIdeas: str
|
| 48 |
+
motivators: list[str] | None = None
|
| 49 |
|
| 50 |
|
| 51 |
class CreativeStrategiesOutput(BaseModel):
|
|
|
|
| 80 |
) -> List[ImageAdEssentials]:
|
| 81 |
"""
|
| 82 |
Research psychology triggers, angles, and concepts.
|
| 83 |
+
|
| 84 |
Args:
|
| 85 |
target_audience: Target audience description
|
| 86 |
offer: Offer to run
|
| 87 |
niche: Niche category
|
| 88 |
+
|
| 89 |
Returns:
|
| 90 |
List of ImageAdEssentials with psychology triggers, angles, and concepts
|
| 91 |
"""
|
|
|
|
| 96 |
{
|
| 97 |
"type": "text",
|
| 98 |
"text": """You are the researcher for the affiliate marketing company which does research on trending angles, concepts and psychology triggers based on the user input.
|
| 99 |
+
Affiliate marketing is a performance-based model where you promote someone else's product or service and earn a commission for each qualified action (click, lead, or sale).
|
| 100 |
+
A psychology trigger is an emotional or cognitive stimulus that pushes someone toward action—clicking, signing up, or buying—before logic kicks in.
|
| 101 |
+
An ad angle is the reason someone should care right now. Same product → different reasons to click → different angles.
|
| 102 |
+
An ad concept is the creative execution style or storyline you use to deliver an angle.
|
| 103 |
+
In affiliate marketing 'Low-production, realistic often outperform studio creatives' runs most.
|
| 104 |
+
|
| 105 |
+
Keeping in mind all this, make sure you provide different angles and concepts we can try based on the psychology triggers for the image ads for the given input based on affiliate marketing.
|
| 106 |
+
User will provide you the category on which he needs to run the ads, what is the offer he is providing and what is target audience."""
|
| 107 |
}
|
| 108 |
]
|
| 109 |
},
|
|
|
|
| 113 |
{
|
| 114 |
"type": "text",
|
| 115 |
"text": f"""Following are the inputs:
|
| 116 |
+
Niche: {niche}
|
| 117 |
+
Offer to run: {offer}
|
| 118 |
+
Target Audience: {target_audience}
|
| 119 |
+
|
| 120 |
+
Provide the different psychology triggers, angles and concept based on the given input."""
|
| 121 |
}
|
| 122 |
]
|
| 123 |
}
|
| 124 |
]
|
| 125 |
+
|
| 126 |
try:
|
| 127 |
completion = self.client.beta.chat.completions.parse(
|
| 128 |
model=self.gpt_model,
|
| 129 |
messages=messages,
|
| 130 |
response_format=ImageAdEssentialsOutput,
|
| 131 |
)
|
| 132 |
+
|
| 133 |
response = completion.choices[0].message
|
| 134 |
if response.parsed:
|
| 135 |
return response.parsed.output
|
|
|
|
| 140 |
except Exception as e:
|
| 141 |
print(f"Error in researcher: {e}")
|
| 142 |
return []
|
| 143 |
+
|
| 144 |
+
async def generate_motivators_for_strategy(
|
| 145 |
+
self,
|
| 146 |
+
strategy: CreativeStrategies,
|
| 147 |
+
niche: str,
|
| 148 |
+
target_audience: str | None,
|
| 149 |
+
offer: str | None,
|
| 150 |
+
count: int = 6,
|
| 151 |
+
) -> list[str]:
|
| 152 |
+
angle_ctx = {
|
| 153 |
+
"name": strategy.angle,
|
| 154 |
+
"trigger": strategy.phsychologyTrigger,
|
| 155 |
+
"example": strategy.titleIdeas,
|
| 156 |
+
}
|
| 157 |
+
print(f"Generating angle {angle_ctx}")
|
| 158 |
+
|
| 159 |
+
concept_ctx = {
|
| 160 |
+
"name": strategy.concept,
|
| 161 |
+
"structure": strategy.visualDirection,
|
| 162 |
+
"visual": strategy.visualDirection,
|
| 163 |
+
}
|
| 164 |
+
print(f"Generating concept {concept_ctx}")
|
| 165 |
+
|
| 166 |
+
motivators = await generate_motivators(
|
| 167 |
+
niche=niche.lower().replace(" ", "_"),
|
| 168 |
+
angle=angle_ctx,
|
| 169 |
+
concept=concept_ctx,
|
| 170 |
+
target_audience=target_audience,
|
| 171 |
+
offer=offer,
|
| 172 |
+
count=count,
|
| 173 |
+
)
|
| 174 |
+
print(f"Generated motivators: {motivators}")
|
| 175 |
+
|
| 176 |
+
return motivators
|
| 177 |
+
|
| 178 |
def retrieve_search(
|
| 179 |
self,
|
| 180 |
target_audience: str,
|
|
|
|
| 482 |
except Exception as e:
|
| 483 |
print(f"Error in creative_director: {e}")
|
| 484 |
return []
|
| 485 |
+
|
| 486 |
+
def creative_designer(self, creative_strategy: CreativeStrategies, niche: str = "Home Insurance",selected_motivator: str | None = None) -> str:
|
| 487 |
"""
|
| 488 |
Generate image prompt from creative strategy.
|
| 489 |
|
|
|
|
| 510 |
AVOID: {', '.join(niche_guidance_data.get('avoid', []))}
|
| 511 |
COLOR PREFERENCE: {niche_guidance_data.get('color_preference', 'balanced')}
|
| 512 |
|
| 513 |
+
CRITICAL: The image MUST be appropriate for {niche} niche.
|
| 514 |
+
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 515 |
else:
|
| 516 |
niche_guidance = f"""
|
| 517 |
+
NICHE-SPECIFIC REQUIREMENTS ({niche}):
|
| 518 |
+
CRITICAL: The image MUST be appropriate for {niche} niche.
|
| 519 |
+
"""
|
| 520 |
+
|
| 521 |
+
motivator_block = (
|
| 522 |
+
f"\nCORE MOTIVATOR (emotional driver): {selected_motivator}\n"
|
| 523 |
+
if selected_motivator
|
| 524 |
+
else ""
|
| 525 |
+
)
|
| 526 |
+
|
| 527 |
strategy_str = f"""Psychology Trigger: {creative_strategy.phsychologyTrigger}
|
| 528 |
Angle: {creative_strategy.angle}
|
| 529 |
Concept: {creative_strategy.concept}
|
|
|
|
| 530 |
CTA: {creative_strategy.cta}
|
| 531 |
Visual Direction: {creative_strategy.visualDirection}
|
| 532 |
+
{motivator_block}
|
| 533 |
"""
|
| 534 |
|
| 535 |
messages = [
|
|
|
|
| 639 |
prompt += " Authentic, relatable style - not overly polished or commercial."
|
| 640 |
|
| 641 |
return prompt.strip()
|
| 642 |
+
|
| 643 |
+
def copy_writer(self, creative_strategy: CreativeStrategies,selected_motivator: str | None = None) -> CopyWriterOutput:
|
| 644 |
"""
|
| 645 |
Generate ad copy from creative strategy.
|
| 646 |
|
|
|
|
| 650 |
Returns:
|
| 651 |
CopyWriterOutput with title, body, and description
|
| 652 |
"""
|
| 653 |
+
|
| 654 |
+
motivator_block = (
|
| 655 |
+
f"\nCORE MOTIVATOR (customer’s internal voice): {selected_motivator}\n"
|
| 656 |
+
if selected_motivator
|
| 657 |
+
else ""
|
| 658 |
+
)
|
| 659 |
+
|
| 660 |
strategy_str = f"""Psychology Trigger: {creative_strategy.phsychologyTrigger}
|
| 661 |
Angle: {creative_strategy.angle}
|
| 662 |
Concept: {creative_strategy.concept}
|
| 663 |
CTA: {creative_strategy.cta}
|
| 664 |
+
{motivator_block}
|
|
|
|
|
|
|
| 665 |
"""
|
| 666 |
|
| 667 |
messages = [
|
|
|
|
| 740 |
def process_strategy(
|
| 741 |
self,
|
| 742 |
creative_strategy: CreativeStrategies,
|
| 743 |
+
niche: str = "Home Insurance",
|
| 744 |
+
selected_motivator: str | None = None,
|
| 745 |
) -> tuple[str, str, str, str]:
|
| 746 |
"""
|
| 747 |
Process a single creative strategy to generate prompt and copy.
|
|
|
|
| 753 |
Returns:
|
| 754 |
Tuple of (prompt, title, body, description)
|
| 755 |
"""
|
| 756 |
+
prompt = self.creative_designer(creative_strategy,niche=niche,selected_motivator=selected_motivator)
|
| 757 |
+
ad_copy = self.copy_writer(creative_strategy,selected_motivator=selected_motivator)
|
| 758 |
+
|
| 759 |
return (
|
| 760 |
prompt,
|
| 761 |
ad_copy.title,
|
|
|
|
| 766 |
|
| 767 |
# Global service instance
|
| 768 |
third_flow_service = ThirdFlowService()
|
| 769 |
+
|
| 770 |
+
if __name__ == "__main__":
|
| 771 |
+
import asyncio
|
| 772 |
+
|
| 773 |
+
# ---- MOCK STRATEGY FOR TESTING ----
|
| 774 |
+
test_strategy = CreativeStrategies(
|
| 775 |
+
phsychologyTrigger="Fear of unexpected financial loss",
|
| 776 |
+
angle="Your home could cost you thousands tomorrow",
|
| 777 |
+
concept="Before vs After comparison",
|
| 778 |
+
text=Text(
|
| 779 |
+
textToBeWrittern="One small issue can turn into a massive repair bill.",
|
| 780 |
+
color="white",
|
| 781 |
+
placement="top"
|
| 782 |
+
),
|
| 783 |
+
cta="Check your coverage now",
|
| 784 |
+
visualDirection="Split image: damaged home vs peaceful protected home",
|
| 785 |
+
titleIdeas="This mistake costs homeowners $12,000",
|
| 786 |
+
captionIdeas="Most homeowners don’t realize this until it’s too late",
|
| 787 |
+
bodyIdeas="A real homeowner story about unexpected damage",
|
| 788 |
+
)
|
| 789 |
+
|
| 790 |
+
async def run_test():
|
| 791 |
+
print("\n========== TEST: GENERATE MOTIVATORS ==========\n")
|
| 792 |
+
motivators = await third_flow_service.generate_motivators_for_strategy(
|
| 793 |
+
strategy=test_strategy,
|
| 794 |
+
niche="home_insurance",
|
| 795 |
+
target_audience="US homeowners 35–65",
|
| 796 |
+
offer="Free home insurance quote",
|
| 797 |
+
)
|
| 798 |
+
|
| 799 |
+
print(motivators)
|
| 800 |
+
|
| 801 |
+
selected_motivator = motivators[0] if motivators else None
|
| 802 |
+
|
| 803 |
+
print("\n========== TEST: PROCESS STRATEGY ==========\n")
|
| 804 |
+
prompt, title, body, description = third_flow_service.process_strategy(
|
| 805 |
+
creative_strategy=test_strategy,
|
| 806 |
+
niche="Home Insurance",
|
| 807 |
+
selected_motivator=selected_motivator,
|
| 808 |
+
)
|
| 809 |
+
|
| 810 |
+
print("\n--- IMAGE PROMPT ---\n")
|
| 811 |
+
print(prompt)
|
| 812 |
+
|
| 813 |
+
print("\n--- TITLE ---\n")
|
| 814 |
+
print(title)
|
| 815 |
+
|
| 816 |
+
print("\n--- BODY ---\n")
|
| 817 |
+
print(body)
|
| 818 |
+
|
| 819 |
+
print("\n--- DESCRIPTION ---\n")
|
| 820 |
+
print(description)
|
| 821 |
+
|
| 822 |
+
asyncio.run(run_test())
|
| 823 |
+
|