import { useState, useCallback, useRef, useEffect } from 'react'; import { useEditorStore } from '../../store/useEditorStore'; import { useSimulatorStore } from '../../store/useSimulatorStore'; import type { BoardKind } from '../../types/board'; import { BOARD_KIND_FQBN, BOARD_KIND_LABELS } from '../../types/board'; import { compileCode } from '../../services/compilation'; import { CompileAllProgress } from './CompileAllProgress'; import type { BoardCompileStatus } from './CompileAllProgress'; import { LibraryManagerModal } from '../simulator/LibraryManagerModal'; import { InstallLibrariesModal } from '../simulator/InstallLibrariesModal'; import { parseCompileResult } from '../../utils/compilationLogger'; import type { CompilationLog } from '../../utils/compilationLogger'; import { exportToWokwiZip, importFromWokwiZip } from '../../utils/wokwiZip'; import { trackCompileCode, trackRunSimulation, trackStopSimulation, trackResetSimulation, trackOpenLibraryManager } from '../../utils/analytics'; import { embedBridge } from '../../services/EmbedBridge'; import './EditorToolbar.css'; interface EditorToolbarProps { consoleOpen: boolean; setConsoleOpen: (open: boolean | ((v: boolean) => boolean)) => void; compileLogs: CompilationLog[]; setCompileLogs: (logs: CompilationLog[] | ((prev: CompilationLog[]) => CompilationLog[])) => void; } const BOARD_PILL_ICON: Record = { 'arduino-uno': '⬤', 'arduino-nano': '▪', 'arduino-mega': '▬', 'raspberry-pi-pico': '◆', 'raspberry-pi-3': '⬛', 'esp32': '⬡', 'esp32-s3': '⬡', 'esp32-c3': '⬡', }; const BOARD_PILL_COLOR: Record = { 'arduino-uno': '#4fc3f7', 'arduino-nano': '#4fc3f7', 'arduino-mega': '#4fc3f7', 'raspberry-pi-pico': '#ce93d8', 'raspberry-pi-3': '#ef9a9a', 'esp32': '#a5d6a7', 'esp32-s3': '#a5d6a7', 'esp32-c3': '#a5d6a7', }; const isEmbedMode = new URLSearchParams(window.location.search).get('embed') === 'true'; export const EditorToolbar = ({ consoleOpen, setConsoleOpen, compileLogs: _compileLogs, setCompileLogs }: EditorToolbarProps) => { const { files, codeChangedSinceLastCompile, markCompiled } = useEditorStore(); const { boards, activeBoardId, compileBoardProgram, updateBoard, startBoard, stopBoard, resetBoard, // legacy compat startSimulation, stopSimulation, resetSimulation, running, compiledHex, } = useSimulatorStore(); const activeBoard = boards.find((b) => b.id === activeBoardId) ?? boards[0]; const [compiling, setCompiling] = useState(false); const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null); const [libManagerOpen, setLibManagerOpen] = useState(false); const [pendingLibraries, setPendingLibraries] = useState([]); const [installModalOpen, setInstallModalOpen] = useState(false); const importInputRef = useRef(null); const toolbarRef = useRef(null); const [overflowOpen, setOverflowOpen] = useState(false); const overflowMenuRef = useRef(null); const [missingLibHint, setMissingLibHint] = useState(false); // (ResizeObserver removed — Library Manager is always visible now, // only import/export live in the overflow menu) // Close overflow dropdown on outside click useEffect(() => { if (!overflowOpen) return; const handler = (e: MouseEvent) => { if (overflowMenuRef.current && !overflowMenuRef.current.contains(e.target as Node)) { setOverflowOpen(false); } }; document.addEventListener('mousedown', handler); return () => document.removeEventListener('mousedown', handler); }, [overflowOpen]); // Compile All state const [compileAllOpen, setCompileAllOpen] = useState(false); const [compileAllRunning, setCompileAllRunning] = useState(false); const [compileAllStatuses, setCompileAllStatuses] = useState([]); const addLog = useCallback((log: CompilationLog) => { setCompileLogs((prev: CompilationLog[]) => [...prev, log]); }, [setCompileLogs]); const handleCompile = async () => { setCompiling(true); setMessage(null); setConsoleOpen(true); trackCompileCode(); const kind = activeBoard?.boardKind; // Raspberry Pi 3B doesn't need arduino-cli compilation if (kind === 'raspberry-pi-3') { addLog({ timestamp: new Date(), type: 'info', message: 'Raspberry Pi 3B: no compilation needed — run Python scripts directly.' }); setMessage({ type: 'success', text: 'Ready (no compilation needed)' }); setCompiling(false); return; } const fqbn = kind ? BOARD_KIND_FQBN[kind] : null; const boardLabel = kind ? BOARD_KIND_LABELS[kind] : 'Unknown'; if (!fqbn) { addLog({ timestamp: new Date(), type: 'error', message: `No FQBN for board kind: ${kind}` }); setMessage({ type: 'error', text: 'Unknown board' }); setCompiling(false); return; } addLog({ timestamp: new Date(), type: 'info', message: `Starting compilation for ${boardLabel} (${fqbn})...` }); try { const sketchFiles = files.map((f) => ({ name: f.name, content: f.content })); const result = await compileCode(sketchFiles, fqbn); const resultLogs = parseCompileResult(result, boardLabel); setCompileLogs((prev: CompilationLog[]) => [...prev, ...resultLogs]); if (result.success) { const program = result.hex_content ?? result.binary_content ?? null; if (program && activeBoardId) { compileBoardProgram(activeBoardId, program); if (result.has_wifi !== undefined) { updateBoard(activeBoardId, { hasWifi: result.has_wifi }); } } setMessage({ type: 'success', text: 'Compiled successfully' }); markCompiled(); setMissingLibHint(false); embedBridge.notifyCompileResult(true); // NEW: push hex to parent (embed mode only — EmbedBridge.send is no-op if not embedded) embedBridge.notifyHexReady(activeBoardId, program, activeBoard?.boardKind ?? null); } else { const errText = result.error || result.stderr || 'Compile failed'; setMessage({ type: 'error', text: errText }); // Detect missing library errors — common patterns: // "No such file or directory" for #include, "fatal error: XXX.h" const looksLikeMissingLib = /No such file or directory|fatal error:.*\.h|library not found/i.test(errText); setMissingLibHint(looksLikeMissingLib); } } catch (err) { const errMsg = err instanceof Error ? err.message : 'Compile failed'; addLog({ timestamp: new Date(), type: 'error', message: errMsg }); setMessage({ type: 'error', text: errMsg }); } finally { setCompiling(false); } }; // Track whether we should auto-run after compilation completes const autoRunAfterCompile = useRef(false); const handleRun = async () => { if (activeBoardId) { const board = boards.find((b) => b.id === activeBoardId); const isQemuBoard = board?.boardKind === 'raspberry-pi-3' || board?.boardKind === 'esp32' || board?.boardKind === 'esp32-s3'; // QEMU boards: auto-compile if no firmware available yet if (isQemuBoard) { if (!board?.compiledProgram || codeChangedSinceLastCompile) { autoRunAfterCompile.current = true; await handleCompile(); const updatedBoard = useSimulatorStore.getState().boards.find((b) => b.id === activeBoardId); if (autoRunAfterCompile.current && updatedBoard?.compiledProgram) { autoRunAfterCompile.current = false; trackRunSimulation(updatedBoard.boardKind); startBoard(activeBoardId); setMessage(null); } else { autoRunAfterCompile.current = false; } return; } trackRunSimulation(board?.boardKind); startBoard(activeBoardId); setMessage(null); return; } // Auto-compile if no program or code changed since last compile if (!board?.compiledProgram || codeChangedSinceLastCompile) { autoRunAfterCompile.current = true; await handleCompile(); // After compile, check if it succeeded and run const updatedBoard = useSimulatorStore.getState().boards.find((b) => b.id === activeBoardId); if (autoRunAfterCompile.current && updatedBoard?.compiledProgram) { autoRunAfterCompile.current = false; trackRunSimulation(updatedBoard.boardKind); startBoard(activeBoardId); setMessage(null); } else { autoRunAfterCompile.current = false; } return; } trackRunSimulation(board?.boardKind); startBoard(activeBoardId); setMessage(null); return; } // Legacy fallback if (!compiledHex || codeChangedSinceLastCompile) { autoRunAfterCompile.current = true; await handleCompile(); const hex = useSimulatorStore.getState().compiledHex; if (autoRunAfterCompile.current && hex) { autoRunAfterCompile.current = false; trackRunSimulation(); startSimulation(); setMessage(null); } else { autoRunAfterCompile.current = false; } } else { trackRunSimulation(); startSimulation(); setMessage(null); } }; const handleStop = () => { trackStopSimulation(); if (activeBoardId) stopBoard(activeBoardId); else stopSimulation(); setMessage(null); }; const handleReset = () => { trackResetSimulation(); if (activeBoardId) resetBoard(activeBoardId); else resetSimulation(); setMessage(null); }; const handleCompileAll = async () => { const boardsList = useSimulatorStore.getState().boards; const initialStatuses: BoardCompileStatus[] = boardsList.map((b) => ({ boardId: b.id, boardKind: b.boardKind, state: 'pending', })); setCompileAllStatuses(initialStatuses); setCompileAllOpen(true); setCompileAllRunning(true); for (const board of boardsList) { const updateStatus = (patch: Partial) => setCompileAllStatuses((prev) => prev.map((s) => s.boardId === board.id ? { ...s, ...patch } : s)); // Pi 3 doesn't need compilation if (board.boardKind === 'raspberry-pi-3') { updateStatus({ state: 'skipped' }); continue; } const fqbn = BOARD_KIND_FQBN[board.boardKind]; if (!fqbn) { updateStatus({ state: 'error', error: `No FQBN configured for ${board.boardKind}` }); continue; } updateStatus({ state: 'compiling' }); try { const groupFiles = useEditorStore.getState().getGroupFiles(board.activeFileGroupId); const sketchFiles = groupFiles.map((f) => ({ name: f.name, content: f.content })); const result = await compileCode(sketchFiles, fqbn); if (result.success) { const program = result.hex_content ?? result.binary_content ?? null; if (program) { compileBoardProgram(board.id, program); if (result.has_wifi !== undefined) { updateBoard(board.id, { hasWifi: result.has_wifi }); } } updateStatus({ state: 'success' }); } else { updateStatus({ state: 'error', error: result.stderr || result.error || 'Compilation failed' }); } } catch (err) { updateStatus({ state: 'error', error: err instanceof Error ? err.message : String(err) }); } // Always continue to next board } setCompileAllRunning(false); }; const handleRunAll = () => { const boardsList = useSimulatorStore.getState().boards; for (const board of boardsList) { const isQemu = board.boardKind === 'raspberry-pi-3' || board.boardKind === 'esp32' || board.boardKind === 'esp32-s3'; if (!board.running && (isQemu || board.compiledProgram)) { startBoard(board.id); } } setCompileAllOpen(false); }; const handleExport = async () => { try { const { components, wires, boardPosition, boardType: legacyBoardType } = useSimulatorStore.getState(); const projectName = files.find((f) => f.name.endsWith('.ino'))?.name.replace('.ino', '') || 'velxio-project'; await exportToWokwiZip(files, components, wires, legacyBoardType, projectName, boardPosition); } catch (err) { setMessage({ type: 'error', text: 'Export failed.' }); } }; /** Export circuit as JSON for Elemes lesson markdown (VELXIO_CIRCUIT block). */ const handleExportCircuitJSON = () => { const { components, wires, boards: allBoards, activeBoardId: bid } = useSimulatorStore.getState(); const board = allBoards.find((b) => b.id === bid); const circuitData = { board: board ? BOARD_KIND_FQBN[board.boardKind] || board.boardKind : 'arduino:avr:uno', components: components.map((c) => ({ type: c.metadataId, id: c.id, x: c.x, y: c.y, rotation: (c.properties as Record)?.rotation || 0, props: Object.fromEntries( Object.entries(c.properties as Record).filter(([k]) => k !== 'rotation') ), })), wires: wires.map((w) => ({ start: { componentId: w.start.componentId, pinName: w.start.pinName }, end: { componentId: w.end.componentId, pinName: w.end.pinName }, })), }; navigator.clipboard.writeText(JSON.stringify(circuitData, null, 2)); setMessage({ type: 'success', text: 'Circuit JSON copied!' }); }; /** Export expected wiring as JSON for Elemes lesson markdown (EXPECTED_WIRING block). */ const handleExportExpectedWiring = () => { const { wires } = useSimulatorStore.getState(); const wiringArray = wires.map((w) => [ `${w.start.componentId}:${w.start.pinName}`, `${w.end.componentId}:${w.end.pinName}`, ]); navigator.clipboard.writeText(JSON.stringify(wiringArray, null, 2)); setMessage({ type: 'success', text: 'Expected wiring JSON copied!' }); }; const handleImportFile = async (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!importInputRef.current) return; importInputRef.current.value = ''; if (!file) return; try { const result = await importFromWokwiZip(file); const { loadFiles } = useEditorStore.getState(); const { setComponents, setWires, setBoardType, setBoardPosition, stopSimulation } = useSimulatorStore.getState(); stopSimulation(); if (result.boardType) setBoardType(result.boardType); setBoardPosition(result.boardPosition); setComponents(result.components); setWires(result.wires); if (result.files.length > 0) loadFiles(result.files); setMessage({ type: 'success', text: `Imported ${file.name}` }); if (result.libraries.length > 0) { setPendingLibraries(result.libraries); setInstallModalOpen(true); } } catch (err: any) { setMessage({ type: 'error', text: err?.message || 'Import failed.' }); } }; return ( <>
{/* Compile All progress panel — floats above the toolbar */} {compileAllOpen && ( setCompileAllOpen(false)} /> )}
{/* Active board context pill */} {activeBoard && (
{BOARD_PILL_ICON[activeBoard.boardKind]} {BOARD_KIND_LABELS[activeBoard.boardKind]} {activeBoard.running && }
)}
{/* Compile */}
{/* Run */} {/* Stop */} {/* Reset */} {boards.length > 1 && ( <>
{/* Compile All */} {/* Run All */} )}
{/* Status message */} {message && ( {message.type === 'success' ? ( ) : ( )} {message.text} )} {/* Hidden file input for import (always present) */} {/* Library Manager — always visible with label */} {/* Import / Export — overflow menu */}
{overflowOpen && (
{!isEmbedMode && ( )} {!isEmbedMode && ( <>
)}
)}
{/* Output Console toggle */}
{/* Error detail bar */} {message?.type === 'error' && message.text.length > 40 && !consoleOpen && (
{message.text}
)} {/* Missing library hint */} {missingLibHint && (
Missing library? Install it from the
)} setLibManagerOpen(false)} /> setInstallModalOpen(false)} libraries={pendingLibraries} /> ); };