iamfebin's picture
Configure AlgoSpaced for Hugging Face Spaces deployment
aaa634c
Raw
History Blame Contribute Delete
31.9 kB
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):&#10; # 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>;
});
});
}