diff options
| author | YurenHao0426 <blackhao0426@gmail.com> | 2026-02-13 19:41:03 +0000 |
|---|---|---|
| committer | YurenHao0426 <blackhao0426@gmail.com> | 2026-02-13 19:41:03 +0000 |
| commit | 711452fc3beae15153711cbed5af0f59eb5398f2 (patch) | |
| tree | 25a3dfab188368bbbe9161c45806482e4f4862a4 /frontend/src/components/Sidebar.tsx | |
| parent | 942749ef10851bf74d068851e6added89bea5900 (diff) | |
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>
Diffstat (limited to 'frontend/src/components/Sidebar.tsx')
| -rw-r--r-- | frontend/src/components/Sidebar.tsx | 53 |
1 files changed, 47 insertions, 6 deletions
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); @@ -1855,6 +1856,15 @@ const Sidebar: React.FC<SidebarProps> = ({ isOpen, onToggle, onInteract }) => { {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} className="p-1 hover:bg-gray-200 rounded text-gray-500 hover:text-gray-700 disabled:opacity-50" @@ -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> |
