'use client'; import * as React from 'react'; import { CheckCircle2, Loader2, RefreshCw, ThumbsUp, XCircle } from 'lucide-react'; import { TaskRuntimeStatusBadge, formatTaskRuntimeTime } from '@/components/task-runtime/TaskRuntimeShared'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Textarea } from '@/components/ui/textarea'; import { pickAppText } from '@/lib/i18n/core'; import { useAppI18n } from '@/lib/i18n/provider'; import type { TaskRuntimeStatus } from '@/lib/task-runtime'; import { containedPreservedLongTextClass } from '@/lib/text-wrapping'; export type TaskFeedbackType = 'accept' | 'revise' | 'abandon'; export type TaskFeedbackItem = { acceptance_type?: unknown; feedback_type?: unknown; comment?: unknown; created_at?: unknown; run_id?: unknown; }; type Props = { sessionId: string; runId: string | null; taskStatus: string; feedbackItems: TaskFeedbackItem[]; actionBusy: string | null; revision?: string; onRevisionChange?: (value: string) => void; onSubmit: (feedbackType: TaskFeedbackType, comment?: string) => Promise; }; const RUNTIME_STATUSES = new Set(['queued', 'running', 'waiting', 'blocked', 'done', 'error', 'cancelled']); const READY_FOR_ACCEPTANCE_STATUSES = new Set(['awaiting_acceptance', 'needs_revision']); function isRuntimeStatus(status: string): status is TaskRuntimeStatus { return RUNTIME_STATUSES.has(status); } function feedbackForRun(items: TaskFeedbackItem[], runId: string | null): TaskFeedbackItem | null { if (!runId) return null; return [...items].reverse().find((item) => String(item.run_id || '') === runId) ?? null; } function latestFeedback(items: TaskFeedbackItem[]): TaskFeedbackItem | null { return [...items].reverse()[0] ?? null; } function feedbackKind(item: TaskFeedbackItem): string { return String(item.acceptance_type || item.feedback_type || ''); } function humanFeedback(type: string, locale: 'zh-CN' | 'en-US') { if (type === 'accept' || type === 'satisfied') return pickAppText(locale, '接受', 'Accepted'); if (type === 'revise') return pickAppText(locale, '请求修改', 'Revision requested'); if (type === 'abandon') return pickAppText(locale, '放弃任务', 'Abandoned'); return type || pickAppText(locale, '验收', 'Acceptance'); } function humanTaskStatus(status: string, locale: 'zh-CN' | 'en-US') { const labels: Record = { open: ['已创建', 'Open'], running: ['执行中', 'Running'], awaiting_acceptance: ['等待验收', 'Awaiting acceptance'], needs_revision: ['需要修改', 'Needs revision'], closed: ['已完成', 'Closed'], abandoned: ['已放弃', 'Abandoned'], accept: ['接受', 'Accepted'], satisfied: ['接受', 'Accepted'], revise: ['请求修改', 'Revision requested'], abandon: ['放弃任务', 'Abandoned'], }; const label = labels[status]; return label ? pickAppText(locale, label[0], label[1]) : status; } function FeedbackButton({ type, icon, label, actionBusy, disabled, onClick, }: { type: TaskFeedbackType; icon: React.ReactNode; label: string; actionBusy: string | null; disabled: boolean; onClick: () => void; }) { const isBusy = actionBusy === type || Boolean(actionBusy?.endsWith(type)); return ( ); } export function TaskAcceptanceCard({ sessionId, runId, taskStatus, feedbackItems, actionBusy, revision, onRevisionChange, onSubmit, }: Props) { const { locale } = useAppI18n(); return (
{pickAppText(locale, '任务验收', 'Task acceptance')} {isRuntimeStatus(taskStatus) ? ( ) : ( {humanTaskStatus(taskStatus, locale)} )}
); } export function TaskAcceptanceControls({ sessionId, runId, taskStatus, feedbackItems, actionBusy, revision, onRevisionChange, onSubmit, }: Props) { const { locale } = useAppI18n(); const [localComment, setLocalComment] = React.useState(''); const comment = revision ?? localComment; const setComment = onRevisionChange ?? setLocalComment; const isFinalized = taskStatus === 'closed' || taskStatus === 'abandoned'; const isReadyForAcceptance = READY_FOR_ACCEPTANCE_STATUSES.has(taskStatus); const recordedFeedback = feedbackForRun(feedbackItems, runId) ?? (isFinalized ? latestFeedback(feedbackItems) : null); const canSubmit = Boolean(runId) && !recordedFeedback && !isFinalized && isReadyForAcceptance && !actionBusy; const trimmedComment = comment.trim(); const submit = (feedbackType: TaskFeedbackType, nextComment?: string) => { if (!runId || !canSubmit) return; void onSubmit(feedbackType, nextComment); }; return (
{recordedFeedback ? (
{pickAppText(locale, '已提交验收', 'Acceptance submitted')}: {humanFeedback(feedbackKind(recordedFeedback), locale)}
{recordedFeedback.comment ?

{String(recordedFeedback.comment)}

: null} {recordedFeedback.created_at ? (

{formatTaskRuntimeTime(String(recordedFeedback.created_at), locale)}

) : null}
) : isFinalized ? (
{pickAppText(locale, '任务已结束,不能再提交新的验收。', 'This task is finalized and cannot accept new acceptance.')}
) : !isReadyForAcceptance ? (
{pickAppText(locale, '任务还在执行,完成后才能验收。', 'The task is still running. Acceptance becomes available when a result is ready.')}
) : !runId ? (
{pickAppText(locale, '暂无可验收的运行记录。', 'No run is available for acceptance yet.')}
) : null}
} label={pickAppText(locale, '接受', 'Accept')} actionBusy={actionBusy} disabled={!canSubmit} onClick={() => submit('accept', trimmedComment || undefined)} /> } label={pickAppText(locale, '需要修改', 'Needs revision')} actionBusy={actionBusy} disabled={!canSubmit || !trimmedComment} onClick={() => submit('revise', trimmedComment)} /> } label={pickAppText(locale, '放弃', 'Abandon')} actionBusy={actionBusy} disabled={!canSubmit} onClick={() => submit('abandon', trimmedComment || undefined)} />