import { useEffect, useMemo, useRef, useState } from 'react'; import { Check, Copy, Download, Loader2, Play, RotateCcw, Send } from 'lucide-react'; import type { ToolCallView } from '@/lib/types'; import { api } from '@/lib/api'; import { fmtDuration } from '@/lib/format'; import { cn } from '@/lib/cn'; import { useUI } from '@/stores/ui'; import { ArtifactChips } from './artifact-chips'; interface RunOut { stdout?: string; stderr?: string; exit_code?: number; duration_ms?: number; artifacts?: { file_id: string; filename: string; type: string }[] } type Tab = 'code' | 'output' | 'figures'; let hlPromise: Promise<(code: string) => string> | null = null; function highlighter() { if (!hlPromise) { hlPromise = (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')], engine: createJavaScriptRegexEngine() }); return (code: string) => hl.codeToHtml(code, { lang: 'python', theme: 'github-dark' }); })(); } return hlPromise; } export function PythonExecCard({ call, conversationId }: { call: ToolCallView; conversationId: string }) { const p = call.payload as { code?: string; description?: string; stdout?: string; stderr?: string; exit_code?: number; duration_ms?: number; artifacts?: ToolCallView['artifacts'] }; const initialArtifacts = (p.artifacts || call.artifacts || []) as NonNullable; const [tab, setTab] = useState(initialArtifacts.some((a) => a.type === 'image') ? 'figures' : p.stdout || p.stderr ? 'output' : 'code'); const [code, setCode] = useState(p.code || ''); const [editing, setEditing] = useState(false); const [html, setHtml] = useState(''); const [running, setRunning] = useState(false); const [out, setOut] = useState({ stdout: p.stdout, stderr: p.stderr, exit_code: p.exit_code, duration_ms: p.duration_ms, artifacts: initialArtifacts.filter((a) => a.file_id).map((a) => ({ file_id: a.file_id!, filename: a.filename || '', type: a.type })) }); const [copied, setCopied] = useState(false); const taRef = useRef(null); const setDraft = useUI((s) => s.setDraft); const dirty = code !== (p.code || ''); useEffect(() => { let alive = true; highlighter().then((f) => alive && setHtml(f(code))).catch(() => undefined); return () => { alive = false; }; }, [code]); const lines = useMemo(() => code.split('\n').length, [code]); const images = (out.artifacts || []).filter((a) => a.type === 'image'); const others = (out.artifacts || []).filter((a) => a.type !== 'image'); const run = async () => { setRunning(true); try { const r = await api('/tools/python/run', { method: 'POST', body: JSON.stringify({ code, conversation_id: conversationId }) }); setOut(r); setTab(r.artifacts?.some((a) => a.type === 'image') ? 'figures' : 'output'); } catch (e) { setOut({ stderr: e instanceof Error ? e.message : 'Erreur', exit_code: 1 }); setTab('output'); } finally { setRunning(false); } }; const copy = async () => { await navigator.clipboard.writeText(code); setCopied(true); setTimeout(() => setCopied(false), 1200); }; const download = () => { const blob = new Blob([code], { type: 'text/x-python' }); const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = 'calcul.py'; a.click(); URL.revokeObjectURL(a.href); }; const askTutor = () => setDraft(`À propos du code Python ci-dessus (${p.description || 'calcul'}) : `); const TabBtn = ({ id, label, count }: { id: Tab; label: string; count?: number }) => ( ); return (
{p.description &&

{p.description}

}
{dirty && }
{tab === 'code' && (
python · {lines} lignes{dirty ? ' · modifié' : ''}
{editing ? (