feat: add JsonTreeViewer component and refactor AgentRunLogDetailDialog to use it for JSON display

This commit is contained in:
mlogclub
2026-04-15 16:48:15 +08:00
parent 3749c533fb
commit 3742140ecc
4 changed files with 119 additions and 18 deletions
@@ -5,6 +5,7 @@ import { BotMessageSquareIcon, WorkflowIcon } from "lucide-react"
import { toast } from "sonner"
import { ImMessageHTML } from "@/components/im-message-html"
import { JsonTreeViewer } from "@/components/json-tree-viewer"
import { ProjectDialog } from "@/components/project-dialog"
import { Button } from "@/components/ui/button"
import { fetchAgentRunLog, type AgentRunLog } from "@/lib/api/admin"
@@ -149,21 +150,15 @@ export function AgentRunLogDetailDialog({
]}
/>
<TextBlock
<JsonBlock
title="动态工具选择"
value={
activeToolSearchTrace
? JSON.stringify(activeToolSearchTrace, null, 2)
: activeLog.toolSearchTrace
}
jsonValue={activeToolSearchTrace}
fallbackValue={activeLog.toolSearchTrace}
/>
<TextBlock
<JsonBlock
title="Graph Tool 调用"
value={
activeGraphToolTrace
? JSON.stringify(activeGraphToolTrace, null, 2)
: activeLog.graphToolTrace
}
jsonValue={activeGraphToolTrace}
fallbackValue={activeLog.graphToolTrace}
/>
<TextBlock
icon={<BotMessageSquareIcon className="size-4" />}
@@ -177,13 +172,10 @@ export function AgentRunLogDetailDialog({
value={activeLog.replyText}
/>
<TextBlock title="错误信息" value={activeLog.errorMessage} tone="danger" />
<TextBlock
<JsonBlock
title="链路 Trace"
value={
activeTraceData
? JSON.stringify(activeTraceData, null, 2)
: activeLog.traceData
}
jsonValue={activeTraceData}
fallbackValue={activeLog.traceData}
/>
</>
) : (
@@ -283,6 +275,31 @@ function TextBlock({
)
}
function JsonBlock({
title,
jsonValue,
fallbackValue,
}: {
title: string
jsonValue: unknown
fallbackValue?: string
}) {
const normalizedFallback = fallbackValue?.trim() || ""
return (
<div className="rounded-lg border p-4">
<div className="text-sm font-medium">{title}</div>
{jsonValue ? (
<JsonTreeViewer value={jsonValue} className="mt-3" />
) : (
<div className="mt-3 select-text whitespace-pre-wrap wrap-break-word text-sm text-muted-foreground">
{normalizedFallback || "-"}
</div>
)}
</div>
)
}
function sanitizeRichHTML(value: string) {
if (typeof window === "undefined") {
return value
+67
View File
@@ -0,0 +1,67 @@
"use client"
import type { CSSProperties } from "react"
import JsonView from "@uiw/react-json-view"
import { cn } from "@/lib/utils"
type JsonTreeViewerProps = {
value: unknown
emptyText?: string
className?: string
collapsed?: boolean | number
}
const viewerTheme = {
"--w-rjv-font-family":
'ui-monospace, SFMono-Regular, "SF Mono", Consolas, "Liberation Mono", Menlo, monospace',
"--w-rjv-background-color": "transparent",
"--w-rjv-color": "hsl(var(--foreground))",
"--w-rjv-line-color": "hsl(var(--border))",
"--w-rjv-arrow-color": "hsl(var(--muted-foreground))",
"--w-rjv-info-color": "hsl(var(--muted-foreground))",
"--w-rjv-curlybraces-color": "hsl(var(--foreground))",
"--w-rjv-brackets-color": "hsl(var(--foreground))",
"--w-rjv-colon-color": "hsl(var(--muted-foreground))",
"--w-rjv-ellipsis-color": "hsl(var(--muted-foreground))",
"--w-rjv-key-string": "hsl(199 89% 58%)",
"--w-rjv-type-string-color": "hsl(142 76% 45%)",
"--w-rjv-type-int-color": "hsl(262 83% 70%)",
"--w-rjv-type-float-color": "hsl(262 83% 70%)",
"--w-rjv-type-boolean-color": "hsl(38 92% 50%)",
"--w-rjv-type-null-color": "hsl(347 77% 50%)",
} as CSSProperties
export function JsonTreeViewer({
value,
emptyText = "暂无数据",
className,
collapsed = 2,
}: JsonTreeViewerProps) {
if (value == null || value === "") {
return (
<div
className={cn(
"rounded-md border bg-muted/20 px-4 py-3 font-mono text-xs text-muted-foreground",
className
)}
>
{emptyText}
</div>
)
}
return (
<div className={cn("rounded-md border bg-muted/20 p-3", className)}>
<JsonView
value={value as Record<string, unknown>}
collapsed={collapsed}
displayDataTypes={false}
displayObjectSize
enableClipboard
shortenTextAfterLength={120}
style={viewerTheme}
/>
</div>
)
}
+1
View File
@@ -23,6 +23,7 @@
"@tiptap/extension-underline": "3.20.2",
"@tiptap/react": "^3.20.2",
"@tiptap/starter-kit": "^3.20.2",
"@uiw/react-json-view": "2.0.0-alpha.41",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
+16
View File
@@ -47,6 +47,9 @@ importers:
'@tiptap/starter-kit':
specifier: ^3.20.2
version: 3.20.2
'@uiw/react-json-view':
specifier: 2.0.0-alpha.41
version: 2.0.0-alpha.41(@babel/runtime@7.28.6)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
class-variance-authority:
specifier: ^0.7.1
version: 0.7.1
@@ -1564,6 +1567,13 @@ packages:
resolution: {integrity: sha512-zm6xx8UT/Xy2oSr2ZXD0pZo7Jx2XsCoID2IUh9YSTFRu7z+WdwYTRk6LhUftm1crwqbuoF6I8zAFeCMw0YjwDg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@uiw/react-json-view@2.0.0-alpha.41':
resolution: {integrity: sha512-botRpQ5AgymYEsqXSdT2/1LefAJEYfMntvdnx1SqhTQCTW9HygeFZXx9inkYqUmiQZ3+0QlZnodjBvwnUfZhVA==}
peerDependencies:
'@babel/runtime': '>=7.10.0'
react: '>=18.0.0'
react-dom: '>=18.0.0'
'@unrs/resolver-binding-android-arm-eabi@1.11.1':
resolution: {integrity: sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==}
cpu: [arm]
@@ -5726,6 +5736,12 @@ snapshots:
'@typescript-eslint/types': 8.57.0
eslint-visitor-keys: 5.0.1
'@uiw/react-json-view@2.0.0-alpha.41(@babel/runtime@7.28.6)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
dependencies:
'@babel/runtime': 7.28.6
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
'@unrs/resolver-binding-android-arm-eabi@1.11.1':
optional: true