Files
beaver_project/app-instance/frontend/app/(app)/office/[taskId]/page.tsx
steven_li cdfc222c9f feat: 添加swarms团队编排功能并优化agent委派系统
- 引入AgentTeamOrchestrator支持多agent协同任务执行
- 增加第三方swarms库依赖并配置git协议替换以改善包管理
- 扩展DelegationManager支持团队任务调度和进度跟踪
- 实现中文bigram分词算法提升中文任务检索准确性
- 调整A2AClient和DelegationManager超时时间从30秒增至600秒
- 优化AgentRunResult状态判断逻辑增加有意义摘要检测
- 修改Dockerfile配置npm仓库镜像地址和git协议映射
- 更新CLI命令行接口支持网关端口配置传递
- 调整提供者超时配置机制增强请求稳定性
- 移除过时的support_group字段简化agent描述符结构
- 增强错误处理和进度事件报告机制改进用户体验
2026-04-14 14:34:23 +08:00

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