Spaces:
Sleeping
Sleeping
| "use client"; | |
| import React, { useState, useEffect } from "react"; | |
| import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/Card"; | |
| import { Select } from "@/components/ui/Select"; | |
| import { IMAGE_MODELS } from "@/lib/constants/models"; | |
| import { getAllAngles, getAllConcepts } from "@/lib/api/endpoints"; | |
| import type { ModificationMode, AnglesResponse, ConceptsResponse } from "@/types/api"; | |
| interface AngleOption { | |
| value: string; | |
| label: string; | |
| trigger: string; | |
| category: string; | |
| } | |
| interface ConceptOption { | |
| value: string; | |
| label: string; | |
| structure: string; | |
| category: string; | |
| } | |
| interface ModificationFormProps { | |
| angle: string; | |
| concept: string; | |
| mode: ModificationMode; | |
| imageModel: string | null; | |
| userPrompt: string; | |
| variationCount: number; | |
| onAngleChange: (value: string) => void; | |
| onConceptChange: (value: string) => void; | |
| onModeChange: (mode: ModificationMode) => void; | |
| onImageModelChange: (model: string | null) => void; | |
| onUserPromptChange: (value: string) => void; | |
| onVariationCountChange: (value: number) => void; | |
| onSubmit: () => void; | |
| isLoading: boolean; | |
| } | |
| import { Brain, Sparkles, Wand2, Lightbulb, ChevronDown, Check, Info } from "lucide-react"; | |
| export const ModificationForm: React.FC<ModificationFormProps> = ({ | |
| angle, | |
| concept, | |
| mode, | |
| imageModel, | |
| userPrompt, | |
| variationCount, | |
| onAngleChange, | |
| onConceptChange, | |
| onModeChange, | |
| onImageModelChange, | |
| onUserPromptChange, | |
| onVariationCountChange, | |
| onSubmit, | |
| isLoading, | |
| }) => { | |
| const [angleOptions, setAngleOptions] = useState<AngleOption[]>([]); | |
| const [conceptOptions, setConceptOptions] = useState<ConceptOption[]>([]); | |
| const [loadingOptions, setLoadingOptions] = useState(true); | |
| const [angleInputMode, setAngleInputMode] = useState<"dropdown" | "custom">("dropdown"); | |
| const [conceptInputMode, setConceptInputMode] = useState<"dropdown" | "custom">("dropdown"); | |
| // Fetch angles and concepts on mount | |
| useEffect(() => { | |
| const fetchOptions = async () => { | |
| try { | |
| setLoadingOptions(true); | |
| const [anglesData, conceptsData] = await Promise.all([ | |
| getAllAngles(), | |
| getAllConcepts(), | |
| ]); | |
| const angles: AngleOption[] = []; | |
| Object.entries(anglesData.categories).forEach(([categoryKey, category]) => { | |
| category.angles.forEach((a) => { | |
| angles.push({ | |
| value: a.name, | |
| label: a.name, | |
| trigger: a.trigger, | |
| category: category.name, | |
| }); | |
| }); | |
| }); | |
| setAngleOptions(angles); | |
| const concepts: ConceptOption[] = []; | |
| Object.entries(conceptsData.categories).forEach(([categoryKey, category]) => { | |
| category.concepts.forEach((c) => { | |
| concepts.push({ | |
| value: c.name, | |
| label: c.name, | |
| structure: c.structure, | |
| category: category.name, | |
| }); | |
| }); | |
| }); | |
| setConceptOptions(concepts); | |
| } catch (error) { | |
| console.error("Failed to fetch angles/concepts:", error); | |
| } finally { | |
| setLoadingOptions(false); | |
| } | |
| }; | |
| fetchOptions(); | |
| }, []); | |
| const isValid = angle.trim() || concept.trim() || userPrompt.trim(); | |
| const groupedAngles = angleOptions.reduce((acc, angle) => { | |
| if (!acc[angle.category]) acc[angle.category] = []; | |
| acc[angle.category].push(angle); | |
| return acc; | |
| }, {} as Record<string, AngleOption[]>); | |
| const groupedConcepts = conceptOptions.reduce((acc, concept) => { | |
| if (!acc[concept.category]) acc[concept.category] = []; | |
| acc[concept.category].push(concept); | |
| return acc; | |
| }, {} as Record<string, ConceptOption[]>); | |
| return ( | |
| <Card variant="glass" className="overflow-hidden border-none shadow-2xl p-0"> | |
| <CardHeader className="bg-gradient-to-r from-blue-600/10 to-cyan-500/10 border-b border-blue-500/10 p-8 pb-10"> | |
| <div className="flex items-center gap-4 mb-3"> | |
| <div className="p-2.5 bg-blue-600 rounded-xl shadow-lg shadow-blue-500/30"> | |
| <Sparkles className="w-6 h-6 text-white" /> | |
| </div> | |
| <CardTitle className="text-3xl font-black tracking-tight text-gray-900 border-none p-0 m-0 bg-transparent flex items-center"> | |
| Refine & Transform | |
| </CardTitle> | |
| </div> | |
| <CardDescription className="text-gray-600 text-lg font-medium leading-relaxed max-w-2xl"> | |
| Apply powerful psychological angles and visual concepts to your creative. | |
| </CardDescription> | |
| </CardHeader> | |
| <CardContent className="p-8 pt-10 space-y-10"> | |
| <div className="grid grid-cols-1 md:grid-cols-2 gap-8"> | |
| {/* Angle Selection */} | |
| <div className="space-y-4"> | |
| <div className="flex items-center justify-between"> | |
| <div className="flex items-center gap-2"> | |
| <Brain className="w-4 h-4 text-orange-500" /> | |
| <label className="text-sm font-bold text-gray-700 uppercase tracking-wider"> | |
| Psychological Angle | |
| </label> | |
| </div> | |
| <div className="inline-flex p-1 bg-gray-100 rounded-lg"> | |
| <button | |
| type="button" | |
| onClick={() => setAngleInputMode("dropdown")} | |
| 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" | |
| }`} | |
| > | |
| Presets | |
| </button> | |
| <button | |
| type="button" | |
| onClick={() => setAngleInputMode("custom")} | |
| 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" | |
| }`} | |
| > | |
| Custom | |
| </button> | |
| </div> | |
| </div> | |
| <div className="relative group"> | |
| {angleInputMode === "dropdown" ? ( | |
| <div className="relative"> | |
| <select | |
| value={angle} | |
| onChange={(e) => onAngleChange(e.target.value)} | |
| disabled={loadingOptions} | |
| 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" | |
| > | |
| <option value="">Choose an angle...</option> | |
| {Object.entries(groupedAngles).map(([category, angles]) => ( | |
| <optgroup key={category} label={category} className="font-bold text-gray-400 uppercase text-[10px]"> | |
| {angles.map((a) => ( | |
| <option key={a.value} value={a.value} className="text-gray-900 font-medium"> | |
| {a.label} | |
| </option> | |
| ))} | |
| </optgroup> | |
| ))} | |
| </select> | |
| <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" /> | |
| </div> | |
| ) : ( | |
| <input | |
| type="text" | |
| value={angle} | |
| onChange={(e) => onAngleChange(e.target.value)} | |
| placeholder="e.g., Scarcity, Fear of Missing Out..." | |
| 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" | |
| /> | |
| )} | |
| </div> | |
| <p className="text-xs text-gray-500 flex items-center gap-1.5 px-1"> | |
| <Info className="w-3 h-3" /> | |
| The "Why" - triggers that motivate user action. | |
| </p> | |
| </div> | |
| {/* Concept Selection */} | |
| <div className="space-y-4"> | |
| <div className="flex items-center justify-between"> | |
| <div className="flex items-center gap-2"> | |
| <Lightbulb className="w-4 h-4 text-green-500" /> | |
| <label className="text-sm font-bold text-gray-700 uppercase tracking-wider"> | |
| Visual Concept | |
| </label> | |
| </div> | |
| <div className="inline-flex p-1 bg-gray-100 rounded-lg"> | |
| <button | |
| type="button" | |
| onClick={() => setConceptInputMode("dropdown")} | |
| 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" | |
| }`} | |
| > | |
| Presets | |
| </button> | |
| <button | |
| type="button" | |
| onClick={() => setConceptInputMode("custom")} | |
| 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" | |
| }`} | |
| > | |
| Custom | |
| </button> | |
| </div> | |
| </div> | |
| <div className="relative group"> | |
| {conceptInputMode === "dropdown" ? ( | |
| <div className="relative"> | |
| <select | |
| value={concept} | |
| onChange={(e) => onConceptChange(e.target.value)} | |
| disabled={loadingOptions} | |
| 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" | |
| > | |
| <option value="">Choose a concept...</option> | |
| {Object.entries(groupedConcepts).map(([category, concepts]) => ( | |
| <optgroup key={category} label={category} className="font-bold text-gray-400 uppercase text-[10px]"> | |
| {concepts.map((c) => ( | |
| <option key={c.value} value={c.value} className="text-gray-900 font-medium"> | |
| {c.label} | |
| </option> | |
| ))} | |
| </optgroup> | |
| ))} | |
| </select> | |
| <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" /> | |
| </div> | |
| ) : ( | |
| <input | |
| type="text" | |
| value={concept} | |
| onChange={(e) => onConceptChange(e.target.value)} | |
| placeholder="e.g., Before/After Comparison, User Testimonial..." | |
| 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" | |
| /> | |
| )} | |
| </div> | |
| <p className="text-xs text-gray-500 flex items-center gap-1.5 px-1"> | |
| <Info className="w-3 h-3" /> | |
| The "How" - visual structure and format. | |
| </p> | |
| </div> | |
| </div> | |
| {/* Mode Selection */} | |
| <div className="space-y-4 pt-4 border-t border-gray-100"> | |
| <label className="text-sm font-bold text-gray-700 uppercase tracking-wider block"> | |
| Transformation Mode | |
| </label> | |
| <div className="grid grid-cols-1 md:grid-cols-2 gap-4"> | |
| <button | |
| type="button" | |
| onClick={() => onModeChange("modify")} | |
| className={`group p-5 rounded-2xl border-2 text-left transition-all ${mode === "modify" | |
| ? "border-blue-500 bg-blue-50/50 shadow-md ring-4 ring-blue-500/5" | |
| : "border-gray-100 bg-gray-50/30 hover:border-gray-200 hover:bg-gray-50/60" | |
| }`} | |
| > | |
| <div className="flex items-center justify-between mb-3"> | |
| <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"}`}> | |
| <Wand2 className="w-5 h-5" /> | |
| </div> | |
| {mode === "modify" && <Check className="w-5 h-5 text-blue-500" />} | |
| </div> | |
| <h4 className={`font-bold text-lg mb-1 ${mode === "modify" ? "text-blue-900" : "text-gray-900"}`}>Modify Image</h4> | |
| <p className="text-sm text-gray-600 leading-relaxed"> | |
| Smart edits to existing elements. Preserves ~90% of your original creative while applying the new angle. | |
| </p> | |
| </button> | |
| <button | |
| type="button" | |
| onClick={() => onModeChange("inspired")} | |
| className={`group p-5 rounded-2xl border-2 text-left transition-all ${mode === "inspired" | |
| ? "border-purple-500 bg-purple-50/50 shadow-md ring-4 ring-purple-500/5" | |
| : "border-gray-100 bg-gray-50/30 hover:border-gray-200 hover:bg-gray-50/60" | |
| }`} | |
| > | |
| <div className="flex items-center justify-between mb-3"> | |
| <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"}`}> | |
| <Sparkles className="w-5 h-5" /> | |
| </div> | |
| {mode === "inspired" && <Check className="w-5 h-5 text-purple-500" />} | |
| </div> | |
| <h4 className={`font-bold text-lg mb-1 ${mode === "inspired" ? "text-purple-900" : "text-gray-900"}`}>Inspired Creation</h4> | |
| <p className="text-sm text-gray-600 leading-relaxed"> | |
| Generates a fresh creative from scratch using the original's style and essence as inspiration. | |
| </p> | |
| </button> | |
| </div> | |
| </div> | |
| {/* Variation Count */} | |
| <div className="space-y-4 pt-4 border-t border-gray-100"> | |
| <div className="flex items-center gap-2"> | |
| <Sparkles className="w-4 h-4 text-blue-500" /> | |
| <label className="text-sm font-bold text-gray-700 uppercase tracking-wider"> | |
| Variations to Generate | |
| </label> | |
| </div> | |
| <div> | |
| <div className="flex items-center justify-between mb-2"> | |
| <span className="text-sm font-semibold text-gray-600"> | |
| {variationCount} variation{variationCount > 1 ? "s" : ""} | |
| </span> | |
| <span className="text-xs text-gray-500">Up to 3 total</span> | |
| </div> | |
| <input | |
| type="range" | |
| min={1} | |
| max={3} | |
| step={1} | |
| value={variationCount} | |
| onChange={(e) => onVariationCountChange(Number(e.target.value))} | |
| disabled={isLoading} | |
| className="w-full accent-blue-500" | |
| /> | |
| <div className="flex justify-between text-xs text-gray-400 mt-1 uppercase tracking-wide"> | |
| <span>1</span> | |
| <span>2</span> | |
| <span>3</span> | |
| </div> | |
| <p className="text-xs text-gray-500 flex items-center gap-1.5 px-1 mt-2"> | |
| <Info className="w-3 h-3" /> | |
| Generates that many distinct remixes of your original creative. | |
| </p> | |
| </div> | |
| </div> | |
| {/* User Prompt (Optional) */} | |
| <div className="space-y-4 pt-4 border-t border-gray-100"> | |
| <div className="flex items-center gap-2"> | |
| <Lightbulb className="w-4 h-4 text-purple-500" /> | |
| <label className="text-sm font-bold text-gray-700 uppercase tracking-wider"> | |
| Custom Instructions (Optional) | |
| </label> | |
| </div> | |
| <textarea | |
| value={userPrompt} | |
| onChange={(e) => onUserPromptChange(e.target.value)} | |
| placeholder="e.g., Make the image more vibrant, add a sense of urgency, change the background to blue..." | |
| className="w-full px-4 py-3 rounded-xl border-2 border-gray-100 bg-gray-50/50 hover:bg-white focus:bg-white focus:border-purple-500 focus:ring-4 focus:ring-purple-500/10 transition-all font-medium text-gray-900 min-h-[100px] resize-y" | |
| /> | |
| <p className="text-xs text-gray-500 flex items-center gap-1.5 px-1"> | |
| <Info className="w-3 h-3" /> | |
| Provide specific instructions for how you want the image modified. | |
| </p> | |
| </div> | |
| {/* Image Model Selection */} | |
| <div className="space-y-4 pt-4 border-t border-gray-100"> | |
| <label className="text-sm font-bold text-gray-700 uppercase tracking-wider block"> | |
| AI Engine | |
| </label> | |
| <div className="relative group max-w-sm"> | |
| <select | |
| value={imageModel || ""} | |
| onChange={(e) => onImageModelChange(e.target.value || null)} | |
| 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" | |
| > | |
| {IMAGE_MODELS.map((model) => ( | |
| <option key={model.value} value={model.value}> | |
| {model.label} | |
| </option> | |
| ))} | |
| </select> | |
| <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" /> | |
| </div> | |
| </div> | |
| {/* Footer / Submit */} | |
| <div className="pt-6"> | |
| <button | |
| type="button" | |
| onClick={onSubmit} | |
| disabled={!isValid || isLoading} | |
| 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" | |
| } 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`} | |
| > | |
| <div className="absolute inset-0 bg-white/20 translate-y-full group-hover:translate-y-0 transition-transform duration-300" /> | |
| <span className="flex items-center justify-center gap-3 relative z-10"> | |
| {isLoading ? ( | |
| <> | |
| <Loader2 className="animate-spin h-6 w-6" /> | |
| Bringing your vision to life... | |
| </> | |
| ) : ( | |
| <> | |
| {mode === "modify" ? <Wand2 className="w-6 h-6" /> : <Sparkles className="w-6 h-6" />} | |
| Generate {mode === "modify" ? "Modified" : "Inspired"} Creative | |
| </> | |
| )} | |
| </span> | |
| </button> | |
| {!isValid && ( | |
| <div className="mt-4 flex items-center justify-center gap-2 text-amber-600 animate-pulse"> | |
| <Info className="w-4 h-4" /> | |
| <p className="text-sm font-bold">Please provide at least one: angle, concept, or custom instructions</p> | |
| </div> | |
| )} | |
| </div> | |
| </CardContent> | |
| </Card> | |
| ); | |
| }; | |
| const Loader2 = ({ className }: { className?: string }) => ( | |
| <svg className={className} viewBox="0 0 24 24"> | |
| <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" fill="none" /> | |
| <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" /> | |
| </svg> | |
| ); | |