- 引入AgentTeamOrchestrator支持多agent协同任务执行 - 增加第三方swarms库依赖并配置git协议替换以改善包管理 - 扩展DelegationManager支持团队任务调度和进度跟踪 - 实现中文bigram分词算法提升中文任务检索准确性 - 调整A2AClient和DelegationManager超时时间从30秒增至600秒 - 优化AgentRunResult状态判断逻辑增加有意义摘要检测 - 修改Dockerfile配置npm仓库镜像地址和git协议映射 - 更新CLI命令行接口支持网关端口配置传递 - 调整提供者超时配置机制增强请求稳定性 - 移除过时的support_group字段简化agent描述符结构 - 增强错误处理和进度事件报告机制改进用户体验
654 lines
31 KiB
TypeScript
654 lines
31 KiB
TypeScript
'use client';
|
|
|
|
import Link from 'next/link';
|
|
import { useParams } from 'next/navigation';
|
|
import React from 'react';
|
|
import {
|
|
ArrowLeft,
|
|
ArrowRight,
|
|
Boxes,
|
|
FolderOutput,
|
|
ListTree,
|
|
MessageSquare,
|
|
PanelRightOpen,
|
|
Siren,
|
|
Users,
|
|
} from 'lucide-react';
|
|
|
|
import {
|
|
OfficeStatusBadge,
|
|
formatOfficeDuration,
|
|
formatOfficeTime,
|
|
progressPercent,
|
|
} from '@/components/office/OfficeShared';
|
|
import { OfficePhaserCanvas } from '@/components/office/OfficePhaserCanvas';
|
|
import { TaskManagementTabs } from '@/components/task-management/TaskManagementTabs';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
|
import {
|
|
Sheet,
|
|
SheetContent,
|
|
SheetDescription,
|
|
SheetHeader,
|
|
SheetTitle,
|
|
} from '@/components/ui/sheet';
|
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
|
import { buildOfficeView, isOfficeTaskTerminal } from '@/lib/office';
|
|
import { appEventKindLabel } from '@/lib/i18n/common';
|
|
import { pickAppText } from '@/lib/i18n/core';
|
|
import { useAppI18n } from '@/lib/i18n/provider';
|
|
import { useChatStore } from '@/lib/store';
|
|
|
|
function traceMetadataLabels(locale: 'zh-CN' | 'en-US'): Record<string, string> {
|
|
return {
|
|
stage_label: pickAppText(locale, '阶段', 'Stage'),
|
|
source: pickAppText(locale, '来源', 'Source'),
|
|
phase: 'Phase',
|
|
step: 'Step',
|
|
selection_mode: pickAppText(locale, '选人方式', 'Selection mode'),
|
|
selected_mode: pickAppText(locale, '选中模式', 'Selected mode'),
|
|
execution_mode: pickAppText(locale, '执行模式', 'Execution mode'),
|
|
selected_targets: pickAppText(locale, '成员', 'Members'),
|
|
selected_count: pickAppText(locale, '成员数', 'Member count'),
|
|
requested_targets: pickAppText(locale, '请求成员', 'Requested targets'),
|
|
planned_targets: pickAppText(locale, '计划成员', 'Planned targets'),
|
|
matched_procedure_id: pickAppText(locale, '命中 Procedure', 'Matched procedure'),
|
|
candidate_procedure_id: pickAppText(locale, '候选 Procedure', 'Candidate procedure'),
|
|
announcement_path: pickAppText(locale, '回流路径', 'Announcement path'),
|
|
announcement_sender_id: pickAppText(locale, '回流 Sender', 'Announcement sender'),
|
|
announcement_category: pickAppText(locale, '回流类别', 'Announcement category'),
|
|
external_fallback_reason: pickAppText(locale, '外部回退原因', 'External fallback reason'),
|
|
failure_type: pickAppText(locale, '失败分类', 'Failure type'),
|
|
failure_reason: pickAppText(locale, '失败原因', 'Failure reason'),
|
|
error: pickAppText(locale, '错误', 'Error'),
|
|
origin_channel: pickAppText(locale, '来源 Channel', 'Origin channel'),
|
|
origin_chat_id: pickAppText(locale, '来源 Chat', 'Origin chat'),
|
|
};
|
|
}
|
|
|
|
function formatTraceValue(value: unknown): string | null {
|
|
if (value === null || value === undefined) return null;
|
|
if (typeof value === 'string') {
|
|
const trimmed = value.trim();
|
|
return trimmed || null;
|
|
}
|
|
if (typeof value === 'number' || typeof value === 'boolean') return String(value);
|
|
if (Array.isArray(value)) {
|
|
const parts = value
|
|
.map((item) => formatTraceValue(item))
|
|
.filter((item): item is string => Boolean(item));
|
|
return parts.length > 0 ? parts.join(', ') : null;
|
|
}
|
|
try {
|
|
return JSON.stringify(value);
|
|
} catch {
|
|
return String(value);
|
|
}
|
|
}
|
|
|
|
function traceMetadataEntries(
|
|
metadata: Record<string, unknown> | null | undefined,
|
|
labels: Record<string, string>
|
|
): Array<{ key: string; label: string; value: string }> {
|
|
if (!metadata) return [];
|
|
|
|
const entries: Array<{ key: string; label: string; value: string }> = [];
|
|
const used = new Set<string>();
|
|
|
|
for (const [key, label] of Object.entries(labels)) {
|
|
const value = formatTraceValue(metadata[key]);
|
|
if (!value) continue;
|
|
used.add(key);
|
|
entries.push({ key, label, value });
|
|
}
|
|
|
|
for (const [key, rawValue] of Object.entries(metadata)) {
|
|
if (used.has(key)) continue;
|
|
const value = formatTraceValue(rawValue);
|
|
if (!value) continue;
|
|
entries.push({ key, label: key, value });
|
|
}
|
|
|
|
return entries;
|
|
}
|
|
|
|
function PixelPanel({
|
|
title,
|
|
subtitle,
|
|
children,
|
|
icon: Icon,
|
|
}: {
|
|
title: string;
|
|
subtitle?: string;
|
|
children: React.ReactNode;
|
|
icon?: React.ComponentType<{ className?: string }>;
|
|
}) {
|
|
return (
|
|
<div className="rounded-none border-4 border-[#0e1119] bg-[#141722] p-4 text-slate-100 shadow-[0_0_0_2px_#1a1b2f_inset]">
|
|
<div className="flex items-center gap-2 font-mono text-sm font-bold uppercase tracking-[0.18em] text-[#fef3c7]">
|
|
{Icon ? <Icon className="h-4 w-4" /> : null}
|
|
{title}
|
|
</div>
|
|
{subtitle ? (
|
|
<div className="mt-2 text-xs text-slate-400">{subtitle}</div>
|
|
) : null}
|
|
<div className="mt-4">{children}</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function BoardPanel({
|
|
icon: Icon,
|
|
title,
|
|
description,
|
|
children,
|
|
}: {
|
|
icon: React.ComponentType<{ className?: string }>;
|
|
title: string;
|
|
description?: string;
|
|
children: React.ReactNode;
|
|
}) {
|
|
return (
|
|
<Card className="rounded-none border-4 border-[#0e1119] bg-[#141722] text-slate-100 shadow-[0_0_0_2px_#1a1b2f_inset]">
|
|
<CardHeader className="border-b border-[#262a3d] pb-3">
|
|
<CardTitle className="flex items-center gap-2 text-base text-[#fef3c7]">
|
|
<Icon className="h-4 w-4" />
|
|
{title}
|
|
</CardTitle>
|
|
{description ? <CardDescription className="text-slate-400">{description}</CardDescription> : null}
|
|
</CardHeader>
|
|
<CardContent>{children}</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
export default function OfficeDetailPage() {
|
|
const { locale } = useAppI18n();
|
|
const params = useParams<{ taskId: string }>();
|
|
const taskId = decodeURIComponent(Array.isArray(params?.taskId) ? params.taskId[0] : params?.taskId ?? '');
|
|
|
|
const sessions = useChatStore((state) => state.sessions);
|
|
const processRuns = useChatStore((state) => state.processRuns);
|
|
const processEvents = useChatStore((state) => state.processEvents);
|
|
const processArtifacts = useChatStore((state) => state.processArtifacts);
|
|
|
|
const office = React.useMemo(
|
|
() => buildOfficeView(taskId, { sessions, processRuns, processEvents, processArtifacts }, locale),
|
|
[locale, processArtifacts, processEvents, processRuns, sessions, taskId]
|
|
);
|
|
const metadataLabels = React.useMemo(() => traceMetadataLabels(locale), [locale]);
|
|
|
|
const [selectedRunId, setSelectedRunId] = React.useState<string | null>(null);
|
|
const [detailOpen, setDetailOpen] = React.useState(false);
|
|
|
|
React.useEffect(() => {
|
|
setSelectedRunId(office?.rootRunId ?? null);
|
|
setDetailOpen(false);
|
|
}, [office?.rootRunId]);
|
|
|
|
const selectedTask = React.useMemo(
|
|
() => office?.tasks.find((task) => task.runId === selectedRunId) ?? office?.tasks[0] ?? null,
|
|
[office?.tasks, selectedRunId]
|
|
);
|
|
const selectedRun = React.useMemo(
|
|
() => processRuns.find((run) => run.run_id === selectedTask?.runId) ?? null,
|
|
[processRuns, selectedTask?.runId]
|
|
);
|
|
const selectedRunMetadata = React.useMemo(
|
|
() => traceMetadataEntries(selectedRun?.metadata, metadataLabels),
|
|
[metadataLabels, selectedRun?.metadata]
|
|
);
|
|
|
|
const selectedEvents = React.useMemo(
|
|
() => processEvents
|
|
.filter((event) => event.run_id === selectedTask?.runId)
|
|
.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())
|
|
.slice(0, 16),
|
|
[processEvents, selectedTask?.runId]
|
|
);
|
|
|
|
const selectedArtifacts = React.useMemo(
|
|
() => processArtifacts
|
|
.filter((artifact) => artifact.run_id === selectedTask?.runId)
|
|
.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()),
|
|
[processArtifacts, selectedTask?.runId]
|
|
);
|
|
|
|
const openRunDetail = React.useCallback((runId: string) => {
|
|
setSelectedRunId(runId);
|
|
setDetailOpen(true);
|
|
}, []);
|
|
|
|
if (!office) {
|
|
return (
|
|
<div className="mx-auto flex max-w-4xl flex-col gap-4 p-6">
|
|
<Button asChild variant="outline" className="w-fit">
|
|
<Link href="/office">
|
|
<ArrowLeft className="mr-2 h-4 w-4" />
|
|
{pickAppText(locale, '返回 Office 列表', 'Back to office list')}
|
|
</Link>
|
|
</Button>
|
|
<Card className="border-dashed">
|
|
<CardContent className="py-16 text-center">
|
|
<h1 className="text-2xl font-semibold">{pickAppText(locale, '任务不存在', 'Task not found')}</h1>
|
|
<p className="mt-2 text-sm text-muted-foreground">
|
|
{pickAppText(
|
|
locale,
|
|
'当前 store 中没有这个 task 的运行数据。先从对话页发起任务,或者回到 Office 列表查看当前可用任务。',
|
|
'The current store does not contain runtime data for this task yet. Start it from chat first, or return to the office list to inspect available tasks.'
|
|
)}
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const progressValue = progressPercent(office.progress.value, office.progress.max);
|
|
|
|
return (
|
|
<div className="mx-auto max-w-[1720px] space-y-6 p-6">
|
|
<TaskManagementTabs />
|
|
|
|
<div className="flex flex-wrap items-center gap-3">
|
|
<Button asChild variant="outline" size="sm">
|
|
<Link href="/office">
|
|
<ArrowLeft className="mr-2 h-4 w-4" />
|
|
{pickAppText(locale, '返回 Office', 'Back to office')}
|
|
</Link>
|
|
</Button>
|
|
<Button asChild variant="ghost" size="sm">
|
|
<Link href="/">
|
|
<MessageSquare className="mr-2 h-4 w-4" />
|
|
{pickAppText(locale, '回到对话', 'Back to chat')}
|
|
</Link>
|
|
</Button>
|
|
</div>
|
|
|
|
<div className="space-y-5">
|
|
<div className="mx-auto max-w-[1280px] rounded-none border-4 border-[#0e1119] bg-[#141522] p-4 shadow-[0_0_0_2px_#241d36_inset]">
|
|
<div className="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
|
|
<div className="min-w-0">
|
|
<div className="flex flex-wrap items-center gap-3">
|
|
<h1 className="truncate font-mono text-3xl font-bold uppercase tracking-[0.18em] text-[#fef3c7]">
|
|
{office.title}
|
|
</h1>
|
|
<OfficeStatusBadge status={office.status} className="bg-black/20" />
|
|
</div>
|
|
<div className="mt-3 flex flex-wrap items-center gap-x-4 gap-y-2 font-mono text-xs uppercase tracking-[0.14em] text-slate-400">
|
|
<span>{pickAppText(locale, '负责人', 'Lead')}: {office.rootActorName}</span>
|
|
<span>{pickAppText(locale, '会话', 'Session')}: {office.sourceSessionLabel}</span>
|
|
<span>{pickAppText(locale, '开始', 'Started')}: {formatOfficeTime(office.createdAt, locale)}</span>
|
|
<span>{pickAppText(locale, '耗时', 'Duration')}: {formatOfficeDuration(office.durationMs, locale)}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid min-w-[320px] gap-3 sm:grid-cols-2 lg:w-[430px]">
|
|
<MetricTile label={pickAppText(locale, '运行实例', 'Runs')} value={String(office.stats.totalRuns)} />
|
|
<MetricTile label={pickAppText(locale, '参与成员', 'Members')} value={String(office.stats.memberCount)} />
|
|
<MetricTile label={pickAppText(locale, '产物数量', 'Artifacts')} value={String(office.stats.artifactCount)} />
|
|
<MetricTile label={pickAppText(locale, '告警数量', 'Alerts')} value={String(office.alerts.length)} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mx-auto max-w-[1280px]">
|
|
<OfficePhaserCanvas
|
|
office={office}
|
|
selectedRunId={selectedTask?.runId ?? null}
|
|
onRunSelect={openRunDetail}
|
|
showMetaBar={false}
|
|
/>
|
|
</div>
|
|
|
|
<div className="mx-auto grid max-w-[1280px] gap-5 xl:grid-cols-[390px_minmax(0,1fr)_390px]">
|
|
<PixelPanel
|
|
title={pickAppText(locale, '昨日小记', 'Yesterday notes')}
|
|
subtitle={pickAppText(locale, '用任务摘要、告警和最近更新来替代原版 memo 区。', 'Use task summaries, alerts, and recent updates instead of the original memo area.')}
|
|
>
|
|
<div className="space-y-3 text-sm leading-6 text-slate-300">
|
|
<div className="rounded-none border-2 border-[#2d3348] bg-[#0f1420] px-3 py-3">
|
|
{selectedTask?.summary || pickAppText(locale, '当前选中任务没有摘要,先从右侧任务看板切一个具体 run 看现场。', 'The selected task has no summary yet. Pick a specific run from the board on the right to inspect the floor.')}
|
|
</div>
|
|
{office.alerts.slice(0, 2).map((alert) => (
|
|
<button
|
|
key={alert.id}
|
|
type="button"
|
|
disabled={!alert.runId}
|
|
onClick={() => alert.runId && openRunDetail(alert.runId)}
|
|
className="block w-full rounded-none border-2 border-[#40202a] bg-[#201118] px-3 py-3 text-left transition-colors enabled:hover:border-[#fb7185] disabled:cursor-default"
|
|
>
|
|
<div className="font-medium text-rose-200">{alert.title}</div>
|
|
{alert.description ? <div className="mt-1 text-xs text-slate-400">{alert.description}</div> : null}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</PixelPanel>
|
|
|
|
<PixelPanel
|
|
title={pickAppText(locale, '任务控制台', 'Task console')}
|
|
subtitle={pickAppText(locale, '保留原版中间控制栏的位置,但改成适配 task runtime 的真实数据。', 'Keep the original center console position, but back it with real task runtime data.')}
|
|
>
|
|
<div className="space-y-4">
|
|
<div className="grid gap-3 sm:grid-cols-2">
|
|
<MiniMetric label={pickAppText(locale, '当前阶段', 'Current stage')} value={office.progress.stageLabel ?? office.currentStageLabel ?? '-'} />
|
|
<MiniMetric label={pickAppText(locale, '活跃实例', 'Active runs')} value={String(office.stats.activeRuns)} />
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<div className="flex items-center justify-between gap-3 font-mono text-[11px] uppercase tracking-[0.14em] text-slate-400">
|
|
<span>{office.progress.label}</span>
|
|
<span>{progressValue}%</span>
|
|
</div>
|
|
<div className="h-4 rounded-none border-2 border-[#263144] bg-[#0f1420] p-[2px]">
|
|
<div
|
|
className="h-full bg-[linear-gradient(90deg,#22d3ee,#fde047,#fb7185)] transition-all"
|
|
style={{ width: `${progressValue}%` }}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{selectedTask ? (
|
|
<div className="rounded-none border-2 border-[#2d3348] bg-[#0f1420] px-3 py-3">
|
|
<div className="font-mono text-[11px] uppercase tracking-[0.14em] text-slate-400">{pickAppText(locale, '当前聚焦', 'Current focus')}</div>
|
|
<div className="mt-2 text-sm font-semibold text-slate-100">{selectedTask.title}</div>
|
|
<div className="mt-1 text-xs text-slate-400">
|
|
{selectedTask.actorName} · {selectedTask.stageLabel ?? pickAppText(locale, '无阶段标签', 'No stage label')}
|
|
</div>
|
|
</div>
|
|
) : null}
|
|
|
|
<div className="grid gap-2 sm:grid-cols-2">
|
|
<Button
|
|
onClick={() => setDetailOpen(true)}
|
|
className="w-full rounded-none border-2 border-[#2f3b16] bg-[#78a340] text-[#f3ffe6] hover:bg-[#8fbe4a]"
|
|
>
|
|
{pickAppText(locale, '打开详情', 'Open details')}
|
|
<PanelRightOpen className="ml-2 h-4 w-4" />
|
|
</Button>
|
|
<Button
|
|
asChild
|
|
variant="outline"
|
|
className="w-full rounded-none border-2 border-[#30364d] bg-[#171b29] text-slate-100 hover:bg-[#21283a]"
|
|
>
|
|
<Link href="/">
|
|
{pickAppText(locale, '回到对话', 'Back to chat')}
|
|
<ArrowRight className="ml-2 h-4 w-4" />
|
|
</Link>
|
|
</Button>
|
|
</div>
|
|
|
|
{isOfficeTaskTerminal(office.status) ? (
|
|
<div className="rounded-none border-2 border-[#365443] bg-[#12221d] px-3 py-3 text-sm text-emerald-200">
|
|
{pickAppText(locale, '任务已结束,办公室已解散,但现场记录仍可回看。', 'The task has ended and the office has dissolved, but the floor record is still available for review.')}
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
</PixelPanel>
|
|
|
|
<PixelPanel
|
|
title={pickAppText(locale, '办公人员名单', 'Roster')}
|
|
subtitle={pickAppText(locale, '原版 visitor 区的替代,这里展示当前参与 task 的 agent 成员。', 'Replacement for the original visitor area, showing the agents currently participating in this task.')}
|
|
>
|
|
<div className="space-y-2">
|
|
{office.members.map((member) => (
|
|
<button
|
|
key={member.memberId}
|
|
type="button"
|
|
onClick={() => openRunDetail(member.currentRunId)}
|
|
className="flex w-full items-center justify-between gap-3 rounded-none border-2 border-[#2d3348] bg-[#0f1420] px-3 py-3 text-left transition-colors hover:border-[#64748b]"
|
|
>
|
|
<div className="min-w-0">
|
|
<div className="truncate font-medium text-slate-100">{member.actorName}</div>
|
|
<div className="truncate text-xs text-slate-400">{member.currentTitle}</div>
|
|
</div>
|
|
<OfficeStatusBadge status={member.status} />
|
|
</button>
|
|
))}
|
|
</div>
|
|
</PixelPanel>
|
|
</div>
|
|
|
|
<div className="mx-auto grid max-w-[1280px] gap-5 xl:grid-cols-[1.08fr_0.92fr]">
|
|
<BoardPanel
|
|
icon={ListTree}
|
|
title={pickAppText(locale, '任务看板', 'Task board')}
|
|
description={pickAppText(locale, '当前 task 下所有 run 的结构化列表。', 'Structured list of all runs under this task.')}
|
|
>
|
|
<div className="space-y-3">
|
|
{office.tasks.map((task) => (
|
|
<button
|
|
key={task.runId}
|
|
type="button"
|
|
onClick={() => openRunDetail(task.runId)}
|
|
className={`w-full rounded-none border-2 px-4 py-3 text-left transition-colors ${
|
|
selectedTask?.runId === task.runId
|
|
? 'border-[#facc15] bg-[#201922]'
|
|
: 'border-[#2d3348] bg-[#0f1420] hover:border-[#64748b]'
|
|
}`}
|
|
>
|
|
<div className="flex items-start justify-between gap-3">
|
|
<div className="min-w-0">
|
|
<div className="flex items-center gap-2">
|
|
<span className="truncate font-medium text-slate-100">{task.title}</span>
|
|
{task.isRoot ? (
|
|
<span className="rounded-none border border-[#4a3c17] bg-[#3b2f12] px-2 py-0.5 font-mono text-[10px] text-[#fef3c7]">
|
|
ROOT
|
|
</span>
|
|
) : null}
|
|
</div>
|
|
<div className="mt-1 flex flex-wrap gap-x-3 gap-y-1 text-xs text-slate-400">
|
|
<span>{task.actorName}</span>
|
|
<span>{formatOfficeTime(task.updatedAt, locale)}</span>
|
|
<span>{pickAppText(locale, `${task.artifactCount} 个产物`, `${task.artifactCount} artifacts`)}</span>
|
|
</div>
|
|
</div>
|
|
<OfficeStatusBadge status={task.status} />
|
|
</div>
|
|
</button>
|
|
))}
|
|
</div>
|
|
</BoardPanel>
|
|
|
|
<div className="space-y-5">
|
|
<BoardPanel
|
|
icon={Boxes}
|
|
title={pickAppText(locale, '分工关系', 'Assignments')}
|
|
description={pickAppText(locale, '主 Agent 到子 Agent 的委派关系。', 'Delegation links from the lead agent to sub-agents.')}
|
|
>
|
|
<div className="space-y-2">
|
|
{office.assignments.length === 0 ? (
|
|
<div className="rounded-none border-2 border-dashed border-[#30364d] bg-[#0f1420] px-3 py-4 text-sm text-slate-400">
|
|
{pickAppText(locale, '当前没有可见的子任务分工。', 'No visible subtask assignments yet.')}
|
|
</div>
|
|
) : (
|
|
office.assignments.map((assignment) => (
|
|
<button
|
|
key={assignment.ownerRunId}
|
|
type="button"
|
|
onClick={() => openRunDetail(assignment.ownerRunId)}
|
|
className="w-full rounded-none border-2 border-[#2d3348] bg-[#0f1420] px-3 py-3 text-left text-sm transition-colors hover:border-[#64748b]"
|
|
>
|
|
<div className="font-medium text-slate-100">{assignment.label}</div>
|
|
<div className="mt-1 text-slate-400">{assignment.assigneeActorNames.join(' / ')}</div>
|
|
</button>
|
|
))
|
|
)}
|
|
</div>
|
|
</BoardPanel>
|
|
|
|
<BoardPanel
|
|
icon={Siren}
|
|
title={pickAppText(locale, '现场告警', 'Live alerts')}
|
|
description={pickAppText(locale, '优先展示失败、阻塞和较高风险的任务信号。', 'Prioritize failed, blocked, and higher-risk task signals.')}
|
|
>
|
|
<div className="space-y-2">
|
|
{office.alerts.length === 0 ? (
|
|
<div className="rounded-none border-2 border-dashed border-[#30364d] bg-[#0f1420] px-3 py-4 text-sm text-slate-400">
|
|
{pickAppText(locale, '当前没有高优先级告警。', 'There are no high-priority alerts right now.')}
|
|
</div>
|
|
) : (
|
|
office.alerts.map((alert) => (
|
|
<button
|
|
key={alert.id}
|
|
type="button"
|
|
disabled={!alert.runId}
|
|
onClick={() => alert.runId && openRunDetail(alert.runId)}
|
|
className="w-full rounded-none border-2 border-[#40202a] bg-[#201118] px-3 py-3 text-left transition-colors enabled:hover:border-[#fb7185] disabled:cursor-default"
|
|
>
|
|
<div className="font-medium text-rose-200">{alert.title}</div>
|
|
{alert.description ? <div className="mt-1 text-sm text-slate-400">{alert.description}</div> : null}
|
|
</button>
|
|
))
|
|
)}
|
|
</div>
|
|
</BoardPanel>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<Sheet open={detailOpen} onOpenChange={setDetailOpen}>
|
|
<SheetContent side="right" className="w-full border-l border-border sm:max-w-3xl">
|
|
<SheetHeader className="pr-8">
|
|
<SheetTitle>{selectedTask?.title ?? pickAppText(locale, '任务详情', 'Task details')}</SheetTitle>
|
|
<SheetDescription>
|
|
{selectedTask
|
|
? `${selectedTask.actorName} · ${selectedTask.stageLabel ?? pickAppText(locale, '无阶段标签', 'No stage label')}`
|
|
: pickAppText(locale, '当前没有选中的任务实例。', 'No task run is currently selected.')}
|
|
</SheetDescription>
|
|
</SheetHeader>
|
|
|
|
{!selectedTask ? (
|
|
<div className="mt-6 rounded-xl border border-dashed border-border/60 px-4 py-6 text-sm text-muted-foreground">
|
|
{pickAppText(locale, '当前没有可展示的任务详情。', 'There are no task details to display right now.')}
|
|
</div>
|
|
) : (
|
|
<ScrollArea className="mt-6 h-[calc(100vh-8.5rem)] pr-3">
|
|
<div className="space-y-4 pb-6">
|
|
<div className="rounded-xl border border-border/60 px-4 py-4">
|
|
<div className="flex items-center justify-between gap-3">
|
|
<div className="min-w-0">
|
|
<div className="truncate font-medium">{selectedTask.title}</div>
|
|
<div className="mt-1 text-xs text-muted-foreground">{selectedTask.actorName}</div>
|
|
</div>
|
|
<OfficeStatusBadge status={selectedTask.status} />
|
|
</div>
|
|
<div className="mt-3 grid gap-2 text-sm">
|
|
<div className="flex items-center justify-between gap-3">
|
|
<span className="text-muted-foreground">{pickAppText(locale, '开始时间', 'Started')}</span>
|
|
<span>{formatOfficeTime(selectedTask.startedAt, locale)}</span>
|
|
</div>
|
|
<div className="flex items-center justify-between gap-3">
|
|
<span className="text-muted-foreground">{pickAppText(locale, '最近更新', 'Last update')}</span>
|
|
<span>{formatOfficeTime(selectedTask.updatedAt, locale)}</span>
|
|
</div>
|
|
<div className="flex items-center justify-between gap-3">
|
|
<span className="text-muted-foreground">{pickAppText(locale, '阶段', 'Stage')}</span>
|
|
<span>{selectedTask.stageLabel ?? '-'}</span>
|
|
</div>
|
|
</div>
|
|
{selectedRunMetadata.length > 0 ? (
|
|
<div className="mt-3 rounded-lg border border-border/60 bg-muted/20 px-3 py-3">
|
|
<div className="text-xs font-medium uppercase tracking-wide text-muted-foreground">{pickAppText(locale, '链路上下文', 'Trace context')}</div>
|
|
<div className="mt-2 space-y-1.5">
|
|
{selectedRunMetadata.map((item) => (
|
|
<div key={item.key} className="grid gap-1 text-xs sm:grid-cols-[110px_minmax(0,1fr)]">
|
|
<span className="text-muted-foreground">{item.label}</span>
|
|
<span className="break-words font-mono text-foreground/90">{item.value}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
) : null}
|
|
{selectedTask.summary ? (
|
|
<div className="mt-3 rounded-lg bg-muted/40 px-3 py-3 text-sm text-muted-foreground">
|
|
{selectedTask.summary}
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
|
|
<div className="grid gap-4 lg:grid-cols-[0.95fr_1.05fr]">
|
|
<div className="rounded-xl border border-border/60">
|
|
<div className="border-b border-border/60 px-4 py-3 text-sm font-medium">{pickAppText(locale, '产物', 'Artifacts')}</div>
|
|
<div className="space-y-2 p-4">
|
|
{selectedArtifacts.length === 0 ? (
|
|
<div className="text-sm text-muted-foreground">{pickAppText(locale, '当前没有产物。', 'There are no artifacts for this task.')}</div>
|
|
) : (
|
|
selectedArtifacts.map((artifact) => (
|
|
<div key={artifact.artifact_id} className="rounded-lg border border-border/60 px-3 py-3">
|
|
<div className="font-medium">{artifact.title}</div>
|
|
<div className="mt-1 text-xs text-muted-foreground">
|
|
{artifact.artifact_type} · {formatOfficeTime(artifact.created_at, locale)}
|
|
</div>
|
|
</div>
|
|
))
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="rounded-xl border border-border/60">
|
|
<div className="border-b border-border/60 px-4 py-3 text-sm font-medium">{pickAppText(locale, '最近事件', 'Recent events')}</div>
|
|
<div className="space-y-2 p-4">
|
|
{selectedEvents.length === 0 ? (
|
|
<div className="text-sm text-muted-foreground">{pickAppText(locale, '当前没有事件。', 'There are no events for this task.')}</div>
|
|
) : (
|
|
selectedEvents.map((event) => {
|
|
const metadataEntries = traceMetadataEntries(event.metadata, metadataLabels);
|
|
return (
|
|
<div key={event.event_id} className="rounded-lg border border-border/60 px-3 py-3">
|
|
<div className="flex items-center justify-between gap-3">
|
|
<div className="text-xs uppercase tracking-wide text-muted-foreground">{appEventKindLabel(event.kind, locale)}</div>
|
|
<div className="text-xs text-muted-foreground">{formatOfficeTime(event.created_at, locale)}</div>
|
|
</div>
|
|
{event.status ? (
|
|
<div className="mt-2 text-xs text-muted-foreground">{pickAppText(locale, '状态', 'Status')}: {event.status}</div>
|
|
) : null}
|
|
<div className="mt-2 text-sm text-foreground/90">
|
|
{event.text || pickAppText(locale, '结构化更新', 'Structured update')}
|
|
</div>
|
|
{metadataEntries.length > 0 ? (
|
|
<div className="mt-3 rounded-md bg-muted/20 px-3 py-2">
|
|
<div className="mb-2 text-[11px] uppercase tracking-wide text-muted-foreground">{pickAppText(locale, '事件上下文', 'Event context')}</div>
|
|
<div className="space-y-1.5">
|
|
{metadataEntries.map((item) => (
|
|
<div key={`${event.event_id}:${item.key}`} className="grid gap-1 text-xs sm:grid-cols-[110px_minmax(0,1fr)]">
|
|
<span className="text-muted-foreground">{item.label}</span>
|
|
<span className="break-words font-mono text-foreground/90">{item.value}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
)})
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</ScrollArea>
|
|
)}
|
|
</SheetContent>
|
|
</Sheet>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function MetricTile({ label, value }: { label: string; value: string }) {
|
|
return (
|
|
<div className="rounded-none border-2 border-[#2d3348] bg-[#0f1420] px-4 py-4 text-slate-100">
|
|
<div className="font-mono text-[11px] uppercase tracking-[0.14em] text-slate-400">{label}</div>
|
|
<div className="mt-2 text-xl font-semibold">{value}</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function MiniMetric({ label, value }: { label: string; value: string }) {
|
|
return (
|
|
<div className="rounded-none border-2 border-[#2d3348] bg-[#0f1420] px-3 py-3 text-slate-100">
|
|
<div className="font-mono text-[11px] uppercase tracking-[0.14em] text-slate-400">{label}</div>
|
|
<div className="mt-2 text-sm font-semibold">{value}</div>
|
|
</div>
|
|
);
|
|
}
|