assistant/renderer/components/Cards.tsx

147 lines
5.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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