'use client';
import { useState, useRef, useEffect, useCallback } from 'react';
import { Sparkles, Send, Loader2, Trash2, ChevronLeft, Copy, Check, Play } from 'lucide-react';
import ReactMarkdown from 'react-markdown';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { clsx } from 'clsx';
import type { CodingProblem } from '@/types';
import { postProcessResponse } from '@/lib/utils/response';
import { LoadingStatus } from '../Chat/LoadingStatus';
interface AIHelperProps {
problem: CodingProblem | null;
userCode: string;
isCollapsed: boolean;
onToggleCollapse: () => void;
onApplyCode?: (code: string) => void;
}
interface HelperMessage {
id: string;
role: 'user' | 'assistant';
content: string;
timestamp: Date;
}
// Custom matte dark theme - matching Chat component
const customTheme: { [key: string]: React.CSSProperties } = {
'code[class*="language-"]': {
color: '#d4d4d8',
background: 'none',
fontFamily: "'JetBrains Mono', Consolas, Monaco, monospace",
fontSize: '0.8rem',
textAlign: 'left',
whiteSpace: 'pre',
wordSpacing: 'normal',
wordBreak: 'normal',
wordWrap: 'normal',
lineHeight: '1.5',
tabSize: 4,
},
'pre[class*="language-"]': {
color: '#d4d4d8',
background: '#18181b',
fontFamily: "'JetBrains Mono', Consolas, Monaco, monospace",
fontSize: '0.8rem',
textAlign: 'left',
whiteSpace: 'pre',
wordSpacing: 'normal',
wordBreak: 'normal',
wordWrap: 'normal',
lineHeight: '1.5',
tabSize: 4,
padding: '0.75rem',
margin: '0',
overflow: 'auto',
borderRadius: '0.375rem',
},
comment: { color: '#71717a' },
prolog: { color: '#71717a' },
doctype: { color: '#71717a' },
punctuation: { color: '#a1a1aa' },
property: { color: '#f0abfc' },
tag: { color: '#f0abfc' },
boolean: { color: '#c4b5fd' },
number: { color: '#c4b5fd' },
constant: { color: '#c4b5fd' },
symbol: { color: '#c4b5fd' },
selector: { color: '#86efac' },
string: { color: '#86efac' },
char: { color: '#86efac' },
builtin: { color: '#86efac' },
operator: { color: '#f0abfc' },
variable: { color: '#d4d4d8' },
function: { color: '#93c5fd' },
'class-name': { color: '#93c5fd' },
keyword: { color: '#c4b5fd' },
regex: { color: '#fcd34d' },
important: { color: '#fcd34d', fontWeight: 'bold' },
};
const HELPER_PROMPT = `You are a helpful coding assistant for quantum computing practice problems using Qiskit.
Your role is to:
1. Provide hints and guidance without giving away the complete solution
2. Explain quantum computing concepts when asked
3. Help debug code issues
4. Suggest improvements to the user's approach
Guidelines:
- Be encouraging and educational
- Give progressively more detailed hints if the user is stuck
- Focus on teaching, not just solving
- Reference Qiskit 2.0 best practices
- Keep responses concise and focused
Current problem context will be provided. Help the user learn while they solve the problem themselves.`;
function getSolvePrompt(problemType: 'function_completion' | 'code_generation') {
if (problemType === 'function_completion') {
return `You are a quantum computing expert using Qiskit.
Your task is to provide ONLY the code lines that complete the function body. Do NOT include the function signature/definition - just the implementation lines that go inside the function.
Guidelines:
- Provide ONLY the implementation code (the lines after the function definition)
- Do NOT repeat the function signature like "def function_name(...):"
- Include proper indentation for the function body
- Use Qiskit 2.0 best practices
- Add brief comments for complex steps
Example: If the function is:
\`\`\`python
def create_bell_state():
"""Create a Bell state circuit."""
pass
\`\`\`
You should respond with ONLY:
\`\`\`python
qc = QuantumCircuit(2)
qc.h(0)
qc.cx(0, 1)
return qc
\`\`\`
Format your response with ONLY the implementation code in a Python code block.`;
}
return `You are a quantum computing expert using Qiskit.
Your task is to provide a complete, working solution for the given problem.
Guidelines:
- Provide a complete, executable Python solution
- Include all necessary imports
- Use Qiskit 2.0 best practices
- Include brief comments explaining key steps
- Make sure the solution passes the provided tests
Format your response with the complete code in a Python code block.`;
}
function looksLikeCode(text: string): boolean {
const codeIndicators = [
/^from\s+/m,
/^import\s+/m,
/^def\s+/m,
/^class\s+/m,
/^\s*return\s+/m,
/QuantumCircuit/,
/Parameter\(/,
/\.\w+\([^)]*\)/m,
/^\s{4}/m, // Indented code
/qc\.\w+/,
/circuit\.\w+/,
];
return codeIndicators.some((p) => p.test(text));
}
interface CodeBlockProps {
code: string;
language: string;
onCopy: () => void;
onApply?: () => void;
copied: boolean;
}
function CodeBlock({ code, language, onCopy, onApply, copied }: CodeBlockProps) {
return (
{/* Action buttons - always visible for better discoverability */}
{language || 'python'}
{onApply && (
)}
{code}
);
}
export function AIHelper({
problem,
userCode,
isCollapsed,
onToggleCollapse,
onApplyCode,
}: AIHelperProps) {
const [messages, setMessages] = useState([]);
const [input, setInput] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [hasStartedStreaming, setHasStartedStreaming] = useState(false);
const [copiedId, setCopiedId] = useState(null);
const abortControllerRef = useRef(null);
const messagesContainerRef = useRef(null);
const scrollToBottom = useCallback(() => {
// Scroll only within the messages container, not the whole page
if (messagesContainerRef.current) {
messagesContainerRef.current.scrollTop = messagesContainerRef.current.scrollHeight;
}
}, []);
useEffect(() => {
// Only scroll within the AI Helper panel, not the whole page
requestAnimationFrame(() => {
scrollToBottom();
});
}, [messages, scrollToBottom]);
useEffect(() => {
setMessages([]);
}, [problem?.id]);
// Fetch image as base64 for multimodal problems
const fetchImageBase64 = async (imageUrl: string): Promise => {
try {
const response = await fetch(imageUrl);
const blob = await response.blob();
return new Promise((resolve) => {
const reader = new FileReader();
reader.onloadend = () => {
const base64 = reader.result as string;
const base64Data = base64.split(',')[1] || base64;
resolve(base64Data);
};
reader.onerror = () => resolve(null);
reader.readAsDataURL(blob);
});
} catch {
return null;
}
};
const handleSendMessage = async (customMessage?: string, isSolveRequest = false) => {
const messageText = customMessage || input.trim();
if (!messageText || isLoading || !problem) return;
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
abortControllerRef.current = new AbortController();
const userMessage: HelperMessage = {
id: crypto.randomUUID(),
role: 'user',
content: messageText,
timestamp: new Date(),
};
const assistantId = crypto.randomUUID();
const loadingMessage: HelperMessage = {
id: assistantId,
role: 'assistant',
content: '',
timestamp: new Date(),
};
setMessages((prev) => [...prev, userMessage, loadingMessage]);
setInput('');
setIsLoading(true);
setHasStartedStreaming(false);
try {
// Build context message with problem info
let contextMessage = `Problem: ${problem.question}`;
if (!isSolveRequest && userCode) {
contextMessage += `\n\nUser's current code:\n\`\`\`python\n${userCode || '# No code written yet'}\n\`\`\``;
}
contextMessage += `\n\nUser's request: ${messageText}`;
// Select appropriate system prompt
const systemPrompt = isSolveRequest
? getSolvePrompt(problem.type as 'function_completion' | 'code_generation')
: HELPER_PROMPT;
// Build messages array
const apiMessages: Array<{
role: 'system' | 'user' | 'assistant';
content: string | Array<{ type: string; text?: string; image_url?: { url: string } }>;
}> = [
{ role: 'system', content: systemPrompt },
...messages.map((m) => ({
role: m.role as 'user' | 'assistant',
content: m.content,
})),
];
// Handle multimodal problems - include image if available
if (problem.imageUrl && problem.hasImage) {
const imageBase64 = await fetchImageBase64(problem.imageUrl);
if (imageBase64) {
apiMessages.push({
role: 'user',
content: [
{ type: 'text', text: contextMessage },
{
type: 'image_url',
image_url: { url: `data:image/jpeg;base64,${imageBase64}` },
},
],
});
} else {
apiMessages.push({ role: 'user', content: contextMessage });
}
} else {
apiMessages.push({ role: 'user', content: contextMessage });
}
const response = await fetch('/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
messages: apiMessages,
stream: true,
}),
signal: abortControllerRef.current.signal,
});
if (!response.ok) {
const data = await response.json();
throw new Error(data.error || 'Request failed');
}
const reader = response.body?.getReader();
if (!reader) throw new Error('No response body');
const decoder = new TextDecoder();
let buffer = '';
let fullContent = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed || !trimmed.startsWith('data: ')) continue;
const jsonStr = trimmed.slice(6);
try {
const data = JSON.parse(jsonStr);
if (data.content) {
// First content received - streaming has started
if (fullContent === '') {
setHasStartedStreaming(true);
}
fullContent += data.content;
// Use postProcessResponse like ChatInterface does for proper formatting
const processedContent = postProcessResponse(fullContent);
setMessages((prev) =>
prev.map((m) =>
m.id === assistantId ? { ...m, content: processedContent } : m
)
);
}
} catch {
continue;
}
}
}
// Apply final post-processing
const finalContent = postProcessResponse(fullContent);
setMessages((prev) =>
prev.map((m) =>
m.id === assistantId ? { ...m, content: finalContent } : m
)
);
} catch (error) {
if ((error as Error).name === 'AbortError') return;
setMessages((prev) =>
prev.map((m) =>
m.id === assistantId
? { ...m, content: `Error: ${error instanceof Error ? error.message : 'Failed'}` }
: m
)
);
} finally {
setIsLoading(false);
setHasStartedStreaming(false);
abortControllerRef.current = null;
}
};
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleSendMessage();
}
};
const clearMessages = () => {
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
setMessages([]);
};
// Extract code blocks from message content, preserving indentation
const extractCodeBlocks = (content: string): string[] => {
const codeBlockRegex = /```(?:python)?\n?([\s\S]*?)```/g;
const blocks: string[] = [];
let match;
while ((match = codeBlockRegex.exec(content)) !== null) {
// Preserve indentation - only trim trailing newlines, not leading whitespace
const code = match[1].replace(/\n+$/, '');
blocks.push(code);
}
return blocks;
};
const handleCopyCode = (code: string, messageId: string) => {
navigator.clipboard.writeText(code);
setCopiedId(messageId);
setTimeout(() => setCopiedId(null), 2000);
};
const handleApplyCode = (code: string) => {
if (onApplyCode) {
onApplyCode(code);
}
};
// Process content to add code blocks where needed
const processContent = useCallback((content: string): string => {
// If already has code blocks, return as-is
if (content.includes('```')) {
return content;
}
// If it looks like code, wrap it
if (looksLikeCode(content)) {
return '```python\n' + content + '\n```';
}
return content;
}, []);
// Collapsed view
if (isCollapsed) {
return (
);
}
// Expanded view
return (