fix(frontend): show raw chat messages and improve roundtable draft saves
This commit is contained in:
1 parent
0d8cdf3afb
commit
93e702f9ca
5 files changed
+92
-13
No files matched your search
@@ -84,5 +84,12 @@ offline-backend-20260512/backend/tests/*
|
||||
deploy
|
||||
.history
|
||||
|
||||
# Repo-root offline dependency pack (local only)
|
||||
/dist/
|
||||
|
||||
# Frontend offline pack working directories
|
||||
frontend-web/.offline-pack-build/
|
||||
frontend-web/.offline-extract-test/
|
||||
|
||||
# Frontend skills components must be tracked (excluded from the broad skills/ ignore above)
|
||||
!frontend-web/src/components/workspace/skills/
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
type MessageResponseProps,
|
||||
} from "@/components/ai-elements/message";
|
||||
import { streamdownPlugins } from "@/core/streamdown";
|
||||
import { desensitize } from "@/lib/desensitize";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
import { CitationLink } from "../citations/citation-link";
|
||||
@@ -67,8 +66,6 @@ export function MarkdownContent({
|
||||
|
||||
if (!content) return null;
|
||||
|
||||
const displayContent = desensitize(content);
|
||||
|
||||
return (
|
||||
<MessageResponse
|
||||
className={className}
|
||||
@@ -76,7 +73,7 @@ export function MarkdownContent({
|
||||
rehypePlugins={rehypePlugins}
|
||||
components={components}
|
||||
>
|
||||
{displayContent}
|
||||
{content}
|
||||
</MessageResponse>
|
||||
);
|
||||
}
|
||||
@@ -42,7 +42,6 @@ import {
|
||||
type RuntimeDisplaySource,
|
||||
} from "@/core/skill-display/runtime";
|
||||
import { humanMessagePlugins } from "@/core/streamdown";
|
||||
import { desensitize } from "@/lib/desensitize";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
import { CitationLink } from "../citations/citation-link";
|
||||
@@ -259,7 +258,7 @@ function MessageContent_({
|
||||
<TaskTrigger title="">
|
||||
<div className="text-muted-foreground flex w-full cursor-default items-center gap-2 text-sm select-none">
|
||||
<Loader className="size-4" />
|
||||
<span>{desensitize(contentToDisplay)}</span>
|
||||
<span>{contentToDisplay}</span>
|
||||
</div>
|
||||
</TaskTrigger>
|
||||
</Task>
|
||||
@@ -273,7 +272,7 @@ function MessageContent_({
|
||||
<AIElementMessageContent className={className}>
|
||||
<Reasoning isStreaming={isLoading}>
|
||||
<ReasoningTrigger />
|
||||
<ReasoningContent>{desensitize(reasoningContent)}</ReasoningContent>
|
||||
<ReasoningContent>{reasoningContent}</ReasoningContent>
|
||||
</Reasoning>
|
||||
<MessageTokenUsage
|
||||
enabled={tokenUsageEnabled}
|
||||
@@ -292,7 +291,7 @@ function MessageContent_({
|
||||
components={components}
|
||||
parseIncompleteMarkdown={false}
|
||||
>
|
||||
{desensitize(contentToDisplay)}
|
||||
{contentToDisplay}
|
||||
</AIElementMessageResponse>
|
||||
) : null;
|
||||
return (
|
||||
|
||||
@@ -57,6 +57,16 @@ export interface UseDraftPersistenceOptions {
|
||||
* 否则保持原 false 让进入 Step 2 时正常初始化。
|
||||
*/
|
||||
setStep2InitGate: (initialized: boolean) => void;
|
||||
/**
|
||||
* Step 1 是否正在等待任务理解回复。由 true→false 视为「本轮模型回答完」,
|
||||
* 触发一次自动保存(详见 autoSave 的两个 effect)。
|
||||
*/
|
||||
intentSending: boolean;
|
||||
/**
|
||||
* Step 2 已收尾(streaming===false)的对话气泡数量。每次递增意味着总控/某个
|
||||
* 子智能体刚交付完一轮,触发一次自动保存。流式增量不改变该计数,因此不会刷屏式保存。
|
||||
*/
|
||||
step2FinalizedCount: number;
|
||||
}
|
||||
|
||||
export interface UseDraftPersistenceReturn {
|
||||
@@ -96,6 +106,8 @@ export function useDraftPersistence(
|
||||
hydrateStep2,
|
||||
cancelAllStreams,
|
||||
setStep2InitGate,
|
||||
intentSending,
|
||||
step2FinalizedCount,
|
||||
} = options;
|
||||
|
||||
const [drafts, setDrafts] = useState<DraftListItem[]>([]);
|
||||
@@ -112,6 +124,11 @@ export function useDraftPersistence(
|
||||
const currentDraftIdRef = useRef<string | null>(null);
|
||||
// 串行化所有写操作,避免并发 create。
|
||||
const saveQueueRef = useRef<Promise<unknown>>(Promise.resolve());
|
||||
// 当前步骤镜像 —— 自动保存(不带 furthestStep)落到 create 分支时,用它兜底建档步骤。
|
||||
const currentStepRef = useRef<number>(currentStep);
|
||||
useEffect(() => {
|
||||
currentStepRef.current = currentStep;
|
||||
}, [currentStep]);
|
||||
|
||||
// 包一层 setter:同时更新 state 与同步 ref。主页面通过它 setCurrentDraftId(null)。
|
||||
const setCurrentDraftId = useCallback<React.Dispatch<React.SetStateAction<string | null>>>(
|
||||
@@ -157,9 +174,15 @@ export function useDraftPersistence(
|
||||
* 串行执行一次保存:有 id 走 update(不带 title,保护重命名);无 id 走 create
|
||||
* (写一次派生标题)。create/update 成功后同步 currentDraftIdRef。
|
||||
* 返回 draft id(无可保存内容返回 null)。
|
||||
*
|
||||
* `furthestStep` 可选:
|
||||
* - 传值(前进切换 / 手动保存 / ensureDraft)→ update 时一并推进 furthest_step;
|
||||
* - 省略(每轮模型回答完的自动保存)→ update **不带** furthest_step,只回写
|
||||
* step1/step2 内容,避免把「回到上一步继续研讨」误判成 furthest 回退。create
|
||||
* 分支用 currentStepRef 兜底建档步骤。
|
||||
*/
|
||||
const enqueueSave = useCallback(
|
||||
(furthestStep: 1 | 2 | 3): Promise<string | null> => {
|
||||
(furthestStep?: 1 | 2 | 3): Promise<string | null> => {
|
||||
const run = async (): Promise<string | null> => {
|
||||
const step1 = getStep1Snapshot();
|
||||
const step2 = getStep2Snapshot();
|
||||
@@ -167,7 +190,11 @@ export function useDraftPersistence(
|
||||
|
||||
const id = currentDraftIdRef.current;
|
||||
if (id) {
|
||||
const updated = await updateDraft(id, { furthestStep, step1, step2 });
|
||||
const updated = await updateDraft(id, {
|
||||
...(furthestStep !== undefined ? { furthestStep } : {}),
|
||||
step1,
|
||||
step2,
|
||||
});
|
||||
if (updated) return updated.id;
|
||||
// 服务端已不存在(被删/换账号)→ 落回新建。
|
||||
}
|
||||
@@ -176,7 +203,7 @@ export function useDraftPersistence(
|
||||
intentReady: step1.intentReady,
|
||||
interactiveMessages: step1.interactiveMessages,
|
||||
}),
|
||||
furthestStep,
|
||||
furthestStep: furthestStep ?? ((currentStepRef.current as 1 | 2 | 3) || 1),
|
||||
step1,
|
||||
step2,
|
||||
});
|
||||
@@ -227,6 +254,41 @@ export function useDraftPersistence(
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [currentStep]);
|
||||
|
||||
/**
|
||||
* 每轮模型回答完成后落一次草稿。不带 furthestStep(见 enqueueSave),只回写内容。
|
||||
* 仅当本次保存是「新建」时才刷新列表 —— 普通增量回写无需让下拉重排/多发一个 GET。
|
||||
*/
|
||||
const autoSave = useCallback(async (): Promise<void> => {
|
||||
const hadId = currentDraftIdRef.current;
|
||||
const id = await enqueueSave();
|
||||
if (id && !hadId) await refreshList();
|
||||
}, [enqueueSave, refreshList]);
|
||||
|
||||
// ── 触发器 1:Step 1 任务理解一轮回复结束(isIntentSending true→false)。 ──
|
||||
const prevIntentSendingRef = useRef<boolean>(intentSending);
|
||||
useEffect(() => {
|
||||
const was = prevIntentSendingRef.current;
|
||||
prevIntentSendingRef.current = intentSending;
|
||||
// 此刻 React 已 flush 完终态 setState(气泡内容/INTENT_READY),
|
||||
// 快照 getter 读到的是收尾后的最新状态,不会丢最后一段。
|
||||
if (was && !intentSending) void autoSave();
|
||||
}, [intentSending, autoSave]);
|
||||
|
||||
// ── 触发器 2:Step 2 某个气泡刚收尾(已收尾计数递增)。 ──
|
||||
// 加载草稿会把历史对话整批 hydrate 进来,计数从 0 跳到 N —— 那不是一次真实交付,
|
||||
// 用 suppress latch 跳过这一跳变(handleLoadDraft 在 hydrate 前置位)。
|
||||
const prevFinalizedCountRef = useRef<number>(step2FinalizedCount);
|
||||
const suppressStep2AutoSaveRef = useRef<boolean>(false);
|
||||
useEffect(() => {
|
||||
const prev = prevFinalizedCountRef.current;
|
||||
prevFinalizedCountRef.current = step2FinalizedCount;
|
||||
if (suppressStep2AutoSaveRef.current) {
|
||||
suppressStep2AutoSaveRef.current = false;
|
||||
return;
|
||||
}
|
||||
if (step2FinalizedCount > prev) void autoSave();
|
||||
}, [step2FinalizedCount, autoSave]);
|
||||
|
||||
const handleLoadDraft = useCallback(
|
||||
(id: string) => {
|
||||
void (async () => {
|
||||
@@ -248,6 +310,9 @@ export function useDraftPersistence(
|
||||
// 先置 latch,再 setCurrentStep —— 顺序反过来会让 step1/step2 init effect
|
||||
// 把 loadedFromDraftRef.current 当成 false 处理,导致角色面板卡在「正在上线」。
|
||||
loadedFromDraftRef.current = true;
|
||||
// hydrate 会让 step2 已收尾计数从 0 跳到 N,抑制由此触发的「假交付」自动保存,
|
||||
// 避免刚加载就把同样内容原样回写一遍、并把该草稿顶到列表最前。
|
||||
suppressStep2AutoSaveRef.current = true;
|
||||
|
||||
if (draft.step1) hydrateStep1(draft.step1 as Step1Snapshot);
|
||||
if (draft.step2) hydrateStep2(draft.step2 as Step2Snapshot);
|
||||
|
||||
@@ -17,11 +17,12 @@
|
||||
* 1) special 顺序派活,**不要**改回 `Promise.all`(详见 §5.4)
|
||||
* 2) step1InitGenRef / step2InitRef 闸门用于规避 StrictMode 双 mount 副作用
|
||||
* 3) 离开 Step 2 不销毁 threadIds,允许返回继续看
|
||||
* 4) 草稿自动保存仅在前进切换(1→2、2→3)触发,后退不保存
|
||||
* 4) 草稿自动保存:前进切换(1→2、2→3)推进 furthestStep;此外每轮模型回答完
|
||||
* (Step 1 一轮任务理解结束 / Step 2 每个气泡收尾)也增量回写一次,后者不动 furthestStep
|
||||
* 5) Persona 抽屉改的 model 不会立即重启编排,下一轮才生效
|
||||
* 6) 模型选择全局默认 `availableModels[0].name`,Persona 覆盖更高优先级
|
||||
*/
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import {
|
||||
Activity,
|
||||
Check,
|
||||
@@ -173,11 +174,21 @@ export default function RoundtablePlanningPage() {
|
||||
};
|
||||
}, [step1.intentReady, step1.interactiveMessages]);
|
||||
|
||||
// Step 2 已收尾(非 streaming)的气泡数量 —— 递增即「某轮模型刚交付完」,
|
||||
// 作为草稿自动保存的触发信号(详见 useDraftPersistence)。流式增量不计入,
|
||||
// 所以不会在 token 级别刷屏式保存。
|
||||
const step2FinalizedCount = useMemo(
|
||||
() => step2.step2RoundtableDialogues.filter((d) => !d.streaming).length,
|
||||
[step2.step2RoundtableDialogues],
|
||||
);
|
||||
|
||||
// 草稿 hook —— 同时依赖 step1 + step2 的 snapshot / hydrate。
|
||||
const drafts = useDraftPersistence({
|
||||
currentStep,
|
||||
setCurrentStep,
|
||||
triggerToast,
|
||||
intentSending: step1.isIntentSending,
|
||||
step2FinalizedCount,
|
||||
getStep1Snapshot: () => ({
|
||||
intentThreadId: step1.intentThreadId,
|
||||
intentReady: step1.intentReady,
|
||||
|
||||
Reference in new issue
Block a user