圆桌 Step1:支持多澄清实时展示与合并提交
流式解析 ask_clarification 逐题渲染澄清卡;草稿恢复 selectedAgents 修复参与角色;Step3 隐藏左侧栏让报告区全宽。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
838cae0d2d
commit
23d6f9efc2
9 files changed
+376
-136
No files matched your search
@@ -184,6 +184,7 @@ hooks/useDraftPersistence → utils/drafts + step1/step2 的 Snapshot 类型
|
||||
- `signal`:必填(页面通过 `intentAborterRef` 控制暂停 / 切步取消)
|
||||
- `onTextDelta(text, messageId)`:每个 AI 文本增量
|
||||
- `onPhaseChange(phase, toolName?)`:`connecting → connected → streaming|tool_calling`
|
||||
- **`onClarification(clarification, index)`(2026-06)**:每解析到一条 `ask_clarification` 工具结果帧就回调一次,让前端**实时**逐个渲染澄清卡。任务理解智能体**一轮可能问多个问题**,这是「实时接收显示协助过程」的关键入口。
|
||||
|
||||
**`StreamIntentResult`**:
|
||||
|
||||
@@ -192,14 +193,16 @@ hooks/useDraftPersistence → utils/drafts + step1/step2 的 Snapshot 类型
|
||||
| `status` | `"asking" \| "clarification" \| "done" \| "error"` | 终态分支 |
|
||||
| `content` | `string` | 完整 AI 文本(含 `[INTENT_READY]` 块) |
|
||||
| `summary` | `IntentSummary \| null` | 仅 `done` 有;前端 fallback 也会从 `content` 里用 `splitIntentReadyBlock` 解 |
|
||||
| `clarification` | `ClarificationPayload \| null` | 仅 `clarification` 有 |
|
||||
| `clarification` | `ClarificationPayload \| null` | **第一个**澄清(向后兼容字段) |
|
||||
| `clarifications` | `ClarificationPayload[]` | **本轮全部澄清问题**(2026-06)。来源是流式 `type:"tool"` 帧而非终态帧——**终态帧只带第 1 个**,多问题必须靠它。每条带 `id`(= `tool_call_id`)供前端按题维护选择。 |
|
||||
|
||||
**SSE 解析要点**:
|
||||
|
||||
- `event: metadata` → phase `connected`
|
||||
- `event: messages` + `extractAiTextDelta` → `onTextDelta`
|
||||
- `event: messages` + `type:"tool"` 且 `name==="ask_clarification"` → `extractClarificationToolMessage` 解出完整澄清(options 已是 `{id,label}`),按 `tool_call_id` 去重后 `onClarification` 实时回调并收进 `result.clarifications`
|
||||
- tool call 帧(`tool_calls` / `tool_call_chunks` 非空)→ phase `tool_calling`(含 `ask_clarification` 工具名)
|
||||
- 终态帧识别:`isFinalIntentFrame`(必须有 `status` + `content`)
|
||||
- 终态帧识别:`isFinalIntentFrame`(必须有 `status` + `content`);`status==="clarification"` 且**未**收到任何流式 tool 澄清帧时,才用终态帧字段兜底合成单条澄清
|
||||
|
||||
**Abort 行为**:
|
||||
|
||||
@@ -436,7 +439,8 @@ isOrchestrating = false
|
||||
> **重大变更**:草稿从浏览器 `localStorage` 迁到**后端 MySQL**,按登录用户隔离、跨设备/会话保留。旧 `utils/drafts.ts` 已删除;`useDraftPersistence` 仍保持对外接口不变(列表项含 `name`,由后端 `title` 映射而来),主页面无需改动。后端表/路由见 backend doc「圆桌草稿持久化」。
|
||||
|
||||
- **API 客户端 `api/drafts.ts`**:`listDrafts()`(轻量元数据)/ `getDraft(id)`(全量 step1+step2)/ `createDraft` / `updateDraft`(partial)/ `deleteDraft` / `deriveDraftName`。snake_case↔camelCase 映射在这一层收口(后端 `furthest_step/created_at/updated_at` ↔ 前端 `furthestStep/...`)。
|
||||
- **快照形状不变**:`DraftStep1Snapshot.{intentThreadId, intentReady, interactiveMessages, timelineEntries}`、`DraftStep2Snapshot.{threadIds, coordinatorName, step2RoundtableDialogues, lastLeaderContent, hasConsensus, consensusPercentage, budgetLimit, seatStubMessages}`;以 JSON 整存进后端 `step1`/`step2`(`PortableLongText`/LONGTEXT,避免大对话超 64KB)。
|
||||
- **快照形状**:`DraftStep1Snapshot.{intentThreadId, intentReady, interactiveMessages, timelineEntries}`、`DraftStep2Snapshot.{selectedAgents, threadIds, coordinatorName, step2RoundtableDialogues, lastLeaderContent, hasConsensus, consensusPercentage, budgetLimit, seatStubMessages}`;以 JSON 整存进后端 `step1`/`step2`(`PortableLongText`/LONGTEXT,避免大对话超 64KB)。
|
||||
- **`selectedAgents`(2026-06 修复)**:本次研讨实际选中的席位。**不存的话加载草稿后 Step 2/3 左侧「参与角色」会回退到 `DEFAULT_SELECTED_AGENTS`(默认 6 个)而非当时真正选的那几个** —— `roleTemplates`/`agentNameToRole` 都由 `selectedAgents` 派生。`useStep2Orchestration.hydrateFromDraft` 在该字段非空时 `setSelectedAgents`,老草稿(无此字段)保持当前值不强行覆盖。
|
||||
- `deriveDraftName({intentReady, interactiveMessages})`:优先 `intentReady.objective`,否则第一个 user 消息,否则 `未命名草稿 <时间>`,截断 40 字。**仅在 create 时写一次**(见下方 bug 修复)。
|
||||
- **列表加载**:挂载时 `useEffect` 调 `listDrafts()`;增删改后 `refreshList()` 重拉。
|
||||
- **自动保存策略**:仅在「前进」步骤切换时保存(1→2、2→3),后退不触发。
|
||||
@@ -460,13 +464,20 @@ isOrchestrating = false
|
||||
|
||||
### 6.1 `IntentClarificationCard`
|
||||
|
||||
- Props:`payload`(`ClarificationPayload`)、`selection`、`onSubmit(overrideSelection?)`、`disabled`、`submitting`、`fallbackContent`
|
||||
- Props:`payload`(`ClarificationPayload`)、`selection`、`onSubmit(overrideSelection?)`、`disabled`、`submitting`、`fallbackContent`、**`deferSubmit?`(2026-06)**
|
||||
- **单选**(默认 `allowMultiple === false`):`resolveAllowMultiple` 返回 false → 点击选项即 `onSubmit({selectedIds:[id], custom: ""})`,UI 不显示「发送回答」按钮(除非 `allowCustom`)
|
||||
- **多选**:勾选多个 + 输入补充,再点「发送回答」;答案格式见 `formatIntentClarificationAnswer`:
|
||||
- 仅选项 → `我选择:A、B`
|
||||
- 仅自定义 → 直接发自定义文本
|
||||
- 同时存在 → `我选择:A、B;补充:…`
|
||||
- 展示条件:`isPendingClarification && !intentReady && !parts.summary && pendingIntentClarificationMsgId === msg.id`(已出现 `[INTENT_READY]` 则隐藏可操作卡片)
|
||||
- **单问题 vs 多问题的判断(2026-06)**:`Step1Message` 按 `clarifications.length` 分流,**默认单选**(后端 `allow_multiple` 缺省 false,`resolveAllowMultiple` 兜底):
|
||||
- **单问题(最常见)**:`deferSubmit={false}`,沿用原交互 —— 单选**点击选项即发送**;多选 / 允许自定义则走卡片自带的「发送回答」按钮。**不**显示合并按钮。
|
||||
- **多问题**:`deferSubmit={true}` —— 连单选点击也只**记录**选择(不立即发送),卡片自身隐藏按钮;父组件在卡片下方渲染**一个**「发送全部回答」按钮,统一提交。
|
||||
- 该判断纯在前端(不动工具/后端):依据 `clarifications.length` + `allow_multiple`,后端 `ask_clarification` 本就默认单选。
|
||||
- **合并提交逻辑**:`submitIntentClarifications(msgId, clarifications, overrideByKey?)` 逐题取选择、格式化、拼成一条消息(多题带 `1. <问题>\n答:<答案>` 编号;单题直接发答案;至少答一题才发)。`overrideByKey` 供「单题单选点击即发送」用——此时选择还没写进 state map(setState 异步),把即时选择按 `clarificationKey` 传进去直接用。
|
||||
- **逐题选择隔离**:选择 Map 改为 `Map<string, IntentClarificationSelection>`,key 由 `clarificationKey(msgId, clar.id, index)` 生成(`clar.id` = `tool_call_id`,缺省退化为 `idx-N`)。
|
||||
- 展示条件:`isAgent && clarifications.length>0 && !intentReady && !parts.summary`;**流式期也展示**(卡片 `disabled`,只读,体现「协助过程」实时出现),仅当终态且 `pendingIntentClarificationMsgId === msg.id && !streaming` 时才可交互。出现 `[INTENT_READY]` / summary 后隐藏。
|
||||
- **步骤链强化**:`onClarification` 同时往 `msg.steps` 追加一条 `追问 N:<问题>`,让 `MessageStepsCard` 把每个 `ask_clarification` 调用显示成独立步骤。
|
||||
|
||||
### 6.2 `RecommendAgentsDialog`
|
||||
|
||||
@@ -502,6 +513,7 @@ isOrchestrating = false
|
||||
- 各席位最终交付(中段折叠区)数据源:父组件传入的 `seatDeliveries`,按发送者去重(后到的覆盖前者)
|
||||
- 「重新起草」→ 父组件 `handleStartNewTask`(清空并 `runStep1Init`)
|
||||
- 「打印 / 另存 PDF」→ `window.print()`,浏览器原生流程
|
||||
- **Step 3 不显示左侧栏(2026-06)**:`LeftSidebar` 在 `currentStep === 3` 时 `return null`(原「参与与结论贡献」列已移除),右侧栏在 Step !== 1 时本就 `return null`,因此中间 `middle-viewport`(`flex-1`)会自适应撑满整行,报告区获得最大宽度。
|
||||
|
||||
### 6.4 `ArtifactPreviewModal`
|
||||
|
||||
|
||||
@@ -30,6 +30,12 @@ export interface DraftStep1Snapshot {
|
||||
}
|
||||
|
||||
export interface DraftStep2Snapshot {
|
||||
/**
|
||||
* The seats actually selected for this session. Restored on load so the
|
||||
* Step 2/3 「参与角色」panel shows the chosen agents instead of the default 6.
|
||||
* Optional — drafts saved before 2026-06 won't carry it.
|
||||
*/
|
||||
selectedAgents?: Array<{ agent_id: string; name: string }>;
|
||||
threadIds: ThreadIdMap | null;
|
||||
coordinatorName: string | null;
|
||||
step2RoundtableDialogues: unknown[];
|
||||
|
||||
@@ -160,6 +160,14 @@ export interface StreamIntentRequest {
|
||||
toolName?: string,
|
||||
args?: Record<string, unknown>,
|
||||
) => void;
|
||||
/**
|
||||
* Fires for every `ask_clarification` tool message parsed out of the live
|
||||
* stream (the intent agent may emit several in a single turn). Lets the UI
|
||||
* surface each assistance question in real-time instead of waiting for the
|
||||
* terminal frame — which only carries the *first* question. `index` is the
|
||||
* 0-based arrival order within this turn.
|
||||
*/
|
||||
onClarification?: (clarification: ClarificationPayload, index: number) => void;
|
||||
}
|
||||
|
||||
export interface StreamIntentResult {
|
||||
@@ -172,8 +180,16 @@ export interface StreamIntentResult {
|
||||
/**
|
||||
* Populated when the intent agent called ask_clarification — render option
|
||||
* cards + custom input instead of treating the turn as plain text only.
|
||||
* This is the *first* clarification (kept for backward compat); prefer
|
||||
* `clarifications` which carries the full set.
|
||||
*/
|
||||
clarification: ClarificationPayload | null;
|
||||
/**
|
||||
* All `ask_clarification` questions the agent asked this turn, in arrival
|
||||
* order. Collected from the live `type:"tool"` frames (the terminal frame
|
||||
* only carries the first). Empty unless status === "clarification".
|
||||
*/
|
||||
clarifications: ClarificationPayload[];
|
||||
/**
|
||||
* Populated only when status === "error" and the backend emitted a
|
||||
* structured diagnostic (new in `_diagnose_exception`). UI uses this to show
|
||||
@@ -241,6 +257,54 @@ function extractAiTextDelta(parsed: unknown): { text: string; id: string } | nul
|
||||
return { text, id };
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a live `type:"tool"` `ask_clarification` frame into a ClarificationPayload.
|
||||
*
|
||||
* The intent agent emits one such frame per question (ClarificationMiddleware
|
||||
* formats them), each carrying a fully-formed clarification in
|
||||
* `additional_kwargs.clarification` — options already normalized to
|
||||
* `{id, label}` objects, snake_case keys. Returns null for any other frame
|
||||
* (AI text chunks, tool_call_chunks, value snapshots).
|
||||
*/
|
||||
function extractClarificationToolMessage(
|
||||
parsed: unknown,
|
||||
): { payload: ClarificationPayload; toolCallId: string } | null {
|
||||
if (!Array.isArray(parsed) || parsed.length < 1) return null;
|
||||
const msg = parsed[0];
|
||||
if (!msg || typeof msg !== "object") return null;
|
||||
const obj = msg as Record<string, unknown>;
|
||||
if (String(obj.type ?? "").toLowerCase() !== "tool") return null;
|
||||
if (obj.name !== "ask_clarification") return null;
|
||||
const kwargs = obj.additional_kwargs;
|
||||
if (!kwargs || typeof kwargs !== "object") return null;
|
||||
const clar = (kwargs as Record<string, unknown>).clarification;
|
||||
if (!clar || typeof clar !== "object") return null;
|
||||
const c = clar as Record<string, unknown>;
|
||||
const toolCallId =
|
||||
typeof obj.tool_call_id === "string" && obj.tool_call_id
|
||||
? obj.tool_call_id
|
||||
: typeof obj.id === "string"
|
||||
? obj.id
|
||||
: "";
|
||||
const clarificationType =
|
||||
typeof c.clarification_type === "string" ? c.clarification_type : "";
|
||||
return {
|
||||
toolCallId,
|
||||
payload: {
|
||||
id: toolCallId || undefined,
|
||||
question: typeof c.question === "string" ? c.question : "",
|
||||
clarificationType,
|
||||
context: typeof c.context === "string" ? c.context : "",
|
||||
options: Array.isArray(c.options) ? c.options : [],
|
||||
allowCustom: c.allow_custom !== false,
|
||||
allowMultiple: resolveAllowMultiple(
|
||||
typeof c.allow_multiple === "boolean" ? c.allow_multiple : undefined,
|
||||
clarificationType,
|
||||
),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Open an SSE connection to /api/intent/stream and resolve when the upstream
|
||||
* stream completes. Each call is one user turn; reuse the same threadId
|
||||
@@ -288,9 +352,24 @@ export async function streamIntent(req: StreamIntentRequest): Promise<StreamInte
|
||||
content: "",
|
||||
summary: null,
|
||||
clarification: null,
|
||||
clarifications: [],
|
||||
errorDetail: null,
|
||||
};
|
||||
|
||||
// Clarifications collected live from `type:"tool"` frames, deduped by
|
||||
// tool_call_id. The terminal frame only carries the first question, so this
|
||||
// is the source of truth for multi-question turns.
|
||||
const seenClarificationIds = new Set<string>();
|
||||
const collectClarification = (payload: ClarificationPayload, toolCallId: string) => {
|
||||
const dedupeKey = toolCallId || `idx-${result.clarifications.length}`;
|
||||
if (seenClarificationIds.has(dedupeKey)) return;
|
||||
seenClarificationIds.add(dedupeKey);
|
||||
const index = result.clarifications.length;
|
||||
result.clarifications.push(payload);
|
||||
if (!result.clarification) result.clarification = payload;
|
||||
req.onClarification?.(payload, index);
|
||||
};
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
@@ -331,10 +410,15 @@ export async function streamIntent(req: StreamIntentRequest): Promise<StreamInte
|
||||
if (parsed.status === "error") {
|
||||
result.errorDetail = parseIntentErrorDetail(parsed.error_detail);
|
||||
}
|
||||
if (parsed.status === "clarification") {
|
||||
if (parsed.status === "clarification" && result.clarifications.length === 0) {
|
||||
// Fallback path: no live `type:"tool"` clarification frames were seen
|
||||
// (older backend, or single-question turn) — synthesize one from the
|
||||
// terminal frame so the UI still gets a card. When tool frames *were*
|
||||
// seen, `clarifications` already holds the full (possibly multi-question)
|
||||
// set and we keep it.
|
||||
const clarificationType =
|
||||
typeof parsed.clarification_type === "string" ? parsed.clarification_type : "";
|
||||
result.clarification = {
|
||||
const fallback: ClarificationPayload = {
|
||||
question: typeof parsed.question === "string" ? parsed.question : "",
|
||||
clarificationType,
|
||||
context:
|
||||
@@ -346,11 +430,22 @@ export async function streamIntent(req: StreamIntentRequest): Promise<StreamInte
|
||||
clarificationType,
|
||||
),
|
||||
};
|
||||
result.clarification = fallback;
|
||||
result.clarifications = [fallback];
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (lastEvent === "messages") {
|
||||
// `ask_clarification` tool result frame → collect + surface in real-time.
|
||||
// These carry the fully-formed clarification (one per question); the
|
||||
// agent may emit several in a single turn.
|
||||
const toolClar = extractClarificationToolMessage(parsed);
|
||||
if (toolClar) {
|
||||
collectClarification(toolClar.payload, toolClar.toolCallId);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Phase inference from LangGraph messages-tuple frames. Tool calls
|
||||
// (typically ask_clarification) flip phase to tool_calling; AI text
|
||||
// chunks flip to streaming. Everything else leaves phase alone.
|
||||
|
||||
@@ -227,6 +227,13 @@ export interface StreamMultiAgentRequest {
|
||||
}
|
||||
|
||||
export interface ClarificationPayload {
|
||||
/**
|
||||
* Stable id for this clarification — the originating `ask_clarification`
|
||||
* tool_call_id. Lets the UI key per-question selections when the agent asks
|
||||
* several clarifications in one turn (Step 1 multi-question support).
|
||||
* Omitted on the Step 2 leader path (single clarification per turn).
|
||||
*/
|
||||
id?: string;
|
||||
/** Raw question text from the model's ask_clarification args. */
|
||||
question: string;
|
||||
/** Optional category hint from the model (e.g. "missing_info"). */
|
||||
|
||||
@@ -12,6 +12,20 @@ export type IntentClarificationSelection = {
|
||||
custom: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Composite selection-map key for one clarification question. When the intent
|
||||
* agent asks several questions in one turn, each needs its own selection state,
|
||||
* so we key by `${msgId}::${clarificationId}` (falling back to the arrival
|
||||
* index when the payload has no id). The map itself lives in `useStep1Intent`.
|
||||
*/
|
||||
export function clarificationKey(
|
||||
msgId: number,
|
||||
clarId: string | undefined,
|
||||
index: number,
|
||||
): string {
|
||||
return `${msgId}::${clarId ?? `idx-${index}`}`;
|
||||
}
|
||||
|
||||
type ClarificationOption = {
|
||||
id: string;
|
||||
label: string;
|
||||
@@ -72,6 +86,13 @@ interface IntentClarificationCardProps {
|
||||
submitting?: boolean;
|
||||
/** Multi-select: submit current selection. Single-select: also used for custom text. */
|
||||
onSubmit: (overrideSelection?: IntentClarificationSelection) => void;
|
||||
/**
|
||||
* Defer submission: even single-select option clicks only *record* the
|
||||
* selection (via onSelectionChange) instead of sending immediately, and the
|
||||
* card hides its own submit button. Used when several questions are answered
|
||||
* together and the parent owns one combined submit button.
|
||||
*/
|
||||
deferSubmit?: boolean;
|
||||
}
|
||||
|
||||
export function IntentClarificationCard({
|
||||
@@ -82,6 +103,7 @@ export function IntentClarificationCard({
|
||||
disabled = false,
|
||||
submitting = false,
|
||||
onSubmit,
|
||||
deferSubmit = false,
|
||||
}: IntentClarificationCardProps) {
|
||||
const options = useMemo(
|
||||
() => normalizeOptions(Array.isArray(payload.options) ? payload.options : []),
|
||||
@@ -97,11 +119,18 @@ export function IntentClarificationCard({
|
||||
return selection.selectedIds.length > 0 || selection.custom.trim().length > 0;
|
||||
}, [selection.custom, selection.selectedIds]);
|
||||
|
||||
const showSubmitButton = allowMultiple || payload.allowCustom !== false;
|
||||
// In defer mode the parent owns the (combined) submit button.
|
||||
const showSubmitButton = !deferSubmit && (allowMultiple || payload.allowCustom !== false);
|
||||
|
||||
function handleOptionClick(optionId: string) {
|
||||
if (disabled || submitting) return;
|
||||
if (!allowMultiple) {
|
||||
if (deferSubmit) {
|
||||
// Single-select but combined submit: just record the choice (replace),
|
||||
// keep any custom text the user already typed.
|
||||
onSelectionChange({ ...selection, selectedIds: [optionId] });
|
||||
return;
|
||||
}
|
||||
onSubmit({ selectedIds: [optionId], custom: "" });
|
||||
return;
|
||||
}
|
||||
@@ -144,6 +173,10 @@ export function IntentClarificationCard({
|
||||
</div>
|
||||
{options.map((option) => {
|
||||
const isSelected = selection.selectedIds.includes(option.id);
|
||||
// Highlight selected options whenever the choice is "sticky" — i.e.
|
||||
// multi-select, or single-select in defer mode (where a click records
|
||||
// rather than immediately sends).
|
||||
const showSelected = isSelected && (allowMultiple || deferSubmit);
|
||||
return (
|
||||
<button
|
||||
key={option.id}
|
||||
@@ -152,10 +185,10 @@ export function IntentClarificationCard({
|
||||
onClick={() => handleOptionClick(option.id)}
|
||||
className={cn(
|
||||
"flex w-full items-start justify-between gap-2 rounded-xl border px-3 py-2 text-left transition",
|
||||
isSelected && allowMultiple
|
||||
showSelected
|
||||
? "border-primary bg-primary text-white shadow-sm"
|
||||
: "border-slate-200 bg-slate-50/80 text-slate-800 hover:border-primary/40 hover:bg-white",
|
||||
!allowMultiple && !disabled && !submitting && "active:scale-[0.99]",
|
||||
!allowMultiple && !deferSubmit && !disabled && !submitting && "active:scale-[0.99]",
|
||||
(disabled || submitting) && "cursor-not-allowed opacity-60",
|
||||
)}
|
||||
>
|
||||
@@ -165,14 +198,14 @@ export function IntentClarificationCard({
|
||||
<span
|
||||
className={cn(
|
||||
"text-[10px] leading-snug",
|
||||
isSelected && allowMultiple ? "text-white/80" : "text-slate-500",
|
||||
showSelected ? "text-white/80" : "text-slate-500",
|
||||
)}
|
||||
>
|
||||
{option.description}
|
||||
</span>
|
||||
) : null}
|
||||
</span>
|
||||
{isSelected && allowMultiple ? (
|
||||
{showSelected ? (
|
||||
<Check className="mt-0.5 h-3.5 w-3.5 shrink-0" />
|
||||
) : null}
|
||||
</button>
|
||||
|
||||
@@ -25,8 +25,10 @@ import {
|
||||
X,
|
||||
} from "lucide-react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Man1Avatar, RobotAvatar } from "@/roundtable-planning/components/Avatars";
|
||||
import {
|
||||
clarificationKey,
|
||||
IntentClarificationCard,
|
||||
type IntentClarificationSelection,
|
||||
} from "@/roundtable-planning/components/IntentClarificationCard";
|
||||
@@ -55,10 +57,10 @@ export interface Step1PanelProps {
|
||||
intentReady: object | null;
|
||||
isIntentSending: boolean;
|
||||
pendingIntentClarificationMsgId: number | null;
|
||||
intentClarificationSelections: Map<number, IntentClarificationSelection>;
|
||||
intentClarificationSelections: Map<string, IntentClarificationSelection>;
|
||||
// ─── Setters / Handlers ──────────────────────────────────────
|
||||
setIntentClarificationSelections: React.Dispatch<
|
||||
React.SetStateAction<Map<number, IntentClarificationSelection>>
|
||||
React.SetStateAction<Map<string, IntentClarificationSelection>>
|
||||
>;
|
||||
onSendMessage: (text: string) => void;
|
||||
onStopIntent: () => void;
|
||||
@@ -67,10 +69,10 @@ export interface Step1PanelProps {
|
||||
onEnterRoundtable: () => void;
|
||||
onManualSave: () => void;
|
||||
onTriggerToast: (msg: string) => void;
|
||||
submitIntentClarification: (
|
||||
submitIntentClarifications: (
|
||||
msgId: number,
|
||||
payload: ClarificationPayload,
|
||||
overrideSelection?: IntentClarificationSelection,
|
||||
clarifications: ClarificationPayload[],
|
||||
overrideByKey?: Record<string, IntentClarificationSelection>,
|
||||
) => void;
|
||||
}
|
||||
|
||||
@@ -90,7 +92,7 @@ export function Step1Panel(props: Step1PanelProps) {
|
||||
onEnterRoundtable,
|
||||
onManualSave,
|
||||
onTriggerToast,
|
||||
submitIntentClarification,
|
||||
submitIntentClarifications,
|
||||
} = props;
|
||||
|
||||
const step1ScrollRef = useRef<HTMLDivElement>(null);
|
||||
@@ -138,7 +140,7 @@ export function Step1Panel(props: Step1PanelProps) {
|
||||
intentClarificationSelections={intentClarificationSelections}
|
||||
setIntentClarificationSelections={setIntentClarificationSelections}
|
||||
isIntentSending={isIntentSending}
|
||||
submitIntentClarification={submitIntentClarification}
|
||||
submitIntentClarifications={submitIntentClarifications}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -266,15 +268,15 @@ interface Step1MessageProps {
|
||||
msg: InteractiveMessage;
|
||||
intentReady: object | null;
|
||||
pendingIntentClarificationMsgId: number | null;
|
||||
intentClarificationSelections: Map<number, IntentClarificationSelection>;
|
||||
intentClarificationSelections: Map<string, IntentClarificationSelection>;
|
||||
setIntentClarificationSelections: React.Dispatch<
|
||||
React.SetStateAction<Map<number, IntentClarificationSelection>>
|
||||
React.SetStateAction<Map<string, IntentClarificationSelection>>
|
||||
>;
|
||||
isIntentSending: boolean;
|
||||
submitIntentClarification: (
|
||||
submitIntentClarifications: (
|
||||
msgId: number,
|
||||
payload: ClarificationPayload,
|
||||
overrideSelection?: IntentClarificationSelection,
|
||||
clarifications: ClarificationPayload[],
|
||||
overrideByKey?: Record<string, IntentClarificationSelection>,
|
||||
) => void;
|
||||
}
|
||||
|
||||
@@ -285,7 +287,7 @@ function Step1Message({
|
||||
intentClarificationSelections,
|
||||
setIntentClarificationSelections,
|
||||
isIntentSending,
|
||||
submitIntentClarification,
|
||||
submitIntentClarifications,
|
||||
}: Step1MessageProps) {
|
||||
const isStreaming = msg.streaming === true;
|
||||
const isAgent = msg.sender !== "user";
|
||||
@@ -297,24 +299,23 @@ function Step1Message({
|
||||
? splitInlineReasoning(rawContent)
|
||||
: { content: rawContent, reasoning: null };
|
||||
const hasContent = thoughtFreeContent.length > 0;
|
||||
const clarification = msg.clarification;
|
||||
// 一轮可能有多条澄清问题(任务理解智能体一次问多个)。流式期就逐个灌进来,
|
||||
// 实时展示「协助过程」;终态再以权威集合覆盖。
|
||||
const clarifications = msg.clarifications ?? [];
|
||||
const parts = isAgent
|
||||
? splitIntentReadyBlock(thoughtFreeContent)
|
||||
: { before: thoughtFreeContent, summary: null as IntentSummary | null, after: "" };
|
||||
const isPendingClarification =
|
||||
isAgent &&
|
||||
clarification &&
|
||||
!intentReady &&
|
||||
!parts.summary &&
|
||||
pendingIntentClarificationMsgId === msg.id;
|
||||
const clarificationSelection =
|
||||
intentClarificationSelections.get(msg.id) ?? {
|
||||
selectedIds: [],
|
||||
custom: "",
|
||||
};
|
||||
// 是否展示澄清卡(还没出 summary / 还没整体就绪时)。流式中也展示(只读),
|
||||
// 终态且该消息为当前待澄清条目时才可交互。
|
||||
const showClarifications =
|
||||
isAgent && clarifications.length > 0 && !intentReady && !parts.summary;
|
||||
// 一轮多个问题才进入「逐题作答 + 合并发送」模式;单问题沿用「点击即发送」。
|
||||
const multiQuestion = clarifications.length > 1;
|
||||
const isPending = pendingIntentClarificationMsgId === msg.id;
|
||||
const clarificationsInteractive = isPending && !isStreaming;
|
||||
const introText =
|
||||
clarification &&
|
||||
parts.before.trim() === (clarification.question || "").trim()
|
||||
clarifications.length === 1 &&
|
||||
parts.before.trim() === (clarifications[0].question || "").trim()
|
||||
? ""
|
||||
: parts.before;
|
||||
|
||||
@@ -361,24 +362,63 @@ function Step1Message({
|
||||
把它丢弃,让用户看到的就是"复述 + 摘要卡",干净结束。
|
||||
*/}
|
||||
{isStreaming && <StreamingCursor />}
|
||||
{isPendingClarification && clarification && (
|
||||
<IntentClarificationCard
|
||||
payload={clarification}
|
||||
fallbackContent={parts.before || undefined}
|
||||
selection={clarificationSelection}
|
||||
onSelectionChange={(next) => {
|
||||
setIntentClarificationSelections((prev) => {
|
||||
const map = new Map(prev);
|
||||
map.set(msg.id, next);
|
||||
return map;
|
||||
});
|
||||
}}
|
||||
disabled={!isPendingClarification}
|
||||
submitting={isIntentSending && Boolean(isPendingClarification)}
|
||||
onSubmit={(override) =>
|
||||
submitIntentClarification(msg.id, clarification, override)
|
||||
}
|
||||
/>
|
||||
{showClarifications && (
|
||||
<div className="space-y-2">
|
||||
{clarifications.map((clar, index) => {
|
||||
const key = clarificationKey(msg.id, clar.id, index);
|
||||
const selection =
|
||||
intentClarificationSelections.get(key) ?? {
|
||||
selectedIds: [],
|
||||
custom: "",
|
||||
};
|
||||
return (
|
||||
<IntentClarificationCard
|
||||
key={key}
|
||||
payload={clar}
|
||||
fallbackContent={clar.question || parts.before || undefined}
|
||||
selection={selection}
|
||||
// 单问题:沿用原交互(单选点击即发送 / 多选 / 自定义走卡片自己的按钮)。
|
||||
// 多问题:才 defer —— 逐题只记录选择,由下方一个合并按钮统一发送。
|
||||
deferSubmit={multiQuestion}
|
||||
onSelectionChange={(next) => {
|
||||
setIntentClarificationSelections((prev) => {
|
||||
const map = new Map(prev);
|
||||
map.set(key, next);
|
||||
return map;
|
||||
});
|
||||
}}
|
||||
disabled={!clarificationsInteractive}
|
||||
submitting={isIntentSending && isPending}
|
||||
onSubmit={(override) =>
|
||||
submitIntentClarifications(
|
||||
msg.id,
|
||||
clarifications,
|
||||
// 单题单选「点击即发送」时选择还没进 state map,把即时值
|
||||
// 按 key 传进去直接用。
|
||||
override ? { [key]: override } : undefined,
|
||||
)
|
||||
}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{/* 合并提交按钮仅多问题时出现;单问题点选项即发送,无需此按钮。 */}
|
||||
{multiQuestion && clarificationsInteractive && (
|
||||
<div className="flex items-center justify-between gap-2 pt-0.5">
|
||||
<span className="text-[10px] text-slate-400">
|
||||
共 {clarifications.length} 个问题,可逐个作答后一并发送
|
||||
</span>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
disabled={isIntentSending}
|
||||
onClick={() => submitIntentClarifications(msg.id, clarifications)}
|
||||
className="h-8 rounded-xl px-4 text-xs font-bold"
|
||||
>
|
||||
{isIntentSending ? "发送中…" : "发送全部回答"}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
|
||||
@@ -22,7 +22,11 @@ import {
|
||||
type IntentStreamPhase,
|
||||
type IntentSummary,
|
||||
} from "@/roundtable-planning/api/intent";
|
||||
import { formatIntentClarificationAnswer, type IntentClarificationSelection } from "@/roundtable-planning/components/IntentClarificationCard";
|
||||
import {
|
||||
clarificationKey,
|
||||
formatIntentClarificationAnswer,
|
||||
type IntentClarificationSelection,
|
||||
} from "@/roundtable-planning/components/IntentClarificationCard";
|
||||
import type { ClarificationPayload } from "@/roundtable-planning/api/multi-agent";
|
||||
import {
|
||||
nowTime,
|
||||
@@ -38,7 +42,15 @@ export type InteractiveMessage = {
|
||||
time: string;
|
||||
streaming?: boolean;
|
||||
phase?: string;
|
||||
/** First clarification (back-compat); prefer `clarifications`. */
|
||||
clarification?: ClarificationPayload;
|
||||
/**
|
||||
* All `ask_clarification` questions the agent asked this turn (the agent may
|
||||
* ask several at once). Populated live from the stream so each question card
|
||||
* appears in real-time. Each carries a stable `id` (tool_call_id) for keying
|
||||
* per-question selections.
|
||||
*/
|
||||
clarifications?: ClarificationPayload[];
|
||||
/**
|
||||
* ChainOfThought 折叠卡的步骤累积。每次 `onPhaseChange` 都会把当前 phase + toolName
|
||||
* 拼成 `phase:toolName` 作为 key,与 Step 2 的 `Step2Dialogue.steps` 同形。
|
||||
@@ -86,23 +98,30 @@ export interface UseStep1IntentReturn {
|
||||
intentInitErrorDetail: IntentErrorDetail | null;
|
||||
isIntentSending: boolean;
|
||||
pendingIntentClarificationMsgId: number | null;
|
||||
intentClarificationSelections: Map<number, IntentClarificationSelection>;
|
||||
/** Per-question selections, keyed by `clarificationKey(msgId, clarId, index)`. */
|
||||
intentClarificationSelections: Map<string, IntentClarificationSelection>;
|
||||
interactiveMessages: InteractiveMessage[];
|
||||
timelineEntries: TimelineEntry[];
|
||||
// ─── Mutators ────────────────────────────────────────────────────────
|
||||
setIntentClarificationSelections: React.Dispatch<
|
||||
React.SetStateAction<Map<number, IntentClarificationSelection>>
|
||||
React.SetStateAction<Map<string, IntentClarificationSelection>>
|
||||
>;
|
||||
// ─── Handlers ────────────────────────────────────────────────────────
|
||||
/** 清空当前 Step 1 状态并重新拉取 intent thread。 */
|
||||
runStep1Init: () => void;
|
||||
/** 把 `userText` 作为用户气泡发给 intent agent,流式渲染回复。 */
|
||||
sendIntentMessage: (userText: string) => Promise<void>;
|
||||
/** 提交澄清卡片的选项 / 自定义回答。 */
|
||||
submitIntentClarification: (
|
||||
/**
|
||||
* 提交某条 agent 消息的全部澄清问题答案 —— 逐题取各自选择,拼成一条合并消息
|
||||
* 发回。至少答一题才会发送。单题时退化为发送该题答案。
|
||||
*
|
||||
* `overrideByKey`:单题「点击即发送」用 —— 单选卡点击选项时选择还没写进 state
|
||||
* map(setState 异步),把该题的即时选择按 `clarificationKey` 传进来直接用。
|
||||
*/
|
||||
submitIntentClarifications: (
|
||||
msgId: number,
|
||||
payload: ClarificationPayload,
|
||||
overrideSelection?: IntentClarificationSelection,
|
||||
clarifications: ClarificationPayload[],
|
||||
overrideByKey?: Record<string, IntentClarificationSelection>,
|
||||
) => void;
|
||||
/** Abort 当前 intent 流。气泡尾部追加「(已暂停)」并 toast。 */
|
||||
stopIntentStream: () => void;
|
||||
@@ -143,7 +162,7 @@ export function useStep1Intent(options: UseStep1IntentOptions): UseStep1IntentRe
|
||||
const [pendingIntentClarificationMsgId, setPendingIntentClarificationMsgId] =
|
||||
useState<number | null>(null);
|
||||
const [intentClarificationSelections, setIntentClarificationSelections] = useState<
|
||||
Map<number, IntentClarificationSelection>
|
||||
Map<string, IntentClarificationSelection>
|
||||
>(new Map());
|
||||
|
||||
// 原始页面带了一段写死的 demo 对话,只是首次进入 Step 1 还没初始化前的占位;
|
||||
@@ -355,10 +374,53 @@ export function useStep1Intent(options: UseStep1IntentOptions): UseStep1IntentRe
|
||||
}),
|
||||
);
|
||||
},
|
||||
onClarification: (clarification, index) => {
|
||||
// 实时把每条澄清问题灌进气泡 —— 用户能边流边看到「协助过程」逐个出现,
|
||||
// 而不是等终态帧(终态帧只带第 1 个问题)。同时往步骤链补一条
|
||||
// 「追问 N:<问题>」,与「强化步骤链」诉求对齐。
|
||||
const clarId = clarification.id ?? `idx-${index}`;
|
||||
const stepKey = `clarification:${clarId}`;
|
||||
const qLabel = clarification.question
|
||||
? `追问 ${index + 1}:${
|
||||
clarification.question.length > 24
|
||||
? clarification.question.slice(0, 24) + "…"
|
||||
: clarification.question
|
||||
}`
|
||||
: `追问 ${index + 1}`;
|
||||
setInteractiveMessages((prev) =>
|
||||
prev.map((m) => {
|
||||
if (m.id !== agentMsgId) return m;
|
||||
const list = m.clarifications ?? [];
|
||||
// 按 id 去重 / 覆盖,流式重发同一条时不重复 push。
|
||||
const existingIdx = list.findIndex(
|
||||
(c) => (c.id ?? "") === (clarification.id ?? ""),
|
||||
);
|
||||
const nextList =
|
||||
existingIdx >= 0 && clarification.id
|
||||
? list.map((c, i) => (i === existingIdx ? clarification : c))
|
||||
: [...list, clarification];
|
||||
const steps = m.steps ?? [];
|
||||
const hasStep = steps.some((s) => s.key === stepKey);
|
||||
const nextSteps = hasStep
|
||||
? steps
|
||||
: [
|
||||
...steps,
|
||||
{
|
||||
id: `${agentMsgId}-clar-${clarId}`,
|
||||
key: stepKey,
|
||||
label: qLabel,
|
||||
ts: nowTime(),
|
||||
},
|
||||
];
|
||||
return { ...m, clarifications: nextList, steps: nextSteps };
|
||||
}),
|
||||
);
|
||||
},
|
||||
});
|
||||
const parsedFromContent = splitIntentReadyBlock(res.content || "").summary;
|
||||
const resolvedSummary = res.summary ?? parsedFromContent;
|
||||
|
||||
const finalClarifications = resolvedSummary ? [] : res.clarifications;
|
||||
setInteractiveMessages((prev) =>
|
||||
prev.map((m) =>
|
||||
m.id === agentMsgId
|
||||
@@ -366,9 +428,13 @@ export function useStep1Intent(options: UseStep1IntentOptions): UseStep1IntentRe
|
||||
...m,
|
||||
content: res.content || m.content || "(无文本输出)",
|
||||
streaming: false,
|
||||
// 终态以 res.clarifications 为权威集合(覆盖流式期增量构建的),
|
||||
// 既保证多问题完整,也在解析出 summary 时清空澄清卡。
|
||||
clarification: resolvedSummary
|
||||
? undefined
|
||||
: res.clarification ?? undefined,
|
||||
clarifications:
|
||||
finalClarifications.length > 0 ? finalClarifications : undefined,
|
||||
}
|
||||
: m,
|
||||
),
|
||||
@@ -390,11 +456,14 @@ export function useStep1Intent(options: UseStep1IntentOptions): UseStep1IntentRe
|
||||
},
|
||||
]);
|
||||
triggerToast("✅ 任务理解完成,可进入多智能体圆桌研讨");
|
||||
} else if (res.status === "clarification" && res.clarification) {
|
||||
} else if (res.status === "clarification" && res.clarifications.length > 0) {
|
||||
setPendingIntentClarificationMsgId(agentMsgId);
|
||||
setIntentClarificationSelections((prev) => {
|
||||
const next = new Map(prev);
|
||||
next.set(agentMsgId, { selectedIds: [], custom: "" });
|
||||
res.clarifications.forEach((c, i) => {
|
||||
const key = clarificationKey(agentMsgId, c.id, i);
|
||||
if (!next.has(key)) next.set(key, { selectedIds: [], custom: "" });
|
||||
});
|
||||
return next;
|
||||
});
|
||||
} else if (res.status === "error") {
|
||||
@@ -448,24 +517,37 @@ export function useStep1Intent(options: UseStep1IntentOptions): UseStep1IntentRe
|
||||
[intentThreadId, triggerToast],
|
||||
);
|
||||
|
||||
const submitIntentClarification = useCallback(
|
||||
const submitIntentClarifications = useCallback(
|
||||
(
|
||||
msgId: number,
|
||||
payload: ClarificationPayload,
|
||||
overrideSelection?: IntentClarificationSelection,
|
||||
clarifications: ClarificationPayload[],
|
||||
overrideByKey?: Record<string, IntentClarificationSelection>,
|
||||
) => {
|
||||
const selection =
|
||||
overrideSelection ??
|
||||
intentClarificationSelections.get(msgId) ?? {
|
||||
selectedIds: [],
|
||||
custom: "",
|
||||
};
|
||||
const answer = formatIntentClarificationAnswer(payload, selection);
|
||||
if (!answer) {
|
||||
triggerToast("请先选择选项或输入回答");
|
||||
// 逐题取选择并格式化;只把答了的题拼进去。
|
||||
const answered = clarifications
|
||||
.map((payload, index) => {
|
||||
const key = clarificationKey(msgId, payload.id, index);
|
||||
const selection =
|
||||
overrideByKey?.[key] ??
|
||||
intentClarificationSelections.get(key) ?? { selectedIds: [], custom: "" };
|
||||
const answer = formatIntentClarificationAnswer(payload, selection);
|
||||
return answer ? { question: payload.question, answer } : null;
|
||||
})
|
||||
.filter((x): x is { question: string; answer: string } => x !== null);
|
||||
|
||||
if (answered.length === 0) {
|
||||
triggerToast("请至少回答一个问题");
|
||||
return;
|
||||
}
|
||||
void sendIntentMessage(answer);
|
||||
|
||||
// 单题:直接发答案(与旧行为一致)。多题:带上问题做编号 Q/A,合并成一条消息。
|
||||
const combined =
|
||||
answered.length === 1
|
||||
? answered[0].answer
|
||||
: answered
|
||||
.map((qa, i) => `${i + 1}. ${qa.question}\n答:${qa.answer}`)
|
||||
.join("\n\n");
|
||||
void sendIntentMessage(combined);
|
||||
},
|
||||
[intentClarificationSelections, sendIntentMessage, triggerToast],
|
||||
);
|
||||
@@ -499,7 +581,7 @@ export function useStep1Intent(options: UseStep1IntentOptions): UseStep1IntentRe
|
||||
setIntentClarificationSelections,
|
||||
runStep1Init,
|
||||
sendIntentMessage,
|
||||
submitIntentClarification,
|
||||
submitIntentClarifications,
|
||||
stopIntentStream,
|
||||
hydrateFromDraft,
|
||||
appendLocalMessage,
|
||||
|
||||
@@ -84,6 +84,13 @@ export type AgentConfig = {
|
||||
|
||||
/** 草稿持久化的 Step 2 快照。 */
|
||||
export type Step2Snapshot = {
|
||||
/**
|
||||
* 本次研讨实际选中的席位 —— 加载草稿后据此重建左侧「参与角色」面板。
|
||||
* 不存的话(2026-06 之前的旧草稿)hydrate 会回退到 DEFAULT_SELECTED_AGENTS,
|
||||
* 导致 Step 2/3 左侧显示默认 6 个而非当时真正选的那几个。反序列化时
|
||||
* 老草稿按 undefined 兜底(保留当前 selectedAgents,不强行覆盖)。
|
||||
*/
|
||||
selectedAgents?: SelectedAgent[];
|
||||
threadIds: ThreadIdMap | null;
|
||||
coordinatorName: string | null;
|
||||
step2RoundtableDialogues: Step2Dialogue[];
|
||||
@@ -1420,6 +1427,12 @@ export function useStep2Orchestration(
|
||||
setPendingClarification(null);
|
||||
setClarificationDraft("");
|
||||
|
||||
// 先恢复 selectedAgents —— roleTemplates / agentNameToRole 都由它派生,
|
||||
// 主页面的「参与角色」面板据此重建,否则会停留在默认 6 个席位。
|
||||
// 老草稿没有这字段时保持当前值不动(避免清空成空数组)。
|
||||
if (snapshot.selectedAgents && snapshot.selectedAgents.length > 0) {
|
||||
setSelectedAgents(snapshot.selectedAgents);
|
||||
}
|
||||
setThreadIds(snapshot.threadIds);
|
||||
setCoordinatorName(snapshot.coordinatorName);
|
||||
setStep2RoundtableDialogues(snapshot.step2RoundtableDialogues);
|
||||
|
||||
@@ -63,8 +63,6 @@ import { useStep2Orchestration } from "@/roundtable-planning/hooks/useStep2Orche
|
||||
import { useDraftPersistence } from "@/roundtable-planning/hooks/useDraftPersistence";
|
||||
import {
|
||||
BUILTIN_AGENT_META,
|
||||
getAvatarTypeFor,
|
||||
getDisplayNameFor,
|
||||
makeRolesFromTemplates,
|
||||
nowTime,
|
||||
type RoleEntry,
|
||||
@@ -187,6 +185,7 @@ export default function RoundtablePlanningPage() {
|
||||
timelineEntries: step1.timelineEntries,
|
||||
}),
|
||||
getStep2Snapshot: () => ({
|
||||
selectedAgents: step2.selectedAgents,
|
||||
threadIds: step2.threadIds,
|
||||
coordinatorName: step2.coordinatorName,
|
||||
step2RoundtableDialogues: step2.step2RoundtableDialogues,
|
||||
@@ -438,7 +437,6 @@ export default function RoundtablePlanningPage() {
|
||||
currentStep={currentStep}
|
||||
roles={roles}
|
||||
agentNameToRole={step2.agentNameToRole}
|
||||
selectedAgents={step2.selectedAgents}
|
||||
budgetLimit={step2.budgetLimit}
|
||||
intentReady={step1.intentReady}
|
||||
sidebarExpanded={step2SidebarExpanded}
|
||||
@@ -474,7 +472,7 @@ export default function RoundtablePlanningPage() {
|
||||
}}
|
||||
onManualSave={drafts.handleManualSave}
|
||||
onTriggerToast={triggerToast}
|
||||
submitIntentClarification={step1.submitIntentClarification}
|
||||
submitIntentClarifications={step1.submitIntentClarifications}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -814,7 +812,6 @@ interface LeftSidebarProps {
|
||||
string,
|
||||
{ speakerId: number; avatarType: string; displayName: string }
|
||||
>;
|
||||
selectedAgents: SelectedAgent[];
|
||||
budgetLimit: number;
|
||||
intentReady: ReturnType<typeof useStep1Intent>["intentReady"];
|
||||
sidebarExpanded: boolean;
|
||||
@@ -851,7 +848,6 @@ function LeftSidebar({
|
||||
currentStep,
|
||||
roles,
|
||||
agentNameToRole,
|
||||
selectedAgents,
|
||||
budgetLimit: _budgetLimit,
|
||||
intentReady,
|
||||
sidebarExpanded,
|
||||
@@ -859,6 +855,10 @@ function LeftSidebar({
|
||||
}: LeftSidebarProps) {
|
||||
const isStep2 = currentStep === 2;
|
||||
|
||||
// Step 3 隐藏左侧栏(原「参与与结论贡献」列),让中间「最终方案展示」自适应
|
||||
// 撑满整行宽度(右侧栏在 Step !==1 时也已 return null)。
|
||||
if (currentStep === 3) return null;
|
||||
|
||||
return (
|
||||
<section
|
||||
id="left-sidebar"
|
||||
@@ -879,55 +879,7 @@ function LeftSidebar({
|
||||
</button>
|
||||
)}
|
||||
|
||||
{currentStep === 3 ? (
|
||||
<>
|
||||
{/* Step 3 时只保留「参与与结论贡献」面板;原文件里其它两个 hidden 块未启用,
|
||||
删掉以降低维护负担,需要时去 git history 翻 fdeffde 之前的 commit。 */}
|
||||
<div className="bg-white border border-slate-200/50 rounded-2xl p-4.5 shadow-[0_2px_8px_rgba(0,0,0,0.015)] flex-1 flex flex-col min-h-[300px]">
|
||||
<div className="flex items-center gap-2 border-b border-slate-100 pb-2.5 mb-3">
|
||||
<Users className="w-4 h-4 text-primary" strokeWidth={2.5} />
|
||||
<h2 className="text-xs font-black text-slate-900 uppercase tracking-widest">参与与结论贡献</h2>
|
||||
</div>
|
||||
<div className="space-y-2 overflow-y-auto flex-1 pr-1 custom-scrollbar">
|
||||
{[
|
||||
{
|
||||
id: "human",
|
||||
name: "用户 (指挥官)",
|
||||
role: "人类介入协调",
|
||||
icon: <Man1Avatar size="h-7 w-7" />,
|
||||
},
|
||||
...selectedAgents.map((agent, i) => ({
|
||||
id: agent.agent_id,
|
||||
name: getDisplayNameFor(agent),
|
||||
role: BUILTIN_AGENT_META[agent.agent_id]
|
||||
? `${BUILTIN_AGENT_META[agent.agent_id].displayName} 席位`
|
||||
: "圆桌席位",
|
||||
icon: getAvatar(getAvatarTypeFor(agent.agent_id, i), "h-7 w-7"),
|
||||
})),
|
||||
].map((contrib, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className="flex items-center justify-between p-2 rounded-xl bg-slate-50 border border-slate-100 transition hover:border-emerald-100 hover:bg-emerald-50/10"
|
||||
>
|
||||
<div className="flex items-center gap-2.5">
|
||||
{contrib.icon}
|
||||
<div className="flex flex-col">
|
||||
<span className="text-xs font-bold text-slate-800 tracking-tight leading-tight">
|
||||
{contrib.name}
|
||||
</span>
|
||||
<span className="text-[9px] text-primary font-semibold">{contrib.role}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 shrink-0 text-emerald-600 bg-emerald-50 font-bold text-[9.5px] px-1.5 py-0.5 rounded-full">
|
||||
<Check className="h-3 w-3 text-emerald-500 font-black" />
|
||||
已贡献
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : currentStep === 2 ? (
|
||||
{currentStep === 2 ? (
|
||||
sidebarExpanded ? (
|
||||
<>
|
||||
<IntentSummaryCard
|
||||
|
||||
Reference in new issue
Block a user