diff --git a/src/canvas/AssistantTracePage.tsx b/src/canvas/AssistantTracePage.tsx index 5f871e01..feba5286 100644 --- a/src/canvas/AssistantTracePage.tsx +++ b/src/canvas/AssistantTracePage.tsx @@ -1,6 +1,7 @@ import { useEffect, useMemo, useState } from "react"; import { traceCategory } from "./traceCategory.ts"; +import { useT } from "../i18n/I18nProvider"; import { normalizeDeepseekTrace } from "./deepseekTrace.ts"; @@ -31,19 +32,21 @@ type TraceRecord = { }; }; -const labels: Record = { - system: ["系统", "System"], user: ["用户", "User"], context: ["上下文", "Context"], - assistant: ["助手", "Assistant"], tool: ["工具", "Tool"], model: ["模型", "Model"], - event: ["事件", "Event"], reasoning: ["思考", "Thinking"], -}; +const labels = { + system: "assistant.trace.kind.system", user: "assistant.trace.kind.user", + context: "assistant.trace.kind.context", assistant: "assistant.trace.kind.assistant", + tool: "assistant.trace.kind.tool", model: "assistant.trace.kind.model", + event: "assistant.trace.kind.event", reasoning: "assistant.trace.kind.reasoning", +} as const; -function TraceRow({ kind, summary, value, chinese }: { - kind: string; summary: string; value: unknown; chinese: boolean; +function TraceRow({ kind, summary, value }: { + kind: keyof typeof labels; summary: string; value: unknown; }) { + const t = useT(); return (
- {labels[kind][chinese ? 0 : 1]} + {t(labels[kind])} {summary}
{typeof value === "string" ? value : JSON.stringify(value, null, 2)}
@@ -51,7 +54,8 @@ function TraceRow({ kind, summary, value, chinese }: { ); } -function TraceEntries({ traces, chinese, rawModel }: { traces: string[]; chinese: boolean; rawModel: boolean }) { +function TraceEntries({ traces, rawModel }: { traces: string[]; rawModel: boolean }) { + const t = useT(); const entries = useMemo(() => { const parsed = traces.flatMap((trace, sourceIndex) => { let timestamp = 0; @@ -93,9 +97,9 @@ function TraceEntries({ traces, chinese, rawModel }: { traces: string[]; chinese message.role === "user" && message.content === "Generate a title for this conversation:"); if (rawModel) { request++; - const label = titleRequest ? (chinese ? "生成会话标题" : "Generate conversation title") : - (chinese ? `第 ${request} 次模型请求` : `Model request ${request}`); - return ; + const label = titleRequest ? t("assistant.trace.titleRequest") : + t("assistant.trace.modelRequest", { count: request }); + return ; } const thoughts = (payload.messages ?? []).flatMap((message, index) => { const text = message.reasoning_content?.trim(); @@ -104,7 +108,7 @@ function TraceEntries({ traces, chinese, rawModel }: { traces: string[]; chinese if (seenReasoning.has(identity)) return []; seenReasoning.add(identity); if (hasStreamedReasoning || firstModelRequest || titleRequest) return []; - return []; + return []; }); if (!titleRequest) firstModelRequest = false; const inputs = titleRequest ? [] : (payload.messages ?? []).flatMap((message, index) => { @@ -114,34 +118,34 @@ function TraceEntries({ traces, chinese, rawModel }: { traces: string[]; chinese seenInputs.add(identity); const content = typeof message.content === "string" ? message.content : identity; return []; + summary={t("assistant.trace.systemPrompt")} + value={content} />]; }); const toolsKey = payload.tools?.length ? JSON.stringify(payload.tools) : undefined; if (toolsKey && !seenInputs.has(toolsKey)) { seenInputs.add(toolsKey); inputs.push(); + summary={t("assistant.trace.toolDefinitions", { count: payload.tools?.length ?? 0 })} + value={payload.tools} />); } return
{thoughts}{inputs}
; } const type = record?.native?.event_type ?? record?.native?.type ?? record?.event ?? record?.type; const part = record?.native?.part; if (type === "model.reasoning" && record?.text) { - return ; + return ; } if (!rawModel && type?.startsWith("model.")) return null; if (type === "reasoning" && part?.text?.trim()) { if (hasStreamedReasoning) return null; - return ; + return ; } const assistantText = type === "text" || type === "reasoning" ? part?.text : type === "item.completed" && record?.native?.item?.type === "agent_message" ? record.native.item.text : type === "assistant" ? record?.native?.message?.content ?.filter((block) => block.type === "text").map((block) => block.text).join("\n") : undefined; if (assistantText?.trim()) { - return ; + return ; } if (type === "step_start" || type === "text" || type === "reasoning") return null; const kind = traceCategory( @@ -153,7 +157,7 @@ function TraceEntries({ traces, chinese, rawModel }: { traces: string[]; chinese part?.state?.status ?? part?.reason ?? record?.error ?? record?.status, record?.elapsedMs === undefined ? undefined : `${record.elapsedMs} ms`, ].filter(Boolean).join(" · ") || line; - return ; + return ; }); } @@ -167,7 +171,7 @@ export default function AssistantTracePage({ params }: { params: URLSearchParams const [showModel, setShowModel] = useState(false); const rawModel = runtime === "opencode" && showModel; const [status, setStatus] = useState(null); - const chinese = navigator.language.startsWith("zh"); + const t = useT(); useEffect(() => { const controller = new AbortController(); @@ -213,31 +217,31 @@ export default function AssistantTracePage({ params }: { params: URLSearchParams return (
-

{chinese ? "助手轨迹" : "Assistant trace"}

+

{t("assistant.trace.title")}

{turn}

{runtime === "opencode" && ( -
- {!rawModel && input !== null && } + {!rawModel && input !== null && } {status === 200 ? ( rawModel && !traces[0]?.trim() ? ( -

{chinese ? "该轮暂无模型请求记录。" : "No model requests recorded for this turn."}

+

{t("assistant.trace.noModelRequests")}

) : ( - + ) ) : ( -

{status === null ? (chinese ? "加载中…" : "Loading…") : - status === 404 && live ? (chinese ? "等待首条轨迹…" : "Waiting for trace events…") : - status === 404 ? (chinese ? "该轮没有此类轨迹。" : "No trace of this type for this turn.") : - (chinese ? "轨迹加载失败。" : "Could not load trace.")}

+

{status === null ? t("assistant.trace.loading") : + status === 404 && live ? t("assistant.trace.waiting") : + status === 404 ? t("assistant.trace.missing") : + t("assistant.trace.failed")}

)}
); diff --git a/src/canvas/traceCategory.ts b/src/canvas/traceCategory.ts index a8257f92..5e38d8c5 100644 --- a/src/canvas/traceCategory.ts +++ b/src/canvas/traceCategory.ts @@ -1,5 +1,8 @@ /** Semantic colors are shared by every harness, independent of its event names. */ -export function traceCategory(type?: string, role?: string): string { +export type TraceCategory = + | "system" | "user" | "context" | "reasoning" | "tool" | "assistant" | "model" | "event"; + +export function traceCategory(type?: string, role?: string): TraceCategory { if (role === "system" || role === "developer" || type === "system") return "system"; if (role === "user" || type === "user" || type === "user_message") return "user"; if (type === "plan" || type === "context" || type === "session_meta" || type === "turn_context") return "context"; diff --git a/src/i18n/messages/en-US/assistant.ts b/src/i18n/messages/en-US/assistant.ts index 81190e9b..a9145163 100644 --- a/src/i18n/messages/en-US/assistant.ts +++ b/src/i18n/messages/en-US/assistant.ts @@ -36,6 +36,27 @@ export const assistant = { "assistant.panel.welcomeTitle": "Start from an idea", "assistant.panel.welcomeLead": "Shape your next work with OpenDirector.", "assistant.panel.trace": "Trace", + // Assistant trace page (opened from a reply's trace link). + "assistant.trace.title": "Assistant trace", + "assistant.trace.views": "Trace views", + "assistant.trace.rawRequests": "Raw model requests", + "assistant.trace.noModelRequests": "No model requests recorded for this turn.", + "assistant.trace.loading": "Loading…", + "assistant.trace.waiting": "Waiting for trace events…", + "assistant.trace.missing": "No trace of this type for this turn.", + "assistant.trace.failed": "Could not load trace.", + "assistant.trace.titleRequest": "Generate conversation title", + "assistant.trace.modelRequest": "Model request {count}", + "assistant.trace.systemPrompt": "System prompt", + "assistant.trace.toolDefinitions": "Tool definitions · {count}", + "assistant.trace.kind.system": "System", + "assistant.trace.kind.user": "User", + "assistant.trace.kind.context": "Context", + "assistant.trace.kind.assistant": "Assistant", + "assistant.trace.kind.tool": "Tool", + "assistant.trace.kind.model": "Model", + "assistant.trace.kind.event": "Event", + "assistant.trace.kind.reasoning": "Thinking", "assistant.panel.welcomeBody": "Describe a story, or type @ to add image, video, audio and text references.", "assistant.panel.composer": "Chat input", diff --git a/src/i18n/messages/zh-CN/assistant.ts b/src/i18n/messages/zh-CN/assistant.ts index fa348e80..677bd595 100644 --- a/src/i18n/messages/zh-CN/assistant.ts +++ b/src/i18n/messages/zh-CN/assistant.ts @@ -35,6 +35,27 @@ export const assistant = { "assistant.panel.welcomeTitle": "从一个想法开始", "assistant.panel.welcomeLead": "和 OpenDirector 一起构思你的下一部作品。", "assistant.panel.trace": "轨迹", + // Assistant trace page (opened from a reply's trace link). + "assistant.trace.title": "助手轨迹", + "assistant.trace.views": "轨迹视图", + "assistant.trace.rawRequests": "模型原始请求", + "assistant.trace.noModelRequests": "该轮暂无模型请求记录。", + "assistant.trace.loading": "加载中…", + "assistant.trace.waiting": "等待首条轨迹…", + "assistant.trace.missing": "该轮没有此类轨迹。", + "assistant.trace.failed": "轨迹加载失败。", + "assistant.trace.titleRequest": "生成会话标题", + "assistant.trace.modelRequest": "第 {count} 次模型请求", + "assistant.trace.systemPrompt": "系统提示词", + "assistant.trace.toolDefinitions": "工具定义 · {count}", + "assistant.trace.kind.system": "系统", + "assistant.trace.kind.user": "用户", + "assistant.trace.kind.context": "上下文", + "assistant.trace.kind.assistant": "助手", + "assistant.trace.kind.tool": "工具", + "assistant.trace.kind.model": "模型", + "assistant.trace.kind.event": "事件", + "assistant.trace.kind.reasoning": "思考", "assistant.panel.welcomeBody": "描述故事,或输入 @ 添加图片、视频、音频与文本参考。", "assistant.panel.composer": "聊天输入框",