mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-15 08:17:09 +00:00
fix(ui): improve traces table layout and readability
Compact columns: status as icon-only, merge time+duration into one column. Truncate long user IDs, clean <media:*> tags from preview, move badges to second line.
This commit is contained in:
1 parent
6ddc112940
commit
85d44e8471
2 files changed
+53
-37
No files matched your search
@@ -25,7 +25,11 @@ export function formatUserLabel(userId: string, resolve?: Resolver): string {
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: prefix numeric IDs with #, string IDs with @
|
||||
if (/^\d+$/.test(userId)) return `#${userId}`;
|
||||
// Fallback: prefix numeric IDs with #
|
||||
if (/^-?\d+$/.test(userId)) return `#${userId}`;
|
||||
|
||||
// Truncate long opaque IDs (e.g. oc_295eb80d325c976cbeb4a779e2010518)
|
||||
if (userId.length > 20) return `${userId.slice(0, 10)}…${userId.slice(-4)}`;
|
||||
|
||||
return userId;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, useCallback, useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Activity, GitFork, RefreshCw, Square, Bot, User, Users, Clock, Network, Globe } from "lucide-react";
|
||||
import { Activity, GitFork, RefreshCw, Square, Bot, User, Users, Clock, Network, Globe, CheckCircle2, XCircle, Loader2, CircleDot, CircleDashed } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
@@ -28,6 +28,12 @@ import { useWs } from "@/hooks/use-ws";
|
||||
import { Methods } from "@/api/protocol";
|
||||
import { toast } from "@/stores/use-toast-store";
|
||||
|
||||
/** Strip media placeholder tags like <media:image> from preview text */
|
||||
function cleanPreview(text: string): string {
|
||||
if (!text) return text;
|
||||
return text.replace(/<media:\w+>/g, "[media]");
|
||||
}
|
||||
|
||||
/** 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" };
|
||||
@@ -168,15 +174,14 @@ export function TracesPage() {
|
||||
/>
|
||||
) : (
|
||||
<div className="rounded-md border overflow-x-auto">
|
||||
<table className="w-full min-w-[700px] text-sm">
|
||||
<table className="w-full min-w-[600px] text-sm">
|
||||
<thead>
|
||||
<tr className="border-b bg-muted/50">
|
||||
<th className="px-4 py-3 text-left font-medium">{t("columns.name")}</th>
|
||||
<th className="px-4 py-3 text-left font-medium">{t("columns.status")}</th>
|
||||
<th className="px-4 py-3 text-left font-medium">{t("columns.duration")}</th>
|
||||
<th className="px-3 py-3 text-center font-medium w-10"></th>
|
||||
<th className="px-4 py-3 text-left font-medium">{t("columns.tokens")}</th>
|
||||
<th className="px-4 py-3 text-left font-medium">{t("columns.spans")}</th>
|
||||
<th className="px-4 py-3 text-left font-medium">{t("columns.time")}</th>
|
||||
<th className="px-4 py-3 text-center font-medium">{t("columns.spans")}</th>
|
||||
<th className="px-4 py-3 text-right font-medium">{t("columns.time")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -192,14 +197,20 @@ export function TracesPage() {
|
||||
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">
|
||||
<td className="px-4 py-2.5">
|
||||
<div className="flex items-center gap-1.5 text-sm font-medium min-w-0">
|
||||
{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>
|
||||
{userLabel && (
|
||||
<>
|
||||
<span className="shrink-0 text-muted-foreground">·</span>
|
||||
<span className="truncate text-xs text-muted-foreground max-w-[140px]">{userLabel}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-0.5 flex items-center gap-1">
|
||||
<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}`)}
|
||||
@@ -210,16 +221,16 @@ export function TracesPage() {
|
||||
{trace.channel}
|
||||
</Badge>
|
||||
)}
|
||||
{trace.input_preview && (
|
||||
<span className="truncate text-xs text-muted-foreground">
|
||||
{cleanPreview(trace.input_preview)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{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} />
|
||||
<td className="px-3 py-2.5 text-center">
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
<StatusIcon status={trace.status} />
|
||||
{(trace.status === "running") && (
|
||||
<Button
|
||||
variant="destructive"
|
||||
@@ -233,9 +244,6 @@ export function TracesPage() {
|
||||
)}
|
||||
</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 && (
|
||||
@@ -244,11 +252,12 @@ export function TracesPage() {
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-2.5 text-muted-foreground">
|
||||
<td className="px-4 py-2.5 text-center text-muted-foreground">
|
||||
{trace.span_count}
|
||||
</td>
|
||||
<td className="px-4 py-2.5 text-muted-foreground">
|
||||
{formatDate(trace.start_time, tz)}
|
||||
<td className="px-4 py-2.5 text-right text-muted-foreground whitespace-nowrap">
|
||||
<div>{formatDate(trace.start_time, tz)}</div>
|
||||
<div className="text-xs">{formatDuration(trace.duration_ms || computeDurationMs(trace.start_time, trace.end_time))}</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
@@ -280,15 +289,18 @@ export function TracesPage() {
|
||||
);
|
||||
}
|
||||
|
||||
function StatusBadge({ status }: { status: string }) {
|
||||
const variant =
|
||||
status === "ok" || status === "success" || status === "completed"
|
||||
? "success"
|
||||
: status === "error" || status === "failed"
|
||||
? "destructive"
|
||||
: status === "running" || status === "pending"
|
||||
? "info"
|
||||
: "secondary";
|
||||
|
||||
return <Badge variant={variant} className="text-xs">{status || "unknown"}</Badge>;
|
||||
function StatusIcon({ status }: { status: string }) {
|
||||
if (status === "ok" || status === "success" || status === "completed") {
|
||||
return <CheckCircle2 className="h-4 w-4 text-green-500" />;
|
||||
}
|
||||
if (status === "error" || status === "failed") {
|
||||
return <XCircle className="h-4 w-4 text-destructive" />;
|
||||
}
|
||||
if (status === "running") {
|
||||
return <Loader2 className="h-4 w-4 text-blue-500 animate-spin" />;
|
||||
}
|
||||
if (status === "pending") {
|
||||
return <CircleDashed className="h-4 w-4 text-muted-foreground" />;
|
||||
}
|
||||
return <CircleDot className="h-4 w-4 text-muted-foreground" />;
|
||||
}
|
||||
Reference in new issue
Block a user