fix: tighten task detail component interactions
This commit is contained in:
@ -34,6 +34,7 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const RUNTIME_STATUSES = new Set<string>(['queued', 'running', 'waiting', 'blocked', 'done', 'error', 'cancelled']);
|
const RUNTIME_STATUSES = new Set<string>(['queued', 'running', 'waiting', 'blocked', 'done', 'error', 'cancelled']);
|
||||||
|
const READY_FOR_ACCEPTANCE_STATUSES = new Set<string>(['awaiting_acceptance', 'needs_revision']);
|
||||||
|
|
||||||
function isRuntimeStatus(status: string): status is TaskRuntimeStatus {
|
function isRuntimeStatus(status: string): status is TaskRuntimeStatus {
|
||||||
return RUNTIME_STATUSES.has(status);
|
return RUNTIME_STATUSES.has(status);
|
||||||
@ -59,6 +60,23 @@ function humanFeedback(type: string, locale: 'zh-CN' | 'en-US') {
|
|||||||
return type || pickAppText(locale, '验收', 'Acceptance');
|
return type || pickAppText(locale, '验收', 'Acceptance');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function humanTaskStatus(status: string, locale: 'zh-CN' | 'en-US') {
|
||||||
|
const labels: Record<string, [string, string]> = {
|
||||||
|
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({
|
function FeedbackButton({
|
||||||
type,
|
type,
|
||||||
icon,
|
icon,
|
||||||
@ -99,8 +117,9 @@ export function TaskAcceptanceCard({
|
|||||||
const comment = revision ?? localComment;
|
const comment = revision ?? localComment;
|
||||||
const setComment = onRevisionChange ?? setLocalComment;
|
const setComment = onRevisionChange ?? setLocalComment;
|
||||||
const isFinalized = taskStatus === 'closed' || taskStatus === 'abandoned';
|
const isFinalized = taskStatus === 'closed' || taskStatus === 'abandoned';
|
||||||
|
const isReadyForAcceptance = READY_FOR_ACCEPTANCE_STATUSES.has(taskStatus);
|
||||||
const recordedFeedback = feedbackForRun(feedbackItems, runId) ?? (isFinalized ? latestFeedback(feedbackItems) : null);
|
const recordedFeedback = feedbackForRun(feedbackItems, runId) ?? (isFinalized ? latestFeedback(feedbackItems) : null);
|
||||||
const canSubmit = Boolean(runId) && !recordedFeedback && !isFinalized && !actionBusy;
|
const canSubmit = Boolean(runId) && !recordedFeedback && !isFinalized && isReadyForAcceptance && !actionBusy;
|
||||||
const trimmedComment = comment.trim();
|
const trimmedComment = comment.trim();
|
||||||
|
|
||||||
const submit = (feedbackType: TaskFeedbackType, nextComment?: string) => {
|
const submit = (feedbackType: TaskFeedbackType, nextComment?: string) => {
|
||||||
@ -117,7 +136,7 @@ export function TaskAcceptanceCard({
|
|||||||
<TaskRuntimeStatusBadge status={taskStatus} />
|
<TaskRuntimeStatusBadge status={taskStatus} />
|
||||||
) : (
|
) : (
|
||||||
<Badge variant="outline" className="text-[11px]">
|
<Badge variant="outline" className="text-[11px]">
|
||||||
{taskStatus}
|
{humanTaskStatus(taskStatus, locale)}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -138,6 +157,10 @@ export function TaskAcceptanceCard({
|
|||||||
<div className="rounded-md border border-border bg-muted/25 p-3 text-sm text-muted-foreground">
|
<div className="rounded-md border border-border bg-muted/25 p-3 text-sm text-muted-foreground">
|
||||||
{pickAppText(locale, '任务已结束,不能再提交新的验收。', 'This task is finalized and cannot accept new acceptance.')}
|
{pickAppText(locale, '任务已结束,不能再提交新的验收。', 'This task is finalized and cannot accept new acceptance.')}
|
||||||
</div>
|
</div>
|
||||||
|
) : !isReadyForAcceptance ? (
|
||||||
|
<div className="rounded-md border border-border bg-muted/25 p-3 text-sm text-muted-foreground">
|
||||||
|
{pickAppText(locale, '任务还在执行,完成后才能验收。', 'The task is still running. Acceptance becomes available when a result is ready.')}
|
||||||
|
</div>
|
||||||
) : !runId ? (
|
) : !runId ? (
|
||||||
<div className="rounded-md border border-border bg-muted/25 p-3 text-sm text-muted-foreground">
|
<div className="rounded-md border border-border bg-muted/25 p-3 text-sm text-muted-foreground">
|
||||||
{pickAppText(locale, '暂无可验收的运行记录。', 'No run is available for acceptance yet.')}
|
{pickAppText(locale, '暂无可验收的运行记录。', 'No run is available for acceptance yet.')}
|
||||||
@ -174,7 +197,7 @@ export function TaskAcceptanceCard({
|
|||||||
<Textarea
|
<Textarea
|
||||||
value={comment}
|
value={comment}
|
||||||
onChange={(event) => setComment(event.target.value)}
|
onChange={(event) => setComment(event.target.value)}
|
||||||
disabled={Boolean(recordedFeedback) || isFinalized || Boolean(actionBusy)}
|
disabled={Boolean(recordedFeedback) || isFinalized || !isReadyForAcceptance || Boolean(actionBusy)}
|
||||||
placeholder={pickAppText(locale, '需要修改时写下具体要求;接受或放弃可选填说明。', 'Describe requested changes; notes are optional for accept or abandon.')}
|
placeholder={pickAppText(locale, '需要修改时写下具体要求;接受或放弃可选填说明。', 'Describe requested changes; notes are optional for accept or abandon.')}
|
||||||
/>
|
/>
|
||||||
<div className="text-xs text-muted-foreground">
|
<div className="text-xs text-muted-foreground">
|
||||||
|
|||||||
@ -55,6 +55,40 @@ function artifactHref(artifact: ProcessArtifact): string | null {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function inlineArtifactPayload(artifact: ProcessArtifact): { content: string; filename: string; mimeType: string } | null {
|
||||||
|
const baseName = (artifact.title || artifact.artifact_id || 'artifact').replace(/[\\/:*?"<>|]+/g, '-');
|
||||||
|
if (artifact.content !== undefined) {
|
||||||
|
const isMarkdown = artifact.artifact_type === 'markdown';
|
||||||
|
return {
|
||||||
|
content: artifact.content,
|
||||||
|
filename: `${baseName}.${isMarkdown ? 'md' : 'txt'}`,
|
||||||
|
mimeType: isMarkdown ? 'text/markdown;charset=utf-8' : 'text/plain;charset=utf-8',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (artifact.data !== undefined) {
|
||||||
|
return {
|
||||||
|
content: JSON.stringify(artifact.data, null, 2),
|
||||||
|
filename: `${baseName}.json`,
|
||||||
|
mimeType: 'application/json;charset=utf-8',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadInlineArtifact(artifact: ProcessArtifact): void {
|
||||||
|
const payload = inlineArtifactPayload(artifact);
|
||||||
|
if (!payload) return;
|
||||||
|
|
||||||
|
const url = URL.createObjectURL(new Blob([payload.content], { type: payload.mimeType }));
|
||||||
|
const anchor = document.createElement('a');
|
||||||
|
anchor.href = url;
|
||||||
|
anchor.download = payload.filename;
|
||||||
|
document.body.appendChild(anchor);
|
||||||
|
anchor.click();
|
||||||
|
anchor.remove();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
|
|
||||||
function RunRow({ run }: { run: ProcessRun }) {
|
function RunRow({ run }: { run: ProcessRun }) {
|
||||||
const { locale } = useAppI18n();
|
const { locale } = useAppI18n();
|
||||||
|
|
||||||
@ -165,6 +199,7 @@ export function TaskSideRail({ task, runs, artifacts, cards }: Props) {
|
|||||||
) : (
|
) : (
|
||||||
artifacts.map((artifact) => {
|
artifacts.map((artifact) => {
|
||||||
const href = artifactHref(artifact);
|
const href = artifactHref(artifact);
|
||||||
|
const inlinePayload = inlineArtifactPayload(artifact);
|
||||||
return (
|
return (
|
||||||
<div key={artifact.artifact_id} className="flex items-center justify-between gap-3 rounded-md border border-border bg-muted/20 p-3">
|
<div key={artifact.artifact_id} className="flex items-center justify-between gap-3 rounded-md border border-border bg-muted/20 p-3">
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
@ -181,6 +216,11 @@ export function TaskSideRail({ task, runs, artifacts, cards }: Props) {
|
|||||||
{artifact.url ? pickAppText(locale, '打开', 'Open') : pickAppText(locale, '下载', 'Download')}
|
{artifact.url ? pickAppText(locale, '打开', 'Open') : pickAppText(locale, '下载', 'Download')}
|
||||||
</a>
|
</a>
|
||||||
</Button>
|
</Button>
|
||||||
|
) : inlinePayload ? (
|
||||||
|
<Button size="sm" variant="outline" className="shrink-0" onClick={() => downloadInlineArtifact(artifact)}>
|
||||||
|
<Download className="mr-2 h-3.5 w-3.5" />
|
||||||
|
{pickAppText(locale, '下载', 'Download')}
|
||||||
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -93,6 +93,24 @@ function cardTypeLabel(type: TaskTimelineCardType, locale: 'zh-CN' | 'en-US') {
|
|||||||
return pickAppText(locale, label[0], label[1]);
|
return pickAppText(locale, label[0], label[1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function humanStatus(status: string, locale: 'zh-CN' | 'en-US') {
|
||||||
|
const labels: Record<string, [string, string]> = {
|
||||||
|
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'],
|
||||||
|
warning: ['提醒', 'Warning'],
|
||||||
|
};
|
||||||
|
const label = labels[status];
|
||||||
|
return label ? pickAppText(locale, label[0], label[1]) : status;
|
||||||
|
}
|
||||||
|
|
||||||
export function TaskTimelineCard({ card }: Props) {
|
export function TaskTimelineCard({ card }: Props) {
|
||||||
const { locale } = useAppI18n();
|
const { locale } = useAppI18n();
|
||||||
const Icon = iconForType(card.type);
|
const Icon = iconForType(card.type);
|
||||||
@ -105,10 +123,10 @@ export function TaskTimelineCard({ card }: Props) {
|
|||||||
<Icon className="h-4 w-4 text-muted-foreground" />
|
<Icon className="h-4 w-4 text-muted-foreground" />
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<div className="flex flex-wrap items-start justify-between gap-3">
|
<div className="flex items-start justify-between gap-3">
|
||||||
<div className="min-w-0">
|
<div className="min-w-0 flex-1">
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="flex min-w-0 items-center gap-2">
|
||||||
<h3 className="truncate text-sm font-semibold">{card.title}</h3>
|
<h3 className="min-w-0 flex-1 truncate text-sm font-semibold">{card.title}</h3>
|
||||||
<Badge variant="secondary" className="shrink-0 text-[11px]">
|
<Badge variant="secondary" className="shrink-0 text-[11px]">
|
||||||
{cardTypeLabel(card.type, locale)}
|
{cardTypeLabel(card.type, locale)}
|
||||||
</Badge>
|
</Badge>
|
||||||
@ -123,8 +141,8 @@ export function TaskTimelineCard({ card }: Props) {
|
|||||||
isRuntimeStatus(card.status) ? (
|
isRuntimeStatus(card.status) ? (
|
||||||
<TaskRuntimeStatusBadge status={card.status} />
|
<TaskRuntimeStatusBadge status={card.status} />
|
||||||
) : (
|
) : (
|
||||||
<Badge variant="outline" className="text-[11px]">
|
<Badge variant="outline" className="shrink-0 text-[11px]">
|
||||||
{card.status}
|
{humanStatus(card.status, locale)}
|
||||||
</Badge>
|
</Badge>
|
||||||
)
|
)
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
Reference in New Issue
Block a user