优化全能创作部分优化脚本
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user