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

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>
);
}