sushilideaclan01 commited on
Commit
4a56a0b
·
1 Parent(s): 2c9d10c

remove the variations option

Browse files
frontend/app/creative/modify/page.tsx CHANGED
@@ -41,10 +41,13 @@ export default function CreativeModifyPage() {
41
  const [mode, setMode] = useState<ModificationMode>("modify");
42
  const [imageModel, setImageModel] = useState<string | null>(null);
43
  const [userPrompt, setUserPrompt] = useState("");
 
44
 
45
  // Result state
46
- const [result, setResult] = useState<ModifiedImageResult | null>(null);
47
- const [generatedPrompt, setGeneratedPrompt] = useState<string | null>(null);
 
 
48
 
49
  // Correction modal state
50
  const [isCorrectionModalOpen, setIsCorrectionModalOpen] = useState(false);
@@ -99,29 +102,37 @@ export default function CreativeModifyPage() {
99
  setError(null);
100
 
101
  try {
102
- const response = await modifyCreative({
103
- image_url: originalImageUrl,
104
- analysis: analysis || undefined,
105
- angle: angle.trim() || undefined,
106
- concept: concept.trim() || undefined,
107
- mode,
108
- image_model: imageModel,
109
- user_prompt: userPrompt.trim() || undefined,
110
- });
111
-
112
- if (response.status !== "success" || !response.image) {
113
- throw new Error(response.error || "Failed to modify creative");
 
 
 
 
 
 
 
 
114
  }
115
 
116
- setResult(response.image);
117
- setGeneratedPrompt(response.prompt || null);
118
  setCurrentStep("result");
119
  } catch (err) {
120
  setError(err instanceof Error ? err.message : "An error occurred");
121
  } finally {
122
  setIsLoading(false);
123
  }
124
- }, [originalImageUrl, analysis, angle, concept, mode, imageModel, userPrompt]);
125
 
126
  // Reset to start over
127
  const handleStartOver = useCallback(() => {
@@ -134,15 +145,16 @@ export default function CreativeModifyPage() {
134
  setMode("modify");
135
  setImageModel(null);
136
  setUserPrompt("");
137
- setResult(null);
138
- setGeneratedPrompt(null);
 
139
  setError(null);
140
  }, []);
141
 
142
  // Go back to modification form
143
  const handleModifyAgain = useCallback(() => {
144
- setResult(null);
145
- setGeneratedPrompt(null);
146
  setCurrentStep("analysis");
147
  }, []);
148
 
@@ -155,8 +167,8 @@ export default function CreativeModifyPage() {
155
 
156
  // Go back to analysis step from result
157
  const handleBackToAnalysis = useCallback(() => {
158
- setResult(null);
159
- setGeneratedPrompt(null);
160
  setCurrentStep("analysis");
161
  setError(null);
162
  }, []);
@@ -234,6 +246,7 @@ export default function CreativeModifyPage() {
234
  - Modify: Change angle/concept while keeping similar structure
235
  - Regenerate: Create completely new version
236
  - Custom: Use your own prompt for specific changes
 
237
 
238
  4. RESULT: Get your modified creative with new image and updated copy
239
 
@@ -242,8 +255,7 @@ Perfect for iterating on winning ads or testing new angles with proven visuals."
242
  />
243
  </div>
244
  <p className="text-gray-600 max-w-2xl mx-auto">
245
- Upload your existing creative, let AI analyze it, then apply new
246
- angles or concepts to generate variations
247
  </p>
248
  </div>
249
 
@@ -381,11 +393,13 @@ Perfect for iterating on winning ads or testing new angles with proven visuals."
381
  mode={mode}
382
  imageModel={imageModel}
383
  userPrompt={userPrompt}
 
384
  onAngleChange={setAngle}
385
  onConceptChange={setConcept}
386
  onModeChange={setMode}
387
  onImageModelChange={setImageModel}
388
  onUserPromptChange={setUserPrompt}
 
389
  onSubmit={handleModify}
390
  isLoading={isLoading}
391
  />
@@ -393,7 +407,7 @@ Perfect for iterating on winning ads or testing new angles with proven visuals."
393
  )}
394
 
395
  {/* Result Step */}
396
- {currentStep === "result" && result && (
397
  <div className="space-y-6">
398
  {/* Back Button */}
399
  <button
@@ -410,10 +424,12 @@ Perfect for iterating on winning ads or testing new angles with proven visuals."
410
  {/* Before / After Comparison */}
411
  <Card variant="glass">
412
  <CardHeader>
413
- <CardTitle>Result</CardTitle>
 
 
414
  </CardHeader>
415
  <CardContent>
416
- <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
417
  {/* Original */}
418
  <div>
419
  <h4 className="text-sm font-semibold text-gray-700 mb-2">
@@ -430,20 +446,31 @@ Perfect for iterating on winning ads or testing new angles with proven visuals."
430
  </div>
431
  </div>
432
 
433
- {/* Modified */}
434
- <div>
435
- <h4 className="text-sm font-semibold text-gray-700 mb-2">
436
- {mode === "modify" ? "Modified" : "Inspired"}
437
- </h4>
438
- <div className="rounded-xl overflow-hidden bg-gray-100">
439
- {result.image_url && (
440
- <img
441
- src={result.image_url}
442
- alt="Modified creative"
443
- className="w-full h-auto object-contain"
444
- />
445
- )}
446
- </div>
 
 
 
 
 
 
 
 
 
 
 
447
  </div>
448
  </div>
449
  </CardContent>
@@ -456,23 +483,23 @@ Perfect for iterating on winning ads or testing new angles with proven visuals."
456
  </CardHeader>
457
  <CardContent className="space-y-4">
458
  <div className="grid grid-cols-2 gap-4">
459
- {result.applied_angle && (
460
  <div className="bg-orange-50 rounded-xl p-4">
461
  <h4 className="text-sm font-semibold text-orange-700 mb-1">
462
  Applied Angle
463
  </h4>
464
  <p className="text-orange-900">
465
- {result.applied_angle}
466
  </p>
467
  </div>
468
  )}
469
- {result.applied_concept && (
470
  <div className="bg-green-50 rounded-xl p-4">
471
  <h4 className="text-sm font-semibold text-green-700 mb-1">
472
  Applied Concept
473
  </h4>
474
  <p className="text-green-900">
475
- {result.applied_concept}
476
  </p>
477
  </div>
478
  )}
@@ -482,26 +509,35 @@ Perfect for iterating on winning ads or testing new angles with proven visuals."
482
  <h4 className="text-sm font-semibold text-gray-700 mb-1">
483
  Mode
484
  </h4>
485
- <p className="text-gray-900 capitalize">{result.mode}</p>
486
  </div>
487
 
488
- {result.model_used && (
489
  <div className="bg-blue-50 rounded-xl p-4">
490
  <h4 className="text-sm font-semibold text-blue-700 mb-1">
491
  Model Used
492
  </h4>
493
- <p className="text-blue-900">{result.model_used}</p>
494
  </div>
495
  )}
496
 
497
- {generatedPrompt && (
498
- <div>
499
- <h4 className="text-sm font-semibold text-gray-700 mb-1">
500
- Generated Prompt
501
  </h4>
502
- <p className="text-gray-600 text-sm bg-gray-50 p-3 rounded-lg">
503
- {generatedPrompt}
504
- </p>
 
 
 
 
 
 
 
 
 
505
  </div>
506
  )}
507
  </CardContent>
@@ -525,33 +561,43 @@ Perfect for iterating on winning ads or testing new angles with proven visuals."
525
  </button>
526
  </div>
527
 
528
- {/* Download Button */}
529
- {result.image_url && (
530
- <div className="text-center">
531
- <a
532
- href={result.image_url}
533
- download={result.filename || "modified-creative.png"}
534
- target="_blank"
535
- rel="noopener noreferrer"
536
- className="inline-flex items-center gap-2 text-blue-600 hover:text-blue-700 font-medium"
537
- >
538
- <svg
539
- className="w-5 h-5"
540
- fill="none"
541
- stroke="currentColor"
542
- viewBox="0 0 24 24"
543
  >
544
- <path
545
- strokeLinecap="round"
546
- strokeLinejoin="round"
547
- strokeWidth={2}
548
- d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"
549
- />
550
- </svg>
551
- Download Image
552
- </a>
553
- </div>
554
- )}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
555
  </div>
556
  )}
557
  </div>
 
41
  const [mode, setMode] = useState<ModificationMode>("modify");
42
  const [imageModel, setImageModel] = useState<string | null>(null);
43
  const [userPrompt, setUserPrompt] = useState("");
44
+ const [variationCount, setVariationCount] = useState(1);
45
 
46
  // Result state
47
+ const [results, setResults] = useState<ModifiedImageResult[]>([]);
48
+ const [generatedPrompts, setGeneratedPrompts] = useState<string[]>([]);
49
+
50
+ const primaryResult = results[0] || null;
51
 
52
  // Correction modal state
53
  const [isCorrectionModalOpen, setIsCorrectionModalOpen] = useState(false);
 
102
  setError(null);
103
 
104
  try {
105
+ const variationResults: ModifiedImageResult[] = [];
106
+ const promptResults: string[] = [];
107
+
108
+ for (let i = 0; i < variationCount; i += 1) {
109
+ const response = await modifyCreative({
110
+ image_url: originalImageUrl,
111
+ analysis: analysis || undefined,
112
+ angle: angle.trim() || undefined,
113
+ concept: concept.trim() || undefined,
114
+ mode,
115
+ image_model: imageModel,
116
+ user_prompt: userPrompt.trim() || undefined,
117
+ });
118
+
119
+ if (response.status !== "success" || !response.image) {
120
+ throw new Error(response.error || "Failed to modify creative");
121
+ }
122
+
123
+ variationResults.push(response.image);
124
+ promptResults.push(response.prompt || "");
125
  }
126
 
127
+ setResults(variationResults);
128
+ setGeneratedPrompts(promptResults);
129
  setCurrentStep("result");
130
  } catch (err) {
131
  setError(err instanceof Error ? err.message : "An error occurred");
132
  } finally {
133
  setIsLoading(false);
134
  }
135
+ }, [originalImageUrl, analysis, angle, concept, mode, imageModel, userPrompt, variationCount]);
136
 
137
  // Reset to start over
138
  const handleStartOver = useCallback(() => {
 
145
  setMode("modify");
146
  setImageModel(null);
147
  setUserPrompt("");
148
+ setVariationCount(1);
149
+ setResults([]);
150
+ setGeneratedPrompts([]);
151
  setError(null);
152
  }, []);
153
 
154
  // Go back to modification form
155
  const handleModifyAgain = useCallback(() => {
156
+ setResults([]);
157
+ setGeneratedPrompts([]);
158
  setCurrentStep("analysis");
159
  }, []);
160
 
 
167
 
168
  // Go back to analysis step from result
169
  const handleBackToAnalysis = useCallback(() => {
170
+ setResults([]);
171
+ setGeneratedPrompts([]);
172
  setCurrentStep("analysis");
173
  setError(null);
174
  }, []);
 
246
  - Modify: Change angle/concept while keeping similar structure
247
  - Regenerate: Create completely new version
248
  - Custom: Use your own prompt for specific changes
249
+ - Variations: Use the slider to request up to 3 remixed outputs in one run
250
 
251
  4. RESULT: Get your modified creative with new image and updated copy
252
 
 
255
  />
256
  </div>
257
  <p className="text-gray-600 max-w-2xl mx-auto">
258
+ Upload your existing creative, let AI analyze it, then apply new angles or concepts to generate up to three fresh variations in a single pass.
 
259
  </p>
260
  </div>
261
 
 
393
  mode={mode}
394
  imageModel={imageModel}
395
  userPrompt={userPrompt}
396
+ variationCount={variationCount}
397
  onAngleChange={setAngle}
398
  onConceptChange={setConcept}
399
  onModeChange={setMode}
400
  onImageModelChange={setImageModel}
401
  onUserPromptChange={setUserPrompt}
402
+ onVariationCountChange={setVariationCount}
403
  onSubmit={handleModify}
404
  isLoading={isLoading}
405
  />
 
407
  )}
408
 
409
  {/* Result Step */}
410
+ {currentStep === "result" && results.length > 0 && primaryResult && (
411
  <div className="space-y-6">
412
  {/* Back Button */}
413
  <button
 
424
  {/* Before / After Comparison */}
425
  <Card variant="glass">
426
  <CardHeader>
427
+ <CardTitle>
428
+ {variationCount > 1 ? "Variations" : "Result"}
429
+ </CardTitle>
430
  </CardHeader>
431
  <CardContent>
432
+ <div className="grid grid-cols-1 lg:grid-cols-[minmax(0,1fr)_minmax(0,2fr)] gap-6">
433
  {/* Original */}
434
  <div>
435
  <h4 className="text-sm font-semibold text-gray-700 mb-2">
 
446
  </div>
447
  </div>
448
 
449
+ {/* Variations */}
450
+ <div className="space-y-4">
451
+ {results.map((variation, index) => (
452
+ <div key={variation.image_url ?? index} className="rounded-xl overflow-hidden bg-gray-100 border border-gray-200/60">
453
+ <div className="flex items-center justify-between px-4 py-3 bg-white/60 border-b border-gray-200/60">
454
+ <span className="text-sm font-semibold text-gray-700">
455
+ Variation {index + 1}
456
+ </span>
457
+ <span className="text-xs uppercase tracking-wide text-gray-400">
458
+ {mode === "modify" ? "Modify" : "Inspired"}
459
+ </span>
460
+ </div>
461
+ {variation.image_url ? (
462
+ <img
463
+ src={variation.image_url}
464
+ alt={`Variation ${index + 1}`}
465
+ className="w-full h-auto object-contain bg-white"
466
+ />
467
+ ) : (
468
+ <div className="p-6 text-center text-sm text-gray-500">
469
+ Image preview unavailable
470
+ </div>
471
+ )}
472
+ </div>
473
+ ))}
474
  </div>
475
  </div>
476
  </CardContent>
 
483
  </CardHeader>
484
  <CardContent className="space-y-4">
485
  <div className="grid grid-cols-2 gap-4">
486
+ {primaryResult.applied_angle && (
487
  <div className="bg-orange-50 rounded-xl p-4">
488
  <h4 className="text-sm font-semibold text-orange-700 mb-1">
489
  Applied Angle
490
  </h4>
491
  <p className="text-orange-900">
492
+ {primaryResult.applied_angle}
493
  </p>
494
  </div>
495
  )}
496
+ {primaryResult.applied_concept && (
497
  <div className="bg-green-50 rounded-xl p-4">
498
  <h4 className="text-sm font-semibold text-green-700 mb-1">
499
  Applied Concept
500
  </h4>
501
  <p className="text-green-900">
502
+ {primaryResult.applied_concept}
503
  </p>
504
  </div>
505
  )}
 
509
  <h4 className="text-sm font-semibold text-gray-700 mb-1">
510
  Mode
511
  </h4>
512
+ <p className="text-gray-900 capitalize">{primaryResult.mode}</p>
513
  </div>
514
 
515
+ {primaryResult.model_used && (
516
  <div className="bg-blue-50 rounded-xl p-4">
517
  <h4 className="text-sm font-semibold text-blue-700 mb-1">
518
  Model Used
519
  </h4>
520
+ <p className="text-blue-900">{primaryResult.model_used}</p>
521
  </div>
522
  )}
523
 
524
+ {generatedPrompts.length > 0 && (
525
+ <div className="space-y-2">
526
+ <h4 className="text-sm font-semibold text-gray-700">
527
+ Generated Prompts
528
  </h4>
529
+ <div className="space-y-2">
530
+ {generatedPrompts.map((prompt, index) => (
531
+ <div key={`prompt-${index}`} className="bg-white rounded-lg border border-gray-100 p-3">
532
+ <p className="text-xs font-semibold text-gray-500 mb-1">
533
+ Variation {index + 1}
534
+ </p>
535
+ <p className="text-gray-600 text-sm whitespace-pre-wrap">
536
+ {prompt}
537
+ </p>
538
+ </div>
539
+ ))}
540
+ </div>
541
  </div>
542
  )}
543
  </CardContent>
 
561
  </button>
562
  </div>
563
 
564
+ {/* Download Buttons */}
565
+ <div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-3">
566
+ {results.map((variation, index) => (
567
+ variation.image_url ? (
568
+ <a
569
+ key={`download-${variation.image_url}-${index}`}
570
+ href={variation.image_url}
571
+ download={variation.filename || `modified-creative-${index + 1}.png`}
572
+ target="_blank"
573
+ rel="noopener noreferrer"
574
+ className="inline-flex items-center justify-center gap-2 py-3 px-4 border-2 border-blue-200 text-blue-600 font-medium rounded-xl hover:bg-blue-50 transition-colors"
 
 
 
 
575
  >
576
+ <svg
577
+ className="w-5 h-5"
578
+ fill="none"
579
+ stroke="currentColor"
580
+ viewBox="0 0 24 24"
581
+ >
582
+ <path
583
+ strokeLinecap="round"
584
+ strokeLinejoin="round"
585
+ strokeWidth={2}
586
+ d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"
587
+ />
588
+ </svg>
589
+ Download Variation {index + 1}
590
+ </a>
591
+ ) : (
592
+ <div
593
+ key={`download-placeholder-${index}`}
594
+ className="py-3 px-4 border-2 border-gray-200 text-gray-400 font-medium rounded-xl text-center"
595
+ >
596
+ Variation {index + 1} unavailable
597
+ </div>
598
+ )
599
+ ))}
600
+ </div>
601
  </div>
602
  )}
603
  </div>
frontend/app/generate/batch/page.tsx CHANGED
@@ -16,7 +16,6 @@ export default function BatchGeneratePage() {
16
  const [progress, setProgress] = useState(0);
17
  const [currentIndex, setCurrentIndex] = useState(0);
18
  const [batchCount, setBatchCount] = useState(0);
19
- const [batchImagesPerAd, setBatchImagesPerAd] = useState(1);
20
  const [generationStartTime, setGenerationStartTime] = useState<number | null>(null);
21
 
22
  // Request notification permission
@@ -39,13 +38,12 @@ export default function BatchGeneratePage() {
39
  }
40
  }, [progress, results.length]);
41
 
42
- const handleGenerate = async (data: { niche: Niche; count: number; images_per_ad: number; image_model?: string | null }) => {
43
  setResults([]);
44
  setIsGenerating(true);
45
  setProgress(0);
46
  setCurrentIndex(0);
47
  setBatchCount(data.count);
48
- setBatchImagesPerAd(data.images_per_ad);
49
  setGenerationStartTime(Date.now());
50
 
51
  // Estimate time per ad (roughly 30-60 seconds per ad)
@@ -113,7 +111,6 @@ export default function BatchGeneratePage() {
113
  progress={progress}
114
  currentIndex={currentIndex}
115
  totalCount={batchCount}
116
- imagesPerAd={batchImagesPerAd}
117
  generationStartTime={generationStartTime}
118
  />
119
  )}
 
16
  const [progress, setProgress] = useState(0);
17
  const [currentIndex, setCurrentIndex] = useState(0);
18
  const [batchCount, setBatchCount] = useState(0);
 
19
  const [generationStartTime, setGenerationStartTime] = useState<number | null>(null);
20
 
21
  // Request notification permission
 
38
  }
39
  }, [progress, results.length]);
40
 
41
+ const handleGenerate = async (data: { niche: Niche; count: number; image_model?: string | null }) => {
42
  setResults([]);
43
  setIsGenerating(true);
44
  setProgress(0);
45
  setCurrentIndex(0);
46
  setBatchCount(data.count);
 
47
  setGenerationStartTime(Date.now());
48
 
49
  // Estimate time per ad (roughly 30-60 seconds per ad)
 
111
  progress={progress}
112
  currentIndex={currentIndex}
113
  totalCount={batchCount}
 
114
  generationStartTime={generationStartTime}
115
  />
116
  )}
frontend/app/generate/page.tsx CHANGED
@@ -33,7 +33,6 @@ export default function GeneratePage() {
33
  const [currentBatchIndex, setCurrentBatchIndex] = useState(0);
34
  const [batchProgress, setBatchProgress] = useState(0);
35
  const [batchCount, setBatchCount] = useState(0);
36
- const [batchImagesPerAd, setBatchImagesPerAd] = useState(1);
37
 
38
  // Motivators (Matrix mode): generate from angle+concept, user selects one or more for ad generation
39
  const [motivators, setMotivators] = useState<string[]>([]);
@@ -239,7 +238,6 @@ export default function GeneratePage() {
239
  const batchResponse = await generateBatch({
240
  niche: formattedData.niche,
241
  count: formattedData.num_images,
242
- images_per_ad: 1, // Each ad gets 1 image
243
  image_model: formattedData.image_model,
244
  method: "standard", // Use standard method only
245
  target_audience: formattedData.target_audience,
@@ -451,7 +449,7 @@ export default function GeneratePage() {
451
  }
452
  };
453
 
454
- const handleBatchGenerate = async (data: { niche: Niche; count: number; images_per_ad: number; image_model?: string | null; target_audience?: string | null; offer?: string | null }) => {
455
  setBatchResults([]);
456
  setIsGenerating(true);
457
  setGenerationStartTime(Date.now());
@@ -464,7 +462,6 @@ export default function GeneratePage() {
464
  setBatchProgress(0);
465
  setCurrentBatchIndex(0);
466
  setBatchCount(data.count);
467
- setBatchImagesPerAd(data.images_per_ad);
468
 
469
  // Estimate time per ad (roughly 30-60 seconds per ad)
470
  const estimatedTimePerAd = 45; // seconds
@@ -529,6 +526,8 @@ export default function GeneratePage() {
529
  setIsGenerating(true);
530
  setGenerationStartTime(Date.now());
531
 
 
 
532
  const formattedData: {
533
  niche: Niche;
534
  custom_niche?: string;
@@ -542,6 +541,7 @@ export default function GeneratePage() {
542
  custom_niche: data.custom_niche || undefined,
543
  target_audience: data.target_audience || undefined,
544
  offer: data.offer || undefined,
 
545
  };
546
 
547
  // Calculate estimated time based on strategies and images
@@ -557,7 +557,7 @@ export default function GeneratePage() {
557
  20 + // Step 2: Retrieve knowledge
558
  25 + // Step 3: Creative Director
559
  (data.num_strategies * estimatedTimePerStrategy) + // Step 4: Process strategies
560
- (data.num_strategies * data.num_images * estimatedTimePerImage); // Step 5: Generate images
561
 
562
  let elapsedTime = 0;
563
  let progressInterval: NodeJS.Timeout | null = null;
@@ -591,8 +591,8 @@ export default function GeneratePage() {
591
  {
592
  step: "image" as const,
593
  progress: 70,
594
- message: `🖼️ Step 5: Generating ${data.num_images} image(s) per strategy...`,
595
- duration: data.num_strategies * data.num_images * estimatedTimePerImage // seconds
596
  },
597
  ];
598
 
@@ -786,13 +786,12 @@ export default function GeneratePage() {
786
  {isGenerating && (
787
  <>
788
  {mode === "batch" ? (
789
- <BatchProgressComponent
790
- progress={batchProgress}
791
- currentIndex={currentBatchIndex}
792
- totalCount={batchCount}
793
- imagesPerAd={batchImagesPerAd}
794
- generationStartTime={generationStartTime}
795
- />
796
  ) : (
797
  <GenerationProgressComponent progress={progress} generationStartTime={generationStartTime} />
798
  )}
 
33
  const [currentBatchIndex, setCurrentBatchIndex] = useState(0);
34
  const [batchProgress, setBatchProgress] = useState(0);
35
  const [batchCount, setBatchCount] = useState(0);
 
36
 
37
  // Motivators (Matrix mode): generate from angle+concept, user selects one or more for ad generation
38
  const [motivators, setMotivators] = useState<string[]>([]);
 
238
  const batchResponse = await generateBatch({
239
  niche: formattedData.niche,
240
  count: formattedData.num_images,
 
241
  image_model: formattedData.image_model,
242
  method: "standard", // Use standard method only
243
  target_audience: formattedData.target_audience,
 
449
  }
450
  };
451
 
452
+ const handleBatchGenerate = async (data: { niche: Niche; count: number; image_model?: string | null; target_audience?: string | null; offer?: string | null }) => {
453
  setBatchResults([]);
454
  setIsGenerating(true);
455
  setGenerationStartTime(Date.now());
 
462
  setBatchProgress(0);
463
  setCurrentBatchIndex(0);
464
  setBatchCount(data.count);
 
465
 
466
  // Estimate time per ad (roughly 30-60 seconds per ad)
467
  const estimatedTimePerAd = 45; // seconds
 
526
  setIsGenerating(true);
527
  setGenerationStartTime(Date.now());
528
 
529
+ const imagesPerStrategy = 1;
530
+
531
  const formattedData: {
532
  niche: Niche;
533
  custom_niche?: string;
 
541
  custom_niche: data.custom_niche || undefined,
542
  target_audience: data.target_audience || undefined,
543
  offer: data.offer || undefined,
544
+ num_images: imagesPerStrategy,
545
  };
546
 
547
  // Calculate estimated time based on strategies and images
 
557
  20 + // Step 2: Retrieve knowledge
558
  25 + // Step 3: Creative Director
559
  (data.num_strategies * estimatedTimePerStrategy) + // Step 4: Process strategies
560
+ (data.num_strategies * imagesPerStrategy * estimatedTimePerImage); // Step 5: Generate images
561
 
562
  let elapsedTime = 0;
563
  let progressInterval: NodeJS.Timeout | null = null;
 
591
  {
592
  step: "image" as const,
593
  progress: 70,
594
+ message: "🖼️ Step 5: Generating one image for each strategy...",
595
+ duration: data.num_strategies * imagesPerStrategy * estimatedTimePerImage // seconds
596
  },
597
  ];
598
 
 
786
  {isGenerating && (
787
  <>
788
  {mode === "batch" ? (
789
+ <BatchProgressComponent
790
+ progress={batchProgress}
791
+ currentIndex={currentBatchIndex}
792
+ totalCount={batchCount}
793
+ generationStartTime={generationStartTime}
794
+ />
 
795
  ) : (
796
  <GenerationProgressComponent progress={progress} generationStartTime={generationStartTime} />
797
  )}
frontend/components/creative/ModificationForm.tsx CHANGED
@@ -27,11 +27,13 @@ interface ModificationFormProps {
27
  mode: ModificationMode;
28
  imageModel: string | null;
29
  userPrompt: string;
 
30
  onAngleChange: (value: string) => void;
31
  onConceptChange: (value: string) => void;
32
  onModeChange: (mode: ModificationMode) => void;
33
  onImageModelChange: (model: string | null) => void;
34
  onUserPromptChange: (value: string) => void;
 
35
  onSubmit: () => void;
36
  isLoading: boolean;
37
  }
@@ -44,11 +46,13 @@ export const ModificationForm: React.FC<ModificationFormProps> = ({
44
  mode,
45
  imageModel,
46
  userPrompt,
 
47
  onAngleChange,
48
  onConceptChange,
49
  onModeChange,
50
  onImageModelChange,
51
  onUserPromptChange,
 
52
  onSubmit,
53
  isLoading,
54
  }) => {
@@ -318,6 +322,43 @@ export const ModificationForm: React.FC<ModificationFormProps> = ({
318
  </div>
319
  </div>
320
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
321
  {/* User Prompt (Optional) */}
322
  <div className="space-y-4 pt-4 border-t border-gray-100">
323
  <div className="flex items-center gap-2">
 
27
  mode: ModificationMode;
28
  imageModel: string | null;
29
  userPrompt: string;
30
+ variationCount: number;
31
  onAngleChange: (value: string) => void;
32
  onConceptChange: (value: string) => void;
33
  onModeChange: (mode: ModificationMode) => void;
34
  onImageModelChange: (model: string | null) => void;
35
  onUserPromptChange: (value: string) => void;
36
+ onVariationCountChange: (value: number) => void;
37
  onSubmit: () => void;
38
  isLoading: boolean;
39
  }
 
46
  mode,
47
  imageModel,
48
  userPrompt,
49
+ variationCount,
50
  onAngleChange,
51
  onConceptChange,
52
  onModeChange,
53
  onImageModelChange,
54
  onUserPromptChange,
55
+ onVariationCountChange,
56
  onSubmit,
57
  isLoading,
58
  }) => {
 
322
  </div>
323
  </div>
324
 
325
+ {/* Variation Count */}
326
+ <div className="space-y-4 pt-4 border-t border-gray-100">
327
+ <div className="flex items-center gap-2">
328
+ <Sparkles className="w-4 h-4 text-blue-500" />
329
+ <label className="text-sm font-bold text-gray-700 uppercase tracking-wider">
330
+ Variations to Generate
331
+ </label>
332
+ </div>
333
+ <div>
334
+ <div className="flex items-center justify-between mb-2">
335
+ <span className="text-sm font-semibold text-gray-600">
336
+ {variationCount} variation{variationCount > 1 ? "s" : ""}
337
+ </span>
338
+ <span className="text-xs text-gray-500">Up to 3 total</span>
339
+ </div>
340
+ <input
341
+ type="range"
342
+ min={1}
343
+ max={3}
344
+ step={1}
345
+ value={variationCount}
346
+ onChange={(e) => onVariationCountChange(Number(e.target.value))}
347
+ disabled={isLoading}
348
+ className="w-full accent-blue-500"
349
+ />
350
+ <div className="flex justify-between text-xs text-gray-400 mt-1 uppercase tracking-wide">
351
+ <span>1</span>
352
+ <span>2</span>
353
+ <span>3</span>
354
+ </div>
355
+ <p className="text-xs text-gray-500 flex items-center gap-1.5 px-1 mt-2">
356
+ <Info className="w-3 h-3" />
357
+ Generates that many distinct remixes of your original creative.
358
+ </p>
359
+ </div>
360
+ </div>
361
+
362
  {/* User Prompt (Optional) */}
363
  <div className="space-y-4 pt-4 border-t border-gray-100">
364
  <div className="flex items-center gap-2">
frontend/components/generation/BatchForm.tsx CHANGED
@@ -13,7 +13,7 @@ import type { Niche } from "@/types/api";
13
  import { InfoButton } from "@/components/ui/InfoButton";
14
 
15
  interface BatchFormProps {
16
- onSubmit: (data: { niche: Niche; count: number; images_per_ad: number; image_model?: string | null; target_audience?: string | null; offer?: string | null }) => Promise<void>;
17
  isLoading: boolean;
18
  }
19
 
@@ -31,7 +31,6 @@ export const BatchForm: React.FC<BatchFormProps> = ({
31
  defaultValues: {
32
  niche: "home_insurance" as const,
33
  count: 5,
34
- images_per_ad: 1,
35
  image_model: null,
36
  target_audience: "",
37
  offer: "",
@@ -39,7 +38,6 @@ export const BatchForm: React.FC<BatchFormProps> = ({
39
  });
40
 
41
  const count = watch("count");
42
- const imagesPerAd = watch("images_per_ad");
43
  const selectedModel = watch("image_model");
44
 
45
  return (
@@ -49,7 +47,7 @@ export const BatchForm: React.FC<BatchFormProps> = ({
49
  <CardTitle>Batch Generation</CardTitle>
50
  <InfoButton
51
  title="Batch Generation Flow"
52
- content="Generate multiple ads simultaneously for A/B testing and variety. Each ad is created with randomized strategies, giving you diverse options to test. You can generate up to 100 ads with 1-3 variations per ad. Perfect for finding winning combinations through volume testing."
53
  position="bottom"
54
  />
55
  </div>
@@ -130,48 +128,13 @@ export const BatchForm: React.FC<BatchFormProps> = ({
130
  )}
131
  </div>
132
 
133
- <div>
134
- <label className="block text-sm font-semibold text-gray-700 mb-2">
135
- Variations per Ad: <span className="text-blue-600 font-bold">{imagesPerAd}</span>
136
- </label>
137
- <input
138
- type="range"
139
- min="1"
140
- max="3"
141
- step="1"
142
- className="w-full accent-blue-500"
143
- {...register("images_per_ad", { valueAsNumber: true })}
144
- />
145
- <div className="flex justify-between text-xs text-gray-500 mt-1 font-medium">
146
- <span>1</span>
147
- <span>3</span>
148
- </div>
149
- <p className="text-xs text-gray-500 mt-1">
150
- Each variation will have a unique image and slight copy variations
151
- </p>
152
- {errors.images_per_ad && (
153
- <p className="mt-1 text-sm text-red-600">
154
- {errors.images_per_ad.message}
155
- </p>
156
- )}
157
- </div>
158
-
159
- <div className="bg-gradient-to-r from-blue-50 to-cyan-50 border border-blue-200 rounded-xl p-4">
160
- <p className="text-sm font-semibold text-gray-800">
161
- <strong>Estimated:</strong> {count} ads × {imagesPerAd} variation(s) = {count * imagesPerAd} total variations
162
- </p>
163
- <p className="text-xs text-gray-600 mt-1">
164
- This may take several minutes to complete
165
- </p>
166
- </div>
167
-
168
  {/* Cost Estimator */}
169
  <div className="bg-gradient-to-r from-green-50 to-emerald-50 border border-green-200 rounded-xl p-4">
170
  <p className="text-sm font-semibold text-gray-800">
171
- 💰 <strong>Estimated Cost:</strong> {formatCost(getModelCost(selectedModel || "", count * imagesPerAd))}
172
  </p>
173
  <p className="text-xs text-gray-600 mt-1">
174
- {count * imagesPerAd} total image{count * imagesPerAd > 1 ? 's' : ''} × {IMAGE_MODELS.find(m => m.value === (selectedModel || ""))?.label.split(' - ')[0] || "Default model"}
175
  </p>
176
  </div>
177
 
 
13
  import { InfoButton } from "@/components/ui/InfoButton";
14
 
15
  interface BatchFormProps {
16
+ onSubmit: (data: { niche: Niche; count: number; image_model?: string | null; target_audience?: string | null; offer?: string | null }) => Promise<void>;
17
  isLoading: boolean;
18
  }
19
 
 
31
  defaultValues: {
32
  niche: "home_insurance" as const,
33
  count: 5,
 
34
  image_model: null,
35
  target_audience: "",
36
  offer: "",
 
38
  });
39
 
40
  const count = watch("count");
 
41
  const selectedModel = watch("image_model");
42
 
43
  return (
 
47
  <CardTitle>Batch Generation</CardTitle>
48
  <InfoButton
49
  title="Batch Generation Flow"
50
+ content="Generate multiple ads simultaneously for A/B testing and variety. Each ad is created with randomized strategies, giving you diverse options to test. You can generate up to 100 ads in a single run to quickly find winning combinations."
51
  position="bottom"
52
  />
53
  </div>
 
128
  )}
129
  </div>
130
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
131
  {/* Cost Estimator */}
132
  <div className="bg-gradient-to-r from-green-50 to-emerald-50 border border-green-200 rounded-xl p-4">
133
  <p className="text-sm font-semibold text-gray-800">
134
+ 💰 <strong>Estimated Cost:</strong> {formatCost(getModelCost(selectedModel || "", count))}
135
  </p>
136
  <p className="text-xs text-gray-600 mt-1">
137
+ {count} total image{count > 1 ? 's' : ''} × {IMAGE_MODELS.find(m => m.value === (selectedModel || ""))?.label.split(' - ')[0] || "Default model"}
138
  </p>
139
  </div>
140
 
frontend/components/generation/BatchProgress.tsx CHANGED
@@ -3,14 +3,9 @@
3
  import React, { useState, useEffect } from "react";
4
  import { Card, CardContent } from "@/components/ui/Card";
5
  import { ProgressBar } from "@/components/ui/ProgressBar";
6
- import { LoadingSpinner } from "@/components/ui/LoadingSpinner";
7
  import {
8
  Sparkles,
9
- Image as ImageIcon,
10
- Database,
11
  CheckCircle2,
12
- AlertCircle,
13
- Wand2,
14
  Zap,
15
  Package,
16
  X
@@ -21,19 +16,17 @@ interface BatchProgressProps {
21
  progress: number; // 0-100
22
  currentIndex?: number; // Current ad being generated (0-based)
23
  totalCount?: number; // Total number of ads
24
- imagesPerAd?: number; // Number of images per ad
25
  generationStartTime?: number | null;
26
  message?: string;
27
  }
28
 
29
  const BATCH_MESSAGES = [
30
- "Creating your ad variations...",
31
- "Generating unique ad creatives...",
32
  "Crafting compelling visuals...",
33
  "Building your ad collection...",
34
  "Almost there! Finalizing your ads...",
35
- "Working on the perfect variations...",
36
- "This batch is going to be amazing!",
37
  "Great things take time - we're crafting perfection!",
38
  ] as const;
39
 
@@ -52,7 +45,6 @@ export const BatchProgressComponent: React.FC<BatchProgressProps> = ({
52
  progress,
53
  currentIndex = 0,
54
  totalCount = 0,
55
- imagesPerAd = 1,
56
  generationStartTime,
57
  message,
58
  }) => {
@@ -65,8 +57,6 @@ export const BatchProgressComponent: React.FC<BatchProgressProps> = ({
65
  const isComplete = clampedProgress >= 100;
66
  const isStuckAtHighProgress = clampedProgress >= 85 && !isComplete;
67
  const currentAdNumber = currentIndex + 1;
68
- const totalVariations = totalCount * imagesPerAd;
69
- const currentVariation = currentIndex * imagesPerAd + 1;
70
 
71
  // Calculate elapsed time
72
  useEffect(() => {
@@ -185,15 +175,6 @@ export const BatchProgressComponent: React.FC<BatchProgressProps> = ({
185
  {totalCount}
186
  </p>
187
  </div>
188
- <div className="bg-gradient-to-br from-cyan-50 to-pink-50 rounded-xl p-4 border border-cyan-200">
189
- <div className="flex items-center space-x-2 mb-1">
190
- <ImageIcon className="h-4 w-4 text-cyan-600" />
191
- <p className="text-xs font-semibold text-gray-600">Variations</p>
192
- </div>
193
- <p className="text-2xl font-bold bg-gradient-to-r from-cyan-600 to-pink-600 bg-clip-text text-transparent">
194
- {totalVariations}
195
- </p>
196
- </div>
197
  <div className="bg-gradient-to-br from-pink-50 to-purple-50 rounded-xl p-4 border border-pink-200">
198
  <div className="flex items-center space-x-2 mb-1">
199
  <Sparkles className="h-4 w-4 text-pink-600" />
@@ -218,10 +199,9 @@ export const BatchProgressComponent: React.FC<BatchProgressProps> = ({
218
  progress={clampedProgress}
219
  showPercentage={false}
220
  />
221
- {totalCount > 0 && currentIndex >= 0 && (
222
  <p className="text-xs text-gray-500 text-center mt-2">
223
  {currentAdNumber} of {totalCount} ads completed
224
- {imagesPerAd > 1 && ` • ${currentVariation} of ${totalVariations} variations`}
225
  </p>
226
  )}
227
  </div>
@@ -236,7 +216,7 @@ export const BatchProgressComponent: React.FC<BatchProgressProps> = ({
236
  Batch generation completed successfully!
237
  </p>
238
  <p className="text-xs text-green-700 mt-0.5">
239
- {totalCount > 0 ? `${totalCount} ads with ${totalVariations} total variations are ready!` : "All ads are ready to use"}
240
  </p>
241
  </div>
242
  </div>
 
3
  import React, { useState, useEffect } from "react";
4
  import { Card, CardContent } from "@/components/ui/Card";
5
  import { ProgressBar } from "@/components/ui/ProgressBar";
 
6
  import {
7
  Sparkles,
 
 
8
  CheckCircle2,
 
 
9
  Zap,
10
  Package,
11
  X
 
16
  progress: number; // 0-100
17
  currentIndex?: number; // Current ad being generated (0-based)
18
  totalCount?: number; // Total number of ads
 
19
  generationStartTime?: number | null;
20
  message?: string;
21
  }
22
 
23
  const BATCH_MESSAGES = [
24
+ "Generating your ad set...",
 
25
  "Crafting compelling visuals...",
26
  "Building your ad collection...",
27
  "Almost there! Finalizing your ads...",
28
+ "Perfecting each creative...",
29
+ "Lining up scroll-stopping ads...",
30
  "Great things take time - we're crafting perfection!",
31
  ] as const;
32
 
 
45
  progress,
46
  currentIndex = 0,
47
  totalCount = 0,
 
48
  generationStartTime,
49
  message,
50
  }) => {
 
57
  const isComplete = clampedProgress >= 100;
58
  const isStuckAtHighProgress = clampedProgress >= 85 && !isComplete;
59
  const currentAdNumber = currentIndex + 1;
 
 
60
 
61
  // Calculate elapsed time
62
  useEffect(() => {
 
175
  {totalCount}
176
  </p>
177
  </div>
 
 
 
 
 
 
 
 
 
178
  <div className="bg-gradient-to-br from-pink-50 to-purple-50 rounded-xl p-4 border border-pink-200">
179
  <div className="flex items-center space-x-2 mb-1">
180
  <Sparkles className="h-4 w-4 text-pink-600" />
 
199
  progress={clampedProgress}
200
  showPercentage={false}
201
  />
202
+ {totalCount > 0 && currentIndex >= 0 && (
203
  <p className="text-xs text-gray-500 text-center mt-2">
204
  {currentAdNumber} of {totalCount} ads completed
 
205
  </p>
206
  )}
207
  </div>
 
216
  Batch generation completed successfully!
217
  </p>
218
  <p className="text-xs text-green-700 mt-0.5">
219
+ {totalCount > 0 ? `${totalCount} ads are ready!` : "All ads are ready to use"}
220
  </p>
221
  </div>
222
  </div>
frontend/components/generation/ExtensiveForm.tsx CHANGED
@@ -1,6 +1,6 @@
1
  "use client";
2
 
3
- import React from "react";
4
  import { useForm } from "react-hook-form";
5
  import { zodResolver } from "@hookform/resolvers/zod";
6
  import { z } from "zod";
@@ -55,6 +55,7 @@ export const ExtensiveForm: React.FC<ExtensiveFormProps> = ({
55
  handleSubmit,
56
  formState: { errors },
57
  watch,
 
58
  } = useForm<ExtensiveFormData>({
59
  resolver: zodResolver(extensiveSchema),
60
  defaultValues: {
@@ -68,11 +69,19 @@ export const ExtensiveForm: React.FC<ExtensiveFormProps> = ({
68
  },
69
  });
70
 
71
- const numImages = watch("num_images");
72
  const numStrategies = watch("num_strategies");
73
  const selectedNiche = watch("niche");
74
  const selectedModel = watch("image_model");
75
 
 
 
 
 
 
 
 
 
76
  return (
77
  <Card variant="glass">
78
  <CardHeader>
@@ -90,7 +99,7 @@ export const ExtensiveForm: React.FC<ExtensiveFormProps> = ({
90
 
91
  4. COPYWRITER: Writes compelling ad copy (title, body, description) that matches each strategy's emotional tone and psychology trigger.
92
 
93
- You can generate multiple strategies (1-10) and multiple variations per strategy (1-3) for comprehensive testing."
94
  position="bottom"
95
  />
96
  </div>
@@ -99,7 +108,8 @@ You can generate multiple strategies (1-10) and multiple variations per strategy
99
  </CardDescription>
100
  </CardHeader>
101
  <CardContent>
102
- <form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
 
103
  <Select
104
  label="Niche"
105
  options={[
@@ -166,27 +176,6 @@ You can generate multiple strategies (1-10) and multiple variations per strategy
166
  {...register("image_model")}
167
  />
168
 
169
- <div>
170
- <label className="block text-sm font-semibold text-gray-700 mb-2">
171
- Variations per Strategy: <span className="text-blue-600 font-bold">{numImages}</span>
172
- </label>
173
- <input
174
- type="range"
175
- min="1"
176
- max="3"
177
- step="1"
178
- className="w-full accent-blue-500"
179
- {...register("num_images", { valueAsNumber: true })}
180
- />
181
- <div className="flex justify-between text-xs text-gray-500 mt-1 font-medium">
182
- <span>1</span>
183
- <span>3</span>
184
- </div>
185
- <p className="text-xs text-gray-500 mt-1">
186
- Each variation will have a unique image and slight copy variations
187
- </p>
188
- </div>
189
-
190
  <div>
191
  <label className="block text-sm font-semibold text-gray-700 mb-2">
192
  Number of Strategies: <span className="text-blue-600 font-bold">{numStrategies}</span>
@@ -208,22 +197,13 @@ You can generate multiple strategies (1-10) and multiple variations per strategy
208
  </p>
209
  </div>
210
 
211
- <div className="bg-gradient-to-r from-blue-50 to-cyan-50 border border-blue-200 rounded-xl p-4">
212
- <p className="text-sm font-semibold text-gray-800">
213
- <strong>Estimated:</strong> {numStrategies} ads × {numImages} variation(s) = {numStrategies * numImages} total variations
214
- </p>
215
- <p className="text-xs text-gray-600 mt-1">
216
- This may take several minutes to complete
217
- </p>
218
- </div>
219
-
220
  {/* Cost Estimator */}
221
  <div className="bg-gradient-to-r from-green-50 to-emerald-50 border border-green-200 rounded-xl p-4">
222
  <p className="text-sm font-semibold text-gray-800">
223
- 💰 <strong>Estimated Cost:</strong> {formatCost(getModelCost(selectedModel || "", numStrategies * numImages))}
224
  </p>
225
  <p className="text-xs text-gray-600 mt-1">
226
- {numStrategies * numImages} total image{numStrategies * numImages > 1 ? 's' : ''} × {IMAGE_MODELS.find(m => m.value === (selectedModel || ""))?.label.split(' - ')[0] || "Default model"}
227
  </p>
228
  </div>
229
 
 
1
  "use client";
2
 
3
+ import React, { useEffect } from "react";
4
  import { useForm } from "react-hook-form";
5
  import { zodResolver } from "@hookform/resolvers/zod";
6
  import { z } from "zod";
 
55
  handleSubmit,
56
  formState: { errors },
57
  watch,
58
+ setValue,
59
  } = useForm<ExtensiveFormData>({
60
  resolver: zodResolver(extensiveSchema),
61
  defaultValues: {
 
69
  },
70
  });
71
 
72
+ const numImages = 1;
73
  const numStrategies = watch("num_strategies");
74
  const selectedNiche = watch("niche");
75
  const selectedModel = watch("image_model");
76
 
77
+ useEffect(() => {
78
+ setValue("num_images", numImages, { shouldDirty: false, shouldValidate: false });
79
+ }, [numImages, setValue]);
80
+
81
+ const onFormSubmit = handleSubmit(({ num_images, ...rest }) =>
82
+ onSubmit({ ...rest, num_images: num_images ?? 1 })
83
+ );
84
+
85
  return (
86
  <Card variant="glass">
87
  <CardHeader>
 
99
 
100
  4. COPYWRITER: Writes compelling ad copy (title, body, description) that matches each strategy's emotional tone and psychology trigger.
101
 
102
+ You can generate multiple strategies (1-10). Each strategy includes one image and a full ad package for comprehensive testing."
103
  position="bottom"
104
  />
105
  </div>
 
108
  </CardDescription>
109
  </CardHeader>
110
  <CardContent>
111
+ <form onSubmit={onFormSubmit} className="space-y-6">
112
+ <input type="hidden" value={numImages} {...register("num_images", { valueAsNumber: true })} />
113
  <Select
114
  label="Niche"
115
  options={[
 
176
  {...register("image_model")}
177
  />
178
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
179
  <div>
180
  <label className="block text-sm font-semibold text-gray-700 mb-2">
181
  Number of Strategies: <span className="text-blue-600 font-bold">{numStrategies}</span>
 
197
  </p>
198
  </div>
199
 
 
 
 
 
 
 
 
 
 
200
  {/* Cost Estimator */}
201
  <div className="bg-gradient-to-r from-green-50 to-emerald-50 border border-green-200 rounded-xl p-4">
202
  <p className="text-sm font-semibold text-gray-800">
203
+ 💰 <strong>Estimated Cost:</strong> {formatCost(getModelCost(selectedModel || "", numStrategies))}
204
  </p>
205
  <p className="text-xs text-gray-600 mt-1">
206
+ {numStrategies} total image{numStrategies > 1 ? 's' : ''} × {IMAGE_MODELS.find(m => m.value === (selectedModel || ""))?.label.split(' - ')[0] || "Default model"}
207
  </p>
208
  </div>
209
 
frontend/lib/api/endpoints.ts CHANGED
@@ -56,7 +56,6 @@ export const generateAd = async (params: {
56
  export const generateBatch = async (params: {
57
  niche: Niche;
58
  count: number;
59
- images_per_ad: number;
60
  image_model?: string | null;
61
  method?: "standard" | "matrix" | null;
62
  target_audience?: string | null;
 
56
  export const generateBatch = async (params: {
57
  niche: Niche;
58
  count: number;
 
59
  image_model?: string | null;
60
  method?: "standard" | "matrix" | null;
61
  target_audience?: string | null;
frontend/lib/utils/validators.ts CHANGED
@@ -13,7 +13,6 @@ export const generateAdSchema = z.object({
13
  export const generateBatchSchema = z.object({
14
  niche: z.enum(["home_insurance", "glp1", "auto_insurance"]),
15
  count: z.number().min(1).max(100),
16
- images_per_ad: z.number().min(1).max(3),
17
  image_model: z.string().optional().nullable(),
18
  target_audience: z.string().optional().nullable(),
19
  offer: z.string().optional().nullable(),
 
13
  export const generateBatchSchema = z.object({
14
  niche: z.enum(["home_insurance", "glp1", "auto_insurance"]),
15
  count: z.number().min(1).max(100),
 
16
  image_model: z.string().optional().nullable(),
17
  target_audience: z.string().optional().nullable(),
18
  offer: z.string().optional().nullable(),
services/generator.py CHANGED
@@ -901,40 +901,23 @@ If you included people or vehicles, they should look realistic. Otherwise focus
901
  else:
902
  authenticity_section = """=== AUTHENTICITY REQUIREMENTS ===
903
  PEOPLE (if present):
904
- - Real people, NOT models
905
- - Age appropriate for niche (home insurance: 30-60 relatable homeowners; GLP-1: 30-50; auto insurance: only if format requires, avoid elderly/senior look)
906
- - Everyday clothes (not styled)
907
- - Natural expressions (not posed smiles)
908
- - Relatable, trustworthy appearance
909
-
910
- FACE REQUIREMENTS (CRITICAL - for natural, original-looking faces):
911
- - Photorealistic faces with natural skin texture and pores
912
- - Subtle facial asymmetry (real faces are never perfectly symmetrical)
913
- - Natural skin imperfections: fine lines, freckles, moles, natural variations in skin tone
914
- - Realistic facial features: unique nose shapes, eye shapes, lip shapes (not generic or perfect)
915
- - Natural hair texture with individual strands visible, not overly smooth or perfect
916
- - Authentic expressions with natural micro-expressions and subtle wrinkles around eyes/mouth
917
- - Realistic lighting on faces showing natural shadows and highlights
918
- - Natural skin tones with realistic color variations and undertones
919
- - Avoid overly smooth, plastic-looking skin
920
- - Avoid perfectly symmetrical faces
921
- - Avoid generic or "model-like" features
922
- - Include natural facial hair, age spots, or other authentic characteristics when appropriate
923
- - Faces should look like real photographs of real people, not AI-generated portraits
924
 
925
  SETTINGS (if present):
926
- - Real locations (homes, yards, offices)
927
- - Lived-in, not staged
928
- - Everyday items and props
929
- - Natural clutter/imperfection
930
 
931
  DOCUMENTS (if present):
932
- - Real-looking bills, statements, cards
933
- - Visible numbers and text
934
- - Red circles around key information
935
- - Slightly crumpled or worn"""
936
 
937
  prompt = f"""Create a Facebook advertisement image that looks like AUTHENTIC, ORGANIC CONTENT.
 
938
 
939
  {vintage_section}
940
  {framework_section}
@@ -959,65 +942,25 @@ COMPOSITION: {composition}
959
  {get_trending_image_guidance(trending_context)}
960
 
961
  {niche_image_guidance}
962
-
963
- === OPTIONAL STYLING ELEMENTS ===
964
- Apply these effects IF they fit the natural aesthetic (not required):
965
- - Film grain (if vintage style)
966
- - Faded colors (if appropriate)
967
- - Natural lighting variations
968
- - Authentic imperfections (if they occur naturally)
969
-
970
  {authenticity_section}
971
 
972
  === NEGATIVE PROMPTS (AVOID) ===
973
- - NO clean, modern, HD digital look
974
- - NO perfect studio lighting
975
- - NO stock photo aesthetics
976
- - NO obviously AI-generated faces
977
- - NO polished advertising look
978
- - NO missing or garbled text
979
- - NO brand watermarks
980
- - NO distorted anatomy
981
- - NO AI-generated faces, synthetic faces, or fake-looking faces
982
- - NO overly smooth or plastic-looking skin
983
- - NO perfectly symmetrical faces
984
- - NO generic or model-like facial features
985
- - NO uncanny valley faces
986
- - NO faces that look like they came from a face generator
987
- - NO overly perfect, flawless skin
988
- - NO cartoon-like or stylized faces
989
- - NO faces with unnatural smoothness or lack of texture
990
- - NO faces that look like stock photos or professional headshots
991
- - NO decorative borders, frames, or boxes around the image
992
- - NO banners, badges, or logos in corners (like "BREAKING", "TRUSTED", etc.)
993
- - NO overlay boxes or rectangular overlays with text
994
- - NO decorative elements that frame the image
995
- - NO news-style chyrons or tickers
996
- - NO graphic design elements that look like they were added on top
997
- {"- NO numbers, prices, dollar amounts, or savings figures displayed prominently (except for auto insurance ad graphic layouts where they are part of the design)" if not is_auto_insurance_ad_format else "- For ad graphic layouts, numbers and prices are part of the design; ensure they are readable and correct"}
998
- {"- NO text overlays with numerical information" if not is_auto_insurance_ad_format else ""}
999
- {"- Focus on the ad layout: clear headline, prices/rates, and CTA as specified in VISUAL SCENE" if is_auto_insurance_ad_format else "- Focus on the natural scene only, no added presentation elements or numbers"}
1000
- {"- NO fake, made-up, or gibberish brand or company names (e.g. no Alcata, MiCass, ECavelos); use only generic labels like 'Compare Providers' or omit" if is_auto_insurance_ad_format else ""}
1001
- {"- NO in-car dashboard mockups, car interior screens, or displays inside vehicles; NO headshots or faces on screens" if is_auto_insurance_ad_format else ""}
1002
- - NO mixing multiple framework formats (e.g., NO WhatsApp + memo, NO iMessage + document)
1003
- - NO gibberish, placeholder text, or random characters in chat bubbles or documents
1004
- - NO "lorem ipsum", placeholder text, or meaningless character strings
1005
- - If using a framework format (e.g. iMessage, memo), use ONLY that one format - NO mixing
1006
- - NO DUPLICATE TEXT - do not show the same text/message in multiple places
1007
- - NO repeating headlines or prices in different formats or locations
1008
- - Text should appear ONCE only, not multiple times in the image
1009
 
1010
  === OUTPUT ===
1011
  {"Create a scroll-stopping auto insurance ad graphic. Follow the VISUAL SCENE layout exactly: headline, rates/prices, and CTA or buttons as specified. Use only the 6 defined formats (official notification, social post, coverage tiers, car brand grid, gift card CTA, savings/urgency). No other creative types. No fake brand names; no in-car dashboard or screen mockups; no headshots on displays. Clean typography and layout only." if is_auto_insurance_ad_format else f"Create a scroll-stopping image that feels authentic and organic. {'Include the headline text ONCE as specified above - do NOT duplicate it.' if include_text_overlay else 'Focus on the visual scene without text.'} The image should feel like real content - NOT like a designed advertisement."}
1012
 
1013
  CRITICAL REQUIREMENTS:
1014
- {"- Use only the defined ad format from VISUAL SCENE. No dashboard/screen-in-car mockups. No made-up brand names. Borders and buttons only where the format specifies." if is_auto_insurance_ad_format else "- NO decorative borders, frames, or boxes"}
1015
- - NO banners, badges, or logos in corners (unless part of the described ad layout)
1016
- {"- Buttons, rate cards, and panels as described in VISUAL SCENE are part of the design." if is_auto_insurance_ad_format else "- NO overlay boxes or rectangular overlays"}
1017
- - NO duplicate text - any text should appear ONLY ONCE
1018
- - NO repeating the same message in different formats
1019
- {"- Focus on the ad layout and typography as described in VISUAL SCENE." if is_auto_insurance_ad_format else "- Focus on the natural, authentic scene only"}
1020
- - If text is included, show it in ONE location only, not multiple places"""
1021
 
1022
  # Refine and clean the prompt before sending (pass niche for demographic fixes)
1023
  refined_prompt = self._refine_image_prompt(prompt, niche=niche)
 
901
  else:
902
  authenticity_section = """=== AUTHENTICITY REQUIREMENTS ===
903
  PEOPLE (if present):
904
+ - Real, relatable individuals in everyday clothing
905
+ - Ages aligned to the niche (home insurance: 30-60 homeowners; GLP-1: 30-50; auto insurance: only when the format calls for people)
906
+ - Natural expressions and trustworthy energy
907
+
908
+ FACES (close-up):
909
+ - Photorealistic texture with visible pores and natural variation
910
+ - Subtle asymmetry and unique features—never plastic or model-perfect
911
+ - Believable micro-expressions with natural lighting and tone shifts
 
 
 
 
 
 
 
 
 
 
 
 
912
 
913
  SETTINGS (if present):
914
+ - Real, lived-in locations with everyday props and a touch of natural clutter
 
 
 
915
 
916
  DOCUMENTS (if present):
917
+ - Realistic bills or statements with legible details, highlights, and gentle wear"""
 
 
 
918
 
919
  prompt = f"""Create a Facebook advertisement image that looks like AUTHENTIC, ORGANIC CONTENT.
920
+ If the image looks like it belongs on a stock website, it has failed.
921
 
922
  {vintage_section}
923
  {framework_section}
 
942
  {get_trending_image_guidance(trending_context)}
943
 
944
  {niche_image_guidance}
 
 
 
 
 
 
 
 
945
  {authenticity_section}
946
 
947
  === NEGATIVE PROMPTS (AVOID) ===
948
+ - No polished studio lighting or stock-photo aesthetics
949
+ - No synthetic, plastic, or perfectly symmetrical faces
950
+ - No decorative frames, overlays, badges, or repeated text
951
+ - No gibberish, placeholder strings, or fake brand/company names{" (keep labels generic and only where the layout specifies)" if is_auto_insurance_ad_format else ""}
952
+ - No mixing multiple framework formats or duplicating the same message
953
+ {"- No in-car dashboard mockups, interior screens, or faces on displays" if is_auto_insurance_ad_format else ""}
954
+ {"- Numbers, prices, or dollar amounts should appear only when they naturally belong in the scene" if not is_auto_insurance_ad_format else "- Keep typography limited to the headline, rates, and CTA exactly as described"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
955
 
956
  === OUTPUT ===
957
  {"Create a scroll-stopping auto insurance ad graphic. Follow the VISUAL SCENE layout exactly: headline, rates/prices, and CTA or buttons as specified. Use only the 6 defined formats (official notification, social post, coverage tiers, car brand grid, gift card CTA, savings/urgency). No other creative types. No fake brand names; no in-car dashboard or screen mockups; no headshots on displays. Clean typography and layout only." if is_auto_insurance_ad_format else f"Create a scroll-stopping image that feels authentic and organic. {'Include the headline text ONCE as specified above - do NOT duplicate it.' if include_text_overlay else 'Focus on the visual scene without text.'} The image should feel like real content - NOT like a designed advertisement."}
958
 
959
  CRITICAL REQUIREMENTS:
960
+ {"- Follow the VISUAL SCENE layout exactly; use borders, buttons, and rate cards only where described." if is_auto_insurance_ad_format else "- Keep the scene natural—no added frames, overlays, or decorative borders"}
961
+ - Place text or CTA elements exactly once in the location described
962
+ - Present the core message once; do not repeat it elsewhere
963
+ {"- Maintain clean typography and composition per VISUAL SCENE." if is_auto_insurance_ad_format else "- Focus on the authentic moment, not a polished ad layout"}"""
 
 
 
964
 
965
  # Refine and clean the prompt before sending (pass niche for demographic fixes)
966
  refined_prompt = self._refine_image_prompt(prompt, niche=niche)