File size: 8,802 Bytes
f201243 4a56a0b f201243 da4a2eb f201243 8ffe335 f201243 bf8e54b c5771b6 f201243 29066e0 f201243 c5771b6 f201243 4a56a0b f201243 8ffe335 29066e0 f201243 4a56a0b f201243 29066e0 da4a2eb f201243 4a56a0b f201243 8ffe335 4a56a0b 8ffe335 f201243 4a56a0b f201243 b3adf58 f201243 29066e0 f201243 c5771b6 f201243 c5771b6 f201243 8ffe335 f201243 da4a2eb 4a56a0b da4a2eb 4a56a0b da4a2eb f201243 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 |
"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, // first option; flow works for any niche
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>
);
};
|