优化视频创作
This commit is contained in:
@@ -274,14 +274,14 @@ aside.sidebar {
|
||||
.brand { display: flex; align-items: center; justify-content: center; min-width: 0; color: var(--accent-black); }
|
||||
.brand-clip {
|
||||
display: block;
|
||||
width: 112px;
|
||||
width: 132px;
|
||||
height: 90px;
|
||||
overflow: hidden;
|
||||
transition: width var(--t-base), height var(--t-base);
|
||||
}
|
||||
.brand-logo {
|
||||
display: block;
|
||||
width: 112px;
|
||||
width: 132px;
|
||||
height: 90px;
|
||||
max-width: none;
|
||||
margin: 0;
|
||||
|
||||
@@ -790,47 +790,9 @@
|
||||
transform: translateY(7px);
|
||||
}
|
||||
|
||||
.omni-case-head {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: flex-start;
|
||||
gap: 20px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.omni-case-head-label {
|
||||
flex: 0 0 auto;
|
||||
color: #252b35;
|
||||
font-size: 17px;
|
||||
font-weight: 750;
|
||||
line-height: 30px;
|
||||
}
|
||||
|
||||
.omni-case-filters {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.omni-case-filters button {
|
||||
height: 30px;
|
||||
padding: 0 12px;
|
||||
border-radius: 8px;
|
||||
color: #707681;
|
||||
background: transparent;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.omni-case-filters button.active {
|
||||
color: #fff;
|
||||
background: var(--black);
|
||||
}
|
||||
|
||||
.omni-case-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
@@ -840,24 +802,24 @@
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.omni-image-preset-intro {
|
||||
.omni-preset-intro {
|
||||
margin: 0 0 14px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.omni-image-preset-intro strong,
|
||||
.omni-image-preset-intro span {
|
||||
.omni-preset-intro strong,
|
||||
.omni-preset-intro span {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.omni-image-preset-intro strong {
|
||||
.omni-preset-intro strong {
|
||||
color: var(--accent-black);
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.omni-image-preset-intro span {
|
||||
.omni-preset-intro span {
|
||||
margin-top: 4px;
|
||||
color: var(--black-alpha-56);
|
||||
font-size: 13px;
|
||||
@@ -899,6 +861,12 @@
|
||||
background: #edf0f4;
|
||||
}
|
||||
|
||||
/* 视频样片本身是竖版,按竖构图展示,避免横向裁切掉人物和商品。 */
|
||||
.omni-case-card[data-mode="video"] .omni-case-visual {
|
||||
height: auto;
|
||||
aspect-ratio: 9 / 15;
|
||||
}
|
||||
|
||||
.omni-case-visual img,
|
||||
.omni-case-visual video {
|
||||
width: 100%;
|
||||
|
||||
@@ -2250,23 +2250,25 @@
|
||||
.omni-gate-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.omni-gate-actions button {
|
||||
padding: 8px 14px;
|
||||
border: 1px solid rgba(34, 42, 54, .12);
|
||||
border-radius: 999px;
|
||||
background: #fff;
|
||||
color: #575d66;
|
||||
font-size: 13px;
|
||||
min-height: 30px;
|
||||
padding: 0 10px;
|
||||
border: 1px solid var(--border-faint);
|
||||
border-radius: var(--r-md);
|
||||
background: var(--surface);
|
||||
color: var(--accent-black);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.omni-gate-actions button.primary {
|
||||
border-color: var(--klein);
|
||||
background: var(--klein);
|
||||
color: #fff;
|
||||
border-color: var(--heat-20);
|
||||
background: var(--heat-12);
|
||||
color: var(--heat);
|
||||
}
|
||||
|
||||
.omni-gate-actions button:disabled {
|
||||
|
||||
@@ -35,18 +35,16 @@ type PresetItem = {
|
||||
desc?: string;
|
||||
starter?: string;
|
||||
cover: string;
|
||||
previewVideo?: string;
|
||||
};
|
||||
|
||||
const VIDEO_PRESETS: PresetItem[] = [
|
||||
{ name: "真实使用演示", category: "commerce", mode: "video", title: "真实使用演示", desc: "从商品真实用途出发,拍清使用过程与卖点证据,避免不合理的夸张演示。", starter: "基于商品的真实用途创作一条可信的使用演示:拍清使用步骤、产品状态和核心卖点,不夸张、不出现不合理的使用场景。", cover: "/assets/prototype/video-oneclick-film-v3.png" },
|
||||
{ name: "剧情反转带货", category: "story", mode: "video", title: "剧情反转带货", desc: "用人物冲突与意外反转建立记忆点,商品承担剧情中的关键作用。", starter: "创作一条有前后反转的剧情带货视频,让商品自然成为解决问题的关键。", cover: "/assets/prototype/photo-1529139574466-a303027c1d8b.jpg" },
|
||||
{ name: "商品拟人广告", category: "commerce", mode: "video", title: "商品拟人广告", desc: "让商品成为故事主角,通过个性、动作和情绪完成趣味表达。", starter: "把商品塑造成有性格的拟人角色,创作一条轻快、有趣的短广告。", cover: "/assets/prototype/photo-1608248543803-ba4f8c70ae0b.jpg" },
|
||||
{ name: "达人口播种草", category: "speaker", mode: "video", title: "达人口播种草", desc: "用真实体验与生活化表达建立信任,适合电商和本地生活内容。", starter: "创作一条真实自然的达人口播种草视频,重点讲清使用场景和核心卖点。", cover: "/assets/prototype/photo-1494790108377-be9c29b29330.jpg" },
|
||||
{ name: "商品图一键成片", category: "commerce", mode: "video", title: "商品图一键成片", desc: "从商品参考图建立镜头语言,自动补齐场景、动作与转场。", starter: "根据我上传的商品图片生成一条节奏清晰的电商短视频。", cover: "/assets/prototype/photo-1556228578-8c89e6adf883.jpg" },
|
||||
{ name: "鱼眼换装", category: "visual", mode: "video", title: "鱼眼换装", desc: "强调近距离透视与连续变装节奏,适合服饰和人物视觉内容。", starter: "创作一条鱼眼镜头风格的连续换装视频,人物和服装需要保持稳定。", cover: "/assets/prototype/photo-1483985988355-763728e1935b.jpg" },
|
||||
{ name: "点触换款", category: "visual", mode: "video", title: "点触换款", desc: "用统一构图和动作触发商品变化,快速展示多个款式或 SKU。", starter: "创作一条通过点击动作连续切换不同商品款式的短视频。", cover: "/assets/prototype/unbranded-running-shoe-remix.png" },
|
||||
{ name: "探店漫游", category: "story", mode: "video", title: "探店漫游", desc: "以空间动线串联门店环境、服务细节与主推项目。", starter: "根据门店素材创作一条有路线感和空间氛围的探店漫游视频。", cover: "/assets/prototype/photo-1497366754035-f200968a6e72.jpg" },
|
||||
{ name: "品牌质感大片", category: "visual", mode: "video", title: "品牌质感大片", desc: "通过统一的光影、材质与镜头节奏建立更强的品牌识别。", starter: "创作一条强调光影、材质和品牌气质的高级感商品短片。", cover: "/assets/prototype/video-free-cinematic.png" },
|
||||
{ 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[] = [
|
||||
@@ -64,19 +62,6 @@ const IMAGE_PRESETS: PresetItem[] = [
|
||||
{ name: "新中式", category: "style", mode: "image", title: "新中式", cover: "/assets/image-presets/neo-chinese.png" },
|
||||
];
|
||||
|
||||
const VIDEO_FILTERS = [
|
||||
{ key: "all", label: "全部" },
|
||||
{ key: "story", label: "剧情" },
|
||||
{ key: "commerce", label: "电商" },
|
||||
{ key: "speaker", label: "口播" },
|
||||
{ key: "visual", label: "视觉" },
|
||||
];
|
||||
|
||||
const IMAGE_FILTERS = [
|
||||
{ key: "all", label: "全部" },
|
||||
{ key: "style", label: "风格" },
|
||||
];
|
||||
|
||||
const MENTION_REF_LIMIT = 5;
|
||||
|
||||
const MENTION_TABS: Array<{ type: CreationRef["type"]; label: string; Icon: typeof ImageIcon }> = [
|
||||
@@ -134,7 +119,6 @@ export function OmniCreatePage({
|
||||
const [typeLabels, setTypeLabels] = useState<Record<string, string>>({});
|
||||
const [pendingRefs, setPendingRefs] = useState<CreationRef[]>([]);
|
||||
const [previewCase, setPreviewCase] = useState<PresetItem | null>(null);
|
||||
const [activeCategory, setActiveCategory] = useState("all");
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const toolsRef = useRef<HTMLDivElement>(null);
|
||||
const [starting, setStarting] = useState(false);
|
||||
@@ -155,7 +139,6 @@ export function OmniCreatePage({
|
||||
setRatio("9:16");
|
||||
setDuration("智能时长");
|
||||
}
|
||||
setActiveCategory("all");
|
||||
setSelectedCase((prev) => (prev && prev.mode !== outputMode ? null : prev));
|
||||
}, [outputMode, modelConfigs]);
|
||||
|
||||
@@ -170,17 +153,13 @@ export function OmniCreatePage({
|
||||
return () => document.removeEventListener("mousedown", onDown);
|
||||
}, []);
|
||||
|
||||
const filters = outputMode === "video" ? VIDEO_FILTERS : IMAGE_FILTERS;
|
||||
const presets = useMemo(() => {
|
||||
const list = outputMode === "video" ? VIDEO_PRESETS : IMAGE_PRESETS;
|
||||
if (activeCategory === "all") return list;
|
||||
return list.filter((item) => item.category === activeCategory);
|
||||
}, [outputMode, activeCategory]);
|
||||
return outputMode === "video" ? VIDEO_PRESETS : IMAGE_PRESETS;
|
||||
}, [outputMode]);
|
||||
|
||||
const applyPreset = (preset: PresetItem) => {
|
||||
setSelectedCase(preset);
|
||||
// 图片预设只作为隐藏的生成风格,用户输入区保留自己的创作描述,不把内部 Prompt 展示出来。
|
||||
if (preset.mode !== "image" && preset.starter) setPrompt(preset.starter);
|
||||
// 预设只作为后台的创作方向。无论图片还是视频,都不把内部生成指令塞进用户输入区。
|
||||
setPreviewCase(null);
|
||||
onNotify?.("info", `已选择预设:${preset.name}`);
|
||||
};
|
||||
@@ -460,32 +439,12 @@ export function OmniCreatePage({
|
||||
|
||||
<section
|
||||
className={`omni-case-library${outputMode === "image" ? " is-image-presets" : ""}`}
|
||||
aria-labelledby={outputMode === "video" ? "omniCaseTitle" : undefined}
|
||||
aria-label={outputMode === "image" ? "图片风格预设" : undefined}
|
||||
aria-label={outputMode === "image" ? "图片风格预设" : "视频创作预设"}
|
||||
>
|
||||
{outputMode === "video" ? (
|
||||
<div className="omni-case-head">
|
||||
<strong className="omni-case-head-label" id="omniCaseTitle">预设分类</strong>
|
||||
<div className="omni-case-filters" role="tablist" aria-label="预设筛选">
|
||||
{filters.map((filter) => (
|
||||
<button
|
||||
type="button"
|
||||
key={filter.key}
|
||||
className={activeCategory === filter.key ? "active" : ""}
|
||||
onClick={() => setActiveCategory(filter.key)}
|
||||
>
|
||||
{filter.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
{outputMode === "image" ? (
|
||||
<div className="omni-image-preset-intro">
|
||||
<strong>从一个风格开始</strong>
|
||||
<span>可选,也可以直接描述你想要的画面。</span>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="omni-preset-intro">
|
||||
<strong>{outputMode === "video" ? "从一个视频方向开始" : "从一个风格开始"}</strong>
|
||||
<span>{outputMode === "video" ? "可选,也可以直接描述你想拍的视频。" : "可选,也可以直接描述你想要的画面。"}</span>
|
||||
</div>
|
||||
<div className="omni-case-grid">
|
||||
{presets.map((card) => (
|
||||
<button
|
||||
@@ -493,16 +452,39 @@ export function OmniCreatePage({
|
||||
className={`omni-case-card${selectedCase?.name === card.name ? " active" : ""}`}
|
||||
data-mode={card.mode}
|
||||
key={card.name}
|
||||
onMouseEnter={(event) => {
|
||||
const video = event.currentTarget.querySelector<HTMLVideoElement>("video");
|
||||
void video?.play().catch(() => undefined);
|
||||
}}
|
||||
onMouseLeave={(event) => {
|
||||
const video = event.currentTarget.querySelector<HTMLVideoElement>("video");
|
||||
if (!video) return;
|
||||
video.pause();
|
||||
video.currentTime = 0;
|
||||
}}
|
||||
onFocus={(event) => {
|
||||
const video = event.currentTarget.querySelector<HTMLVideoElement>("video");
|
||||
void video?.play().catch(() => undefined);
|
||||
}}
|
||||
onBlur={(event) => {
|
||||
const video = event.currentTarget.querySelector<HTMLVideoElement>("video");
|
||||
if (!video) return;
|
||||
video.pause();
|
||||
video.currentTime = 0;
|
||||
}}
|
||||
onClick={() => {
|
||||
if (card.mode === "image") applyPreset(card);
|
||||
else setPreviewCase(card);
|
||||
}}
|
||||
>
|
||||
<span className="omni-case-visual">
|
||||
<img src={card.cover} alt="" />
|
||||
<span className="omni-case-play">
|
||||
{card.mode === "video" ? <Play className="lucide-play" /> : <ImageIcon />}
|
||||
</span>
|
||||
{card.previewVideo ? (
|
||||
<video muted loop playsInline preload="metadata" poster={card.cover} aria-label={`${card.title} 样片预览`}>
|
||||
<source src={card.previewVideo} type="video/mp4" />
|
||||
</video>
|
||||
) : (
|
||||
<img src={card.cover} alt="" />
|
||||
)}
|
||||
</span>
|
||||
<span className="omni-case-copy">
|
||||
<strong>{card.title}</strong>
|
||||
@@ -530,8 +512,15 @@ export function OmniCreatePage({
|
||||
<X />
|
||||
</button>
|
||||
<div className="omni-preset-media-frame">
|
||||
<img src={previewCase.cover} alt="预设示例" />
|
||||
{previewCase.mode === "video" && (
|
||||
{previewCase.previewVideo ? (
|
||||
<video controls playsInline preload="metadata" poster={previewCase.cover}>
|
||||
<source src={previewCase.previewVideo} type="video/mp4" />
|
||||
当前浏览器不支持播放该示例视频。
|
||||
</video>
|
||||
) : (
|
||||
<img src={previewCase.cover} alt="预设示例" />
|
||||
)}
|
||||
{previewCase.mode === "video" && !previewCase.previewVideo && (
|
||||
<span className="omni-preset-video-mark">
|
||||
<Play />
|
||||
</span>
|
||||
|
||||
@@ -399,17 +399,6 @@ function collectSessionAssets(
|
||||
});
|
||||
}
|
||||
|
||||
if (message.kind === "prompt_file") {
|
||||
const title = String(message.payload?.title || "视频生成Prompt.md");
|
||||
const body = String(message.payload?.body || "");
|
||||
push({
|
||||
key: `prompt:${message.id || title}`,
|
||||
kind: "document",
|
||||
title,
|
||||
promptTitle: title,
|
||||
promptBody: body || "暂无内容",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
@@ -512,7 +501,7 @@ function PlanCard({ payload }: { payload: Record<string, unknown> }) {
|
||||
<section className="omni-video-plan-card">
|
||||
<header className="omni-video-plan-head">
|
||||
<strong>视频最终方案</strong>
|
||||
<span>请确认后再出 Prompt</span>
|
||||
<span>确认后进入出片参数核对</span>
|
||||
</header>
|
||||
<section className="omni-plan-section">
|
||||
<strong>卖点做减法</strong>
|
||||
@@ -555,9 +544,9 @@ function PlanCard({ payload }: { payload: Record<string, unknown> }) {
|
||||
</span>
|
||||
) : null}
|
||||
<span className="omni-plan-final-input">
|
||||
最终输入:
|
||||
出片准备:
|
||||
<b>
|
||||
Prompt.md + <em className="omni-plan-point-body">{String(payload.ref_count ?? 0)}</em> 组参考素材
|
||||
已整合生成参数与 <em className="omni-plan-point-body">{String(payload.ref_count ?? 0)}</em> 组参考素材
|
||||
</b>
|
||||
</span>
|
||||
</div>
|
||||
@@ -565,30 +554,21 @@ function PlanCard({ payload }: { payload: Record<string, unknown> }) {
|
||||
);
|
||||
}
|
||||
|
||||
function PromptFileCard({
|
||||
payload,
|
||||
onView,
|
||||
}: {
|
||||
payload: Record<string, unknown>;
|
||||
onView: () => void;
|
||||
}) {
|
||||
function PromptFileCard({ payload }: { payload: Record<string, unknown> }) {
|
||||
return (
|
||||
<section className="omni-prompt-file-card">
|
||||
<span className="omni-prompt-file-icon">
|
||||
<FileText />
|
||||
</span>
|
||||
<div className="omni-prompt-file-copy">
|
||||
<strong>{String(payload.title || "视频生成Prompt.md")}</strong>
|
||||
<small>已整合策略、镜头、口播、节奏与 {String(payload.ref_count ?? 0)} 组参考素材</small>
|
||||
<strong>出片指令已准备好</strong>
|
||||
<small>已根据方案、参数和 {String(payload.ref_count ?? 0)} 组参考素材在后台整理完成</small>
|
||||
</div>
|
||||
<button type="button" onClick={onView}>
|
||||
查看 Prompt
|
||||
</button>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
/** 视频 5 步闸门:策略/方案/Prompt 下方的「按这个继续 / 我想改」。
|
||||
/** 视频阶段闸门:策略/方案下方的「按这个继续 / 我想改」。
|
||||
* 「我想改」在卡片内展开反馈输入,不再依赖底部 composer。 */
|
||||
function StepConfirmCard({
|
||||
message,
|
||||
@@ -606,14 +586,14 @@ function StepConfirmCard({
|
||||
const answers = (message.payload.answers as Record<string, string> | undefined) || {};
|
||||
const action = String(answers.step_action || "");
|
||||
const stepLabel =
|
||||
step === "strategy" ? "创作策略" : step === "plan" ? "视频方案" : step === "prompt" ? "出片 Prompt" : "这一步";
|
||||
step === "strategy" ? "创作策略" : step === "plan" ? "视频方案" : step === "prompt" ? "出片指令" : "这一步";
|
||||
const placeholder =
|
||||
step === "strategy"
|
||||
? "说说你想怎么改策略…"
|
||||
: step === "plan"
|
||||
? "说说你想怎么改方案…"
|
||||
: step === "prompt"
|
||||
? "说说你想怎么改 Prompt…"
|
||||
? "说说你想怎么改出片细节…"
|
||||
: "说说你想怎么改…";
|
||||
const [revising, setRevising] = useState(false);
|
||||
const [feedback, setFeedback] = useState("");
|
||||
@@ -781,21 +761,6 @@ function ElicitCard({
|
||||
);
|
||||
}
|
||||
|
||||
if (interaction === "chat") {
|
||||
return (
|
||||
<div className="omni-chat-row agent">
|
||||
<span className="omni-chat-avatar">
|
||||
<Sparkles />
|
||||
</span>
|
||||
<div className="omni-chat-bubble">
|
||||
<p className="omni-chat-text">
|
||||
{message.text || fields[0]?.label || "这项你想怎么定?"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const toggleMulti = (key: string, value: string) => {
|
||||
setAnswers((prev) => {
|
||||
const current = Array.isArray(prev[key]) ? (prev[key] as string[]) : [];
|
||||
@@ -828,12 +793,12 @@ function ElicitCard({
|
||||
<div className="omni-chat-bubble omni-gate-bubble">
|
||||
<p className="omni-chat-text">{message.text || gateLabel}</p>
|
||||
{!submitted ? (
|
||||
<div className="omni-gate-actions">
|
||||
{(gateField?.options || []).map((option) => (
|
||||
<div className="omni-gate-actions" aria-label="商品选择操作">
|
||||
{(gateField?.options || []).map((option, index) => (
|
||||
<button
|
||||
type="button"
|
||||
key={option.value}
|
||||
className={option.value === "send" ? "primary" : ""}
|
||||
className={index === 0 ? "primary" : ""}
|
||||
disabled={disabled}
|
||||
onClick={() => {
|
||||
if (!gateField?.key) return;
|
||||
@@ -854,6 +819,32 @@ function ElicitCard({
|
||||
);
|
||||
}
|
||||
|
||||
if (interaction === "chat") {
|
||||
const question = message.text || fields[0]?.label || "这项你想怎么定?";
|
||||
return (
|
||||
<div className="omni-chat-row agent">
|
||||
<span className="omni-chat-avatar">
|
||||
<Sparkles />
|
||||
</span>
|
||||
<div className="omni-chat-stack">
|
||||
<div className="omni-chat-bubble">
|
||||
<p className="omni-chat-text">{question}</p>
|
||||
</div>
|
||||
{!submitted ? (
|
||||
<div className="omni-reply-guide" aria-label="下一步引导">
|
||||
<p><span>下一步</span>直接在下方输入你的想法,回答这一项就行。</p>
|
||||
<div className="omni-reply-guide-options">
|
||||
<button type="button" className="primary" onClick={() => document.getElementById("omniSessionPrompt")?.focus()}>
|
||||
现在填写
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="omni-elicit-slot">
|
||||
<section className={`omni-elicit-card${submitted ? " is-submitted" : ""}`}>
|
||||
@@ -1051,11 +1042,15 @@ function ConfirmCard({
|
||||
);
|
||||
}
|
||||
|
||||
function ResultCard({ payload }: { payload: Record<string, unknown> }) {
|
||||
function ResultCard({
|
||||
payload,
|
||||
}: {
|
||||
payload: Record<string, unknown>;
|
||||
}) {
|
||||
const assets = (payload.assets as Array<Record<string, string>> | undefined) || [];
|
||||
const first = assets[0] || {};
|
||||
const meta = [payload.model, payload.resolution, payload.ratio].filter(Boolean).join(" · ");
|
||||
const isVideo = first.type === "video";
|
||||
const isGeneratedVideo = first.type === "video";
|
||||
const [preview, setPreview] = useState<{ src: string; kind: "image" | "video"; name: string } | null>(null);
|
||||
|
||||
return (
|
||||
@@ -1100,7 +1095,7 @@ function ResultCard({ payload }: { payload: Record<string, unknown> }) {
|
||||
</div>
|
||||
<div className="omni-result-info">
|
||||
<div>
|
||||
<strong>{isVideo ? "视频已生成" : assets.length > 1 ? `已生成 ${assets.length} 张图` : "图片已生成"}</strong>
|
||||
<strong>{isGeneratedVideo ? "视频已生成" : assets.length > 1 ? `已生成 ${assets.length} 张图` : "图片已生成"}</strong>
|
||||
<small>{meta}</small>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2178,10 +2173,6 @@ export function OmniSessionPage({
|
||||
<PromptFileCard
|
||||
key={message.clientKey || message.id}
|
||||
payload={message.payload}
|
||||
onView={() => setPromptView({
|
||||
title: String(message.payload.title || "视频生成Prompt.md"),
|
||||
body: String(message.payload.body || "暂无内容"),
|
||||
})}
|
||||
/>
|
||||
);
|
||||
case "confirm": {
|
||||
@@ -2213,7 +2204,12 @@ export function OmniSessionPage({
|
||||
case "generating":
|
||||
return <ProcessCard key={message.clientKey || message.id} payload={message.payload} />;
|
||||
case "result":
|
||||
return <ResultCard key={message.clientKey || message.id} payload={message.payload} />;
|
||||
return (
|
||||
<ResultCard
|
||||
key={message.clientKey || message.id}
|
||||
payload={message.payload}
|
||||
/>
|
||||
);
|
||||
default: {
|
||||
const refs = message.refs || [];
|
||||
const body = stripMentionText(message.text, refs);
|
||||
|
||||
Reference in New Issue
Block a user