From 962e96b0f0f0b91341b65e53b9b52b7c5b10f6d6 Mon Sep 17 00:00:00 2001
From: YurenHao0426 <blackhao0426@gmail.com>
Date: Fri, 13 Feb 2026 19:23:14 +0000
Subject: Add LaTeX math rendering with KaTeX

Support both $...$ / $$...$$ (remark-math) and \(...\) / \[...\] (GPT-style)
delimiters via a preprocessor that normalizes to dollar-sign syntax before
passing to ReactMarkdown.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---
 frontend/src/components/Sidebar.tsx | 17 ++++++++++++++---
 1 file changed, 14 insertions(+), 3 deletions(-)

(limited to 'frontend/src/components/Sidebar.tsx')

diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx
index 13cf3e9..f114151 100644
--- a/frontend/src/components/Sidebar.tsx
+++ b/frontend/src/components/Sidebar.tsx
@@ -6,6 +6,17 @@ import type { NodeData, Trace, Message, MergedTrace, MergeStrategy } from '../st
 import type { Edge } from 'reactflow';
 import ReactMarkdown from 'react-markdown';
 import remarkGfm from 'remark-gfm';
+import remarkMath from 'remark-math';
+import rehypeKatex from 'rehype-katex';
+import 'katex/dist/katex.min.css';
+
+// Preprocess LaTeX delimiters: convert \(...\) → $...$ and \[...\] → $$...$$
+const preprocessLaTeX = (content: string): string => {
+  return content
+    .replace(/\\\[([\s\S]*?)\\\]/g, (_, math) => `$$${math}$$`)
+    .replace(/\\\(([\s\S]*?)\\\)/g, (_, math) => `$${math}$`);
+};
+
 import { Play, Settings, Info, ChevronLeft, ChevronRight, Maximize2, Edit3, X, Check, FileText, MessageCircle, Send, GripVertical, GitMerge, Trash2, AlertCircle, Loader2, Navigation, Upload, Search, Link, Layers } from 'lucide-react';
 
 interface SidebarProps {
@@ -1904,7 +1915,7 @@ const Sidebar: React.FC<SidebarProps> = ({ isOpen, onToggle, onInteract }) => {
                     ? 'bg-gray-900 border-gray-700 prose-invert text-gray-200' 
                     : 'bg-gray-50 border-gray-200 text-gray-900'
                 }`}>
-                  <ReactMarkdown remarkPlugins={[remarkGfm]}>{selectedNode.data.response || (streamingNodeId === selectedNode.id ? streamBuffer : '')}</ReactMarkdown>
+                  <ReactMarkdown remarkPlugins={[remarkGfm, remarkMath]} rehypePlugins={[rehypeKatex]}>{preprocessLaTeX(selectedNode.data.response || (streamingNodeId === selectedNode.id ? streamBuffer : ''))}</ReactMarkdown>
                 </div>
               )}
             </div>
@@ -2203,7 +2214,7 @@ const Sidebar: React.FC<SidebarProps> = ({ isOpen, onToggle, onInteract }) => {
                 />
               ) : (
                 <div className="prose prose-sm max-w-none">
-                  <ReactMarkdown remarkPlugins={[remarkGfm]}>{selectedNode.data.response}</ReactMarkdown>
+                  <ReactMarkdown remarkPlugins={[remarkGfm, remarkMath]} rehypePlugins={[rehypeKatex]}>{preprocessLaTeX(selectedNode.data.response || '')}</ReactMarkdown>
                 </div>
               )}
             </div>
@@ -2599,7 +2610,7 @@ const Sidebar: React.FC<SidebarProps> = ({ isOpen, onToggle, onInteract }) => {
                             <p className="whitespace-pre-wrap">{msg.content}</p>
                           ) : (
                             <div className={`prose prose-sm max-w-none ${isDark ? 'prose-invert' : ''}`}>
-                              <ReactMarkdown remarkPlugins={[remarkGfm]}>{msg.content}</ReactMarkdown>
+                              <ReactMarkdown remarkPlugins={[remarkGfm, remarkMath]} rehypePlugins={[rehypeKatex]}>{preprocessLaTeX(msg.content)}</ReactMarkdown>
                             </div>
                           )}
                         </div>
-- 
cgit v1.2.3

