From 963a5c462cd543f5636d12a8f5fcb9cfd7dcdef1 Mon Sep 17 00:00:00 2001 From: Celes Renata Date: Wed, 29 Apr 2026 19:33:24 +0000 Subject: [PATCH] feat: syntax-highlight decision trace JSON on order detail page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add lightweight JSON highlighter for the decision trace panel: - Keys: cyan - Strings: green - Numbers: yellow - Booleans: purple - Null: red - Structural chars: gray SQL explorer already uses Monaco with SQL highlighting — no changes needed there. --- frontend/src/pages/OrderDetail.tsx | 53 ++++++++++++++++++++++++++++-- 1 file changed, 51 insertions(+), 2 deletions(-) diff --git a/frontend/src/pages/OrderDetail.tsx b/frontend/src/pages/OrderDetail.tsx index 4dbb7b5..1c97423 100644 --- a/frontend/src/pages/OrderDetail.tsx +++ b/frontend/src/pages/OrderDetail.tsx @@ -2,6 +2,55 @@ import { useParams } from '@tanstack/react-router'; import { useOrder } from '../api/hooks'; import { StatusBadge, LoadingSpinner, Card } from '../components/ui'; +/** + * Lightweight JSON syntax highlighter for read-only display. + * Returns React elements with colored spans for keys, strings, numbers, booleans, and null. + */ +function highlightJson(json: string): React.ReactNode { + const parts: React.ReactNode[] = []; + // Regex matches JSON tokens: strings, numbers, booleans, null, and structural chars + const tokenRe = /("(?:\\.|[^"\\])*")\s*:|("(?:\\.|[^"\\])*")|(-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)|(\btrue\b|\bfalse\b)|(\bnull\b)|([{}[\],])/g; + let lastIndex = 0; + let match: RegExpExecArray | null; + + while ((match = tokenRe.exec(json)) !== null) { + // Add any whitespace/text between tokens + if (match.index > lastIndex) { + parts.push(json.slice(lastIndex, match.index)); + } + + if (match[1]) { + // Key (string followed by colon) + parts.push({match[1]}); + parts.push(':'); + } else if (match[2]) { + // String value + parts.push({match[2]}); + } else if (match[3]) { + // Number + parts.push({match[3]}); + } else if (match[4]) { + // Boolean + parts.push({match[4]}); + } else if (match[5]) { + // Null + parts.push({match[5]}); + } else if (match[6]) { + // Structural characters + parts.push({match[6]}); + } + + lastIndex = match.index + match[0].length; + } + + // Remaining text + if (lastIndex < json.length) { + parts.push(json.slice(lastIndex)); + } + + return <>{parts}; +} + export function OrderDetailPage() { const { id } = useParams({ from: '/orders/$id' }); const { data: order, isLoading } = useOrder(id); @@ -33,8 +82,8 @@ export function OrderDetailPage() { {order.decision_trace && Object.keys(order.decision_trace).length > 0 && (

Decision Trace

-
-            {JSON.stringify(order.decision_trace, null, 2)}
+          
+            {highlightJson(JSON.stringify(order.decision_trace, null, 2))}
           
)}