"use client"; import { useEffect, useState } from "react"; import { ArrowRight, Check, Loader2, Sparkles, Upload, X } from "lucide-react"; import { requestCreativeAnalysis, type CreativeAnalysisModel } from "./creativeAnalysisClient"; export type StudioCharacter = { name: string; description: string; look?: string; wardrobe?: string; signatureAction?: string; traits?: string[]; prompt: string; }; const styles = ["电影写实", "国风动画", "二维动漫", "三维动画", "插画概念"]; const views = ["角色立绘", "脸部近景", "表情参考", "角色三视图"] as const; export type StudioView = (typeof views)[number]; export function CharacterStudioPanel({ canvasId, models, onApply, onClose, onOpenLibrary, onUpload, }: { canvasId: string; models: CreativeAnalysisModel[]; onApply: (character: StudioCharacter, style: string, selectedViews: StudioView[], referenceUrl?: string) => Promise; onClose: () => void; onOpenLibrary: () => void; onUpload: (file: File) => Promise; }) { const [description, setDescription] = useState(""); const [style, setStyle] = useState(styles[0]); const [characters, setCharacters] = useState([]); const [selected, setSelected] = useState(0); const [selectedViews, setSelectedViews] = useState([...views]); const [referenceUrl, setReferenceUrl] = useState(); const [referenceName, setReferenceName] = useState(""); const [busy, setBusy] = useState(false); const [error, setError] = useState(""); const [restoredDraftFor, setRestoredDraftFor] = useState(""); const character = characters[selected]; useEffect(() => { try { const draft = JSON.parse(localStorage.getItem(`tvstar:character-draft:${canvasId}`) || "null"); if (draft && typeof draft === "object") { if (typeof draft.description === "string") setDescription(draft.description); if (styles.includes(draft.style)) setStyle(draft.style); if (Array.isArray(draft.characters)) setCharacters(draft.characters.filter((item: StudioCharacter) => item && typeof item.name === "string" && typeof item.prompt === "string").slice(0, 8)); if (Number.isInteger(draft.selected)) setSelected(Math.max(0, draft.selected)); if (Array.isArray(draft.selectedViews)) setSelectedViews(draft.selectedViews.filter((item: StudioView) => views.includes(item))); if (typeof draft.referenceUrl === "string") setReferenceUrl(draft.referenceUrl); if (typeof draft.referenceName === "string") setReferenceName(draft.referenceName); } } catch { /* a damaged local draft must not block the studio */ } setRestoredDraftFor(canvasId); }, [canvasId]); useEffect(() => { if (restoredDraftFor !== canvasId) return; try { localStorage.setItem(`tvstar:character-draft:${canvasId}`, JSON.stringify({ description, style, characters, selected, selectedViews, referenceUrl, referenceName, })); } catch { /* storage is optional */ } }, [canvasId, characters, description, referenceName, referenceUrl, restoredDraftFor, selected, selectedViews, style]); const resetDraft = () => { setDescription(""); setStyle(styles[0]); setCharacters([]); setSelected(0); setSelectedViews([...views]); setReferenceUrl(undefined); setReferenceName(""); setError(""); }; const analyze = async () => { if (!description.trim() || busy) return; setError(""); setBusy(true); try { const data = await requestCreativeAnalysis({ mode: "character-scene-extraction", nodeTitle: "角色造型室", models, prompt: `视觉风格:${style}。请提取并具体定义角色身份、稳定的面部与服装特征,提供可复用的造型提示词。\n角色描述:${description.trim()}`, imageUrls: referenceUrl ? [referenceUrl] : [], }); const items = Array.isArray(data.characters) ? data.characters.filter((item): item is StudioCharacter => Boolean(item && typeof item === "object" && (item as StudioCharacter).name && (item as StudioCharacter).prompt)).slice(0, 8) : []; if (!items.length) throw new Error("模型没有返回有效角色,请补充外观描述后重试"); setCharacters(items); setSelected(0); } catch (cause) { setError(cause instanceof Error ? cause.message : "角色分析失败"); } finally { setBusy(false); } }; const upload = async (file?: File) => { if (!file || !file.type.startsWith("image/")) return; setError(""); setBusy(true); try { setReferenceUrl(await onUpload(file)); setReferenceName(file.name); } catch (cause) { setError(cause instanceof Error ? cause.message : "参考图上传失败"); } finally { setBusy(false); } }; const toggleView = (view: StudioView) => setSelectedViews(current => current.includes(view) ? current.filter(item => item !== view) : [...current, view]); const apply = async () => { if (!character || !selectedViews.length || busy) return; setError(""); setBusy(true); try { if (!await onApply(character, style, selectedViews, referenceUrl)) setError("角色视图任务未能启动,请检查模型与生成配置后重试"); } catch (cause) { setError(cause instanceof Error ? cause.message : "角色视图任务未能启动"); } finally { setBusy(false); } }; return

角色造型室

建立稳定角色设定,再送入画布生成视图