diff --git a/frontend/src/components/editor/CompilationConsole.tsx b/frontend/src/components/editor/CompilationConsole.tsx index f1d915ed..be699a08 100644 --- a/frontend/src/components/editor/CompilationConsole.tsx +++ b/frontend/src/components/editor/CompilationConsole.tsx @@ -5,7 +5,37 @@ import React, { useRef, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import type { CompilationLog } from '../../utils/compilationLogger'; +import type { CompilationLog, CompileTarget } from '../../utils/compilationLogger'; + +/** One contiguous run of log lines for the same target (or none). */ +interface LogGroup { + target?: CompileTarget; + entries: { log: CompilationLog; index: number }[]; +} + +/** Group consecutive log lines by target so the console renders a section per + * board/chip. Consecutive-run (not collapse-all) preserves chronological order + * — a leading "Compiling all targets" and a trailing "Done" stay where they + * are, around the per-target sections. */ +function groupLogs(logs: CompilationLog[]): LogGroup[] { + const groups: LogGroup[] = []; + logs.forEach((log, index) => { + const last = groups[groups.length - 1]; + if (last && (last.target?.id ?? null) === (log.target?.id ?? null)) { + last.entries.push({ log, index }); + } else { + groups.push({ target: log.target, entries: [{ log, index }] }); + } + }); + return groups; +} + +/** A target section's overall outcome, derived from its lines. */ +function groupStatus(entries: LogGroup['entries']): 'error' | 'success' | 'running' { + if (entries.some((e) => e.log.type === 'error')) return 'error'; + if (entries.some((e) => e.log.type === 'success')) return 'success'; + return 'running'; +} interface CompilationConsoleProps { isOpen: boolean; @@ -154,33 +184,65 @@ export const CompilationConsole: React.FC = ({ - {/* Output content */} + {/* Output content — grouped into a section per board/chip target. */}
{filteredLogs.length === 0 ? (
{t('editor.console.empty')}
) : ( - filteredLogs.map((log, i) => ( -
- - {log.timestamp.toLocaleTimeString('en-US', { - hour12: false, - hour: '2-digit', - minute: '2-digit', - second: '2-digit', - })} - - - {log.type === 'core-install' && CORE } - {log.message} - -
- )) + groupLogs(filteredLogs).map((group, gi) => + group.target ? ( +
+
+ + {statusGlyph(groupStatus(group.entries))} + + {group.target.label} + {group.target.kind} +
+
+ {group.entries.map(({ log, index }) => ( + + ))} +
+
+ ) : ( + // No target — plain narration ("Compiling all targets...", "Done"). + group.entries.map(({ log, index }) => ) + ), + ) )}
); }; +const LogLine: React.FC<{ log: CompilationLog }> = ({ log }) => ( +
+ + {log.timestamp.toLocaleTimeString('en-US', { + hour12: false, + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + })} + + + {log.type === 'core-install' && CORE } + {log.message} + +
+); + +function statusColor(status: 'error' | 'success' | 'running'): string { + return status === 'error' ? '#ef5350' : status === 'success' ? '#66bb6a' : '#9aa0a6'; +} + +function statusGlyph(status: 'error' | 'success' | 'running'): string { + return status === 'error' ? '✕' : status === 'success' ? '✓' : '▸'; +} + function logColor(type: CompilationLog['type']): string { switch (type) { case 'error': @@ -301,6 +363,40 @@ const styles: Record = { padding: '12px 0', fontFamily: 'system-ui, sans-serif', }, + targetGroup: { + marginTop: 6, + borderLeft: '2px solid #3a3a3a', + paddingLeft: 8, + }, + targetHeader: { + display: 'flex', + alignItems: 'center', + gap: 7, + padding: '1px 0 2px', + }, + targetStatus: { + fontWeight: 700, + flexShrink: 0, + }, + targetLabel: { + color: '#e0e0e0', + fontWeight: 700, + fontSize: 11.5, + fontFamily: 'system-ui, sans-serif', + }, + targetKind: { + color: '#777', + fontSize: 9, + fontFamily: 'system-ui, sans-serif', + textTransform: 'uppercase' as const, + letterSpacing: '0.5px', + border: '1px solid #3a3a3a', + borderRadius: 3, + padding: '0 4px', + }, + targetBody: { + paddingLeft: 4, + }, logLine: { display: 'flex', gap: 8, diff --git a/frontend/src/components/editor/EditorToolbar.tsx b/frontend/src/components/editor/EditorToolbar.tsx index 589aab93..c332272a 100644 --- a/frontend/src/components/editor/EditorToolbar.tsx +++ b/frontend/src/components/editor/EditorToolbar.tsx @@ -25,7 +25,7 @@ import { useProjectStore } from '../../store/useProjectStore'; import { LibraryManagerModal } from '../simulator/LibraryManagerModal'; import { InstallLibrariesModal } from '../simulator/InstallLibrariesModal'; import { parseCompileResult } from '../../utils/compilationLogger'; -import type { CompilationLog } from '../../utils/compilationLogger'; +import type { CompilationLog, CompileTarget } from '../../utils/compilationLogger'; import { exportToWokwiZip } from '../../utils/wokwiZip'; import { importProjectFile, PROJECT_FILE_ACCEPT } from '../../utils/importProject'; import { readFirmwareFile } from '../../utils/firmwareLoader'; @@ -287,38 +287,33 @@ export const EditorToolbar = ({ const sourceC = String(props.sourceC ?? ''); const chipJson = String(props.chipJson ?? '{}'); let changed = false; + // Stamp every line for this chip with its target so the console groups + // it under its own section (alongside the boards). + const chipTarget: CompileTarget = { id: chip.id, label: chipLabel, kind: 'chip' }; + const clog = (type: CompilationLog['type'], message: string) => + addLog({ timestamp: new Date(), type, message, target: chipTarget }); // 1. C -> WASM. Only when missing — the chip designer fills this too. if (!String(props.wasmBase64 ?? '') && sourceC) { - addLog({ - timestamp: new Date(), - type: 'info', - message: `Compiling chip "${chipLabel}" to WASM...`, - }); + clog('info', `Compiling chip "${chipLabel}" to WASM...`); try { const r = await compileChip(sourceC, chipJson); if (r.success && r.wasm_base64) { props.wasmBase64 = r.wasm_base64; changed = true; - addLog({ - timestamp: new Date(), - type: 'success', - message: `Chip "${chipLabel}" compiled (${r.byte_size} B WASM).`, - }); + clog('success', `Chip "${chipLabel}" compiled (${r.byte_size} B WASM).`); } else { - addLog({ - timestamp: new Date(), - type: 'error', - message: `Chip "${chipLabel}" WASM compile failed: ${r.error || r.stderr || 'unknown error'}`, - }); + clog( + 'error', + `Chip "${chipLabel}" WASM compile failed: ${r.error || r.stderr || 'unknown error'}`, + ); failed++; } } catch (e) { - addLog({ - timestamp: new Date(), - type: 'error', - message: `Chip "${chipLabel}" WASM compile error: ${e instanceof Error ? e.message : String(e)}`, - }); + clog( + 'error', + `Chip "${chipLabel}" WASM compile error: ${e instanceof Error ? e.message : String(e)}`, + ); failed++; } } @@ -338,45 +333,34 @@ export const EditorToolbar = ({ chipGroupFiles.find((f) => f.name === programFile) ?? boardFiles.find((f) => f.name === programFile); if (!file) { - addLog({ - timestamp: new Date(), - type: 'error', - message: `Chip "${chipLabel}": program file "${programFile}" not found in the chip's files.`, - }); + clog('error', `Chip "${chipLabel}": program file "${programFile}" not found in the chip's files.`); failed++; } else { const target = targetForChip(chipJson); const fmt = formatForFile(programFile); - addLog({ - timestamp: new Date(), - type: 'info', - message: `Assembling "${programFile}" (target=${target}, format=${fmt}) for chip "${chipLabel}"...`, - }); + clog( + 'info', + `Assembling "${programFile}" (target=${target}, format=${fmt}) for chip "${chipLabel}"...`, + ); try { const rr = await compileRom(file.content, target, fmt); if (rr.success && rr.rom_base64) { props.romBytes = rr.rom_base64; props.programFile = programFile; changed = true; - addLog({ - timestamp: new Date(), - type: 'success', - message: `ROM ready: ${rr.byte_size} B injected into "${chipLabel}".`, - }); + clog('success', `ROM ready: ${rr.byte_size} B injected into "${chipLabel}".`); } else { - addLog({ - timestamp: new Date(), - type: 'error', - message: `ROM compile failed for "${programFile}": ${rr.error || rr.stderr || 'unknown error'}`, - }); + clog( + 'error', + `ROM compile failed for "${programFile}": ${rr.error || rr.stderr || 'unknown error'}`, + ); failed++; } } catch (e) { - addLog({ - timestamp: new Date(), - type: 'error', - message: `ROM compile error for "${programFile}": ${e instanceof Error ? e.message : String(e)}`, - }); + clog( + 'error', + `ROM compile error for "${programFile}": ${e instanceof Error ? e.message : String(e)}`, + ); failed++; } } @@ -432,14 +416,18 @@ export const EditorToolbar = ({ // ── End custom-chip preparation ───────────────────────────────────── const kind = activeBoard?.boardKind; + // The active board's console target, defined up front so EVERY board path + // (Pi, MicroPython, arduino-cli, errors) groups its lines under one section. + const boardLabel = activeBoard ? boardDisplayName(activeBoard) : 'Unknown'; + const boardTarget: CompileTarget | undefined = activeBoardId + ? { id: activeBoardId, label: boardLabel, kind: 'board' } + : undefined; + const blog = (type: CompilationLog['type'], message: string) => + addLog({ timestamp: new Date(), type, message, target: boardTarget }); // Raspberry Pi 3B doesn't need arduino-cli compilation if (isPiBoardKind(kind)) { - addLog({ - timestamp: new Date(), - type: 'info', - message: 'Raspberry Pi 3B: no compilation needed — run Python scripts directly.', - }); + blog('info', 'Raspberry Pi 3B: no compilation needed — run Python scripts directly.'); setMessage({ type: 'success', text: 'Ready (no compilation needed)' }); setCompiling(false); return; @@ -447,24 +435,16 @@ export const EditorToolbar = ({ // MicroPython mode — no backend compilation needed if (activeBoard?.languageMode === 'micropython' && activeBoardId) { - addLog({ - timestamp: new Date(), - type: 'info', - message: 'MicroPython: loading firmware and user files...', - }); + blog('info', 'MicroPython: loading firmware and user files...'); try { const groupFiles = useEditorStore.getState().getGroupFiles(activeBoard.activeFileGroupId); const pyFiles = groupFiles.map((f) => ({ name: f.name, content: f.content })); await loadMicroPythonProgram(activeBoardId, pyFiles); - addLog({ - timestamp: new Date(), - type: 'success', - message: 'MicroPython firmware loaded successfully', - }); + blog('success', 'MicroPython firmware loaded successfully'); setMessage({ type: 'success', text: 'MicroPython ready' }); } catch (err) { const errMsg = err instanceof Error ? err.message : 'Failed to load MicroPython'; - addLog({ timestamp: new Date(), type: 'error', message: errMsg }); + blog('error', errMsg); setMessage({ type: 'error', text: errMsg }); } finally { setCompiling(false); @@ -473,20 +453,15 @@ export const EditorToolbar = ({ } const fqbn = kind ? BOARD_KIND_FQBN[kind] : null; - const boardLabel = activeBoard ? boardDisplayName(activeBoard) : 'Unknown'; if (!fqbn) { - addLog({ timestamp: new Date(), type: 'error', message: `No FQBN for board kind: ${kind}` }); + blog('error', `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})...`, - }); + blog('info', `Starting compilation for ${boardLabel} (${fqbn})...`); try { const groupFiles = activeBoard?.activeFileGroupId @@ -524,6 +499,7 @@ export const EditorToolbar = ({ timestamp: now, type: 'info' as const, message: line, + target: boardTarget, })), ]); }, @@ -539,7 +515,7 @@ export const EditorToolbar = ({ // the live stream — parseCompileResult highlights FAILED blocks and // tags compiler errors with type='error', which the console uses for // colour + the auto-switch-to-errors filter. - const resultLogs = parseCompileResult(result, boardLabel); + const resultLogs = parseCompileResult(result, boardLabel, boardTarget); setCompileLogs((prev: CompilationLog[]) => [...prev, ...resultLogs]); if (result.success) { @@ -571,7 +547,7 @@ export const EditorToolbar = ({ } } catch (err) { const errMsg = err instanceof Error ? err.message : 'Compile failed'; - addLog({ timestamp: new Date(), type: 'error', message: errMsg }); + blog('error', errMsg); setMessage({ type: 'error', text: errMsg }); } finally { setCompiling(false); @@ -747,23 +723,22 @@ export const EditorToolbar = ({ setCompiling(true); setMessage(null); - addLog({ - timestamp: new Date(), - type: 'info', - message: 'MicroPython: loading firmware and user files...', - }); + const mpyTarget: CompileTarget = { + id: activeBoardId, + label: boardDisplayName(board), + kind: 'board', + }; + const mlog = (type: CompilationLog['type'], message: string) => + addLog({ timestamp: new Date(), type, message, target: mpyTarget }); + mlog('info', 'MicroPython: loading firmware and user files...'); try { const groupFiles = useEditorStore.getState().getGroupFiles(board.activeFileGroupId); const pyFiles = groupFiles.map((f) => ({ name: f.name, content: f.content })); await loadMicroPythonProgram(activeBoardId, pyFiles); - addLog({ - timestamp: new Date(), - type: 'success', - message: 'MicroPython firmware loaded', - }); + mlog('success', 'MicroPython firmware loaded'); } catch (err) { const errMsg = err instanceof Error ? err.message : 'Failed to load MicroPython'; - addLog({ timestamp: new Date(), type: 'error', message: errMsg }); + mlog('error', errMsg); setMessage({ type: 'error', text: errMsg }); setCompiling(false); return; @@ -955,29 +930,25 @@ export const EditorToolbar = ({ for (const board of boardsList) { const label = boardDisplayName(board); + // Stamp this board's lines so the console groups them under its section. + const boardTarget: CompileTarget = { id: board.id, label, kind: 'board' }; + const blog = (type: CompilationLog['type'], message: string) => + addLog({ timestamp: new Date(), type, message, target: boardTarget }); if (isPiBoardKind(board.boardKind)) { - addLog({ - timestamp: new Date(), - type: 'info', - message: `${label}: skipped (no compilation needed)`, - }); + blog('info', 'skipped (no compilation needed)'); ok++; continue; } const fqbn = BOARD_KIND_FQBN[board.boardKind]; if (!fqbn) { - addLog({ - timestamp: new Date(), - type: 'error', - message: `${label}: no FQBN configured`, - }); + blog('error', 'no FQBN configured'); boardFailed++; continue; } - addLog({ timestamp: new Date(), type: 'info', message: `${label}: compiling...` }); + blog('info', 'compiling...'); try { const groupFiles = useEditorStore.getState().getGroupFiles(board.activeFileGroupId); @@ -1000,17 +971,19 @@ export const EditorToolbar = ({ const now = new Date(); setCompileLogs((prev: CompilationLog[]) => [ ...prev, + // No `${label}: ` prefix — the target section header carries it. ...newLines.map((line) => ({ timestamp: now, type: 'info' as const, - message: `${label}: ${line}`, + message: line, + target: boardTarget, })), ]); }, { boardOptions: board.boardOptions, spiffsFiles: board.spiffsFiles }, ); - const resultLogs = parseCompileResult(result, label); + const resultLogs = parseCompileResult(result, label, boardTarget); setCompileLogs((prev: CompilationLog[]) => [...prev, ...resultLogs]); if (result.success) { @@ -1026,11 +999,7 @@ export const EditorToolbar = ({ boardFailed++; } } catch (err) { - addLog({ - timestamp: new Date(), - type: 'error', - message: `${label}: ${err instanceof Error ? err.message : String(err)}`, - }); + blog('error', err instanceof Error ? err.message : String(err)); boardFailed++; } } diff --git a/frontend/src/utils/compilationLogger.ts b/frontend/src/utils/compilationLogger.ts index 1146f31f..dc4fbaca 100644 --- a/frontend/src/utils/compilationLogger.ts +++ b/frontend/src/utils/compilationLogger.ts @@ -4,13 +4,28 @@ import type { CompileResult } from '../services/compilation'; +/** Which run target a log line belongs to — lets the console group output by + * board / chip, the way multiple Arduinos already stream. Optional: lines with + * no target (e.g. "Compiling all targets...", "Done") render as plain + * narration. */ +export interface CompileTarget { + id: string; + label: string; + kind: 'board' | 'chip'; +} + export interface CompilationLog { timestamp: Date; type: 'info' | 'success' | 'error' | 'warning' | 'core-install'; message: string; + target?: CompileTarget; } -export function parseCompileResult(result: CompileResult, board: string): CompilationLog[] { +export function parseCompileResult( + result: CompileResult, + board: string, + target?: CompileTarget, +): CompilationLog[] { const logs: CompilationLog[] = []; const now = new Date(); @@ -88,5 +103,5 @@ export function parseCompileResult(result: CompileResult, board: string): Compil logs.push({ timestamp: now, type: 'error', message: `✕ ${errorMsg}` }); } - return logs; + return target ? logs.map((l) => ({ ...l, target })) : logs; }