From 9a70329db27d53aaa2b5544d3f02cab5c807c3cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=91=A3=E5=AE=8F=E5=87=AF?= Date: Mon, 25 May 2026 16:08:18 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BB=A3=E7=A0=81=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../docs/recommend-agents-progress.md | 268 ++++++++++++++++++ .../pages/RoundtablePlanningPage.tsx | 215 ++++++++++---- 2 files changed, 423 insertions(+), 60 deletions(-) create mode 100644 frontend-web/docs/recommend-agents-progress.md diff --git a/frontend-web/docs/recommend-agents-progress.md b/frontend-web/docs/recommend-agents-progress.md new file mode 100644 index 00000000..f892f033 --- /dev/null +++ b/frontend-web/docs/recommend-agents-progress.md @@ -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 +``` + +**排除规则(`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 +const FALLBACK_AVATAR_TYPES = ["blue","purple","emerald","amber","pink"] +function getAvatarTypeFor(agentId, index): string +function getDisplayNameFor(agent: SelectedAgent): string +function buildRoleTemplates(agents): ReadonlyArray> +function makeRolesFromTemplates(templates, defaultStatus, overrides?): RoleEntry[] +function buildAgentNameToRole(agents): Record +const DEFAULT_SELECTED_AGENTS: SelectedAgent[] // 原 6 个内置席位 +``` + +**新增 state + useMemo:** +```typescript +const [selectedAgents, setSelectedAgents] = useState(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 ? ` 链 | `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(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 + 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` 字段按新选角色动态渲染 diff --git a/frontend-web/src/roundtable-planning/pages/RoundtablePlanningPage.tsx b/frontend-web/src/roundtable-planning/pages/RoundtablePlanningPage.tsx index 6c0ed51e..c60071b4 100644 --- a/frontend-web/src/roundtable-planning/pages/RoundtablePlanningPage.tsx +++ b/frontend-web/src/roundtable-planning/pages/RoundtablePlanningPage.tsx @@ -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 remarkGfm from "remark-gfm"; import { HighFidelityReport } from "@/roundtable-planning/components/HighFidelityReport"; @@ -21,6 +21,7 @@ import { type IntentStreamPhase, type IntentSummary, } from "@/roundtable-planning/api/intent"; +import { RecommendAgentsDialog } from "@/roundtable-planning/components/RecommendAgentsDialog"; import { deleteDraft, deriveDraftName, @@ -159,42 +160,111 @@ type RoleEntry = { status: string; }; -const ROUNDTABLE_ROLE_TEMPLATES: ReadonlyArray> = [ - { id: 1, name: "指挥员 / 主持人", type: "human" }, - { 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" }, -]; +/** Stable seat id reserved for the human commander row. Agent seats start at 2. */ +const COMMANDER_SEAT_ID = 1; -function makeRoles(defaultStatus: string, overrides: Record = {}): RoleEntry[] { - return ROUNDTABLE_ROLE_TEMPLATES.map((t) => ({ +/** One entry per agent the user picked for this round of Step 2. */ +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 = { + "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> { + const out: Omit[] = [ + { 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>, + defaultStatus: string, + overrides: Record = {}, +): RoleEntry[] { + return templates.map((t) => ({ ...t, 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. -const ROUNDTABLE_AGENT_IDS = Array.from( - new Set( - ROUNDTABLE_ROLE_TEMPLATES.filter((r) => r.agentId).map((r) => r.agentId as string), - ), -); +/** + * Map `agent_name` (lowercase) → bubble meta. Used while streaming output so + * the right speaker bubble lights up and the dialogue card picks the right + * avatar. Speaker ids stay in sync with {@link buildRoleTemplates} (agents + * are id 2..N+1, indexed by selection order). + */ +function buildAgentNameToRole( + agents: SelectedAgent[], +): Record { + const out: Record = {}; + 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. -const AGENT_NAME_TO_ROLE: Record = { - "roundtable-summary": { speakerId: 2, avatarType: "blue", displayName: "总结陈述" }, - "roundtable-intelligence": { speakerId: 3, avatarType: "purple", displayName: "情报收集" }, - "roundtable-environment": { speakerId: 4, avatarType: "emerald", displayName: "环境评估" }, - "roundtable-solution-design": { speakerId: 5, avatarType: "human", displayName: "方案设计" }, - "roundtable-risk-review": { speakerId: 6, avatarType: "amber", displayName: "风险审查" }, - "roundtable-execution-plan": { speakerId: 7, avatarType: "pink", displayName: "执行规划" }, -}; +/** + * Default Step-2 roster when the user hasn't picked anything yet (e.g. the + * recommend dialog was skipped or hasn't run). Matches the original + * hand-coded 6 roundtable seats so the page renders identically until the + * recommend flow overrides it. + */ +const DEFAULT_SELECTED_AGENTS: SelectedAgent[] = [ + { agent_id: "roundtable-summary", name: "总结陈述" }, + { 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() { 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!) - const [roles, setRoles] = useState([ - { id: 1, name: "指挥员 / 主持人", type: "human", status: "等待激活" }, - { id: 2, name: "总结陈述", agentId: "roundtable-summary", type: "agent", status: "等待激活" }, - { id: 3, name: "情报收集", agentId: "roundtable-intelligence", type: "agent", status: "等待激活" }, - { id: 4, name: "环境评估", agentId: "roundtable-environment", type: "agent", status: "等待激活" }, - { id: 5, name: "方案设计", agentId: "roundtable-solution-design", type: "agent", status: "等待激活" }, - { id: 6, name: "风险审查", agentId: "roundtable-risk-review", type: "agent", status: "等待激活" }, - { id: 7, name: "执行规划", agentId: "roundtable-execution-plan", type: "agent", status: "等待激活" }, - { id: 8, name: "总结陈述", agentId: "roundtable-summary", type: "agent", status: "等待陈述" }, - ]); + // The user's chosen Step-2 participants. Defaults to the 6 built-in + // roundtable seats so behavior is unchanged before the recommend dialog + // overrides it. Step 6 will replace this via the dialog's onConfirm. + const [selectedAgents, setSelectedAgents] = useState(DEFAULT_SELECTED_AGENTS); + + // Derived: template list for the seat panel + dispatch-time name lookup + + // the `agent_names` payload for /api/multi-agent/init. All three regenerate + // together whenever the selection changes, so seat ids stay in sync. + const roleTemplates = useMemo(() => buildRoleTemplates(selectedAgents), [selectedAgents]); + 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(() => + makeRolesFromTemplates(buildRoleTemplates(DEFAULT_SELECTED_AGENTS), "等待激活"), + ); + + // Dialog: recommend agents before entering Step 2. + const [isRecommendDialogOpen, setIsRecommendDialogOpen] = useState(false); // Step 2 dialogue list — populated by the real orchestration loop once the // user enters the roundtable. Starts empty so we never flash mock content. @@ -739,7 +819,7 @@ export default function RoundtablePlanningPage() { // when the leader 派活给多个席位。改为顺序后,同一时刻只有一个 SSE 在跑。 for (const [subName, task] of leaderRes.dispatched) { if (orchestrationCancelRef.current) break; - const meta = AGENT_NAME_TO_ROLE[subName.toLowerCase()]; + const meta = agentNameToRole[subName.toLowerCase()]; if (meta) markSpeakerStreaming(meta.speakerId); const perSeatModel = meta ? agentConfigs[meta.speakerId]?.model : undefined; @@ -806,7 +886,23 @@ export default function RoundtablePlanningPage() { clearAllStreamingSpeakers(); 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. // 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(); try { const initRes = await initMultiAgent({ - agentNames: ROUNDTABLE_AGENT_IDS, + agentNames: roundtableAgentIds, mainAgentName: coord, model: DEFAULT_MODEL, // 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 // stuck at "正在上线". const baseStatus = loadedFromDraftRef.current ? "在线" : "正在上线"; - setRoles(makeRoles(baseStatus, { 1: "在线" })); + setRoles(makeRolesFromTemplates(roleTemplates, baseStatus, { 1: "在线" })); } 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. const handleInjectToRoundtable = () => { @@ -1544,6 +1640,14 @@ export default function RoundtablePlanningPage() { > {/* Toast Notice Banner */} + setIsRecommendDialogOpen(false)} + onConfirm={handleConfirmAgents} + /> + {showToast && (
@@ -2009,16 +2113,10 @@ export default function RoundtablePlanningPage() { }`} >
- {/* Avatar chooser based on role.id */} {role.type === "human" ? ( ) : ( - role.id === 2 ? : - role.id === 3 ? : - role.id === 4 ? : - role.id === 6 ? : - role.id === 7 ? : - + getAvatar(role.agentId ? (agentNameToRole[role.agentId]?.avatarType ?? "blue") : "blue", "h-7 w-7") )}
@@ -2146,10 +2244,7 @@ export default function RoundtablePlanningPage() {