代码提交

This commit is contained in:
董宏凯 committed 2026-05-25 16:08:18 +08:00
1 parent 357f245aec
commit 9a70329db2
2 files changed
+423 -60

No files matched your search

@@ -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<Omit<RoleEntry, "status">> = [
{ 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<number, string> = {}): 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<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,
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<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.
const AGENT_NAME_TO_ROLE: Record<string, { speakerId: number; avatarType: string; displayName: string }> = {
"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<RoleEntry[]>([
{ 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<SelectedAgent[]>(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<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
// 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 */}
<RecommendAgentsDialog
open={isRecommendDialogOpen}
intent={intentReady}
model={DEFAULT_MODEL}
onCancel={() => setIsRecommendDialogOpen(false)}
onConfirm={handleConfirmAgents}
/>
{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">
<Sparkles className="h-4 w-4 shrink-0" />
@@ -2009,16 +2113,10 @@ export default function RoundtablePlanningPage() {
}`}
>
<div className="flex items-center gap-2.5">
{/* Avatar chooser based on role.id */}
{role.type === "human" ? (
<Man1Avatar size="h-7 w-7" />
) : (
role.id === 2 ? <RobotAvatar headColor="#f5222d" size="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" />
getAvatar(role.agentId ? (agentNameToRole[role.agentId]?.avatarType ?? "blue") : "blue", "h-7 w-7")
)}
<div className="flex flex-col">
@@ -2146,10 +2244,7 @@ export default function RoundtablePlanningPage() {
</div>
<button
id="confirm-roundtable-btn"
onClick={() => {
setCurrentStep(2);
triggerToast("成功确认!已切换至第二阶段圆桌研讨系统");
}}
onClick={() => setIsRecommendDialogOpen(true)}
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("⚠️ 请先与任务理解智能体完成澄清,生成任务摘要后再进入");
return;
}
setCurrentStep(2);
setIsRecommendDialogOpen(true);
}}
disabled={!intentReady}
title={intentReady ? undefined : "请先完成任务意图澄清"}
@@ -3123,7 +3218,7 @@ export default function RoundtablePlanningPage() {
content: String(d.content ?? ""),
}))}
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}
/>