| "use client"; | |
| import React from "react"; | |
| import { useForm } from "react-hook-form"; | |
| import { zodResolver } from "@hookform/resolvers/zod"; | |
| import { generateBatchSchema } from "@/lib/utils/validators"; | |
| import { Input } from "@/components/ui/Input"; | |
| import { Select } from "@/components/ui/Select"; | |
| import { Button } from "@/components/ui/Button"; | |
| import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/Card"; | |
| import { IMAGE_MODELS, getModelCost, formatCost } from "@/lib/constants/models"; | |
| import type { Niche } from "@/types/api"; | |
| import { InfoButton } from "@/components/ui/InfoButton"; | |
| interface BatchFormProps { | |
| onSubmit: (data: { niche: Niche; count: number; image_model?: string | null; target_audience?: string | null; offer?: string | null }) => Promise<void>; | |
| isLoading: boolean; | |
| } | |
| export const BatchForm: React.FC<BatchFormProps> = ({ | |
| onSubmit, | |
| isLoading, | |
| }) => { | |
| const { | |
| register, | |
| handleSubmit, | |
| formState: { errors }, | |
| watch, | |
| } = useForm({ | |
| resolver: zodResolver(generateBatchSchema), | |
| defaultValues: { | |
| niche: "home_insurance" as const, | |
| count: 5, | |
| image_model: null, | |
| target_audience: "", | |
| offer: "", | |
| }, | |
| }); | |
| const count = watch("count"); | |
| const selectedModel = watch("image_model"); | |
| return ( | |
| <Card variant="glass"> | |
| <CardHeader> | |
| <div className="flex items-center gap-2"> | |
| <CardTitle>Batch Generation</CardTitle> | |
| <InfoButton | |
| title="Batch Generation Flow" | |
| 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." | |
| position="bottom" | |
| /> | |
| </div> | |
| <CardDescription> | |
| Generate multiple ads at once for testing and variety | |
| </CardDescription> | |
| </CardHeader> | |
| <CardContent> | |
| <form onSubmit={handleSubmit(onSubmit)} className="space-y-6"> | |
| <Select | |
| label="Niche" | |
| options={[ | |
| { value: "home_insurance", label: "Home Insurance" }, | |
| { value: "glp1", label: "GLP-1" }, | |
| { value: "auto_insurance", label: "Auto Insurance" }, | |
| ]} | |
| error={errors.niche?.message} | |
| {...register("niche")} | |
| /> | |
| <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., Don't overpay your insurance" | |
| {...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-medium text-gray-700 mb-2"> | |
| Number of Ads: {count} | |
| </label> | |
| <input | |
| type="range" | |
| min="1" | |
| max="100" | |
| step="1" | |
| className="w-full" | |
| {...register("count", { valueAsNumber: true })} | |
| /> | |
| <div className="flex justify-between text-xs text-gray-500 mt-1"> | |
| <span>1</span> | |
| <span>100</span> | |
| </div> | |
| {errors.count && ( | |
| <p className="mt-1 text-sm text-red-600"> | |
| {errors.count.message} | |
| </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 || "", count))} | |
| </p> | |
| <p className="text-xs text-gray-600 mt-1"> | |
| {count} total image{count > 1 ? 's' : ''} × {IMAGE_MODELS.find(m => m.value === (selectedModel || ""))?.label.split(' - ')[0] || "Default model"} | |
| </p> | |
| </div> | |
| <Button | |
| type="submit" | |
| variant="primary" | |
| size="lg" | |
| isLoading={isLoading} | |
| className="w-full" | |
| > | |
| Generate Batch | |
| </Button> | |
| </form> | |
| </CardContent> | |
| </Card> | |
| ); | |
| }; | |