'use client'; import { useMemo, useState, useCallback } from 'react'; import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; 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 { MarkdownContent } from './MarkdownContent'; import { ExpandableImage } from '../ui/ImageLightbox'; 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'; } 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); return (
{avatar}
{imageSource && (
)}
{isLoading ? ( loadingStatus || (
) ) : ( ( )} /> )}
{message.timestamp.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', })}
); }