|
|
"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"); |
|
|
|
|
|
|
|
|
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> |
|
|
); |
|
|
|