194 lines
6.4 KiB
TypeScript
194 lines
6.4 KiB
TypeScript
import React, { useState, useEffect, useRef } from 'react';
|
|
import './AgentCard.css';
|
|
import MarkdownContent from './MarkdownContent';
|
|
|
|
function getPhaseBadgeClass(phase?: string): string {
|
|
switch (phase) {
|
|
case 'starting': return 'badge badge-starting';
|
|
case 'diverging': return 'badge badge-diverging';
|
|
case 'closing': return 'badge badge-closing';
|
|
case 'done': return 'badge badge-done';
|
|
default: return 'badge badge-default';
|
|
}
|
|
}
|
|
|
|
const PHASE_LABELS: Record<string, string> = {
|
|
starting: '启动',
|
|
diverging: '辩论',
|
|
closing: '收敛',
|
|
done: '完成',
|
|
};
|
|
|
|
type RetrievalLogItem = {
|
|
query?: string;
|
|
source_name?: string;
|
|
source_id?: string;
|
|
source?: string;
|
|
adopted?: boolean;
|
|
content?: any[];
|
|
content_ids?: string[];
|
|
};
|
|
|
|
type Props = {
|
|
agentId: string;
|
|
agentName?: string;
|
|
dimension?: string;
|
|
round: number;
|
|
phase: string;
|
|
content?: string;
|
|
thinking?: string;
|
|
dimensionOutput?: Record<string, any>;
|
|
retrievalLog?: RetrievalLogItem[];
|
|
retrievalCount?: number;
|
|
referenceMap?: Record<string, string>;
|
|
streaming?: boolean;
|
|
};
|
|
|
|
export default function AgentCard({
|
|
agentId,
|
|
agentName,
|
|
dimension,
|
|
round,
|
|
phase,
|
|
content,
|
|
thinking,
|
|
dimensionOutput,
|
|
retrievalLog,
|
|
retrievalCount = 0,
|
|
referenceMap,
|
|
streaming = false,
|
|
}: Props): JSX.Element {
|
|
const [thinkingOpen, setThinkingOpen] = useState(false);
|
|
const autoOpenedRef = useRef(false);
|
|
|
|
useEffect(() => {
|
|
if (streaming && thinking && thinking.length > 0 && !autoOpenedRef.current) {
|
|
setThinkingOpen(true);
|
|
autoOpenedRef.current = true;
|
|
}
|
|
}, [streaming, thinking]);
|
|
|
|
const streamingPhase = streaming
|
|
? (content && content.length > 0 ? 'content' : 'thinking')
|
|
: null;
|
|
|
|
const hasThinking = thinking && thinking.trim().length > 0;
|
|
const hasRetrieval = retrievalCount > 0 && retrievalLog && retrievalLog.length > 0;
|
|
const hasDimOutput = dimensionOutput && Object.keys(dimensionOutput).length > 0;
|
|
|
|
return (
|
|
<div className={`agent-card${streaming ? ' agent-card-streaming' : ''}`}>
|
|
<div className="agent-card-header">
|
|
<span className="agent-card-name">{agentName || agentId}</span>
|
|
<div className="agent-card-badges">
|
|
{dimension && (
|
|
<span className="badge badge-default">{dimension}</span>
|
|
)}
|
|
<span className="badge badge-default">第 {round} 轮</span>
|
|
<span className={getPhaseBadgeClass(phase)}>
|
|
{PHASE_LABELS[phase] || phase}
|
|
</span>
|
|
{streaming && (
|
|
<span className="badge badge-streaming">生成中</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{(hasThinking || streamingPhase === 'thinking') && (
|
|
<div className="agent-card-section">
|
|
<button
|
|
className="agent-card-thinking-toggle"
|
|
onClick={() => setThinkingOpen(o => !o)}
|
|
>
|
|
<span>{thinkingOpen ? '▾' : '▸'}</span>
|
|
<span>思考过程</span>
|
|
{streamingPhase === 'thinking' && (
|
|
<span className="streaming-cursor" style={{ marginLeft: 6 }} />
|
|
)}
|
|
</button>
|
|
{thinkingOpen && (
|
|
<div className="agent-card-thinking-text">
|
|
{streaming ? (
|
|
<span className="streaming-text" style={{ fontStyle: 'italic', fontSize: 12 }}>
|
|
{thinking}
|
|
</span>
|
|
) : (
|
|
<MarkdownContent content={thinking} />
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{(content || streamingPhase === 'content') && (
|
|
<div className="agent-card-section">
|
|
<div className="agent-card-section-title">分析内容</div>
|
|
<div className="agent-card-content-text">
|
|
{streaming ? (
|
|
<div className="streaming-text">
|
|
{content}
|
|
{streamingPhase === 'content' && <span className="streaming-cursor" />}
|
|
</div>
|
|
) : (
|
|
<MarkdownContent content={content} referenceMap={referenceMap} />
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{hasDimOutput && (
|
|
<div className="agent-card-section">
|
|
<div className="agent-card-section-title">维度输出</div>
|
|
<div className="dim-output-grid">
|
|
{Object.entries(dimensionOutput).map(([key, value]) => (
|
|
<div key={key} className="dim-output-item">
|
|
<div className="dim-output-key">{key}</div>
|
|
<div className="dim-output-value">
|
|
<MarkdownContent
|
|
content={typeof value === 'string' ? value : JSON.stringify(value, null, 2)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{hasRetrieval && (
|
|
<div className="agent-card-section">
|
|
<div className="agent-card-section-title">检索信息 ({retrievalCount})</div>
|
|
{retrievalLog.map((log, logIdx) => (
|
|
<div key={logIdx} className="retrieval-source-group">
|
|
<div className="retrieval-query-label">
|
|
<span>"{log.query}"</span>
|
|
{(log.source_name || log.source_id || log.source) && (
|
|
<span className="retrieval-source-label">{log.source_name || log.source_id || log.source}</span>
|
|
)}
|
|
{log.adopted !== undefined && (
|
|
<span className={log.adopted ? 'retrieval-adopted-badge' : 'retrieval-not-adopted-badge'}>
|
|
{log.adopted ? '已采用' : '未采用'}
|
|
</span>
|
|
)}
|
|
</div>
|
|
{log.content && log.content.map((item, itemIdx) => {
|
|
const refNum = log.content_ids ? log.content_ids[itemIdx] : null;
|
|
const refKey = refNum ? Object.entries(referenceMap || {}).find(([, v]) => v === refNum)?.[0] : null;
|
|
return (
|
|
<div key={itemIdx} className="retrieval-item">
|
|
{refKey && <span className="retrieval-item-num">[{refKey}]</span>}
|
|
<div className="retrieval-item-text">
|
|
{typeof item === 'string'
|
|
? (item.length > 200 ? item.slice(0, 200) + '…' : item)
|
|
: JSON.stringify(item).slice(0, 200)}
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|