import { useEffect, useMemo, useRef, useState } from "react"; import { ArrowLeft, Box, ChevronRight, FolderOpen, History, Image as ImageIcon, LoaderCircle, Play, Plus, Sparkles, Trash2, Upload, UserRound, Users, Video, WandSparkles, X, } from "lucide-react"; import { api, ApiError } from "../api"; import { findCatalogModel } from "../components/omni-param-bar"; import { modelResolutions } from "../components/free-create/constants"; import { ConfirmModal } from "../components/overlays"; import type { CreationConversation, CreationRef, ModelConfig } from "../types"; import type { NavigateFn } from "./route-config"; type OutputMode = "video" | "image"; type PresetItem = { name: string; category: string; mode: OutputMode; title: string; desc?: string; starter?: string; cover: string; previewVideo?: string; }; const VIDEO_PRESETS: PresetItem[] = [ { name: "痛点解决演示", category: "commerce", mode: "video", title: "痛点解决演示", desc: "从真实痛点到正常使用,再用看得见的过程证明商品价值。", starter: "基于商品的真实用途创作一条痛点解决演示:先让人看见困扰,再拍清使用步骤和真实结果,不夸张、不出现不合理的测试。", cover: "/assets/video-presets/pain-point-demo.jpg", previewVideo: "/assets/video-presets/pain-point-demo.mp4" }, { name: "剧情反转带货", category: "story", mode: "video", title: "剧情反转带货", desc: "用人物冲突与意外反转建立记忆点,商品承担剧情中的关键作用。", starter: "创作一条有前后反转的剧情带货视频,让商品自然成为解决问题的关键。", cover: "/assets/video-presets/plot-twist-commerce.jpg", previewVideo: "/assets/video-presets/plot-twist-commerce.mp4" }, { name: "达人口播种草", category: "speaker", mode: "video", title: "达人口播种草", desc: "用真实体验与生活化表达建立信任,适合电商和本地生活内容。", starter: "创作一条真实自然的达人口播种草视频,重点讲清使用场景和核心卖点。", cover: "/assets/video-presets/creator-recommendation.jpg", previewVideo: "/assets/video-presets/creator-recommendation.mp4" }, { name: "鱼眼换装", category: "visual", mode: "video", title: "鱼眼换装", desc: "强调近距离透视与连续变装节奏,适合服饰和人物视觉内容。", starter: "创作一条鱼眼镜头风格的连续换装视频,人物和服装需要保持稳定。", cover: "/assets/video-presets/rhythm-outfit-change.jpg", previewVideo: "/assets/video-presets/rhythm-outfit-change.mp4" }, { name: "多色商品换款", category: "visual", mode: "video", title: "多色商品换款", desc: "统一商品比例与机位,用动作连续展示不同颜色和款式。", starter: "创作一条多色商品换款视频:统一构图展示不同颜色或款式,用明确动作触发切换,最后给出全款总览。", cover: "/assets/video-presets/multi-sku-switch.jpg", previewVideo: "/assets/video-presets/multi-sku-switch.mp4" }, { name: "AI 宠物拟人", category: "story", mode: "video", title: "AI 宠物拟人", desc: "让宠物角色参与有趣小剧情,同时保留商品真实结构与用途。", starter: "创作一条 AI 宠物拟人短片:宠物有明确性格和动作,商品以真实结构和正常用法自然参与剧情。", cover: "/assets/video-presets/ai-pet-personification.jpg", previewVideo: "/assets/video-presets/ai-pet-personification.mp4" }, ]; const IMAGE_PRESETS: PresetItem[] = [ { name: "电影感", category: "style", mode: "image", title: "电影感", cover: "/assets/image-presets/cinematic.png" }, { name: "日式清新", category: "style", mode: "image", title: "日式清新", cover: "/assets/image-presets/japanese-fresh.png" }, { name: "复古胶片", category: "style", mode: "image", title: "复古胶片", cover: "/assets/image-presets/film-vintage.png" }, { name: "暖调生活感", category: "style", mode: "image", title: "暖调生活感", cover: "/assets/image-presets/warm-lifestyle.png" }, { name: "高质感杂志", category: "style", mode: "image", title: "高质感杂志", cover: "/assets/image-presets/editorial.png" }, { name: "梦幻柔光", category: "style", mode: "image", title: "梦幻柔光", cover: "/assets/image-presets/dreamy-soft.png" }, { name: "黑金高级", category: "style", mode: "image", title: "黑金高级", cover: "/assets/image-presets/black-gold.png" }, { name: "冷峻科技", category: "style", mode: "image", title: "冷峻科技", cover: "/assets/image-presets/tech-cool.png" }, { name: "赛博朋克", category: "style", mode: "image", title: "赛博朋克", cover: "/assets/image-presets/cyberpunk.png" }, { name: "日系手绘动画", category: "style", mode: "image", title: "日系手绘动画", cover: "/assets/image-presets/japanese-hand-drawn-anime.png" }, { name: "国风水墨", category: "style", mode: "image", title: "国风水墨", cover: "/assets/image-presets/chinese-ink-wash.png" }, { name: "新中式", category: "style", mode: "image", title: "新中式", cover: "/assets/image-presets/neo-chinese.png" }, ]; const MENTION_REF_LIMIT = 5; const MENTION_TABS: Array<{ type: CreationRef["type"]; label: string; Icon: typeof ImageIcon }> = [ { type: "asset", label: "素材", Icon: ImageIcon }, { type: "character", label: "角色", Icon: UserRound }, { type: "model", label: "模特", Icon: Users }, { type: "product", label: "商品", Icon: Box }, { type: "scene", label: "场景", Icon: FolderOpen }, ]; function mergeSessionUploads(results: CreationRef[], uploads: CreationRef[], query: string, type: CreationRef["type"]) { if (type !== "asset") return results; const q = query.trim().toLowerCase(); const extras = uploads.filter((item) => !q || item.name.toLowerCase().includes(q)); const seen = new Set(extras.map((item) => item.id)); return [...extras, ...results.filter((item) => !seen.has(item.id))]; } function formatRelativeTime(iso: string) { const then = new Date(iso).getTime(); if (Number.isNaN(then)) return ""; const minutes = Math.floor((Date.now() - then) / 60000); if (minutes < 1) return "刚刚"; if (minutes < 60) return `${minutes} 分钟前`; const hours = Math.floor(minutes / 60); if (hours < 24) return `${hours} 小时前`; const days = Math.floor(hours / 24); return days < 30 ? `${days} 天前` : new Date(then).toLocaleDateString("zh-CN"); } export function OmniCreatePage({ modelConfigs, navigate, onNotify, }: { modelConfigs?: ModelConfig[]; navigate: NavigateFn; onNotify?: (type: "success" | "error" | "info", text: string) => void; }) { const [outputMode, setOutputMode] = useState("video"); const [selectedCase, setSelectedCase] = useState(null); const [prompt, setPrompt] = useState(""); const [sessionUploads, setSessionUploads] = useState([]); const [uploading, setUploading] = useState(false); const [uploadProgress, setUploadProgress] = useState<{ current: number; total: number } | null>(null); const [model, setModel] = useState("Seedance 2.5"); const [resolution, setResolution] = useState("1080p"); const [ratio, setRatio] = useState("9:16"); const [duration, setDuration] = useState("智能时长"); const [uploadMenuOpen, setUploadMenuOpen] = useState(false); const [mentionMenuOpen, setMentionMenuOpen] = useState(false); const [mentionTab, setMentionTab] = useState("asset"); const [mentionLoading, setMentionLoading] = useState(false); const [mentionResults, setMentionResults] = useState([]); const [typeLabels, setTypeLabels] = useState>({}); const [pendingRefs, setPendingRefs] = useState([]); const [previewCase, setPreviewCase] = useState(null); const fileInputRef = useRef(null); const toolsRef = useRef(null); const [starting, setStarting] = useState(false); useEffect(() => { if (outputMode === "image") { const img = findCatalogModel(modelConfigs, "", "image"); setModel(img ? (img.display_name || img.name) : "Seedream5.0"); setResolution("模型默认"); setRatio("1:1"); setDuration("1 张"); } else { const vid = findCatalogModel(modelConfigs, "", "video"); const label = vid ? (vid.display_name || vid.name) : "Seedance 2.5"; const resList = modelResolutions(vid); setModel(label); setResolution(resList.includes("720p") ? "720p" : (resList[0] || "1080p")); setRatio("9:16"); setDuration("智能时长"); } setSelectedCase((prev) => (prev && prev.mode !== outputMode ? null : prev)); }, [outputMode, modelConfigs]); useEffect(() => { const onDown = (event: MouseEvent) => { const target = event.target as Node; if (toolsRef.current?.contains(target)) return; setUploadMenuOpen(false); setMentionMenuOpen(false); }; document.addEventListener("mousedown", onDown); return () => document.removeEventListener("mousedown", onDown); }, []); const presets = useMemo(() => { return outputMode === "video" ? VIDEO_PRESETS : IMAGE_PRESETS; }, [outputMode]); const applyPreset = (preset: PresetItem) => { setSelectedCase(preset); // 预设只作为后台的创作方向。无论图片还是视频,都不把内部生成指令塞进用户输入区。 setPreviewCase(null); onNotify?.("info", `已选择预设:${preset.name}`); }; const openMentions = async (query = "", type: CreationRef["type"] = mentionTab) => { setMentionTab(type); setMentionMenuOpen(true); setUploadMenuOpen(false); setMentionLoading(true); setMentionResults([]); try { const res = await api.searchMentions({ q: query, types: [type], limit: 12 }); setMentionResults(mergeSessionUploads(res.results, sessionUploads, query, type)); setTypeLabels(res.type_labels); } catch (error) { onNotify?.("error", (error as Error).message); } finally { setMentionLoading(false); } }; const insertMention = (ref: CreationRef) => { if (pendingRefs.some((r) => r.id === ref.id)) { setMentionMenuOpen(false); return; } if (pendingRefs.length >= MENTION_REF_LIMIT) { onNotify?.("info", `最多引用 ${MENTION_REF_LIMIT} 个`); setMentionMenuOpen(false); return; } setPendingRefs((prev) => [...prev, ref]); setPrompt((prev) => prev.replace(/@\S*$/, "").replace(/\s+$/, " ")); setMentionMenuOpen(false); }; const handleFileChange = async (event: React.ChangeEvent) => { const files = Array.from(event.target.files || []); event.target.value = ""; setUploadMenuOpen(false); if (!files.length) return; const images = files.filter((file) => file.type.startsWith("image/")); if (images.length !== files.length) { onNotify?.("info", "全能创作仅支持上传图片"); } if (!images.length) return; setUploading(true); setUploadProgress({ current: 1, total: images.length }); try { for (const [index, file] of images.entries()) { setUploadProgress({ current: index + 1, total: images.length }); const form = new FormData(); form.append("file", file); const data = await api.uploadFreeVideoRef(form); const ref: CreationRef = { type: "asset", id: data.asset_id, name: data.name || file.name, cover: data.thumb_url || data.url, }; setSessionUploads((prev) => (prev.some((item) => item.id === ref.id) ? prev : [...prev, ref])); setPendingRefs((prev) => { if (prev.some((item) => item.id === ref.id) || prev.length >= MENTION_REF_LIMIT) return prev; return [...prev, ref]; }); } } catch (error) { onNotify?.("error", (error as Error).message); } finally { setUploading(false); setUploadProgress(null); } }; return (
YINGQING CREATIVE AGENT

和全能创作聊聊你的想法

选择一种创作方法,或直接描述你想生成的画面。