feat(engine): 添加MCP连接管理和工具集成功能
- 集成MCP连接管理器,支持MCP服务器连接 - 添加多种内置工具:ClarifyTool、CronTool、DelegateTool、ExecuteCodeTool、 PatchFileTool、ProcessTool、SendMessageTool、SpawnTool、TerminalTool、 TodoTool、WebFetchTool、WebSearchTool、WriteFileTool等 - 实现工具注册和装配功能 - 添加技能选择上下文参数 - 支持思考模式控制参数thinking_enabled feat(coordinator): 重构任务执行计划器参数命名 - 将learning_candidate_enabled重命名为allow_candidate_generation - 更新TeamGraphScheduler中的参数传递 - 修改LocalAgentRunner中的相关参数处理 - 更新README文档中的相应描述 refactor(context): 标准化工具调用参数格式 - 添加_json导入用于参数序列化 - 实现_provider_tool_calls方法标准化OpenAI兼容的工具调用载荷 - 修复工具调用中参数非字符串类型的序列化问题 refactor(session): 优化消息历史记录过滤逻辑 - 修改get_messages_as_conversation为基于运行状态过滤消息 - 排除未完成、失败或错误结束的运行记录 - 改进对话历史的可见性控制机制 fix(store): 修复FTS索引重建逻辑 - 添加异常处理防止FTS索引创建失败 - 实现_rebuild_fts_index方法重新构建全文搜索索引 - 优化索引触发器和表的维护流程
This commit is contained in:
@ -1,653 +1,5 @@
|
||||
'use client';
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
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>
|
||||
);
|
||||
export default function OfficeTaskRedirectPage({ params }: { params: { taskId: string } }) {
|
||||
redirect(`/tasks/${params.taskId}`);
|
||||
}
|
||||
|
||||
@ -1,284 +1,5 @@
|
||||
'use client';
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
import Link from 'next/link';
|
||||
import React from 'react';
|
||||
import {
|
||||
Activity,
|
||||
ArrowRight,
|
||||
Clock3,
|
||||
FolderKanban,
|
||||
Loader2,
|
||||
Sparkles,
|
||||
Users,
|
||||
} from 'lucide-react';
|
||||
|
||||
import { OfficeStatusBadge, formatOfficeTime, progressPercent } from '@/components/office/OfficeShared';
|
||||
import { TaskManagementTabs } from '@/components/task-management/TaskManagementTabs';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { buildOfficeTaskList, isOfficeTaskTerminal } from '@/lib/office';
|
||||
import { appConnectionStatusLabel } from '@/lib/i18n/common';
|
||||
import { pickAppText } from '@/lib/i18n/core';
|
||||
import { useAppI18n } from '@/lib/i18n/provider';
|
||||
import { useChatStore } from '@/lib/store';
|
||||
|
||||
function TaskCard({
|
||||
taskId,
|
||||
title,
|
||||
sessionLabel,
|
||||
rootActorName,
|
||||
status,
|
||||
updatedAt,
|
||||
memberCount,
|
||||
activeRuns,
|
||||
artifactCount,
|
||||
errorCount,
|
||||
currentStageLabel,
|
||||
progressLabel,
|
||||
progressValue,
|
||||
locale,
|
||||
}: {
|
||||
taskId: string;
|
||||
title: string;
|
||||
sessionLabel: string;
|
||||
rootActorName: string;
|
||||
status: Parameters<typeof OfficeStatusBadge>[0]['status'];
|
||||
updatedAt: string;
|
||||
memberCount: number;
|
||||
activeRuns: number;
|
||||
artifactCount: number;
|
||||
errorCount: number;
|
||||
currentStageLabel: string | null;
|
||||
progressLabel: string;
|
||||
progressValue: number;
|
||||
locale: 'zh-CN' | 'en-US';
|
||||
}) {
|
||||
return (
|
||||
<Card className="border-border/80 transition-colors hover:border-primary/30">
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="min-w-0 flex-1">
|
||||
<CardTitle className="truncate text-lg">{title}</CardTitle>
|
||||
<CardDescription className="mt-2 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs">
|
||||
<span>{pickAppText(locale, '会话', 'Session')}: {sessionLabel}</span>
|
||||
<span>{pickAppText(locale, '主 Agent', 'Lead agent')}: {rootActorName}</span>
|
||||
<span>{pickAppText(locale, '更新于', 'Updated')} {formatOfficeTime(updatedAt, locale)}</span>
|
||||
</CardDescription>
|
||||
</div>
|
||||
<OfficeStatusBadge status={status} />
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid gap-3 sm:grid-cols-4">
|
||||
<Metric icon={Users} label={pickAppText(locale, '成员', 'Members')} value={String(memberCount)} />
|
||||
<Metric icon={Activity} label={pickAppText(locale, '活跃', 'Active')} value={String(activeRuns)} />
|
||||
<Metric icon={FolderKanban} label={pickAppText(locale, '产物', 'Artifacts')} value={String(artifactCount)} />
|
||||
<Metric icon={Sparkles} label={pickAppText(locale, '异常', 'Alerts')} value={String(errorCount)} />
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between gap-3 text-sm">
|
||||
<span className="truncate text-muted-foreground">{progressLabel}</span>
|
||||
{currentStageLabel ? <span className="truncate font-medium">{currentStageLabel}</span> : null}
|
||||
</div>
|
||||
<div className="h-2.5 overflow-hidden rounded-full bg-secondary">
|
||||
<div
|
||||
className="h-full rounded-full bg-primary transition-all"
|
||||
style={{ width: `${progressValue}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button asChild size="sm">
|
||||
<Link href={`/office/${encodeURIComponent(taskId)}`}>
|
||||
{pickAppText(locale, '进入办公室', 'Open office')}
|
||||
<ArrowRight className="ml-2 h-4 w-4" />
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function Metric({
|
||||
icon: Icon,
|
||||
label,
|
||||
value,
|
||||
}: {
|
||||
icon: React.ComponentType<{ className?: string }>;
|
||||
label: string;
|
||||
value: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="rounded-xl border border-border/60 bg-muted/30 px-3 py-3">
|
||||
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||
<Icon className="h-3.5 w-3.5" />
|
||||
<span>{label}</span>
|
||||
</div>
|
||||
<div className="mt-2 text-lg font-semibold">{value}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function OfficeListPage() {
|
||||
const { locale } = useAppI18n();
|
||||
const sessionId = useChatStore((state) => state.sessionId);
|
||||
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 wsStatus = useChatStore((state) => state.wsStatus);
|
||||
|
||||
const tasks = React.useMemo(
|
||||
() => buildOfficeTaskList({
|
||||
sessionId,
|
||||
sessions,
|
||||
processRuns,
|
||||
processEvents,
|
||||
processArtifacts,
|
||||
}, locale),
|
||||
[locale, processArtifacts, processEvents, processRuns, sessionId, sessions]
|
||||
);
|
||||
|
||||
const activeTasks = tasks.filter((task) => !isOfficeTaskTerminal(task.status));
|
||||
const recentTasks = tasks.filter((task) => isOfficeTaskTerminal(task.status));
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-7xl space-y-6 p-6">
|
||||
<TaskManagementTabs />
|
||||
|
||||
<div className="flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-semibold tracking-tight">Office</h1>
|
||||
<p className="mt-2 max-w-3xl text-sm text-muted-foreground">
|
||||
{pickAppText(
|
||||
locale,
|
||||
'基于当前会话的真实运行数据,展示主 Agent 与子 Agent 的任务现场。任务结束后会从活跃现场移除,但保留回看入口。',
|
||||
'Show the live task floor for the lead agent and its sub-agents using real runtime data from the current session. Finished tasks leave the active floor but remain available for review.'
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<Card className="min-w-[280px] border-border/70">
|
||||
<CardContent className="flex items-center justify-between gap-4 p-4">
|
||||
<div>
|
||||
<div className="text-xs text-muted-foreground">{pickAppText(locale, '当前会话', 'Current session')}</div>
|
||||
<div className="mt-1 font-medium">{sessionId}</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-xs text-muted-foreground">{pickAppText(locale, '连接状态', 'Connection')}</div>
|
||||
<div className="mt-1 font-medium">{appConnectionStatusLabel(wsStatus, wsStatus === 'connected' ? true : null, locale)}</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{wsStatus === 'connecting' && tasks.length === 0 ? (
|
||||
<div className="flex items-center gap-3 rounded-xl border border-dashed border-border px-4 py-6 text-sm text-muted-foreground">
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
{pickAppText(locale, '正在等待运行时数据...', 'Waiting for runtime data...')}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{tasks.length === 0 ? (
|
||||
<Card className="border-dashed">
|
||||
<CardContent className="flex flex-col items-center justify-center py-16 text-center">
|
||||
<Clock3 className="h-10 w-10 text-muted-foreground/50" />
|
||||
<h2 className="mt-4 text-xl font-semibold">{pickAppText(locale, '当前没有可展示的任务现场', 'No task floor is available yet')}</h2>
|
||||
<p className="mt-2 max-w-xl text-sm text-muted-foreground">
|
||||
{pickAppText(
|
||||
locale,
|
||||
'先回到对话页发起一次主 Agent 任务。开始执行后,这里会出现活跃的 office 卡片。',
|
||||
'Start a lead-agent task from the chat page first. Once it begins running, active office cards will appear here.'
|
||||
)}
|
||||
</p>
|
||||
<Button asChild className="mt-6">
|
||||
<Link href="/">{pickAppText(locale, '回到对话', 'Back to chat')}</Link>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<>
|
||||
<section className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold">{pickAppText(locale, '活跃 Office', 'Active office')}</h2>
|
||||
<p className="text-sm text-muted-foreground">{pickAppText(locale, '正在运行中的任务现场会优先显示。', 'Running task floors are shown first.')}</p>
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">{pickAppText(locale, `${activeTasks.length} 个任务`, `${activeTasks.length} tasks`)}</div>
|
||||
</div>
|
||||
{activeTasks.length === 0 ? (
|
||||
<Card className="border-dashed">
|
||||
<CardContent className="py-10 text-center text-sm text-muted-foreground">
|
||||
{pickAppText(locale, '当前没有活跃任务,下面可以查看最近结束的任务。', 'There are no active tasks right now. Recent finished tasks are listed below.')}
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<div className="grid gap-4 lg:grid-cols-2">
|
||||
{activeTasks.map((task) => (
|
||||
<TaskCard
|
||||
key={task.taskId}
|
||||
taskId={task.taskId}
|
||||
title={task.title}
|
||||
sessionLabel={task.sessionLabel}
|
||||
rootActorName={task.rootActorName}
|
||||
status={task.status}
|
||||
updatedAt={task.updatedAt}
|
||||
memberCount={task.memberCount}
|
||||
activeRuns={task.activeRuns}
|
||||
artifactCount={task.artifactCount}
|
||||
errorCount={task.errorCount}
|
||||
currentStageLabel={task.currentStageLabel}
|
||||
progressLabel={task.progress.label}
|
||||
progressValue={progressPercent(task.progress.value, task.progress.max)}
|
||||
locale={locale}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold">{pickAppText(locale, '最近结束', 'Recently finished')}</h2>
|
||||
<p className="text-sm text-muted-foreground">{pickAppText(locale, '已完成、失败或取消的任务仍保留回看入口。', 'Completed, failed, or cancelled tasks remain available for review.')}</p>
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">{pickAppText(locale, `${recentTasks.length} 个任务`, `${recentTasks.length} tasks`)}</div>
|
||||
</div>
|
||||
{recentTasks.length === 0 ? (
|
||||
<Card className="border-dashed">
|
||||
<CardContent className="py-10 text-center text-sm text-muted-foreground">
|
||||
{pickAppText(locale, '还没有历史任务。', 'There is no task history yet.')}
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<div className="grid gap-4 lg:grid-cols-2">
|
||||
{recentTasks.map((task) => (
|
||||
<TaskCard
|
||||
key={task.taskId}
|
||||
taskId={task.taskId}
|
||||
title={task.title}
|
||||
sessionLabel={task.sessionLabel}
|
||||
rootActorName={task.rootActorName}
|
||||
status={task.status}
|
||||
updatedAt={task.updatedAt}
|
||||
memberCount={task.memberCount}
|
||||
activeRuns={task.activeRuns}
|
||||
artifactCount={task.artifactCount}
|
||||
errorCount={task.errorCount}
|
||||
currentStageLabel={task.currentStageLabel}
|
||||
progressLabel={task.progress.label}
|
||||
progressValue={progressPercent(task.progress.value, task.progress.max)}
|
||||
locale={locale}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
export default function OfficeRedirectPage() {
|
||||
redirect('/tasks');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user