优化全能创作部分优化脚本

This commit is contained in:
Azmat@qq.com
2026-09-10 18:59:08 +08:00
parent 0d56fdb299
commit 43f80a4d90
12 changed files with 1223 additions and 95 deletions
+71
View File
@@ -1528,6 +1528,35 @@
display: none;
}
.omni-chat-bubble.is-reasoning {
display: flex;
flex-direction: column;
align-items: stretch;
gap: 8px;
color: #5c6570;
}
.omni-think-head {
display: inline-flex;
align-items: center;
gap: 8px;
font-size: 13px;
color: #5c6570;
}
.omni-think-text {
margin: 0;
font-size: 12px;
line-height: 1.65;
color: #8a93a0;
white-space: pre-wrap;
word-break: break-word;
max-height: 168px;
overflow-y: auto;
border-left: 1px solid rgba(15, 23, 42, 0.08);
padding-left: 10px;
}
@keyframes omniShimmer {
0% { background-position: 100% 0; }
100% { background-position: -100% 0; }
@@ -1672,3 +1701,45 @@
white-space: pre-wrap;
word-break: break-word;
}
.omni-gate-bubble {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 12px;
max-width: min(520px, calc(100% - 44px));
}
.omni-gate-actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.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;
cursor: pointer;
}
.omni-gate-actions button.primary {
border-color: var(--klein);
background: var(--klein);
color: #fff;
}
.omni-gate-actions button:disabled {
opacity: .55;
cursor: not-allowed;
}
.omni-gate-answered {
margin: 0;
font-size: 12px;
color: #8a93a0;
}
+116 -8
View File
@@ -239,10 +239,7 @@ function PromptFileCard({
);
}
/**
* 追问卡:后端 ask_user 工具的落点(设计稿里没有这张卡,新增)。
* 提交后变只读 —— 重复提交后端会 409。
*/
/** 追问默认是普通聊天气泡;下面的卡片只为兼容旧会话数据。 */
function ElicitCard({
message,
disabled,
@@ -257,12 +254,13 @@ function ElicitCard({
const fields = ((message.payload.fields as CreationField[] | undefined) || []).filter(Boolean);
const submitted = Boolean(message.payload.submitted);
const saved = (message.payload.answers as Record<string, string | string[]> | undefined) || {};
const interaction = String(message.payload.interaction || "");
const [answers, setAnswers] = useState<Record<string, string | string[]>>(saved);
const [assetOptions, setAssetOptions] = useState<Record<string, CreationRef[]>>({});
const [assetPicked, setAssetPicked] = useState<Record<string, CreationRef>>({});
useEffect(() => {
if (submitted) return;
if (submitted || interaction === "chat") return;
// 选素材字段要现拉候选:后端只给了 asset_types,具体有哪些素材是团队数据
const assetFields = fields.filter((f) => f.type === "asset");
if (assetFields.length === 0) return;
@@ -290,7 +288,22 @@ function ElicitCard({
cancelled = true;
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [message.id, submitted]);
}, [message.id, submitted, interaction]);
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) => {
@@ -306,6 +319,48 @@ function ElicitCard({
return Array.isArray(value) ? value.length > 0 : Boolean(value);
});
const phase = String(message.payload.phase || "pick");
const gateField = fields[0];
const gateLabel = gateField?.label || "要我把列表发给你选吗?";
// 素材选择闸门:先轻量询问,用户愿意时再展开真正的选择卡
if (phase === "gate") {
const gateAnswer = saved._asset_gate;
const selectedLabel = gateField?.options?.find((option) => option.value === gateAnswer)?.label;
return (
<div className="omni-chat-row agent">
<span className="omni-chat-avatar">
<Sparkles />
</span>
<div className="omni-chat-bubble omni-gate-bubble">
<p className="omni-chat-text">{gateLabel}</p>
{!submitted ? (
<div className="omni-gate-actions">
{(gateField?.options || []).map((option) => (
<button
type="button"
key={option.value}
className={option.value === "send" ? "primary" : ""}
disabled={disabled}
onClick={() => {
if (!gateField?.key) return;
onSubmit({ [gateField.key]: option.value }, []);
}}
>
{option.label}
</button>
))}
</div>
) : (
<p className="omni-gate-answered">
{selectedLabel ? `已选择:${selectedLabel}` : "已完成选择"}
</p>
)}
</div>
</div>
);
}
return (
<div className="omni-elicit-slot">
<section className={`omni-elicit-card${submitted ? " is-submitted" : ""}`}>
@@ -615,6 +670,37 @@ function keepUnackedLocals(prev: CreationMessage[], incoming: CreationMessage[])
return collapseDupUser(leftover.length ? [...incoming, ...leftover] : incoming);
}
function withoutLegacyGateArtifacts(list: CreationMessage[]): CreationMessage[] {
/**
* 旧版本点「先不用」时会落一条系统伪造的 user 消息,紧接着再回一句空话。
* 数据先保留,这里只在能确认完整错误序列时隐藏,避免老会话刷新后还显示这组脏气泡。
*/
const hidden = new Set<string>();
for (let index = 1; index < list.length; index += 1) {
const gate = list[index - 1];
const echo = list[index];
const gatePayload = gate?.payload || {};
if (
gate?.kind !== "elicit"
|| gatePayload.phase !== "gate"
|| (gatePayload.answers as Record<string, unknown> | undefined)?._asset_gate !== "skip"
|| echo?.role !== "user"
|| echo.text !== "先不选素材也可以,有需要再说。"
) continue;
hidden.add(echo.id);
const deadEnd = list[index + 1];
if (
deadEnd?.role === "assistant"
&& deadEnd.kind === "text"
&& /^在的[,]有需要再说(?:一声)?[。.]?$/.test(deadEnd.text.trim())
) {
hidden.add(deadEnd.id);
}
}
return hidden.size ? list.filter((message) => !hidden.has(message.id)) : list;
}
function ProcessCard({ payload }: { payload: Record<string, unknown> }) {
const isVideo = payload.kind === "video";
return (
@@ -684,6 +770,7 @@ export function OmniSessionPage({
const fileInputRef = useRef<HTMLInputElement>(null);
const [streaming, setStreaming] = useState(false);
const [liveText, setLiveText] = useState("");
const [liveReasoning, setLiveReasoning] = useState("");
const [activeTool, setActiveTool] = useState("");
const [mentionMenuOpen, setMentionMenuOpen] = useState(false);
const [mentionTab, setMentionTab] = useState<CreationRef["type"]>("asset");
@@ -805,6 +892,7 @@ export function OmniSessionPage({
() => messages.some((message) => message.kind === "generating"),
[messages]
);
const visibleMessages = useMemo(() => withoutLegacyGateArtifacts(messages), [messages]);
// 出片在 worker 里跑。刷新后 GENERATING 还在库里,进来立刻拉一次再轮询,
// 不要等流式对话结束,也不要空等一个间隔才看见进度。
@@ -847,6 +935,7 @@ export function OmniSessionPage({
if (event.type === "message") {
const message = event.message as CreationMessage;
setLiveText("");
setLiveReasoning("");
setMessages((prev) => collapseDupUser(replaceLocalUser(prev, message)));
if (message.role === "user" && pendingUserIdRef.current) {
pendingUserIdRef.current = null;
@@ -854,6 +943,10 @@ export function OmniSessionPage({
}
return;
}
if (event.type === "reasoning") {
setLiveReasoning((prev) => prev + String(event.text || ""));
return;
}
if (event.type === "delta") {
setLiveText((prev) => prev + String(event.text || ""));
return;
@@ -864,6 +957,7 @@ export function OmniSessionPage({
}
if (event.type === "error") {
setLiveText("");
setLiveReasoning("");
}
}, []);
@@ -873,6 +967,7 @@ export function OmniSessionPage({
streamingRef.current = true;
setStreaming(true);
setLiveText("");
setLiveReasoning("");
setActiveTool("");
const isTextTurn = payload.kind !== "elicit_answer";
let localId: string | null = null;
@@ -920,6 +1015,7 @@ export function OmniSessionPage({
streamingRef.current = false;
setStreaming(false);
setLiveText("");
setLiveReasoning("");
setActiveTool("");
abortRef.current = null;
// 流结束再拉一次:worker 可能已经把 GENERATING 改成 RESULT,
@@ -1145,7 +1241,7 @@ export function OmniSessionPage({
<div className="omni-session-shell">
<main className="omni-session-feed" ref={feedRef} aria-live="polite">
{messages.map(renderMessage)}
{visibleMessages.map(renderMessage)}
{/* 流式中的临时气泡。挂 is-live 关掉入场动画 —— 它每来一个字符都会
重渲染,带动画的话整条会一直在闪;真消息落地时它被替换掉,
那一下也不该再演一次入场。 */}
@@ -1157,9 +1253,21 @@ export function OmniSessionPage({
<span className="omni-chat-avatar">
<Sparkles />
</span>
<div className={`omni-chat-bubble${liveText ? "" : " is-thinking"}`}>
<div className={`omni-chat-bubble${liveText || liveReasoning ? "" : " is-thinking"}${liveReasoning && !liveText ? " is-reasoning" : ""}`}>
{liveText ? (
<p className="omni-chat-text">{liveText}</p>
) : liveReasoning ? (
<>
<div className="omni-think-head">
<span className="omni-typing" aria-hidden="true">
<i />
<i />
<i />
</span>
<span>{activeTool ? `${activeTool}` : "思考中"}</span>
</div>
<p className="omni-think-text">{liveReasoning}</p>
</>
) : (
<>
<span className="omni-typing" aria-hidden="true">