sushilideaclan01 commited on
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 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 { selectedAngle, selectedConcept, setSelectedAngle, setSelectedConcept } = useMatrixStore();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 (!selectedAngle || !selectedConcept) {
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: selectedAngle.key,
225
- concept_key: selectedConcept.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: selectedAngle.key,
274
- concept_key: selectedConcept.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
- <AngleSelector
729
- onSelect={setSelectedAngle}
730
- selectedAngle={selectedAngle}
731
- />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
732
 
733
- <ConceptSelector
734
- onSelect={setSelectedConcept}
735
- selectedConcept={selectedConcept}
736
- angleKey={selectedAngle?.key}
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={!selectedAngle || !selectedConcept}
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 { Layers, Search, TestTube, Sparkles } from "lucide-react";
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.3s" }}
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="md:col-span-2 lg:col-span-3 animate-scale-in hover:scale-[1.02] transition-all duration-300 group cursor-pointer"
103
- style={{ animationDelay: "0.4s" }}
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
- # Get or generate angle × concept combination
1710
- if angle_key and concept_key:
1711
- from data.angles import get_angle_by_key
1712
- from data.concepts import get_concept_by_key
1713
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1714
  angle = get_angle_by_key(angle_key)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1715
  concept = get_concept_by_key(concept_key)
1716
-
1717
- if not angle or not concept:
1718
- raise ValueError(f"Invalid angle_key or concept_key")
1719
-
 
1720
  combination = {
1721
  "angle": angle,
1722
  "concept": concept,
1723
  "prompt_guidance": f"""
1724
- ANGLE: {angle['name']}
1725
- - Psychological trigger: {angle['trigger']}
1726
- - Example: "{angle['example']}"
1727
 
1728
- CONCEPT: {concept['name']}
1729
- - Structure: {concept['structure']}
1730
- - Visual: {concept['visual']}
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
  # ========================================================================