代码提交
This commit is contained in:
1 parent
357f245aec
commit
9a70329db2
2 files changed
+423
-60
No files matched your search
@@ -0,0 +1,268 @@
|
|||||||
|
# 智能体推荐弹窗功能 — 开发进度
|
||||||
|
|
||||||
|
> 分支: `dhk-1`
|
||||||
|
> 最后更新: 2026-05-25
|
||||||
|
> 状态: **全部完成 ✅**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 功能概述
|
||||||
|
|
||||||
|
在圆桌规划流程的 Step 1(任务意图理解)和 Step 2(多智能体圆桌研讨)之间,插入一个**推荐弹窗**:
|
||||||
|
|
||||||
|
1. 用户完成任务澄清后,点击"进入多智能体研讨"→ 弹出推荐弹窗
|
||||||
|
2. 弹窗拉取 `GET /api/agents` 候选池,调用 `roundtable-recommender` 智能体流式推荐 2-6 个最合适的智能体
|
||||||
|
3. 推荐结果默认勾选,用户可取消勾选 / 从候选池增选(总选 2-8 个)
|
||||||
|
4. 确认后 Step 2 使用用户最终选定的智能体列表,不再硬编码 6 个圆桌席位
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 已完成的所有步骤
|
||||||
|
|
||||||
|
### Step 1 — 后端:新建 `roundtable-recommender` 智能体 ✅
|
||||||
|
|
||||||
|
**文件:**
|
||||||
|
- `offline-backend-20260512/backend/.deer-flow/agents/roundtable-recommender/config.yaml`
|
||||||
|
- `offline-backend-20260512/backend/.deer-flow/agents/roundtable-recommender/SOUL.md`
|
||||||
|
|
||||||
|
**要点:**
|
||||||
|
- `id: roundtable-recommender`,`name: 智能体推荐`
|
||||||
|
- SOUL.md 规定输出协议:自然语言推理 + `[RECOMMEND_READY]` 标记 + ` ```json {"picks": [...]} ``` ` 块
|
||||||
|
- 每个 pick: `{"agent_id": "...", "reason": "..."}`,agent_id 必须来自候选池
|
||||||
|
- 约束:2 ≤ picks ≤ 6,禁止使用任何工具
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Step 2 — 后端:新建 `/api/recommend/stream` 接口 ✅
|
||||||
|
|
||||||
|
**文件:**
|
||||||
|
- `offline-backend-20260512/backend/app/gateway/routers/recommend.py`(新建)
|
||||||
|
- `offline-backend-20260512/backend/app/gateway/app.py`(注册路由)
|
||||||
|
|
||||||
|
**接口规格:**
|
||||||
|
```
|
||||||
|
POST /api/recommend/stream
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
Body:
|
||||||
|
{
|
||||||
|
"intent": { "objective": "...", "constraints": [...], "assumptions": [...] },
|
||||||
|
"candidates": [{ "agent_id": "...", "name": "...", "description": "..." }, ...],
|
||||||
|
"model": "deepseek-chat" // 可选
|
||||||
|
}
|
||||||
|
|
||||||
|
SSE 流:透传上游 LangGraph frames + 最终状态帧
|
||||||
|
最终帧: { "status": "done"|"asking"|"error", "content": "...", "picks": [...] | null }
|
||||||
|
```
|
||||||
|
|
||||||
|
**关键实现:**
|
||||||
|
- 无 `/init`,每次请求内部创建新 thread(stateless)
|
||||||
|
- `_validate_picks()`:对比候选池 id、去重、夹断到 2-6,< 2 返回 `[]`
|
||||||
|
- `_build_prompt()`:将 intent + 候选池渲染为单条 user message
|
||||||
|
- `_RECOMMEND_READY_PATTERN`:正则提取 `[RECOMMEND_READY]` 后的 JSON 块
|
||||||
|
- `httpx.AsyncClient(trust_env=False)`:防止 loopback 被代理拦截
|
||||||
|
- 复用 `multi_agent.py` 的 `_auth_headers`, `_create_thread`, `_stream_upstream` 等
|
||||||
|
|
||||||
|
**app.py 注册:**
|
||||||
|
```python
|
||||||
|
from app.gateway.routers import recommend
|
||||||
|
app.include_router(recommend.router)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Step 3 — 前端:`recommend.ts` API 客户端 ✅
|
||||||
|
|
||||||
|
**文件:** `src/roundtable-planning/api/recommend.ts`
|
||||||
|
|
||||||
|
**导出:**
|
||||||
|
```typescript
|
||||||
|
// 类型
|
||||||
|
export interface RecommendPick { agent_id: string; reason: string }
|
||||||
|
export interface StreamRecommendResult { status: "done"|"asking"|"error"; content: string; picks: RecommendPick[]|null }
|
||||||
|
export type RecommendStreamPhase = "connecting"|"connected"|"streaming"
|
||||||
|
export interface RecommendCandidate { agent_id: string; name: string; description: string }
|
||||||
|
|
||||||
|
// 候选池过滤(排除规则集中管理)
|
||||||
|
export function filterRecommendCandidates(agents: Agent[]): RecommendCandidate[]
|
||||||
|
|
||||||
|
// 流式推荐
|
||||||
|
export async function streamRecommend(req: StreamRecommendRequest): Promise<StreamRecommendResult>
|
||||||
|
```
|
||||||
|
|
||||||
|
**排除规则(`filterRecommendCandidates`):**
|
||||||
|
- `roundtable-intent` — Step 1 澄清专员
|
||||||
|
- `roundtable-recommender` — 推荐者自身
|
||||||
|
- 前缀 `roundtable-coordinator-` — 每 session 生成的一次性协调器
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Step 4 — 前端:`RecommendAgentsDialog.tsx` 弹窗组件 ✅
|
||||||
|
|
||||||
|
**文件:** `src/roundtable-planning/components/RecommendAgentsDialog.tsx`
|
||||||
|
|
||||||
|
**Props:**
|
||||||
|
```typescript
|
||||||
|
export interface RecommendAgentsDialogProps {
|
||||||
|
open: boolean;
|
||||||
|
intent: IntentSummary | null;
|
||||||
|
model?: string;
|
||||||
|
onCancel: () => void;
|
||||||
|
onConfirm: (agentIds: string[]) => void;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**内部阶段机器:**
|
||||||
|
```
|
||||||
|
"idle" → "loading_agents" → "recommending" → "ready"
|
||||||
|
↘ "fallback" (推荐失败,兜底)
|
||||||
|
↘ "error"
|
||||||
|
```
|
||||||
|
|
||||||
|
**UI 分区:**
|
||||||
|
- **推荐参会**(默认全选):推荐智能体卡片,带 reason 说明
|
||||||
|
- **其他候选**(默认未选):可增选,补足 8 个上限
|
||||||
|
|
||||||
|
**选择约束:** min 2,max 8;确认按钮在范围外 disabled
|
||||||
|
|
||||||
|
**安全机制:**
|
||||||
|
- `generationRef` 计数器防止 stale async 污染状态
|
||||||
|
- `AbortController` 取消在途推荐请求
|
||||||
|
- `intersectInOrder()`:前端二次验证推荐 id 在候选池中存在
|
||||||
|
- 兜底列表 `FALLBACK_AGENT_IDS`:6 个内置 roundtable-* 智能体
|
||||||
|
|
||||||
|
**确认顺序:** 推荐且已选 → 用户增选(按交互顺序)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Step 5 — 前端:`RoundtablePlanningPage.tsx` 动态 Agent 角色 ✅
|
||||||
|
|
||||||
|
**删除:** `ROUNDTABLE_ROLE_TEMPLATES`, `makeRoles`, `ROUNDTABLE_AGENT_IDS`, `AGENT_NAME_TO_ROLE`(全部硬编码常量)
|
||||||
|
|
||||||
|
**新增模块级:**
|
||||||
|
```typescript
|
||||||
|
const COMMANDER_SEAT_ID = 1
|
||||||
|
type SelectedAgent = { agent_id: string; name: string }
|
||||||
|
const BUILTIN_AGENT_META: Record<string, { avatarType: string; displayName: string }>
|
||||||
|
const FALLBACK_AVATAR_TYPES = ["blue","purple","emerald","amber","pink"]
|
||||||
|
function getAvatarTypeFor(agentId, index): string
|
||||||
|
function getDisplayNameFor(agent: SelectedAgent): string
|
||||||
|
function buildRoleTemplates(agents): ReadonlyArray<Omit<RoleEntry,"status">>
|
||||||
|
function makeRolesFromTemplates(templates, defaultStatus, overrides?): RoleEntry[]
|
||||||
|
function buildAgentNameToRole(agents): Record<string, {speakerId,avatarType,displayName}>
|
||||||
|
const DEFAULT_SELECTED_AGENTS: SelectedAgent[] // 原 6 个内置席位
|
||||||
|
```
|
||||||
|
|
||||||
|
**新增 state + useMemo:**
|
||||||
|
```typescript
|
||||||
|
const [selectedAgents, setSelectedAgents] = useState<SelectedAgent[]>(DEFAULT_SELECTED_AGENTS);
|
||||||
|
const roleTemplates = useMemo(() => buildRoleTemplates(selectedAgents), [selectedAgents]);
|
||||||
|
const agentNameToRole = useMemo(() => buildAgentNameToRole(selectedAgents), [selectedAgents]);
|
||||||
|
const roundtableAgentIds = useMemo(() => selectedAgents.map(a => a.agent_id), [selectedAgents]);
|
||||||
|
```
|
||||||
|
|
||||||
|
**替换调用点:**
|
||||||
|
| 替换前 | 替换后 |
|
||||||
|
|---|---|
|
||||||
|
| `AGENT_NAME_TO_ROLE[name]` | `agentNameToRole[name]` |
|
||||||
|
| `ROUNDTABLE_AGENT_IDS` | `roundtableAgentIds` |
|
||||||
|
| `makeRoles(status, ...)` | `makeRolesFromTemplates(roleTemplates, status, ...)` |
|
||||||
|
| 角色面板 `role.id === 2 ? <RobotAvatar ...>` 链 | `getAvatar(agentNameToRole[role.agentId]?.avatarType ?? "blue", "h-7 w-7")` |
|
||||||
|
| `HighFidelityReport seatNameMap` 用 `AGENT_NAME_TO_ROLE` | 用 `agentNameToRole` |
|
||||||
|
| useEffect 依赖 | 加入 `roleTemplates` |
|
||||||
|
| dispatch useCallback 依赖 | 加入 `agentNameToRole` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Step 6 — 前端:弹窗接入页面 ✅
|
||||||
|
|
||||||
|
**新增 import:**
|
||||||
|
```typescript
|
||||||
|
import { RecommendAgentsDialog } from "@/roundtable-planning/components/RecommendAgentsDialog";
|
||||||
|
```
|
||||||
|
|
||||||
|
**新增 state:**
|
||||||
|
```typescript
|
||||||
|
const [isRecommendDialogOpen, setIsRecommendDialogOpen] = useState<boolean>(false);
|
||||||
|
```
|
||||||
|
|
||||||
|
**新增 callback `handleConfirmAgents`:**
|
||||||
|
```typescript
|
||||||
|
const handleConfirmAgents = useCallback((agentIds: string[]) => {
|
||||||
|
setSelectedAgents(
|
||||||
|
agentIds.map(id => ({
|
||||||
|
agent_id: id,
|
||||||
|
name: BUILTIN_AGENT_META[id]?.displayName ?? id,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
step2InitRef.current = false; // 重置 Step 2,让新 coordinator 按新名单初始化
|
||||||
|
setIsRecommendDialogOpen(false);
|
||||||
|
setCurrentStep(2);
|
||||||
|
setShowToast("成功确认!已切换至第二阶段圆桌研讨系统");
|
||||||
|
}, []);
|
||||||
|
```
|
||||||
|
|
||||||
|
**两个"进入多智能体研讨"按钮改为:**
|
||||||
|
```typescript
|
||||||
|
onClick={() => setIsRecommendDialogOpen(true)}
|
||||||
|
```
|
||||||
|
|
||||||
|
**JSX 渲染(在 showToast 之前):**
|
||||||
|
```tsx
|
||||||
|
<RecommendAgentsDialog
|
||||||
|
open={isRecommendDialogOpen}
|
||||||
|
intent={intentReady}
|
||||||
|
model={DEFAULT_MODEL}
|
||||||
|
onCancel={() => setIsRecommendDialogOpen(false)}
|
||||||
|
onConfirm={handleConfirmAgents}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 文件清单(本次改动)
|
||||||
|
|
||||||
|
### 后端(新建)
|
||||||
|
| 文件 | 说明 |
|
||||||
|
|---|---|
|
||||||
|
| `.deer-flow/agents/roundtable-recommender/config.yaml` | 推荐智能体元数据 |
|
||||||
|
| `.deer-flow/agents/roundtable-recommender/SOUL.md` | 推荐智能体系统提示 |
|
||||||
|
| `app/gateway/routers/recommend.py` | `/api/recommend/stream` 接口 |
|
||||||
|
|
||||||
|
### 后端(修改)
|
||||||
|
| 文件 | 说明 |
|
||||||
|
|---|---|
|
||||||
|
| `app/gateway/app.py` | 注册 `recommend.router` |
|
||||||
|
|
||||||
|
### 前端(新建)
|
||||||
|
| 文件 | 说明 |
|
||||||
|
|---|---|
|
||||||
|
| `src/roundtable-planning/api/recommend.ts` | 前端 SSE 客户端 |
|
||||||
|
| `src/roundtable-planning/components/RecommendAgentsDialog.tsx` | 推荐弹窗组件 |
|
||||||
|
|
||||||
|
### 前端(修改)
|
||||||
|
| 文件 | 说明 |
|
||||||
|
|---|---|
|
||||||
|
| `src/roundtable-planning/pages/RoundtablePlanningPage.tsx` | 动态 Agent 角色 + 弹窗接入 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 下次开始前检查项
|
||||||
|
|
||||||
|
1. **后端是否已重启** — `recommend.router` 注册需重启 FastAPI(`make dev` 或 `./scripts/restart-all.sh`)
|
||||||
|
2. **`roundtable-recommender` 智能体已注册** — 访问 `GET /api/agents` 确认列表里有 `roundtable-recommender`
|
||||||
|
3. **typecheck 干净** — `pnpm typecheck 2>&1 | grep roundtable-planning` 无输出
|
||||||
|
4. **弹窗流程端到端测试**:
|
||||||
|
- Step 1 完成意图澄清 → `intentReady` 不为 null
|
||||||
|
- 点击"进入多智能体研讨"→ 弹窗出现,开始加载智能体列表
|
||||||
|
- 弹窗展示推荐结果(流式文字 + 卡片列表)
|
||||||
|
- 调整勾选 → 确认 → Step 2 用所选智能体初始化
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 潜在后续工作(未列入本次范围)
|
||||||
|
|
||||||
|
- 弹窗内显示每个候选智能体的完整 description(目前从 `/api/agents` 取,已有字段)
|
||||||
|
- 推荐失败时的 Toast 提示(目前静默兜底)
|
||||||
|
- 推荐理由(reason)在 Step 2 角色面板中展示
|
||||||
|
- `HighFidelityReport` 的 `avatarType` 字段按新选角色动态渲染
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useEffect, useRef, useCallback } from "react";
|
import React, { useState, useEffect, useRef, useCallback, useMemo } from "react";
|
||||||
import ReactMarkdown from "react-markdown";
|
import ReactMarkdown from "react-markdown";
|
||||||
import remarkGfm from "remark-gfm";
|
import remarkGfm from "remark-gfm";
|
||||||
import { HighFidelityReport } from "@/roundtable-planning/components/HighFidelityReport";
|
import { HighFidelityReport } from "@/roundtable-planning/components/HighFidelityReport";
|
||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
type IntentStreamPhase,
|
type IntentStreamPhase,
|
||||||
type IntentSummary,
|
type IntentSummary,
|
||||||
} from "@/roundtable-planning/api/intent";
|
} from "@/roundtable-planning/api/intent";
|
||||||
|
import { RecommendAgentsDialog } from "@/roundtable-planning/components/RecommendAgentsDialog";
|
||||||
import {
|
import {
|
||||||
deleteDraft,
|
deleteDraft,
|
||||||
deriveDraftName,
|
deriveDraftName,
|
||||||
@@ -159,42 +160,111 @@ type RoleEntry = {
|
|||||||
status: string;
|
status: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ROUNDTABLE_ROLE_TEMPLATES: ReadonlyArray<Omit<RoleEntry, "status">> = [
|
/** Stable seat id reserved for the human commander row. Agent seats start at 2. */
|
||||||
{ id: 1, name: "指挥员 / 主持人", type: "human" },
|
const COMMANDER_SEAT_ID = 1;
|
||||||
{ id: 2, name: "总结陈述", agentId: "roundtable-summary", type: "agent" },
|
|
||||||
{ id: 3, name: "情报收集", agentId: "roundtable-intelligence", type: "agent" },
|
|
||||||
{ id: 4, name: "环境评估", agentId: "roundtable-environment", type: "agent" },
|
|
||||||
{ id: 5, name: "方案设计", agentId: "roundtable-solution-design", type: "agent" },
|
|
||||||
{ id: 6, name: "风险审查", agentId: "roundtable-risk-review", type: "agent" },
|
|
||||||
{ id: 7, name: "执行规划", agentId: "roundtable-execution-plan", type: "agent" },
|
|
||||||
{ id: 8, name: "总结陈述", agentId: "roundtable-summary", type: "agent" },
|
|
||||||
];
|
|
||||||
|
|
||||||
function makeRoles(defaultStatus: string, overrides: Record<number, string> = {}): RoleEntry[] {
|
/** One entry per agent the user picked for this round of Step 2. */
|
||||||
return ROUNDTABLE_ROLE_TEMPLATES.map((t) => ({
|
type SelectedAgent = { agent_id: string; name: string };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display metadata for the 6 built-in roundtable seats — kept so the
|
||||||
|
* familiar emoji/color/display name survives when the user keeps the
|
||||||
|
* defaults. Custom agents fall through to {@link FALLBACK_AVATAR_TYPES}.
|
||||||
|
*/
|
||||||
|
const BUILTIN_AGENT_META: Record<string, { avatarType: string; displayName: string }> = {
|
||||||
|
"roundtable-intelligence": { avatarType: "purple", displayName: "情报收集" },
|
||||||
|
"roundtable-environment": { avatarType: "emerald", displayName: "环境评估" },
|
||||||
|
"roundtable-solution-design": { avatarType: "human", displayName: "方案设计" },
|
||||||
|
"roundtable-risk-review": { avatarType: "amber", displayName: "风险审查" },
|
||||||
|
"roundtable-execution-plan": { avatarType: "pink", displayName: "执行规划" },
|
||||||
|
"roundtable-summary": { avatarType: "blue", displayName: "总结陈述" },
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Visual rotation for non-built-in agents. Each entry must be a key
|
||||||
|
* recognized by {@link getAvatar} — anything else falls back to the plain
|
||||||
|
* `RobotAvatar`.
|
||||||
|
*/
|
||||||
|
const FALLBACK_AVATAR_TYPES = ["blue", "purple", "emerald", "amber", "pink"] as const;
|
||||||
|
|
||||||
|
function getAvatarTypeFor(agentId: string, index: number): string {
|
||||||
|
return (
|
||||||
|
BUILTIN_AGENT_META[agentId]?.avatarType ??
|
||||||
|
FALLBACK_AVATAR_TYPES[index % FALLBACK_AVATAR_TYPES.length]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDisplayNameFor(agent: SelectedAgent): string {
|
||||||
|
return BUILTIN_AGENT_META[agent.agent_id]?.displayName ?? agent.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the seat-template list shown in the right-rail role panel.
|
||||||
|
* The commander always sits at id=1; agents take id=2..N+1, in the order
|
||||||
|
* the user picked them so the panel mirrors the recommend dialog.
|
||||||
|
*/
|
||||||
|
function buildRoleTemplates(
|
||||||
|
agents: SelectedAgent[],
|
||||||
|
): ReadonlyArray<Omit<RoleEntry, "status">> {
|
||||||
|
const out: Omit<RoleEntry, "status">[] = [
|
||||||
|
{ id: COMMANDER_SEAT_ID, name: "指挥员 / 主持人", type: "human" },
|
||||||
|
];
|
||||||
|
agents.forEach((a, i) => {
|
||||||
|
out.push({
|
||||||
|
id: i + 2,
|
||||||
|
name: getDisplayNameFor(a),
|
||||||
|
agentId: a.agent_id,
|
||||||
|
type: "agent",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeRolesFromTemplates(
|
||||||
|
templates: ReadonlyArray<Omit<RoleEntry, "status">>,
|
||||||
|
defaultStatus: string,
|
||||||
|
overrides: Record<number, string> = {},
|
||||||
|
): RoleEntry[] {
|
||||||
|
return templates.map((t) => ({
|
||||||
...t,
|
...t,
|
||||||
status: overrides[t.id] ?? defaultStatus,
|
status: overrides[t.id] ?? defaultStatus,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Subset of role templates that map to real backend agents (excludes the
|
/**
|
||||||
// human commander seat) — used as the `agent_names` payload on init.
|
* Map `agent_name` (lowercase) → bubble meta. Used while streaming output so
|
||||||
const ROUNDTABLE_AGENT_IDS = Array.from(
|
* the right speaker bubble lights up and the dialogue card picks the right
|
||||||
new Set(
|
* avatar. Speaker ids stay in sync with {@link buildRoleTemplates} (agents
|
||||||
ROUNDTABLE_ROLE_TEMPLATES.filter((r) => r.agentId).map((r) => r.agentId as string),
|
* are id 2..N+1, indexed by selection order).
|
||||||
),
|
*/
|
||||||
);
|
function buildAgentNameToRole(
|
||||||
|
agents: SelectedAgent[],
|
||||||
|
): Record<string, { speakerId: number; avatarType: string; displayName: string }> {
|
||||||
|
const out: Record<string, { speakerId: number; avatarType: string; displayName: string }> = {};
|
||||||
|
agents.forEach((a, i) => {
|
||||||
|
out[a.agent_id] = {
|
||||||
|
speakerId: i + 2,
|
||||||
|
avatarType: getAvatarTypeFor(a.agent_id, i),
|
||||||
|
displayName: getDisplayNameFor(a),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
// agent_name (lowercase) → bubble meta. Used when streaming output, so we can
|
/**
|
||||||
// light up the right speaker bubble and pick an avatar for the dialogue card.
|
* Default Step-2 roster when the user hasn't picked anything yet (e.g. the
|
||||||
const AGENT_NAME_TO_ROLE: Record<string, { speakerId: number; avatarType: string; displayName: string }> = {
|
* recommend dialog was skipped or hasn't run). Matches the original
|
||||||
"roundtable-summary": { speakerId: 2, avatarType: "blue", displayName: "总结陈述" },
|
* hand-coded 6 roundtable seats so the page renders identically until the
|
||||||
"roundtable-intelligence": { speakerId: 3, avatarType: "purple", displayName: "情报收集" },
|
* recommend flow overrides it.
|
||||||
"roundtable-environment": { speakerId: 4, avatarType: "emerald", displayName: "环境评估" },
|
*/
|
||||||
"roundtable-solution-design": { speakerId: 5, avatarType: "human", displayName: "方案设计" },
|
const DEFAULT_SELECTED_AGENTS: SelectedAgent[] = [
|
||||||
"roundtable-risk-review": { speakerId: 6, avatarType: "amber", displayName: "风险审查" },
|
{ agent_id: "roundtable-summary", name: "总结陈述" },
|
||||||
"roundtable-execution-plan": { speakerId: 7, avatarType: "pink", displayName: "执行规划" },
|
{ agent_id: "roundtable-intelligence", name: "情报收集" },
|
||||||
};
|
{ agent_id: "roundtable-environment", name: "环境评估" },
|
||||||
|
{ agent_id: "roundtable-solution-design", name: "方案设计" },
|
||||||
|
{ agent_id: "roundtable-risk-review", name: "风险审查" },
|
||||||
|
{ agent_id: "roundtable-execution-plan", name: "执行规划" },
|
||||||
|
];
|
||||||
|
|
||||||
function nowTime() {
|
function nowTime() {
|
||||||
return new Date().toLocaleTimeString("zh-CN", { hour12: false });
|
return new Date().toLocaleTimeString("zh-CN", { hour12: false });
|
||||||
@@ -334,17 +404,27 @@ export default function RoundtablePlanningPage() {
|
|||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Roles in roundtable state (updates as we switch steps!)
|
// The user's chosen Step-2 participants. Defaults to the 6 built-in
|
||||||
const [roles, setRoles] = useState<RoleEntry[]>([
|
// roundtable seats so behavior is unchanged before the recommend dialog
|
||||||
{ id: 1, name: "指挥员 / 主持人", type: "human", status: "等待激活" },
|
// overrides it. Step 6 will replace this via the dialog's onConfirm.
|
||||||
{ id: 2, name: "总结陈述", agentId: "roundtable-summary", type: "agent", status: "等待激活" },
|
const [selectedAgents, setSelectedAgents] = useState<SelectedAgent[]>(DEFAULT_SELECTED_AGENTS);
|
||||||
{ id: 3, name: "情报收集", agentId: "roundtable-intelligence", type: "agent", status: "等待激活" },
|
|
||||||
{ id: 4, name: "环境评估", agentId: "roundtable-environment", type: "agent", status: "等待激活" },
|
// Derived: template list for the seat panel + dispatch-time name lookup +
|
||||||
{ id: 5, name: "方案设计", agentId: "roundtable-solution-design", type: "agent", status: "等待激活" },
|
// the `agent_names` payload for /api/multi-agent/init. All three regenerate
|
||||||
{ id: 6, name: "风险审查", agentId: "roundtable-risk-review", type: "agent", status: "等待激活" },
|
// together whenever the selection changes, so seat ids stay in sync.
|
||||||
{ id: 7, name: "执行规划", agentId: "roundtable-execution-plan", type: "agent", status: "等待激活" },
|
const roleTemplates = useMemo(() => buildRoleTemplates(selectedAgents), [selectedAgents]);
|
||||||
{ id: 8, name: "总结陈述", agentId: "roundtable-summary", type: "agent", status: "等待陈述" },
|
const agentNameToRole = useMemo(() => buildAgentNameToRole(selectedAgents), [selectedAgents]);
|
||||||
]);
|
const roundtableAgentIds = useMemo(() => selectedAgents.map((a) => a.agent_id), [selectedAgents]);
|
||||||
|
|
||||||
|
// Roles in roundtable state (updates as we switch steps!). Seeded from
|
||||||
|
// `roleTemplates`'s initial value (selectedAgents starts at the default
|
||||||
|
// roster), so the panel renders identically to the previous hand-coded list.
|
||||||
|
const [roles, setRoles] = useState<RoleEntry[]>(() =>
|
||||||
|
makeRolesFromTemplates(buildRoleTemplates(DEFAULT_SELECTED_AGENTS), "等待激活"),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Dialog: recommend agents before entering Step 2.
|
||||||
|
const [isRecommendDialogOpen, setIsRecommendDialogOpen] = useState<boolean>(false);
|
||||||
|
|
||||||
// Step 2 dialogue list — populated by the real orchestration loop once the
|
// Step 2 dialogue list — populated by the real orchestration loop once the
|
||||||
// user enters the roundtable. Starts empty so we never flash mock content.
|
// user enters the roundtable. Starts empty so we never flash mock content.
|
||||||
@@ -739,7 +819,7 @@ export default function RoundtablePlanningPage() {
|
|||||||
// when the leader 派活给多个席位。改为顺序后,同一时刻只有一个 SSE 在跑。
|
// when the leader 派活给多个席位。改为顺序后,同一时刻只有一个 SSE 在跑。
|
||||||
for (const [subName, task] of leaderRes.dispatched) {
|
for (const [subName, task] of leaderRes.dispatched) {
|
||||||
if (orchestrationCancelRef.current) break;
|
if (orchestrationCancelRef.current) break;
|
||||||
const meta = AGENT_NAME_TO_ROLE[subName.toLowerCase()];
|
const meta = agentNameToRole[subName.toLowerCase()];
|
||||||
if (meta) markSpeakerStreaming(meta.speakerId);
|
if (meta) markSpeakerStreaming(meta.speakerId);
|
||||||
const perSeatModel = meta ? agentConfigs[meta.speakerId]?.model : undefined;
|
const perSeatModel = meta ? agentConfigs[meta.speakerId]?.model : undefined;
|
||||||
|
|
||||||
@@ -806,7 +886,23 @@ export default function RoundtablePlanningPage() {
|
|||||||
clearAllStreamingSpeakers();
|
clearAllStreamingSpeakers();
|
||||||
setIsOrchestrating(false);
|
setIsOrchestrating(false);
|
||||||
}
|
}
|
||||||
}, [appendStreamingDialogue, appendToDialogue, finalizeDialogue, highlightSpeaker, markSpeakerStreaming, markSpeakerIdle, clearAllStreamingSpeakers, isOrchestrating, agentConfigs]);
|
}, [appendStreamingDialogue, appendToDialogue, finalizeDialogue, highlightSpeaker, markSpeakerStreaming, markSpeakerIdle, clearAllStreamingSpeakers, isOrchestrating, agentConfigs, agentNameToRole]);
|
||||||
|
|
||||||
|
// Called by RecommendAgentsDialog onConfirm: update the participant roster,
|
||||||
|
// reset step-2 init so the new coordinator is created for the new roster,
|
||||||
|
// then navigate to Step 2.
|
||||||
|
const handleConfirmAgents = useCallback((agentIds: string[]) => {
|
||||||
|
setSelectedAgents(
|
||||||
|
agentIds.map((id) => ({
|
||||||
|
agent_id: id,
|
||||||
|
name: BUILTIN_AGENT_META[id]?.displayName ?? id,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
step2InitRef.current = false;
|
||||||
|
setIsRecommendDialogOpen(false);
|
||||||
|
setCurrentStep(2);
|
||||||
|
setShowToast("成功确认!已切换至第二阶段圆桌研讨系统");
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Public stop handler used by the pause/stop button and the step-2 teardown.
|
// Public stop handler used by the pause/stop button and the step-2 teardown.
|
||||||
// Sets the cancel flag and aborts every in-flight stream so the loop can
|
// Sets the cancel flag and aborts every in-flight stream so the loop can
|
||||||
@@ -849,7 +945,7 @@ export default function RoundtablePlanningPage() {
|
|||||||
const coord = newCoordinatorName();
|
const coord = newCoordinatorName();
|
||||||
try {
|
try {
|
||||||
const initRes = await initMultiAgent({
|
const initRes = await initMultiAgent({
|
||||||
agentNames: ROUNDTABLE_AGENT_IDS,
|
agentNames: roundtableAgentIds,
|
||||||
mainAgentName: coord,
|
mainAgentName: coord,
|
||||||
model: DEFAULT_MODEL,
|
model: DEFAULT_MODEL,
|
||||||
// As each sub-agent's thread comes online, flip every role that
|
// As each sub-agent's thread comes online, flip every role that
|
||||||
@@ -1473,11 +1569,11 @@ export default function RoundtablePlanningPage() {
|
|||||||
// seat to 在线 — assume all seats are online so the panel doesn't get
|
// seat to 在线 — assume all seats are online so the panel doesn't get
|
||||||
// stuck at "正在上线".
|
// stuck at "正在上线".
|
||||||
const baseStatus = loadedFromDraftRef.current ? "在线" : "正在上线";
|
const baseStatus = loadedFromDraftRef.current ? "在线" : "正在上线";
|
||||||
setRoles(makeRoles(baseStatus, { 1: "在线" }));
|
setRoles(makeRolesFromTemplates(roleTemplates, baseStatus, { 1: "在线" }));
|
||||||
} else {
|
} else {
|
||||||
setRoles(makeRoles("已完成"));
|
setRoles(makeRolesFromTemplates(roleTemplates, "已完成"));
|
||||||
}
|
}
|
||||||
}, [currentStep, intentThreadId, intentInitError]);
|
}, [currentStep, intentThreadId, intentInitError, roleTemplates]);
|
||||||
|
|
||||||
// Inject user input to the roundtable room — sends it as a new leader call.
|
// Inject user input to the roundtable room — sends it as a new leader call.
|
||||||
const handleInjectToRoundtable = () => {
|
const handleInjectToRoundtable = () => {
|
||||||
@@ -1544,6 +1640,14 @@ export default function RoundtablePlanningPage() {
|
|||||||
>
|
>
|
||||||
|
|
||||||
{/* Toast Notice Banner */}
|
{/* Toast Notice Banner */}
|
||||||
|
<RecommendAgentsDialog
|
||||||
|
open={isRecommendDialogOpen}
|
||||||
|
intent={intentReady}
|
||||||
|
model={DEFAULT_MODEL}
|
||||||
|
onCancel={() => setIsRecommendDialogOpen(false)}
|
||||||
|
onConfirm={handleConfirmAgents}
|
||||||
|
/>
|
||||||
|
|
||||||
{showToast && (
|
{showToast && (
|
||||||
<div className="fixed top-4 left-1/2 transform -translate-x-1/2 z-50 bg-primary text-white text-xs px-4 py-2.5 rounded-xl shadow-lg border border-primary/40 font-medium flex items-center gap-2 animate-bounce">
|
<div className="fixed top-4 left-1/2 transform -translate-x-1/2 z-50 bg-primary text-white text-xs px-4 py-2.5 rounded-xl shadow-lg border border-primary/40 font-medium flex items-center gap-2 animate-bounce">
|
||||||
<Sparkles className="h-4 w-4 shrink-0" />
|
<Sparkles className="h-4 w-4 shrink-0" />
|
||||||
@@ -2009,16 +2113,10 @@ export default function RoundtablePlanningPage() {
|
|||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2.5">
|
<div className="flex items-center gap-2.5">
|
||||||
{/* Avatar chooser based on role.id */}
|
|
||||||
{role.type === "human" ? (
|
{role.type === "human" ? (
|
||||||
<Man1Avatar size="h-7 w-7" />
|
<Man1Avatar size="h-7 w-7" />
|
||||||
) : (
|
) : (
|
||||||
role.id === 2 ? <RobotAvatar headColor="#f5222d" size="h-7 w-7" /> :
|
getAvatar(role.agentId ? (agentNameToRole[role.agentId]?.avatarType ?? "blue") : "blue", "h-7 w-7")
|
||||||
role.id === 3 ? <PurpleRobotAvatar size="h-7 w-7" /> :
|
|
||||||
role.id === 4 ? <EmeraldRobotAvatar size="h-7 w-7" /> :
|
|
||||||
role.id === 6 ? <AmberRobotAvatar size="h-7 w-7" /> :
|
|
||||||
role.id === 7 ? <PinkRobotAvatar size="h-7 w-7" /> :
|
|
||||||
<BlueRobotAvatar size="h-7 w-7" />
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
@@ -2146,10 +2244,7 @@ export default function RoundtablePlanningPage() {
|
|||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
id="confirm-roundtable-btn"
|
id="confirm-roundtable-btn"
|
||||||
onClick={() => {
|
onClick={() => setIsRecommendDialogOpen(true)}
|
||||||
setCurrentStep(2);
|
|
||||||
triggerToast("成功确认!已切换至第二阶段圆桌研讨系统");
|
|
||||||
}}
|
|
||||||
className="bg-primary hover:bg-[var(--brand-900)] text-white font-bold text-xs px-4 py-2 rounded-xl transition shadow-[0_2px_8px_rgba(245,34,45,0.2)] active:scale-95 shrink-0"
|
className="bg-primary hover:bg-[var(--brand-900)] text-white font-bold text-xs px-4 py-2 rounded-xl transition shadow-[0_2px_8px_rgba(245,34,45,0.2)] active:scale-95 shrink-0"
|
||||||
>
|
>
|
||||||
用户确认,进入多智能体研讨
|
用户确认,进入多智能体研讨
|
||||||
@@ -2435,7 +2530,7 @@ export default function RoundtablePlanningPage() {
|
|||||||
triggerToast("⚠️ 请先与任务理解智能体完成澄清,生成任务摘要后再进入");
|
triggerToast("⚠️ 请先与任务理解智能体完成澄清,生成任务摘要后再进入");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setCurrentStep(2);
|
setIsRecommendDialogOpen(true);
|
||||||
}}
|
}}
|
||||||
disabled={!intentReady}
|
disabled={!intentReady}
|
||||||
title={intentReady ? undefined : "请先完成任务意图澄清"}
|
title={intentReady ? undefined : "请先完成任务意图澄清"}
|
||||||
@@ -3123,7 +3218,7 @@ export default function RoundtablePlanningPage() {
|
|||||||
content: String(d.content ?? ""),
|
content: String(d.content ?? ""),
|
||||||
}))}
|
}))}
|
||||||
seatNameMap={Object.fromEntries(
|
seatNameMap={Object.fromEntries(
|
||||||
Object.entries(AGENT_NAME_TO_ROLE).map(([k, v]) => [k, v.displayName]),
|
Object.entries(agentNameToRole).map(([k, v]) => [k, v.displayName]),
|
||||||
)}
|
)}
|
||||||
onReset={handleStartNewTask}
|
onReset={handleStartNewTask}
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in new issue
Block a user