mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-15 04:18:44 +00:00
feat(web): show richer context in traces list (agent, user, source, input preview)
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.
This commit is contained in:
1 parent
040ac31156
commit
62724780b8
4 files changed
+134
-53
No files matched your search
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -70,6 +70,14 @@
|
||||
"abortFailed": "停止运行失败",
|
||||
"abortNotFound": "运行已完成或未找到"
|
||||
},
|
||||
"source": {
|
||||
"direct": "私聊",
|
||||
"group": "群组",
|
||||
"cron": "定时",
|
||||
"team": "团队",
|
||||
"ws": "网页",
|
||||
"unknown": "其他"
|
||||
},
|
||||
"unnamed": "未命名",
|
||||
"stopRun": "停止运行"
|
||||
}
|
||||
@@ -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<string, typeof Bot> = {
|
||||
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<string, string>();
|
||||
for (const a of agents) map.set(a.id, a.display_name || a.agent_key || a.id);
|
||||
return map;
|
||||
}, [agents]);
|
||||
|
||||
const [abortingRunId, setAbortingRunId] = useState<string | null>(null);
|
||||
|
||||
const { traces, total, loading, fetching, refresh, getTrace } = useTraces({
|
||||
@@ -148,58 +184,79 @@ export function TracesPage() {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{traces.map((trace: TraceData) => (
|
||||
<tr
|
||||
key={trace.id}
|
||||
className="cursor-pointer border-b last:border-0 hover:bg-muted/30"
|
||||
onClick={() => setSelectedTraceId(trace.id)}
|
||||
>
|
||||
<td className="max-w-[200px] truncate px-4 py-3 font-medium">
|
||||
{trace.parent_trace_id && (
|
||||
<GitFork className="mr-1.5 inline-block h-3.5 w-3.5 text-muted-foreground" />
|
||||
)}
|
||||
{trace.name || t("unnamed")}
|
||||
{trace.channel && (
|
||||
<Badge variant="outline" className="ml-2 text-xs">
|
||||
{trace.channel}
|
||||
</Badge>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<StatusBadge status={trace.status} />
|
||||
{(trace.status === "running") && (
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="icon-xs"
|
||||
onClick={(e) => handleAbortRun(trace, e)}
|
||||
disabled={abortingRunId === trace.run_id}
|
||||
title={t("stopRun")}
|
||||
>
|
||||
<Square className="h-3 w-3" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-muted-foreground">
|
||||
{formatDuration(trace.duration_ms || computeDurationMs(trace.start_time, trace.end_time))}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-muted-foreground">
|
||||
<div>{formatTokens(trace.total_input_tokens)} / {formatTokens(trace.total_output_tokens)}</div>
|
||||
{(trace.metadata?.total_cache_read_tokens ?? 0) > 0 && (
|
||||
<div className="text-xs text-green-400">
|
||||
{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 (
|
||||
<tr
|
||||
key={trace.id}
|
||||
className="cursor-pointer border-b last:border-0 hover:bg-muted/30"
|
||||
onClick={() => setSelectedTraceId(trace.id)}
|
||||
>
|
||||
<td className="max-w-[360px] px-4 py-2.5">
|
||||
<div className="flex items-center gap-1.5 text-sm font-medium">
|
||||
{trace.parent_trace_id && (
|
||||
<GitFork className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
||||
)}
|
||||
<span className="truncate">{agentName || trace.name || t("unnamed")}</span>
|
||||
<span className="shrink-0 text-muted-foreground">·</span>
|
||||
<span className="shrink-0 text-xs text-muted-foreground">{userLabel}</span>
|
||||
<Badge variant="outline" className="shrink-0 gap-0.5 text-[10px] px-1.5 py-0">
|
||||
<SourceIcon className="h-2.5 w-2.5" />
|
||||
{t(`source.${source.type}`)}
|
||||
{source.topic && ` #${source.topic}`}
|
||||
</Badge>
|
||||
{trace.channel && (
|
||||
<Badge variant="secondary" className="shrink-0 text-[10px] px-1.5 py-0">
|
||||
{trace.channel}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-muted-foreground">
|
||||
{trace.span_count}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-muted-foreground">
|
||||
{formatDate(trace.start_time, tz)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{trace.input_preview && (
|
||||
<p className="mt-0.5 truncate text-xs text-muted-foreground">
|
||||
{trace.input_preview}
|
||||
</p>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-2.5">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<StatusBadge status={trace.status} />
|
||||
{(trace.status === "running") && (
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="icon-xs"
|
||||
onClick={(e) => handleAbortRun(trace, e)}
|
||||
disabled={abortingRunId === trace.run_id}
|
||||
title={t("stopRun")}
|
||||
>
|
||||
<Square className="h-3 w-3" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-2.5 text-muted-foreground">
|
||||
{formatDuration(trace.duration_ms || computeDurationMs(trace.start_time, trace.end_time))}
|
||||
</td>
|
||||
<td className="px-4 py-2.5 text-muted-foreground">
|
||||
<div>{formatTokens(trace.total_input_tokens)} / {formatTokens(trace.total_output_tokens)}</div>
|
||||
{(trace.metadata?.total_cache_read_tokens ?? 0) > 0 && (
|
||||
<div className="text-xs text-green-400">
|
||||
{formatTokens(trace.metadata!.total_cache_read_tokens!)} {t("cached")}
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-2.5 text-muted-foreground">
|
||||
{trace.span_count}
|
||||
</td>
|
||||
<td className="px-4 py-2.5 text-muted-foreground">
|
||||
{formatDate(trace.start_time, tz)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
<Pagination
|
||||
|
||||
Reference in new issue
Block a user