import { useEffect, useMemo, useState } from "react"; import { useSearchParams } from "react-router-dom"; import type { LoraCheckpoint } from "../../types"; interface GenerateTabProps { checkpoints: LoraCheckpoint[]; onQueued?: () => void; } type GenerateMode = "image" | "t2v" | "i2v"; interface GenerateResponse { ok: boolean; prompt_id?: string | null; checkpoint?: string | null; lora_name?: string | null; mode?: string; node_errors?: Record | null; } interface CharacterSummary { id: string; name: string; trigger: string | null; loras: { workflow?: string; name?: string; strength?: number }[]; source_images: string[]; } const DEFAULT_IMAGE_PROMPT = "Rigo, portrait in a sleek AI media studio, leaning one hand on a workstation beside glowing video timeline screens, calm confident expression, modern editorial photography, warm cinematic key light, 85mm lens, shallow depth of field, realistic skin texture and sharp facial detail."; const DEFAULT_VIDEO_PROMPT = "cinematic motion, dramatic camera movement, atmospheric lighting, dynamic composition, polished short-form video style"; function checkpointLabel(checkpoint: LoraCheckpoint) { if (checkpoint.step == null) return `${checkpoint.name} · final`; return `${checkpoint.name} · step ${checkpoint.step.toLocaleString()}`; } function slugPrompt(prompt: string) { const slug = prompt .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/^-+|-+$/g, "") .slice(0, 36); return slug || "generation"; } function outputPrefix(mode: GenerateMode, prompt: string) { const bucket = mode === "image" ? "images" : "videos"; return `${bucket}/${slugPrompt(prompt)}-${Date.now()}`; } export function GenerateTab({ checkpoints, onQueued }: GenerateTabProps) { const latestCheckpoint = useMemo(() => { const final = checkpoints.find((checkpoint) => checkpoint.step == null); return final?.name || checkpoints[checkpoints.length - 1]?.name || "latest"; }, [checkpoints]); const [searchParams, setSearchParams] = useSearchParams(); const [mode, setMode] = useState("image"); const [characters, setCharacters] = useState([]); const [characterId, setCharacterId] = useState("none"); const [checkpoint, setCheckpoint] = useState("base"); const [prompt, setPrompt] = useState(DEFAULT_IMAGE_PROMPT); const [sourceImage, setSourceImage] = useState(""); const [width, setWidth] = useState(1248); const [height, setHeight] = useState(832); const [steps, setSteps] = useState(20); const [guidance, setGuidance] = useState(4); const [loraStrength, setLoraStrength] = useState(1); const [submitting, setSubmitting] = useState(false); const [result, setResult] = useState(null); const [error, setError] = useState(null); useEffect(() => { fetch("/api/characters") .then((response) => response.json()) .then((data) => setCharacters(data.characters || [])) .catch(() => setCharacters([])); }, []); // When ?character= appears in the URL, pre-select it and clear the param useEffect(() => { const preselect = searchParams.get("character"); if (preselect) { setCharacterId(preselect); setSearchParams({}, { replace: true }); } }, [searchParams.get("character")]); const selectedCharacter = characters.find((character) => character.id === characterId); const selectedCharacterHasImageLora = Boolean(selectedCharacter?.loras?.some((lora) => lora.workflow === "flux2_lora")); function selectMode(nextMode: GenerateMode) { setMode(nextMode); setPrompt(nextMode === "image" ? DEFAULT_IMAGE_PROMPT : DEFAULT_VIDEO_PROMPT); setResult(null); setError(null); } async function submit() { const cleanPrompt = prompt.trim(); if (!cleanPrompt) { setError("Prompt is required."); return; } if (mode === "i2v" && !sourceImage.trim()) { setError("Image-to-video needs a source image path from the gallery, like images/example.png."); return; } setSubmitting(true); setError(null); setResult(null); try { const filenamePrefix = outputPrefix(mode, cleanPrompt); const endpoint = mode === "image" ? "/api/image/generate" : "/api/video/generate"; const useCharacter = characterId !== "none"; const useCheckpoint = !useCharacter && checkpoint !== "base"; const body = mode === "image" ? { workflow: "flux2_lora", character: useCharacter ? characterId : undefined, checkpoint: useCheckpoint ? (checkpoint || latestCheckpoint) : undefined, prompt: cleanPrompt, width, height, steps, guidance, lora_strength: loraStrength, filename_prefix: filenamePrefix, submit: true, } : { mode, character: useCharacter ? characterId : undefined, image: mode === "i2v" ? sourceImage.trim() : undefined, prompt: cleanPrompt, width, height, filename_prefix: filenamePrefix, submit: true, }; const response = await fetch(endpoint, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }); const data = await response.json().catch(() => ({})); if (!response.ok) { throw new Error(data?.detail || `${response.status}: failed to queue generation`); } setResult(data); onQueued?.(); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } finally { setSubmitting(false); } } const characterPhrase = selectedCharacter ? ` using ${selectedCharacter.name}` : ""; const agentInstruction = mode === "image" ? `Generate a new image${characterPhrase} from this idea.` : mode === "t2v" ? "Generate a short video from this idea." : "Animate this gallery image into a short video."; return (

Tell your agent what to make

Describe the result. The agent chooses the character, workflow, endpoint, and settings.

“Generate an image of me walking through a rainy cyberpunk street.”

{[ ["image", "Image"], ["t2v", "Text → Video"], ["i2v", "Image → Video"], ].map(([id, label]) => ( ))}