优化全能创作
Deploy dev / deploy (push) Successful in 44s

This commit is contained in:
Azmat@qq.com
2026-09-29 18:11:24 +08:00
parent c687ec4bbd
commit fa94051381
14 changed files with 669 additions and 183 deletions
@@ -89,7 +89,7 @@ function canCreateSegmentedVideo(config: ModelConfig | undefined, model: string)
// 31–60 秒总时长会拆成 ≤30 秒片段(长视频 >60 秒入口已临时关闭)。
// 只有 Seedance 2.5(或目录里声明了 30 秒能力的模型)可选这些总时长。
return modelDurations(config).some((seconds) => seconds >= 30)
|| /seedance\s*2\.5/i.test(model || "");
|| normalizeModelKey(model).includes("seedance25");
}
function durationLabelsForModel(config: ModelConfig | undefined, model: string, isVideo: boolean): string[] {
@@ -111,6 +111,7 @@ export function OmniParamBar({
resolution,
ratio,
duration,
fixedDuration = false,
catalogModels,
onModel,
onResolution,
@@ -123,6 +124,7 @@ export function OmniParamBar({
resolution: string;
ratio: string;
duration: string;
fixedDuration?: boolean;
catalogModels?: ModelConfig[];
onModel: (value: string) => void;
onResolution: (value: string) => void;
@@ -141,10 +143,11 @@ export function OmniParamBar({
[catalogModels, model, capability],
);
const models = withCurrent(
catalogModelLabels(catalogModels, capability, isVideo ? OMNI_VIDEO_MODELS : OMNI_IMAGE_MODELS),
model,
);
const availableModels = catalogModelLabels(catalogModels, capability, isVideo ? OMNI_VIDEO_MODELS : OMNI_IMAGE_MODELS);
const compatibleModels = fixedDuration && isVideo
? availableModels.filter((label) => canCreateSegmentedVideo(findCatalogModel(catalogModels, label, "video"), label))
: [];
const models = withCurrent(fixedDuration && isVideo ? compatibleModels : availableModels, model);
const allowedRes = isVideo ? modelResolutions(selected) : [];
const resolutions = withCurrent(allowedRes.length ? allowedRes : OMNI_RESOLUTIONS, resolution);
const durations = withCurrent(durationLabelsForModel(selected, model, isVideo), duration);
@@ -200,6 +203,7 @@ export function OmniParamBar({
if (nextRes.length && resolution && !nextRes.includes(resolution)) {
onResolution(nextRes.includes("720p") ? "720p" : nextRes[0]);
}
if (fixedDuration) return;
const nextDur = durationLabelsForModel(selected, model, true);
const seconds = Number(String(duration || "").replace(/\D/g, ""));
// 已选 >60 秒的旧会话:收回长视频入口后压回 60 秒。
@@ -215,6 +219,12 @@ export function OmniParamBar({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selected?.id, isVideo, model]);
useEffect(() => {
if (!fixedDuration || !isVideo || !compatibleModels.length) return;
if (!canCreateSegmentedVideo(selected, model)) onModel(compatibleModels[0]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [fixedDuration, isVideo, model, selected?.id, compatibleModels.join("|")]);
return (
<>
<label className="omni-parameter omni-parameter-model">
@@ -250,7 +260,7 @@ export function OmniParamBar({
options={toOptions(withCurrent(OMNI_RATIOS, ratio))}
/>
</label>
<div className={`omni-duration-control${isVideo ? "" : " is-image-count"}`} ref={wrapRef}>
{fixedDuration ? null : <div className={`omni-duration-control${isVideo ? "" : " is-image-count"}`} ref={wrapRef}>
<button
type="button"
className="omni-duration-trigger"
@@ -321,7 +331,7 @@ export function OmniParamBar({
document.body,
)
: null}
</div>
</div>}
</>
);
}
+5
View File
@@ -1249,6 +1249,11 @@ body.sidebar-collapsed .user::after { display: none; }
overflow: hidden;
animation: none;
}
.content:has(.omni-session-page) {
/* 对话页内部已按顶栏以下的视口高度排版;通用内容 padding 会让自动滚底遮住首条消息。 */
padding: 0;
animation: none;
}
@keyframes yz-page-enter {
from { opacity: 0; }
to { opacity: 1; }
+5
View File
@@ -0,0 +1,5 @@
/** 已保存的旧短剧会话保留原始预设值,界面统一显示新名称。 */
export function displayOmniPresetLabel(preset?: string | null): string {
if (preset === "剧情反转带货") return "短剧带货";
return preset || "自由创作";
}
+150 -78
View File
@@ -431,11 +431,40 @@
}
/* 横向角色设定图需要留出三视图和两处脸部细节,预览时不得裁掉边缘。 */
.omni-person-reference-flow > .omni-result-card.is-person-reference,
.omni-person-reference-flow > .omni-result-card.is-person-reference {
flex: 0 0 min(372px, 100%);
width: min(372px, 100%);
max-width: 372px;
}
/* 角色定妆图生成时占满整条流程区域;完成后恢复为可并排的大图卡。 */
.omni-result-card.omni-process-card.is-person-reference {
width: min(760px, calc(100% - 44px));
max-width: 760px;
height: 400px;
display: flex;
flex-direction: column;
}
.omni-person-reference-flow > .omni-process-card.is-person-reference {
flex-basis: 360px;
width: min(360px, 100%);
max-width: 360px;
flex: 1 0 100%;
width: 100%;
max-width: 100%;
}
.omni-process-card.is-person-reference .omni-result-media {
flex: 1;
min-height: 0;
}
.omni-process-card.is-person-reference .omni-result-tile {
height: 100%;
aspect-ratio: auto;
}
.omni-process-card.is-person-reference .omni-process-frame {
min-height: 0;
aspect-ratio: auto;
}
.omni-result-card.is-person-reference .omni-result-tile img {
@@ -1535,6 +1564,11 @@
margin-left: 0;
}
.omni-result-card.omni-process-card.is-person-reference {
width: 100%;
height: clamp(280px, 58vw, 400px);
}
.omni-strategy-grid,
.omni-plan-points,
.omni-plan-timeline,
@@ -2572,27 +2606,47 @@
height: 14px;
}
/* 发送中:不要文案;左侧柔和呼吸高亮,气泡本身不变形 */
.omni-chat-row.is-user .omni-chat-stack.is-pending {
padding-left: 6px;
}
.omni-chat-row.is-user .omni-chat-stack.is-pending::before {
content: "";
position: absolute;
left: 0;
top: 4px;
bottom: 4px;
width: 3px;
border-radius: 2px;
background: #5b6572;
animation: omniSendPulse 1.35s ease-in-out infinite;
pointer-events: none;
}
/* 发送状态只留在原消息气泡内,不额外占一行。 */
.omni-chat-row.is-user .omni-chat-stack.is-pending .omni-chat-bubble {
background: color-mix(in srgb, var(--surface) 84%, transparent);
border-color: var(--black-alpha-16);
flex-direction: row;
align-items: center;
background: var(--black-alpha-4);
border-color: var(--black-alpha-12);
animation: omniPendingBubble 1.5s ease-in-out infinite;
}
.omni-send-dots {
display: inline-flex;
align-items: center;
flex: 0 0 auto;
gap: 3px;
}
.omni-send-dots i {
width: 4px;
height: 4px;
border-radius: 50%;
background: currentColor;
opacity: .35;
animation: omniSendDotPulse 1.1s ease-in-out infinite;
}
.omni-send-dots i:nth-child(2) { animation-delay: .18s; }
.omni-send-dots i:nth-child(3) { animation-delay: .36s; }
@keyframes omniSendDotPulse {
0%, 70%, 100% { opacity: .35; transform: translateY(0); }
35% { opacity: 1; transform: translateY(-3px); }
}
@keyframes omniPendingBubble {
0%, 100% { border-color: var(--black-alpha-12); }
50% { border-color: var(--black-alpha-24); }
}
@media (prefers-reduced-motion: reduce) {
.omni-chat-row.is-user .omni-chat-stack.is-pending .omni-chat-bubble,
.omni-send-dots i { animation: none; }
}
.omni-send-spinner {
@@ -2601,45 +2655,75 @@
border-radius: 50%;
}
.omni-chat-bubble.is-thinking {
display: flex;
flex-direction: row;
flex-wrap: wrap;
align-items: center;
gap: 8px 10px;
color: var(--black-alpha-64);
}
.omni-chat-bubble.is-reasoning {
display: flex;
flex-direction: column;
align-items: stretch;
gap: 8px;
color: var(--black-alpha-64);
}
/* 规划过程不是一行 loading:给过程本身足够横向空间,历史在内部滚动。 */
.omni-chat-row.agent.is-live .omni-chat-bubble.is-reasoning {
.omni-chat-row.agent.is-live .omni-chat-bubble.omni-wait-card {
position: relative;
width: min(620px, calc(100% - 44px));
max-width: min(620px, calc(100% - 44px));
align-items: stretch;
gap: 0;
overflow: hidden;
padding: 18px 20px 16px;
border-radius: var(--r-md);
color: var(--black-alpha-64);
}
.omni-wait-card::before,
.omni-wait-card::after {
position: absolute;
top: 0;
left: 0;
height: 3px;
content: "";
}
.omni-wait-card::before {
width: 100%;
background: linear-gradient(
90deg,
var(--border-muted) 0%,
var(--border-muted) 35%,
var(--heat-40) 50%,
var(--border-muted) 65%,
var(--border-muted) 100%
);
background-size: 200% 100%;
animation: omniWaitPending 2.6s linear infinite;
}
html[data-theme="dark"] .omni-wait-card::before {
background-image: linear-gradient(
90deg,
var(--background-base) 0%,
var(--background-base) 35%,
var(--heat-40) 50%,
var(--background-base) 65%,
var(--background-base) 100%
);
}
.omni-wait-card::after {
width: 100%;
background: var(--klein);
transform: scaleX(.06);
transform-origin: left center;
animation: omniWaitProgress 36s linear forwards;
}
.omni-think-head {
display: inline-flex;
display: flex;
align-items: center;
gap: 8px;
min-height: 24px;
color: var(--accent-black);
font-size: 13px;
color: var(--black-alpha-64);
font-weight: 600;
}
.omni-think-log {
display: grid;
gap: 8px;
max-height: 248px;
overflow-y: auto;
padding: 10px 8px 2px 0;
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid var(--border-faint);
scrollbar-gutter: stable;
}
.omni-think-log-row {
@@ -2673,27 +2757,17 @@
word-break: break-word;
}
.omni-typing {
display: inline-flex;
align-items: center;
gap: 4px;
@keyframes omniWaitProgress {
0% { transform: scaleX(.06); }
10% { transform: scaleX(.38); }
45% { transform: scaleX(.66); }
75% { transform: scaleX(.8); }
100% { transform: scaleX(.94); }
}
.omni-typing i {
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--klein);
opacity: .35;
animation: omniTyping 1.1s ease-in-out infinite;
}
.omni-typing i:nth-child(2) { animation-delay: .15s; }
.omni-typing i:nth-child(3) { animation-delay: .3s; }
@keyframes omniTyping {
0%, 80%, 100% { opacity: .28; transform: translateY(0); }
40% { opacity: 1; transform: translateY(-2px); }
@keyframes omniWaitPending {
from { background-position: 100% 0; }
to { background-position: -100% 0; }
}
@keyframes omniCurrentStepPulse {
@@ -2701,15 +2775,13 @@
50% { opacity: 1; }
}
@keyframes omniSendPulse {
0%, 100% {
background: #5b6572;
opacity: .45;
}
50% {
background: #8b95a3;
opacity: 1;
}
@media (prefers-reduced-motion: reduce) {
.omni-wait-card::before,
.omni-wait-card::after,
.omni-think-log-row.is-current::before { animation: none; }
.omni-wait-card::before { background: var(--border-muted); }
html[data-theme="dark"] .omni-wait-card::before { background: var(--background-base); }
.omni-wait-card::after { transform: scaleX(.6); }
}
.omni-process-skeleton {
+4 -3
View File
@@ -19,6 +19,7 @@ import {
X,
} from "lucide-react";
import { api, ApiError } from "../api";
import { displayOmniPresetLabel } from "../omni-preset-label";
import { findCatalogModel } from "../components/omni-param-bar";
import { modelResolutions } from "../components/free-create/constants";
import { ConfirmModal, MediaLightbox } from "../components/overlays";
@@ -46,7 +47,7 @@ type PresetItem = {
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: "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" },
@@ -667,7 +668,7 @@ export function OmniCreatePage({
model,
ratio,
...(outputMode === "video"
? { resolution, duration: selectedCase?.name === "剧情反转带货" ? "60 秒" : duration }
? { resolution, duration: selectedCase?.name === "短剧带货" ? "60 秒" : duration }
: { count: duration }),
},
})
@@ -931,7 +932,7 @@ export function OmniHistoryPage({
<h2>{item.title}</h2>
<p>
{[
item.preset || "自由创作",
displayOmniPresetLabel(item.preset),
item.mode === "video" ? "视频" : "图片",
item.params?.model,
item.params?.ratio,
+49 -25
View File
@@ -23,6 +23,7 @@ import {
X,
} from "lucide-react";
import { api } from "../api";
import { displayOmniPresetLabel } from "../omni-preset-label";
import { findCatalogModel, normalizeDurationValue, OmniParamBar } from "../components/omni-param-bar";
import { estimateCost, pointsPerImageFromCatalog } from "../components/free-create/constants";
import { MediaLightbox } from "../components/overlays";
@@ -1620,7 +1621,7 @@ function ElicitCard({
{selected ? (
<button
type="button"
className="btn btn-sm btn-ghost"
className="btn btn-ghost"
disabled={disabled}
onClick={() => setAnswers((prev) => ({ ...prev, story_direction: "" }))}
>
@@ -1629,7 +1630,7 @@ function ElicitCard({
) : null}
<button
type="button"
className="btn btn-sm btn-primary"
className="btn btn-primary"
disabled={disabled || !selected}
onClick={() => onSubmit({ story_direction: selected }, [])}
>
@@ -2048,6 +2049,7 @@ function ConfirmCard({
message,
sessionParams,
isVideo,
isShortDrama,
catalogModels,
disabled,
onConfirm,
@@ -2056,6 +2058,7 @@ function ConfirmCard({
message: CreationMessage;
sessionParams: Record<string, string>;
isVideo: boolean;
isShortDrama: boolean;
catalogModels?: ModelConfig[];
disabled: boolean;
onConfirm: (params: Record<string, string>) => void;
@@ -2066,7 +2069,7 @@ function ConfirmCard({
const payloadCredits = Number(message.payload.estimated_credits || 0);
const payloadParams = asStringMap(message.payload.params);
const snapshot = { ...sessionParams, ...payloadParams };
const [draft, setDraft] = useState(snapshot);
const [draft, setDraft] = useState(() => isShortDrama && isVideo && !submitted ? { ...snapshot, duration: "60 秒" } : snapshot);
const [initialDuration] = useState(snapshot.duration || "");
const cardIsVideo = message.payload.kind !== "image" && isVideo;
const durationChanged =
@@ -2112,6 +2115,7 @@ function ConfirmCard({
: generationPhase === "failed" ? "可重新确认方案后再生成"
: generationPhase === "running" ? "正在出片,请稍候"
: generationPhase === "submitted" ? "正在提交生成"
: isShortDrama ? "短剧带货固定 60 秒;确认前可以调整模型、分辨率和画面比例。"
: "确认前可以改参数。只有改时长时会同步更新视频架构与 Prompt。";
const foot =
generationPhase === "done" ? "已完成出片"
@@ -2137,6 +2141,7 @@ function ConfirmCard({
<div className="omni-confirm-params">
<OmniParamBar
isVideo={cardIsVideo}
fixedDuration={cardIsVideo && isShortDrama}
disabled={disabled}
catalogModels={catalogModels}
model={draft.model || ""}
@@ -2173,8 +2178,10 @@ function ConfirmCard({
function ResultCard({
payload,
fallbackRatio,
}: {
payload: Record<string, unknown>;
fallbackRatio?: string;
}) {
const assets = (payload.assets as Array<Record<string, string>> | undefined) || [];
const first = assets[0] || {};
@@ -2187,7 +2194,7 @@ function ResultCard({
const showSegmentGrid = partialFailure && assets.length > 0;
const showSingleMedia = !showSegmentGrid && assets.length > 0;
const [preview, setPreview] = useState<{ src: string; kind: "image" | "video"; name: string } | null>(null);
const ratio = String(payload.ratio || "").trim();
const ratio = String(payload.ratio || fallbackRatio || "").trim();
const ratioClass = ratio === "16:9" ? "is-ratio-16-9" : ratio === "1:1" ? "is-ratio-1-1" : "is-ratio-9-16";
const personReferenceClass = payload.kind === "person_reference" ? " is-person-reference" : "";
const tileCount = showSegmentGrid ? assets.length : showSingleMedia ? 1 : 0;
@@ -2622,15 +2629,18 @@ function groupVisibleCreationMessages(list: CreationMessage[]): CreationMessage[
function ProcessCard({
payload,
fallbackRatio,
}: {
payload: Record<string, unknown>;
fallbackRatio?: string;
}) {
const isSegmentedVideo = payload.kind === "video_segments";
const isMerge = payload.kind === "video_merge";
const isVideo = payload.kind === "video" || isSegmentedVideo || isMerge;
const segmentCount = Array.isArray(payload.segments) ? payload.segments.length : 0;
const completedSegments = Number(payload.completed_segment_count || 0);
const ratio = String(payload.ratio || "").trim();
const generationSpec = payload.generation_spec as Record<string, unknown> | undefined;
const ratio = String(payload.ratio || generationSpec?.aspect_ratio || fallbackRatio || "").trim();
const ratioClass = ratio === "16:9" ? "is-ratio-16-9" : ratio === "1:1" ? "is-ratio-1-1" : "is-ratio-9-16";
const personReferenceClass = payload.kind === "person_reference" ? " is-person-reference" : "";
@@ -2900,14 +2910,23 @@ export function OmniSessionPage({
const scrollFrameRef = useRef(0);
const initialScrolledForRef = useRef<string | null>(null);
const bottomScrollTarget = useCallback(() => {
const maxScroll = Math.max(0, document.documentElement.scrollHeight - window.innerHeight);
const latest = feedRef.current?.lastElementChild as HTMLElement | null;
if (!latest || latest.getBoundingClientRect().height > window.innerHeight * 0.6) return maxScroll;
const topbarHeight = Number.parseFloat(getComputedStyle(document.documentElement).getPropertyValue("--topbar-height")) || 0;
const latestTop = latest.getBoundingClientRect().top + window.scrollY;
// 短对话或低视口时,滚到底部不能把最后一条消息推进 sticky 顶栏背后。
return Math.min(maxScroll, Math.max(0, latestTop - topbarHeight - 16));
}, []);
useEffect(() => {
const onScroll = () => {
const gap = document.documentElement.scrollHeight - window.scrollY - window.innerHeight;
stickToBottomRef.current = gap < 120;
stickToBottomRef.current = bottomScrollTarget() - window.scrollY < 120;
};
window.addEventListener("scroll", onScroll, { passive: true });
return () => window.removeEventListener("scroll", onScroll);
}, []);
}, [bottomScrollTarget]);
const scrollToBottom = useCallback((smooth: boolean) => {
if (!stickToBottomRef.current) return;
@@ -2915,11 +2934,11 @@ export function OmniSessionPage({
scrollFrameRef.current = requestAnimationFrame(() => {
scrollFrameRef.current = 0;
window.scrollTo({
top: document.documentElement.scrollHeight,
top: bottomScrollTarget(),
behavior: smooth ? "smooth" : "instant",
});
});
}, []);
}, [bottomScrollTarget]);
// 重进已有会话时,等详情与消息一起落到 DOM 后直接显示最新内容。
// 首次定位不能用 smooth:长历史的滚动动画会在中途触发 onScroll,误判成用户上滑。
@@ -2928,12 +2947,12 @@ export function OmniSessionPage({
initialScrolledForRef.current = conversationId;
stickToBottomRef.current = true;
if (scrollFrameRef.current) cancelAnimationFrame(scrollFrameRef.current);
window.scrollTo({ top: document.documentElement.scrollHeight, behavior: "instant" });
window.scrollTo({ top: bottomScrollTarget(), behavior: "instant" });
const frame = requestAnimationFrame(() => {
window.scrollTo({ top: document.documentElement.scrollHeight, behavior: "instant" });
window.scrollTo({ top: bottomScrollTarget(), behavior: "instant" });
});
return () => cancelAnimationFrame(frame);
}, [conversation, conversationId]);
}, [bottomScrollTarget, conversation, conversationId]);
// 新消息落地:平滑滚一次
useEffect(() => {
@@ -3729,6 +3748,7 @@ export function OmniSessionPage({
message={message}
sessionParams={params}
isVideo={isVideo}
isShortDrama={Boolean(conversation?.preset && ["短剧带货", "剧情反转带货"].includes(conversation.preset))}
catalogModels={modelConfigs}
generationPhase={generationPhase}
disabled={
@@ -3745,6 +3765,7 @@ export function OmniSessionPage({
<ProcessCard
key={message.clientKey || message.id}
payload={message.payload}
fallbackRatio={params.ratio}
/>
);
case "result":
@@ -3752,6 +3773,7 @@ export function OmniSessionPage({
<ResultCard
key={message.clientKey || message.id}
payload={message.payload}
fallbackRatio={params.ratio}
/>
);
default: {
@@ -3786,6 +3808,11 @@ export function OmniSessionPage({
{(body || message.role !== "user") ? (
<div className="omni-chat-bubble" aria-busy={pending || undefined}>
{body ? (message.role === "assistant" ? <ChatMarkdown text={body} /> : <p className="omni-chat-text">{body}</p>) : null}
{pending ? (
<span className="omni-send-dots" role="status" aria-label="发送中">
<i aria-hidden="true" /><i aria-hidden="true" /><i aria-hidden="true" />
</span>
) : null}
</div>
) : null}
{showReplyGuide ? (
@@ -3848,7 +3875,7 @@ export function OmniSessionPage({
<h1>{conversation?.title || "未命名创作"}</h1>
<div className="omni-session-meta">
{[
conversation?.preset || "自由创作",
displayOmniPresetLabel(conversation?.preset),
params.model,
params.resolution && params.ratio
? `${params.resolution} · ${params.ratio}`
@@ -3903,7 +3930,7 @@ export function OmniSessionPage({
&& Boolean(conversation?.agent_progress)
&& !currentPlanningHasError ? (
/生成图片|生成视频/.test(activeTool) && !liveText ? (
<ProcessCard payload={{ kind: /生成视频/.test(activeTool) ? "video" : "image" }} />
<ProcessCard payload={{ kind: /生成视频/.test(activeTool) ? "video" : "image" }} fallbackRatio={params.ratio} />
) : liveText ? (
<div className="omni-chat-row agent is-live">
<span className="omni-chat-avatar">
@@ -3914,22 +3941,20 @@ export function OmniSessionPage({
</div>
</div>
) : (
<div className="omni-chat-row agent is-live" aria-label="正在回复">
<div className="omni-chat-row agent is-live" aria-label="正在生成创作内容">
<span className="omni-chat-avatar">
<Sparkles />
</span>
<div className={`omni-chat-bubble${liveReasoning ? " is-reasoning" : " is-thinking"}`}>
<div className="omni-chat-bubble omni-wait-card" role="status" aria-live="polite">
<div className="omni-think-head">
<span className="omni-typing" aria-hidden="true"><i /><i /><i /></span>
<span>{activeTool || (liveReasoning ? "正在思考" : isVideo ? "正在整理方案" : "正在整理画面")}</span>
</div>
{liveReasoning ? (
<div className="omni-think-log" ref={thinkingLogRef} aria-label="创作过程">
{(visibleProgressHistory.length
? visibleProgressHistory
<div className="omni-think-log" ref={thinkingLogRef} aria-label="创作过程">
{(visibleProgressHistory.length
? visibleProgressHistory.slice(-2)
: [{
label: activeTool || "正在思考",
detail: liveReasoning,
detail: liveReasoning || "正在准备创作内容,请稍候…",
}]
).map((progress, index, history) => (
<div
@@ -3939,8 +3964,7 @@ export function OmniSessionPage({
<p>{progress.detail}</p>
</div>
))}
</div>
) : null}
</div>
</div>
</div>
)