接口联调
This commit is contained in:
1 parent
a85f1571a0
commit
87c9986b33
142 files changed
+38111
-73
No files matched your search
@@ -48,6 +48,12 @@ export interface StreamMultiAgentRequest {
|
||||
signal?: AbortSignal;
|
||||
/** Fires for every non-status data frame the upstream emits. */
|
||||
onUpstreamFrame?: (frame: unknown) => void;
|
||||
/**
|
||||
* Fires for every AI text chunk parsed out of LangGraph messages-tuple frames.
|
||||
* `text` is the incremental delta (concatenate to rebuild the message);
|
||||
* `messageId` is stable per AI message so callers can group deltas.
|
||||
*/
|
||||
onTextDelta?: (text: string, messageId: string) => void;
|
||||
}
|
||||
|
||||
export interface StreamMultiAgentResult {
|
||||
@@ -84,6 +90,41 @@ function isFinalStatusFrame(value: unknown): value is {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract an AI text delta from a LangGraph `messages-tuple` data frame.
|
||||
* Frames look like `[chunk, metadata]`; an AI text chunk carries `type` in
|
||||
* {"ai","AIMessageChunk"} and a `content` that is either a string or a list of
|
||||
* structured content blocks. Returns null when the frame is not an AI text
|
||||
* chunk (e.g., tool calls, tool results, value snapshots).
|
||||
*/
|
||||
function extractAiTextDelta(parsed: unknown): { text: string; id: string } | null {
|
||||
if (!Array.isArray(parsed) || parsed.length < 1) return null;
|
||||
const chunk = parsed[0];
|
||||
if (!chunk || typeof chunk !== "object") return null;
|
||||
const obj = chunk as Record<string, unknown>;
|
||||
const type = String(obj.type ?? "").toLowerCase();
|
||||
// Accept any AI-shaped message chunk LangChain might emit across versions:
|
||||
// "ai" (older), "AIMessageChunk" (newer streaming), or plain "AIMessage".
|
||||
if (type !== "ai" && type !== "aimessagechunk" && type !== "aimessage") return null;
|
||||
const content = obj.content;
|
||||
let text = "";
|
||||
if (typeof content === "string") {
|
||||
text = content;
|
||||
} else if (Array.isArray(content)) {
|
||||
for (const item of content) {
|
||||
if (typeof item === "string") text += item;
|
||||
else if (item && typeof item === "object") {
|
||||
const block = item as Record<string, unknown>;
|
||||
const blockText = block.text;
|
||||
if (typeof blockText === "string") text += blockText;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!text) return null;
|
||||
const id = typeof obj.id === "string" ? obj.id : "";
|
||||
return { text, id };
|
||||
}
|
||||
|
||||
/**
|
||||
* Open an SSE connection to /api/multi-agent/run/stream and resolve once the
|
||||
* stream completes. The final frame (`{status: ...}`) is interpreted; every
|
||||
@@ -115,6 +156,17 @@ export async function streamMultiAgent(
|
||||
const reader = res.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = "";
|
||||
// Track the last `event:` line so logging can show which event a `data:`
|
||||
// belongs to. The frontend doesn't actually dispatch by event name — every
|
||||
// data frame goes through the same parser — but the label is invaluable
|
||||
// for diagnosing why a sub-agent stream "looks empty".
|
||||
let lastEvent = "";
|
||||
let frameSeq = 0;
|
||||
|
||||
const debugLabel = `[stream:${req.agentType}:${req.agentName}]`;
|
||||
const debugEnabled =
|
||||
typeof window !== "undefined" &&
|
||||
(window as unknown as { __MULTI_AGENT_DEBUG__?: boolean }).__MULTI_AGENT_DEBUG__ !== false;
|
||||
|
||||
const result: StreamMultiAgentResult = { dispatched: [], message: null, content: "", agentName: null };
|
||||
|
||||
@@ -128,6 +180,11 @@ export async function streamMultiAgent(
|
||||
|
||||
for (const rawLine of lines) {
|
||||
const line = rawLine.trimEnd();
|
||||
if (!line) continue;
|
||||
if (line.startsWith("event:")) {
|
||||
lastEvent = line.slice(6).trim();
|
||||
continue;
|
||||
}
|
||||
if (!line.startsWith("data:")) continue;
|
||||
const payload = line.slice(5).trim();
|
||||
if (!payload) continue;
|
||||
@@ -136,9 +193,13 @@ export async function streamMultiAgent(
|
||||
try {
|
||||
parsed = JSON.parse(payload);
|
||||
} catch {
|
||||
if (debugEnabled) console.debug(debugLabel, `bad JSON in event=${lastEvent}:`, payload.slice(0, 200));
|
||||
continue;
|
||||
}
|
||||
|
||||
frameSeq += 1;
|
||||
if (debugEnabled) console.debug(debugLabel, `frame#${frameSeq} event=${lastEvent}`, parsed);
|
||||
|
||||
if (isFinalStatusFrame(parsed)) {
|
||||
if (Array.isArray(parsed.status)) {
|
||||
result.dispatched = parsed.status;
|
||||
@@ -154,9 +215,19 @@ export async function streamMultiAgent(
|
||||
continue;
|
||||
}
|
||||
|
||||
const delta = extractAiTextDelta(parsed);
|
||||
if (delta) {
|
||||
if (debugEnabled) console.debug(debugLabel, `→ delta (${delta.text.length} chars)`);
|
||||
req.onTextDelta?.(delta.text, delta.id);
|
||||
}
|
||||
|
||||
req.onUpstreamFrame?.(parsed);
|
||||
}
|
||||
}
|
||||
|
||||
if (debugEnabled) {
|
||||
console.debug(debugLabel, `stream ended after ${frameSeq} frames; final content length=${result.content.length}`);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -25,8 +25,9 @@ import {
|
||||
Clock,
|
||||
Play,
|
||||
Pause,
|
||||
ChevronRight,
|
||||
Settings,
|
||||
ChevronRight,
|
||||
ChevronDown,
|
||||
Settings,
|
||||
Download,
|
||||
Share2,
|
||||
Check,
|
||||
@@ -200,6 +201,10 @@ export default function RoundtablePlanningPage() {
|
||||
const [consensusPercentage, setConsensusPercentage] = useState<number>(89.4);
|
||||
const [selectedAgentId, setSelectedAgentId] = useState<number | null>(null);
|
||||
const [activeSpeakerId, setActiveSpeakerId] = useState<number | null>(null);
|
||||
// Multiple speakers can stream in parallel (one bubble per sub-agent). The Set
|
||||
// tracks every currently-streaming speakerId so the avatar animation lights up
|
||||
// for each of them concurrently instead of only the most recently set one.
|
||||
const [activeSpeakerSet, setActiveSpeakerSet] = useState<Set<number>>(new Set());
|
||||
|
||||
// Custom presets for agent model, weight, temperature and prompt editors
|
||||
const [agentConfigs, setAgentConfigs] = useState<{ [key: number]: { model: string; weight: number; prompt: string; temp: number } }>({
|
||||
@@ -291,6 +296,9 @@ export default function RoundtablePlanningPage() {
|
||||
const [roundTableSpeed, setRoundTableSpeed] = useState<string>("正常");
|
||||
const [isRoundTableRunning, setIsRoundTableRunning] = useState<boolean>(true);
|
||||
const [customAgentInput, setCustomAgentInput] = useState<string>("");
|
||||
// Whether the roundtable visualizer + constraint console panel is collapsed.
|
||||
// Collapsing gives the conversation log more vertical space.
|
||||
const [isUpperPanelCollapsed, setIsUpperPanelCollapsed] = useState<boolean>(false);
|
||||
|
||||
// Multi-agent orchestration state (lives only for one step-2 visit).
|
||||
const [threadIds, setThreadIds] = useState<ThreadIdMap | null>(null);
|
||||
@@ -300,6 +308,17 @@ export default function RoundtablePlanningPage() {
|
||||
const [isOrchestrating, setIsOrchestrating] = useState<boolean>(false);
|
||||
const orchestrationCancelRef = useRef<boolean>(false);
|
||||
const step2InitRef = useRef<boolean>(false);
|
||||
// Tracks every AbortController active in this Step 2 visit so the "停止" /
|
||||
// "挂起协商" button can abort all in-flight stream fetches at once.
|
||||
const activeAbortersRef = useRef<Set<AbortController>>(new Set());
|
||||
|
||||
// Abort every in-flight streaming fetch. Safe to call multiple times.
|
||||
const abortAllInFlight = useCallback(() => {
|
||||
activeAbortersRef.current.forEach(ctrl => {
|
||||
try { ctrl.abort(); } catch { /* noop */ }
|
||||
});
|
||||
activeAbortersRef.current.clear();
|
||||
}, []);
|
||||
|
||||
// Toast auto-dismiss logic
|
||||
useEffect(() => {
|
||||
@@ -349,7 +368,62 @@ export default function RoundtablePlanningPage() {
|
||||
]);
|
||||
}, []);
|
||||
|
||||
// Highlight a speaker bubble + flip the role's status label.
|
||||
// Streaming variant: append a placeholder card and return its id so subsequent
|
||||
// text deltas can be appended in-place via appendToDialogue.
|
||||
const appendStreamingDialogue = useCallback((card: {
|
||||
sender: string;
|
||||
avatarType: string;
|
||||
initialContent?: string;
|
||||
confidence?: string;
|
||||
}): number => {
|
||||
const id = Date.now() + Math.random();
|
||||
setStep2RoundtableDialogues(prev => [
|
||||
...prev,
|
||||
{
|
||||
id,
|
||||
sender: card.sender,
|
||||
avatarType: card.avatarType,
|
||||
content: card.initialContent ?? "",
|
||||
time: nowTime(),
|
||||
confidence: card.confidence ?? "正在输出…",
|
||||
streaming: true,
|
||||
},
|
||||
]);
|
||||
return id;
|
||||
}, []);
|
||||
|
||||
// Append an AI text delta to an existing streaming card (matched by id).
|
||||
const appendToDialogue = useCallback((id: number, deltaText: string) => {
|
||||
if (!deltaText) return;
|
||||
setStep2RoundtableDialogues(prev =>
|
||||
prev.map(d => (d.id === id ? { ...d, content: (d.content ?? "") + deltaText } : d)),
|
||||
);
|
||||
}, []);
|
||||
|
||||
// Finalize a streaming card with the official content + final confidence label.
|
||||
// If the streamed content already matches, we still overwrite with the final
|
||||
// text (some upstream paths emit trailing whitespace differently).
|
||||
const finalizeDialogue = useCallback((
|
||||
id: number,
|
||||
finalContent: string,
|
||||
confidence?: string,
|
||||
) => {
|
||||
setStep2RoundtableDialogues(prev =>
|
||||
prev.map(d => {
|
||||
if (d.id !== id) return d;
|
||||
const next = finalContent && finalContent.trim().length > 0 ? finalContent : (d.content || "(无文本输出)");
|
||||
return {
|
||||
...d,
|
||||
content: next,
|
||||
confidence: confidence ?? d.confidence,
|
||||
streaming: false,
|
||||
};
|
||||
}),
|
||||
);
|
||||
}, []);
|
||||
|
||||
// Highlight a speaker bubble + flip the role's status label. Single-speaker
|
||||
// mode (used by the leader cycle and the legacy code paths).
|
||||
const highlightSpeaker = useCallback((speakerId: number | null) => {
|
||||
setActiveSpeakerId(speakerId);
|
||||
setRoles(prev =>
|
||||
@@ -365,6 +439,39 @@ export default function RoundtablePlanningPage() {
|
||||
);
|
||||
}, []);
|
||||
|
||||
// Add a speaker to the concurrent-streaming set so its avatar lights up
|
||||
// while a streaming sub-agent run is in flight. Pair with markSpeakerIdle.
|
||||
const markSpeakerStreaming = useCallback((speakerId: number) => {
|
||||
setActiveSpeakerSet(prev => {
|
||||
if (prev.has(speakerId)) return prev;
|
||||
const next = new Set(prev);
|
||||
next.add(speakerId);
|
||||
return next;
|
||||
});
|
||||
setRoles(prev =>
|
||||
prev.map(item => (item.id === speakerId ? { ...item, status: "研讨中" } : item)),
|
||||
);
|
||||
}, []);
|
||||
|
||||
const markSpeakerIdle = useCallback((speakerId: number) => {
|
||||
setActiveSpeakerSet(prev => {
|
||||
if (!prev.has(speakerId)) return prev;
|
||||
const next = new Set(prev);
|
||||
next.delete(speakerId);
|
||||
return next;
|
||||
});
|
||||
setRoles(prev =>
|
||||
prev.map(item => (item.id === speakerId && item.status === "研讨中" ? { ...item, status: "在线" } : item)),
|
||||
);
|
||||
}, []);
|
||||
|
||||
const clearAllStreamingSpeakers = useCallback(() => {
|
||||
setActiveSpeakerSet(new Set());
|
||||
setRoles(prev =>
|
||||
prev.map(item => (item.status === "研讨中" ? { ...item, status: "在线" } : item)),
|
||||
);
|
||||
}, []);
|
||||
|
||||
// Pull the consolidated user intent out of step-1 history. Falls back to a
|
||||
// generic prompt if the user never spoke.
|
||||
const buildInitialIntent = useCallback(() => {
|
||||
@@ -393,31 +500,47 @@ export default function RoundtablePlanningPage() {
|
||||
while (!orchestrationCancelRef.current && safetyCounter < MAX_CYCLES) {
|
||||
safetyCounter += 1;
|
||||
highlightSpeaker(2);
|
||||
appendDialogue({
|
||||
|
||||
// Leader bubble — created BEFORE the call so users see "正在输出…" the
|
||||
// moment the request is in flight, then text fills in as deltas arrive.
|
||||
const leaderBubbleId = appendStreamingDialogue({
|
||||
sender: "总控协调",
|
||||
avatarType: "blue",
|
||||
content: `🧭 [总控派活] 正在分析当前进展并决定下一步:\n${nextLeaderTask.slice(0, 140)}${nextLeaderTask.length > 140 ? "..." : ""}`,
|
||||
initialContent: "",
|
||||
confidence: "调度中",
|
||||
});
|
||||
|
||||
const leaderRes = await streamMultiAgent({
|
||||
agentType: "leader",
|
||||
dAgentThreadId: ids,
|
||||
agentName: coord,
|
||||
newMessage: nextLeaderTask,
|
||||
model: DEFAULT_MODEL,
|
||||
});
|
||||
// Track this leader call's aborter so the stop button can kill it.
|
||||
const leaderAborter = new AbortController();
|
||||
activeAbortersRef.current.add(leaderAborter);
|
||||
let leaderRes;
|
||||
try {
|
||||
leaderRes = await streamMultiAgent({
|
||||
agentType: "leader",
|
||||
dAgentThreadId: ids,
|
||||
agentName: coord,
|
||||
newMessage: nextLeaderTask,
|
||||
model: DEFAULT_MODEL,
|
||||
signal: leaderAborter.signal,
|
||||
onTextDelta: (text) => appendToDialogue(leaderBubbleId, text),
|
||||
});
|
||||
} catch (err) {
|
||||
activeAbortersRef.current.delete(leaderAborter);
|
||||
if (leaderAborter.signal.aborted || (err as Error).name === "AbortError") {
|
||||
finalizeDialogue(leaderBubbleId, "(已停止)", "已停止");
|
||||
break;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
activeAbortersRef.current.delete(leaderAborter);
|
||||
|
||||
if (orchestrationCancelRef.current) break;
|
||||
|
||||
if (leaderRes.content) {
|
||||
appendDialogue({
|
||||
sender: "总结陈述",
|
||||
avatarType: "blue",
|
||||
content: leaderRes.content,
|
||||
confidence: leaderRes.dispatched.length === 0 ? "全案盖印" : "派活说明",
|
||||
});
|
||||
}
|
||||
finalizeDialogue(
|
||||
leaderBubbleId,
|
||||
leaderRes.content,
|
||||
leaderRes.dispatched.length === 0 ? "全案盖印" : "派活说明",
|
||||
);
|
||||
|
||||
if (leaderRes.dispatched.length === 0) {
|
||||
highlightSpeaker(null);
|
||||
@@ -427,29 +550,49 @@ export default function RoundtablePlanningPage() {
|
||||
}
|
||||
|
||||
// Dispatch all sub-agents in parallel; per-seat model preference
|
||||
// overrides the default if the user picked one in the drawer.
|
||||
// overrides the default if the user picked one in the drawer. Each
|
||||
// sub-agent gets its own streaming bubble that fills independently.
|
||||
await Promise.allSettled(
|
||||
leaderRes.dispatched.map(async ([subName, task]) => {
|
||||
if (orchestrationCancelRef.current) return;
|
||||
const meta = AGENT_NAME_TO_ROLE[subName.toLowerCase()];
|
||||
if (meta) highlightSpeaker(meta.speakerId);
|
||||
// Add this speaker to the concurrent active set so its avatar
|
||||
// lights up alongside any other sub-agents streaming in parallel.
|
||||
if (meta) markSpeakerStreaming(meta.speakerId);
|
||||
const perSeatModel = meta ? agentConfigs[meta.speakerId]?.model : undefined;
|
||||
|
||||
const specRes = await streamMultiAgent({
|
||||
agentType: "special",
|
||||
dAgentThreadId: ids,
|
||||
agentName: subName,
|
||||
newMessage: task,
|
||||
model: perSeatModel || DEFAULT_MODEL,
|
||||
});
|
||||
|
||||
if (orchestrationCancelRef.current) return;
|
||||
appendDialogue({
|
||||
const subBubbleId = appendStreamingDialogue({
|
||||
sender: meta?.displayName ?? subName,
|
||||
avatarType: meta?.avatarType ?? "blue",
|
||||
content: specRes.content || `(无文本输出)`,
|
||||
confidence: "子智能体交付",
|
||||
initialContent: "",
|
||||
confidence: "正在输出…",
|
||||
});
|
||||
|
||||
const subAborter = new AbortController();
|
||||
activeAbortersRef.current.add(subAborter);
|
||||
try {
|
||||
const specRes = await streamMultiAgent({
|
||||
agentType: "special",
|
||||
dAgentThreadId: ids,
|
||||
agentName: subName,
|
||||
newMessage: task,
|
||||
model: perSeatModel || DEFAULT_MODEL,
|
||||
signal: subAborter.signal,
|
||||
onTextDelta: (text) => appendToDialogue(subBubbleId, text),
|
||||
});
|
||||
if (orchestrationCancelRef.current) return;
|
||||
finalizeDialogue(subBubbleId, specRes.content, "子智能体交付");
|
||||
} catch (err) {
|
||||
if (subAborter.signal.aborted || (err as Error).name === "AbortError") {
|
||||
finalizeDialogue(subBubbleId, "(已停止)", "已停止");
|
||||
} else {
|
||||
console.error(`[special:${subName}] error:`, err);
|
||||
finalizeDialogue(subBubbleId, `(错误) ${(err as Error).message}`, "调用失败");
|
||||
}
|
||||
} finally {
|
||||
activeAbortersRef.current.delete(subAborter);
|
||||
if (meta) markSpeakerIdle(meta.speakerId);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -469,9 +612,18 @@ export default function RoundtablePlanningPage() {
|
||||
triggerToast(`❌ 研讨异常: ${(err as Error).message}`);
|
||||
} finally {
|
||||
highlightSpeaker(null);
|
||||
clearAllStreamingSpeakers();
|
||||
setIsOrchestrating(false);
|
||||
}
|
||||
}, [appendDialogue, highlightSpeaker, isOrchestrating, agentConfigs]);
|
||||
}, [appendStreamingDialogue, appendToDialogue, finalizeDialogue, highlightSpeaker, markSpeakerStreaming, markSpeakerIdle, clearAllStreamingSpeakers, isOrchestrating, agentConfigs]);
|
||||
|
||||
// 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
|
||||
// unwind immediately (rather than waiting for the current LLM call to finish).
|
||||
const stopOrchestration = useCallback(() => {
|
||||
orchestrationCancelRef.current = true;
|
||||
abortAllInFlight();
|
||||
}, [abortAllInFlight]);
|
||||
|
||||
// On first entry to step 2: init the coordinator + sub-agent threads, then
|
||||
// fire the first leader cycle. Leaving step 2 resets so re-entering builds
|
||||
@@ -480,6 +632,7 @@ export default function RoundtablePlanningPage() {
|
||||
if (currentStep !== 2) {
|
||||
// tear down: cancel any in-flight orchestration; reset bookkeeping.
|
||||
orchestrationCancelRef.current = true;
|
||||
abortAllInFlight();
|
||||
step2InitRef.current = false;
|
||||
setIsOrchestrating(false);
|
||||
return;
|
||||
@@ -531,12 +684,13 @@ export default function RoundtablePlanningPage() {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [currentStep]);
|
||||
|
||||
// Pause/resume button — pausing cancels in-flight stream, resuming kicks off
|
||||
// another leader cycle to continue the conversation.
|
||||
// Pause/resume button — pausing aborts every in-flight stream so the run
|
||||
// halts immediately rather than waiting for the current LLM call to finish.
|
||||
// Resuming kicks off another leader cycle.
|
||||
useEffect(() => {
|
||||
if (currentStep !== 2) return;
|
||||
if (!isRoundTableRunning) {
|
||||
orchestrationCancelRef.current = true;
|
||||
stopOrchestration();
|
||||
return;
|
||||
}
|
||||
if (isOrchestrating || isInitializing || !threadIds || !coordinatorName) return;
|
||||
@@ -1474,8 +1628,35 @@ export default function RoundtablePlanningPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Collapsed handle: when the upper panel is hidden, show a thin
|
||||
bar at the top of the conversation area so the user can bring
|
||||
it back. */}
|
||||
{isUpperPanelCollapsed && (
|
||||
<div className="shrink-0 px-6 py-2 border-b border-slate-100 bg-white flex items-center justify-center">
|
||||
<button
|
||||
onClick={() => setIsUpperPanelCollapsed(false)}
|
||||
className="flex items-center gap-1.5 text-[11px] font-bold text-slate-500 hover:text-primary hover:bg-slate-50 px-3 py-1 rounded-lg transition cursor-pointer"
|
||||
title="展开圆桌控制台"
|
||||
>
|
||||
<ChevronDown className="w-3.5 h-3.5" />
|
||||
展开圆桌控制台
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Split layout: Roundtable visualizer and Interactive Controllers */}
|
||||
<div className="shrink-0 px-6 py-5 bg-gradient-to-b from-[var(--brand-200)]/10 to-transparent border-b border-slate-100 bg-white">
|
||||
{!isUpperPanelCollapsed && (
|
||||
<div className="shrink-0 px-6 py-5 bg-gradient-to-b from-[var(--brand-200)]/10 to-transparent border-b border-slate-100 bg-white relative">
|
||||
{/* Close button — collapses this whole upper panel so the
|
||||
conversation log takes the freed vertical space. */}
|
||||
<button
|
||||
onClick={() => setIsUpperPanelCollapsed(true)}
|
||||
className="absolute top-3 right-4 flex items-center gap-1 text-[10px] font-bold text-slate-400 hover:text-primary hover:bg-slate-100 px-2 py-1 rounded-lg transition cursor-pointer z-10"
|
||||
title="收起圆桌控制台"
|
||||
>
|
||||
<X className="w-3.5 h-3.5" />
|
||||
收起
|
||||
</button>
|
||||
<div className="grid grid-cols-1 xl:grid-cols-12 gap-8 items-center max-w-7xl mx-auto">
|
||||
|
||||
{/* Left part: Roundtable circle diagram */}
|
||||
@@ -1497,14 +1678,14 @@ export default function RoundtablePlanningPage() {
|
||||
|
||||
{/* Clickable Roundtable Agent Bubbles */}
|
||||
{/* Role 3: 情报收集 (Top) */}
|
||||
<button
|
||||
<button
|
||||
onClick={() => setSelectedAgentId(3)}
|
||||
className={`absolute -top-3.5 left-1/2 -translate-x-1/2 transition-transform duration-300 hover:scale-[1.18] active:scale-95 bg-white p-0.5 rounded-full border shadow-md cursor-pointer ${selectedAgentId === 3 || activeSpeakerId === 3 ? 'ring-4 ring-purple-400 scale-[1.12]' : 'border-slate-200'}`}
|
||||
className={`absolute -top-3.5 left-1/2 -translate-x-1/2 transition-transform duration-300 hover:scale-[1.18] active:scale-95 bg-white p-0.5 rounded-full border shadow-md cursor-pointer ${selectedAgentId === 3 || (activeSpeakerId === 3 || activeSpeakerSet.has(3)) ? 'ring-4 ring-purple-400 scale-[1.12]' : 'border-slate-200'}`}
|
||||
title="点击配置 情报收集 智能体人设与LLM"
|
||||
>
|
||||
<PurpleRobotAvatar size="h-8.5 w-8.5" />
|
||||
<span className="absolute -bottom-4.5 left-1/2 -translate-x-1/2 text-[8px] bg-purple-900 text-white font-extrabold px-1 rounded whitespace-nowrap shadow-sm">情报收集</span>
|
||||
{(activeSpeakerId === 3 || selectedAgentId === 3) && (
|
||||
{((activeSpeakerId === 3 || activeSpeakerSet.has(3)) || selectedAgentId === 3) && (
|
||||
<span className="absolute -top-1 -right-1 flex h-2 w-2">
|
||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-purple-400 opacity-75"></span>
|
||||
<span className="relative inline-flex rounded-full h-2 w-2 bg-purple-500"></span>
|
||||
@@ -1515,12 +1696,12 @@ export default function RoundtablePlanningPage() {
|
||||
{/* Role 4: 环境评估 (Top Right) */}
|
||||
<button
|
||||
onClick={() => setSelectedAgentId(4)}
|
||||
className={`absolute top-5 -right-4 transition-transform duration-300 hover:scale-[1.18] active:scale-95 bg-white p-0.5 rounded-full border shadow-md cursor-pointer ${selectedAgentId === 4 || activeSpeakerId === 4 ? 'ring-4 ring-emerald-400 scale-[1.12]' : 'border-slate-200'}`}
|
||||
className={`absolute top-5 -right-4 transition-transform duration-300 hover:scale-[1.18] active:scale-95 bg-white p-0.5 rounded-full border shadow-md cursor-pointer ${selectedAgentId === 4 || (activeSpeakerId === 4 || activeSpeakerSet.has(4)) ? 'ring-4 ring-emerald-400 scale-[1.12]' : 'border-slate-200'}`}
|
||||
title="点击配置 环境评估 智能体人设与LLM"
|
||||
>
|
||||
<EmeraldRobotAvatar size="h-8.5 w-8.5" />
|
||||
<span className="absolute -bottom-4.5 left-1/2 -translate-x-1/2 text-[8px] bg-emerald-900 text-white font-extrabold px-1 rounded whitespace-nowrap shadow-sm">环境评估</span>
|
||||
{(activeSpeakerId === 4 || selectedAgentId === 4) && (
|
||||
{((activeSpeakerId === 4 || activeSpeakerSet.has(4)) || selectedAgentId === 4) && (
|
||||
<span className="absolute -top-1 -right-1 flex h-2 w-2">
|
||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75"></span>
|
||||
<span className="relative inline-flex rounded-full h-2 w-2 bg-emerald-500"></span>
|
||||
@@ -1531,12 +1712,12 @@ export default function RoundtablePlanningPage() {
|
||||
{/* Human Participant: 方案设计 (Bottom Right) */}
|
||||
<button
|
||||
onClick={() => setSelectedAgentId(5)}
|
||||
className={`absolute bottom-5 -right-4 transition-transform duration-300 hover:scale-[1.18] active:scale-95 bg-white p-0.5 rounded-full border shadow-md cursor-pointer ${selectedAgentId === 5 || activeSpeakerId === 5 ? 'ring-4 ring-primary/50 scale-[1.12]' : 'border-slate-200'}`}
|
||||
className={`absolute bottom-5 -right-4 transition-transform duration-300 hover:scale-[1.18] active:scale-95 bg-white p-0.5 rounded-full border shadow-md cursor-pointer ${selectedAgentId === 5 || (activeSpeakerId === 5 || activeSpeakerSet.has(5)) ? 'ring-4 ring-primary/50 scale-[1.12]' : 'border-slate-200'}`}
|
||||
title="点击查看 方案设计 角色参数"
|
||||
>
|
||||
<Man1Avatar size="h-8.5 w-8.5" />
|
||||
<span className="absolute -bottom-4.5 left-1/2 -translate-x-1/2 text-[8px] bg-[var(--brand-950)] text-white font-extrabold px-1 rounded whitespace-nowrap shadow-sm">方案设计</span>
|
||||
{(activeSpeakerId === 5 || selectedAgentId === 5) && (
|
||||
{((activeSpeakerId === 5 || activeSpeakerSet.has(5)) || selectedAgentId === 5) && (
|
||||
<span className="absolute -top-1 -right-1 flex h-2 w-2">
|
||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-primary/60 opacity-75"></span>
|
||||
<span className="relative inline-flex rounded-full h-2 w-2 bg-[var(--brand-200)]0"></span>
|
||||
@@ -1547,12 +1728,12 @@ export default function RoundtablePlanningPage() {
|
||||
{/* Role 6: 风险审查 (Bottom) */}
|
||||
<button
|
||||
onClick={() => setSelectedAgentId(6)}
|
||||
className={`absolute -bottom-3.5 left-1/2 -translate-x-1/2 transition-transform duration-300 hover:scale-[1.18] active:scale-95 bg-white p-0.5 rounded-full border shadow-md cursor-pointer ${selectedAgentId === 6 || activeSpeakerId === 6 ? 'ring-4 ring-amber-400 scale-[1.12]' : 'border-slate-200'}`}
|
||||
className={`absolute -bottom-3.5 left-1/2 -translate-x-1/2 transition-transform duration-300 hover:scale-[1.18] active:scale-95 bg-white p-0.5 rounded-full border shadow-md cursor-pointer ${selectedAgentId === 6 || (activeSpeakerId === 6 || activeSpeakerSet.has(6)) ? 'ring-4 ring-amber-400 scale-[1.12]' : 'border-slate-200'}`}
|
||||
title="点击配置 风险审查 智能体人设与LLM"
|
||||
>
|
||||
<AmberRobotAvatar size="h-8.5 w-8.5" />
|
||||
<span className="absolute -bottom-4.5 left-1/2 -translate-x-1/2 text-[8px] bg-amber-900 text-white font-extrabold px-1 rounded whitespace-nowrap shadow-sm">风险审查</span>
|
||||
{(activeSpeakerId === 6 || selectedAgentId === 6) && (
|
||||
{((activeSpeakerId === 6 || activeSpeakerSet.has(6)) || selectedAgentId === 6) && (
|
||||
<span className="absolute -top-1 -right-1 flex h-2 w-2">
|
||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-amber-400 opacity-75"></span>
|
||||
<span className="relative inline-flex rounded-full h-2 w-2 bg-amber-500"></span>
|
||||
@@ -1563,12 +1744,12 @@ export default function RoundtablePlanningPage() {
|
||||
{/* Role 7: 执行规划 (Bottom Left) */}
|
||||
<button
|
||||
onClick={() => setSelectedAgentId(7)}
|
||||
className={`absolute bottom-5 -left-4 transition-transform duration-300 hover:scale-[1.18] active:scale-95 bg-white p-0.5 rounded-full border shadow-md cursor-pointer ${selectedAgentId === 7 || activeSpeakerId === 7 ? 'ring-4 ring-pink-400 scale-[1.12]' : 'border-slate-200'}`}
|
||||
className={`absolute bottom-5 -left-4 transition-transform duration-300 hover:scale-[1.18] active:scale-95 bg-white p-0.5 rounded-full border shadow-md cursor-pointer ${selectedAgentId === 7 || (activeSpeakerId === 7 || activeSpeakerSet.has(7)) ? 'ring-4 ring-pink-400 scale-[1.12]' : 'border-slate-200'}`}
|
||||
title="点击配置 执行规划 智能体人设与LLM"
|
||||
>
|
||||
<PinkRobotAvatar size="h-8.5 w-8.5" />
|
||||
<span className="absolute -bottom-4.5 left-1/2 -translate-x-1/2 text-[8px] bg-pink-900 text-white font-extrabold px-1 rounded whitespace-nowrap shadow-sm">执行规划</span>
|
||||
{(activeSpeakerId === 7 || selectedAgentId === 7) && (
|
||||
{((activeSpeakerId === 7 || activeSpeakerSet.has(7)) || selectedAgentId === 7) && (
|
||||
<span className="absolute -top-1 -right-1 flex h-2 w-2">
|
||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-pink-400 opacity-75"></span>
|
||||
<span className="relative inline-flex rounded-full h-2 w-2 bg-pink-500"></span>
|
||||
@@ -1579,12 +1760,12 @@ export default function RoundtablePlanningPage() {
|
||||
{/* Role 2: 总结陈述 (Top Left) */}
|
||||
<button
|
||||
onClick={() => setSelectedAgentId(2)}
|
||||
className={`absolute top-5 -left-4 transition-transform duration-300 hover:scale-[1.18] active:scale-95 bg-white p-0.5 rounded-full border shadow-md cursor-pointer ${selectedAgentId === 2 || activeSpeakerId === 2 ? 'ring-4 ring-primary/50 scale-[1.12]' : 'border-slate-200'}`}
|
||||
className={`absolute top-5 -left-4 transition-transform duration-300 hover:scale-[1.18] active:scale-95 bg-white p-0.5 rounded-full border shadow-md cursor-pointer ${selectedAgentId === 2 || (activeSpeakerId === 2 || activeSpeakerSet.has(2)) ? 'ring-4 ring-primary/50 scale-[1.12]' : 'border-slate-200'}`}
|
||||
title="点击配置 总结陈述 主持人"
|
||||
>
|
||||
<RobotAvatar bg="bg-[var(--brand-200)]" headColor="#cf1322" size="h-8.5 w-8.5" />
|
||||
<span className="absolute -bottom-4.5 left-1/2 -translate-x-1/2 text-[8px] bg-[var(--brand-900)] text-white font-extrabold px-1 rounded whitespace-nowrap shadow-sm">总结陈述</span>
|
||||
{(activeSpeakerId === 2 || selectedAgentId === 2) && (
|
||||
{((activeSpeakerId === 2 || activeSpeakerSet.has(2)) || selectedAgentId === 2) && (
|
||||
<span className="absolute -top-1 -right-1 flex h-2 w-2">
|
||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-primary/60 opacity-75"></span>
|
||||
<span className="relative inline-flex rounded-full h-2 w-2 bg-[var(--brand-200)]0"></span>
|
||||
@@ -1659,6 +1840,7 @@ export default function RoundtablePlanningPage() {
|
||||
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Selected Agent Persona Config Drawer / Drawer dialog box */}
|
||||
{selectedAgentId && (
|
||||
@@ -1791,9 +1973,9 @@ export default function RoundtablePlanningPage() {
|
||||
)}
|
||||
|
||||
{/* Conversation Log inside Step 2 */}
|
||||
<div
|
||||
ref={step2ScrollRef}
|
||||
className="flex-1 overflow-y-auto p-6 space-y-4 bg-white border-t border-slate-150 border-b border-slate-150 shadow-inner max-h-[350px] min-h-[140px]"
|
||||
<div
|
||||
ref={step2ScrollRef}
|
||||
className="flex-1 min-h-0 overflow-y-auto p-6 space-y-4 bg-white border-t border-slate-150 border-b border-slate-150 shadow-inner"
|
||||
>
|
||||
{step2RoundtableDialogues.map((msg) => (
|
||||
<div
|
||||
@@ -1813,8 +1995,11 @@ export default function RoundtablePlanningPage() {
|
||||
</div>
|
||||
<span className="text-[10px] text-slate-400 font-mono font-medium">{msg.time}</span>
|
||||
</div>
|
||||
<p className="text-xs text-slate-700 leading-relaxed font-medium">
|
||||
<p className="text-xs text-slate-700 leading-relaxed font-medium whitespace-pre-wrap break-words">
|
||||
{msg.content}
|
||||
{msg.streaming && (
|
||||
<span className="inline-block w-1 h-3 ml-0.5 bg-primary align-middle animate-pulse" />
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user