sushilideaclan01 commited on
Commit
0fbc3f9
·
1 Parent(s): 2bfe32d

feat: Enhance AI image generation prompts for natural, photorealistic advertising creatives by enriching context and refining modification instructions.

Browse files
frontend/app/creative/modify/page.tsx CHANGED
@@ -4,6 +4,7 @@ import React, { useState, useCallback } from "react";
4
  import { CreativeUploader } from "@/components/creative/CreativeUploader";
5
  import { CreativeAnalysis } from "@/components/creative/CreativeAnalysis";
6
  import { ModificationForm } from "@/components/creative/ModificationForm";
 
7
  import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/Card";
8
  import {
9
  uploadCreative,
@@ -15,6 +16,7 @@ import type {
15
  CreativeAnalysisData,
16
  ModificationMode,
17
  ModifiedImageResult,
 
18
  } from "@/types/api";
19
 
20
  type WorkflowStep = "upload" | "analysis" | "modify" | "result";
@@ -42,6 +44,11 @@ export default function CreativeModifyPage() {
42
  const [result, setResult] = useState<ModifiedImageResult | null>(null);
43
  const [generatedPrompt, setGeneratedPrompt] = useState<string | null>(null);
44
 
 
 
 
 
 
45
  // Handle image ready from uploader
46
  const handleImageReady = useCallback(async (imageUrl: string, file?: File) => {
47
  setIsLoading(true);
@@ -59,7 +66,7 @@ export default function CreativeModifyPage() {
59
  throw new Error(uploadResponse.error || "Failed to upload image");
60
  }
61
  setOriginalImageUrl(uploadResponse.image_url);
62
-
63
  // Now analyze using the uploaded URL
64
  analysisResponse = await analyzeCreativeByUrl({
65
  image_url: uploadResponse.image_url,
@@ -167,7 +174,7 @@ export default function CreativeModifyPage() {
167
  // Handle step click for navigation
168
  const handleStepClick = useCallback((stepKey: string) => {
169
  if (!isStepAccessible(stepKey)) return;
170
-
171
  if (stepKey === "upload") {
172
  handleBackToUpload();
173
  } else if (stepKey === "analysis" && currentStep === "result") {
@@ -175,6 +182,29 @@ export default function CreativeModifyPage() {
175
  }
176
  }, [currentStep, handleBackToUpload, handleBackToAnalysis]);
177
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
178
  return (
179
  <div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 to-cyan-50">
180
  <div className="container mx-auto px-4 py-8">
@@ -197,36 +227,33 @@ export default function CreativeModifyPage() {
197
  { key: "analysis", label: "Analysis & Modify" },
198
  { key: "result", label: "Result" },
199
  ].map((step, index) => {
200
- const isActive = currentStep === step.key ||
201
  (step.key === "analysis" && currentStep === "analysis");
202
  const isCompleted = getStepIndex(currentStep) > index;
203
  const canClick = isStepAccessible(step.key) && step.key !== currentStep;
204
-
205
  return (
206
  <React.Fragment key={step.key}>
207
  {index > 0 && (
208
  <div
209
- className={`w-12 h-0.5 ${
210
- isCompleted || isActive ? "bg-blue-500" : "bg-gray-300"
211
- }`}
212
  />
213
  )}
214
  <button
215
  type="button"
216
  onClick={() => handleStepClick(step.key)}
217
  disabled={!canClick}
218
- className={`flex flex-col items-center gap-1 transition-all ${
219
- canClick ? "cursor-pointer hover:scale-105" : "cursor-default"
220
- }`}
221
  >
222
  <div
223
- className={`flex items-center justify-center w-10 h-10 rounded-full text-sm font-medium transition-all ${
224
- isActive
225
- ? "bg-blue-500 text-white shadow-lg"
226
- : isCompleted
227
  ? "bg-blue-100 text-blue-600 hover:bg-blue-200"
228
  : "bg-gray-200 text-gray-500"
229
- }`}
230
  >
231
  {isCompleted ? (
232
  <svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
@@ -236,9 +263,8 @@ export default function CreativeModifyPage() {
236
  index + 1
237
  )}
238
  </div>
239
- <span className={`text-xs font-medium ${
240
- isActive ? "text-blue-600" : isCompleted ? "text-blue-500" : "text-gray-500"
241
- }`}>
242
  {step.label}
243
  </span>
244
  </button>
@@ -314,7 +340,11 @@ export default function CreativeModifyPage() {
314
  )}
315
 
316
  {/* Analysis Display */}
317
- <CreativeAnalysis analysis={analysis} />
 
 
 
 
318
 
319
  {/* Modification Form */}
320
  <ModificationForm
@@ -496,6 +526,22 @@ export default function CreativeModifyPage() {
496
  )}
497
  </div>
498
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
499
  </div>
500
  );
501
  }
 
4
  import { CreativeUploader } from "@/components/creative/CreativeUploader";
5
  import { CreativeAnalysis } from "@/components/creative/CreativeAnalysis";
6
  import { ModificationForm } from "@/components/creative/ModificationForm";
7
+ import { CorrectionModal } from "@/components/generation/CorrectionModal";
8
  import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/Card";
9
  import {
10
  uploadCreative,
 
16
  CreativeAnalysisData,
17
  ModificationMode,
18
  ModifiedImageResult,
19
+ ImageCorrectResponse,
20
  } from "@/types/api";
21
 
22
  type WorkflowStep = "upload" | "analysis" | "modify" | "result";
 
44
  const [result, setResult] = useState<ModifiedImageResult | null>(null);
45
  const [generatedPrompt, setGeneratedPrompt] = useState<string | null>(null);
46
 
47
+ // Correction modal state
48
+ const [isCorrectionModalOpen, setIsCorrectionModalOpen] = useState(false);
49
+ const [correctionInstructions, setCorrectionInstructions] = useState("");
50
+ const [uploadedAdId, setUploadedAdId] = useState<string | null>(null);
51
+
52
  // Handle image ready from uploader
53
  const handleImageReady = useCallback(async (imageUrl: string, file?: File) => {
54
  setIsLoading(true);
 
66
  throw new Error(uploadResponse.error || "Failed to upload image");
67
  }
68
  setOriginalImageUrl(uploadResponse.image_url);
69
+
70
  // Now analyze using the uploaded URL
71
  analysisResponse = await analyzeCreativeByUrl({
72
  image_url: uploadResponse.image_url,
 
174
  // Handle step click for navigation
175
  const handleStepClick = useCallback((stepKey: string) => {
176
  if (!isStepAccessible(stepKey)) return;
177
+
178
  if (stepKey === "upload") {
179
  handleBackToUpload();
180
  } else if (stepKey === "analysis" && currentStep === "result") {
 
182
  }
183
  }, [currentStep, handleBackToUpload, handleBackToAnalysis]);
184
 
185
+ // Handle improvement click - open correction modal
186
+ const handleImprovementClick = useCallback((improvement: string) => {
187
+ setCorrectionInstructions(improvement);
188
+ setIsCorrectionModalOpen(true);
189
+ }, []);
190
+
191
+ // Handle multiple improvements - combine them and apply
192
+ const handleMultipleImprovementsApply = useCallback((improvements: string[]) => {
193
+ const combined = improvements.join(". ");
194
+ setCorrectionInstructions(combined);
195
+ setIsCorrectionModalOpen(true);
196
+ }, []);
197
+
198
+ // Handle correction success
199
+ const handleCorrectionSuccess = useCallback((correctionResult: ImageCorrectResponse, keepCorrected: boolean) => {
200
+ // Update the original image URL with the corrected image ONLY if user selected it
201
+ if (keepCorrected && correctionResult.corrected_image?.image_url) {
202
+ setOriginalImageUrl(correctionResult.corrected_image.image_url);
203
+ // Re-analyze the corrected image
204
+ handleImageReady(correctionResult.corrected_image.image_url);
205
+ }
206
+ }, [handleImageReady]);
207
+
208
  return (
209
  <div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 to-cyan-50">
210
  <div className="container mx-auto px-4 py-8">
 
227
  { key: "analysis", label: "Analysis & Modify" },
228
  { key: "result", label: "Result" },
229
  ].map((step, index) => {
230
+ const isActive = currentStep === step.key ||
231
  (step.key === "analysis" && currentStep === "analysis");
232
  const isCompleted = getStepIndex(currentStep) > index;
233
  const canClick = isStepAccessible(step.key) && step.key !== currentStep;
234
+
235
  return (
236
  <React.Fragment key={step.key}>
237
  {index > 0 && (
238
  <div
239
+ className={`w-12 h-0.5 ${isCompleted || isActive ? "bg-blue-500" : "bg-gray-300"
240
+ }`}
 
241
  />
242
  )}
243
  <button
244
  type="button"
245
  onClick={() => handleStepClick(step.key)}
246
  disabled={!canClick}
247
+ className={`flex flex-col items-center gap-1 transition-all ${canClick ? "cursor-pointer hover:scale-105" : "cursor-default"
248
+ }`}
 
249
  >
250
  <div
251
+ className={`flex items-center justify-center w-10 h-10 rounded-full text-sm font-medium transition-all ${isActive
252
+ ? "bg-blue-500 text-white shadow-lg"
253
+ : isCompleted
 
254
  ? "bg-blue-100 text-blue-600 hover:bg-blue-200"
255
  : "bg-gray-200 text-gray-500"
256
+ }`}
257
  >
258
  {isCompleted ? (
259
  <svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
 
263
  index + 1
264
  )}
265
  </div>
266
+ <span className={`text-xs font-medium ${isActive ? "text-blue-600" : isCompleted ? "text-blue-500" : "text-gray-500"
267
+ }`}>
 
268
  {step.label}
269
  </span>
270
  </button>
 
340
  )}
341
 
342
  {/* Analysis Display */}
343
+ <CreativeAnalysis
344
+ analysis={analysis}
345
+ onImprovementClick={handleImprovementClick}
346
+ onMultipleImprovementsApply={handleMultipleImprovementsApply}
347
+ />
348
 
349
  {/* Modification Form */}
350
  <ModificationForm
 
526
  )}
527
  </div>
528
  </div>
529
+
530
+ {/* Correction Modal - Note: CorrectionModal expects an adId but we're using image URL */}
531
+ {/* For now, we'll pass a placeholder ID since the correction API needs to be adapted */}
532
+ {isCorrectionModalOpen && originalImageUrl && (
533
+ <CorrectionModal
534
+ isOpen={isCorrectionModalOpen}
535
+ onClose={() => {
536
+ setIsCorrectionModalOpen(false);
537
+ setCorrectionInstructions("");
538
+ }}
539
+ adId={uploadedAdId || "temp-id"}
540
+ imageUrl={originalImageUrl}
541
+ initialInstructions={correctionInstructions}
542
+ onSuccess={handleCorrectionSuccess}
543
+ />
544
+ )}
545
  </div>
546
  );
547
  }
frontend/components/creative/CreativeAnalysis.tsx CHANGED
@@ -6,11 +6,36 @@ import type { CreativeAnalysisData } from "@/types/api";
6
 
7
  interface CreativeAnalysisProps {
8
  analysis: CreativeAnalysisData;
 
 
9
  }
10
 
11
  export const CreativeAnalysis: React.FC<CreativeAnalysisProps> = ({
12
  analysis,
 
 
13
  }) => {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  return (
15
  <Card variant="glass">
16
  <CardHeader>
@@ -105,14 +130,63 @@ export const CreativeAnalysis: React.FC<CreativeAnalysisProps> = ({
105
 
106
  {/* Areas for Improvement */}
107
  <div>
108
- <h4 className="text-sm font-semibold text-gray-700 mb-2">Areas for Improvement</h4>
109
- <ul className="space-y-1">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
110
  {analysis.areas_for_improvement.map((area, index) => (
111
- <li key={index} className="flex items-start gap-2 text-sm text-gray-600">
112
- <svg className="w-4 h-4 text-amber-500 mt-0.5 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
113
- <path fillRule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
114
- </svg>
115
- {area}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
116
  </li>
117
  ))}
118
  </ul>
 
6
 
7
  interface CreativeAnalysisProps {
8
  analysis: CreativeAnalysisData;
9
+ onImprovementClick?: (improvement: string) => void;
10
+ onMultipleImprovementsApply?: (improvements: string[]) => void;
11
  }
12
 
13
  export const CreativeAnalysis: React.FC<CreativeAnalysisProps> = ({
14
  analysis,
15
+ onImprovementClick,
16
+ onMultipleImprovementsApply,
17
  }) => {
18
+ const [selectedImprovements, setSelectedImprovements] = React.useState<Set<number>>(new Set());
19
+
20
+ const toggleImprovement = (index: number) => {
21
+ setSelectedImprovements(prev => {
22
+ const newSet = new Set(prev);
23
+ if (newSet.has(index)) {
24
+ newSet.delete(index);
25
+ } else {
26
+ newSet.add(index);
27
+ }
28
+ return newSet;
29
+ });
30
+ };
31
+
32
+ const handleApplySelected = () => {
33
+ const selected = Array.from(selectedImprovements).map(idx => analysis.areas_for_improvement[idx]);
34
+ if (selected.length > 0 && onMultipleImprovementsApply) {
35
+ onMultipleImprovementsApply(selected);
36
+ setSelectedImprovements(new Set());
37
+ }
38
+ };
39
  return (
40
  <Card variant="glass">
41
  <CardHeader>
 
130
 
131
  {/* Areas for Improvement */}
132
  <div>
133
+ <div className="flex items-center justify-between mb-2">
134
+ <h4 className="text-sm font-semibold text-gray-700">
135
+ Areas for Improvement
136
+ {onMultipleImprovementsApply && (
137
+ <span className="ml-2 text-xs text-gray-500 font-normal">
138
+ (Select multiple and apply together)
139
+ </span>
140
+ )}
141
+ </h4>
142
+ {onMultipleImprovementsApply && selectedImprovements.size > 0 && (
143
+ <button
144
+ type="button"
145
+ onClick={handleApplySelected}
146
+ className="px-3 py-1 bg-amber-500 text-white text-xs font-medium rounded-lg hover:bg-amber-600 transition-colors"
147
+ >
148
+ Apply {selectedImprovements.size} Selected
149
+ </button>
150
+ )}
151
+ </div>
152
+ <ul className="space-y-2">
153
  {analysis.areas_for_improvement.map((area, index) => (
154
+ <li key={index}>
155
+ <div
156
+ className={`flex items-start gap-2 text-sm p-3 rounded-lg transition-all border ${
157
+ selectedImprovements.has(index)
158
+ ? "bg-amber-50 border-amber-300"
159
+ : onMultipleImprovementsApply
160
+ ? "hover:bg-amber-50 hover:border-amber-200 border-transparent cursor-pointer"
161
+ : onImprovementClick
162
+ ? "hover:bg-amber-50 hover:border-amber-300 border-transparent cursor-pointer"
163
+ : "border-transparent cursor-default"
164
+ }`}
165
+ >
166
+ {onMultipleImprovementsApply && (
167
+ <input
168
+ type="checkbox"
169
+ checked={selectedImprovements.has(index)}
170
+ onChange={() => toggleImprovement(index)}
171
+ className="w-4 h-4 text-amber-600 border-gray-300 rounded focus:ring-amber-500 mt-0.5 cursor-pointer"
172
+ />
173
+ )}
174
+ <svg className="w-4 h-4 text-amber-500 mt-0.5 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
175
+ <path fillRule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
176
+ </svg>
177
+ <span className="text-gray-600 flex-1">{area}</span>
178
+ {!onMultipleImprovementsApply && onImprovementClick && (
179
+ <button
180
+ type="button"
181
+ onClick={() => onImprovementClick(area)}
182
+ className="text-amber-500 hover:text-amber-600 transition-colors"
183
+ >
184
+ <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
185
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
186
+ </svg>
187
+ </button>
188
+ )}
189
+ </div>
190
  </li>
191
  ))}
192
  </ul>
frontend/components/creative/ModificationForm.tsx CHANGED
@@ -34,6 +34,8 @@ interface ModificationFormProps {
34
  isLoading: boolean;
35
  }
36
 
 
 
37
  export const ModificationForm: React.FC<ModificationFormProps> = ({
38
  angle,
39
  concept,
@@ -62,13 +64,12 @@ export const ModificationForm: React.FC<ModificationFormProps> = ({
62
  getAllConcepts(),
63
  ]);
64
 
65
- // Transform angles data
66
  const angles: AngleOption[] = [];
67
  Object.entries(anglesData.categories).forEach(([categoryKey, category]) => {
68
  category.angles.forEach((a) => {
69
  angles.push({
70
  value: a.name,
71
- label: `${a.name} (${a.trigger})`,
72
  trigger: a.trigger,
73
  category: category.name,
74
  });
@@ -76,13 +77,12 @@ export const ModificationForm: React.FC<ModificationFormProps> = ({
76
  });
77
  setAngleOptions(angles);
78
 
79
- // Transform concepts data
80
  const concepts: ConceptOption[] = [];
81
  Object.entries(conceptsData.categories).forEach(([categoryKey, category]) => {
82
  category.concepts.forEach((c) => {
83
  concepts.push({
84
  value: c.name,
85
- label: `${c.name}`,
86
  structure: c.structure,
87
  category: category.name,
88
  });
@@ -101,291 +101,280 @@ export const ModificationForm: React.FC<ModificationFormProps> = ({
101
 
102
  const isValid = angle.trim() || concept.trim();
103
 
104
- // Group angles by category for the dropdown
105
  const groupedAngles = angleOptions.reduce((acc, angle) => {
106
- if (!acc[angle.category]) {
107
- acc[angle.category] = [];
108
- }
109
  acc[angle.category].push(angle);
110
  return acc;
111
  }, {} as Record<string, AngleOption[]>);
112
 
113
- // Group concepts by category for the dropdown
114
  const groupedConcepts = conceptOptions.reduce((acc, concept) => {
115
- if (!acc[concept.category]) {
116
- acc[concept.category] = [];
117
- }
118
  acc[concept.category].push(concept);
119
  return acc;
120
  }, {} as Record<string, ConceptOption[]>);
121
 
122
  return (
123
- <Card variant="glass">
124
- <CardHeader>
125
- <CardTitle>Apply New Angle / Concept</CardTitle>
126
- <CardDescription>
127
- Select or enter a new angle or concept to apply to your creative
 
 
 
 
 
 
 
128
  </CardDescription>
129
  </CardHeader>
130
- <CardContent className="space-y-6">
131
- {/* Angle Selection */}
132
- <div>
133
- <div className="flex items-center justify-between mb-2">
134
- <label className="block text-sm font-semibold text-gray-700">
135
- New Angle <span className="text-gray-400 font-normal">(psychological WHY)</span>
136
- </label>
137
- <div className="flex gap-1">
138
- <button
139
- type="button"
140
- onClick={() => setAngleInputMode("dropdown")}
141
- className={`px-2 py-1 text-xs rounded ${
142
- angleInputMode === "dropdown"
143
- ? "bg-orange-100 text-orange-700"
144
- : "bg-gray-100 text-gray-600 hover:bg-gray-200"
145
- }`}
146
- >
147
- Select
148
- </button>
149
- <button
150
- type="button"
151
- onClick={() => setAngleInputMode("custom")}
152
- className={`px-2 py-1 text-xs rounded ${
153
- angleInputMode === "custom"
154
- ? "bg-orange-100 text-orange-700"
155
- : "bg-gray-100 text-gray-600 hover:bg-gray-200"
156
- }`}
157
- >
158
- Custom
159
- </button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
160
  </div>
 
 
 
 
161
  </div>
162
-
163
- {angleInputMode === "dropdown" ? (
164
- <select
165
- value={angle}
166
- onChange={(e) => onAngleChange(e.target.value)}
167
- disabled={loadingOptions}
168
- 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-orange-500 focus:border-orange-500 transition-all"
169
- >
170
- <option value="">-- Select an angle --</option>
171
- {Object.entries(groupedAngles).map(([category, angles]) => (
172
- <optgroup key={category} label={category}>
173
- {angles.map((a) => (
174
- <option key={a.value} value={a.value}>
175
- {a.label}
176
- </option>
177
- ))}
178
- </optgroup>
179
- ))}
180
- </select>
181
- ) : (
182
- <input
183
- type="text"
184
- value={angle}
185
- onChange={(e) => onAngleChange(e.target.value)}
186
- placeholder="e.g., Fear of Missing Out, Social Proof, Urgency..."
187
- 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-orange-500 focus:border-orange-500 transition-all"
188
- />
189
- )}
190
-
191
- <p className="text-xs text-gray-500 mt-1">
192
- The psychological trigger that motivates action
193
- </p>
194
- </div>
195
 
196
- {/* Concept Selection */}
197
- <div>
198
- <div className="flex items-center justify-between mb-2">
199
- <label className="block text-sm font-semibold text-gray-700">
200
- New Concept <span className="text-gray-400 font-normal">(visual HOW)</span>
201
- </label>
202
- <div className="flex gap-1">
203
- <button
204
- type="button"
205
- onClick={() => setConceptInputMode("dropdown")}
206
- className={`px-2 py-1 text-xs rounded ${
207
- conceptInputMode === "dropdown"
208
- ? "bg-green-100 text-green-700"
209
- : "bg-gray-100 text-gray-600 hover:bg-gray-200"
210
- }`}
211
- >
212
- Select
213
- </button>
214
- <button
215
- type="button"
216
- onClick={() => setConceptInputMode("custom")}
217
- className={`px-2 py-1 text-xs rounded ${
218
- conceptInputMode === "custom"
219
- ? "bg-green-100 text-green-700"
220
- : "bg-gray-100 text-gray-600 hover:bg-gray-200"
221
- }`}
222
- >
223
- Custom
224
- </button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
225
  </div>
 
 
 
 
226
  </div>
227
-
228
- {conceptInputMode === "dropdown" ? (
229
- <select
230
- value={concept}
231
- onChange={(e) => onConceptChange(e.target.value)}
232
- disabled={loadingOptions}
233
- 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-green-500 focus:border-green-500 transition-all"
234
- >
235
- <option value="">-- Select a concept --</option>
236
- {Object.entries(groupedConcepts).map(([category, concepts]) => (
237
- <optgroup key={category} label={category}>
238
- {concepts.map((c) => (
239
- <option key={c.value} value={c.value}>
240
- {c.label} - {c.structure}
241
- </option>
242
- ))}
243
- </optgroup>
244
- ))}
245
- </select>
246
- ) : (
247
- <input
248
- type="text"
249
- value={concept}
250
- onChange={(e) => onConceptChange(e.target.value)}
251
- placeholder="e.g., Testimonial, Before/After, Lifestyle Scene..."
252
- 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-green-500 focus:border-green-500 transition-all"
253
- />
254
- )}
255
-
256
- <p className="text-xs text-gray-500 mt-1">
257
- The creative format or style to present the message
258
- </p>
259
  </div>
260
 
261
  {/* Mode Selection */}
262
- <div>
263
- <label className="block text-sm font-semibold text-gray-700 mb-3">
264
- Modification Mode
265
  </label>
266
- <div className="grid grid-cols-2 gap-3">
267
  <button
268
  type="button"
269
  onClick={() => onModeChange("modify")}
270
- className={`p-4 rounded-xl border-2 text-left transition-all ${
271
- mode === "modify"
272
- ? "border-blue-500 bg-blue-50"
273
- : "border-gray-200 hover:border-gray-300"
274
- }`}
275
  >
276
- <div className="flex items-center gap-2 mb-1">
277
- <svg
278
- className={`w-5 h-5 ${mode === "modify" ? "text-blue-500" : "text-gray-400"}`}
279
- fill="none"
280
- stroke="currentColor"
281
- viewBox="0 0 24 24"
282
- >
283
- <path
284
- strokeLinecap="round"
285
- strokeLinejoin="round"
286
- strokeWidth={2}
287
- d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"
288
- />
289
- </svg>
290
- <span className="font-semibold text-gray-900">Modify</span>
291
  </div>
292
- <p className="text-xs text-gray-500">
293
- Make targeted changes while preserving most of the original image
 
294
  </p>
295
  </button>
 
296
  <button
297
  type="button"
298
  onClick={() => onModeChange("inspired")}
299
- className={`p-4 rounded-xl border-2 text-left transition-all ${
300
- mode === "inspired"
301
- ? "border-purple-500 bg-purple-50"
302
- : "border-gray-200 hover:border-gray-300"
303
- }`}
304
  >
305
- <div className="flex items-center gap-2 mb-1">
306
- <svg
307
- className={`w-5 h-5 ${mode === "inspired" ? "text-purple-500" : "text-gray-400"}`}
308
- fill="none"
309
- stroke="currentColor"
310
- viewBox="0 0 24 24"
311
- >
312
- <path
313
- strokeLinecap="round"
314
- strokeLinejoin="round"
315
- strokeWidth={2}
316
- d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"
317
- />
318
- </svg>
319
- <span className="font-semibold text-gray-900">Inspired</span>
320
  </div>
321
- <p className="text-xs text-gray-500">
322
- Generate a completely new image inspired by the original
 
323
  </p>
324
  </button>
325
  </div>
326
  </div>
327
 
328
  {/* Image Model Selection */}
329
- <Select
330
- label="Image Model"
331
- value={imageModel || ""}
332
- onChange={(e) => onImageModelChange(e.target.value || null)}
333
- options={IMAGE_MODELS.map((model) => ({
334
- value: model.value,
335
- label: model.label,
336
- }))}
337
- />
338
-
339
- {/* Info Box */}
340
- <div className="bg-gradient-to-r from-blue-50 to-cyan-50 border border-blue-200 rounded-xl p-4">
341
- <p className="text-sm text-gray-700">
342
- <strong>
343
- {mode === "modify" ? "Modify Mode:" : "Inspired Mode:"}
344
- </strong>{" "}
345
- {mode === "modify"
346
- ? "The AI will make targeted adjustments to apply your new angle/concept while keeping ~80-95% of the original image intact."
347
- : "The AI will generate a completely new creative that captures the style and essence of the original while fully applying your new angle/concept."}
348
- </p>
349
  </div>
350
 
351
- {/* Submit Button */}
352
- <button
353
- type="button"
354
- onClick={onSubmit}
355
- disabled={!isValid || isLoading}
356
- className="w-full bg-gradient-to-r from-blue-500 to-cyan-500 text-white font-bold py-4 px-6 rounded-xl hover:from-blue-600 hover:to-cyan-600 transition-all duration-300 shadow-lg hover:shadow-xl disabled:opacity-50 disabled:cursor-not-allowed"
357
- >
358
- {isLoading ? (
359
- <span className="flex items-center justify-center gap-2">
360
- <svg className="animate-spin h-5 w-5" viewBox="0 0 24 24">
361
- <circle
362
- className="opacity-25"
363
- cx="12"
364
- cy="12"
365
- r="10"
366
- stroke="currentColor"
367
- strokeWidth="4"
368
- fill="none"
369
- />
370
- <path
371
- className="opacity-75"
372
- fill="currentColor"
373
- d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
374
- />
375
- </svg>
376
- Generating...
377
  </span>
378
- ) : (
379
- `Generate ${mode === "modify" ? "Modified" : "Inspired"} Creative`
380
- )}
381
- </button>
382
 
383
- {!isValid && (
384
- <p className="text-xs text-amber-600 text-center">
385
- Please select or enter at least one angle or concept to continue
386
- </p>
387
- )}
 
 
388
  </CardContent>
389
  </Card>
390
  );
391
  };
 
 
 
 
 
 
 
 
34
  isLoading: boolean;
35
  }
36
 
37
+ import { Brain, Sparkles, Wand2, Lightbulb, ChevronDown, Check, Info } from "lucide-react";
38
+
39
  export const ModificationForm: React.FC<ModificationFormProps> = ({
40
  angle,
41
  concept,
 
64
  getAllConcepts(),
65
  ]);
66
 
 
67
  const angles: AngleOption[] = [];
68
  Object.entries(anglesData.categories).forEach(([categoryKey, category]) => {
69
  category.angles.forEach((a) => {
70
  angles.push({
71
  value: a.name,
72
+ label: a.name,
73
  trigger: a.trigger,
74
  category: category.name,
75
  });
 
77
  });
78
  setAngleOptions(angles);
79
 
 
80
  const concepts: ConceptOption[] = [];
81
  Object.entries(conceptsData.categories).forEach(([categoryKey, category]) => {
82
  category.concepts.forEach((c) => {
83
  concepts.push({
84
  value: c.name,
85
+ label: c.name,
86
  structure: c.structure,
87
  category: category.name,
88
  });
 
101
 
102
  const isValid = angle.trim() || concept.trim();
103
 
 
104
  const groupedAngles = angleOptions.reduce((acc, angle) => {
105
+ if (!acc[angle.category]) acc[angle.category] = [];
 
 
106
  acc[angle.category].push(angle);
107
  return acc;
108
  }, {} as Record<string, AngleOption[]>);
109
 
 
110
  const groupedConcepts = conceptOptions.reduce((acc, concept) => {
111
+ if (!acc[concept.category]) acc[concept.category] = [];
 
 
112
  acc[concept.category].push(concept);
113
  return acc;
114
  }, {} as Record<string, ConceptOption[]>);
115
 
116
  return (
117
+ <Card variant="glass" className="overflow-hidden border-none shadow-2xl p-0">
118
+ <CardHeader className="bg-gradient-to-r from-blue-600/10 to-cyan-500/10 border-b border-blue-500/10 p-8 pb-10">
119
+ <div className="flex items-center gap-4 mb-3">
120
+ <div className="p-2.5 bg-blue-600 rounded-xl shadow-lg shadow-blue-500/30">
121
+ <Sparkles className="w-6 h-6 text-white" />
122
+ </div>
123
+ <CardTitle className="text-3xl font-black tracking-tight text-gray-900 border-none p-0 m-0 bg-transparent flex items-center">
124
+ Refine & Transform
125
+ </CardTitle>
126
+ </div>
127
+ <CardDescription className="text-gray-600 text-lg font-medium leading-relaxed max-w-2xl">
128
+ Apply powerful psychological angles and visual concepts to your creative.
129
  </CardDescription>
130
  </CardHeader>
131
+
132
+ <CardContent className="p-8 pt-10 space-y-10">
133
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-8">
134
+ {/* Angle Selection */}
135
+ <div className="space-y-4">
136
+ <div className="flex items-center justify-between">
137
+ <div className="flex items-center gap-2">
138
+ <Brain className="w-4 h-4 text-orange-500" />
139
+ <label className="text-sm font-bold text-gray-700 uppercase tracking-wider">
140
+ Psychological Angle
141
+ </label>
142
+ </div>
143
+ <div className="inline-flex p-1 bg-gray-100 rounded-lg">
144
+ <button
145
+ type="button"
146
+ onClick={() => setAngleInputMode("dropdown")}
147
+ className={`px-3 py-1 text-xs font-bold rounded-md transition-all ${angleInputMode === "dropdown" ? "bg-white text-orange-600 shadow-sm" : "text-gray-500 hover:text-gray-700"
148
+ }`}
149
+ >
150
+ Presets
151
+ </button>
152
+ <button
153
+ type="button"
154
+ onClick={() => setAngleInputMode("custom")}
155
+ className={`px-3 py-1 text-xs font-bold rounded-md transition-all ${angleInputMode === "custom" ? "bg-white text-orange-600 shadow-sm" : "text-gray-500 hover:text-gray-700"
156
+ }`}
157
+ >
158
+ Custom
159
+ </button>
160
+ </div>
161
+ </div>
162
+
163
+ <div className="relative group">
164
+ {angleInputMode === "dropdown" ? (
165
+ <div className="relative">
166
+ <select
167
+ value={angle}
168
+ onChange={(e) => onAngleChange(e.target.value)}
169
+ disabled={loadingOptions}
170
+ className="w-full pl-4 pr-10 py-4 rounded-xl border-2 border-gray-100 bg-gray-50/50 hover:bg-white focus:bg-white focus:border-orange-500 focus:ring-4 focus:ring-orange-500/10 transition-all appearance-none font-medium text-gray-900"
171
+ >
172
+ <option value="">Choose an angle...</option>
173
+ {Object.entries(groupedAngles).map(([category, angles]) => (
174
+ <optgroup key={category} label={category} className="font-bold text-gray-400 uppercase text-[10px]">
175
+ {angles.map((a) => (
176
+ <option key={a.value} value={a.value} className="text-gray-900 font-medium">
177
+ {a.label}
178
+ </option>
179
+ ))}
180
+ </optgroup>
181
+ ))}
182
+ </select>
183
+ <ChevronDown className="absolute right-4 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400 pointer-events-none group-focus-within:text-orange-500 transition-colors" />
184
+ </div>
185
+ ) : (
186
+ <input
187
+ type="text"
188
+ value={angle}
189
+ onChange={(e) => onAngleChange(e.target.value)}
190
+ placeholder="e.g., Scarcity, Fear of Missing Out..."
191
+ className="w-full px-4 py-4 rounded-xl border-2 border-gray-100 bg-gray-50/50 hover:bg-white focus:bg-white focus:border-orange-500 focus:ring-4 focus:ring-orange-500/10 transition-all font-medium text-gray-900"
192
+ />
193
+ )}
194
  </div>
195
+ <p className="text-xs text-gray-500 flex items-center gap-1.5 px-1">
196
+ <Info className="w-3 h-3" />
197
+ The "Why" - triggers that motivate user action.
198
+ </p>
199
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
200
 
201
+ {/* Concept Selection */}
202
+ <div className="space-y-4">
203
+ <div className="flex items-center justify-between">
204
+ <div className="flex items-center gap-2">
205
+ <Lightbulb className="w-4 h-4 text-green-500" />
206
+ <label className="text-sm font-bold text-gray-700 uppercase tracking-wider">
207
+ Visual Concept
208
+ </label>
209
+ </div>
210
+ <div className="inline-flex p-1 bg-gray-100 rounded-lg">
211
+ <button
212
+ type="button"
213
+ onClick={() => setConceptInputMode("dropdown")}
214
+ className={`px-3 py-1 text-xs font-bold rounded-md transition-all ${conceptInputMode === "dropdown" ? "bg-white text-green-600 shadow-sm" : "text-gray-500 hover:text-gray-700"
215
+ }`}
216
+ >
217
+ Presets
218
+ </button>
219
+ <button
220
+ type="button"
221
+ onClick={() => setConceptInputMode("custom")}
222
+ className={`px-3 py-1 text-xs font-bold rounded-md transition-all ${conceptInputMode === "custom" ? "bg-white text-green-600 shadow-sm" : "text-gray-500 hover:text-gray-700"
223
+ }`}
224
+ >
225
+ Custom
226
+ </button>
227
+ </div>
228
+ </div>
229
+
230
+ <div className="relative group">
231
+ {conceptInputMode === "dropdown" ? (
232
+ <div className="relative">
233
+ <select
234
+ value={concept}
235
+ onChange={(e) => onConceptChange(e.target.value)}
236
+ disabled={loadingOptions}
237
+ className="w-full pl-4 pr-10 py-4 rounded-xl border-2 border-gray-100 bg-gray-50/50 hover:bg-white focus:bg-white focus:border-green-500 focus:ring-4 focus:ring-green-500/10 transition-all appearance-none font-medium text-gray-900"
238
+ >
239
+ <option value="">Choose a concept...</option>
240
+ {Object.entries(groupedConcepts).map(([category, concepts]) => (
241
+ <optgroup key={category} label={category} className="font-bold text-gray-400 uppercase text-[10px]">
242
+ {concepts.map((c) => (
243
+ <option key={c.value} value={c.value} className="text-gray-900 font-medium">
244
+ {c.label}
245
+ </option>
246
+ ))}
247
+ </optgroup>
248
+ ))}
249
+ </select>
250
+ <ChevronDown className="absolute right-4 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400 pointer-events-none group-focus-within:text-green-500 transition-colors" />
251
+ </div>
252
+ ) : (
253
+ <input
254
+ type="text"
255
+ value={concept}
256
+ onChange={(e) => onConceptChange(e.target.value)}
257
+ placeholder="e.g., Before/After Comparison, User Testimonial..."
258
+ className="w-full px-4 py-4 rounded-xl border-2 border-gray-100 bg-gray-50/50 hover:bg-white focus:bg-white focus:border-green-500 focus:ring-4 focus:ring-green-500/10 transition-all font-medium text-gray-900"
259
+ />
260
+ )}
261
  </div>
262
+ <p className="text-xs text-gray-500 flex items-center gap-1.5 px-1">
263
+ <Info className="w-3 h-3" />
264
+ The "How" - visual structure and format.
265
+ </p>
266
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
267
  </div>
268
 
269
  {/* Mode Selection */}
270
+ <div className="space-y-4 pt-4 border-t border-gray-100">
271
+ <label className="text-sm font-bold text-gray-700 uppercase tracking-wider block">
272
+ Transformation Mode
273
  </label>
274
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
275
  <button
276
  type="button"
277
  onClick={() => onModeChange("modify")}
278
+ className={`group p-5 rounded-2xl border-2 text-left transition-all ${mode === "modify"
279
+ ? "border-blue-500 bg-blue-50/50 shadow-md ring-4 ring-blue-500/5"
280
+ : "border-gray-100 bg-gray-50/30 hover:border-gray-200 hover:bg-gray-50/60"
281
+ }`}
 
282
  >
283
+ <div className="flex items-center justify-between mb-3">
284
+ <div className={`p-2 rounded-lg ${mode === "modify" ? "bg-blue-500 text-white" : "bg-gray-200 text-gray-500 group-hover:bg-gray-300 transition-colors"}`}>
285
+ <Wand2 className="w-5 h-5" />
286
+ </div>
287
+ {mode === "modify" && <Check className="w-5 h-5 text-blue-500" />}
 
 
 
 
 
 
 
 
 
 
288
  </div>
289
+ <h4 className={`font-bold text-lg mb-1 ${mode === "modify" ? "text-blue-900" : "text-gray-900"}`}>Modify Image</h4>
290
+ <p className="text-sm text-gray-600 leading-relaxed">
291
+ Smart edits to existing elements. Preserves ~90% of your original creative while applying the new angle.
292
  </p>
293
  </button>
294
+
295
  <button
296
  type="button"
297
  onClick={() => onModeChange("inspired")}
298
+ className={`group p-5 rounded-2xl border-2 text-left transition-all ${mode === "inspired"
299
+ ? "border-purple-500 bg-purple-50/50 shadow-md ring-4 ring-purple-500/5"
300
+ : "border-gray-100 bg-gray-50/30 hover:border-gray-200 hover:bg-gray-50/60"
301
+ }`}
 
302
  >
303
+ <div className="flex items-center justify-between mb-3">
304
+ <div className={`p-2 rounded-lg ${mode === "inspired" ? "bg-purple-500 text-white" : "bg-gray-200 text-gray-500 group-hover:bg-gray-300 transition-colors"}`}>
305
+ <Sparkles className="w-5 h-5" />
306
+ </div>
307
+ {mode === "inspired" && <Check className="w-5 h-5 text-purple-500" />}
 
 
 
 
 
 
 
 
 
 
308
  </div>
309
+ <h4 className={`font-bold text-lg mb-1 ${mode === "inspired" ? "text-purple-900" : "text-gray-900"}`}>Inspired Creation</h4>
310
+ <p className="text-sm text-gray-600 leading-relaxed">
311
+ Generates a fresh creative from scratch using the original's style and essence as inspiration.
312
  </p>
313
  </button>
314
  </div>
315
  </div>
316
 
317
  {/* Image Model Selection */}
318
+ <div className="space-y-4 pt-4 border-t border-gray-100">
319
+ <label className="text-sm font-bold text-gray-700 uppercase tracking-wider block">
320
+ AI Engine
321
+ </label>
322
+ <div className="relative group max-w-sm">
323
+ <select
324
+ value={imageModel || ""}
325
+ onChange={(e) => onImageModelChange(e.target.value || null)}
326
+ className="w-full pl-4 pr-10 py-3 rounded-xl border-2 border-gray-100 bg-gray-50/50 hover:bg-white focus:bg-white focus:border-blue-500 focus:ring-4 focus:ring-blue-500/10 transition-all appearance-none font-medium text-gray-900"
327
+ >
328
+ {IMAGE_MODELS.map((model) => (
329
+ <option key={model.value} value={model.value}>
330
+ {model.label}
331
+ </option>
332
+ ))}
333
+ </select>
334
+ <ChevronDown className="absolute right-4 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400 pointer-events-none group-focus-within:text-blue-500 transition-colors" />
335
+ </div>
 
 
336
  </div>
337
 
338
+ {/* Footer / Submit */}
339
+ <div className="pt-6">
340
+ <button
341
+ type="button"
342
+ onClick={onSubmit}
343
+ disabled={!isValid || isLoading}
344
+ className={`w-full relative group overflow-hidden bg-gradient-to-r ${mode === "modify" ? "from-blue-600 to-cyan-500" : "from-purple-600 to-pink-500"
345
+ } text-white font-black text-lg py-5 px-8 rounded-2xl transition-all duration-300 shadow-xl hover:shadow-2xl hover:-translate-y-1 disabled:opacity-50 disabled:translate-y-0 disabled:shadow-none`}
346
+ >
347
+ <div className="absolute inset-0 bg-white/20 translate-y-full group-hover:translate-y-0 transition-transform duration-300" />
348
+ <span className="flex items-center justify-center gap-3 relative z-10">
349
+ {isLoading ? (
350
+ <>
351
+ <Loader2 className="animate-spin h-6 w-6" />
352
+ Bringing your vision to life...
353
+ </>
354
+ ) : (
355
+ <>
356
+ {mode === "modify" ? <Wand2 className="w-6 h-6" /> : <Sparkles className="w-6 h-6" />}
357
+ Generate {mode === "modify" ? "Modified" : "Inspired"} Creative
358
+ </>
359
+ )}
 
 
 
 
360
  </span>
361
+ </button>
 
 
 
362
 
363
+ {!isValid && (
364
+ <div className="mt-4 flex items-center justify-center gap-2 text-amber-600 animate-pulse">
365
+ <Info className="w-4 h-4" />
366
+ <p className="text-sm font-bold">Please select or enter at least one angle or concept</p>
367
+ </div>
368
+ )}
369
+ </div>
370
  </CardContent>
371
  </Card>
372
  );
373
  };
374
+
375
+ const Loader2 = ({ className }: { className?: string }) => (
376
+ <svg className={className} viewBox="0 0 24 24">
377
+ <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" fill="none" />
378
+ <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
379
+ </svg>
380
+ );
frontend/components/generation/CorrectionModal.tsx CHANGED
@@ -1,7 +1,7 @@
1
  "use client";
2
 
3
  import React, { useState, useEffect } from "react";
4
- import { X, Wand2, Image as ImageIcon, CheckCircle2, AlertCircle, Loader2, Sparkles } from "lucide-react";
5
  import { correctImage } from "@/lib/api/endpoints";
6
  import type { ImageCorrectResponse, AdCreativeDB } from "@/types/api";
7
  import { ProgressBar } from "@/components/ui/ProgressBar";
@@ -13,8 +13,10 @@ interface CorrectionModalProps {
13
  isOpen: boolean;
14
  onClose: () => void;
15
  adId: string;
 
 
16
  ad?: AdCreativeDB | null;
17
- onSuccess?: (result: ImageCorrectResponse) => void;
18
  }
19
 
20
  type CorrectionStep = "idle" | "input" | "analyzing" | "correcting" | "regenerating" | "complete" | "error";
@@ -23,6 +25,8 @@ export const CorrectionModal: React.FC<CorrectionModalProps> = ({
23
  isOpen,
24
  onClose,
25
  adId,
 
 
26
  ad,
27
  onSuccess,
28
  }) => {
@@ -32,6 +36,7 @@ export const CorrectionModal: React.FC<CorrectionModalProps> = ({
32
  const [error, setError] = useState<string | null>(null);
33
  const [userInstructions, setUserInstructions] = useState("");
34
  const [useAutoAnalyze, setUseAutoAnalyze] = useState(false);
 
35
 
36
  useEffect(() => {
37
  if (isOpen) {
@@ -39,7 +44,7 @@ export const CorrectionModal: React.FC<CorrectionModalProps> = ({
39
  setProgress(0);
40
  setResult(null);
41
  setError(null);
42
- setUserInstructions("");
43
  setUseAutoAnalyze(false);
44
  } else {
45
  // Reset state when modal closes
@@ -49,6 +54,7 @@ export const CorrectionModal: React.FC<CorrectionModalProps> = ({
49
  setError(null);
50
  setUserInstructions("");
51
  setUseAutoAnalyze(false);
 
52
  }
53
  }, [isOpen]);
54
 
@@ -84,8 +90,9 @@ export const CorrectionModal: React.FC<CorrectionModalProps> = ({
84
  await new Promise((resolve) => setTimeout(resolve, 2000));
85
 
86
  // Actually perform the correction
87
- const response = await correctImage({
88
  image_id: adId,
 
89
  user_instructions: userInstructions || undefined,
90
  auto_analyze: useAutoAnalyze,
91
  });
@@ -154,8 +161,8 @@ export const CorrectionModal: React.FC<CorrectionModalProps> = ({
154
  <div>
155
  <h2 className="text-xl font-bold text-gray-900">Correct Image</h2>
156
  <p className="text-sm text-gray-500">
157
- {step === "input"
158
- ? "Specify what you want to correct"
159
  : "Analyzing and correcting your ad creative"}
160
  </p>
161
  </div>
@@ -257,22 +264,104 @@ export const CorrectionModal: React.FC<CorrectionModalProps> = ({
257
  </div>
258
  </div>
259
 
260
- {result.corrected_image?.image_url && (
261
- <div className="space-y-3">
262
- <h3 className="font-semibold text-gray-900">Corrected Image</h3>
263
- <img
264
- src={result.corrected_image.image_url}
265
- alt="Corrected"
266
- className="w-full rounded-lg border border-gray-200"
267
- />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
268
  </div>
269
- )}
270
 
271
  {/* Show corrections details */}
272
  {result.corrections && (
273
  <div className="space-y-4">
274
  <h3 className="font-semibold text-gray-900 text-lg">Correction Details</h3>
275
-
276
  {result.corrections.spelling_corrections && result.corrections.spelling_corrections.length > 0 && (
277
  <div>
278
  <h4 className="font-semibold text-gray-900 mb-2 flex items-center gap-2">
@@ -321,11 +410,10 @@ export const CorrectionModal: React.FC<CorrectionModalProps> = ({
321
  <p className="text-gray-600 mt-1">{correction.suggestion}</p>
322
  </div>
323
  {correction.priority && (
324
- <span className={`text-xs px-2 py-1 rounded ${
325
- correction.priority === "high" ? "bg-red-100 text-red-700" :
326
  correction.priority === "medium" ? "bg-yellow-100 text-yellow-700" :
327
- "bg-gray-100 text-gray-700"
328
- }`}>
329
  {correction.priority}
330
  </span>
331
  )}
@@ -345,13 +433,25 @@ export const CorrectionModal: React.FC<CorrectionModalProps> = ({
345
  </div>
346
  )}
347
 
348
- {/* Show message if no corrections were made */}
349
- {(!result.corrections.spelling_corrections || result.corrections.spelling_corrections.length === 0) &&
350
- (!result.corrections.visual_corrections || result.corrections.visual_corrections.length === 0) && (
351
- <div className="bg-gray-50 border border-gray-200 rounded-lg p-3 text-sm text-gray-600">
352
- <p>No specific corrections were identified. The image was regenerated based on your instructions.</p>
 
 
 
 
353
  </div>
354
  )}
 
 
 
 
 
 
 
 
355
  </div>
356
  )}
357
  </div>
@@ -362,8 +462,8 @@ export const CorrectionModal: React.FC<CorrectionModalProps> = ({
362
  <div className="sticky bottom-0 bg-gray-50 border-t border-gray-200 px-6 py-4 rounded-b-2xl">
363
  <div className="flex justify-end gap-3">
364
  {step === "input" && (
365
- <Button
366
- onClick={handleCorrection}
367
  variant="primary"
368
  disabled={!userInstructions && !useAutoAnalyze}
369
  >
@@ -380,13 +480,13 @@ export const CorrectionModal: React.FC<CorrectionModalProps> = ({
380
  onClick={() => {
381
  if (step === "complete" && result) {
382
  // Call onSuccess when user clicks "Done" to reload the ad
383
- onSuccess?.(result);
384
  }
385
  onClose();
386
  }}
387
  variant={step === "complete" ? "primary" : "secondary"}
388
  >
389
- {step === "complete" ? "Done" : "Close"}
390
  </Button>
391
  </div>
392
  </div>
 
1
  "use client";
2
 
3
  import React, { useState, useEffect } from "react";
4
+ import { X, Wand2, Image as ImageIcon, CheckCircle2, AlertCircle, Loader2, Sparkles, Download } from "lucide-react";
5
  import { correctImage } from "@/lib/api/endpoints";
6
  import type { ImageCorrectResponse, AdCreativeDB } from "@/types/api";
7
  import { ProgressBar } from "@/components/ui/ProgressBar";
 
13
  isOpen: boolean;
14
  onClose: () => void;
15
  adId: string;
16
+ imageUrl?: string | null;
17
+ initialInstructions?: string;
18
  ad?: AdCreativeDB | null;
19
+ onSuccess?: (result: ImageCorrectResponse, keepCorrected: boolean) => void;
20
  }
21
 
22
  type CorrectionStep = "idle" | "input" | "analyzing" | "correcting" | "regenerating" | "complete" | "error";
 
25
  isOpen,
26
  onClose,
27
  adId,
28
+ imageUrl,
29
+ initialInstructions = "",
30
  ad,
31
  onSuccess,
32
  }) => {
 
36
  const [error, setError] = useState<string | null>(null);
37
  const [userInstructions, setUserInstructions] = useState("");
38
  const [useAutoAnalyze, setUseAutoAnalyze] = useState(false);
39
+ const [keepCorrected, setKeepCorrected] = useState(true);
40
 
41
  useEffect(() => {
42
  if (isOpen) {
 
44
  setProgress(0);
45
  setResult(null);
46
  setError(null);
47
+ setUserInstructions(initialInstructions);
48
  setUseAutoAnalyze(false);
49
  } else {
50
  // Reset state when modal closes
 
54
  setError(null);
55
  setUserInstructions("");
56
  setUseAutoAnalyze(false);
57
+ setKeepCorrected(true);
58
  }
59
  }, [isOpen]);
60
 
 
90
  await new Promise((resolve) => setTimeout(resolve, 2000));
91
 
92
  // Actually perform the correction
93
+ const response = await correctImage({
94
  image_id: adId,
95
+ image_url: imageUrl || undefined,
96
  user_instructions: userInstructions || undefined,
97
  auto_analyze: useAutoAnalyze,
98
  });
 
161
  <div>
162
  <h2 className="text-xl font-bold text-gray-900">Correct Image</h2>
163
  <p className="text-sm text-gray-500">
164
+ {step === "input"
165
+ ? "Specify what you want to correct"
166
  : "Analyzing and correcting your ad creative"}
167
  </p>
168
  </div>
 
264
  </div>
265
  </div>
266
 
267
+ <div className="space-y-6">
268
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
269
+ {/* Original Image */}
270
+ <div className={`space-y-3 p-4 rounded-2xl border-2 transition-all ${!keepCorrected ? 'border-blue-500 bg-blue-50/50 shadow-lg' : 'border-gray-100 hover:border-gray-200'}`}>
271
+ <div className="flex items-center justify-between">
272
+ <div className="flex items-center gap-2">
273
+ <div className={`w-4 h-4 rounded-full border-2 flex items-center justify-center ${!keepCorrected ? 'border-blue-500 bg-blue-500' : 'border-gray-300'}`}>
274
+ {!keepCorrected && <div className="w-1.5 h-1.5 rounded-full bg-white" />}
275
+ </div>
276
+ <h3 className="font-bold text-gray-900">Original</h3>
277
+ </div>
278
+ <Button
279
+ variant={!keepCorrected ? "primary" : "secondary"}
280
+ size="sm"
281
+ onClick={() => setKeepCorrected(false)}
282
+ >
283
+ {!keepCorrected ? "Selected" : "Keep Original"}
284
+ </Button>
285
+ </div>
286
+ <div className="aspect-square relative rounded-xl overflow-hidden border border-gray-200 cursor-pointer" onClick={() => setKeepCorrected(false)}>
287
+ {(imageUrl || ad?.r2_url || ad?.image_url) ? (
288
+ <img
289
+ src={(imageUrl || ad?.r2_url || ad?.image_url)!}
290
+ alt="Original"
291
+ className="w-full h-full object-cover"
292
+ />
293
+ ) : (
294
+ <div className="w-full h-full bg-gray-50 flex flex-col items-center justify-center text-gray-400">
295
+ <ImageIcon className="h-8 w-8 mb-2" />
296
+ <span className="text-xs">No image</span>
297
+ </div>
298
+ )}
299
+ </div>
300
+ </div>
301
+
302
+ {/* Corrected Image */}
303
+ <div className={`space-y-3 p-4 rounded-2xl border-2 transition-all ${keepCorrected ? 'border-blue-500 bg-blue-50/50 shadow-lg' : 'border-gray-100 hover:border-gray-200'}`}>
304
+ <div className="flex items-center justify-between">
305
+ <div className="flex items-center gap-2">
306
+ <div className={`w-4 h-4 rounded-full border-2 flex items-center justify-center ${keepCorrected ? 'border-blue-500 bg-blue-500' : 'border-gray-300'}`}>
307
+ {keepCorrected && <div className="w-1.5 h-1.5 rounded-full bg-white" />}
308
+ </div>
309
+ <h3 className="font-bold text-gray-900">Corrected</h3>
310
+ </div>
311
+ <div className="flex gap-2">
312
+ <Button
313
+ variant="secondary"
314
+ size="sm"
315
+ onClick={async (e) => {
316
+ e.stopPropagation();
317
+ try {
318
+ const response = await fetch(result.corrected_image!.image_url!);
319
+ const blob = await response.blob();
320
+ const url = window.URL.createObjectURL(blob);
321
+ const link = document.createElement("a");
322
+ link.href = url;
323
+ link.download = result.corrected_image!.filename || "corrected-image.png";
324
+ document.body.appendChild(link);
325
+ link.click();
326
+ document.body.removeChild(link);
327
+ window.URL.revokeObjectURL(url);
328
+ } catch (err) {
329
+ window.open(result.corrected_image!.image_url!, "_blank");
330
+ }
331
+ }}
332
+ >
333
+ <Download className="h-4 w-4" />
334
+ </Button>
335
+ <Button
336
+ variant={keepCorrected ? "primary" : "secondary"}
337
+ size="sm"
338
+ onClick={() => setKeepCorrected(true)}
339
+ >
340
+ {keepCorrected ? "Selected" : "Use Corrected"}
341
+ </Button>
342
+ </div>
343
+ </div>
344
+ <div className="aspect-square relative rounded-xl overflow-hidden border border-gray-200 cursor-pointer" onClick={() => setKeepCorrected(true)}>
345
+ {result.corrected_image?.image_url && (
346
+ <img
347
+ src={result.corrected_image.image_url}
348
+ alt="Corrected"
349
+ className="w-full h-full object-cover"
350
+ />
351
+ )}
352
+ <div className="absolute top-2 right-2 px-2 py-1 bg-green-500 text-white text-xs font-bold rounded-md shadow-sm">
353
+ NEW
354
+ </div>
355
+ </div>
356
+ </div>
357
  </div>
358
+ </div>
359
 
360
  {/* Show corrections details */}
361
  {result.corrections && (
362
  <div className="space-y-4">
363
  <h3 className="font-semibold text-gray-900 text-lg">Correction Details</h3>
364
+
365
  {result.corrections.spelling_corrections && result.corrections.spelling_corrections.length > 0 && (
366
  <div>
367
  <h4 className="font-semibold text-gray-900 mb-2 flex items-center gap-2">
 
410
  <p className="text-gray-600 mt-1">{correction.suggestion}</p>
411
  </div>
412
  {correction.priority && (
413
+ <span className={`text-xs px-2 py-1 rounded ${correction.priority === "high" ? "bg-red-100 text-red-700" :
 
414
  correction.priority === "medium" ? "bg-yellow-100 text-yellow-700" :
415
+ "bg-gray-100 text-gray-700"
416
+ }`}>
417
  {correction.priority}
418
  </span>
419
  )}
 
433
  </div>
434
  )}
435
 
436
+ {result.analysis && (
437
+ <div className="pt-4 border-t border-gray-100">
438
+ <h4 className="font-bold text-gray-900 mb-3 flex items-center gap-2">
439
+ <ImageIcon className="h-5 w-5 text-blue-500" />
440
+ Detailed AI Analysis
441
+ </h4>
442
+ <div className="bg-blue-50/50 border border-blue-100 rounded-xl p-4 text-sm whitespace-pre-wrap leading-relaxed text-gray-700 max-h-60 overflow-y-auto custom-scrollbar">
443
+ {result.analysis}
444
+ </div>
445
  </div>
446
  )}
447
+
448
+ {/* Show message if no corrections were made */}
449
+ {(!result.corrections.spelling_corrections || result.corrections.spelling_corrections.length === 0) &&
450
+ (!result.corrections.visual_corrections || result.corrections.visual_corrections.length === 0) && (
451
+ <div className="bg-gray-50 border border-gray-200 rounded-lg p-3 text-sm text-gray-600">
452
+ <p>No specific corrections were identified. The image was regenerated based on your instructions.</p>
453
+ </div>
454
+ )}
455
  </div>
456
  )}
457
  </div>
 
462
  <div className="sticky bottom-0 bg-gray-50 border-t border-gray-200 px-6 py-4 rounded-b-2xl">
463
  <div className="flex justify-end gap-3">
464
  {step === "input" && (
465
+ <Button
466
+ onClick={handleCorrection}
467
  variant="primary"
468
  disabled={!userInstructions && !useAutoAnalyze}
469
  >
 
480
  onClick={() => {
481
  if (step === "complete" && result) {
482
  // Call onSuccess when user clicks "Done" to reload the ad
483
+ onSuccess?.(result, keepCorrected);
484
  }
485
  onClose();
486
  }}
487
  variant={step === "complete" ? "primary" : "secondary"}
488
  >
489
+ {step === "complete" ? `Use ${keepCorrected ? 'Corrected' : 'Original'}` : "Close"}
490
  </Button>
491
  </div>
492
  </div>
frontend/components/generation/RegenerationModal.tsx CHANGED
@@ -1,7 +1,7 @@
1
  "use client";
2
 
3
  import React, { useState, useEffect } from "react";
4
- import { X, RefreshCw, CheckCircle2, AlertCircle, Loader2, Sparkles, ChevronDown, Check, ArrowLeft } from "lucide-react";
5
  import { regenerateImage, getImageModels, confirmImageSelection } from "@/lib/api/endpoints";
6
  import type { ImageRegenerateResponse, AdCreativeDB, ImageModel } from "@/types/api";
7
  import { ProgressBar } from "@/components/ui/ProgressBar";
@@ -103,7 +103,7 @@ export const RegenerationModal: React.FC<RegenerationModalProps> = ({
103
  }, 400);
104
 
105
  // Actually perform the regeneration (preview mode)
106
- const response = await regenerateImage({
107
  image_id: adId,
108
  image_model: selectedModel,
109
  preview_only: true,
@@ -300,12 +300,11 @@ export const RegenerationModal: React.FC<RegenerationModalProps> = ({
300
  {/* Side by side comparison */}
301
  <div className="grid grid-cols-2 gap-4">
302
  {/* Original Image */}
303
- <div
304
- className={`relative cursor-pointer rounded-xl overflow-hidden border-4 transition-all ${
305
- selectedImage === "original"
306
- ? "border-green-500 ring-4 ring-green-200"
307
- : "border-gray-200 hover:border-gray-400"
308
- }`}
309
  onClick={() => setSelectedImage("original")}
310
  >
311
  <div className="absolute top-3 left-3 z-10">
@@ -320,11 +319,17 @@ export const RegenerationModal: React.FC<RegenerationModalProps> = ({
320
  </div>
321
  </div>
322
  )}
323
- <img
324
- src={result.original_image_url || ad?.r2_url || ad?.image_url || ""}
325
- alt="Original"
326
- className="w-full aspect-square object-cover"
327
- />
 
 
 
 
 
 
328
  <div className="p-3 bg-gray-50">
329
  <p className="text-xs text-gray-500">Model</p>
330
  <p className="text-sm font-medium text-gray-900">
@@ -334,12 +339,11 @@ export const RegenerationModal: React.FC<RegenerationModalProps> = ({
334
  </div>
335
 
336
  {/* New Image */}
337
- <div
338
- className={`relative cursor-pointer rounded-xl overflow-hidden border-4 transition-all ${
339
- selectedImage === "new"
340
- ? "border-green-500 ring-4 ring-green-200"
341
- : "border-gray-200 hover:border-gray-400"
342
- }`}
343
  onClick={() => setSelectedImage("new")}
344
  >
345
  <div className="absolute top-3 left-3 z-10">
@@ -410,8 +414,8 @@ export const RegenerationModal: React.FC<RegenerationModalProps> = ({
410
  <div>
411
  <h3 className="font-semibold text-green-900">Selection Saved!</h3>
412
  <p className="text-sm text-green-700">
413
- {selectedImage === "new"
414
- ? "Your ad has been updated with the new image."
415
  : "The original image has been kept."}
416
  </p>
417
  </div>
@@ -421,15 +425,17 @@ export const RegenerationModal: React.FC<RegenerationModalProps> = ({
421
  {/* Show the selected image */}
422
  <div className="flex justify-center">
423
  <div className="max-w-md">
424
- <img
425
- src={
426
- selectedImage === "new"
427
- ? (result?.regenerated_image?.image_url || "")
428
- : (result?.original_image_url || ad?.r2_url || ad?.image_url || "")
429
- }
430
- alt="Selected"
431
- className="w-full rounded-xl border border-gray-200"
432
- />
 
 
433
  <p className="text-center text-sm text-gray-500 mt-2">
434
  {selectedImage === "new" ? "New image saved" : "Original image kept"}
435
  </p>
@@ -445,12 +451,12 @@ export const RegenerationModal: React.FC<RegenerationModalProps> = ({
445
  {/* Left side buttons */}
446
  <div>
447
  {step === "compare" && (
448
- <Button
449
  onClick={() => {
450
  setStep("input");
451
  setResult(null);
452
  setSelectedImage(null);
453
- }}
454
  variant="secondary"
455
  >
456
  <ArrowLeft className="h-4 w-4 mr-2" />
@@ -462,8 +468,8 @@ export const RegenerationModal: React.FC<RegenerationModalProps> = ({
462
  {/* Right side buttons */}
463
  <div className="flex gap-3">
464
  {step === "input" && (
465
- <Button
466
- onClick={handleRegenerate}
467
  variant="primary"
468
  disabled={!selectedModel || loadingModels}
469
  >
@@ -471,10 +477,10 @@ export const RegenerationModal: React.FC<RegenerationModalProps> = ({
471
  Regenerate Image
472
  </Button>
473
  )}
474
-
475
  {step === "compare" && (
476
- <Button
477
- onClick={handleConfirmSelection}
478
  variant="primary"
479
  disabled={!selectedImage || savingSelection}
480
  >
 
1
  "use client";
2
 
3
  import React, { useState, useEffect } from "react";
4
+ import { X, RefreshCw, CheckCircle2, AlertCircle, Loader2, Sparkles, ChevronDown, Check, ArrowLeft, Image as ImageIcon } from "lucide-react";
5
  import { regenerateImage, getImageModels, confirmImageSelection } from "@/lib/api/endpoints";
6
  import type { ImageRegenerateResponse, AdCreativeDB, ImageModel } from "@/types/api";
7
  import { ProgressBar } from "@/components/ui/ProgressBar";
 
103
  }, 400);
104
 
105
  // Actually perform the regeneration (preview mode)
106
+ const response = await regenerateImage({
107
  image_id: adId,
108
  image_model: selectedModel,
109
  preview_only: true,
 
300
  {/* Side by side comparison */}
301
  <div className="grid grid-cols-2 gap-4">
302
  {/* Original Image */}
303
+ <div
304
+ className={`relative cursor-pointer rounded-xl overflow-hidden border-4 transition-all ${selectedImage === "original"
305
+ ? "border-green-500 ring-4 ring-green-200"
306
+ : "border-gray-200 hover:border-gray-400"
307
+ }`}
 
308
  onClick={() => setSelectedImage("original")}
309
  >
310
  <div className="absolute top-3 left-3 z-10">
 
319
  </div>
320
  </div>
321
  )}
322
+ {(result.original_image_url || ad?.r2_url || ad?.image_url) ? (
323
+ <img
324
+ src={(result.original_image_url || ad?.r2_url || ad?.image_url)!}
325
+ alt="Original"
326
+ className="w-full aspect-square object-cover"
327
+ />
328
+ ) : (
329
+ <div className="w-full aspect-square bg-gray-50 flex items-center justify-center text-gray-400">
330
+ <ImageIcon className="h-8 w-8" />
331
+ </div>
332
+ )}
333
  <div className="p-3 bg-gray-50">
334
  <p className="text-xs text-gray-500">Model</p>
335
  <p className="text-sm font-medium text-gray-900">
 
339
  </div>
340
 
341
  {/* New Image */}
342
+ <div
343
+ className={`relative cursor-pointer rounded-xl overflow-hidden border-4 transition-all ${selectedImage === "new"
344
+ ? "border-green-500 ring-4 ring-green-200"
345
+ : "border-gray-200 hover:border-gray-400"
346
+ }`}
 
347
  onClick={() => setSelectedImage("new")}
348
  >
349
  <div className="absolute top-3 left-3 z-10">
 
414
  <div>
415
  <h3 className="font-semibold text-green-900">Selection Saved!</h3>
416
  <p className="text-sm text-green-700">
417
+ {selectedImage === "new"
418
+ ? "Your ad has been updated with the new image."
419
  : "The original image has been kept."}
420
  </p>
421
  </div>
 
425
  {/* Show the selected image */}
426
  <div className="flex justify-center">
427
  <div className="max-w-md">
428
+ {((selectedImage === "new" ? result?.regenerated_image?.image_url : (result?.original_image_url || ad?.r2_url || ad?.image_url))) ? (
429
+ <img
430
+ src={(selectedImage === "new" ? result?.regenerated_image?.image_url : (result?.original_image_url || ad?.r2_url || ad?.image_url))!}
431
+ alt="Selected"
432
+ className="w-full rounded-xl border border-gray-200"
433
+ />
434
+ ) : (
435
+ <div className="w-full aspect-square bg-gray-50 flex items-center justify-center text-gray-400 rounded-xl border border-gray-200">
436
+ <ImageIcon className="h-8 w-8" />
437
+ </div>
438
+ )}
439
  <p className="text-center text-sm text-gray-500 mt-2">
440
  {selectedImage === "new" ? "New image saved" : "Original image kept"}
441
  </p>
 
451
  {/* Left side buttons */}
452
  <div>
453
  {step === "compare" && (
454
+ <Button
455
  onClick={() => {
456
  setStep("input");
457
  setResult(null);
458
  setSelectedImage(null);
459
+ }}
460
  variant="secondary"
461
  >
462
  <ArrowLeft className="h-4 w-4 mr-2" />
 
468
  {/* Right side buttons */}
469
  <div className="flex gap-3">
470
  {step === "input" && (
471
+ <Button
472
+ onClick={handleRegenerate}
473
  variant="primary"
474
  disabled={!selectedModel || loadingModels}
475
  >
 
477
  Regenerate Image
478
  </Button>
479
  )}
480
+
481
  {step === "compare" && (
482
+ <Button
483
+ onClick={handleConfirmSelection}
484
  variant="primary"
485
  disabled={!selectedImage || savingSelection}
486
  >
frontend/lib/api/endpoints.ts CHANGED
@@ -196,6 +196,7 @@ export const getStrategies = async (niche: Niche): Promise<any> => {
196
  // Image Correction Endpoints
197
  export const correctImage = async (params: {
198
  image_id: string;
 
199
  user_instructions?: string;
200
  auto_analyze?: boolean;
201
  }): Promise<ImageCorrectResponse> => {
@@ -275,7 +276,7 @@ export const downloadImageProxy = async (params: {
275
  export const uploadCreative = async (file: File): Promise<FileUploadResponse> => {
276
  const formData = new FormData();
277
  formData.append("file", file);
278
-
279
  const response = await apiClient.post<FileUploadResponse>(
280
  "/api/creative/upload",
281
  formData,
@@ -305,7 +306,7 @@ export const analyzeCreativeByFile = async (
305
  ): Promise<CreativeAnalysisResponse> => {
306
  const formData = new FormData();
307
  formData.append("file", file);
308
-
309
  const response = await apiClient.post<CreativeAnalysisResponse>(
310
  "/api/creative/analyze/upload",
311
  formData,
 
196
  // Image Correction Endpoints
197
  export const correctImage = async (params: {
198
  image_id: string;
199
+ image_url?: string;
200
  user_instructions?: string;
201
  auto_analyze?: boolean;
202
  }): Promise<ImageCorrectResponse> => {
 
276
  export const uploadCreative = async (file: File): Promise<FileUploadResponse> => {
277
  const formData = new FormData();
278
  formData.append("file", file);
279
+
280
  const response = await apiClient.post<FileUploadResponse>(
281
  "/api/creative/upload",
282
  formData,
 
306
  ): Promise<CreativeAnalysisResponse> => {
307
  const formData = new FormData();
308
  formData.append("file", file);
309
+
310
  const response = await apiClient.post<CreativeAnalysisResponse>(
311
  "/api/creative/analyze/upload",
312
  formData,
main.py CHANGED
@@ -700,7 +700,11 @@ async def download_image_proxy(
700
  class ImageCorrectRequest(BaseModel):
701
  """Request schema for image correction."""
702
  image_id: str = Field(
703
- description="ID of existing ad creative in database"
 
 
 
 
704
  )
705
  user_instructions: Optional[str] = Field(
706
  default=None,
@@ -780,21 +784,27 @@ async def correct_image(
780
 
781
  try:
782
  # Fetch ad from database to get image and metadata (only if it belongs to current user)
783
- api_logger.info(f"Fetching ad creative from database...")
784
- ad = await db_service.get_ad_creative(request.image_id, username=username)
785
- if not ad:
786
- api_logger.error(f"Ad creative {request.image_id} not found or access denied for user {username}")
787
- raise HTTPException(status_code=404, detail=f"Ad creative with ID {request.image_id} not found or access denied")
788
-
789
- api_logger.info(f"Ad creative found: {ad.get('title', 'N/A')} (niche: {ad.get('niche', 'N/A')})")
 
 
 
 
 
 
 
 
790
 
791
- # Get image URL from ad (prefer R2 URL, fallback to image_url)
792
- image_url = ad.get("r2_url") or ad.get("image_url")
793
  if not image_url:
794
- api_logger.error(f"Image URL not found for ad {request.image_id}")
795
  raise HTTPException(
796
- status_code=404,
797
- detail="Image URL not found for this ad creative"
798
  )
799
 
800
  api_logger.info(f"Image URL: {image_url}")
@@ -802,23 +812,23 @@ async def correct_image(
802
  # Load image bytes for analysis (needed for vision API)
803
  api_logger.info("Loading image bytes for analysis...")
804
  image_bytes = await image_service.load_image(
805
- image_id=request.image_id,
806
- image_url=None,
807
  image_bytes=None,
808
  filepath=None,
809
  )
810
 
811
  if not image_bytes:
812
- api_logger.error(f"Failed to load image bytes for ad {request.image_id}")
813
  raise HTTPException(
814
  status_code=404,
815
- detail="Image not found for analysis"
816
  )
817
 
818
  api_logger.info(f"Image bytes loaded: {len(image_bytes)} bytes")
819
 
820
  # Get original prompt from ad metadata if available
821
- original_prompt = ad.get("image_prompt") or None
822
  if original_prompt:
823
  api_logger.info(f"Original prompt available: {len(original_prompt)} characters")
824
 
@@ -830,7 +840,7 @@ async def correct_image(
830
  original_prompt=original_prompt,
831
  width=1024,
832
  height=1024,
833
- niche=ad.get("niche"),
834
  user_instructions=request.user_instructions,
835
  auto_analyze=request.auto_analyze,
836
  )
 
700
  class ImageCorrectRequest(BaseModel):
701
  """Request schema for image correction."""
702
  image_id: str = Field(
703
+ description="ID of existing ad creative in database, or 'temp-id' for images not in DB"
704
+ )
705
+ image_url: Optional[str] = Field(
706
+ default=None,
707
+ description="Optional image URL for images not in DB (required if image_id='temp-id')"
708
  )
709
  user_instructions: Optional[str] = Field(
710
  default=None,
 
784
 
785
  try:
786
  # Fetch ad from database to get image and metadata (only if it belongs to current user)
787
+ image_url = request.image_url
788
+ ad = None
789
+
790
+ if request.image_id != "temp-id":
791
+ api_logger.info(f"Fetching ad creative from database...")
792
+ ad = await db_service.get_ad_creative(request.image_id, username=username)
793
+ if not ad:
794
+ api_logger.error(f"Ad creative {request.image_id} not found or access denied for user {username}")
795
+ raise HTTPException(status_code=404, detail=f"Ad creative with ID {request.image_id} not found or access denied")
796
+
797
+ api_logger.info(f"Ad creative found: {ad.get('title', 'N/A')} (niche: {ad.get('niche', 'N/A')})")
798
+
799
+ # Get image URL from ad if not provided in request
800
+ if not image_url:
801
+ image_url = ad.get("r2_url") or ad.get("image_url")
802
 
 
 
803
  if not image_url:
804
+ api_logger.error(f"Image URL not found for request")
805
  raise HTTPException(
806
+ status_code=400,
807
+ detail="Image URL must be provided for images not in database, or found in database for provided ID"
808
  )
809
 
810
  api_logger.info(f"Image URL: {image_url}")
 
812
  # Load image bytes for analysis (needed for vision API)
813
  api_logger.info("Loading image bytes for analysis...")
814
  image_bytes = await image_service.load_image(
815
+ image_id=request.image_id if request.image_id != "temp-id" else None,
816
+ image_url=image_url,
817
  image_bytes=None,
818
  filepath=None,
819
  )
820
 
821
  if not image_bytes:
822
+ api_logger.error(f"Failed to load image bytes for request")
823
  raise HTTPException(
824
  status_code=404,
825
+ detail="Image not found for analysis. Please ensure the URL is accessible."
826
  )
827
 
828
  api_logger.info(f"Image bytes loaded: {len(image_bytes)} bytes")
829
 
830
  # Get original prompt from ad metadata if available
831
+ original_prompt = ad.get("image_prompt") or None if ad else None
832
  if original_prompt:
833
  api_logger.info(f"Original prompt available: {len(original_prompt)} characters")
834
 
 
840
  original_prompt=original_prompt,
841
  width=1024,
842
  height=1024,
843
+ niche=ad.get("niche") if ad else "others",
844
  user_instructions=request.user_instructions,
845
  auto_analyze=request.auto_analyze,
846
  )
services/creative_modifier.py CHANGED
@@ -248,59 +248,90 @@ Be specific and detailed in your analysis. If you cannot determine something wit
248
  logger.info(f"User angle: {user_angle}")
249
  logger.info(f"User concept: {user_concept}")
250
 
251
- system_prompt = """You are an expert prompt engineer for AI image generation models.
252
- Your task is to create effective prompts that will generate or modify advertising images.
253
- For 'modify' mode, create minimal, focused prompts that specify only what should change.
254
- For 'inspired' mode, create comprehensive prompts that capture the essence of the original while applying new angles/concepts.
255
-
256
- CRITICAL: ALL generated images MUST be photorealistic. Always include photorealistic quality descriptors in your prompts."""
257
 
258
- if mode == "modify":
259
- # Minimal prompt for image-to-image modification
260
- prompt_request = f"""Based on this creative analysis:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
261
 
262
- Visual Style: {analysis.get('visual_style', 'Unknown')}
263
- Subject Matter: {analysis.get('subject_matter', 'Unknown')}
264
- Current Angle: {analysis.get('current_angle', 'Unknown')}
265
- Current Concept: {analysis.get('current_concept', 'Unknown')}
 
 
 
 
 
 
266
 
267
- The user wants to apply:
268
- - New Angle: {user_angle or 'Keep current'}
269
- - New Concept: {user_concept or 'Keep current'}
 
 
 
 
 
270
 
271
- Generate a MINIMAL image modification prompt (under 40 words) that specifies what should change.
272
- The image-to-image model will preserve most of the original image automatically.
273
- Focus only on the specific changes needed to apply the new angle/concept.
274
 
275
- CRITICAL: The output MUST be photorealistic. Include "photorealistic" or "realistic photograph" in the prompt.
 
 
 
 
 
276
 
277
- Return ONLY the prompt text, no explanations."""
 
 
278
  else:
279
- # Full prompt for inspired generation
280
- prompt_request = f"""Based on this creative analysis:
 
 
281
 
282
- Visual Style: {analysis.get('visual_style', 'Unknown')}
283
- Color Palette: {', '.join(analysis.get('color_palette', ['Unknown']))}
284
- Mood: {analysis.get('mood', 'Unknown')}
285
- Composition: {analysis.get('composition', 'Unknown')}
286
- Subject Matter: {analysis.get('subject_matter', 'Unknown')}
287
- Target Audience: {analysis.get('target_audience', 'Unknown')}
288
 
289
- The user wants to create a NEW creative inspired by this one, applying:
290
- - New Angle: {user_angle or 'Similar to original'}
291
- - New Concept: {user_concept or 'Similar to original'}
 
 
 
292
 
293
- Generate a COMPREHENSIVE image generation prompt (60-100 words) that:
294
- 1. MUST be photorealistic - include "photorealistic", "realistic photograph", "high-resolution photo" in the prompt
295
- 2. Captures the mood of the original
296
- 3. Incorporates the new angle/concept effectively
297
- 4. Maintains appeal to the target audience
298
- 5. Includes specific visual details for high-quality photorealistic generation
299
- 6. If people are included, specify realistic skin texture, natural lighting, authentic expressions
300
-
301
- CRITICAL: The final image MUST look like a real photograph, not illustrated or AI-generated looking.
302
-
303
- Return ONLY the prompt text, no explanations."""
304
 
305
  try:
306
  prompt = await llm_service.generate(
@@ -309,29 +340,57 @@ Return ONLY the prompt text, no explanations."""
309
  temperature=0.7,
310
  )
311
 
312
- # Clean up the response
313
  prompt = prompt.strip().strip('"').strip("'")
314
 
315
- # Ensure photorealistic is in the prompt
316
- photorealistic_keywords = ["photorealistic", "realistic photograph", "real photo", "high-resolution photo"]
 
 
 
 
 
 
317
  has_photorealistic = any(keyword.lower() in prompt.lower() for keyword in photorealistic_keywords)
318
 
 
 
 
 
319
  if not has_photorealistic:
320
- # Prepend photorealistic requirement
321
- prompt = f"Photorealistic, realistic photograph. {prompt}"
322
- logger.info("Added photorealistic prefix to prompt")
 
 
 
 
 
 
323
 
324
- logger.info(f"Generated prompt: {prompt[:100]}...")
 
 
 
 
 
 
 
 
 
 
 
 
325
 
326
  return prompt
327
  except Exception as e:
328
  logger.error(f"Failed to generate modification prompt: {e}")
329
- # Fallback to a basic prompt with photorealistic requirements
330
  if mode == "modify":
331
- return f"Photorealistic, realistic photograph. Apply {user_angle or 'new angle'} with {user_concept or 'improved concept'}. Natural lighting, authentic details."
332
  else:
333
- return f"Photorealistic advertising photograph, high-resolution photo, {analysis.get('mood', 'professional')} mood, featuring {analysis.get('subject_matter', 'product')}, applying {user_angle or 'compelling angle'} through {user_concept or 'effective concept'}. Natural lighting, realistic textures, authentic appearance."
334
-
335
  async def modify_creative(
336
  self,
337
  image_url: str,
@@ -391,29 +450,26 @@ Return ONLY the prompt text, no explanations."""
391
  logger.info("STEP 2: Generating modified image...")
392
  try:
393
  if mode == "modify":
394
- # Use image-to-image with the original
395
- model_key = image_model or "nano-banana"
396
- logger.info(f"Using image-to-image with model: {model_key}")
397
-
398
- image_bytes, model_used, generated_url = await image_service.generate(
399
- prompt=prompt,
400
- model_key=model_key,
401
- width=width,
402
- height=height,
403
- image_url=image_url, # Pass original for image-to-image
404
- )
405
  else:
406
- # Generate new image inspired by original
407
- model_key = image_model or "nano-banana"
408
- logger.info(f"Generating new image with model: {model_key}")
409
-
410
- image_bytes, model_used, generated_url = await image_service.generate(
411
- prompt=prompt,
412
- model_key=model_key,
413
- width=width,
414
- height=height,
415
- # No image_url - generate fresh
416
- )
417
 
418
  if not image_bytes:
419
  raise Exception("Image generation returned no data")
 
248
  logger.info(f"User angle: {user_angle}")
249
  logger.info(f"User concept: {user_concept}")
250
 
251
+ system_prompt = """You are an expert advertising creative director with 20+ years experience.
252
+ Your task is to create seamless, organic modifications that enhance existing creatives without appearing forced.
253
+ You understand that effective ads feel authentic and natural, not like concepts were "pasted on."
254
+ Focus on subtlety, consistency, and maintaining the original's visual language while applying new psychological angles.
255
+ CRITICAL: ALL generated images MUST be photorealistic. Never mention "AI-generated" or similar terms."""
 
256
 
257
+ # Try to enrich angle/concept with library data if not already detailed
258
+ angle_info = user_angle
259
+ concept_info = user_concept
260
+
261
+ try:
262
+ from data.angles import get_all_angles
263
+ from data.concepts import get_all_concepts
264
+
265
+ # Lookup angle details if it's just a name
266
+ if user_angle and "(" not in user_angle:
267
+ all_angles = get_all_angles()
268
+ for a in all_angles:
269
+ if a["name"].lower() == user_angle.lower():
270
+ angle_info = f"{a['name']} (Psychological trigger: {a['trigger']})"
271
+ break
272
+
273
+ # Lookup concept details if it's just a name
274
+ if user_concept and "(" not in user_concept:
275
+ all_concepts = get_all_concepts()
276
+ for c in all_concepts:
277
+ if c["name"].lower() == user_concept.lower():
278
+ concept_info = f"{c['name']} (Visual structure: {c['structure']})"
279
+ break
280
+ except Exception as e:
281
+ logger.warning(f"Failed to enrich angle/concept data: {e}")
282
 
283
+ # Extract original creative's essence for natural integration
284
+ original_mood = analysis.get('mood', 'Unknown')
285
+ original_style = analysis.get('visual_style', 'Unknown')
286
+ color_palette = analysis.get('color_palette', [])
287
+ subject_matter = analysis.get('subject_matter', 'Unknown')
288
+ composition = analysis.get('composition', 'Unknown')
289
+
290
+ # Guard against common hallucinations (e.g. "fan")
291
+ if "fan of" in subject_matter.lower() and "bills" in subject_matter.lower():
292
+ subject_matter = subject_matter.replace("fan of", "fanned-out stack of").replace("bills", "money/dollar bills")
293
 
294
+ if mode == "modify":
295
+ # For image-to-image: visually clear modifications
296
+ prompt_request = f"""ORIGINAL IMAGE CONTEXT:
297
+ - Subject: {subject_matter}
298
+ - Mood: {original_mood}
299
+ - Style: {original_style}
300
+ - Composition: {composition}
301
+ - Colors: {', '.join(color_palette[:3]) if color_palette else 'Natural tones'}
302
 
303
+ TRANSFORMATION TASK:
304
+ - Apply Angle: {angle_info or 'natural enhancement'}
305
+ - Apply Concept: {concept_info or 'subtle adjustment'}
306
 
307
+ Generate a clear, descriptive transformation prompt (20-40 words) that:
308
+ 1. Specifically describes the VISUAL change needed to reflect the new angle and concept.
309
+ 2. Makes the transformation feel like a professional edit of the original - NOT a different image.
310
+ 3. Keeps the core subjects ({subject_matter}) but adapts their presentation, lighting, or surrounding elements.
311
+ 4. Maintain the {original_style} style and {original_mood} tone.
312
+ 5. Be explicit about what to change (e.g., "Change the lighting to be more dramatic", "Rearrange elements for [concept]")
313
 
314
+ CRITICAL: The instruction must be strong enough for an image-to-image AI to actually make a visible change.
315
+
316
+ Return ONLY the prompt text, no explanations."""
317
  else:
318
+ # For inspired generation: create new but keep the same authentic vibe
319
+ prompt_request = f"""ORIGINAL CAMPAIGN INSPIRATION:
320
+ - Core Vibe: {original_mood} mood, {original_style} aesthetic
321
+ - Visual Language: {composition}, {subject_matter}
322
 
323
+ NEW AD CREATIVE REQUIREMENTS:
324
+ - Target Angle: {angle_info or 'natural persuasion'}
325
+ - Target Concept: {concept_info or 'authentic presentation'}
 
 
 
326
 
327
+ Generate a premium, authentic advertising photography prompt (60-90 words) that:
328
+ 1. Creates a NEW photo that looks like it belongs in the same campaign as the original.
329
+ 2. Centers the visual around {concept_info or 'the concept'}.
330
+ 3. Conveys the {angle_info or 'the angle'} through unposed, authentic human moments and natural lighting.
331
+ 4. Maintains high photorealistic quality with realistic textures and real-world lighting.
332
+ 5. Do NOT describe a generic stock photo; describe a high-end, cinematic brand image.
333
 
334
+ Return ONLY the prompt text, no explanations."""
 
 
 
 
 
 
 
 
 
 
335
 
336
  try:
337
  prompt = await llm_service.generate(
 
340
  temperature=0.7,
341
  )
342
 
343
+ # Clean up and ensure natural language
344
  prompt = prompt.strip().strip('"').strip("'")
345
 
346
+ # Enhance for seamlessness if needed
347
+ seamless_keywords = ["seamless", "organic", "natural", "authentic", "integrated"]
348
+ has_seamless = any(keyword in prompt.lower() for keyword in seamless_keywords)
349
+
350
+ photorealistic_keywords = [
351
+ "photorealistic", "realistic photograph", "cinematic photography",
352
+ "authentic photo", "natural lighting", "real-world"
353
+ ]
354
  has_photorealistic = any(keyword.lower() in prompt.lower() for keyword in photorealistic_keywords)
355
 
356
+ # Add subtle guidance if missing key elements
357
+ if not has_seamless and mode == "modify":
358
+ prompt = f"Seamlessly integrated, {prompt}"
359
+
360
  if not has_photorealistic:
361
+ # Add natural photography terms
362
+ prompt = f"Cinematic photography, authentic moment, natural lighting. {prompt}"
363
+ logger.info("Added natural photography emphasis to prompt")
364
+
365
+ # Remove any phrases that sound artificial or forced
366
+ forced_phrases = [
367
+ "add", "insert", "place", "include the concept of",
368
+ "visibly show", "clearly demonstrate", "obviously"
369
+ ]
370
 
371
+ for phrase in forced_phrases:
372
+ if phrase in prompt.lower():
373
+ # Replace with more natural alternatives
374
+ if phrase == "add":
375
+ prompt = prompt.lower().replace("add", "naturally incorporate")
376
+ elif phrase == "insert":
377
+ prompt = prompt.lower().replace("insert", "subtly integrate")
378
+ elif phrase == "place":
379
+ prompt = prompt.lower().replace("place", "position naturally")
380
+ elif "visibly show" in prompt.lower():
381
+ prompt = prompt.lower().replace("visibly show", "suggest through")
382
+
383
+ logger.info(f"Generated natural prompt: {prompt[:100]}...")
384
 
385
  return prompt
386
  except Exception as e:
387
  logger.error(f"Failed to generate modification prompt: {e}")
388
+ # Fallback prompts with emphasis on natural integration
389
  if mode == "modify":
390
+ return f"Cinematic photography, seamless integration. Subtly enhance {subject_matter} to naturally incorporate {user_angle or 'emotional resonance'} through {user_concept or 'visual storytelling'}. Authentic moment, natural lighting, feels like original campaign."
391
  else:
392
+ return f"Cinematic advertising photograph, authentic human moment. {subject_matter} presented with {original_mood} emotional tone, naturally integrating {user_angle or 'persuasive angle'} through {user_concept or 'visual concept'}. Real-world lighting, natural skin textures, campaign-consistent styling."
393
+
394
  async def modify_creative(
395
  self,
396
  image_url: str,
 
450
  logger.info("STEP 2: Generating modified image...")
451
  try:
452
  if mode == "modify":
453
+ # For subtle modifications with image-to-image
454
+ # Note: nano-banana models don't support guidance_scale or strength parameters
455
+ # The model will naturally preserve the original image based on the prompt
456
+ generation_params = {
457
+ "prompt": prompt,
458
+ "model_key": image_model or "nano-banana",
459
+ "width": width,
460
+ "height": height,
461
+ "image_url": image_url,
462
+ }
 
463
  else:
464
+ # For inspired generation (text-to-image)
465
+ generation_params = {
466
+ "prompt": prompt,
467
+ "model_key": image_model or "nano-banana",
468
+ "width": width,
469
+ "height": height,
470
+ }
471
+
472
+ image_bytes, model_used, generated_url = await image_service.generate(**generation_params)
 
 
473
 
474
  if not image_bytes:
475
  raise Exception("Image generation returned no data")