147 lines
5.7 KiB
TypeScript
147 lines
5.7 KiB
TypeScript
import type { ReactNode } from 'react';
|
||
import { Hand } from './Icons';
|
||
import type { Task } from '../types';
|
||
|
||
export type CardKind = 'confirm' | 'blocker' | 'question' | 'result' | 'danger' | 'info';
|
||
|
||
export interface CardAction {
|
||
label: string;
|
||
kind?: 'primary' | 'secondary' | 'danger' | 'text';
|
||
disabled?: boolean;
|
||
onClick: () => void;
|
||
}
|
||
|
||
export function ActionCard({ kind, title, body, actions = [], children }: {
|
||
kind: CardKind;
|
||
title: string;
|
||
body?: string;
|
||
actions?: CardAction[];
|
||
children?: ReactNode;
|
||
}) {
|
||
return (
|
||
<section className={`card ${kind}`} data-kind={kind}>
|
||
<header className="card-head">{title}</header>
|
||
{body && <div className="card-body">{body}</div>}
|
||
{children}
|
||
{actions.length > 0 && (
|
||
<div className="card-actions">
|
||
{actions.map(action => (
|
||
<button
|
||
key={action.label}
|
||
type="button"
|
||
className={`${action.kind ?? 'secondary'}-button small`}
|
||
disabled={action.disabled}
|
||
onClick={action.onClick}
|
||
>
|
||
{action.label}
|
||
</button>
|
||
))}
|
||
<span className="card-hint">也可以直接在下方输入回答</span>
|
||
</div>
|
||
)}
|
||
</section>
|
||
);
|
||
}
|
||
|
||
export type PauseKind = 'confirm' | 'blocker-self' | 'blocker-other' | 'question';
|
||
|
||
export function classifyPause(task: Task): PauseKind {
|
||
const pending = task.inputs.pending_blocker;
|
||
if (pending) return pending.resolution === 'self' ? 'blocker-self' : 'blocker-other';
|
||
const question = task.pause_reason ?? '';
|
||
if ((question.includes('确认') && question.includes('提交')) || question.includes('即将提交')) return 'confirm';
|
||
return 'question';
|
||
}
|
||
|
||
export function PauseCard({ task, pending, onAnswer, onTakeover }: {
|
||
task: Task;
|
||
pending: boolean;
|
||
onAnswer: (text: string) => void;
|
||
onTakeover: () => void;
|
||
}) {
|
||
const kind = classifyPause(task);
|
||
const question = task.pause_reason ?? '';
|
||
const takeover: CardAction = { label: '我来接管', kind: 'secondary', onClick: onTakeover, disabled: pending };
|
||
if (kind === 'confirm') {
|
||
return (
|
||
<ActionCard kind="confirm" title="请确认后再提交" body={question} actions={[
|
||
{ label: '确认提交', kind: 'primary', disabled: pending, onClick: () => onAnswer('确认提交') },
|
||
{ label: '取消,不提交', kind: 'danger', disabled: pending, onClick: () => onAnswer('取消,不要提交') },
|
||
takeover,
|
||
]} />
|
||
);
|
||
}
|
||
if (kind === 'blocker-self') {
|
||
return (
|
||
<ActionCard kind="blocker" title="遇到前置事项" body={question} actions={[
|
||
{ label: '是,先去办', kind: 'primary', disabled: pending, onClick: () => onAnswer('是,先去办') },
|
||
{ label: '否,先结束', kind: 'text', disabled: pending, onClick: () => onAnswer('否') },
|
||
]} />
|
||
);
|
||
}
|
||
if (kind === 'blocker-other') {
|
||
return (
|
||
<ActionCard kind="blocker" title="需要别人先处理" body={question} actions={[
|
||
{ label: '已处理,继续', kind: 'primary', disabled: pending, onClick: () => onAnswer('已处理,请继续') },
|
||
{ label: '先结束', kind: 'text', disabled: pending, onClick: () => onAnswer('取消') },
|
||
]} />
|
||
);
|
||
}
|
||
return <ActionCard kind="question" title="助手有个问题" body={question} actions={[takeover]} />;
|
||
}
|
||
|
||
export function ResultCard({ task, onRetry }: { task: Task; onRetry: () => void }) {
|
||
const summary = task.artifacts.summary ?? task.reply ?? '';
|
||
if (task.status === 'completed' || task.status === 'submitted') {
|
||
const extras = Object.entries(task.artifacts).filter(([key, value]) =>
|
||
!key.startsWith('summary') && key !== 'tracking' && typeof value !== 'object');
|
||
return (
|
||
<ActionCard kind="result" title={task.status === 'submitted' ? '已提交,等待审批' : '已完成'} body={summary}>
|
||
{extras.length > 0 && (
|
||
<dl className="card-facts">
|
||
{extras.map(([key, value]) => <div key={key}><dt>{key}</dt><dd>{String(value)}</dd></div>)}
|
||
</dl>
|
||
)}
|
||
{task.status === 'submitted' && (
|
||
<p className="card-note">我会定时回查审批状态,有结果时告诉你。也可以在下方点「立即回查」。</p>
|
||
)}
|
||
</ActionCard>
|
||
);
|
||
}
|
||
if (task.status === 'failed') {
|
||
return (
|
||
<ActionCard kind="danger" title="办理未完成" body={task.pause_reason ?? '未知原因'} actions={[
|
||
{ label: '重试', kind: 'primary', onClick: onRetry },
|
||
]} />
|
||
);
|
||
}
|
||
if (task.status === 'blocked') {
|
||
return (
|
||
<ActionCard kind="blocker" title="被前置事项拦住" body={`${task.pause_reason ?? ''}\n处理好之后点「继续执行」我再重试。`} />
|
||
);
|
||
}
|
||
if (task.status === 'rejected') {
|
||
return <ActionCard kind="danger" title="审批被驳回" body={task.pause_reason ?? ''} />;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
export function TakeoverCard({ onDone }: { onDone: () => void }) {
|
||
return (
|
||
<ActionCard kind="info" title="你已接管浏览器" body="Agent 会在当前这一步做完后停下等你。直接在右侧页面里操作,完成后点「接管完成,继续」,Agent 会从页面当前状态接着做。" actions={[
|
||
{ label: '接管完成,继续', kind: 'primary', onClick: onDone },
|
||
]} />
|
||
);
|
||
}
|
||
|
||
export function UserInputHint({ onTakeover, onDismiss }: { onTakeover: () => void; onDismiss: () => void }) {
|
||
return (
|
||
<ActionCard kind="info" title="页面正在由 Agent 操作" body="现在键盘输入会被拦住。如果你想自己操作,先点「我来接管」。" actions={[
|
||
{ label: '我来接管', kind: 'primary', onClick: onTakeover },
|
||
{ label: '知道了', kind: 'text', onClick: onDismiss },
|
||
]}>
|
||
<span className="sr-only"><Hand size={12} /></span>
|
||
</ActionCard>
|
||
);
|
||
}
|