Spaces:
Sleeping
Sleeping
Commit ·
68556d7
1
Parent(s): ff98a02
Add custom angle and concept refinement features
Browse files- Introduced custom angle and concept fields in the MatrixGenerateRequest model, allowing users to provide personalized inputs for ad generation.
- Implemented a new RefineCustomRequest model and corresponding API endpoint to refine user-provided angles and concepts using AI, enhancing the ad generation process.
- Updated the frontend to support custom angle and concept inputs, including state management for refining these inputs and displaying refined results.
- Refactored the generator service to handle custom angles and concepts, ensuring proper integration with existing ad generation workflows.
- Enhanced type definitions and API endpoints to accommodate the new features, ensuring type safety and consistency across the application.
- frontend/app/generate/matrix/page.tsx +0 -232
- frontend/app/generate/page.tsx +329 -20
- frontend/app/matrix/page.tsx +4 -28
- frontend/lib/api/endpoints.ts +13 -0
- frontend/store/matrixStore.ts +69 -0
- frontend/types/api.ts +36 -0
- main.py +98 -0
- services/generator.py +208 -15
frontend/app/generate/matrix/page.tsx
DELETED
|
@@ -1,232 +0,0 @@
|
|
| 1 |
-
"use client";
|
| 2 |
-
|
| 3 |
-
import React, { useState, useEffect } 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 |
-
generationStartTime,
|
| 26 |
-
setCurrentGeneration,
|
| 27 |
-
setProgress,
|
| 28 |
-
setIsGenerating,
|
| 29 |
-
setError,
|
| 30 |
-
setGenerationStartTime,
|
| 31 |
-
reset,
|
| 32 |
-
} = useGenerationStore();
|
| 33 |
-
|
| 34 |
-
const { selectedAngle, selectedConcept, setSelectedAngle, setSelectedConcept } = useMatrixStore();
|
| 35 |
-
|
| 36 |
-
const [niche, setNiche] = useState<Niche>("home_insurance");
|
| 37 |
-
const [numImages, setNumImages] = useState(1);
|
| 38 |
-
const [imageModel, setImageModel] = useState<string | null>(null);
|
| 39 |
-
|
| 40 |
-
// Request notification permission and show notification when generation completes
|
| 41 |
-
const showNotification = (title: string, body: string) => {
|
| 42 |
-
if ("Notification" in window && Notification.permission === "granted") {
|
| 43 |
-
new Notification(title, {
|
| 44 |
-
body,
|
| 45 |
-
icon: "/favicon.ico",
|
| 46 |
-
tag: "generation-complete",
|
| 47 |
-
});
|
| 48 |
-
}
|
| 49 |
-
};
|
| 50 |
-
|
| 51 |
-
// Request notification permission on mount
|
| 52 |
-
useEffect(() => {
|
| 53 |
-
if ("Notification" in window && Notification.permission === "default") {
|
| 54 |
-
Notification.requestPermission();
|
| 55 |
-
}
|
| 56 |
-
}, []);
|
| 57 |
-
|
| 58 |
-
// Show notification when generation completes
|
| 59 |
-
useEffect(() => {
|
| 60 |
-
if (progress.step === "complete" && currentGeneration) {
|
| 61 |
-
showNotification("Ad Generated Successfully!", "Your ad is ready to view.");
|
| 62 |
-
}
|
| 63 |
-
}, [progress.step, currentGeneration]);
|
| 64 |
-
|
| 65 |
-
const handleGenerate = async () => {
|
| 66 |
-
if (!selectedAngle || !selectedConcept) {
|
| 67 |
-
toast.error("Please select both an angle and a concept");
|
| 68 |
-
return;
|
| 69 |
-
}
|
| 70 |
-
|
| 71 |
-
reset();
|
| 72 |
-
setIsGenerating(true);
|
| 73 |
-
setGenerationStartTime(Date.now());
|
| 74 |
-
setProgress({
|
| 75 |
-
step: "copy",
|
| 76 |
-
progress: 10,
|
| 77 |
-
message: "Generating ad with selected angle and concept...",
|
| 78 |
-
});
|
| 79 |
-
|
| 80 |
-
try {
|
| 81 |
-
const result = await generateMatrixAd({
|
| 82 |
-
niche,
|
| 83 |
-
angle_key: selectedAngle.key,
|
| 84 |
-
concept_key: selectedConcept.key,
|
| 85 |
-
num_images: numImages,
|
| 86 |
-
image_model: imageModel,
|
| 87 |
-
});
|
| 88 |
-
|
| 89 |
-
setCurrentGeneration(result);
|
| 90 |
-
setProgress({
|
| 91 |
-
step: "complete",
|
| 92 |
-
progress: 100,
|
| 93 |
-
message: "Ad generated successfully!",
|
| 94 |
-
});
|
| 95 |
-
|
| 96 |
-
toast.success("Ad generated successfully!");
|
| 97 |
-
} catch (error: any) {
|
| 98 |
-
setError(error.message || "Failed to generate ad");
|
| 99 |
-
setProgress({
|
| 100 |
-
step: "error",
|
| 101 |
-
progress: 0,
|
| 102 |
-
message: error.message || "An error occurred",
|
| 103 |
-
});
|
| 104 |
-
toast.error(error.message || "Failed to generate ad");
|
| 105 |
-
} finally {
|
| 106 |
-
setIsGenerating(false);
|
| 107 |
-
}
|
| 108 |
-
};
|
| 109 |
-
|
| 110 |
-
return (
|
| 111 |
-
<div className="min-h-screen pb-12">
|
| 112 |
-
{/* Hero Section */}
|
| 113 |
-
<div className="relative overflow-hidden bg-gradient-to-br from-blue-50 via-cyan-50 to-pink-50 py-12 mb-8">
|
| 114 |
-
<div className="absolute inset-0 bg-grid-pattern opacity-5"></div>
|
| 115 |
-
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
|
| 116 |
-
<div className="text-center animate-fade-in">
|
| 117 |
-
<h1 className="text-4xl md:text-5xl font-extrabold mb-4">
|
| 118 |
-
<span className="gradient-text">Matrix</span>
|
| 119 |
-
<span className="text-gray-900"> Generation</span>
|
| 120 |
-
</h1>
|
| 121 |
-
<p className="text-lg text-gray-600 max-w-2xl mx-auto">
|
| 122 |
-
Generate ads using specific angle × concept combinations
|
| 123 |
-
</p>
|
| 124 |
-
</div>
|
| 125 |
-
</div>
|
| 126 |
-
</div>
|
| 127 |
-
|
| 128 |
-
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
| 129 |
-
{/* Progress Component - Sticky at top */}
|
| 130 |
-
{isGenerating && (
|
| 131 |
-
<GenerationProgressComponent progress={progress} generationStartTime={generationStartTime} />
|
| 132 |
-
)}
|
| 133 |
-
|
| 134 |
-
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
| 135 |
-
{/* Left Column - Selection */}
|
| 136 |
-
<div className="lg:col-span-1 space-y-6">
|
| 137 |
-
<Card variant="glass" className="animate-slide-in border-2 border-transparent hover:border-blue-200/50 transition-all duration-300">
|
| 138 |
-
<CardHeader>
|
| 139 |
-
<CardTitle className="bg-gradient-to-r from-blue-600 to-cyan-600 bg-clip-text text-transparent">
|
| 140 |
-
Configuration
|
| 141 |
-
</CardTitle>
|
| 142 |
-
</CardHeader>
|
| 143 |
-
<CardContent className="space-y-4">
|
| 144 |
-
<div>
|
| 145 |
-
<label className="block text-sm font-semibold text-gray-700 mb-2">Niche</label>
|
| 146 |
-
<select
|
| 147 |
-
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 hover:border-blue-300"
|
| 148 |
-
value={niche}
|
| 149 |
-
onChange={(e) => setNiche(e.target.value as Niche)}
|
| 150 |
-
>
|
| 151 |
-
<option value="home_insurance">Home Insurance</option>
|
| 152 |
-
<option value="glp1">GLP-1</option>
|
| 153 |
-
</select>
|
| 154 |
-
</div>
|
| 155 |
-
|
| 156 |
-
<div>
|
| 157 |
-
<label className="block text-sm font-semibold text-gray-700 mb-2">Image Model</label>
|
| 158 |
-
<select
|
| 159 |
-
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-cyan-500 focus:border-cyan-500 transition-all duration-250 hover:border-cyan-300"
|
| 160 |
-
value={imageModel || ""}
|
| 161 |
-
onChange={(e) => setImageModel(e.target.value || null)}
|
| 162 |
-
>
|
| 163 |
-
{IMAGE_MODELS.map((model) => (
|
| 164 |
-
<option key={model.value} value={model.value}>
|
| 165 |
-
{model.label}
|
| 166 |
-
</option>
|
| 167 |
-
))}
|
| 168 |
-
</select>
|
| 169 |
-
</div>
|
| 170 |
-
|
| 171 |
-
<div>
|
| 172 |
-
<label className="block text-sm font-semibold text-gray-700 mb-2">
|
| 173 |
-
Number of Ad Images: <span className="text-cyan-600 font-bold">{numImages}</span>
|
| 174 |
-
</label>
|
| 175 |
-
<input
|
| 176 |
-
type="range"
|
| 177 |
-
min="1"
|
| 178 |
-
max="5"
|
| 179 |
-
step="1"
|
| 180 |
-
className="w-full accent-gradient-to-r from-blue-500 to-cyan-500"
|
| 181 |
-
style={{
|
| 182 |
-
accentColor: '#06b6d4'
|
| 183 |
-
}}
|
| 184 |
-
value={numImages}
|
| 185 |
-
onChange={(e) => setNumImages(Number(e.target.value))}
|
| 186 |
-
/>
|
| 187 |
-
<div className="flex justify-between text-xs text-gray-500 mt-1 font-medium">
|
| 188 |
-
<span>1</span>
|
| 189 |
-
<span>5</span>
|
| 190 |
-
</div>
|
| 191 |
-
</div>
|
| 192 |
-
</CardContent>
|
| 193 |
-
</Card>
|
| 194 |
-
|
| 195 |
-
<AngleSelector
|
| 196 |
-
onSelect={setSelectedAngle}
|
| 197 |
-
selectedAngle={selectedAngle}
|
| 198 |
-
/>
|
| 199 |
-
|
| 200 |
-
<ConceptSelector
|
| 201 |
-
onSelect={setSelectedConcept}
|
| 202 |
-
selectedConcept={selectedConcept}
|
| 203 |
-
angleKey={selectedAngle?.key}
|
| 204 |
-
/>
|
| 205 |
-
|
| 206 |
-
<Button
|
| 207 |
-
variant="primary"
|
| 208 |
-
size="lg"
|
| 209 |
-
className="w-full"
|
| 210 |
-
onClick={handleGenerate}
|
| 211 |
-
isLoading={isGenerating}
|
| 212 |
-
disabled={!selectedAngle || !selectedConcept}
|
| 213 |
-
>
|
| 214 |
-
Generate Ad
|
| 215 |
-
</Button>
|
| 216 |
-
</div>
|
| 217 |
-
|
| 218 |
-
{/* Right Column - Preview */}
|
| 219 |
-
<div className="lg:col-span-2">
|
| 220 |
-
{currentGeneration ? (
|
| 221 |
-
<AdPreview ad={currentGeneration} />
|
| 222 |
-
) : (
|
| 223 |
-
<div className="text-center py-12 text-gray-500">
|
| 224 |
-
<p>Select an angle and concept, then click "Generate Ad"</p>
|
| 225 |
-
</div>
|
| 226 |
-
)}
|
| 227 |
-
</div>
|
| 228 |
-
</div>
|
| 229 |
-
</div>
|
| 230 |
-
</div>
|
| 231 |
-
);
|
| 232 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/app/generate/page.tsx
CHANGED
|
@@ -12,13 +12,13 @@ import { ConceptSelector } from "@/components/matrix/ConceptSelector";
|
|
| 12 |
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/Card";
|
| 13 |
import { Button } from "@/components/ui/Button";
|
| 14 |
import { ProgressBar } from "@/components/ui/ProgressBar";
|
| 15 |
-
import { generateAd, generateMatrixAd, generateBatch, generateExtensiveAd } from "@/lib/api/endpoints";
|
| 16 |
import { useGenerationStore } from "@/store/generationStore";
|
| 17 |
import { useMatrixStore } from "@/store/matrixStore";
|
| 18 |
import { toast } from "react-hot-toast";
|
| 19 |
-
import { Sparkles, Zap, Layers, Package, Workflow } from "lucide-react";
|
| 20 |
import { IMAGE_MODELS } from "@/lib/constants/models";
|
| 21 |
-
import type { Niche, GenerateResponse } from "@/types/api";
|
| 22 |
|
| 23 |
type GenerationMode = "standard" | "matrix" | "batch" | "extensive";
|
| 24 |
|
|
@@ -48,7 +48,33 @@ export default function GeneratePage() {
|
|
| 48 |
reset,
|
| 49 |
} = useGenerationStore();
|
| 50 |
|
| 51 |
-
const {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
|
| 53 |
// Request notification permission and show notification when generation completes
|
| 54 |
const showNotification = (title: string, body: string) => {
|
|
@@ -75,6 +101,68 @@ export default function GeneratePage() {
|
|
| 75 |
}
|
| 76 |
}, [progress.step, currentGeneration]);
|
| 77 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
const handleStandardGenerate = async (data: { niche: Niche; num_images: number; image_model?: string | null; target_audience?: string; offer?: string }) => {
|
| 79 |
reset();
|
| 80 |
setIsGenerating(true);
|
|
@@ -193,8 +281,8 @@ export default function GeneratePage() {
|
|
| 193 |
};
|
| 194 |
|
| 195 |
const handleMatrixGenerate = async () => {
|
| 196 |
-
if (!
|
| 197 |
-
toast.error("Please select both an angle and a concept");
|
| 198 |
return;
|
| 199 |
}
|
| 200 |
|
|
@@ -221,8 +309,10 @@ export default function GeneratePage() {
|
|
| 221 |
Array.from({ length: numImages }, (_, i) =>
|
| 222 |
generateMatrixAd({
|
| 223 |
niche,
|
| 224 |
-
angle_key:
|
| 225 |
-
concept_key:
|
|
|
|
|
|
|
| 226 |
num_images: 1, // Each ad gets 1 image
|
| 227 |
image_model: imageModel,
|
| 228 |
target_audience: targetAudience || undefined,
|
|
@@ -270,8 +360,10 @@ export default function GeneratePage() {
|
|
| 270 |
try {
|
| 271 |
const result = await generateMatrixAd({
|
| 272 |
niche,
|
| 273 |
-
angle_key:
|
| 274 |
-
concept_key:
|
|
|
|
|
|
|
| 275 |
num_images: 1, // Single image
|
| 276 |
image_model: imageModel,
|
| 277 |
target_audience: targetAudience || undefined,
|
|
@@ -725,16 +817,233 @@ export default function GeneratePage() {
|
|
| 725 |
</CardContent>
|
| 726 |
</Card>
|
| 727 |
|
| 728 |
-
|
| 729 |
-
|
| 730 |
-
|
| 731 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 732 |
|
| 733 |
-
|
| 734 |
-
|
| 735 |
-
|
| 736 |
-
|
| 737 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 738 |
|
| 739 |
<Button
|
| 740 |
variant="primary"
|
|
@@ -742,7 +1051,7 @@ export default function GeneratePage() {
|
|
| 742 |
className="w-full"
|
| 743 |
onClick={handleMatrixGenerate}
|
| 744 |
isLoading={isGenerating}
|
| 745 |
-
disabled={!
|
| 746 |
>
|
| 747 |
Generate Ad
|
| 748 |
</Button>
|
|
|
|
| 12 |
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/Card";
|
| 13 |
import { Button } from "@/components/ui/Button";
|
| 14 |
import { ProgressBar } from "@/components/ui/ProgressBar";
|
| 15 |
+
import { generateAd, generateMatrixAd, generateBatch, generateExtensiveAd, refineCustomAngleOrConcept } from "@/lib/api/endpoints";
|
| 16 |
import { useGenerationStore } from "@/store/generationStore";
|
| 17 |
import { useMatrixStore } from "@/store/matrixStore";
|
| 18 |
import { toast } from "react-hot-toast";
|
| 19 |
+
import { Sparkles, Zap, Layers, Package, Workflow, Wand2, Check, Loader2 } from "lucide-react";
|
| 20 |
import { IMAGE_MODELS } from "@/lib/constants/models";
|
| 21 |
+
import type { Niche, GenerateResponse, AngleInfo, ConceptInfo } from "@/types/api";
|
| 22 |
|
| 23 |
type GenerationMode = "standard" | "matrix" | "batch" | "extensive";
|
| 24 |
|
|
|
|
| 48 |
reset,
|
| 49 |
} = useGenerationStore();
|
| 50 |
|
| 51 |
+
const {
|
| 52 |
+
selectedAngle,
|
| 53 |
+
selectedConcept,
|
| 54 |
+
setSelectedAngle,
|
| 55 |
+
setSelectedConcept,
|
| 56 |
+
// Custom angle/concept state
|
| 57 |
+
customAngleText,
|
| 58 |
+
customConceptText,
|
| 59 |
+
customAngleRefined,
|
| 60 |
+
customConceptRefined,
|
| 61 |
+
isRefiningAngle,
|
| 62 |
+
isRefiningConcept,
|
| 63 |
+
useCustomAngle,
|
| 64 |
+
useCustomConcept,
|
| 65 |
+
setCustomAngleText,
|
| 66 |
+
setCustomConceptText,
|
| 67 |
+
setCustomAngleRefined,
|
| 68 |
+
setCustomConceptRefined,
|
| 69 |
+
setIsRefiningAngle,
|
| 70 |
+
setIsRefiningConcept,
|
| 71 |
+
setUseCustomAngle,
|
| 72 |
+
setUseCustomConcept,
|
| 73 |
+
clearCustomAngle,
|
| 74 |
+
clearCustomConcept,
|
| 75 |
+
} = useMatrixStore();
|
| 76 |
+
|
| 77 |
+
const [userGoal, setUserGoal] = useState("");
|
| 78 |
|
| 79 |
// Request notification permission and show notification when generation completes
|
| 80 |
const showNotification = (title: string, body: string) => {
|
|
|
|
| 101 |
}
|
| 102 |
}, [progress.step, currentGeneration]);
|
| 103 |
|
| 104 |
+
// Handle refining custom angle with AI
|
| 105 |
+
const handleRefineAngle = async () => {
|
| 106 |
+
if (!customAngleText.trim()) {
|
| 107 |
+
toast.error("Please enter your custom angle idea");
|
| 108 |
+
return;
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
setIsRefiningAngle(true);
|
| 112 |
+
try {
|
| 113 |
+
const result = await refineCustomAngleOrConcept({
|
| 114 |
+
text: customAngleText,
|
| 115 |
+
type: "angle",
|
| 116 |
+
niche,
|
| 117 |
+
goal: userGoal || undefined,
|
| 118 |
+
});
|
| 119 |
+
|
| 120 |
+
if (result.status === "success" && result.refined) {
|
| 121 |
+
setCustomAngleRefined(result.refined as AngleInfo);
|
| 122 |
+
toast.success("Angle refined successfully!");
|
| 123 |
+
} else {
|
| 124 |
+
toast.error(result.error || "Failed to refine angle");
|
| 125 |
+
}
|
| 126 |
+
} catch (error: any) {
|
| 127 |
+
toast.error(error.message || "Failed to refine angle");
|
| 128 |
+
} finally {
|
| 129 |
+
setIsRefiningAngle(false);
|
| 130 |
+
}
|
| 131 |
+
};
|
| 132 |
+
|
| 133 |
+
// Handle refining custom concept with AI
|
| 134 |
+
const handleRefineConcept = async () => {
|
| 135 |
+
if (!customConceptText.trim()) {
|
| 136 |
+
toast.error("Please enter your custom concept idea");
|
| 137 |
+
return;
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
setIsRefiningConcept(true);
|
| 141 |
+
try {
|
| 142 |
+
const result = await refineCustomAngleOrConcept({
|
| 143 |
+
text: customConceptText,
|
| 144 |
+
type: "concept",
|
| 145 |
+
niche,
|
| 146 |
+
goal: userGoal || undefined,
|
| 147 |
+
});
|
| 148 |
+
|
| 149 |
+
if (result.status === "success" && result.refined) {
|
| 150 |
+
setCustomConceptRefined(result.refined as ConceptInfo);
|
| 151 |
+
toast.success("Concept refined successfully!");
|
| 152 |
+
} else {
|
| 153 |
+
toast.error(result.error || "Failed to refine concept");
|
| 154 |
+
}
|
| 155 |
+
} catch (error: any) {
|
| 156 |
+
toast.error(error.message || "Failed to refine concept");
|
| 157 |
+
} finally {
|
| 158 |
+
setIsRefiningConcept(false);
|
| 159 |
+
}
|
| 160 |
+
};
|
| 161 |
+
|
| 162 |
+
// Get the effective angle and concept (custom or selected)
|
| 163 |
+
const effectiveAngle = useCustomAngle ? customAngleRefined : selectedAngle;
|
| 164 |
+
const effectiveConcept = useCustomConcept ? customConceptRefined : selectedConcept;
|
| 165 |
+
|
| 166 |
const handleStandardGenerate = async (data: { niche: Niche; num_images: number; image_model?: string | null; target_audience?: string; offer?: string }) => {
|
| 167 |
reset();
|
| 168 |
setIsGenerating(true);
|
|
|
|
| 281 |
};
|
| 282 |
|
| 283 |
const handleMatrixGenerate = async () => {
|
| 284 |
+
if (!effectiveAngle || !effectiveConcept) {
|
| 285 |
+
toast.error("Please select or create both an angle and a concept");
|
| 286 |
return;
|
| 287 |
}
|
| 288 |
|
|
|
|
| 309 |
Array.from({ length: numImages }, (_, i) =>
|
| 310 |
generateMatrixAd({
|
| 311 |
niche,
|
| 312 |
+
angle_key: useCustomAngle ? "custom" : effectiveAngle.key,
|
| 313 |
+
concept_key: useCustomConcept ? "custom" : effectiveConcept.key,
|
| 314 |
+
custom_angle: useCustomAngle ? JSON.stringify(effectiveAngle) : null,
|
| 315 |
+
custom_concept: useCustomConcept ? JSON.stringify(effectiveConcept) : null,
|
| 316 |
num_images: 1, // Each ad gets 1 image
|
| 317 |
image_model: imageModel,
|
| 318 |
target_audience: targetAudience || undefined,
|
|
|
|
| 360 |
try {
|
| 361 |
const result = await generateMatrixAd({
|
| 362 |
niche,
|
| 363 |
+
angle_key: useCustomAngle ? "custom" : effectiveAngle.key,
|
| 364 |
+
concept_key: useCustomConcept ? "custom" : effectiveConcept.key,
|
| 365 |
+
custom_angle: useCustomAngle ? JSON.stringify(effectiveAngle) : null,
|
| 366 |
+
custom_concept: useCustomConcept ? JSON.stringify(effectiveConcept) : null,
|
| 367 |
num_images: 1, // Single image
|
| 368 |
image_model: imageModel,
|
| 369 |
target_audience: targetAudience || undefined,
|
|
|
|
| 817 |
</CardContent>
|
| 818 |
</Card>
|
| 819 |
|
| 820 |
+
{/* Custom Angle Input */}
|
| 821 |
+
<Card variant="glass" className="border-2 border-transparent hover:border-purple-200/50 transition-all duration-300">
|
| 822 |
+
<CardHeader className="pb-3">
|
| 823 |
+
<div className="flex items-center justify-between">
|
| 824 |
+
<CardTitle className="bg-gradient-to-r from-purple-600 to-pink-600 bg-clip-text text-transparent text-base">
|
| 825 |
+
Custom Angle (Optional)
|
| 826 |
+
</CardTitle>
|
| 827 |
+
<label className="flex items-center space-x-2 cursor-pointer">
|
| 828 |
+
<input
|
| 829 |
+
type="checkbox"
|
| 830 |
+
checked={useCustomAngle}
|
| 831 |
+
onChange={(e) => {
|
| 832 |
+
setUseCustomAngle(e.target.checked);
|
| 833 |
+
if (!e.target.checked) {
|
| 834 |
+
clearCustomAngle();
|
| 835 |
+
}
|
| 836 |
+
}}
|
| 837 |
+
className="rounded border-gray-300 text-purple-600 focus:ring-purple-500"
|
| 838 |
+
/>
|
| 839 |
+
<span className="text-xs text-gray-600">Use custom</span>
|
| 840 |
+
</label>
|
| 841 |
+
</div>
|
| 842 |
+
</CardHeader>
|
| 843 |
+
{useCustomAngle && (
|
| 844 |
+
<CardContent className="space-y-3 pt-0">
|
| 845 |
+
<div>
|
| 846 |
+
<label className="block text-xs font-medium text-gray-600 mb-1">
|
| 847 |
+
Your angle idea (why should they care?)
|
| 848 |
+
</label>
|
| 849 |
+
<textarea
|
| 850 |
+
className="w-full px-3 py-2 text-sm rounded-lg border-2 border-gray-200 bg-white/80 backdrop-blur-sm focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-purple-500 transition-all duration-250 resize-none"
|
| 851 |
+
rows={2}
|
| 852 |
+
placeholder="e.g., 'Make them fear losing their savings to unexpected home repairs' or 'Appeal to their pride as responsible homeowners'"
|
| 853 |
+
value={customAngleText}
|
| 854 |
+
onChange={(e) => {
|
| 855 |
+
setCustomAngleText(e.target.value);
|
| 856 |
+
setCustomAngleRefined(null);
|
| 857 |
+
}}
|
| 858 |
+
/>
|
| 859 |
+
</div>
|
| 860 |
+
|
| 861 |
+
<Button
|
| 862 |
+
variant="secondary"
|
| 863 |
+
size="sm"
|
| 864 |
+
className="w-full flex items-center justify-center gap-2"
|
| 865 |
+
onClick={handleRefineAngle}
|
| 866 |
+
disabled={!customAngleText.trim() || isRefiningAngle}
|
| 867 |
+
>
|
| 868 |
+
{isRefiningAngle ? (
|
| 869 |
+
<>
|
| 870 |
+
<Loader2 className="w-4 h-4 animate-spin" />
|
| 871 |
+
Refining...
|
| 872 |
+
</>
|
| 873 |
+
) : (
|
| 874 |
+
<>
|
| 875 |
+
<Wand2 className="w-4 h-4" />
|
| 876 |
+
Refine with AI
|
| 877 |
+
</>
|
| 878 |
+
)}
|
| 879 |
+
</Button>
|
| 880 |
+
|
| 881 |
+
{customAngleRefined && (
|
| 882 |
+
<div className="p-3 rounded-lg bg-gradient-to-r from-purple-50 to-pink-50 border border-purple-200">
|
| 883 |
+
<div className="flex items-start justify-between mb-2">
|
| 884 |
+
<h4 className="font-semibold text-purple-700 text-sm">
|
| 885 |
+
{customAngleRefined.name}
|
| 886 |
+
</h4>
|
| 887 |
+
<Check className="w-4 h-4 text-green-500 flex-shrink-0" />
|
| 888 |
+
</div>
|
| 889 |
+
<p className="text-xs text-purple-600 mb-1">
|
| 890 |
+
<span className="font-medium">Trigger:</span> {customAngleRefined.trigger}
|
| 891 |
+
</p>
|
| 892 |
+
<p className="text-xs text-gray-600 italic">
|
| 893 |
+
"{(customAngleRefined as any).example || customAngleRefined.name}"
|
| 894 |
+
</p>
|
| 895 |
+
</div>
|
| 896 |
+
)}
|
| 897 |
+
</CardContent>
|
| 898 |
+
)}
|
| 899 |
+
</Card>
|
| 900 |
+
|
| 901 |
+
{/* Standard Angle Selector - show if not using custom */}
|
| 902 |
+
{!useCustomAngle && (
|
| 903 |
+
<AngleSelector
|
| 904 |
+
onSelect={setSelectedAngle}
|
| 905 |
+
selectedAngle={selectedAngle}
|
| 906 |
+
/>
|
| 907 |
+
)}
|
| 908 |
+
|
| 909 |
+
{/* Custom Concept Input */}
|
| 910 |
+
<Card variant="glass" className="border-2 border-transparent hover:border-teal-200/50 transition-all duration-300">
|
| 911 |
+
<CardHeader className="pb-3">
|
| 912 |
+
<div className="flex items-center justify-between">
|
| 913 |
+
<CardTitle className="bg-gradient-to-r from-teal-600 to-cyan-600 bg-clip-text text-transparent text-base">
|
| 914 |
+
Custom Concept (Optional)
|
| 915 |
+
</CardTitle>
|
| 916 |
+
<label className="flex items-center space-x-2 cursor-pointer">
|
| 917 |
+
<input
|
| 918 |
+
type="checkbox"
|
| 919 |
+
checked={useCustomConcept}
|
| 920 |
+
onChange={(e) => {
|
| 921 |
+
setUseCustomConcept(e.target.checked);
|
| 922 |
+
if (!e.target.checked) {
|
| 923 |
+
clearCustomConcept();
|
| 924 |
+
}
|
| 925 |
+
}}
|
| 926 |
+
className="rounded border-gray-300 text-teal-600 focus:ring-teal-500"
|
| 927 |
+
/>
|
| 928 |
+
<span className="text-xs text-gray-600">Use custom</span>
|
| 929 |
+
</label>
|
| 930 |
+
</div>
|
| 931 |
+
</CardHeader>
|
| 932 |
+
{useCustomConcept && (
|
| 933 |
+
<CardContent className="space-y-3 pt-0">
|
| 934 |
+
<div>
|
| 935 |
+
<label className="block text-xs font-medium text-gray-600 mb-1">
|
| 936 |
+
Your concept idea (how should it look?)
|
| 937 |
+
</label>
|
| 938 |
+
<textarea
|
| 939 |
+
className="w-full px-3 py-2 text-sm rounded-lg border-2 border-gray-200 bg-white/80 backdrop-blur-sm focus:outline-none focus:ring-2 focus:ring-teal-500 focus:border-teal-500 transition-all duration-250 resize-none"
|
| 940 |
+
rows={2}
|
| 941 |
+
placeholder="e.g., 'Show a split screen before/after of a damaged vs protected home' or 'Happy family in front of their house with a shield overlay'"
|
| 942 |
+
value={customConceptText}
|
| 943 |
+
onChange={(e) => {
|
| 944 |
+
setCustomConceptText(e.target.value);
|
| 945 |
+
setCustomConceptRefined(null);
|
| 946 |
+
}}
|
| 947 |
+
/>
|
| 948 |
+
</div>
|
| 949 |
+
|
| 950 |
+
<Button
|
| 951 |
+
variant="secondary"
|
| 952 |
+
size="sm"
|
| 953 |
+
className="w-full flex items-center justify-center gap-2"
|
| 954 |
+
onClick={handleRefineConcept}
|
| 955 |
+
disabled={!customConceptText.trim() || isRefiningConcept}
|
| 956 |
+
>
|
| 957 |
+
{isRefiningConcept ? (
|
| 958 |
+
<>
|
| 959 |
+
<Loader2 className="w-4 h-4 animate-spin" />
|
| 960 |
+
Refining...
|
| 961 |
+
</>
|
| 962 |
+
) : (
|
| 963 |
+
<>
|
| 964 |
+
<Wand2 className="w-4 h-4" />
|
| 965 |
+
Refine with AI
|
| 966 |
+
</>
|
| 967 |
+
)}
|
| 968 |
+
</Button>
|
| 969 |
+
|
| 970 |
+
{customConceptRefined && (
|
| 971 |
+
<div className="p-3 rounded-lg bg-gradient-to-r from-teal-50 to-cyan-50 border border-teal-200">
|
| 972 |
+
<div className="flex items-start justify-between mb-2">
|
| 973 |
+
<h4 className="font-semibold text-teal-700 text-sm">
|
| 974 |
+
{customConceptRefined.name}
|
| 975 |
+
</h4>
|
| 976 |
+
<Check className="w-4 h-4 text-green-500 flex-shrink-0" />
|
| 977 |
+
</div>
|
| 978 |
+
<p className="text-xs text-teal-600 mb-1">
|
| 979 |
+
<span className="font-medium">Structure:</span> {customConceptRefined.structure}
|
| 980 |
+
</p>
|
| 981 |
+
<p className="text-xs text-gray-600">
|
| 982 |
+
<span className="font-medium">Visual:</span> {customConceptRefined.visual}
|
| 983 |
+
</p>
|
| 984 |
+
</div>
|
| 985 |
+
)}
|
| 986 |
+
</CardContent>
|
| 987 |
+
)}
|
| 988 |
+
</Card>
|
| 989 |
|
| 990 |
+
{/* Standard Concept Selector - show if not using custom */}
|
| 991 |
+
{!useCustomConcept && (
|
| 992 |
+
<ConceptSelector
|
| 993 |
+
onSelect={setSelectedConcept}
|
| 994 |
+
selectedConcept={selectedConcept}
|
| 995 |
+
angleKey={selectedAngle?.key}
|
| 996 |
+
/>
|
| 997 |
+
)}
|
| 998 |
+
|
| 999 |
+
{/* User Goal Input (helps AI refine custom angles/concepts better) */}
|
| 1000 |
+
{(useCustomAngle || useCustomConcept) && (
|
| 1001 |
+
<Card variant="glass" className="border-2 border-transparent hover:border-gray-200/50 transition-all duration-300">
|
| 1002 |
+
<CardContent className="pt-4">
|
| 1003 |
+
<label className="block text-xs font-medium text-gray-600 mb-1">
|
| 1004 |
+
Your goal (optional, helps AI understand context)
|
| 1005 |
+
</label>
|
| 1006 |
+
<input
|
| 1007 |
+
type="text"
|
| 1008 |
+
className="w-full px-3 py-2 text-sm rounded-lg border-2 border-gray-200 bg-white/80 backdrop-blur-sm focus:outline-none focus:ring-2 focus:ring-gray-400 focus:border-gray-400 transition-all duration-250"
|
| 1009 |
+
placeholder="e.g., 'Target homeowners aged 45+ worried about coverage gaps'"
|
| 1010 |
+
value={userGoal}
|
| 1011 |
+
onChange={(e) => setUserGoal(e.target.value)}
|
| 1012 |
+
/>
|
| 1013 |
+
</CardContent>
|
| 1014 |
+
</Card>
|
| 1015 |
+
)}
|
| 1016 |
+
|
| 1017 |
+
{/* Selection Summary */}
|
| 1018 |
+
<Card variant="glass" className="border-2 border-blue-100 bg-gradient-to-r from-blue-50/50 to-cyan-50/50">
|
| 1019 |
+
<CardContent className="pt-4 pb-4">
|
| 1020 |
+
<h4 className="text-sm font-semibold text-gray-700 mb-2">Current Selection:</h4>
|
| 1021 |
+
<div className="space-y-2 text-xs">
|
| 1022 |
+
<div className="flex items-center gap-2">
|
| 1023 |
+
<span className="font-medium text-gray-600">Angle:</span>
|
| 1024 |
+
{effectiveAngle ? (
|
| 1025 |
+
<span className="text-blue-600 font-medium">
|
| 1026 |
+
{effectiveAngle.name}
|
| 1027 |
+
{useCustomAngle && <span className="text-purple-500 ml-1">(Custom)</span>}
|
| 1028 |
+
</span>
|
| 1029 |
+
) : (
|
| 1030 |
+
<span className="text-gray-400 italic">Not selected</span>
|
| 1031 |
+
)}
|
| 1032 |
+
</div>
|
| 1033 |
+
<div className="flex items-center gap-2">
|
| 1034 |
+
<span className="font-medium text-gray-600">Concept:</span>
|
| 1035 |
+
{effectiveConcept ? (
|
| 1036 |
+
<span className="text-cyan-600 font-medium">
|
| 1037 |
+
{effectiveConcept.name}
|
| 1038 |
+
{useCustomConcept && <span className="text-teal-500 ml-1">(Custom)</span>}
|
| 1039 |
+
</span>
|
| 1040 |
+
) : (
|
| 1041 |
+
<span className="text-gray-400 italic">Not selected</span>
|
| 1042 |
+
)}
|
| 1043 |
+
</div>
|
| 1044 |
+
</div>
|
| 1045 |
+
</CardContent>
|
| 1046 |
+
</Card>
|
| 1047 |
|
| 1048 |
<Button
|
| 1049 |
variant="primary"
|
|
|
|
| 1051 |
className="w-full"
|
| 1052 |
onClick={handleMatrixGenerate}
|
| 1053 |
isLoading={isGenerating}
|
| 1054 |
+
disabled={!effectiveAngle || !effectiveConcept}
|
| 1055 |
>
|
| 1056 |
Generate Ad
|
| 1057 |
</Button>
|
frontend/app/matrix/page.tsx
CHANGED
|
@@ -4,7 +4,7 @@ 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 {
|
| 8 |
|
| 9 |
export default function MatrixPage() {
|
| 10 |
return (
|
|
@@ -31,30 +31,6 @@ export default function MatrixPage() {
|
|
| 31 |
variant="glass"
|
| 32 |
className="animate-scale-in hover:scale-105 transition-all duration-300 group cursor-pointer"
|
| 33 |
style={{ animationDelay: "0.1s" }}
|
| 34 |
-
>
|
| 35 |
-
<Link href="/generate/matrix" className="block">
|
| 36 |
-
<CardHeader>
|
| 37 |
-
<div className="p-3 rounded-xl bg-gradient-to-br from-blue-500 via-blue-600 to-cyan-500 w-fit mb-3 shadow-lg group-hover:shadow-xl group-hover:scale-110 transition-all duration-300">
|
| 38 |
-
<Layers className="h-6 w-6 text-white" />
|
| 39 |
-
</div>
|
| 40 |
-
<CardTitle className="group-hover:text-blue-600 transition-colors">Generate with Matrix</CardTitle>
|
| 41 |
-
<CardDescription>
|
| 42 |
-
Select specific angle and concept combinations
|
| 43 |
-
</CardDescription>
|
| 44 |
-
</CardHeader>
|
| 45 |
-
<CardContent>
|
| 46 |
-
<Button variant="primary" className="w-full group-hover:shadow-lg transition-all">
|
| 47 |
-
<Sparkles className="h-4 w-4 mr-2 group-hover:rotate-12 transition-transform duration-300" />
|
| 48 |
-
Generate Ad
|
| 49 |
-
</Button>
|
| 50 |
-
</CardContent>
|
| 51 |
-
</Link>
|
| 52 |
-
</Card>
|
| 53 |
-
|
| 54 |
-
<Card
|
| 55 |
-
variant="glass"
|
| 56 |
-
className="animate-scale-in hover:scale-105 transition-all duration-300 group cursor-pointer"
|
| 57 |
-
style={{ animationDelay: "0.2s" }}
|
| 58 |
>
|
| 59 |
<Link href="/browse/angles" className="block">
|
| 60 |
<CardHeader>
|
|
@@ -77,7 +53,7 @@ export default function MatrixPage() {
|
|
| 77 |
<Card
|
| 78 |
variant="glass"
|
| 79 |
className="animate-scale-in hover:scale-105 transition-all duration-300 group cursor-pointer"
|
| 80 |
-
style={{ animationDelay: "0.
|
| 81 |
>
|
| 82 |
<Link href="/browse/concepts" className="block">
|
| 83 |
<CardHeader>
|
|
@@ -99,8 +75,8 @@ export default function MatrixPage() {
|
|
| 99 |
|
| 100 |
<Card
|
| 101 |
variant="glass"
|
| 102 |
-
className="
|
| 103 |
-
style={{ animationDelay: "0.
|
| 104 |
>
|
| 105 |
<Link href="/matrix/testing" className="block">
|
| 106 |
<CardHeader>
|
|
|
|
| 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 { Search, TestTube } from "lucide-react";
|
| 8 |
|
| 9 |
export default function MatrixPage() {
|
| 10 |
return (
|
|
|
|
| 31 |
variant="glass"
|
| 32 |
className="animate-scale-in hover:scale-105 transition-all duration-300 group cursor-pointer"
|
| 33 |
style={{ animationDelay: "0.1s" }}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
>
|
| 35 |
<Link href="/browse/angles" className="block">
|
| 36 |
<CardHeader>
|
|
|
|
| 53 |
<Card
|
| 54 |
variant="glass"
|
| 55 |
className="animate-scale-in hover:scale-105 transition-all duration-300 group cursor-pointer"
|
| 56 |
+
style={{ animationDelay: "0.2s" }}
|
| 57 |
>
|
| 58 |
<Link href="/browse/concepts" className="block">
|
| 59 |
<CardHeader>
|
|
|
|
| 75 |
|
| 76 |
<Card
|
| 77 |
variant="glass"
|
| 78 |
+
className="animate-scale-in hover:scale-105 transition-all duration-300 group cursor-pointer"
|
| 79 |
+
style={{ animationDelay: "0.3s" }}
|
| 80 |
>
|
| 81 |
<Link href="/matrix/testing" className="block">
|
| 82 |
<CardHeader>
|
frontend/lib/api/endpoints.ts
CHANGED
|
@@ -67,6 +67,8 @@ export const generateMatrixAd = async (params: {
|
|
| 67 |
niche: Niche;
|
| 68 |
angle_key?: string | null;
|
| 69 |
concept_key?: string | null;
|
|
|
|
|
|
|
| 70 |
num_images: number;
|
| 71 |
image_model?: string | null;
|
| 72 |
target_audience?: string;
|
|
@@ -135,6 +137,17 @@ export const getCompatibleConcepts = async (angleKey: string): Promise<Compatibl
|
|
| 135 |
return response.data;
|
| 136 |
};
|
| 137 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 138 |
// Database Endpoints
|
| 139 |
export const getDbStats = async (): Promise<DbStatsResponse> => {
|
| 140 |
const response = await apiClient.get<DbStatsResponse>("/db/stats");
|
|
|
|
| 67 |
niche: Niche;
|
| 68 |
angle_key?: string | null;
|
| 69 |
concept_key?: string | null;
|
| 70 |
+
custom_angle?: string | null;
|
| 71 |
+
custom_concept?: string | null;
|
| 72 |
num_images: number;
|
| 73 |
image_model?: string | null;
|
| 74 |
target_audience?: string;
|
|
|
|
| 137 |
return response.data;
|
| 138 |
};
|
| 139 |
|
| 140 |
+
// Refine custom angle or concept using AI
|
| 141 |
+
export const refineCustomAngleOrConcept = async (params: {
|
| 142 |
+
text: string;
|
| 143 |
+
type: "angle" | "concept";
|
| 144 |
+
niche: Niche;
|
| 145 |
+
goal?: string;
|
| 146 |
+
}): Promise<{ status: string; type: "angle" | "concept"; refined?: any; error?: string }> => {
|
| 147 |
+
const response = await apiClient.post("/matrix/refine-custom", params);
|
| 148 |
+
return response.data;
|
| 149 |
+
};
|
| 150 |
+
|
| 151 |
// Database Endpoints
|
| 152 |
export const getDbStats = async (): Promise<DbStatsResponse> => {
|
| 153 |
const response = await apiClient.get<DbStatsResponse>("/db/stats");
|
frontend/store/matrixStore.ts
CHANGED
|
@@ -13,6 +13,16 @@ interface MatrixState {
|
|
| 13 |
isLoading: boolean;
|
| 14 |
error: string | null;
|
| 15 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
setAngles: (angles: AnglesResponse) => void;
|
| 17 |
setConcepts: (concepts: ConceptsResponse) => void;
|
| 18 |
setSelectedAngle: (angle: AngleInfo | null) => void;
|
|
@@ -22,6 +32,19 @@ interface MatrixState {
|
|
| 22 |
setConceptFilters: (filters: Partial<MatrixFilters>) => void;
|
| 23 |
setIsLoading: (isLoading: boolean) => void;
|
| 24 |
setError: (error: string | null) => void;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
reset: () => void;
|
| 26 |
}
|
| 27 |
|
|
@@ -35,6 +58,15 @@ const initialState = {
|
|
| 35 |
conceptFilters: {},
|
| 36 |
isLoading: false,
|
| 37 |
error: null,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
};
|
| 39 |
|
| 40 |
export const useMatrixStore = create<MatrixState>((set) => ({
|
|
@@ -62,5 +94,42 @@ export const useMatrixStore = create<MatrixState>((set) => ({
|
|
| 62 |
|
| 63 |
setError: (error) => set({ error }),
|
| 64 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
reset: () => set(initialState),
|
| 66 |
}));
|
|
|
|
| 13 |
isLoading: boolean;
|
| 14 |
error: string | null;
|
| 15 |
|
| 16 |
+
// Custom angle/concept state
|
| 17 |
+
customAngleText: string;
|
| 18 |
+
customConceptText: string;
|
| 19 |
+
customAngleRefined: AngleInfo | null;
|
| 20 |
+
customConceptRefined: ConceptInfo | null;
|
| 21 |
+
isRefiningAngle: boolean;
|
| 22 |
+
isRefiningConcept: boolean;
|
| 23 |
+
useCustomAngle: boolean;
|
| 24 |
+
useCustomConcept: boolean;
|
| 25 |
+
|
| 26 |
setAngles: (angles: AnglesResponse) => void;
|
| 27 |
setConcepts: (concepts: ConceptsResponse) => void;
|
| 28 |
setSelectedAngle: (angle: AngleInfo | null) => void;
|
|
|
|
| 32 |
setConceptFilters: (filters: Partial<MatrixFilters>) => void;
|
| 33 |
setIsLoading: (isLoading: boolean) => void;
|
| 34 |
setError: (error: string | null) => void;
|
| 35 |
+
|
| 36 |
+
// Custom angle/concept setters
|
| 37 |
+
setCustomAngleText: (text: string) => void;
|
| 38 |
+
setCustomConceptText: (text: string) => void;
|
| 39 |
+
setCustomAngleRefined: (angle: AngleInfo | null) => void;
|
| 40 |
+
setCustomConceptRefined: (concept: ConceptInfo | null) => void;
|
| 41 |
+
setIsRefiningAngle: (isRefining: boolean) => void;
|
| 42 |
+
setIsRefiningConcept: (isRefining: boolean) => void;
|
| 43 |
+
setUseCustomAngle: (use: boolean) => void;
|
| 44 |
+
setUseCustomConcept: (use: boolean) => void;
|
| 45 |
+
clearCustomAngle: () => void;
|
| 46 |
+
clearCustomConcept: () => void;
|
| 47 |
+
|
| 48 |
reset: () => void;
|
| 49 |
}
|
| 50 |
|
|
|
|
| 58 |
conceptFilters: {},
|
| 59 |
isLoading: false,
|
| 60 |
error: null,
|
| 61 |
+
// Custom state
|
| 62 |
+
customAngleText: "",
|
| 63 |
+
customConceptText: "",
|
| 64 |
+
customAngleRefined: null,
|
| 65 |
+
customConceptRefined: null,
|
| 66 |
+
isRefiningAngle: false,
|
| 67 |
+
isRefiningConcept: false,
|
| 68 |
+
useCustomAngle: false,
|
| 69 |
+
useCustomConcept: false,
|
| 70 |
};
|
| 71 |
|
| 72 |
export const useMatrixStore = create<MatrixState>((set) => ({
|
|
|
|
| 94 |
|
| 95 |
setError: (error) => set({ error }),
|
| 96 |
|
| 97 |
+
// Custom angle/concept setters
|
| 98 |
+
setCustomAngleText: (text) => set({ customAngleText: text }),
|
| 99 |
+
|
| 100 |
+
setCustomConceptText: (text) => set({ customConceptText: text }),
|
| 101 |
+
|
| 102 |
+
setCustomAngleRefined: (angle) => set({ customAngleRefined: angle }),
|
| 103 |
+
|
| 104 |
+
setCustomConceptRefined: (concept) => set({ customConceptRefined: concept }),
|
| 105 |
+
|
| 106 |
+
setIsRefiningAngle: (isRefining) => set({ isRefiningAngle: isRefining }),
|
| 107 |
+
|
| 108 |
+
setIsRefiningConcept: (isRefining) => set({ isRefiningConcept: isRefining }),
|
| 109 |
+
|
| 110 |
+
setUseCustomAngle: (use) => set({
|
| 111 |
+
useCustomAngle: use,
|
| 112 |
+
// Clear selected angle if switching to custom
|
| 113 |
+
selectedAngle: use ? null : null,
|
| 114 |
+
}),
|
| 115 |
+
|
| 116 |
+
setUseCustomConcept: (use) => set({
|
| 117 |
+
useCustomConcept: use,
|
| 118 |
+
// Clear selected concept if switching to custom
|
| 119 |
+
selectedConcept: use ? null : null,
|
| 120 |
+
}),
|
| 121 |
+
|
| 122 |
+
clearCustomAngle: () => set({
|
| 123 |
+
customAngleText: "",
|
| 124 |
+
customAngleRefined: null,
|
| 125 |
+
useCustomAngle: false,
|
| 126 |
+
}),
|
| 127 |
+
|
| 128 |
+
clearCustomConcept: () => set({
|
| 129 |
+
customConceptText: "",
|
| 130 |
+
customConceptRefined: null,
|
| 131 |
+
useCustomConcept: false,
|
| 132 |
+
}),
|
| 133 |
+
|
| 134 |
reset: () => set(initialState),
|
| 135 |
}));
|
frontend/types/api.ts
CHANGED
|
@@ -49,6 +49,8 @@ export interface AngleInfo {
|
|
| 49 |
name: string;
|
| 50 |
trigger: string;
|
| 51 |
category: string;
|
|
|
|
|
|
|
| 52 |
}
|
| 53 |
|
| 54 |
export interface ConceptInfo {
|
|
@@ -57,6 +59,40 @@ export interface ConceptInfo {
|
|
| 57 |
structure: string;
|
| 58 |
visual: string;
|
| 59 |
category: string;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
}
|
| 61 |
|
| 62 |
export interface MatrixResult {
|
|
|
|
| 49 |
name: string;
|
| 50 |
trigger: string;
|
| 51 |
category: string;
|
| 52 |
+
example?: string;
|
| 53 |
+
original_text?: string; // For custom angles
|
| 54 |
}
|
| 55 |
|
| 56 |
export interface ConceptInfo {
|
|
|
|
| 59 |
structure: string;
|
| 60 |
visual: string;
|
| 61 |
category: string;
|
| 62 |
+
original_text?: string; // For custom concepts
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
// Custom angle/concept refinement types
|
| 66 |
+
export interface RefineCustomRequest {
|
| 67 |
+
text: string;
|
| 68 |
+
type: "angle" | "concept";
|
| 69 |
+
niche: Niche;
|
| 70 |
+
goal?: string;
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
export interface RefinedAngle {
|
| 74 |
+
key: string;
|
| 75 |
+
name: string;
|
| 76 |
+
trigger: string;
|
| 77 |
+
example: string;
|
| 78 |
+
category: string;
|
| 79 |
+
original_text: string;
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
export interface RefinedConcept {
|
| 83 |
+
key: string;
|
| 84 |
+
name: string;
|
| 85 |
+
structure: string;
|
| 86 |
+
visual: string;
|
| 87 |
+
category: string;
|
| 88 |
+
original_text: string;
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
export interface RefineCustomResponse {
|
| 92 |
+
status: string;
|
| 93 |
+
type: "angle" | "concept";
|
| 94 |
+
refined?: RefinedAngle | RefinedConcept | null;
|
| 95 |
+
error?: string | null;
|
| 96 |
}
|
| 97 |
|
| 98 |
export interface MatrixResult {
|
main.py
CHANGED
|
@@ -225,6 +225,14 @@ class MatrixGenerateRequest(BaseModel):
|
|
| 225 |
default=None,
|
| 226 |
description="Specific concept key (random if not provided)"
|
| 227 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 228 |
num_images: int = Field(
|
| 229 |
default=1,
|
| 230 |
ge=1,
|
|
@@ -245,6 +253,51 @@ class MatrixGenerateRequest(BaseModel):
|
|
| 245 |
)
|
| 246 |
|
| 247 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 248 |
class MatrixBatchRequest(BaseModel):
|
| 249 |
"""Request for batch matrix generation."""
|
| 250 |
niche: Literal["home_insurance", "glp1"] = Field(
|
|
@@ -1366,12 +1419,18 @@ async def generate_with_matrix(
|
|
| 1366 |
|
| 1367 |
If angle_key and concept_key are not provided, a compatible
|
| 1368 |
combination will be selected automatically based on the niche.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1369 |
"""
|
| 1370 |
try:
|
| 1371 |
result = await ad_generator.generate_ad_with_matrix(
|
| 1372 |
niche=request.niche,
|
| 1373 |
angle_key=request.angle_key,
|
| 1374 |
concept_key=request.concept_key,
|
|
|
|
|
|
|
| 1375 |
num_images=request.num_images,
|
| 1376 |
image_model=request.image_model,
|
| 1377 |
username=username, # Pass current user
|
|
@@ -1536,6 +1595,45 @@ async def get_compatible_concepts(angle_key: str):
|
|
| 1536 |
}
|
| 1537 |
|
| 1538 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1539 |
# =============================================================================
|
| 1540 |
# EXTENSIVE ENDPOINTS
|
| 1541 |
# =============================================================================
|
|
|
|
| 225 |
default=None,
|
| 226 |
description="Specific concept key (random if not provided)"
|
| 227 |
)
|
| 228 |
+
custom_angle: Optional[str] = Field(
|
| 229 |
+
default=None,
|
| 230 |
+
description="Custom angle text (AI will structure it properly). Used when angle_key is 'custom'"
|
| 231 |
+
)
|
| 232 |
+
custom_concept: Optional[str] = Field(
|
| 233 |
+
default=None,
|
| 234 |
+
description="Custom concept text (AI will structure it properly). Used when concept_key is 'custom'"
|
| 235 |
+
)
|
| 236 |
num_images: int = Field(
|
| 237 |
default=1,
|
| 238 |
ge=1,
|
|
|
|
| 253 |
)
|
| 254 |
|
| 255 |
|
| 256 |
+
class RefineCustomRequest(BaseModel):
|
| 257 |
+
"""Request to refine custom angle or concept text using AI."""
|
| 258 |
+
text: str = Field(
|
| 259 |
+
description="The raw custom text from user"
|
| 260 |
+
)
|
| 261 |
+
type: Literal["angle", "concept"] = Field(
|
| 262 |
+
description="Whether this is an angle or concept"
|
| 263 |
+
)
|
| 264 |
+
niche: Literal["home_insurance", "glp1"] = Field(
|
| 265 |
+
description="Target niche for context"
|
| 266 |
+
)
|
| 267 |
+
goal: Optional[str] = Field(
|
| 268 |
+
default=None,
|
| 269 |
+
description="Optional user goal or context"
|
| 270 |
+
)
|
| 271 |
+
|
| 272 |
+
|
| 273 |
+
class RefinedAngleResponse(BaseModel):
|
| 274 |
+
"""Response for refined angle."""
|
| 275 |
+
key: str = Field(default="custom")
|
| 276 |
+
name: str
|
| 277 |
+
trigger: str
|
| 278 |
+
example: str
|
| 279 |
+
category: str = Field(default="Custom")
|
| 280 |
+
original_text: str
|
| 281 |
+
|
| 282 |
+
|
| 283 |
+
class RefinedConceptResponse(BaseModel):
|
| 284 |
+
"""Response for refined concept."""
|
| 285 |
+
key: str = Field(default="custom")
|
| 286 |
+
name: str
|
| 287 |
+
structure: str
|
| 288 |
+
visual: str
|
| 289 |
+
category: str = Field(default="Custom")
|
| 290 |
+
original_text: str
|
| 291 |
+
|
| 292 |
+
|
| 293 |
+
class RefineCustomResponse(BaseModel):
|
| 294 |
+
"""Response for refined custom angle or concept."""
|
| 295 |
+
status: str
|
| 296 |
+
type: Literal["angle", "concept"]
|
| 297 |
+
refined: Optional[dict] = None
|
| 298 |
+
error: Optional[str] = None
|
| 299 |
+
|
| 300 |
+
|
| 301 |
class MatrixBatchRequest(BaseModel):
|
| 302 |
"""Request for batch matrix generation."""
|
| 303 |
niche: Literal["home_insurance", "glp1"] = Field(
|
|
|
|
| 1419 |
|
| 1420 |
If angle_key and concept_key are not provided, a compatible
|
| 1421 |
combination will be selected automatically based on the niche.
|
| 1422 |
+
|
| 1423 |
+
Supports custom angles/concepts:
|
| 1424 |
+
- Set angle_key='custom' and provide custom_angle text
|
| 1425 |
+
- Set concept_key='custom' and provide custom_concept text
|
| 1426 |
"""
|
| 1427 |
try:
|
| 1428 |
result = await ad_generator.generate_ad_with_matrix(
|
| 1429 |
niche=request.niche,
|
| 1430 |
angle_key=request.angle_key,
|
| 1431 |
concept_key=request.concept_key,
|
| 1432 |
+
custom_angle=request.custom_angle,
|
| 1433 |
+
custom_concept=request.custom_concept,
|
| 1434 |
num_images=request.num_images,
|
| 1435 |
image_model=request.image_model,
|
| 1436 |
username=username, # Pass current user
|
|
|
|
| 1595 |
}
|
| 1596 |
|
| 1597 |
|
| 1598 |
+
@app.post("/matrix/refine-custom", response_model=RefineCustomResponse)
|
| 1599 |
+
async def refine_custom_angle_or_concept(request: RefineCustomRequest):
|
| 1600 |
+
"""
|
| 1601 |
+
Refine a custom angle or concept text using AI.
|
| 1602 |
+
|
| 1603 |
+
This endpoint takes raw user input and structures it properly
|
| 1604 |
+
according to the angle/concept framework used in ad generation.
|
| 1605 |
+
|
| 1606 |
+
For angles, it extracts:
|
| 1607 |
+
- name: Short descriptive name
|
| 1608 |
+
- trigger: Psychological trigger (e.g., Fear, Hope, Pride)
|
| 1609 |
+
- example: Example hook text
|
| 1610 |
+
|
| 1611 |
+
For concepts, it extracts:
|
| 1612 |
+
- name: Short descriptive name
|
| 1613 |
+
- structure: How to structure the visual/copy
|
| 1614 |
+
- visual: Visual guidance for the image
|
| 1615 |
+
"""
|
| 1616 |
+
try:
|
| 1617 |
+
result = await ad_generator.refine_custom_angle_or_concept(
|
| 1618 |
+
text=request.text,
|
| 1619 |
+
type=request.type,
|
| 1620 |
+
niche=request.niche,
|
| 1621 |
+
goal=request.goal,
|
| 1622 |
+
)
|
| 1623 |
+
return {
|
| 1624 |
+
"status": "success",
|
| 1625 |
+
"type": request.type,
|
| 1626 |
+
"refined": result,
|
| 1627 |
+
}
|
| 1628 |
+
except Exception as e:
|
| 1629 |
+
return {
|
| 1630 |
+
"status": "error",
|
| 1631 |
+
"type": request.type,
|
| 1632 |
+
"refined": None,
|
| 1633 |
+
"error": str(e),
|
| 1634 |
+
}
|
| 1635 |
+
|
| 1636 |
+
|
| 1637 |
# =============================================================================
|
| 1638 |
# EXTENSIVE ENDPOINTS
|
| 1639 |
# =============================================================================
|
services/generator.py
CHANGED
|
@@ -1687,6 +1687,8 @@ CRITICAL REQUIREMENTS:
|
|
| 1687 |
niche: str,
|
| 1688 |
angle_key: Optional[str] = None,
|
| 1689 |
concept_key: Optional[str] = None,
|
|
|
|
|
|
|
| 1690 |
num_images: int = 1,
|
| 1691 |
image_model: Optional[str] = None,
|
| 1692 |
username: Optional[str] = None, # Username of the user generating the ad
|
|
@@ -1701,36 +1703,89 @@ CRITICAL REQUIREMENTS:
|
|
| 1701 |
niche: Target niche
|
| 1702 |
angle_key: Specific angle key (optional, random if not provided)
|
| 1703 |
concept_key: Specific concept key (optional, random if not provided)
|
|
|
|
|
|
|
| 1704 |
num_images: Number of images to generate
|
| 1705 |
|
| 1706 |
Returns:
|
| 1707 |
Complete ad creative with angle and concept metadata
|
| 1708 |
"""
|
| 1709 |
-
|
| 1710 |
-
|
| 1711 |
-
|
| 1712 |
-
|
| 1713 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1714 |
angle = get_angle_by_key(angle_key)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1715 |
concept = get_concept_by_key(concept_key)
|
| 1716 |
-
|
| 1717 |
-
|
| 1718 |
-
|
| 1719 |
-
|
|
|
|
| 1720 |
combination = {
|
| 1721 |
"angle": angle,
|
| 1722 |
"concept": concept,
|
| 1723 |
"prompt_guidance": f"""
|
| 1724 |
-
ANGLE: {angle
|
| 1725 |
-
- Psychological trigger: {angle
|
| 1726 |
-
- Example: "{angle
|
| 1727 |
|
| 1728 |
-
CONCEPT: {concept
|
| 1729 |
-
- Structure: {concept
|
| 1730 |
-
- Visual: {concept
|
| 1731 |
""",
|
| 1732 |
}
|
| 1733 |
else:
|
|
|
|
| 1734 |
combination = matrix_service.generate_single_combination(niche)
|
| 1735 |
|
| 1736 |
angle = combination["angle"]
|
|
@@ -2268,6 +2323,144 @@ CONCEPT: {concept['name']}
|
|
| 2268 |
else:
|
| 2269 |
raise ValueError("No ads generated from extensive")
|
| 2270 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2271 |
# ========================================================================
|
| 2272 |
# MATRIX-SPECIFIC PROMPT METHODS
|
| 2273 |
# ========================================================================
|
|
|
|
| 1687 |
niche: str,
|
| 1688 |
angle_key: Optional[str] = None,
|
| 1689 |
concept_key: Optional[str] = None,
|
| 1690 |
+
custom_angle: Optional[str] = None,
|
| 1691 |
+
custom_concept: Optional[str] = None,
|
| 1692 |
num_images: int = 1,
|
| 1693 |
image_model: Optional[str] = None,
|
| 1694 |
username: Optional[str] = None, # Username of the user generating the ad
|
|
|
|
| 1703 |
niche: Target niche
|
| 1704 |
angle_key: Specific angle key (optional, random if not provided)
|
| 1705 |
concept_key: Specific concept key (optional, random if not provided)
|
| 1706 |
+
custom_angle: Custom angle dict (used when angle_key is 'custom')
|
| 1707 |
+
custom_concept: Custom concept dict (used when concept_key is 'custom')
|
| 1708 |
num_images: Number of images to generate
|
| 1709 |
|
| 1710 |
Returns:
|
| 1711 |
Complete ad creative with angle and concept metadata
|
| 1712 |
"""
|
| 1713 |
+
from data.angles import get_angle_by_key
|
| 1714 |
+
from data.concepts import get_concept_by_key
|
| 1715 |
+
|
| 1716 |
+
# Handle custom or predefined angle
|
| 1717 |
+
angle = None
|
| 1718 |
+
if angle_key == "custom" and custom_angle:
|
| 1719 |
+
# Parse custom angle - it should be a dict with name, trigger, example
|
| 1720 |
+
if isinstance(custom_angle, str):
|
| 1721 |
+
# If it's a JSON string, parse it
|
| 1722 |
+
try:
|
| 1723 |
+
import json
|
| 1724 |
+
angle = json.loads(custom_angle)
|
| 1725 |
+
except:
|
| 1726 |
+
# If plain text, create a basic structure
|
| 1727 |
+
angle = {
|
| 1728 |
+
"key": "custom",
|
| 1729 |
+
"name": "Custom Angle",
|
| 1730 |
+
"trigger": "Emotion",
|
| 1731 |
+
"example": custom_angle,
|
| 1732 |
+
"category": "Custom",
|
| 1733 |
+
}
|
| 1734 |
+
else:
|
| 1735 |
+
angle = custom_angle
|
| 1736 |
+
# Ensure required fields
|
| 1737 |
+
angle["key"] = "custom"
|
| 1738 |
+
angle["category"] = angle.get("category", "Custom")
|
| 1739 |
+
elif angle_key:
|
| 1740 |
angle = get_angle_by_key(angle_key)
|
| 1741 |
+
if not angle:
|
| 1742 |
+
raise ValueError(f"Invalid angle_key: {angle_key}")
|
| 1743 |
+
|
| 1744 |
+
# Handle custom or predefined concept
|
| 1745 |
+
concept = None
|
| 1746 |
+
if concept_key == "custom" and custom_concept:
|
| 1747 |
+
# Parse custom concept - it should be a dict with name, structure, visual
|
| 1748 |
+
if isinstance(custom_concept, str):
|
| 1749 |
+
# If it's a JSON string, parse it
|
| 1750 |
+
try:
|
| 1751 |
+
import json
|
| 1752 |
+
concept = json.loads(custom_concept)
|
| 1753 |
+
except:
|
| 1754 |
+
# If plain text, create a basic structure
|
| 1755 |
+
concept = {
|
| 1756 |
+
"key": "custom",
|
| 1757 |
+
"name": "Custom Concept",
|
| 1758 |
+
"structure": custom_concept,
|
| 1759 |
+
"visual": custom_concept,
|
| 1760 |
+
"category": "Custom",
|
| 1761 |
+
}
|
| 1762 |
+
else:
|
| 1763 |
+
concept = custom_concept
|
| 1764 |
+
# Ensure required fields
|
| 1765 |
+
concept["key"] = "custom"
|
| 1766 |
+
concept["category"] = concept.get("category", "Custom")
|
| 1767 |
+
elif concept_key:
|
| 1768 |
concept = get_concept_by_key(concept_key)
|
| 1769 |
+
if not concept:
|
| 1770 |
+
raise ValueError(f"Invalid concept_key: {concept_key}")
|
| 1771 |
+
|
| 1772 |
+
# If both angle and concept are provided (custom or predefined)
|
| 1773 |
+
if angle and concept:
|
| 1774 |
combination = {
|
| 1775 |
"angle": angle,
|
| 1776 |
"concept": concept,
|
| 1777 |
"prompt_guidance": f"""
|
| 1778 |
+
ANGLE: {angle.get('name', 'Custom Angle')}
|
| 1779 |
+
- Psychological trigger: {angle.get('trigger', 'Emotion')}
|
| 1780 |
+
- Example: "{angle.get('example', '')}"
|
| 1781 |
|
| 1782 |
+
CONCEPT: {concept.get('name', 'Custom Concept')}
|
| 1783 |
+
- Structure: {concept.get('structure', '')}
|
| 1784 |
+
- Visual: {concept.get('visual', '')}
|
| 1785 |
""",
|
| 1786 |
}
|
| 1787 |
else:
|
| 1788 |
+
# Fall back to auto-generation
|
| 1789 |
combination = matrix_service.generate_single_combination(niche)
|
| 1790 |
|
| 1791 |
angle = combination["angle"]
|
|
|
|
| 2323 |
else:
|
| 2324 |
raise ValueError("No ads generated from extensive")
|
| 2325 |
|
| 2326 |
+
# ========================================================================
|
| 2327 |
+
# CUSTOM ANGLE/CONCEPT REFINEMENT
|
| 2328 |
+
# ========================================================================
|
| 2329 |
+
|
| 2330 |
+
async def refine_custom_angle_or_concept(
|
| 2331 |
+
self,
|
| 2332 |
+
text: str,
|
| 2333 |
+
type: str, # "angle" or "concept"
|
| 2334 |
+
niche: str,
|
| 2335 |
+
goal: Optional[str] = None,
|
| 2336 |
+
) -> Dict[str, Any]:
|
| 2337 |
+
"""
|
| 2338 |
+
Refine a custom angle or concept text using AI.
|
| 2339 |
+
|
| 2340 |
+
Takes raw user input and structures it properly according to
|
| 2341 |
+
the angle/concept framework used in ad generation.
|
| 2342 |
+
|
| 2343 |
+
Args:
|
| 2344 |
+
text: Raw user input text
|
| 2345 |
+
type: "angle" or "concept"
|
| 2346 |
+
niche: Target niche for context
|
| 2347 |
+
goal: Optional user goal or context
|
| 2348 |
+
|
| 2349 |
+
Returns:
|
| 2350 |
+
Structured angle or concept dict
|
| 2351 |
+
"""
|
| 2352 |
+
import json
|
| 2353 |
+
|
| 2354 |
+
if type == "angle":
|
| 2355 |
+
prompt = f"""You are an expert in direct-response advertising psychology.
|
| 2356 |
+
|
| 2357 |
+
The user wants to create a custom marketing ANGLE for {niche.replace('_', ' ')} ads.
|
| 2358 |
+
|
| 2359 |
+
An ANGLE answers "WHY should I care?" - it's the psychological hook that makes someone stop scrolling.
|
| 2360 |
+
|
| 2361 |
+
User's custom angle idea:
|
| 2362 |
+
"{text}"
|
| 2363 |
+
|
| 2364 |
+
{f'User goal/context: {goal}' if goal else ''}
|
| 2365 |
+
|
| 2366 |
+
EXAMPLES OF WELL-STRUCTURED ANGLES:
|
| 2367 |
+
1. Name: "Fear / Loss Prevention", Trigger: "Fear", Example: "Don't lose your home to disaster"
|
| 2368 |
+
2. Name: "Save Money", Trigger: "Greed", Example: "Save $600/year"
|
| 2369 |
+
3. Name: "Peace of Mind", Trigger: "Relief", Example: "Sleep better knowing you're protected"
|
| 2370 |
+
4. Name: "Trending Now", Trigger: "FOMO", Example: "Join thousands already using this"
|
| 2371 |
+
|
| 2372 |
+
Structure the user's idea into a proper angle format.
|
| 2373 |
+
|
| 2374 |
+
Return JSON:
|
| 2375 |
+
{{
|
| 2376 |
+
"name": "Short descriptive name (2-4 words)",
|
| 2377 |
+
"trigger": "Primary psychological trigger (e.g., Fear, Hope, Pride, Greed, Relief, FOMO, Curiosity, Anger, Trust)",
|
| 2378 |
+
"example": "A compelling example hook using this angle (5-10 words)"
|
| 2379 |
+
}}"""
|
| 2380 |
+
|
| 2381 |
+
response_format = {
|
| 2382 |
+
"type": "json_schema",
|
| 2383 |
+
"json_schema": {
|
| 2384 |
+
"name": "refined_angle",
|
| 2385 |
+
"schema": {
|
| 2386 |
+
"type": "object",
|
| 2387 |
+
"properties": {
|
| 2388 |
+
"name": {"type": "string"},
|
| 2389 |
+
"trigger": {"type": "string"},
|
| 2390 |
+
"example": {"type": "string"},
|
| 2391 |
+
},
|
| 2392 |
+
"required": ["name", "trigger", "example"],
|
| 2393 |
+
},
|
| 2394 |
+
},
|
| 2395 |
+
}
|
| 2396 |
+
|
| 2397 |
+
response = await llm_service.generate(
|
| 2398 |
+
prompt=prompt,
|
| 2399 |
+
temperature=0.7,
|
| 2400 |
+
response_format=response_format,
|
| 2401 |
+
)
|
| 2402 |
+
|
| 2403 |
+
result = json.loads(response)
|
| 2404 |
+
result["key"] = "custom"
|
| 2405 |
+
result["category"] = "Custom"
|
| 2406 |
+
result["original_text"] = text
|
| 2407 |
+
return result
|
| 2408 |
+
|
| 2409 |
+
else: # concept
|
| 2410 |
+
prompt = f"""You are an expert in visual advertising and creative direction.
|
| 2411 |
+
|
| 2412 |
+
The user wants to create a custom visual CONCEPT for {niche.replace('_', ' ')} ads.
|
| 2413 |
+
|
| 2414 |
+
A CONCEPT answers "HOW do we show it?" - it's the visual approach and structure of the ad.
|
| 2415 |
+
|
| 2416 |
+
User's custom concept idea:
|
| 2417 |
+
"{text}"
|
| 2418 |
+
|
| 2419 |
+
{f'User goal/context: {goal}' if goal else ''}
|
| 2420 |
+
|
| 2421 |
+
EXAMPLES OF WELL-STRUCTURED CONCEPTS:
|
| 2422 |
+
1. Name: "Before/After Split", Structure: "Side-by-side comparison showing transformation", Visual: "Split screen with contrasting imagery, clear difference visible"
|
| 2423 |
+
2. Name: "Person + Quote", Structure: "Real person with testimonial overlay", Visual: "Authentic photo of relatable person, quote bubble or text overlay"
|
| 2424 |
+
3. Name: "Problem Close-up", Structure: "Zoom in on the pain point", Visual: "Detailed shot showing the problem clearly, emotional resonance"
|
| 2425 |
+
4. Name: "Lifestyle Scene", Structure: "Show the desired outcome/lifestyle", Visual: "Aspirational scene, happy people enjoying results"
|
| 2426 |
+
|
| 2427 |
+
Structure the user's idea into a proper concept format.
|
| 2428 |
+
|
| 2429 |
+
Return JSON:
|
| 2430 |
+
{{
|
| 2431 |
+
"name": "Short descriptive name (2-4 words)",
|
| 2432 |
+
"structure": "How to structure the visual/ad (one sentence)",
|
| 2433 |
+
"visual": "Visual guidance for the image (one sentence, specific details)"
|
| 2434 |
+
}}"""
|
| 2435 |
+
|
| 2436 |
+
response_format = {
|
| 2437 |
+
"type": "json_schema",
|
| 2438 |
+
"json_schema": {
|
| 2439 |
+
"name": "refined_concept",
|
| 2440 |
+
"schema": {
|
| 2441 |
+
"type": "object",
|
| 2442 |
+
"properties": {
|
| 2443 |
+
"name": {"type": "string"},
|
| 2444 |
+
"structure": {"type": "string"},
|
| 2445 |
+
"visual": {"type": "string"},
|
| 2446 |
+
},
|
| 2447 |
+
"required": ["name", "structure", "visual"],
|
| 2448 |
+
},
|
| 2449 |
+
},
|
| 2450 |
+
}
|
| 2451 |
+
|
| 2452 |
+
response = await llm_service.generate(
|
| 2453 |
+
prompt=prompt,
|
| 2454 |
+
temperature=0.7,
|
| 2455 |
+
response_format=response_format,
|
| 2456 |
+
)
|
| 2457 |
+
|
| 2458 |
+
result = json.loads(response)
|
| 2459 |
+
result["key"] = "custom"
|
| 2460 |
+
result["category"] = "Custom"
|
| 2461 |
+
result["original_text"] = text
|
| 2462 |
+
return result
|
| 2463 |
+
|
| 2464 |
# ========================================================================
|
| 2465 |
# MATRIX-SPECIFIC PROMPT METHODS
|
| 2466 |
# ========================================================================
|