feat: add AirShelf core implementation
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
import { useState } from "react";
|
||||
import { Copy, Play } from "lucide-react";
|
||||
import type { Project } from "../types";
|
||||
import { FragmentStageStep } from "../components/pipeline-stage";
|
||||
import { stageOrder, statusPill } from "./stage-config";
|
||||
|
||||
export function PipelinePage({
|
||||
project,
|
||||
loading,
|
||||
onRefresh,
|
||||
onGenerateScript,
|
||||
onAdoptScript,
|
||||
onGenerateBaseAsset,
|
||||
onGenerateStoryboard,
|
||||
onSkipStoryboard,
|
||||
onSubmitVideo,
|
||||
onPollVideo,
|
||||
onSubmitAllVideos,
|
||||
onPollAllVideos,
|
||||
onSubmitExport
|
||||
}: {
|
||||
project: Project;
|
||||
loading: boolean;
|
||||
onRefresh: () => void;
|
||||
onGenerateScript: (prompt: string) => void;
|
||||
onAdoptScript: (scriptId: string) => void;
|
||||
onGenerateBaseAsset: (kind: "product" | "person" | "scene", prompt: string) => void;
|
||||
onGenerateStoryboard: (prompt: string) => void;
|
||||
onSkipStoryboard: () => void;
|
||||
onSubmitVideo: (segmentId: string, prompt: string) => void;
|
||||
onPollVideo: (segmentId: string) => void;
|
||||
onSubmitAllVideos: (prompt: string) => void;
|
||||
onPollAllVideos: () => void;
|
||||
onSubmitExport: () => void;
|
||||
}) {
|
||||
const [activeStage, setActiveStage] = useState<string>(project.current_stage || "script");
|
||||
const [prompt, setPrompt] = useState("突出商品卖点,节奏紧凑,适合短视频投放");
|
||||
const [storyboardPrompt, setStoryboardPrompt] = useState("统一商品、人物、场景风格,生成可直接指导视频的分镜图");
|
||||
const [videoPrompt, setVideoPrompt] = useState("竖屏电商短视频,镜头稳定,商品露出清晰,节奏有转化感");
|
||||
const canExport = project.video_segments.length > 0 && project.video_segments.every((segment) => Boolean(segment.adopted_version));
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="proj-head"><div className="hstack" style={{ gap: 14 }}><div className="placeholder" style={{ width: 42, height: 54 }}><span className="ph-frame">9:16</span></div><div><div className="hstack"><h1>{project.name}</h1><span className={`pill ${statusPill(project.status)}`}><span className="dot" />{project.status}</span></div><div className="muted-2 mono" style={{ fontSize: 11.5, marginTop: 4 }}>// AI 全生 · 4 段 · 60s · 9:16</div></div></div><div className="hstack"><button className="btn btn-sm" type="button" onClick={onRefresh}>刷新</button><button className="btn btn-ghost btn-sm" type="button"><Copy size={12} />复制项目</button></div></div>
|
||||
<div className="stepper"><span className="corner-tr">+</span><span className="corner-bl">+</span>{stageOrder.map((stage, index) => { const projectIndex = stageOrder.indexOf(project.current_stage as never); return <FragmentStageStep key={stage} stage={stage} done={index < projectIndex} active={activeStage === stage} lineDone={index < projectIndex} isLast={index === stageOrder.length - 1} onClick={() => setActiveStage(stage)} />; })}</div>
|
||||
{activeStage === "script" && <section className="stage active"><div className="stage-script"><div className="pane chat-pane"><div className="pane-h"><div className="ai-avatar">AI</div><strong>脚本助手</strong><span className="muted-2 mono">· Doubao</span></div><div className="chat-input"><textarea className="textarea" value={prompt} onChange={(event) => setPrompt(event.target.value)} /><div className="hstack" style={{ marginTop: 8 }}><span className="spacer" /><button className="btn btn-primary" type="button" disabled={loading} onClick={() => onGenerateScript(prompt)}>生成脚本 ⌘↵</button></div></div></div><div className="pane shot-list"><div className="pane-h"><strong>脚本 / 读秒分镜</strong><span className="spacer" /><span className="muted-2 mono">{project.script_versions.length} versions</span></div>{project.script_versions.map((script) => <article className={`shot-card ${script.is_adopted ? "highlight" : ""}`} key={script.id}><div className="shot-row"><div className="shot-k">TXT</div><div className="shot-v">{script.content}</div></div><button className="btn btn-sm" type="button" onClick={() => onAdoptScript(script.id)}>采用</button></article>)}</div></div></section>}
|
||||
{activeStage === "base_assets" && <section className="stage active"><div className="asset-grid-2">{(["product", "person", "scene"] as const).map((kind) => <article className="asset-card-2" key={kind}><div className="placeholder thumb-2"><span className="ph-frame">{kind}</span></div><div className="body-2"><strong>{kind}</strong><div className="prompt-box">{project.name} {kind} 16:9 基础资产</div><button className="btn btn-sm" type="button" disabled={loading} onClick={() => onGenerateBaseAsset(kind, `${project.name} ${kind} 16:9 基础资产`)}>生成 {kind}</button></div></article>)}</div></section>}
|
||||
{activeStage === "storyboard" && <section className="stage active"><div className="stage-storyboard"><div className="sb-canvas">{[0, 1, 2, 3].map((index) => <div className="sb-row" key={index}><div className="sb-num">{String(index + 1).padStart(2, "0")}<span className="t">{index * 15}-{(index + 1) * 15}s</span></div><div className="sb-img"><div className="placeholder"><span className="ph-frame">16:9</span></div></div><div className="sb-text"><div className="meta">PROMPT</div><div className="dialog">等待脚本段落</div></div></div>)}</div><div className="sb-side"><div className="pane"><textarea className="textarea" value={storyboardPrompt} onChange={(event) => setStoryboardPrompt(event.target.value)} /><div className="hstack"><button className="btn btn-primary" type="button" disabled={loading} onClick={() => onGenerateStoryboard(storyboardPrompt)}>生成故事板</button><button className="btn" type="button" disabled={loading} onClick={onSkipStoryboard}>跳过故事板</button></div></div></div></div></section>}
|
||||
{activeStage === "video" && <section className="stage active"><div className="queue-bar"><span className="mono">[ VIDEO QUEUE ]</span><div className="bar-wrap"><span style={{ width: `${(project.video_segments.filter((s) => s.status === "succeeded").length / 4) * 100}%` }} /></div><span className="mono">{project.video_segments.filter((s) => s.status === "succeeded").length}/4</span></div><div className="field"><textarea className="textarea" value={videoPrompt} onChange={(event) => setVideoPrompt(event.target.value)} /></div><div className="hstack" style={{ marginBottom: 14 }}><button className="btn btn-primary" type="button" disabled={loading} onClick={() => onSubmitAllVideos(videoPrompt)}>生成完整 60s</button><button className="btn" type="button" disabled={loading} onClick={onPollAllVideos}>刷新全部</button></div><div className="video-grid">{project.video_segments.map((segment) => <article className="video-card" key={segment.id}><div className="placeholder video-thumb"><span className="ph-frame">SEG {segment.sort_order + 1}</span><div className="play"><div className="btn-play"><Play size={14} fill="currentColor" /></div></div></div><div className="body"><div className="hstack"><strong>{segment.target_duration_seconds}s</strong><span className="spacer" /><span className={`pill ${statusPill(segment.status)}`}><span className="dot" />{segment.status}</span></div><div className="hstack" style={{ marginTop: 8 }}><button className="btn btn-sm" type="button" disabled={loading || segment.status === "running"} onClick={() => onSubmitVideo(segment.id, `${videoPrompt} 第 ${segment.sort_order + 1} 段`)}>生成</button><button className="btn btn-sm" type="button" onClick={() => onPollVideo(segment.id)}>刷新</button></div></div></article>)}</div></section>}
|
||||
{activeStage === "export" && <section className="stage active"><div className="editor"><div className="editor-preview"><div className="canvas">9:16 / 1080×1920</div></div><div className="editor-props"><button className="btn btn-primary" type="button" disabled={loading || !canExport} onClick={onSubmitExport}>提交导出</button></div></div></section>}
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user