From 711452fc3beae15153711cbed5af0f59eb5398f2 Mon Sep 17 00:00:00 2001
From: YurenHao0426 <blackhao0426@gmail.com>
Date: Fri, 13 Feb 2026 19:41:03 +0000
Subject: Add rendered/plain text toggle for response views

Add an Eye/EyeOff toggle button to switch between rendered markdown and
raw plain text in all three response locations: main response panel,
expanded response modal, and Quick Chat messages. Useful for copying
raw text without formatting artifacts.

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

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

diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx
index 079b4d5..b73c513 100644
--- a/frontend/src/components/Sidebar.tsx
+++ b/frontend/src/components/Sidebar.tsx
@@ -17,7 +17,7 @@ const preprocessLaTeX = (content: string): string => {
     .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';
+import { Play, Settings, Info, ChevronLeft, ChevronRight, Maximize2, Edit3, X, Check, FileText, MessageCircle, Send, GripVertical, GitMerge, Trash2, AlertCircle, Loader2, Navigation, Upload, Search, Link, Layers, Eye, EyeOff } from 'lucide-react';
 
 interface SidebarProps {
   isOpen: boolean;
@@ -55,6 +55,7 @@ const Sidebar: React.FC<SidebarProps> = ({ isOpen, onToggle, onInteract }) => {
   const [isModalOpen, setIsModalOpen] = useState(false);
   const [isEditing, setIsEditing] = useState(false);
   const [editedResponse, setEditedResponse] = useState('');
+  const [rawTextMode, setRawTextMode] = useState(false);
   
   // Summary states
   const [showSummaryModal, setShowSummaryModal] = useState(false);
@@ -1854,6 +1855,15 @@ const Sidebar: React.FC<SidebarProps> = ({ isOpen, onToggle, onInteract }) => {
                 <div className="flex gap-1">
                   {selectedNode.data.response && (
                     <>
+                      <button
+                        onClick={() => setRawTextMode(!rawTextMode)}
+                        className={`p-1 rounded ${rawTextMode
+                          ? (isDark ? 'bg-blue-900/50 text-blue-400' : 'bg-blue-100 text-blue-600')
+                          : (isDark ? 'text-gray-400 hover:bg-gray-700' : 'text-gray-500 hover:bg-gray-200')}`}
+                        title={rawTextMode ? 'Rendered view' : 'Plain text'}
+                      >
+                        {rawTextMode ? <Eye size={14} /> : <EyeOff size={14} />}
+                      </button>
                       <button
                         onClick={() => setShowSummaryModal(true)}
                         disabled={isSummarizing}
@@ -1909,10 +1919,16 @@ const Sidebar: React.FC<SidebarProps> = ({ isOpen, onToggle, onInteract }) => {
                     </button>
                   </div>
                 </div>
+              ) : rawTextMode ? (
+                <pre className={`p-3 rounded-md border min-h-[150px] text-sm whitespace-pre-wrap break-words overflow-auto ${
+                  isDark
+                    ? 'bg-gray-900 border-gray-700 text-gray-200'
+                    : 'bg-gray-50 border-gray-200 text-gray-900'
+                }`}>{selectedNode.data.response || (streamingNodeId === selectedNode.id ? streamBuffer : '')}</pre>
               ) : (
                 <div className={`p-3 rounded-md border min-h-[150px] text-sm prose prose-sm max-w-none ${
-                  isDark 
-                    ? 'bg-gray-900 border-gray-700 prose-invert text-gray-200' 
+                  isDark
+                    ? 'bg-gray-900 border-gray-700 prose-invert text-gray-200'
                     : 'bg-gray-50 border-gray-200 text-gray-900'
                 }`}>
                   <ReactMarkdown remarkPlugins={[remarkGfm, remarkMath]} rehypePlugins={[rehypeKatex]}>{preprocessLaTeX(selectedNode.data.response || (streamingNodeId === selectedNode.id ? streamBuffer : ''))}</ReactMarkdown>
@@ -2186,18 +2202,30 @@ const Sidebar: React.FC<SidebarProps> = ({ isOpen, onToggle, onInteract }) => {
             {/* Modal Header */}
             <div className={`flex items-center justify-between p-4 border-b ${isDark ? 'border-gray-700' : 'border-gray-200'}`}>
               <h3 className="font-semibold text-lg">{selectedNode.data.label} - Response</h3>
-              <div className="flex gap-2">
+              <div className="flex gap-2 items-center">
+                {!isEditing && (
+                  <button
+                    onClick={() => setRawTextMode(!rawTextMode)}
+                    className={`px-3 py-1 text-sm rounded flex items-center gap-1 ${rawTextMode
+                      ? (isDark ? 'bg-blue-900/50 text-blue-400' : 'bg-blue-100 text-blue-600')
+                      : (isDark ? 'text-gray-400 hover:bg-gray-700' : 'text-gray-600 hover:bg-gray-100')}`}
+                    title={rawTextMode ? 'Rendered view' : 'Plain text'}
+                  >
+                    {rawTextMode ? <Eye size={14} /> : <EyeOff size={14} />}
+                    {rawTextMode ? 'Rendered' : 'Plain text'}
+                  </button>
+                )}
                 {!isEditing && (
                   <button
                     onClick={() => setIsEditing(true)}
-                    className="px-3 py-1 text-sm text-gray-600 hover:bg-gray-100 rounded flex items-center gap-1"
+                    className={`px-3 py-1 text-sm rounded flex items-center gap-1 ${isDark ? 'text-gray-400 hover:bg-gray-700' : 'text-gray-600 hover:bg-gray-100'}`}
                   >
                     <Edit3 size={14} /> Edit
                   </button>
                 )}
                 <button
                   onClick={() => { setIsModalOpen(false); setIsEditing(false); }}
-                  className="p-1 hover:bg-gray-200 rounded text-gray-500"
+                  className={`p-1 rounded ${isDark ? 'text-gray-400 hover:bg-gray-700' : 'text-gray-500 hover:bg-gray-200'}`}
                 >
                   <X size={18} />
                 </button>
@@ -2212,6 +2240,8 @@ const Sidebar: React.FC<SidebarProps> = ({ isOpen, onToggle, onInteract }) => {
                   onChange={(e) => setEditedResponse(e.target.value)}
                   className="w-full h-full min-h-[400px] border border-gray-300 rounded-md p-3 text-sm font-mono focus:ring-2 focus:ring-blue-500 resize-y"
                 />
+              ) : rawTextMode ? (
+                <pre className={`text-sm whitespace-pre-wrap break-words ${isDark ? 'text-gray-200' : 'text-gray-900'}`}>{selectedNode.data.response || ''}</pre>
               ) : (
                 <div className={`prose prose-sm max-w-none ${isDark ? 'prose-invert' : ''}`}>
                   <ReactMarkdown remarkPlugins={[remarkGfm, remarkMath]} rehypePlugins={[rehypeKatex]}>{preprocessLaTeX(selectedNode.data.response || '')}</ReactMarkdown>
@@ -2527,6 +2557,15 @@ const Sidebar: React.FC<SidebarProps> = ({ isOpen, onToggle, onInteract }) => {
                     <option value="o3" disabled={!canUsePremiumModels}>o3 {!canUsePremiumModels && '🔒'}</option>
                   </optgroup>
                 </select>
+                <button
+                  onClick={() => setRawTextMode(!rawTextMode)}
+                  className={`p-1 rounded ${rawTextMode
+                    ? (isDark ? 'bg-blue-900/50 text-blue-400' : 'bg-blue-100 text-blue-600')
+                    : (isDark ? 'text-gray-400 hover:bg-gray-700' : 'text-gray-500 hover:bg-gray-200')}`}
+                  title={rawTextMode ? 'Rendered view' : 'Plain text'}
+                >
+                  {rawTextMode ? <Eye size={16} /> : <EyeOff size={16} />}
+                </button>
                 <button onClick={closeQuickChat} className={`p-1 rounded ${isDark ? 'hover:bg-gray-700' : 'hover:bg-gray-200'}`}>
                   <X size={20} className={isDark ? 'text-gray-400' : 'text-gray-500'} />
                 </button>
@@ -2608,6 +2647,8 @@ const Sidebar: React.FC<SidebarProps> = ({ isOpen, onToggle, onInteract }) => {
                           
                           {msg.role === 'user' ? (
                             <p className="whitespace-pre-wrap">{msg.content}</p>
+                          ) : rawTextMode ? (
+                            <pre className="text-sm whitespace-pre-wrap break-words">{msg.content}</pre>
                           ) : (
                             <div className={`prose prose-sm max-w-none ${isDark ? 'prose-invert' : ''}`}>
                               <ReactMarkdown remarkPlugins={[remarkGfm, remarkMath]} rehypePlugins={[rehypeKatex]}>{preprocessLaTeX(msg.content)}</ReactMarkdown>
-- 
cgit v1.2.3

