'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 (

AI Helper

{messages.length > 0 && ( )}
{messages.length === 0 ? (

Need help? Ask for hints or get the solution.

{problem && (
{[ { label: 'Give me a hint', isSolve: false }, { label: 'Explain the concept', isSolve: false }, { label: 'Solve it', isSolve: true }, ].map(({ label, isSolve }) => ( ))}
)}
) : ( messages.map((message) => { const processedContent = processContent(message.content); const codeBlocks = extractCodeBlocks(processedContent); const hasCode = codeBlocks.length > 0; return (
{message.role === 'assistant' && !message.content ? ( ) : ( handleCopyCode(code, message.id)} onApply={onApplyCode ? () => handleApplyCode(code) : undefined} copied={copiedId === message.id} /> ); } return ( {children} ); }, pre({ children }) { return <>{children}; }, p({ children }) { return

{children}

; }, ul({ children }) { return
    {children}
; }, ol({ children }) { return
    {children}
; }, li({ children }) { return
  • {children}
  • ; }, strong({ children }) { return {children}; }, }} > {processedContent}
    )}
    ); }) )}
    {problem && (