'use client'; import { useMemo, useState, useCallback } from 'react'; import ReactMarkdown from 'react-markdown'; import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; import remarkMath from 'remark-math'; import rehypeKatex from 'rehype-katex'; import { InlineMath, BlockMath } from 'react-katex'; import { Copy, Check, Play, Square, Edit2, X } from 'lucide-react'; import Editor from '@monaco-editor/react'; import { clsx } from 'clsx'; import { QubitIcon } from './QubitIcon'; import { ExecutionResult, ExecutionResultData } from './ExecutionResult'; import type { Message as MessageType } from '@/types'; interface MessageProps { message: MessageType; onCopyCode?: (code: string) => void; loadingStatus?: React.ReactNode; } // Custom matte dark theme - muted, professional colors const customTheme: { [key: string]: React.CSSProperties } = { 'code[class*="language-"]': { color: '#d4d4d8', background: 'none', fontFamily: "'JetBrains Mono', Consolas, Monaco, 'Andale Mono', monospace", fontSize: '0.875rem', textAlign: 'left', whiteSpace: 'pre', wordSpacing: 'normal', wordBreak: 'normal', wordWrap: 'normal', lineHeight: '1.6', tabSize: 4, hyphens: 'none', }, 'pre[class*="language-"]': { color: '#d4d4d8', background: '#18181b', fontFamily: "'JetBrains Mono', Consolas, Monaco, 'Andale Mono', monospace", fontSize: '0.875rem', textAlign: 'left', whiteSpace: 'pre', wordSpacing: 'normal', wordBreak: 'normal', wordWrap: 'normal', lineHeight: '1.6', tabSize: 4, hyphens: 'none', padding: '1rem', margin: '0', overflow: 'auto', borderRadius: '0.5rem', }, comment: { color: '#71717a' }, prolog: { color: '#71717a' }, doctype: { color: '#71717a' }, cdata: { color: '#71717a' }, punctuation: { color: '#a1a1aa' }, namespace: { opacity: 0.7 }, property: { color: '#f0abfc' }, tag: { color: '#f0abfc' }, boolean: { color: '#c4b5fd' }, number: { color: '#c4b5fd' }, constant: { color: '#c4b5fd' }, symbol: { color: '#c4b5fd' }, deleted: { color: '#fca5a5' }, selector: { color: '#86efac' }, 'attr-name': { color: '#fcd34d' }, string: { color: '#86efac' }, char: { color: '#86efac' }, builtin: { color: '#86efac' }, inserted: { color: '#86efac' }, operator: { color: '#f0abfc' }, entity: { color: '#fcd34d', cursor: 'help' }, url: { color: '#67e8f9' }, '.language-css .token.string': { color: '#67e8f9' }, '.style .token.string': { color: '#67e8f9' }, variable: { color: '#d4d4d8' }, atrule: { color: '#93c5fd' }, 'attr-value': { color: '#86efac' }, function: { color: '#93c5fd' }, 'class-name': { color: '#93c5fd' }, keyword: { color: '#c4b5fd' }, regex: { color: '#fcd34d' }, important: { color: '#fcd34d', fontWeight: 'bold' }, bold: { fontWeight: 'bold' }, italic: { fontStyle: 'italic' }, }; function isPythonCode(language: string, code: string): boolean { if (language === 'python') return true; const pythonPatterns = [ /^from\s+\w+\s+import/m, /^import\s+\w+/m, /^def\s+\w+\s*\(/m, /^class\s+\w+/m, /QuantumCircuit/, /qiskit/i, ]; return pythonPatterns.some(p => p.test(code)); } function CodeBlock({ language, code: initialCode, onCopy, }: { language: string; code: string; onCopy?: (code: string) => void; }) { const [copied, setCopied] = useState(false); const [isExecuting, setIsExecuting] = useState(false); const [isEditing, setIsEditing] = useState(false); const [editedCode, setEditedCode] = useState(initialCode); const [executionResult, setExecutionResult] = useState(null); // The code to use (edited or original) const code = isEditing ? editedCode : initialCode; const handleCopy = async () => { await navigator.clipboard.writeText(code); setCopied(true); onCopy?.(code); setTimeout(() => setCopied(false), 2000); }; const handleExecute = useCallback(async () => { if (isExecuting) return; setIsExecuting(true); setExecutionResult(null); try { const response = await fetch('/api/execute', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ code, timeout: 30 }), }); const result = await response.json(); setExecutionResult(result); } catch (error) { setExecutionResult({ success: false, output: '', error: error instanceof Error ? error.message : 'Execution failed', executionTime: 0, hasCircuitOutput: false, }); } finally { setIsExecuting(false); } }, [code, isExecuting]); const handleStopExecution = () => { setIsExecuting(false); }; const handleToggleEdit = () => { if (isEditing) { // Exiting edit mode - keep the edited code setIsEditing(false); } else { // Entering edit mode setEditedCode(initialCode); setIsEditing(true); } }; const handleCancelEdit = () => { setEditedCode(initialCode); setIsEditing(false); }; const detectedLanguage = language || detectLanguage(code); const canExecute = isPythonCode(detectedLanguage, code); // Calculate editor height based on line count const lineCount = code.split('\n').length; const editorHeight = Math.min(Math.max(lineCount * 20 + 32, 100), 400); return (
{/* Action buttons */}
{detectedLanguage || 'code'} {/* Edit toggle */} {canExecute && ( )}
{isEditing ? ( // Monaco Editor for editing
setEditedCode(value || '')} theme="vs-dark" options={{ fontSize: 14, fontFamily: "'JetBrains Mono', Consolas, Monaco, monospace", minimap: { enabled: false }, scrollBeyondLastLine: false, lineNumbers: 'on', glyphMargin: false, folding: false, lineDecorationsWidth: 8, lineNumbersMinChars: 3, padding: { top: 12, bottom: 12 }, renderLineHighlight: 'line', tabSize: 4, insertSpaces: true, wordWrap: 'on', automaticLayout: true, }} />
) : ( // Static code display {code} )} {/* Execution result */} {(isExecuting || executionResult) && ( )}
); } function detectLanguage(code: string): string { const pythonPatterns = [ /^from\s+\w+\s+import/m, /^import\s+\w+/m, /^def\s+\w+\s*\(/m, /^class\s+\w+/m, /QuantumCircuit/, /qiskit/i, /\.measure/, /numpy|np\./, /print\s*\(/, ]; if (pythonPatterns.some((p) => p.test(code))) { return 'python'; } const jsPatterns = [ /^const\s+\w+\s*=/m, /^let\s+\w+\s*=/m, /^function\s+\w+/m, /=>\s*{/, /console\.log/, ]; if (jsPatterns.some((p) => p.test(code))) { return 'javascript'; } const bashPatterns = [/^\$\s+/m, /^#!\/bin\/(ba)?sh/m, /\|\s*grep/, /apt-get|pip\s+install/]; if (bashPatterns.some((p) => p.test(code))) { return 'bash'; } return 'python'; } function looksLikeCode(text: string): boolean { // Multi-line code indicators if (text.includes('\n')) { const codeIndicators = [ /^from\s+/m, /^import\s+/m, /^def\s+/m, /^class\s+/m, /^\s*return\s+/m, /QuantumCircuit/, /Parameter\(/, /\.\w+\([^)]*\)/m, // Method calls like qc.h(), qc.cx() ]; return codeIndicators.some((p) => p.test(text)); } // Single-line code indicators for function completion responses const singleLinePatterns = [ /^return\s+\w+/, // return circuit.control(...) /^\w+\s*=\s*\w+\([^)]*\)/, // theta = Parameter("theta") /^\w+\.\w+\([^)]*\)$/, // circuit.control(num_ctrl_qubits) /\w+\s*=\s*\w+\([^)]*\)(?:\s+\w+\.|\s+\w+\s*=)/, // Multiple statements /QuantumCircuit\(/, /Parameter\(/, /\.control\(/, /\.measure\(/, ]; return singleLinePatterns.some((p) => p.test(text.trim())); } export function Message({ message, onCopyCode, loadingStatus }: MessageProps) { const isUser = message.role === 'user'; const isLoading = message.isLoading; const avatar = useMemo(() => { if (isUser) { return (
YOU
); } return (
); }, [isUser]); const imageSource = message.imageUrl || (message.imageBase64 ? `data:image/jpeg;base64,${message.imageBase64}` : null); const processedContent = useMemo(() => { let content = message.content; // Convert non-standard math delimiters to standard LaTeX format // Display math: [ ... ] containing LaTeX → $$ ... $$ content = content.replace( /\[\s*(\\[a-zA-Z][^\]]*)\s*\]/g, (match, inner) => `\n$$\n${inner.trim()}\n$$\n` ); // Inline math with \(...\) → $...$ content = content.replace( /\\\(([^)]+)\\\)/g, (match, inner) => `$${inner}$` ); // Inline math: (expression) containing LaTeX → $...$ // Match parentheses containing backslash commands but not nested parens content = content.replace( /\(([^()]*(?:\\[a-zA-Z{}^_]|\\frac|\\sqrt|\\sum|\\exp|\\left|\\right|\\bigl|\\bigr|\\Bigl|\\Bigr|\|[01]\\rangle)[^()]*)\)/g, (match, inner) => { // Only convert if it really looks like math if (/\\[a-zA-Z]/.test(inner) || /\|[01n]\\rangle/.test(inner)) { return `$${inner}$`; } return match; } ); // Code detection for non-markdown responses if (!content.includes('```') && !content.includes('$$') && !content.includes('$') && looksLikeCode(content)) { content = content .replace(/(\w+\s*=\s*\w+\([^)]*\))\s+(\w+\.)/g, '$1\n$2') .replace(/(\w+\.[a-z_]+\([^)]*\))\s+(\w+\.)/g, '$1\n$2'); content = '```python\n' + content + '\n```'; } return content; }, [message.content]); return (
{avatar}
{imageSource && (
Attached image
)}
{isLoading ? ( loadingStatus || (
) ) : (
; } catch { return {code}; } } const isBlock = match || code.includes('\n') || looksLikeCode(code); if (isBlock) { return ; } return ( {children} ); }, pre({ children }) { return <>{children}; }, // Handle math blocks from remark-math span({ className, children, ...props }) { if (className === 'math math-inline') { try { const math = String(children); return ; } catch { return {children}; } } return {children}; }, div({ className, children, ...props }) { if (className === 'math math-display') { try { const math = String(children); return ; } catch { return
{children}
; } } return
{children}
; }, }} > {processedContent}
)}
{message.timestamp.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', })}
); }