diff --git a/client/src/components/CompositePreview.tsx b/client/src/components/CompositePreview.tsx index 39cf490..d530fdb 100644 --- a/client/src/components/CompositePreview.tsx +++ b/client/src/components/CompositePreview.tsx @@ -10,7 +10,7 @@ interface CompositePreviewProps { maskUrl?: string | null; compositedUrl?: string | null; } | null | undefined; - layers: Array<{ + layers?: Array<{ id: number; name: string; type: string; @@ -20,6 +20,7 @@ interface CompositePreviewProps { blendMode?: string | null; }>; className?: string; + preferFinalComposite?: boolean; } const blendModeMap: Record = { @@ -29,13 +30,7 @@ const blendModeMap: Record = { overlay: "overlay", }; -export default function CompositePreview({ frame, layers, className }: CompositePreviewProps) { - const visibleLayers = useMemo(() => { - return [...layers] - .filter(l => l.visible !== false) - .sort((a, b) => (a.order ?? 0) - (b.order ?? 0)); - }, [layers]); - +export default function CompositePreview({ frame, layers = [], className, preferFinalComposite = true }: CompositePreviewProps) { if (!frame) { return (
@@ -44,6 +39,24 @@ export default function CompositePreview({ frame, layers, className }: Composite ); } + // If a final composite exists and we prefer it, show it directly (fastest path) + if (preferFinalComposite && frame.compositedUrl) { + return ( + Composite final + ); + } + + // Build layer stack from explicit layers (DB) if any + const visibleDbLayers = useMemo(() => { + return [...layers] + .filter(l => l.visible !== false) + .sort((a, b) => (a.order ?? 0) - (b.order ?? 0)); + }, [layers]); + const getLayerUrl = (layerType: string): string | null => { switch (layerType) { case "background": @@ -58,39 +71,55 @@ export default function CompositePreview({ frame, layers, className }: Composite } }; - // Fallback: if no visible layers, just show the original or composited URL - if (visibleLayers.length === 0) { - const fallback = frame.compositedUrl || frame.originalUrl; - if (!fallback) return null; + // If we have DB layers, render via layer stack + if (visibleDbLayers.length > 0) { return ( - Frame +
+ {visibleDbLayers.map((layer, idx) => { + const url = getLayerUrl(layer.type); + if (!url) return null; + const blendMode = blendModeMap[layer.blendMode || "normal"] || "normal"; + return ( + {layer.name} + ); + })} +
); } + // Fallback: stack the natural layers from frame URLs (bg + fg) + // This works without any DB layers + const naturalLayers: Array<{ url: string; key: string; opacity?: number }> = []; + const bgUrl = frame.regeneratedBgUrl || frame.backgroundUrl || frame.originalUrl; + if (bgUrl) naturalLayers.push({ url: bgUrl, key: "bg" }); + const fgUrl = frame.regeneratedFgUrl || frame.foregroundUrl; + if (fgUrl && fgUrl !== bgUrl) naturalLayers.push({ url: fgUrl, key: "fg" }); + + if (naturalLayers.length === 0) { + return null; + } + return (
- {visibleLayers.map((layer, idx) => { - const url = getLayerUrl(layer.type); - if (!url) return null; - const blendMode = blendModeMap[layer.blendMode || "normal"] || "normal"; - return ( - {layer.name} - ); - })} + {naturalLayers.map((layer, idx) => ( + {layer.key} + ))}
); } diff --git a/client/src/pages/ProjectWorkspace.tsx b/client/src/pages/ProjectWorkspace.tsx index faa0e81..8182e7d 100644 --- a/client/src/pages/ProjectWorkspace.tsx +++ b/client/src/pages/ProjectWorkspace.tsx @@ -11,7 +11,7 @@ import { trpc } from "@/lib/trpc"; import { ArrowLeft, Play, Pause, SkipBack, SkipForward, Layers, Eye, EyeOff, Lock, Unlock, - Wand2, MessageSquare, Settings, Film, Sparkles, + Wand2, MessageSquare, Settings, Film, Sparkles, AlertCircle, Scissors, Image, Users, Bot, Download, ChevronLeft, ChevronRight, Loader2 } from "lucide-react"; @@ -666,128 +666,250 @@ function CharactersPanel({ projectId }: { projectId: number }) { ); } -// Generation panel for AI-powered background/character regeneration +// Generation panel - Studio de comparaison: Original / Fond IA / Perso IA / Composite function GenerationPanel({ projectId, selectedFrame }: { projectId: number; selectedFrame: number }) { - const [bgPrompt, setBgPrompt] = useState("Redessiner cet arrière-plan dans un style Studio Ghibli, couleurs douces et atmosphère onirique"); - const [charPrompt, setCharPrompt] = useState("Redessiner ce personnage dans un style moderne, lignes nettes et couleurs vives"); + const [bgPrompt, setBgPrompt] = useState("Style Studio Ghibli, aquarelle douce, atmosphère onirique"); + const [charPrompt, setCharPrompt] = useState("Style anime moderne, lignes nettes, couleurs vives"); const [bgStyle, setBgStyle] = useState("Studio Ghibli"); - const [resultUrl, setResultUrl] = useState(null); + const [bigPreview, setBigPreview] = useState<"original" | "bg" | "fg" | "composite">("composite"); + const [zoomImage, setZoomImage] = useState(null); + const utils = trpc.useUtils(); + const { data: frame } = trpc.frames.getByIndex.useQuery( + { projectId, frameIndex: selectedFrame }, + { enabled: projectId > 0, staleTime: 5_000 } + ); const { data: characters } = trpc.characters.list.useQuery({ projectId }); const [selectedCharId, setSelectedCharId] = useState(); + const invalidateFrame = () => { + utils.frames.getByIndex.invalidate({ projectId, frameIndex: selectedFrame }); + utils.frames.list.invalidate({ projectId }); + }; + const regenBg = trpc.generation.regenerateBackground.useMutation({ - onSuccess: (data) => { - setResultUrl(data.resultUrl); - toast.success("Arrière-plan regénéré !"); - }, - onError: (err) => { - console.error("[Generation] BG error:", err); - toast.error(`Erreur génération fond: ${err.message}`, { duration: 8000 }); - }, + onSuccess: () => { toast.success("Arrière-plan regénéré !"); invalidateFrame(); setBigPreview("bg"); }, + onError: (err) => toast.error(`Fond: ${err.message}`, { duration: 8000 }), }); const regenChar = trpc.generation.regenerateCharacter.useMutation({ - onSuccess: (data) => { - setResultUrl(data.resultUrl); - toast.success("Personnage redessiné !"); - }, - onError: (err) => { - console.error("[Generation] Char error:", err); - toast.error(`Erreur génération personnage: ${err.message}`, { duration: 8000 }); - }, + onSuccess: () => { toast.success("Personnage redessiné !"); invalidateFrame(); setBigPreview("fg"); }, + onError: (err) => toast.error(`Personnage: ${err.message}`, { duration: 8000 }), }); - const isGenerating = regenBg.isPending || regenChar.isPending; + const composeFrame = trpc.compositing.composeFrame.useMutation({ + onSuccess: () => { toast.success("Composite généré !"); invalidateFrame(); setBigPreview("composite"); }, + onError: (err) => toast.error(`Compositing: ${err.message}`, { duration: 8000 }), + }); + + const isGenerating = regenBg.isPending || regenChar.isPending || composeFrame.isPending; + + const versions = [ + { key: "original" as const, label: "Original", icon: Film, url: frame?.originalUrl, color: "border-blue-500/50 text-blue-400" }, + { key: "bg" as const, label: "Fond IA", icon: Image, url: frame?.regeneratedBgUrl || frame?.backgroundUrl, color: "border-purple-500/50 text-purple-400", loading: regenBg.isPending }, + { key: "fg" as const, label: "Perso IA", icon: Users, url: frame?.regeneratedFgUrl || frame?.foregroundUrl, color: "border-green-500/50 text-green-400", loading: regenChar.isPending }, + { key: "composite" as const, label: "Composite", icon: Sparkles, url: frame?.compositedUrl, color: "border-amber-500/50 text-amber-400", loading: composeFrame.isPending }, + ]; + + const bigUrl = versions.find(v => v.key === bigPreview)?.url; + const canCompose = !!(frame?.regeneratedBgUrl || frame?.backgroundUrl) && !!(frame?.regeneratedFgUrl || frame?.foregroundUrl); return ( -
-
- - Frame sélectionnée : #{selectedFrame + 1} +
+ {/* LEFT: Big preview + version selector */} +
+
+
+ + Frame #{selectedFrame + 1} +
+
+ {versions.map(v => { + const VIcon = v.icon; + const active = bigPreview === v.key; + const hasContent = !!v.url; + return ( + + ); + })} +
+
+ + {/* Big preview */} +
bigUrl && setZoomImage(bigUrl)} + > + {bigUrl ? ( + {bigPreview} + ) : ( +
+ +

Aucune image pour "{versions.find(v=>v.key===bigPreview)?.label}"

+

Lancez la génération à droite →

+
+ )} + + {versions.find(v=>v.key===bigPreview)?.label} + +
+ + {/* Thumbnails strip */} +
+ {versions.map(v => { + const VIcon = v.icon; + const active = bigPreview === v.key; + return ( + + ); + })} +
- {/* Background regeneration */} -
-
- - Regénérer l'arrière-plan -
-