|
|
"use client"; |
|
|
|
|
|
import React, { useEffect } from "react"; |
|
|
import { useForm } from "react-hook-form"; |
|
|
import { zodResolver } from "@hookform/resolvers/zod"; |
|
|
import { z } from "zod"; |
|
|
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/Card"; |
|
|
import { Select } from "@/components/ui/Select"; |
|
|
import { IMAGE_MODELS, getModelCost, formatCost } from "@/lib/constants/models"; |
|
|
import type { Niche } from "@/types/api"; |
|
|
import { InfoButton } from "@/components/ui/InfoButton"; |
|
|
|
|
|
const extensiveSchema = z.object({ |
|
|
niche: z.enum(["home_insurance", "glp1", "auto_insurance", "others"]), |
|
|
custom_niche: z.string().optional().nullable(), |
|
|
target_audience: z.string().optional().nullable(), |
|
|
offer: z.string().optional().nullable(), |
|
|
num_images: z.number().min(1).max(3), |
|
|
num_strategies: z.number().min(1).max(10), |
|
|
image_model: z.string().nullable().optional(), |
|
|
}).refine( |
|
|
(data) => { |
|
|
if (data.niche === "others") { |
|
|
return data.custom_niche && data.custom_niche.trim().length > 0; |
|
|
} |
|
|
return true; |
|
|
}, |
|
|
{ |
|
|
message: "Please enter your custom niche", |
|
|
path: ["custom_niche"], |
|
|
} |
|
|
); |
|
|
|
|
|
type ExtensiveFormData = z.infer<typeof extensiveSchema>; |
|
|
|
|
|
interface ExtensiveFormProps { |
|
|
onSubmit: (data: { |
|
|
niche: Niche; |
|
|
custom_niche?: string | null; |
|
|
target_audience?: string | null; |
|
|
offer?: string | null; |
|
|
num_images: number; |
|
|
num_strategies: number; |
|
|
image_model?: string | null; |
|
|
}) => Promise<void>; |
|
|
isLoading: boolean; |
|
|
} |
|
|
|
|
|
export const ExtensiveForm: React.FC<ExtensiveFormProps> = ({ |
|
|
onSubmit, |
|
|
isLoading, |
|
|
}) => { |
|
|
const { |
|
|
register, |
|
|
handleSubmit, |
|
|
formState: { errors }, |
|
|
watch, |
|
|
setValue, |
|
|
} = useForm<ExtensiveFormData>({ |
|
|
resolver: zodResolver(extensiveSchema), |
|
|
defaultValues: { |
|
|
niche: "home_insurance" as const, |
|
|
custom_niche: "", |
|
|
target_audience: "", |
|
|
offer: "", |
|
|
num_images: 1, |
|
|
num_strategies: 5, |
|
|
image_model: null, |
|
|
}, |
|
|
}); |
|
|
|
|
|
const numImages = 1; |
|
|
const numStrategies = watch("num_strategies"); |
|
|
const selectedNiche = watch("niche"); |
|
|
const selectedModel = watch("image_model"); |
|
|
|
|
|
useEffect(() => { |
|
|
setValue("num_images", numImages, { shouldDirty: false, shouldValidate: false }); |
|
|
}, [numImages, setValue]); |
|
|
|
|
|
const onFormSubmit = handleSubmit(({ num_images, ...rest }) => |
|
|
onSubmit({ ...rest, num_images: num_images ?? 1 }) |
|
|
); |
|
|
|
|
|
return ( |
|
|
<Card variant="glass"> |
|
|
<CardHeader> |
|
|
<div className="flex items-center gap-2"> |
|
|
<CardTitle>Extensive Generation</CardTitle> |
|
|
<InfoButton |
|
|
title="Extensive Generation Flow" |
|
|
content="This flow works for any niche (insurance, GLP-1, auto, or custom). It uses a 4-stage process: |
|
|
|
|
|
1. RESEARCHER: Analyzes your inputs and researches psychology triggers, angles, and concepts that work best for your niche and audience. |
|
|
|
|
|
2. CREATIVE DIRECTOR: Uses marketing knowledge and successful ad patterns to create multiple creative strategies. Each strategy includes visual direction, text placement, CTA, and copy ideas. |
|
|
|
|
|
3. DESIGNER: Converts each creative strategy into detailed image generation prompts optimized for affiliate marketing (authentic, low-production style). |
|
|
|
|
|
4. COPYWRITER: Writes compelling ad copy (title, body, description) that matches each strategy's emotional tone and psychology trigger. |
|
|
|
|
|
You can generate multiple strategies (1-10). Each strategy includes one image and a full ad package for comprehensive testing." |
|
|
position="bottom" |
|
|
/> |
|
|
</div> |
|
|
<CardDescription> |
|
|
Researcher → Creative Director → Designer → Copywriter flow |
|
|
</CardDescription> |
|
|
</CardHeader> |
|
|
<CardContent> |
|
|
<form onSubmit={onFormSubmit} className="space-y-6"> |
|
|
<input type="hidden" value={numImages} {...register("num_images", { valueAsNumber: true })} /> |
|
|
<Select |
|
|
label="Niche" |
|
|
options={[ |
|
|
{ value: "home_insurance", label: "Home Insurance" }, |
|
|
{ value: "glp1", label: "GLP-1" }, |
|
|
{ value: "auto_insurance", label: "Auto Insurance" }, |
|
|
{ value: "others", label: "Others (Custom)" }, |
|
|
]} |
|
|
error={errors.niche?.message} |
|
|
{...register("niche")} |
|
|
/> |
|
|
|
|
|
{selectedNiche === "others" && ( |
|
|
<div> |
|
|
<label className="block text-sm font-semibold text-gray-700 mb-2"> |
|
|
Custom Niche <span className="text-red-500">*</span> |
|
|
</label> |
|
|
<input |
|
|
type="text" |
|
|
className="w-full px-4 py-3 rounded-xl border-2 border-gray-300 bg-white/80 backdrop-blur-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all duration-250" |
|
|
placeholder="e.g., Pet Insurance, Solar Panels, Fitness Supplements" |
|
|
{...register("custom_niche")} |
|
|
/> |
|
|
{errors.custom_niche && ( |
|
|
<p className="text-red-500 text-xs mt-1">{errors.custom_niche.message}</p> |
|
|
)} |
|
|
</div> |
|
|
)} |
|
|
|
|
|
<div> |
|
|
<label className="block text-sm font-semibold text-gray-700 mb-2"> |
|
|
Target Audience <span className="text-gray-400 font-normal">(Optional)</span> |
|
|
</label> |
|
|
<input |
|
|
type="text" |
|
|
className="w-full px-4 py-3 rounded-xl border-2 border-gray-300 bg-white/80 backdrop-blur-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all duration-250" |
|
|
placeholder="e.g., US people over 50+ age" |
|
|
{...register("target_audience")} |
|
|
/> |
|
|
{errors.target_audience && ( |
|
|
<p className="text-red-500 text-xs mt-1">{errors.target_audience.message}</p> |
|
|
)} |
|
|
</div> |
|
|
|
|
|
<div> |
|
|
<label className="block text-sm font-semibold text-gray-700 mb-2"> |
|
|
Offer <span className="text-gray-400 font-normal">(Optional)</span> |
|
|
</label> |
|
|
<input |
|
|
type="text" |
|
|
className="w-full px-4 py-3 rounded-xl border-2 border-gray-300 bg-white/80 backdrop-blur-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all duration-250" |
|
|
placeholder="e.g., Free quote, Save $500/year, Limited-time offer" |
|
|
{...register("offer")} |
|
|
/> |
|
|
{errors.offer && ( |
|
|
<p className="text-red-500 text-xs mt-1">{errors.offer.message}</p> |
|
|
)} |
|
|
</div> |
|
|
|
|
|
<Select |
|
|
label="Image Model" |
|
|
options={IMAGE_MODELS.map(model => ({ value: model.value, label: model.label }))} |
|
|
error={errors.image_model?.message} |
|
|
{...register("image_model")} |
|
|
/> |
|
|
|
|
|
<div> |
|
|
<label className="block text-sm font-semibold text-gray-700 mb-2"> |
|
|
Number of Strategies: <span className="text-blue-600 font-bold">{numStrategies}</span> |
|
|
</label> |
|
|
<input |
|
|
type="range" |
|
|
min="1" |
|
|
max="10" |
|
|
step="1" |
|
|
className="w-full accent-blue-500" |
|
|
{...register("num_strategies", { valueAsNumber: true })} |
|
|
/> |
|
|
<div className="flex justify-between text-xs text-gray-500 mt-1 font-medium"> |
|
|
<span>1</span> |
|
|
<span>10</span> |
|
|
</div> |
|
|
<p className="text-xs text-gray-500 mt-1"> |
|
|
More strategies = more variety, but longer generation time |
|
|
</p> |
|
|
</div> |
|
|
|
|
|
{/* Cost Estimator */} |
|
|
<div className="bg-gradient-to-r from-green-50 to-emerald-50 border border-green-200 rounded-xl p-4"> |
|
|
<p className="text-sm font-semibold text-gray-800"> |
|
|
💰 <strong>Estimated Cost:</strong> {formatCost(getModelCost(selectedModel || "", numStrategies))} |
|
|
</p> |
|
|
<p className="text-xs text-gray-600 mt-1"> |
|
|
{numStrategies} total image{numStrategies > 1 ? 's' : ''} × {IMAGE_MODELS.find(m => m.value === (selectedModel || ""))?.label.split(' - ')[0] || "Default model"} |
|
|
</p> |
|
|
</div> |
|
|
|
|
|
<button |
|
|
type="submit" |
|
|
disabled={isLoading} |
|
|
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" |
|
|
> |
|
|
{isLoading ? "Generating..." : "Generate with Extensive"} |
|
|
</button> |
|
|
</form> |
|
|
</CardContent> |
|
|
</Card> |
|
|
); |
|
|
}; |
|
|
|