视频创作解决已发现问题
This commit is contained in:
@@ -163,6 +163,54 @@
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.omni-chat-markdown {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.omni-chat-markdown > :first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.omni-chat-markdown > :last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.omni-chat-markdown h3 {
|
||||
margin: 0 0 8px;
|
||||
color: var(--accent-black);
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.omni-chat-markdown p {
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
|
||||
.omni-chat-markdown ol,
|
||||
.omni-chat-markdown ul {
|
||||
margin: 0 0 8px;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.omni-chat-markdown li + li {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.omni-chat-markdown strong {
|
||||
color: var(--accent-black);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.omni-chat-markdown code {
|
||||
padding: 1px 4px;
|
||||
border-radius: var(--r-sm);
|
||||
color: var(--accent-black);
|
||||
background: var(--black-alpha-4);
|
||||
font-family: var(--font-mono);
|
||||
font-size: .92em;
|
||||
}
|
||||
|
||||
|
||||
.omni-reply-hint {
|
||||
margin: 8px 0 0;
|
||||
|
||||
@@ -556,7 +556,7 @@ export function OmniHistoryPage({
|
||||
navigate: NavigateFn;
|
||||
onNotify?: (type: "success" | "error" | "info", text: string) => void;
|
||||
}) {
|
||||
const [filter, setFilter] = useState<"all" | "running" | "completed">("all");
|
||||
const [modeFilter, setModeFilter] = useState<"all" | "video" | "image">("all");
|
||||
const [items, setItems] = useState<CreationConversation[] | null>(null);
|
||||
const [deleteTarget, setDeleteTarget] = useState<CreationConversation | null>(null);
|
||||
// 同会话页:onNotify 是内联箭头,进依赖会让列表每次 App 重渲染都重拉一遍
|
||||
@@ -567,7 +567,7 @@ export function OmniHistoryPage({
|
||||
let cancelled = false;
|
||||
setItems(null);
|
||||
api
|
||||
.listCreations(filter === "all" ? {} : { status: filter })
|
||||
.listCreations(modeFilter === "all" ? {} : { mode: modeFilter })
|
||||
.then((page) => {
|
||||
if (!cancelled) setItems(page.results || []);
|
||||
})
|
||||
@@ -579,7 +579,7 @@ export function OmniHistoryPage({
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [filter]);
|
||||
}, [modeFilter]);
|
||||
|
||||
const remove = async (id: string) => {
|
||||
// 软删:会话没了,已生成的图和视频仍留在资产库里
|
||||
@@ -602,20 +602,20 @@ export function OmniHistoryPage({
|
||||
</button>
|
||||
<h1>创作历史</h1>
|
||||
</div>
|
||||
<p>查看和继续全能创作中的每一次独立会话。</p>
|
||||
<p>按视频创作或图片创作查看,并继续每一次独立会话。</p>
|
||||
</div>
|
||||
<div className="omni-history-tools">
|
||||
<div className="omni-history-toolbar">
|
||||
{([
|
||||
["all", "全部"],
|
||||
["running", "进行中"],
|
||||
["completed", "已完成"],
|
||||
["video", "视频创作"],
|
||||
["image", "图片创作"],
|
||||
] as const).map(([key, label]) => (
|
||||
<button
|
||||
type="button"
|
||||
key={key}
|
||||
className={filter === key ? "active" : ""}
|
||||
onClick={() => setFilter(key)}
|
||||
className={modeFilter === key ? "active" : ""}
|
||||
onClick={() => setModeFilter(key)}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
@@ -634,7 +634,13 @@ export function OmniHistoryPage({
|
||||
</div>
|
||||
) : items.length === 0 ? (
|
||||
<div className="omni-history-empty">
|
||||
<p>还没有创作记录,从全能创作开始。</p>
|
||||
<p>
|
||||
{modeFilter === "video"
|
||||
? "还没有视频创作记录,从全能创作开始。"
|
||||
: modeFilter === "image"
|
||||
? "还没有图片创作记录,从全能创作开始。"
|
||||
: "还没有创作记录,从全能创作开始。"}
|
||||
</p>
|
||||
<button type="button" onClick={() => navigate("omniCreate")}>
|
||||
去全能创作
|
||||
<ChevronRight size={14} />
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type FormEvent, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
||||
import { type FormEvent, type ReactNode, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import {
|
||||
ArrowLeft,
|
||||
@@ -43,6 +43,94 @@ type PromptBlock =
|
||||
|
||||
type ReplyOption = { label: string; text: string };
|
||||
|
||||
function renderInlineMarkdown(text: string): ReactNode[] {
|
||||
return text.split(/(\*\*[^*]+\*\*|`[^`]+`|\*[^*]+\*)/g).filter(Boolean).map((part, index) => {
|
||||
if (part.startsWith("**") && part.endsWith("**")) {
|
||||
return <strong key={index}>{part.slice(2, -2)}</strong>;
|
||||
}
|
||||
if (part.startsWith("`") && part.endsWith("`")) {
|
||||
return <code key={index}>{part.slice(1, -1)}</code>;
|
||||
}
|
||||
if (part.startsWith("*") && part.endsWith("*")) {
|
||||
return <em key={index}>{part.slice(1, -1)}</em>;
|
||||
}
|
||||
return part;
|
||||
});
|
||||
}
|
||||
|
||||
/** 对话正文的安全 Markdown 子集;不使用 HTML 注入,避免模型文本影响页面结构。 */
|
||||
function ChatMarkdown({ text }: { text: string }) {
|
||||
const lines = text.replace(/\r\n/g, "\n").split("\n");
|
||||
const nodes: ReactNode[] = [];
|
||||
let index = 0;
|
||||
|
||||
while (index < lines.length) {
|
||||
const line = lines[index].trim();
|
||||
if (!line) {
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
const heading = line.match(/^#{1,3}\s+(.+)$/);
|
||||
if (heading) {
|
||||
nodes.push(<h3 key={`heading-${index}`}>{renderInlineMarkdown(heading[1])}</h3>);
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
const ordered = line.match(/^(\d+)[.、.]\s+(.+)$/);
|
||||
if (ordered) {
|
||||
const start = Number(ordered[1]);
|
||||
const items: Array<{ key: number; text: string }> = [];
|
||||
while (index < lines.length) {
|
||||
const match = lines[index].trim().match(/^(\d+)[.、.]\s+(.+)$/);
|
||||
if (!match) break;
|
||||
items.push({ key: Number(match[1]), text: match[2] });
|
||||
index += 1;
|
||||
}
|
||||
nodes.push(
|
||||
<ol key={`ordered-${index}`} start={start}>
|
||||
{items.map((item) => <li key={item.key}>{renderInlineMarkdown(item.text)}</li>)}
|
||||
</ol>,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (/^[-*+](?:\s+)/.test(line)) {
|
||||
const items: string[] = [];
|
||||
while (index < lines.length) {
|
||||
const match = lines[index].trim().match(/^[-*+](?:\s+)(.+)$/);
|
||||
if (!match) break;
|
||||
items.push(match[1]);
|
||||
index += 1;
|
||||
}
|
||||
nodes.push(
|
||||
<ul key={`unordered-${index}`}>
|
||||
{items.map((item, itemIndex) => <li key={itemIndex}>{renderInlineMarkdown(item)}</li>)}
|
||||
</ul>,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const paragraph: string[] = [];
|
||||
while (index < lines.length) {
|
||||
const current = lines[index].trim();
|
||||
if (!current || /^#{1,3}\s+/.test(current) || /^(\d+)[.、.]\s+/.test(current) || /^[-*+](?:\s+)/.test(current)) break;
|
||||
paragraph.push(lines[index]);
|
||||
index += 1;
|
||||
}
|
||||
nodes.push(
|
||||
<p key={`paragraph-${index}`}>
|
||||
{paragraph.map((item, lineIndex) => (
|
||||
<span key={lineIndex}>{lineIndex ? <br /> : null}{renderInlineMarkdown(item)}</span>
|
||||
))}
|
||||
</p>,
|
||||
);
|
||||
}
|
||||
|
||||
return <div className="omni-chat-markdown">{nodes}</div>;
|
||||
}
|
||||
|
||||
function stripNumericReplyInstruction(text: string): string {
|
||||
return (text || "")
|
||||
.replace(
|
||||
@@ -61,7 +149,46 @@ function numberedReplyOptions(text: string): ReplyOption[] {
|
||||
}));
|
||||
}
|
||||
|
||||
function replyOptionsForMessage(text: string, payload: Record<string, unknown>, isVideo: boolean): ReplyOption[] {
|
||||
function contextualReplyOptions(text: string): ReplyOption[] {
|
||||
if (/颜色|色号|色彩|SKU|款式|几种/i.test(text)) {
|
||||
return [
|
||||
{ label: "补充颜色和顺序", text: "我来补充每个颜色和展示顺序" },
|
||||
{ label: "上传各款实物图", text: "我补充各颜色/款式的实物图" },
|
||||
{ label: "先按当前主款做", text: "先按当前主款做,其他颜色后面再补" },
|
||||
];
|
||||
}
|
||||
if (/商品|产品|主推|哪款/i.test(text)) {
|
||||
return [
|
||||
{ label: "发商品列表", text: "把商品列表发给我选" },
|
||||
{ label: "我直接说商品名", text: "我直接告诉你商品名" },
|
||||
{ label: "你来推荐", text: "你根据当前需求推荐一款" },
|
||||
];
|
||||
}
|
||||
if (/人物|角色|模特|出镜/i.test(text)) {
|
||||
return [
|
||||
{ label: "上传人物图", text: "我上传人物参考图" },
|
||||
{ label: "由你设定角色", text: "你先帮我设定一个合适的角色" },
|
||||
{ label: "不需要人物", text: "这条先不需要人物出镜" },
|
||||
];
|
||||
}
|
||||
if (/场景|地点|背景|在哪/i.test(text)) {
|
||||
return [
|
||||
{ label: "上传场景图", text: "我上传场景参考图" },
|
||||
{ label: "你来推荐场景", text: "你按商品和预设推荐场景" },
|
||||
{ label: "用干净日常场景", text: "先用干净自然的日常场景" },
|
||||
];
|
||||
}
|
||||
if (/卖点|功能|效果|优惠|价格/i.test(text)) {
|
||||
return [
|
||||
{ label: "补充真实卖点", text: "我来补充商品真实卖点" },
|
||||
{ label: "从素材里判断", text: "先根据我上传的素材判断可表达的卖点" },
|
||||
{ label: "先只突出一个点", text: "先围绕一个最核心的卖点创作" },
|
||||
];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
function replyOptionsForMessage(text: string, payload: Record<string, unknown>): ReplyOption[] {
|
||||
const numbered = numberedReplyOptions(text);
|
||||
if (numbered.length) return numbered;
|
||||
const stored = payload.reply_options;
|
||||
@@ -71,19 +198,10 @@ function replyOptionsForMessage(text: string, payload: Record<string, unknown>,
|
||||
.map((item) => ({ label: String(item.label || "").trim(), text: String(item.text || "").trim() }))
|
||||
.filter((item) => item.label && item.text)
|
||||
.slice(0, 3);
|
||||
if (options.length) return options;
|
||||
const legacyGeneric = ["继续完善方案", "换个场景", "改卖点", "按这个方向出图", "改人物状态"];
|
||||
if (options.length && !options.every((item) => legacyGeneric.includes(item.label))) return options;
|
||||
}
|
||||
return isVideo
|
||||
? [
|
||||
{ label: "继续完善方案", text: "继续完善方案" },
|
||||
{ label: "换个场景", text: "我想换个场景" },
|
||||
{ label: "改卖点", text: "我想改卖点" },
|
||||
]
|
||||
: [
|
||||
{ label: "按这个方向出图", text: "按这个方向出图" },
|
||||
{ label: "换个场景", text: "我想换个场景" },
|
||||
{ label: "改人物状态", text: "我想改人物状态" },
|
||||
];
|
||||
return contextualReplyOptions(text);
|
||||
}
|
||||
|
||||
function ReplyActions({
|
||||
@@ -954,7 +1072,7 @@ function ElicitCard({
|
||||
<Sparkles />
|
||||
</span>
|
||||
<div className="omni-chat-bubble omni-gate-bubble">
|
||||
<p className="omni-chat-text">{message.text || gateLabel}</p>
|
||||
<ChatMarkdown text={message.text || gateLabel} />
|
||||
{!submitted ? (
|
||||
<div className="omni-gate-actions" aria-label="商品选择操作">
|
||||
{(gateField?.options || []).map((option, index) => (
|
||||
@@ -991,7 +1109,7 @@ function ElicitCard({
|
||||
</span>
|
||||
<div className="omni-chat-stack">
|
||||
<div className="omni-chat-bubble">
|
||||
<p className="omni-chat-text">{question}</p>
|
||||
<ChatMarkdown text={question} />
|
||||
</div>
|
||||
{!submitted ? (
|
||||
<div className="omni-reply-guide" aria-label="回复操作">
|
||||
@@ -2403,7 +2521,7 @@ export function OmniSessionPage({
|
||||
&& message.kind === "text"
|
||||
&& messages[messages.length - 1]?.id === message.id
|
||||
&& !streaming;
|
||||
const replyOptions = showReplyGuide ? replyOptionsForMessage(body, message.payload, isVideo) : [];
|
||||
const replyOptions = showReplyGuide ? replyOptionsForMessage(body, message.payload) : [];
|
||||
return (
|
||||
<div
|
||||
className={`omni-chat-row ${message.role === "user" ? "is-user" : "agent"}${pending ? " is-pending" : ""}`}
|
||||
@@ -2418,7 +2536,7 @@ export function OmniSessionPage({
|
||||
{message.role === "user" ? <MentionChips refs={refs} tone="user" /> : null}
|
||||
{(body || message.role !== "user") ? (
|
||||
<div className="omni-chat-bubble" aria-busy={pending || undefined}>
|
||||
{body ? <p className="omni-chat-text">{body}</p> : null}
|
||||
{body ? (message.role === "assistant" ? <ChatMarkdown text={body} /> : <p className="omni-chat-text">{body}</p>) : null}
|
||||
</div>
|
||||
) : null}
|
||||
{showReplyGuide ? (
|
||||
@@ -2521,7 +2639,7 @@ export function OmniSessionPage({
|
||||
<Sparkles />
|
||||
</span>
|
||||
<div className="omni-chat-bubble">
|
||||
<p className="omni-chat-text">{liveText}</p>
|
||||
<ChatMarkdown text={liveText} />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
|
||||
Reference in New Issue
Block a user