diff --git a/desktop/src/components/chat/CodeViewer.tsx b/desktop/src/components/chat/CodeViewer.tsx index 4af18121..57abd7ed 100644 --- a/desktop/src/components/chat/CodeViewer.tsx +++ b/desktop/src/components/chat/CodeViewer.tsx @@ -1,5 +1,5 @@ -import { useMemo, useState } from 'react' -import hljs from 'highlight.js' +import { useState } from 'react' +import { Highlight, themes } from 'prism-react-renderer' import { CopyButton } from '../shared/CopyButton' type Props = { @@ -12,37 +12,14 @@ type Props = { export function CodeViewer({ code, language, maxLines = 20, showLineNumbers = true }: Props) { const [expanded, setExpanded] = useState(false) - const lines = code.split('\n') - const isTruncated = !expanded && lines.length > maxLines - const visibleCode = isTruncated ? lines.slice(0, maxLines).join('\n') : code - - // Only highlight when language is explicitly known. - // Do NOT use highlightAuto — it misdetects plain text as code. - const hasLanguage = !!language && !!hljs.getLanguage(language) - const effectiveShowLineNumbers = showLineNumbers && hasLanguage - - const highlightedLines = useMemo(() => { - return visibleCode.split('\n').map((line) => { - try { - if (hasLanguage) { - return hljs.highlight(line || ' ', { language: language!, ignoreIllegals: true }).value - } - return escapeHtml(line || ' ') - } catch { - return escapeHtml(line) - } - }) - }, [hasLanguage, language, visibleCode]) - - const startLine = 1 + const allLines = code.split('\n') + const isTruncated = !expanded && allLines.length > maxLines + const visibleCode = isTruncated ? allLines.slice(0, maxLines).join('\n') : code + const effectiveShowLineNumbers = showLineNumbers && !!language && language !== 'text' const languageLabel = language || 'code' - - const visibleLines = visibleCode.split('\n') - - const lineCountLabel = `${lines.length} ${lines.length === 1 ? 'line' : 'lines'}` - - const showExpandToggle = lines.length > maxLines + const lineCountLabel = `${allLines.length} ${allLines.length === 1 ? 'line' : 'lines'}` + const showExpandToggle = allLines.length > maxLines return (
@@ -58,34 +35,45 @@ export function CodeViewer({ code, language, maxLines = 20, showLineNumbers = tr
-
- {visibleLines.map((line, index) => ( - effectiveShowLineNumbers ? ( -
- - {startLine + index} - - -
- ) : ( -
- -
- ) - ))} -
+ + {({ tokens, getLineProps, getTokenProps }) => ( +
+              {tokens.map((line, i) => {
+                const lineProps = getLineProps({ line })
+                return effectiveShowLineNumbers ? (
+                  
+ + {i + 1} + + + {line.map((token, key) => ( + + ))} + +
+ ) : ( +
+ + {line.map((token, key) => ( + + ))} + +
+ ) + })} +
+ )} +
{showExpandToggle && ( @@ -93,17 +81,9 @@ export function CodeViewer({ code, language, maxLines = 20, showLineNumbers = tr onClick={() => setExpanded((value) => !value)} className="w-full border-t border-[#d0d7de] bg-[#f6f8fa] py-1.5 text-[10px] font-semibold uppercase tracking-[0.14em] text-[#57606a] transition-colors hover:bg-[#eaeef2] hover:text-[#24292f]" > - {expanded ? 'Collapse' : `Show ${lines.length - maxLines} more lines`} + {expanded ? 'Collapse' : `Show ${allLines.length - maxLines} more lines`} )} ) } - -function escapeHtml(str: string): string { - return str - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') -}