github-actions[bot]
Deploy demo from GitHub Actions - 2025-12-24 02:23:20
6cdce85
'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<ExecutionResultData | null>(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 (
<div className="relative group my-3 code-block-wrapper">
{/* Action buttons */}
<div className="absolute right-2 top-2 flex items-center gap-1.5 opacity-0 group-hover:opacity-100 transition-opacity z-10">
<span className="text-xs text-zinc-500 bg-zinc-800 px-2 py-0.5 rounded">
{detectedLanguage || 'code'}
</span>
{/* Edit toggle */}
<button
onClick={isEditing ? handleCancelEdit : handleToggleEdit}
className={clsx(
'flex items-center gap-1 px-2 py-1 rounded text-xs font-medium transition-all',
isEditing
? 'bg-amber-600/20 text-amber-400 hover:bg-amber-600/30'
: 'bg-zinc-700/50 text-zinc-400 hover:bg-zinc-700'
)}
title={isEditing ? 'Cancel editing' : 'Edit code'}
>
{isEditing ? (
<>
<X className="w-3 h-3" />
<span>Cancel</span>
</>
) : (
<>
<Edit2 className="w-3 h-3" />
<span>Edit</span>
</>
)}
</button>
{canExecute && (
<button
onClick={isExecuting ? handleStopExecution : handleExecute}
disabled={isExecuting}
className={clsx(
'flex items-center gap-1 px-2 py-1 rounded text-xs font-medium transition-all',
isExecuting
? 'bg-red-600/20 text-red-400 hover:bg-red-600/30'
: 'bg-teal-600/20 text-teal-400 hover:bg-teal-600/30'
)}
title={isExecuting ? 'Stop execution' : 'Run code'}
>
{isExecuting ? (
<>
<Square className="w-3 h-3" />
<span>Stop</span>
</>
) : (
<>
<Play className="w-3 h-3" />
<span>Run</span>
</>
)}
</button>
)}
<button
onClick={handleCopy}
className="p-1.5 rounded bg-zinc-800 hover:bg-zinc-700 transition-colors"
title="Copy code"
>
{copied ? (
<Check className="w-3.5 h-3.5 text-emerald-400" />
) : (
<Copy className="w-3.5 h-3.5 text-zinc-400" />
)}
</button>
</div>
{isEditing ? (
// Monaco Editor for editing
<div
className="rounded-lg overflow-hidden border border-amber-600/30"
style={{ height: editorHeight }}
>
<Editor
height="100%"
language={detectedLanguage || 'python'}
value={editedCode}
onChange={(value) => 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,
}}
/>
</div>
) : (
// Static code display
<SyntaxHighlighter
style={customTheme}
language={detectedLanguage || 'python'}
PreTag="div"
customStyle={{
margin: 0,
borderRadius: '0.5rem',
background: '#18181b',
padding: '1rem',
fontSize: '0.875rem',
border: '1px solid #27272a',
lineHeight: '1.6',
}}
codeTagProps={{
style: {
background: 'none',
padding: 0,
},
}}
wrapLongLines={false}
>
{code}
</SyntaxHighlighter>
)}
{/* Execution result */}
{(isExecuting || executionResult) && (
<ExecutionResult
result={executionResult || { success: false, output: '', error: '', executionTime: 0 }}
isLoading={isExecuting}
/>
)}
</div>
);
}
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 (
<div className="w-8 h-8 rounded-md bg-zinc-800 flex items-center justify-center border border-zinc-700/50">
<span className="text-[10px] font-bold text-zinc-400 font-mono">YOU</span>
</div>
);
}
return (
<div className="w-8 h-8 rounded-md bg-zinc-800 flex items-center justify-center border border-teal-700/40">
<QubitIcon size={18} className="text-teal-400" />
</div>
);
}, [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 (
<div className={clsx('flex gap-3 animate-in', isUser ? 'flex-row-reverse' : 'flex-row')}>
<div className="flex-shrink-0">{avatar}</div>
<div className={clsx('flex-1 max-w-[85%]', isUser ? 'flex flex-col items-end' : '')}>
{imageSource && (
<div className="mb-2 max-w-xs">
<img
src={imageSource}
alt="Attached image"
className="rounded-lg border border-zinc-700/50 max-h-64 object-contain bg-zinc-900"
/>
</div>
)}
<div
className={clsx(
'rounded-xl px-4 py-3',
isUser
? 'bg-teal-700/80 text-white rounded-tr-sm'
: 'bg-zinc-800/90 border border-zinc-700/50 rounded-tl-sm'
)}
>
{isLoading ? (
loadingStatus || (
<div className="typing-indicator py-1">
<span />
<span />
<span />
</div>
)
) : (
<div className={clsx('markdown-content', isUser && 'text-white/90')}>
<ReactMarkdown
remarkPlugins={[remarkMath]}
rehypePlugins={[rehypeKatex]}
components={{
code({ className, children, ...props }) {
const match = /language-(\w+)/.exec(className || '');
const code = String(children).replace(/\n$/, '');
// Check if this is a math block (from remark-math)
if (className === 'language-math' || className === 'math-inline') {
try {
return <InlineMath math={code} />;
} catch {
return <code className="text-red-400">{code}</code>;
}
}
const isBlock = match || code.includes('\n') || looksLikeCode(code);
if (isBlock) {
return <CodeBlock language={match?.[1] || ''} code={code} onCopy={onCopyCode} />;
}
return (
<code className={clsx('bg-zinc-700/50 px-1.5 py-0.5 rounded text-sm', className)} {...props}>
{children}
</code>
);
},
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 <InlineMath math={math} />;
} catch {
return <span className="text-red-400">{children}</span>;
}
}
return <span className={className} {...props}>{children}</span>;
},
div({ className, children, ...props }) {
if (className === 'math math-display') {
try {
const math = String(children);
return <BlockMath math={math} />;
} catch {
return <div className="text-red-400">{children}</div>;
}
}
return <div className={className} {...props}>{children}</div>;
},
}}
>
{processedContent}
</ReactMarkdown>
</div>
)}
</div>
<span className="text-xs text-zinc-500 mt-1 px-2">
{message.timestamp.toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit',
})}
</span>
</div>
</div>
);
}