feat: 优化对话历史与会商引导体验

列表引导改为手动触发且不跳转创建页;历史 Tab 展示会商步骤/链条与深度研究模板;Step2 增加暂停与内容高度跟随滚动,并修复旧草稿恢复兜底。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
董宏凯andCursor committed 2026-08-28 20:25:21 +08:00
1 parent 832e180c52
commit 4ac7bc21cd
18 files changed
+184 -88

No files matched your search

@@ -615,7 +615,11 @@ export function AgentGallery() {
</TabsContent>
)}
</Tabs>
<AgentGuide stage="list" eligible={isAgentGuideEnvironment()} />
<AgentGuide
stage="list"
eligible={isAgentGuideEnvironment()}
autoStart={false}
/>
</div>
);
}
@@ -1,7 +1,6 @@
"use client";
import { useCallback, useEffect, useRef, useState } from "react";
import { useRouter } from "next/navigation";
import { Guide, type GuideStep } from "tdesign-react";
import { env } from "@/env";
@@ -13,7 +12,8 @@ import { isWujieEnv } from "@/core/embed/wujie";
* 「浏览管理 → 创建与配置 → 发起问答」流程:
*
* - list :智能体列表页(/page/workspace/agents)——分区、搜索排序、卡片操作、
* 创建入口;最后一步「去创建智能体 →」直接跳到创建页衔接下一段。
* 创建入口;完成 / 跳过就地结束,不跳转创建页(创建页有自己的一段
* 引导,点「创建智能体」进入时自动演示)。
* - create :创建页(/page/workspace/agents/new)——对话式创建、基本信息、模型、
* 知识空间、SOUL.md、技能、保存。
* - chat :智能体对话页(/page/workspace/agents/:id/chats/...)——侧栏切换与
@@ -23,6 +23,10 @@ import { isWujieEnv } from "@/core/embed/wujie";
* localStorage 标记,**只有用户显式点「完成」或「跳过」才写入**;中途刷新/离开
* 不记忆,下次进入该页面还会自动演示。各页头部的「使用引导」按钮可随时强制重放。
*
* 自动演示范围:只有 create 段(进入新建页)与 chat 段自动弹;list 段挂在
* 智能体列表页但不自动演示(`autoStart={false}`,避免一进列表页就弹引导),
* 仅由头部「使用引导」按钮手动触发。
*
* 高亮目标通过 `data-agent-guide` 标记在各组件上;启动时按目标是否存在过滤步骤
* (如新用户无智能体卡片、无预设常用问题时自动剔除该步)——Guide 对找不到的
* 目标元素会抛错,不能盲传。
@@ -121,7 +125,7 @@ const LIST_STEPS: StepSpec[] = [
mode: "popup",
placement: "bottom-left",
title: "创建智能体",
body: "准备好了就点这里,开始创建你的专属智能体。点「去创建智能体」直接进入下一站。",
body: "准备好了就点这里,开始创建你的专属智能体;进入创建页后还会有一段配置引导。",
},
];
@@ -260,17 +264,19 @@ function buildSteps(stage: AgentGuideStage): GuideStep[] {
}
/**
* 智能体三段引导的运行组件:`stage` 决定步骤内容,`eligible` 为false 时完全
* 静默(不监听、不自动弹)。挂在三个页面各自的作用域内。
* 智能体三段引导的运行组件:`stage` 决定步骤内容,`eligible` 为 false 时完全
* 静默(不监听、不自动弹)。挂在三个页面各自的作用域内;`autoStart` 为 false
* 时仅响应手动触发(列表页不自动弹,进新建页才自动演示)。
*/
export function AgentGuide({
stage,
eligible,
autoStart = true,
}: {
stage: AgentGuideStage;
eligible: boolean;
autoStart?: boolean;
}) {
const router = useRouter();
const [current, setCurrent] = useState(-1);
const stepsRef = useRef<GuideStep[]>([]);
@@ -285,7 +291,7 @@ export function AgentGuide({
// 「已看过」只由 finish 回调(完成/跳过)写入。
const autoTriedRef = useRef(false);
useEffect(() => {
if (!eligible || autoTriedRef.current) return;
if (!eligible || !autoStart || autoTriedRef.current) return;
autoTriedRef.current = true;
if (isStageFinished(stage)) return;
const timer = setTimeout(start, START_DELAY_MS);
@@ -294,7 +300,7 @@ export function AgentGuide({
// 组件在计时窗口内卸载(用户迅速离开页面)则下次进入再演示。
autoTriedRef.current = false;
};
}, [eligible, stage, start]);
}, [eligible, autoStart, stage, start]);
// 手动触发:页面头部「使用引导」按钮广播事件,忽略已看过记忆强制重放。
useEffect(() => {
@@ -305,15 +311,11 @@ export function AgentGuide({
window.removeEventListener(startEventName(stage), handler);
}, [eligible, stage, start]);
// 完成 / 跳过是用户显式结束引导 —— 只有这两种情况才记忆「已看过」。
// list 阶段的最后一步按钮文案是「去创建智能体 →」,完成即跳转衔接下一段。
// 完成 / 跳过都是用户显式结束引导,就地关闭并记忆「已看过」,不跳转任何页面。
const finish = useCallback(() => {
markStageFinished(stage);
setCurrent(-1);
if (stage === "list") {
router.push("/page/workspace/agents/new");
}
}, [stage, router]);
}, [stage]);
if (!eligible) return null;
@@ -324,9 +326,6 @@ export function AgentGuide({
onChange={setCurrent}
onFinish={finish}
onSkip={finish}
finishButtonProps={
stage === "list" ? { content: "去创建智能体 →" } : undefined
}
/>
);
}
@@ -19,17 +19,16 @@ const TOOLBOX_ICON_TONES = [
* 简洁模式右侧工具箱。
*
* 菜单来源与左侧简洁菜单、设置和更多完全一致,由管理员在菜单管理中把项目
* 放到 ``toolbox`` 泳道。点击右侧竖排按钮后展开单列竖向抽屉。
* 放到 ``toolbox`` 泳道。默认展开常显(不再藏进右侧按钮后面);点 X 或 Esc
* 收起后右侧出现竖排按钮,可再次展开。
*/
export function CompactToolbox() {
const { toolboxItems } = useWorkspaceCompactNavItems();
const { pathname } = useLocation();
const navigate = useNavigate();
const [open, setOpen] = useState(false);
const [open, setOpen] = useState(true);
const triggerRef = useRef<HTMLButtonElement>(null);
const closeRef = useRef<HTMLButtonElement>(null);
useEffect(() => setOpen(false), [pathname]);
useEffect(() => {
if (!open) return;
@@ -41,7 +40,6 @@ export function CompactToolbox() {
};
document.addEventListener("keydown", handleKeyDown);
closeRef.current?.focus();
return () => {
document.removeEventListener("keydown", handleKeyDown);
};
@@ -73,22 +71,8 @@ export function CompactToolbox() {
<span className="text-xs font-semibold leading-4">工具箱</span>
</button>
<button
type="button"
tabIndex={-1}
aria-label="关闭工具箱抽屉"
aria-hidden={!open}
onClick={closeDrawer}
className={cn(
"fixed inset-0 z-[90] bg-black/20 transition-opacity duration-200",
open ? "pointer-events-auto opacity-100" : "pointer-events-none opacity-0",
)}
/>
<aside
id="compact-toolbox-panel"
role="dialog"
aria-modal={open}
aria-label="工具箱"
aria-hidden={!open}
inert={!open}
@@ -102,10 +86,9 @@ export function CompactToolbox() {
<header className="relative flex h-12 shrink-0 items-center justify-center px-2">
<h2 className="text-sm font-semibold">工具箱</h2>
<button
ref={closeRef}
type="button"
aria-label="关闭工具箱"
title="关闭"
aria-label="收起工具箱"
title="收起"
onClick={closeDrawer}
className="absolute right-2 flex size-7 shrink-0 items-center justify-center rounded-lg text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
>
@@ -131,10 +114,7 @@ export function CompactToolbox() {
? "bg-primary/12 text-primary ring-1 ring-inset ring-primary/25"
: "bg-muted/55 text-card-foreground hover:bg-primary/8 hover:text-primary",
)}
onClick={() => {
setOpen(false);
navigate(item.path ?? "/page/workspace/chats/new");
}}
onClick={() => navigate(item.path ?? "/page/workspace/chats/new")}
>
<span
className={cn(
@@ -18,6 +18,7 @@ import {
useResearchSessions,
type DeepResearchSession,
} from "@/strategy-components/api/deep-research";
import { useReportStructures } from "@/strategy-components/api/report-structures";
import { formatRelative } from "@/open-canvas/components/ai-writing/AIWritingHistoryDropdown";
import { desensitize } from "@/lib/desensitize";
@@ -48,6 +49,25 @@ const RESEARCH_STATUS: Record<
cancelled: { label: "已取消", className: "text-muted-foreground" },
};
/**
* 深度研究会话把选中的报告模板快照为 custom_outline;用结构库里的内容反查标题。
* 内容不再存在或用户输入了自定义大纲时,明确展示为「自定义报告结构」,避免冒充模板名。
*/
function reportTemplateLabel(
session: DeepResearchSession,
structures: { title: string; content: string }[],
): string {
const config = session.config ?? {};
const outline = typeof config.custom_outline === "string"
? config.custom_outline.trim()
: typeof config.customOutline === "string"
? config.customOutline.trim()
: "";
if (!outline) return "默认报告结构";
return structures.find((structure) => structure.content.trim() === outline)?.title ?? "自定义报告结构";
}
export function ResearchHistoryTab({
query,
onTotalChange,
@@ -57,6 +77,7 @@ export function ResearchHistoryTab({
}) {
const navigate = useNavigate();
const { data, isLoading } = useResearchSessions(100);
const structuresQuery = useReportStructures();
const actions = useResearchActions();
const [renameId, setRenameId] = useState<string | null>(null);
const [renameTitle, setRenameTitle] = useState("");
@@ -124,6 +145,7 @@ export function ResearchHistoryTab({
className: "text-muted-foreground",
};
const timestamp = session.updatedAt ?? session.createdAt ?? null;
const template = reportTemplateLabel(session, structuresQuery.data ?? []);
return (
<HistoryRow
key={session.id}
@@ -141,6 +163,10 @@ export function ResearchHistoryTab({
<span>{session.sourceCount} 条来源</span>
</>
)}
<span className="text-muted-foreground/60">·</span>
<span className="max-w-56 truncate" title={`模板:${template}`}>
模板:{template}
</span>
{timestamp && (
<>
<span className="text-muted-foreground/60">·</span>
@@ -33,19 +33,36 @@ import {
const ROUNDTABLE_PATH = "/page/workspace/roundtable/planning";
/** 草稿后台作业状态 → 小标签(映射与原侧栏列表一致)。 */
function statusOf(draft: DraftMeta): { label: string; className: string } {
if (draft.status === "running" || draft.status === "queued") {
return { label: "进行中", className: "text-primary" };
const ROUNDTABLE_STEP_LABEL: Record<DraftMeta["furthestStep"], string> = {
1: "议题梳理",
2: "多智能体会商",
3: "报告生成",
};
/**
* 列表接口会返回草稿已推进到的工作台步骤以及最新后台作业状态。两者须同时展示:
* `furthestStep` 告诉用户做到哪一环,`status` 告诉用户是否已真正完成或被中断。
*/
function progressOf(draft: DraftMeta): {
label: string;
className: string;
stepLabel: string;
} {
const stepLabel = `第 ${draft.furthestStep} 步 · ${ROUNDTABLE_STEP_LABEL[draft.furthestStep]}`;
if (draft.status === "done" || (!draft.status && draft.furthestStep === 3)) {
return { label: "已完成", className: "text-emerald-600", stepLabel };
}
if (draft.status === "error") {
return { label: "执行失败", className: "text-rose-500", stepLabel: `停在${stepLabel}` };
}
if (draft.status === "cancelled") {
return { label: "已取消", className: "text-muted-foreground", stepLabel: `停在${stepLabel}` };
}
if (draft.status === "awaiting_input") {
return { label: "待澄清", className: "text-amber-500" };
return { label: "等待确认", className: "text-amber-500", stepLabel: `当前${stepLabel}` };
}
if (draft.status === "error" || draft.status === "cancelled") {
return { label: "出错", className: "text-rose-500" };
}
// 无进行中作业时展示本次会商所用业务链(无链条 → 自由模式)。
return { label: draft.chainTitle || "自由模式", className: "text-muted-foreground" };
return { label: "进行中", className: "text-primary", stepLabel: `当前${stepLabel}` };
}
export function RoundtableHistoryTab({
@@ -122,7 +139,7 @@ export function RoundtableHistoryTab({
<>
<div>
{filtered.map((draft) => {
const status = statusOf(draft);
const progress = progressOf(draft);
return (
<HistoryRow
key={draft.id}
@@ -131,10 +148,18 @@ export function RoundtableHistoryTab({
meta={
<>
<HistoryStatusPill
label={status.label}
className={status.className}
label={progress.label}
className={progress.className}
/>
<span className="text-muted-foreground/60">·</span>
<span className="max-w-48 truncate" title={progress.stepLabel}>
{progress.stepLabel}
</span>
<span className="text-muted-foreground/60">·</span>
<span className="max-w-40 truncate" title={draft.chainTitle || "自由模式"}>
{draft.chainTitle || "自由模式"}
</span>
<span className="text-muted-foreground/60">·</span>
<span>{formatRelative(draft.updatedAt)}</span>
</>
}
@@ -28,8 +28,9 @@ import type { JobStatus } from "@/roundtable-planning/lib/job-types";
export interface DraftStep1Snapshot {
intentThreadId: string | null;
intentReady: IntentSummary | null;
interactiveMessages: unknown[];
timelineEntries: unknown[];
/** 早期草稿可能缺这两个数组字段 —— hydrate 按空数组兜底。 */
interactiveMessages?: unknown[];
timelineEntries?: unknown[];
}
export interface DraftStep2Snapshot {
@@ -9,7 +9,7 @@
* 卡片席位名优先用实时 agent 数据(`listAgents()`,与 `BusinessChainPicker` 一致),
* agent 改名后此弹窗也能同步显示;解析不到才回退到落库快照(`seat.name`)。
*/
import { Fragment, useCallback, useEffect, useMemo, useState } from "react";
import { useCallback, useEffect, useMemo, useState } from "react";
import {
ArrowRightIcon,
ClockIcon,
@@ -593,8 +593,9 @@ function ChainSeatsFlow({
return (
<div className="mt-3.5 flex flex-wrap items-center gap-1.5">
{/* 同 DispatchChainModal:div.contents 代替 <Fragment>,规避 dev tagger 注入 data-lov-id 的警告。 */}
{stages.map((stageIds, stageIdx) => (
<Fragment key={stageIdx}>
<div className="contents" key={stageIdx}>
{stageIdx > 0 && (
<ArrowRightIcon className="size-3.5 shrink-0 text-muted-foreground/50" />
)}
@@ -609,7 +610,7 @@ function ChainSeatsFlow({
) : (
seatTag(stageIds[0])
)}
</Fragment>
</div>
))}
</div>
);
@@ -234,6 +234,16 @@ export function DashboardChatPanel({
}
className="min-h-8 max-h-56 flex-1 resize-none bg-transparent py-1.5 text-xs leading-relaxed text-slate-800 placeholder:text-slate-400 focus:outline-none disabled:cursor-not-allowed disabled:opacity-60"
/>
{busy && (
<button
type="button"
onClick={onPause}
title="中止当前绘制(之后可输入修改要求继续)"
className="mb-0.5 flex h-8 w-8 shrink-0 items-center justify-center rounded-xl bg-amber-100 border border-amber-200/60 text-amber-700 hover:bg-amber-200 transition active:scale-95"
>
<Pause className="h-4 w-4" />
</button>
)}
<button
type="submit"
disabled={inputLocked || text.trim().length === 0}
@@ -14,7 +14,7 @@
* (business-chain.ts)。连线动画 CSS 在 `styles/roundtable-planning.css` 全局段
* (Radix Dialog portal 到 body,不在 .roundtable-planning 作用域内,故用全局类 + 语义 token)。
*/
import { Fragment, useEffect, useRef, useState } from "react";
import { useEffect, useRef, useState } from "react";
import { CheckIcon, ChevronRightIcon, EyeIcon, Loader2Icon, SendIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
@@ -229,9 +229,10 @@ export function DispatchChainModal({
state={coordinatorState}
/>
{/* 席位节点 */}
{/* 席位节点(div.contents 而非 <Fragment>:dev 期 tagger 会往具名 Fragment 注入
data-lov-id 触发 React 警告,contents 不生成盒子、布局不变)。 */}
{nodes.map((node) => (
<Fragment key={node.agentId}>
<div className="contents" key={node.agentId}>
<Connector
active={node.state === "active"}
done={node.state === "done"}
@@ -242,7 +243,7 @@ export function DispatchChainModal({
state={node.state}
order={node.order}
/>
</Fragment>
</div>
))}
{/* 共识里程碑 */}
@@ -221,8 +221,13 @@ export function Step2Panel(props: Step2PanelProps) {
} = props;
const step2ScrollRef = useRef<HTMLDivElement>(null);
// contentRef 与 Step1Panel 同款:消息内容(markdown/代码高亮/折叠卡/write_file 预览)在
// 状态更新之后才继续长高,deps 效果抓不到,靠 ResizeObserver 跟随实际内容高度。
const step2ContentRef = useRef<HTMLDivElement>(null);
const { atBottom: step2AtBottom, scrollToBottom: scrollStep2ToBottom } =
useAutoScroll(step2ScrollRef, [step2RoundtableDialogues]);
useAutoScroll(step2ScrollRef, [step2RoundtableDialogues], {
contentRef: step2ContentRef,
});
// 「回答速度指示器」—— 与主页面共用全局开关 streamSpeedMeterEnabled。
// 度量始终计算(成本低、便于中途开启时也有时长基线),仅展示按开关门控。
@@ -601,14 +606,15 @@ export function Step2Panel(props: Step2PanelProps) {
// 竖滚条滑块时极易误触发横向拖动、抓不住滑块。宽内容(代码块/表格)在各自气泡内
// 已有 max-w-full + overflow-x-auto 自带横滚,故此处裁掉横向不影响查看。
className={cn(
"flex-1 min-h-0 overflow-y-auto overflow-x-hidden p-6 space-y-4",
"flex-1 min-h-0 overflow-y-auto overflow-x-hidden",
useStep1InputAppearance
? "eightbf-roundtable-step2-log"
: "bg-white border-t border-slate-150 border-b border-slate-150 shadow-inner",
)}
>
<div ref={step2ContentRef} className="flex min-h-full flex-col space-y-4 p-6">
{isInitializing && step2RoundtableDialogues.length === 0 && (
<div className="flex h-full flex-col items-center justify-center gap-3 py-12 text-slate-500">
<div className="flex flex-1 flex-col items-center justify-center gap-3 py-12 text-slate-500">
<Loader2 className="w-8 h-8 animate-spin text-primary" />
<div className="text-sm font-medium text-slate-700">正在初始化圆桌...</div>
<div className="text-xs text-slate-400">正在为各席位建立独立线程,请稍候</div>
@@ -704,6 +710,7 @@ export function Step2Panel(props: Step2PanelProps) {
)}
</>
)}
</div>
</div>
{!step2AtBottom && (
<ScrollToBottomButton
@@ -848,6 +855,8 @@ export function Step2Panel(props: Step2PanelProps) {
onAddFiles={addInjectFiles}
onRemoveFile={removeInjectFile}
onSend={handleInject}
busy={isStep2Responding}
onPause={onPauseRoundtable}
hasConsensus={hasConsensus}
onProceed={onProceedToStep3}
/>
@@ -888,6 +897,8 @@ function Step2LegacyComposer({
onAddFiles,
onRemoveFile,
onSend,
busy,
onPause,
hasConsensus,
onProceed,
}: {
@@ -897,6 +908,9 @@ function Step2LegacyComposer({
onAddFiles: (files: File[]) => void;
onRemoveFile: (index: number) => void;
onSend: () => void;
/** 编排中或席位仍在流式输出 —— 此时显示输入框「暂停」键(与第一步输入框一致)。 */
busy: boolean;
onPause: () => void;
hasConsensus: boolean;
onProceed: () => void;
}) {
@@ -958,6 +972,19 @@ function Step2LegacyComposer({
>
<Paperclip className="size-4" />
</button>
{busy && (
<button
type="button"
onClick={onPause}
title="暂停当前会商编排,可稍后恢复"
className="shrink-0 cursor-pointer rounded-xl bg-amber-100 px-3 py-2.5 text-xs font-bold text-amber-800 shadow-sm transition hover:bg-amber-200 active:scale-95"
>
<span className="flex items-center gap-1.5">
<Pause className="size-3" />
暂停
</span>
</button>
)}
<button
type="button"
onClick={onSend}
@@ -388,6 +388,7 @@ export function Step3ReportMode({
error={error}
themeMismatch={themeMismatch}
onRetry={() => redraw()}
onPause={pause}
onRevisionSubmit={interject}
onArtifactStepClick={openArtifactFromStep}
onArtifactPreview={openArtifactPreview}
@@ -513,6 +514,7 @@ function AgentProgressPanel({
error,
themeMismatch,
onRetry,
onPause,
onRevisionSubmit,
onArtifactStepClick,
onArtifactPreview,
@@ -527,6 +529,8 @@ function AgentProgressPanel({
error: string | null;
themeMismatch: boolean;
onRetry: () => void;
/** 输入框「暂停」键 —— 绘制中显示,中断在途请求并把当前气泡收尾为「已暂停」。 */
onPause: () => void;
onRevisionSubmit: (revisionInstruction: string) => void;
onArtifactStepClick: UseStep3ReportReturn["openArtifactFromStep"];
onArtifactPreview: UseStep3ReportReturn["openArtifactPreview"];
@@ -700,6 +704,16 @@ function AgentProgressPanel({
}
className="min-h-8 max-h-56 flex-1 resize-none bg-transparent py-1.5 text-xs leading-relaxed text-slate-800 placeholder:text-slate-400 focus:outline-none disabled:cursor-not-allowed disabled:opacity-60"
/>
{busy && (
<button
type="button"
onClick={onPause}
title="暂停当前绘制(沙箱保留已生成内容,之后可输入修改要求继续)"
className="mb-0.5 flex h-8 w-8 shrink-0 items-center justify-center rounded-xl bg-amber-100 border border-amber-200/60 text-amber-700 hover:bg-amber-200 transition active:scale-95"
>
<Pause className="h-4 w-4" />
</button>
)}
<button
type="submit"
disabled={inputLocked || revisionText.trim().length === 0}
@@ -283,8 +283,8 @@ export function useDraftPersistence(
/** 当前会话是否有值得保存的内容。 */
const hasSavableContent = useCallback((step1: Step1Snapshot, step2: DraftStep2Snapshot): boolean => {
const hasUserMessage = step1.interactiveMessages.some((m) => m.sender === "user");
return hasUserMessage || step2.step2RoundtableDialogues.length > 0;
const hasUserMessage = step1.interactiveMessages?.some((m) => m.sender === "user") ?? false;
return hasUserMessage || (step2.step2RoundtableDialogues?.length ?? 0) > 0;
}, []);
/**
@@ -32,6 +32,7 @@ import {
} from "@/roundtable-planning/components/IntentClarificationCard";
import type { ClarificationPayload } from "@/roundtable-planning/api/multi-agent";
import {
normalizeIntentReadySummary,
nowTime,
splitIntentReadyBlock,
} from "@/roundtable-planning/lib/roundtable-constants";
@@ -82,8 +83,9 @@ export type TimelineEntry = {
export type Step1Snapshot = {
intentThreadId: string | null;
intentReady: IntentSummary | null;
interactiveMessages: InteractiveMessage[];
timelineEntries: TimelineEntry[];
/** 早期草稿可能缺这两个数组字段 —— hydrate 按空数组兜底,避免 undefined 流进渲染层。 */
interactiveMessages?: InteractiveMessage[];
timelineEntries?: TimelineEntry[];
};
export interface UseStep1IntentOptions {
@@ -699,11 +701,14 @@ export function useStep1Intent(options: UseStep1IntentOptions): UseStep1IntentRe
setPendingIntentClarificationMsgId(null);
setIntentClarificationSelections(new Map());
setIntentThreadId(snapshot.intentThreadId);
setIntentReady(snapshot.intentReady);
// 旧草稿存的 summary 可能缺 constraints/assumptions(实时流两条入口都过
// normalizeIntentReadySummary,唯独这里直接落 state)—— 先归一化再恢复,
// 保证下游 IntentSummaryCard 等消费者拿到的一定是数组。
setIntentReady(normalizeIntentReadySummary(snapshot.intentReady));
setIntentInitError(null);
setIntentInitErrorDetail(null);
setInteractiveMessages(snapshot.interactiveMessages);
setTimelineEntries(snapshot.timelineEntries);
setInteractiveMessages(snapshot.interactiveMessages ?? []);
setTimelineEntries(snapshot.timelineEntries ?? []);
step1InitRef.current = true;
// 让任何在途的旧 init 在 resolve 时被识别为过期,丢弃 setState。
step1InitGenRef.current += 1;
@@ -214,7 +214,8 @@ export type Step2Snapshot = {
selectedAgents?: SelectedAgent[];
threadIds: ThreadIdMap | null;
coordinatorName: string | null;
step2RoundtableDialogues: Step2Dialogue[];
/** 早期草稿 / 旧版后台作业写入的 run 可能缺这字段 —— hydrate 按空数组兜底。 */
step2RoundtableDialogues?: Step2Dialogue[];
lastLeaderContent: string;
hasConsensus: boolean;
consensusPercentage: number;
@@ -3408,7 +3409,7 @@ export function useStep2Orchestration(
// 气泡仍显示「调度中 / 等待响应 / 正在输出…」+ 加载光标,让人误以为还在进行(其实早已结束)。
// streaming===true 与「加载中」文案一一对应(finalizeDialogue 收尾时必置 false),故据此即可精确识别。
setStep2RoundtableDialogues(
snapshot.step2RoundtableDialogues.map((d) => {
(snapshot.step2RoundtableDialogues ?? []).map((d) => {
if (!d.streaming) return d;
const hasText = (d.content ?? "").trim().length > 0;
return {
@@ -18,7 +18,7 @@
*
* 详见 `docs/roundtable-dag-orchestration.md` §2(Phase 2,分层编辑器)。
*/
import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useNavigate, useSearchParams } from "react-router-dom";
import { toast } from "sonner";
import { nanoid } from "nanoid";
@@ -1238,7 +1238,8 @@ export default function BusinessChainEditorPage() {
{ n: 1 as const, label: "链条编排" },
{ n: 2 as const, label: "基本信息" },
]).map((s, i) => (
<Fragment key={s.n}>
// div.contents 代替 <Fragment>:dev tagger 注入 data-lov-id 会触发 React 警告。
<div className="contents" key={s.n}>
{i > 0 && <span className="h-px w-10 bg-border" />}
<button
type="button"
@@ -1261,7 +1262,7 @@ export default function BusinessChainEditorPage() {
</span>
{s.label}
</button>
</Fragment>
</div>
))}
</nav>
@@ -22,7 +22,6 @@
* 仍走旧实现,本页不复用它,避免互相牵连。
*/
import {
Fragment,
useCallback,
useEffect,
useMemo,
@@ -853,8 +852,9 @@ function ChainFlow({ chain }: { chain: BusinessChain }) {
style={{ maxHeight: 220 }}
>
<div className="flex min-w-max items-stretch gap-3 py-1">
{/* div.contents 代替 <Fragment>:dev tagger 注入 data-lov-id 会触发 React 警告。 */}
{stages.map((stage, sIdx) => (
<Fragment key={sIdx}>
<div className="contents" key={sIdx}>
<div
className={cn(
"flex shrink-0 flex-col rounded-xl border p-2.5",
@@ -897,7 +897,7 @@ function ChainFlow({ chain }: { chain: BusinessChain }) {
<ArrowRightIcon className="size-4 text-zinc-300 dark:text-[#4A6491]" />
</div>
)}
</Fragment>
</div>
))}
</div>
</div>
@@ -1054,8 +1054,9 @@ function TableFlowSummary({ chain }: { chain: BusinessChain }) {
const { stages, seatById } = useChainStages(chain);
return (
<div className="custom-scrollbar flex max-w-[280px] select-none items-center gap-1.5 overflow-x-auto py-1">
{/* div.contents 代替 <Fragment>:dev tagger 注入 data-lov-id 会触发 React 警告。 */}
{stages.map((stage, sIdx) => (
<Fragment key={sIdx}>
<div className="contents" key={sIdx}>
<div
className={cn(
"flex shrink-0 items-center rounded border px-2 py-1",
@@ -1073,7 +1074,7 @@ function TableFlowSummary({ chain }: { chain: BusinessChain }) {
{sIdx < stages.length - 1 && (
<span className="shrink-0 text-zinc-300 dark:text-[#4A6491]">→</span>
)}
</Fragment>
</div>
))}
</div>
);
@@ -4343,7 +4343,7 @@ function IntentSummaryCard({
)}
{showFullDetail && (
<ul className="text-xs text-slate-700 space-y-2.5 font-normal">
{intentReady.constraints.map((c, i) => (
{(intentReady.constraints ?? []).map((c, i) => (
<li key={`c-${i}`} className="flex items-baseline gap-2 leading-relaxed">
<span className="text-primary text-base leading-none">•</span>
<span>