import { useEffect, useState } from 'react'; import { Check, Copy, Pencil, Play, Square } from 'lucide-react'; import { api } from '@/lib/api'; import { fmtDuration } from '@/lib/format'; import { Button } from '@/components/ui/button'; let highlighterPromise: Promise<{ codeToHtml: (code: string, o: { lang: string; theme: string }) => string }> | null = null; /** Fine-grained shiki bundle: only the languages students actually meet (keeps the PWA small). */ async function getHighlighter() { if (!highlighterPromise) { highlighterPromise = (async () => { const [{ createHighlighterCore }, { createJavaScriptRegexEngine }] = await Promise.all([ import('shiki/core'), import('shiki/engine/javascript'), ]); const hl = await createHighlighterCore({ themes: [import('shiki/themes/github-dark.mjs')], langs: [import('shiki/langs/python.mjs'), import('shiki/langs/json.mjs'), import('shiki/langs/bash.mjs'), import('shiki/langs/sql.mjs'), import('shiki/langs/javascript.mjs')], engine: createJavaScriptRegexEngine(), }); const loaded = new Set(hl.getLoadedLanguages()); return { codeToHtml: (code: string, o: { lang: string; theme: string }) => hl.codeToHtml(code, { lang: loaded.has(o.lang) ? o.lang : 'text', theme: o.theme }) }; })(); } return highlighterPromise; } interface RunOut { stdout?: string; stderr?: string; exit_code?: number; duration_ms?: number; artifacts?: { file_id: string; filename: string; type: string }[] } export function CodeBlock({ code, lang, conversationId, streaming }: { code: string; lang: string; conversationId?: string; streaming?: boolean }) { const [html, setHtml] = useState(''); const [copied, setCopied] = useState(false); const [editing, setEditing] = useState(false); const [draft, setDraft] = useState(code); const [running, setRunning] = useState(false); const [out, setOut] = useState(null); const runnable = lang === 'python' || lang === 'py'; useEffect(() => { if (streaming) return; let alive = true; getHighlighter().then((h) => alive && setHtml(h.codeToHtml(editing ? draft : code, { lang: lang || 'text', theme: 'github-dark' }))).catch(() => undefined); return () => { alive = false; }; }, [code, draft, editing, lang, streaming]); const copy = async () => { await navigator.clipboard.writeText(editing ? draft : code); setCopied(true); setTimeout(() => setCopied(false), 1200); }; const run = async () => { setRunning(true); setOut(null); try { const r = await api('/tools/python/run', { method: 'POST', body: JSON.stringify({ code: editing ? draft : code, conversation_id: conversationId }) }); setOut(r); } catch (e) { setOut({ stderr: e instanceof Error ? e.message : 'Erreur', exit_code: 1 }); } finally { setRunning(false); } }; return (
{lang || 'code'}
{runnable && !streaming && ( <> )}
{editing ? (