import React, { useState, useEffect } from 'react'; import { Sparkles, Sliders, RefreshCw, Send, AlertCircle, ChevronDown, ChevronUp } from 'lucide-react'; import MediaUploader from './MediaUploader'; import { RESOLUTIONS, DURATIONS, STEPS_BY_TURBO, SEED_OPTIONS, DEFAULT_RES, DEFAULT_DUR } from '../constants'; import { createJob } from '../api'; import { useToast } from './Toast'; export default function CreateForm({ initialValues, onCreated }) { const { addToast } = useToast(); const [tab, setTab] = useState('text'); // 'text' | 'fl2va' | 'ref2va' const [prompt, setPrompt] = useState(''); const [resValue, setResValue] = useState(DEFAULT_RES); const [durValue, setDurValue] = useState(DEFAULT_DUR); const [turbo, setTurbo] = useState(false); const [steps, setSteps] = useState('20'); const [seed, setSeed] = useState('0'); const [cfgScale, setCfgScale] = useState(''); const [negativePrompt, setNegativePrompt] = useState(''); const [showAdvanced, setShowAdvanced] = useState(false); const [submitting, setSubmitting] = useState(false); const [err, setErr] = useState(''); // 媒体状态 const [firstFrame, setFirstFrame] = useState(null); const [lastFrame, setLastFrame] = useState(null); const [refImages, setRefImages] = useState([]); const [refVideos, setRefVideos] = useState([]); const [refAudios, setRefAudios] = useState([]); // 如果从详情页点了“重跑”,注入初始参数 useEffect(() => { if (!initialValues) return; const req = initialValues; if (req.first_frame_url || req.last_frame_url) { setTab('fl2va'); setFirstFrame(req.first_frame_url || null); setLastFrame(req.last_frame_url || null); } else if ((req.reference_image_urls || []).length || (req.reference_video_urls || []).length || (req.reference_audio_urls || []).length) { setTab('ref2va'); setRefImages(req.reference_image_urls || []); setRefVideos(req.reference_video_urls || []); setRefAudios(req.reference_audio_urls || []); } else { setTab('text'); } setPrompt(req.prompt || ''); const matchedRes = RESOLUTIONS.find((r) => r.width === req.width && r.height === req.height); if (matchedRes) setResValue(matchedRes.value); const matchedDur = DURATIONS.find((d) => d.frames === req.frames); if (matchedDur) setDurValue(matchedDur.value); const isTurbo = req.turbo === false ? false : true; setTurbo(isTurbo); if (req.steps != null) setSteps(String(req.steps)); if (req.seed != null) setSeed(String(req.seed)); if (req.cfg_scale != null) setCfgScale(String(req.cfg_scale)); if (req.negative_prompt) setNegativePrompt(req.negative_prompt); }, [initialValues]); // 分辨率对应的时长合法性校验 const currentRes = RESOLUTIONS.find((r) => r.value === resValue) || RESOLUTIONS[0]; const allowedDurations = DURATIONS.filter((d) => d.frames <= currentRes.maxFrames); const droppedDurations = DURATIONS.filter((d) => d.frames > currentRes.maxFrames); useEffect(() => { if (!allowedDurations.some((d) => d.value === durValue)) { setDurValue(allowedDurations[0]?.value || DEFAULT_DUR); } }, [resValue]); // Turbo 切换自动联动步数 const stepOptions = STEPS_BY_TURBO[turbo]; useEffect(() => { if (!stepOptions.some((s) => s.value === steps)) { setSteps(stepOptions[0].value); } }, [turbo]); const handleClear = () => { if (!confirm('确定清空当前表单所有内容?')) return; setPrompt(''); setResValue(DEFAULT_RES); setDurValue(DEFAULT_DUR); setTurbo(false); setSteps('20'); setSeed('0'); setCfgScale(''); setNegativePrompt(''); setFirstFrame(null); setLastFrame(null); setRefImages([]); setRefVideos([]); setRefAudios([]); setErr(''); addToast('已恢复默认配置', 'info'); }; const handleSubmit = async (e) => { e.preventDefault(); setErr(''); if (!prompt.trim()) { setErr('请填写 Prompt 提示词'); return; } const durObj = DURATIONS.find((d) => d.value === durValue); const body = { prompt: prompt.trim(), width: currentRes.width, height: currentRes.height, frames: durObj ? durObj.frames : 243, turbo, steps: parseInt(steps, 10), seed: seed === 'random' ? Math.floor(Math.random() * 2 ** 31) : parseInt(seed, 10), }; if (cfgScale.trim() !== '') body.cfg_scale = parseFloat(cfgScale); if (negativePrompt.trim() !== '') body.negative_prompt = negativePrompt.trim(); if (tab === 'fl2va') { if (!firstFrame) { setErr('首尾帧模式下必须上传【首帧图】'); return; } body.first_frame_url = firstFrame; if (lastFrame) body.last_frame_url = lastFrame; } else if (tab === 'ref2va') { if (refImages.length === 0 && refVideos.length === 0 && refAudios.length === 0) { setErr('参考模式下至少需要上传 1 张参考图、1 段参考视频或 1 个音频'); return; } body.reference_image_urls = refImages; body.reference_video_urls = refVideos; body.reference_audio_urls = refAudios; } setSubmitting(true); try { const job = await createJob(body); addToast(`任务提交成功 (#${job.id.substring(0, 8)})`, 'success'); onCreated(job.id); } catch (e) { setErr('提交失败: ' + e.message); addToast('提交失败: ' + e.message, 'error'); } finally { setSubmitting(false); } }; return (
{/* 顶部标签切换 */}
{[ { key: 'text', label: '文生视频', desc: 'Text to Video' }, { key: 'fl2va', label: '首尾帧生视频', desc: 'FL2VA' }, { key: 'ref2va', label: '多参考图生视频', desc: 'Ref2VA' }, ].map((t) => ( ))}
{/* Prompt 输入 */}
{prompt.length} 字