From 62724780b8ae502ea45bcd8588ba85682135a225 Mon Sep 17 00:00:00 2001 From: viettranx Date: Thu, 2 Apr 2026 12:15:09 +0700 Subject: [PATCH] feat(web): show richer context in traces list (agent, user, source, input preview) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Traces list Name column now displays agent display name, user label, source type badge (Direct/Group/Cron/Team/Web), channel badge, and truncated input preview — making it easy to identify which chat each trace belongs to. --- ui/web/src/i18n/locales/en/traces.json | 8 ++ ui/web/src/i18n/locales/vi/traces.json | 8 ++ ui/web/src/i18n/locales/zh/traces.json | 8 ++ ui/web/src/pages/traces/traces-page.tsx | 163 ++++++++++++++++-------- 4 files changed, 134 insertions(+), 53 deletions(-) diff --git a/ui/web/src/i18n/locales/en/traces.json b/ui/web/src/i18n/locales/en/traces.json index 876188b2..40947c80 100644 --- a/ui/web/src/i18n/locales/en/traces.json +++ b/ui/web/src/i18n/locales/en/traces.json @@ -15,6 +15,14 @@ "spans": "Spans", "time": "Time" }, + "source": { + "direct": "Direct", + "group": "Group", + "cron": "Cron", + "team": "Team", + "ws": "Web", + "unknown": "Other" + }, "unnamed": "Unnamed", "stopRun": "Stop run", "cached": "cached", diff --git a/ui/web/src/i18n/locales/vi/traces.json b/ui/web/src/i18n/locales/vi/traces.json index f066bea5..03b38e54 100644 --- a/ui/web/src/i18n/locales/vi/traces.json +++ b/ui/web/src/i18n/locales/vi/traces.json @@ -15,6 +15,14 @@ "spans": "Khoảng", "time": "Thời gian" }, + "source": { + "direct": "Trực tiếp", + "group": "Nhóm", + "cron": "Cron", + "team": "Team", + "ws": "Web", + "unknown": "Khác" + }, "unnamed": "Không tên", "stopRun": "Dừng chạy", "cached": "đã cache", diff --git a/ui/web/src/i18n/locales/zh/traces.json b/ui/web/src/i18n/locales/zh/traces.json index b72a5204..6fd4b6af 100644 --- a/ui/web/src/i18n/locales/zh/traces.json +++ b/ui/web/src/i18n/locales/zh/traces.json @@ -70,6 +70,14 @@ "abortFailed": "停止运行失败", "abortNotFound": "运行已完成或未找到" }, + "source": { + "direct": "私聊", + "group": "群组", + "cron": "定时", + "team": "团队", + "ws": "网页", + "unknown": "其他" + }, "unnamed": "未命名", "stopRun": "停止运行" } diff --git a/ui/web/src/pages/traces/traces-page.tsx b/ui/web/src/pages/traces/traces-page.tsx index 5e9ed240..e4d43efb 100644 --- a/ui/web/src/pages/traces/traces-page.tsx +++ b/ui/web/src/pages/traces/traces-page.tsx @@ -1,6 +1,6 @@ -import { useState, useCallback } from "react"; +import { useState, useCallback, useMemo } from "react"; import { useTranslation } from "react-i18next"; -import { Activity, GitFork, RefreshCw, Square } from "lucide-react"; +import { Activity, GitFork, RefreshCw, Square, Bot, User, Users, Clock, Network, Globe } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { @@ -26,6 +26,36 @@ import { useWs } from "@/hooks/use-ws"; import { Methods } from "@/api/protocol"; import { toast } from "@/stores/use-toast-store"; +/** Parse session_key to extract source type: Direct, Group, Cron, Team, WS */ +function parseSourceType(sessionKey: string): { type: string; topic?: string } { + if (!sessionKey) return { type: "unknown" }; + if (sessionKey.includes(":cron:")) return { type: "cron" }; + if (sessionKey.includes(":team:")) return { type: "team" }; + const topicMatch = sessionKey.match(/:topic:(\d+)/); + if (topicMatch) return { type: "group", topic: topicMatch[1] }; + if (sessionKey.includes(":group:")) return { type: "group" }; + if (sessionKey.includes(":ws:")) return { type: "ws" }; + if (sessionKey.includes(":direct:")) return { type: "direct" }; + return { type: "unknown" }; +} + +/** Parse user_id into a human-readable display label */ +function parseUserDisplay(userId: string): string { + if (!userId) return ""; + if (userId === "system") return "System"; + if (userId.startsWith("group:")) return "Group"; + if (/^\d+$/.test(userId)) return `#${userId}`; + return `@${userId}`; +} + +const SOURCE_ICONS: Record = { + cron: Clock, + team: Network, + group: Users, + direct: User, + ws: Globe, +}; + export function TracesPage() { const { t } = useTranslation("traces"); const { t: tc } = useTranslation("common"); @@ -40,6 +70,12 @@ export function TracesPage() { const { agents } = useAgents(); const { instances: channels } = useChannelInstances(); + const agentMap = useMemo(() => { + const map = new Map(); + for (const a of agents) map.set(a.id, a.display_name || a.agent_key || a.id); + return map; + }, [agents]); + const [abortingRunId, setAbortingRunId] = useState(null); const { traces, total, loading, fetching, refresh, getTrace } = useTraces({ @@ -148,58 +184,79 @@ export function TracesPage() { - {traces.map((trace: TraceData) => ( - setSelectedTraceId(trace.id)} - > - - {trace.parent_trace_id && ( - - )} - {trace.name || t("unnamed")} - {trace.channel && ( - - {trace.channel} - - )} - - -
- - {(trace.status === "running") && ( - - )} -
- - - {formatDuration(trace.duration_ms || computeDurationMs(trace.start_time, trace.end_time))} - - -
{formatTokens(trace.total_input_tokens)} / {formatTokens(trace.total_output_tokens)}
- {(trace.metadata?.total_cache_read_tokens ?? 0) > 0 && ( -
- {formatTokens(trace.metadata!.total_cache_read_tokens!)} {t("cached")} + {traces.map((trace: TraceData) => { + const source = parseSourceType(trace.session_key); + const userLabel = parseUserDisplay(trace.user_id); + const agentName = trace.agent_id ? agentMap.get(trace.agent_id) : undefined; + const SourceIcon = SOURCE_ICONS[source.type] || Bot; + + return ( + setSelectedTraceId(trace.id)} + > + +
+ {trace.parent_trace_id && ( + + )} + {agentName || trace.name || t("unnamed")} + · + {userLabel} + + + {t(`source.${source.type}`)} + {source.topic && ` #${source.topic}`} + + {trace.channel && ( + + {trace.channel} + + )}
- )} - - - {trace.span_count} - - - {formatDate(trace.start_time, tz)} - - - ))} + {trace.input_preview && ( +

+ {trace.input_preview} +

+ )} + + +
+ + {(trace.status === "running") && ( + + )} +
+ + + {formatDuration(trace.duration_ms || computeDurationMs(trace.start_time, trace.end_time))} + + +
{formatTokens(trace.total_input_tokens)} / {formatTokens(trace.total_output_tokens)}
+ {(trace.metadata?.total_cache_read_tokens ?? 0) > 0 && ( +
+ {formatTokens(trace.metadata!.total_cache_read_tokens!)} {t("cached")} +
+ )} + + + {trace.span_count} + + + {formatDate(trace.start_time, tz)} + + + ); + })}