73 lines
3.5 KiB
TypeScript
73 lines
3.5 KiB
TypeScript
import { useState } from 'react';
|
||
import { ChevronDown, ChevronRight, FileText } from './Icons';
|
||
import { CATEGORY_LABEL, STATUS_LABEL, type LearnedEntry, type Task } from '../types';
|
||
|
||
function screenshotUrl(path: string): string {
|
||
return `file:///${path.replace(/\\/g, '/')}?t=${Date.now()}`;
|
||
}
|
||
|
||
export function TaskCard({ task, timeline, learned, operatorMode, embedded, onOpenDoc, onLearned }: {
|
||
task: Task;
|
||
timeline: string[];
|
||
learned: LearnedEntry[];
|
||
operatorMode: boolean;
|
||
embedded: boolean;
|
||
onOpenDoc: () => void;
|
||
onLearned: (id: number, action: 'verify' | 'reject') => void;
|
||
}) {
|
||
const [showTimeline, setShowTimeline] = useState(false);
|
||
const tracking = task.artifacts.tracking;
|
||
const last = tracking?.history?.[tracking.history.length - 1];
|
||
const reasonLabel = task.status === 'failed' ? '原因' : task.status === 'blocked' ? '阻断' : task.status === 'rejected' ? '驳回' : null;
|
||
|
||
return (
|
||
<article className="task-card" data-status={task.status} data-testid="task-card">
|
||
<header className="task-card-head">
|
||
<strong>{task.business_name ?? '任务'}</strong>
|
||
<span className={`status-chip ${task.status}`}>{STATUS_LABEL[task.status] ?? task.status}</span>
|
||
</header>
|
||
<dl className="task-facts">
|
||
<div><dt>步骤</dt><dd>{task.current_step ?? '-'}</dd></div>
|
||
<div><dt>创建</dt><dd>{task.created_at}</dd></div>
|
||
{task.status === 'submitted' && (
|
||
<div><dt>审批</dt><dd>{last ? `${last.status}(${last.at})` : '待回查'}{task.next_check_at ? `,下次回查 ${task.next_check_at}` : ''}</dd></div>
|
||
)}
|
||
{reasonLabel && task.pause_reason && <div><dt>{reasonLabel}</dt><dd>{task.pause_reason.slice(0, 240)}</dd></div>}
|
||
</dl>
|
||
{!embedded && task.latest_screenshot && (
|
||
<img className="task-shot" src={screenshotUrl(task.latest_screenshot)} alt="Agent 最近一步的页面截图" />
|
||
)}
|
||
<div className="task-card-tools">
|
||
<button type="button" className="text-action" onClick={() => setShowTimeline(v => !v)} aria-expanded={showTimeline}>
|
||
{showTimeline ? <ChevronDown size={14} /> : <ChevronRight size={14} />}办理记录 · {timeline.length}
|
||
</button>
|
||
<button type="button" className="text-action" onClick={onOpenDoc} disabled={!task.progress_doc_path}>
|
||
<FileText size={14} />打开 progress.md
|
||
</button>
|
||
</div>
|
||
{showTimeline && (
|
||
<ol className="task-timeline">
|
||
{timeline.slice(-12).map((line, index) => <li key={`${index}-${line.slice(0, 24)}`}>{line}</li>)}
|
||
</ol>
|
||
)}
|
||
{operatorMode && learned.length > 0 && (
|
||
<section className="learned-block" aria-label="沉淀草稿">
|
||
<div className="section-label">沉淀草稿 · 运维模式</div>
|
||
<ul className="learned-list">
|
||
{learned.map(entry => (
|
||
<li key={entry.id} className="learned-item">
|
||
<div className="learned-key">{entry.key} <span className="muted">{CATEGORY_LABEL[entry.category] ?? entry.category} · 观察 {entry.observations} 次</span></div>
|
||
<div className="learned-value">{entry.value}</div>
|
||
<div className="learned-actions">
|
||
<button type="button" className="primary-button small" onClick={() => onLearned(entry.id, 'verify')}>采纳</button>
|
||
<button type="button" className="text-action" onClick={() => onLearned(entry.id, 'reject')}>忽略</button>
|
||
</div>
|
||
</li>
|
||
))}
|
||
</ul>
|
||
</section>
|
||
)}
|
||
</article>
|
||
);
|
||
}
|