Spaces:
Running
Running
| import { useEffect, useState } from 'react'; | |
| import { supabase } from '../supabaseClient'; | |
| import { Sparkles, Check, AlertTriangle, Code, Award, ExternalLink, RefreshCw, ChevronDown } from 'lucide-react'; | |
| import CodeMirror from '@uiw/react-codemirror'; | |
| import { python } from '@codemirror/lang-python'; | |
| interface ReviewQueueProps { | |
| overrideProblemId: string | null; | |
| onClearOverride: () => void; | |
| } | |
| export default function ReviewQueue({ overrideProblemId, onClearOverride }: ReviewQueueProps) { | |
| const [reviews, setReviews] = useState<any[]>([]); | |
| const [loading, setLoading] = useState(true); | |
| const [submitting, setSubmitting] = useState(false); | |
| const [code, setCode] = useState(''); | |
| const [activeTab, setActiveTab] = useState<'ai' | 'offline'>('ai'); | |
| // AI evaluation states | |
| const [aiLoading, setAiLoading] = useState(false); | |
| const [aiResult, setAiResult] = useState<any>(null); | |
| const [aiError, setAiError] = useState<string | null>(null); | |
| // Reference code collapse state | |
| const [showRefCode, setShowRefCode] = useState(false); | |
| // Code explanation states | |
| const [explainLoading, setExplainLoading] = useState(false); | |
| const [explanationText, setExplanationText] = useState<string | null>(null); | |
| const [explainError, setExplainError] = useState<string | null>(null); | |
| // Autocomplete state (persistent) | |
| const [isAutocompleteEnabled, setIsAutocompleteEnabled] = useState<boolean>(() => { | |
| const saved = localStorage.getItem('isAutocompleteEnabled'); | |
| return saved !== 'false'; | |
| }); | |
| const fetchQueue = async () => { | |
| setLoading(true); | |
| setAiResult(null); | |
| setAiError(null); | |
| setCode(''); | |
| setShowRefCode(false); | |
| setExplanationText(null); | |
| setExplainError(null); | |
| try { | |
| const { data: { session } } = await supabase.auth.getSession(); | |
| if (!session) return; | |
| const headers = { 'Authorization': `Bearer ${session.access_token}` }; | |
| let url = `${import.meta.env.VITE_API_BASE_URL}/api/reviews/due`; | |
| if (overrideProblemId) { | |
| url = `${import.meta.env.VITE_API_BASE_URL}/api/reviews/problems/id/${overrideProblemId}`; | |
| } | |
| const res = await fetch(url, { headers }); | |
| if (res.ok) { | |
| const data = await res.json(); | |
| setReviews(data); | |
| if (data && data.length > 0) { | |
| const activeReview = data[0]; | |
| const problem = activeReview.problems; | |
| if (problem && problem.starter_code) { | |
| setCode(problem.starter_code); | |
| } | |
| } | |
| } | |
| } catch (err) { | |
| console.error('Error fetching review queue:', err); | |
| } finally { | |
| setLoading(false); | |
| } | |
| }; | |
| useEffect(() => { | |
| fetchQueue(); | |
| }, [overrideProblemId]); | |
| const handleGrade = async (rating: 'Correct' | 'Mixed' | 'Incorrect') => { | |
| if (reviews.length === 0) return; | |
| const activeReview = reviews[0]; | |
| setSubmitting(true); | |
| try { | |
| const { data: { session } } = await supabase.auth.getSession(); | |
| if (!session) return; | |
| const res = await fetch(`${import.meta.env.VITE_API_BASE_URL}/api/reviews/complete`, { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| 'Authorization': `Bearer ${session.access_token}` | |
| }, | |
| body: JSON.stringify({ | |
| review_id: activeReview.id, | |
| box_level: activeReview.box_level, | |
| times_correct: activeReview.times_correct, | |
| total_attempts: activeReview.total_attempts, | |
| rating | |
| }) | |
| }); | |
| if (res.ok) { | |
| if (overrideProblemId) { | |
| // If we completed the custom override card, exit override mode | |
| onClearOverride(); | |
| } else { | |
| // Refresh the daily queue | |
| fetchQueue(); | |
| } | |
| } | |
| } catch (err) { | |
| console.error('Error logging review completion:', err); | |
| } finally { | |
| setSubmitting(false); | |
| } | |
| }; | |
| const handleAiEvaluation = async () => { | |
| if (reviews.length === 0 || !code.trim()) return; | |
| const activeReview = reviews[0]; | |
| const problem = activeReview.problems; | |
| setAiLoading(true); | |
| setAiResult(null); | |
| setAiError(null); | |
| try { | |
| const { data: { session } } = await supabase.auth.getSession(); | |
| if (!session) return; | |
| const res = await fetch(`${import.meta.env.VITE_API_BASE_URL}/api/reviews/evaluate`, { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| 'Authorization': `Bearer ${session.access_token}` | |
| }, | |
| body: JSON.stringify({ | |
| problem_name: problem.name, | |
| pattern: problem.pattern, | |
| optimal_time: problem.optimal_time, | |
| optimal_space: problem.optimal_space, | |
| code: code | |
| }) | |
| }); | |
| if (res.ok) { | |
| const result = await res.json(); | |
| setAiResult(result); | |
| } else { | |
| const errorData = await res.json(); | |
| setAiError(errorData.detail || 'Failed to analyze code.'); | |
| } | |
| } catch (err) { | |
| setAiError('Connection to evaluation server failed.'); | |
| } finally { | |
| setAiLoading(false); | |
| } | |
| }; | |
| const handleExplainCode = async () => { | |
| if (reviews.length === 0) return; | |
| const activeReview = reviews[0]; | |
| const problem = activeReview.problems; | |
| if (!problem || !problem.problem_number) return; | |
| setExplainLoading(true); | |
| setExplanationText(null); | |
| setExplainError(null); | |
| try { | |
| const { data: { session } } = await supabase.auth.getSession(); | |
| if (!session) return; | |
| const res = await fetch(`${import.meta.env.VITE_API_BASE_URL}/api/reviews/explain`, { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| 'Authorization': `Bearer ${session.access_token}` | |
| }, | |
| body: JSON.stringify({ | |
| problem_number: problem.problem_number, | |
| method: problem.pattern | |
| }) | |
| }); | |
| if (res.ok) { | |
| const result = await res.json(); | |
| setExplanationText(result.explanation); | |
| } else { | |
| const errorData = await res.json(); | |
| setExplainError(errorData.detail || 'Failed to generate code explanation.'); | |
| } | |
| } catch (err) { | |
| setExplainError('Connection to explanation server failed.'); | |
| } finally { | |
| setExplainLoading(false); | |
| } | |
| }; | |
| if (loading) { | |
| return ( | |
| <div className="w-full h-full flex items-center justify-center bg-[#fdf8e1] text-ink-black"> | |
| <div className="flex flex-col items-center gap-3"> | |
| <RefreshCw className="w-8 h-8 text-primary animate-spin" /> | |
| <span className="font-arcade text-[10px] tracking-wide">LOADING SYSTEM CARD...</span> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| if (reviews.length === 0) { | |
| return ( | |
| <div className="w-full h-full flex items-center justify-center bg-[#fdf8e1] px-6"> | |
| <div className="w-full max-w-md text-center space-y-6 py-12 px-8 bg-paper-white border-[3px] border-ink-black gadget-shadow relative overflow-hidden"> | |
| <div className="absolute top-0 left-0 right-0 h-[8px] bg-grass-green" /> | |
| <div className="w-16 h-16 bg-emerald-100 border-[3px] border-ink-black flex items-center justify-center mx-auto text-emerald-600 gadget-shadow"> | |
| <Check className="w-8 h-8 stroke-[3px]" /> | |
| </div> | |
| <div> | |
| <h2 className="text-xl font-bold text-ink-black font-headline-md">Queue Clear!</h2> | |
| <p className="text-xs font-sans text-trash-gray mt-2 leading-relaxed font-semibold"> | |
| Fantastic work. All active Leitner cards are up to date. Come back tomorrow for new spaced-repetition reviews! | |
| </p> | |
| </div> | |
| {overrideProblemId && ( | |
| <button | |
| onClick={onClearOverride} | |
| className="bg-white border-[3px] border-ink-black text-ink-black font-bold px-4 py-2 text-xs uppercase tracking-wide hover:bg-slate-100 active:translate-y-0.5 shadow-[2px_2px_0px_0px_rgba(30,41,59,1)] active:shadow-none transition-all cursor-pointer inline-block" | |
| > | |
| Return to Daily Queue | |
| </button> | |
| )} | |
| </div> | |
| </div> | |
| ); | |
| } | |
| const activeReview = reviews[0]; | |
| const problem = activeReview.problems; | |
| return ( | |
| <div className="w-full h-full bg-[#fdf8e1] overflow-y-auto custom-scrollbar p-6 flex flex-col"> | |
| <div className="flex-1 flex flex-col"> | |
| {/* Custom Override Header Banner */} | |
| {overrideProblemId && ( | |
| <div className="p-3 mb-5 border-[3px] border-ink-black bg-yellow-100 flex flex-col sm:flex-row gap-3 justify-between items-center text-xs font-arcade text-ink-black shadow-[3px_3px_0px_0px_rgba(30,41,59,1)]"> | |
| <span className="flex items-center gap-2 font-bold"> | |
| <AlertTriangle className="w-4 h-4 text-amber-600 shrink-0" /> | |
| OVERRIDE: SELECTED MAP PROBLEM | |
| </span> | |
| <button | |
| onClick={onClearOverride} | |
| className="bg-white hover:bg-slate-100 text-ink-black font-bold px-3 py-1.5 border-[3px] border-ink-black shadow-[2px_2px_0px_0px_rgba(30,41,59,1)] active:translate-y-0.5 active:shadow-none transition-all text-[8px] uppercase tracking-wide cursor-pointer shrink-0" | |
| > | |
| Exit Override Mode | |
| </button> | |
| </div> | |
| )} | |
| {/* Problem Stats Header Card */} | |
| <div className="mb-6 bg-paper-white border-[3px] border-ink-black p-5 relative overflow-hidden gadget-shadow shadow-[inset_2px_2px_0px_rgba(255,255,255,0.8)]"> | |
| <div className="absolute top-0 left-0 right-0 h-[5px] bg-gradient-to-r from-primary via-[#ffcc00] to-highlight-pink" /> | |
| <div className="flex justify-between items-start gap-4"> | |
| <div> | |
| <div className="flex flex-wrap items-center gap-2 font-arcade text-[8px] tracking-wide text-trash-gray font-bold"> | |
| <span className="px-1.5 py-0.5 bg-white border-2 border-ink-black">BOX {activeReview.box_level}</span> | |
| <span>•</span> | |
| <span>CORRECT: {activeReview.times_correct}/{activeReview.total_attempts}</span> | |
| <span>•</span> | |
| <span>PATTERN: {problem.pattern}</span> | |
| </div> | |
| <h2 className="text-xl md:text-2xl font-extrabold text-ink-black tracking-tight mt-2 font-headline-md leading-tight">{problem.name}</h2> | |
| </div> | |
| <span className={`text-[10px] font-arcade font-bold px-3 py-1 border-[3px] border-ink-black shadow-[3px_3px_0px_0px_rgba(30,41,59,1)] shrink-0 ${ | |
| problem.difficulty === 'Easy' ? 'bg-green-500 text-white' : | |
| problem.difficulty === 'Medium' ? 'bg-orange-500 text-white' : | |
| 'bg-red-500 text-white' | |
| }`}> | |
| {problem.difficulty.toUpperCase()} | |
| </span> | |
| </div> | |
| {problem.description && ( | |
| <div className="mt-4 p-4 bg-slate-50 border-[3px] border-ink-black font-sans text-xs text-slate-800 leading-relaxed max-h-48 overflow-y-auto custom-scrollbar"> | |
| <div className="text-[9px] font-arcade text-slate-400 uppercase mb-2 font-bold">Problem Description</div> | |
| <div className="font-semibold space-y-1"> | |
| {problem.description.split('\n').map((line: string, i: number) => ( | |
| <p key={i} className="m-0 leading-relaxed whitespace-pre-wrap">{line}</p> | |
| ))} | |
| </div> | |
| </div> | |
| )} | |
| <div className="grid grid-cols-2 sm:grid-cols-4 gap-4 mt-5 border-t-[3px] border-dashed border-ink-black pt-4 text-xs font-bold text-ink-black"> | |
| <div> | |
| <span className="text-[8px] font-arcade text-trash-gray block uppercase tracking-wider">Optimal Time</span> | |
| <code className="bg-[#1E293B] text-[#4ADE80] font-code-mono px-2 py-0.5 border border-ink-black mt-1 inline-block text-xs">{problem.optimal_time}</code> | |
| </div> | |
| <div> | |
| <span className="text-[8px] font-arcade text-trash-gray block uppercase tracking-wider">Optimal Space</span> | |
| <code className="bg-[#1E293B] text-[#4ADE80] font-code-mono px-2 py-0.5 border border-ink-black mt-1 inline-block text-xs">{problem.optimal_space}</code> | |
| </div> | |
| <div> | |
| <span className="text-[8px] font-arcade text-trash-gray block uppercase tracking-wider">Doc Link</span> | |
| <a | |
| href={problem.google_doc_url} | |
| target="_blank" | |
| rel="noreferrer" | |
| className="flex items-center gap-1.5 text-primary hover:underline mt-1 inline-flex font-headline-md font-bold" | |
| > | |
| Google Doc | |
| <ExternalLink className="w-3.5 h-3.5" /> | |
| </a> | |
| </div> | |
| <div> | |
| <span className="text-[8px] font-arcade text-trash-gray block uppercase tracking-wider">Next Due</span> | |
| <span className="text-xs text-ink-black block mt-2 font-code-mono"> | |
| {new Date(activeReview.next_review).toLocaleDateString()} | |
| </span> | |
| </div> | |
| </div> | |
| </div> | |
| {/* Reference Code Expander (Chunky details element from code.html style) */} | |
| <details className="mb-6 group border-[3px] border-ink-black bg-white shadow-[4px_4px_0px_0px_rgba(30,41,59,1)]"> | |
| <summary | |
| onClick={() => setShowRefCode(!showRefCode)} | |
| className="font-arcade text-[10px] p-3 cursor-pointer flex justify-between items-center outline-none hover:bg-secondary-container transition-colors select-none font-bold" | |
| > | |
| <span className="flex items-center gap-2"> | |
| <Code className="w-4 h-4 text-primary" /> | |
| {showRefCode ? 'HIDE REFERENCE CODE' : 'SHOW REFERENCE CODE'} | |
| </span> | |
| <ChevronDown className="w-4 h-4 group-open:rotate-180 transition-transform" /> | |
| </summary> | |
| <div className="p-4 border-t-[3px] border-ink-black bg-[#1E293B] text-paper-white font-code-mono text-xs shadow-[inset_4px_4px_0px_rgba(0,0,0,0.5)] overflow-x-auto"> | |
| <pre className="font-mono text-xs leading-relaxed select-all text-[#4ADE80]"> | |
| <code>{problem.reference_code || '# No reference code found for this problem'}</code> | |
| </pre> | |
| {problem.reference_code && ( | |
| <div className="pt-4 mt-4 border-t border-slate-700 flex flex-col gap-4"> | |
| <button | |
| onClick={handleExplainCode} | |
| disabled={explainLoading} | |
| className="w-fit bg-white hover:bg-slate-100 border-[3px] border-ink-black text-ink-black font-bold px-4 py-2 text-xs flex items-center gap-2 transition-all cursor-pointer disabled:opacity-50 shadow-[2px_2px_0px_0px_rgba(30,41,59,1)] active:translate-y-0.5 active:shadow-none" | |
| > | |
| {explainLoading ? ( | |
| <> | |
| <RefreshCw className="w-3.5 h-3.5 animate-spin text-primary" /> | |
| Generating Explanation... | |
| </> | |
| ) : ( | |
| <> | |
| <span>💡 Explain Reference Code</span> | |
| </> | |
| )} | |
| </button> | |
| {/* Explain Error */} | |
| {explainError && ( | |
| <div className="p-3 border-2 border-red-500 bg-red-100 text-error text-xs font-semibold"> | |
| {explainError} | |
| </div> | |
| )} | |
| {/* Explanation display */} | |
| {explanationText && ( | |
| <div className="bg-paper-white border-[3px] border-ink-black p-4 text-ink-black max-h-80 overflow-y-auto space-y-3 custom-scrollbar shadow-[inset_2px_2px_0px_rgba(0,0,0,0.05)]"> | |
| <h4 className="text-[10px] font-arcade font-bold text-primary mb-1 flex items-center gap-1.5 uppercase"> | |
| <Sparkles className="w-3.5 h-3.5" /> | |
| Code Breakdown | |
| </h4> | |
| <div className="border-t-2 border-dashed border-ink-black pt-2 font-sans font-semibold"> | |
| {parseMarkdown(explanationText)} | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| )} | |
| </div> | |
| </details> | |
| {/* Interactive Tabs */} | |
| <div className="mb-6 flex-1 flex flex-col"> | |
| <div className="flex gap-2 mb-4 font-arcade text-[10px]"> | |
| <button | |
| onClick={() => setActiveTab('ai')} | |
| className={`px-4 py-2 border-[3px] border-ink-black cursor-pointer outline-none transition-all shadow-[4px_4px_0px_0px_rgba(30,41,59,1)] hover:bg-slate-100 ${ | |
| activeTab === 'ai' | |
| ? 'bg-[#c0e8ff] translate-y-1 shadow-[2px_2px_0px_0px_rgba(30,41,59,1)] font-bold' | |
| : 'bg-white text-trash-gray' | |
| }`} | |
| > | |
| AI GEMINI EVALUATION | |
| </button> | |
| <button | |
| onClick={() => setActiveTab('offline')} | |
| className={`px-4 py-2 border-[3px] border-ink-black cursor-pointer outline-none transition-all shadow-[4px_4px_0px_0px_rgba(30,41,59,1)] hover:bg-slate-100 ${ | |
| activeTab === 'offline' | |
| ? 'bg-[#c0e8ff] translate-y-1 shadow-[2px_2px_0px_0px_rgba(30,41,59,1)] font-bold' | |
| : 'bg-white text-trash-gray' | |
| }`} | |
| > | |
| OFFLINE SELF-GRADE | |
| </button> | |
| </div> | |
| {/* Tab Content Box */} | |
| <div className="border-[3px] border-ink-black bg-white p-5 shadow-[4px_4px_0px_0px_rgba(30,41,59,1)] flex-1 flex flex-col"> | |
| {activeTab === 'ai' ? ( | |
| <div className="flex-1 flex flex-col space-y-4"> | |
| <div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-2"> | |
| <label className="block text-[9px] font-arcade font-bold text-trash-gray"> | |
| PASTE YOUR SOLUTION CODE FROM MEMORY: | |
| </label> | |
| <label className="inline-flex items-center gap-2 cursor-pointer select-none"> | |
| <input | |
| type="checkbox" | |
| checked={isAutocompleteEnabled} | |
| onChange={(e) => { | |
| setIsAutocompleteEnabled(e.target.checked); | |
| localStorage.setItem('isAutocompleteEnabled', String(e.target.checked)); | |
| }} | |
| className="sr-only peer" | |
| /> | |
| <div className="relative w-8 h-4 bg-slate-300 border border-ink-black rounded-full peer peer-checked:bg-grass-green transition-all after:content-[''] after:absolute after:top-[1px] after:left-[2px] after:bg-white after:border after:border-ink-black after:rounded-full after:h-2.5 after:w-2.5 after:transition-all peer-checked:after:translate-x-3.5"></div> | |
| <span className="text-[8px] font-arcade font-bold text-trash-gray peer-checked:text-ink-black transition-colors"> | |
| 🤖 AUTOCOMPLETE | |
| </span> | |
| </label> | |
| </div> | |
| <div className="flex-1 min-h-[220px] flex flex-col"> | |
| <CodeMirror | |
| value={code} | |
| onChange={(value) => setCode(value)} | |
| placeholder="def solve(nums): # Write code here..." | |
| height="100%" | |
| theme="dark" | |
| extensions={[python()]} | |
| basicSetup={{ | |
| autocompletion: isAutocompleteEnabled, | |
| lineNumbers: true, | |
| foldGutter: true, | |
| dropCursor: true, | |
| allowMultipleSelections: true, | |
| indentOnInput: true, | |
| bracketMatching: true, | |
| closeBrackets: true, | |
| rectangularSelection: true, | |
| crosshairCursor: true, | |
| highlightActiveLine: true, | |
| highlightSelectionMatches: true, | |
| closeBracketsKeymap: true, | |
| defaultKeymap: true, | |
| searchKeymap: true, | |
| historyKeymap: true, | |
| foldKeymap: true, | |
| completionKeymap: true, | |
| lintKeymap: true, | |
| }} | |
| className="w-full h-full flex-1 min-h-[200px]" | |
| /> | |
| </div> | |
| <div className="flex flex-col sm:flex-row justify-between items-center gap-4 pt-2"> | |
| <span className="text-[10px] text-trash-gray italic max-w-sm font-semibold leading-normal"> | |
| Gemini will inspect your algorithm for logical correctness, time/space limits, and edge cases. | |
| </span> | |
| <button | |
| onClick={handleAiEvaluation} | |
| disabled={aiLoading || !code.trim()} | |
| className="w-full sm:w-auto bg-[#a855f7] hover:bg-[#9333ea] text-white font-arcade text-[10px] py-3 px-6 border-[3px] border-ink-black shadow-[4px_4px_0px_0px_rgba(30,41,59,1)] active:translate-y-1 active:translate-x-1 active:shadow-none transition-all cursor-pointer font-bold select-none shrink-0" | |
| > | |
| {aiLoading ? ( | |
| <span className="flex items-center gap-2 justify-center"> | |
| <RefreshCw className="w-3.5 h-3.5 animate-spin" /> | |
| RUNNING EVAL... | |
| </span> | |
| ) : ( | |
| 'SUBMIT FOR AI REVIEW' | |
| )} | |
| </button> | |
| </div> | |
| {/* AI Review Loader Panel */} | |
| {aiLoading && ( | |
| <div className="p-6 border-[3px] border-ink-black bg-[#fdf8e1] gadget-shadow flex flex-col items-center justify-center gap-3 animate-pulse"> | |
| <RefreshCw className="w-7 h-7 text-primary animate-spin" /> | |
| <div className="text-center"> | |
| <h4 className="text-xs font-bold text-ink-black font-arcade">AI COMPILING SUBMISSION</h4> | |
| <p className="text-[9px] text-trash-gray mt-1 font-bold">Verifying constraints, checking edge cases, evaluating big-O complexity...</p> | |
| </div> | |
| </div> | |
| )} | |
| {/* AI Error display */} | |
| {aiError && ( | |
| <div className="p-4 border-[3px] border-red-500 bg-red-100 text-error text-xs font-semibold shadow-[2px_2px_0px_0px_rgba(30,41,59,1)]"> | |
| {aiError} | |
| </div> | |
| )} | |
| {/* AI Result details */} | |
| {aiResult && ( | |
| <div className="border-[3px] border-ink-black bg-[#fdf8e1] p-5 gadget-shadow space-y-4"> | |
| <div className="flex items-center gap-3 pb-3 border-b-2 border-dashed border-ink-black"> | |
| <div className={`w-10 h-10 border-[3px] border-ink-black flex items-center justify-center gadget-shadow shrink-0 ${ | |
| aiResult.is_correct | |
| ? 'bg-grass-green text-ink-black' | |
| : 'bg-highlight-pink text-white' | |
| }`}> | |
| <Award className="w-5 h-5" /> | |
| </div> | |
| <div> | |
| <h3 className="text-xs font-bold font-arcade text-ink-black">VERDICT RESULT</h3> | |
| <p className="text-[9px] text-trash-gray font-bold uppercase mt-1"> | |
| {aiResult.is_correct ? 'Logical verification passed successfully.' : 'Logical verification failed.'} | |
| </p> | |
| </div> | |
| </div> | |
| <div className="grid grid-cols-2 gap-4"> | |
| <div className="p-3 bg-white border-[3px] border-ink-black shadow-[2px_2px_0px_0px_rgba(30,41,59,1)]"> | |
| <span className="text-[8px] font-arcade text-trash-gray block uppercase">Detected Time</span> | |
| <p className="font-bold text-xs text-ink-black mt-1 font-code-mono">{aiResult.detected_complexity?.time || 'N/A'}</p> | |
| </div> | |
| <div className="p-3 bg-white border-[3px] border-ink-black shadow-[2px_2px_0px_0px_rgba(30,41,59,1)]"> | |
| <span className="text-[8px] font-arcade text-trash-gray block uppercase">Detected Space</span> | |
| <p className="font-bold text-xs text-ink-black mt-1 font-code-mono">{aiResult.detected_complexity?.space || 'N/A'}</p> | |
| </div> | |
| </div> | |
| {aiResult.bugs && aiResult.bugs.length > 0 && ( | |
| <div className="space-y-1.5"> | |
| <span className="text-[8px] font-arcade text-trash-gray uppercase block font-bold">Traceback / Edge Case Failures:</span> | |
| <ul className="list-disc list-inside text-xs text-ink-black space-y-1 pl-1 font-semibold"> | |
| {aiResult.bugs.map((bug: string, idx: number) => ( | |
| <li key={idx} className="leading-relaxed">{bug}</li> | |
| ))} | |
| </ul> | |
| </div> | |
| )} | |
| {aiResult.key_suggestion && ( | |
| <div className="p-3 bg-yellow-50 border-[3px] border-ink-black rounded-lg space-y-1"> | |
| <span className="text-[8px] font-arcade text-[#d97706] uppercase font-bold block">Refactoring Tip</span> | |
| <p className="text-xs text-ink-black font-semibold leading-relaxed m-0">{aiResult.key_suggestion}</p> | |
| </div> | |
| )} | |
| <div className="pt-2 border-t-2 border-dashed border-ink-black"> | |
| <p className="text-xs text-ink-black font-bold"> | |
| 🤖 AI SUGGESTION: {aiResult.is_correct ? ( | |
| <span className="text-emerald-600 font-bold">Click "Smashed It" to increment box level!</span> | |
| ) : ( | |
| <span className="text-red-500 font-bold">Click "Forgot" to reset back to Box 1.</span> | |
| )} | |
| </p> | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| ) : ( | |
| <div className="flex-1 font-sans text-xs text-ink-black leading-relaxed space-y-4 font-semibold"> | |
| <h3 className="font-bold text-sm font-arcade uppercase m-0">Offline grading instructions:</h3> | |
| <ol className="list-decimal list-inside space-y-2.5"> | |
| <li>Expand the <span className="font-bold text-primary">"Show Reference Code"</span> block above.</li> | |
| <li>Compare your logic or mental draft with the reference algorithm.</li> | |
| <li>Verify if you correctly recalled the patterns, Big-O limit, and transitions.</li> | |
| <li>Score yourself using the buttons below:</li> | |
| </ol> | |
| <div className="border-t-2 border-dashed border-ink-black pt-4 font-arcade text-[8px] leading-loose text-trash-gray space-y-2 font-bold uppercase"> | |
| <div>🔴 <span className="text-red-500">Forgot</span>: resets box back to Box 1.</div> | |
| <div>🟡 <span className="text-amber-500">Mixed</span>: keeps the current Box level.</div> | |
| <div>🟢 <span className="text-emerald-500">Smashed It</span>: increments box level by 1.</div> | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| {/* SRS Completion Logging Buttons */} | |
| <div className="flex gap-4 mt-auto shrink-0 pt-4 border-t-[3px] border-ink-black border-dashed"> | |
| <button | |
| onClick={() => handleGrade('Incorrect')} | |
| disabled={submitting} | |
| className="flex-1 bg-[#ff1a1a] border-[4px] border-ink-black rounded-xl py-4 font-window-title text-base md:text-lg text-white font-bold arcade-btn cursor-pointer disabled:opacity-50" | |
| > | |
| Forgot | |
| </button> | |
| <button | |
| onClick={() => handleGrade('Mixed')} | |
| disabled={submitting} | |
| className="flex-1 bg-[#ffcc00] border-[4px] border-ink-black rounded-xl py-4 font-window-title text-base md:text-lg text-ink-black font-bold arcade-btn cursor-pointer disabled:opacity-50" | |
| > | |
| Mixed | |
| </button> | |
| <button | |
| onClick={() => handleGrade('Correct')} | |
| disabled={submitting} | |
| className="flex-1 bg-[#39ff14] border-[4px] border-ink-black rounded-xl py-4 font-window-title text-base md:text-lg text-ink-black font-bold arcade-btn cursor-pointer disabled:opacity-50" | |
| > | |
| Smashed It | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| // Simple Markdown Parser Helpers | |
| function renderInlineMarkdown(text: string) { | |
| const regex = /(\*\*.*?\*\*|`.*?`)/g; | |
| const tokens = text.split(regex); | |
| return tokens.map((token, index) => { | |
| if (token.startsWith('**') && token.endsWith('**')) { | |
| return ( | |
| <strong key={index} className="text-ink-black font-bold"> | |
| {token.slice(2, -2)} | |
| </strong> | |
| ); | |
| } | |
| if (token.startsWith('`') && token.endsWith('`')) { | |
| return ( | |
| <code key={index} className="bg-[#1E293B] text-[#4ADE80] px-1.5 py-0.5 rounded text-[11px] font-mono border border-ink-black"> | |
| {token.slice(1, -1)} | |
| </code> | |
| ); | |
| } | |
| return token; | |
| }); | |
| } | |
| function parseMarkdown(text: string) { | |
| const parts = text.split(/(```[\s\S]*?```)/g); | |
| return parts.map((part, index) => { | |
| if (part.startsWith('```')) { | |
| const lines = part.split('\n'); | |
| const code = lines.slice(1, -1).join('\n'); | |
| return ( | |
| <pre key={index} className="bg-[#1E293B] p-3 rounded-lg border border-ink-black overflow-x-auto font-mono text-[11px] text-[#4ADE80] my-2"> | |
| <code>{code}</code> | |
| </pre> | |
| ); | |
| } | |
| const lines = part.split('\n'); | |
| return lines.map((line, lineIndex) => { | |
| const key = `${index}-${lineIndex}`; | |
| if (line.startsWith('### ')) { | |
| return <h3 key={key} className="text-xs font-bold text-ink-black mt-3 mb-1.5 uppercase font-arcade">{line.substring(4)}</h3>; | |
| } | |
| if (line.startsWith('## ')) { | |
| return <h2 key={key} className="text-sm font-bold text-ink-black mt-4 mb-2 font-headline-md">{line.substring(3)}</h2>; | |
| } | |
| if (line.startsWith('# ')) { | |
| return <h1 key={key} className="text-base font-extrabold text-ink-black mt-5 mb-2 font-headline-md">{line.substring(2)}</h1>; | |
| } | |
| if (line.trim().startsWith('- ') || line.trim().startsWith('* ')) { | |
| const content = line.trim().substring(2); | |
| return ( | |
| <div key={key} className="flex items-start gap-1.5 pl-2 my-1"> | |
| <span className="text-primary mt-1">•</span> | |
| <span className="text-ink-black text-xs leading-relaxed">{renderInlineMarkdown(content)}</span> | |
| </div> | |
| ); | |
| } | |
| if (line.trim() === '') { | |
| return <div key={key} className="h-2" />; | |
| } | |
| return <p key={key} className="text-ink-black text-xs leading-relaxed my-1">{renderInlineMarkdown(line)}</p>; | |
| }); | |
| }); | |
| } | |