175 lines
6.5 KiB
TypeScript
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>
|
|
);
|
|
}
|