Files
deerflow-code/web-code/components/qa/components/Sidebar.tsx
T
2026-05-27 19:43:15 +08:00

175 lines
6.5 KiB
TypeScript

import './Sidebar.css';
type Agent = {
id: string;
name?: string;
dimension?: string;
};
type AgentState = {
status?: string;
name?: string;
dimension?: string;
};
type Props = {
agents: Agent[];
agentStates: Record<string, AgentState>;
phase: string;
currentRound: number;
divergeRounds: number;
};
const PHASES = [
{ id: 'starting', label: '启动', dotLabel: '1' },
{ id: 'diverging', label: '辩论', dotLabel: '2' },
{ id: 'closing', label: '收敛', dotLabel: '3' },
{ id: 'done', label: '完成', dotLabel: '✓' },
];
const PHASE_ORDER = ['starting', 'diverging', 'closing', 'done'];
function getPhaseIndex(phase: string): number {
return PHASE_ORDER.indexOf(phase);
}
function scrollToTimelineElement(selector: string): void {
const appMain = document.querySelector('.app-main') as HTMLElement | null;
if (!appMain) return;
const el = appMain.querySelector(selector) as HTMLElement | null;
if (!el) return;
const targetRect = el.getBoundingClientRect();
const containerRect = appMain.getBoundingClientRect();
const targetScrollTop = appMain.scrollTop + targetRect.top - containerRect.top - 20;
(window as any).__mopeProgrammaticScroll = ((window as any).__mopeProgrammaticScroll || 0) + 1;
appMain.scrollTo({ top: targetScrollTop, behavior: 'smooth' });
}
function scrollToDivergeRound(roundNum: number): void {
const appMain = document.querySelector('.app-main') as HTMLElement | null;
if (!appMain) return;
const divergeRounds = appMain.querySelectorAll('[data-phase="diverging"][data-round]');
const target = divergeRounds[roundNum - 1] as HTMLElement | undefined;
if (!target) return;
const targetRect = target.getBoundingClientRect();
const containerRect = appMain.getBoundingClientRect();
const targetScrollTop = appMain.scrollTop + targetRect.top - containerRect.top - 20;
(window as any).__mopeProgrammaticScroll = ((window as any).__mopeProgrammaticScroll || 0) + 1;
appMain.scrollTo({ top: targetScrollTop, behavior: 'smooth' });
}
export default function Sidebar({
agents,
agentStates,
phase,
currentRound,
divergeRounds,
}: Props): JSX.Element {
const currentPhaseIdx = getPhaseIndex(phase);
return (
<div className="sidebar">
<div className="sidebar-section">
<div className="sidebar-section-title">进度</div>
<div className="phase-stepper">
{PHASES.map((p, idx) => {
const isActive = phase === p.id;
const isDone = currentPhaseIdx > idx;
const stepClass = `phase-step phase-${p.id}${isActive ? ' active' : ''}${isDone ? ' done' : ''}`;
return (
<div
key={p.id}
className={stepClass}
onClick={() => scrollToTimelineElement(`[data-phase="${p.id}"]`)}
title={`点击跳转到${p.label}阶段`}
style={{ cursor: 'pointer' }}
>
{idx < PHASES.length - 1 && <div className="phase-step-line" />}
<div className="phase-step-dot-col">
<div className="phase-step-dot">
{isDone ? '✓' : p.dotLabel}
</div>
</div>
<div className="phase-step-content">
<div className="phase-step-label">{p.label}</div>
{p.id === 'diverging' && (isDone || isActive) && divergeRounds > 0 && (
<div className="diverge-rounds">
{Array.from({ length: divergeRounds }, (_, i) => {
const roundNum = i + 1;
const isActiveDiverge = isActive && roundNum === (currentRound - 1);
const isDoneDiverge = isDone || (isActive && (currentRound - 1) > roundNum);
const itemClass = `diverge-round-item${isActiveDiverge ? ' active' : ''}${isDoneDiverge ? ' done' : ''}`;
return (
<div
key={roundNum}
className={itemClass}
onClick={(e) => {
e.stopPropagation();
scrollToDivergeRound(roundNum);
}}
title={`点击跳转到辩论轮 ${roundNum}`}
style={{ cursor: 'pointer' }}
>
<div className="diverge-round-dot" />
<span>辩论轮 {roundNum}</span>
</div>
);
})}
</div>
)}
{p.id === 'diverging' && !isDone && !isActive && divergeRounds > 0 && (
<div className="phase-step-sublabel">{divergeRounds} 轮</div>
)}
</div>
</div>
);
})}
</div>
</div>
{agents && agents.length > 0 && (
<div className="sidebar-section">
<div className="sidebar-section-title">智能体</div>
<div className="agent-list">
{agents.map(agent => {
const agentState = agentStates[agent.id] || {};
const status = agentState.status || 'idle';
const handleClick = () => {
const cards = document.querySelectorAll(`[data-agent-id="${agent.id}"]`);
if (cards.length > 0) {
(cards[cards.length - 1] as HTMLElement).scrollIntoView({ behavior: 'smooth', block: 'start' });
}
};
return (
<div
key={agent.id}
className={`agent-item agent-item-clickable${status === 'thinking' ? ' agent-item-active' : ''}`}
onClick={handleClick}
title="点击跳转到最新输出"
>
<div className="agent-status-icon">
{status === 'idle' && <span className="agent-status-idle">○</span>}
{status === 'thinking' && <span className="agent-status-thinking spinner">⟳</span>}
{status === 'done' && <span className="agent-status-done">✓</span>}
</div>
<div className="agent-item-info">
<div className="agent-item-name">{agent.name || agent.id}</div>
<div className="agent-item-dim">{agent.dimension}</div>
</div>
</div>
);
})}
</div>
</div>
)}
</div>
);
}