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 './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', }; export const EditorToolbar = ({ consoleOpen, setConsoleOpen, compileLogs: _compileLogs, setCompileLogs }: EditorToolbarProps) => { const { files } = useEditorStore(); const { boards, activeBoardId, compileBoardProgram, 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 [overflowCollapsed, setOverflowCollapsed] = useState(false); const [overflowOpen, setOverflowOpen] = useState(false); const overflowMenuRef = useRef(null); // Collapse secondary buttons when toolbar is too narrow useEffect(() => { const el = toolbarRef.current; if (!el) return; const ro = new ResizeObserver(([entry]) => { setOverflowCollapsed(entry.contentRect.width < 500); }); ro.observe(el); return () => ro.disconnect(); }, []); // 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); 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); } setMessage({ type: 'success', text: 'Compiled successfully' }); } else { setMessage({ type: 'error', text: result.error || result.stderr || 'Compile failed' }); } } 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); } }; const handleRun = () => { if (activeBoardId) { const board = boards.find((b) => b.id === activeBoardId); const isQemuBoard = board?.boardKind === 'raspberry-pi-3' || board?.boardKind === 'esp32' || board?.boardKind === 'esp32-s3'; if (isQemuBoard || board?.compiledProgram) { startBoard(activeBoardId); setMessage(null); return; } } // legacy fallback if (compiledHex) { startSimulation(); setMessage(null); } else { setMessage({ type: 'error', text: 'Compile first' }); } }; const handleStop = () => { if (activeBoardId) stopBoard(activeBoardId); else stopSimulation(); setMessage(null); }; const handleReset = () => { 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); 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.' }); } }; 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) */} {/* Secondary buttons — hidden when toolbar is narrow */} {!overflowCollapsed && ( <> {/* Import Wokwi zip */} {/* Export Wokwi zip */} {/* Libraries */} )} {/* Overflow (…) button — shown when toolbar is narrow */} {overflowCollapsed && (
{overflowOpen && (
)}
)}
{/* Output Console toggle */}
{/* Error detail bar */} {message?.type === 'error' && message.text.length > 40 && !consoleOpen && (
{message.text}
)} setLibManagerOpen(false)} /> setInstallModalOpen(false)} libraries={pendingLibraries} /> ); };