diff --git a/ui/web/src/i18n/locales/en/traces.json b/ui/web/src/i18n/locales/en/traces.json index 56014d0f..30be6bd3 100644 --- a/ui/web/src/i18n/locales/en/traces.json +++ b/ui/web/src/i18n/locales/en/traces.json @@ -16,6 +16,7 @@ "time": "Time" }, "unnamed": "Unnamed", + "stopRun": "Stop run", "cached": "cached", "detail": { "title": "Trace Detail", @@ -36,7 +37,8 @@ "spansCount": "Spans ({{count}})", "llmCalls": "LLM", "toolCalls": "tool", - "export": "Export trace" + "export": "Export trace", + "stopRun": "Stop run" }, "span": { "model": "Model:", @@ -53,6 +55,9 @@ "cached": "cached" }, "toast": { - "failedLoad": "Failed to load traces" + "failedLoad": "Failed to load traces", + "abortSent": "Run stopped", + "abortFailed": "Failed to stop run", + "abortNotFound": "Run already finished or not found" } } diff --git a/ui/web/src/i18n/locales/vi/traces.json b/ui/web/src/i18n/locales/vi/traces.json index 3ed43aea..de4f28a2 100644 --- a/ui/web/src/i18n/locales/vi/traces.json +++ b/ui/web/src/i18n/locales/vi/traces.json @@ -16,6 +16,7 @@ "time": "Thời gian" }, "unnamed": "Không tên", + "stopRun": "Dừng chạy", "cached": "đã cache", "detail": { "title": "Chi tiết theo dõi", @@ -36,7 +37,8 @@ "spansCount": "Khoảng ({{count}})", "llmCalls": "LLM", "toolCalls": "công cụ", - "export": "Xuất trace" + "export": "Xuất trace", + "stopRun": "Dừng chạy" }, "span": { "model": "Model:", @@ -53,6 +55,9 @@ "cached": "đã cache" }, "toast": { - "failedLoad": "Không thể tải dữ liệu theo dõi" + "failedLoad": "Không thể tải dữ liệu theo dõi", + "abortSent": "Đã dừng chạy", + "abortFailed": "Không thể dừng chạy", + "abortNotFound": "Lệnh đã hoàn tất hoặc không tìm thấy" } } diff --git a/ui/web/src/i18n/locales/zh/traces.json b/ui/web/src/i18n/locales/zh/traces.json index e13751b5..6e3dcffb 100644 --- a/ui/web/src/i18n/locales/zh/traces.json +++ b/ui/web/src/i18n/locales/zh/traces.json @@ -28,7 +28,8 @@ "title": "追踪详情", "tokens": "令牌:", "toolCalls": "工具", - "export": "导出追踪" + "export": "导出追踪", + "stopRun": "停止运行" }, "emptyDescription": "暂无追踪记录。当Agent处理请求时将自动记录追踪。", "emptyTitle": "暂无追踪", @@ -51,7 +52,11 @@ }, "title": "追踪", "toast": { - "failedLoad": "加载追踪失败" + "failedLoad": "加载追踪失败", + "abortSent": "已停止运行", + "abortFailed": "停止运行失败", + "abortNotFound": "运行已完成或未找到" }, - "unnamed": "未命名" + "unnamed": "未命名", + "stopRun": "停止运行" } diff --git a/ui/web/src/pages/traces/trace-detail-dialog.tsx b/ui/web/src/pages/traces/trace-detail-dialog.tsx index d3806345..b58775bf 100644 --- a/ui/web/src/pages/traces/trace-detail-dialog.tsx +++ b/ui/web/src/pages/traces/trace-detail-dialog.tsx @@ -7,7 +7,7 @@ import { DialogTitle, } from "@/components/ui/dialog"; import { Badge } from "@/components/ui/badge"; -import { ChevronRight, ChevronDown, Copy, Check, Download, CircleCheck, CircleX, Loader, CircleMinus } from "lucide-react"; +import { ChevronRight, ChevronDown, Copy, Check, Download, CircleCheck, CircleX, Loader, CircleMinus, Square } from "lucide-react"; import { useClipboard } from "@/hooks/use-clipboard"; import { useHttp } from "@/hooks/use-ws"; import { useWsEvent } from "@/hooks/use-ws-event"; @@ -53,9 +53,10 @@ interface TraceDetailDialogProps { onClose: () => void; getTrace: (id: string) => Promise<{ trace: TraceData; spans: SpanData[] } | null>; onNavigateTrace?: (traceId: string) => void; + onAbortRun?: (trace: TraceData, e: React.MouseEvent) => void; } -export function TraceDetailDialog({ traceId, onClose, getTrace, onNavigateTrace }: TraceDetailDialogProps) { +export function TraceDetailDialog({ traceId, onClose, getTrace, onNavigateTrace, onAbortRun }: TraceDetailDialogProps) { const { t } = useTranslation("traces"); const tz = useUiStore((s) => s.timezone); const http = useHttp(); @@ -160,6 +161,16 @@ export function TraceDetailDialog({ traceId, onClose, getTrace, onNavigateTrace )} {t("detail.export")} + {trace && (trace.status === "running") && onAbortRun && ( + + )} diff --git a/ui/web/src/pages/traces/traces-page.tsx b/ui/web/src/pages/traces/traces-page.tsx index 42b1cd43..4594e0cb 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 } from "react"; +import { useState, useCallback } from "react"; import { useTranslation } from "react-i18next"; -import { Activity, GitFork, RefreshCw } from "lucide-react"; +import { Activity, GitFork, RefreshCw, Square } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { @@ -22,6 +22,9 @@ import { useDeferredLoading } from "@/hooks/use-deferred-loading"; import { useUiStore } from "@/stores/use-ui-store"; import { useAgents } from "@/pages/agents/hooks/use-agents"; import { useChannelInstances } from "@/pages/channels/hooks/use-channel-instances"; +import { useWs } from "@/hooks/use-ws"; +import { Methods } from "@/api/protocol"; +import { toast } from "@/stores/use-toast-store"; export function TracesPage() { const { t } = useTranslation("traces"); @@ -33,6 +36,7 @@ export function TracesPage() { const [page, setPage] = useState(1); const [pageSize, setPageSize] = useState(20); + const ws = useWs(); const { agents } = useAgents(); const { instances: channels } = useChannelInstances(); @@ -42,6 +46,28 @@ export function TracesPage() { limit: pageSize, offset: (page - 1) * pageSize, }); + + const handleAbortRun = useCallback( + async (trace: TraceData, e: React.MouseEvent) => { + e.stopPropagation(); // Don't open trace detail + if (!ws.isConnected) return; + try { + const res = await ws.call(Methods.CHAT_ABORT, { + sessionKey: trace.session_key, + runId: trace.run_id, + }) as { aborted?: boolean }; + if (res?.aborted) { + toast.success(t("toast.abortSent")); + refresh(); + } else { + toast.info(t("toast.abortNotFound")); + } + } catch { + toast.error(t("toast.abortFailed")); + } + }, + [ws, t, refresh], + ); const spinning = useMinLoading(fetching); const showSkeleton = useDeferredLoading(loading && traces.length === 0); @@ -134,7 +160,19 @@ export function TracesPage() { )}