|
7 | 7 | import CodeBlock from './CodeBlock.svelte'; |
8 | 8 | import Icon from './Icon.svelte'; |
9 | 9 | import { tooltip } from './Tooltip.svelte'; |
| 10 | + import ConsoleOutput, { type LogEntry } from './ConsoleOutput.svelte'; |
10 | 11 | import { notebookStore, type CellStatus } from '$lib/stores/notebookStore'; |
11 | 12 | import { pyodideState } from '$lib/stores/pyodideStore'; |
12 | 13 | import CellOutput from '$lib/components/notebook/CellOutput.svelte'; |
|
40 | 41 | let codeBlockRef = $state<{ getCurrentCode: () => string } | undefined>(undefined); |
41 | 42 |
|
42 | 43 | // Execution output state |
43 | | - let stdout = $state(''); |
44 | | - let stderr = $state(''); |
| 44 | + let consoleLogs = $state<LogEntry[]>([]); |
| 45 | + let nextLogId = 0; |
45 | 46 | let plots = $state<string[]>([]); |
46 | 47 | let error = $state<{ message: string; traceback?: string } | null>(null); |
47 | 48 | let duration = $state<number | null>(null); |
|
55 | 56 | // Computed states |
56 | 57 | let isRunning = $derived(cellState.status === 'running'); |
57 | 58 | let isPending = $derived(cellState.status === 'pending'); |
58 | | - let hasLiveOutput = $derived(stdout || stderr || plots.length > 0 || error); |
| 59 | + let hasLiveOutput = $derived(consoleLogs.length > 0 || plots.length > 0 || error); |
59 | 60 | let showStaticOutputs = $derived(!hasLiveOutput && staticOutputs.length > 0); |
60 | 61 | let hasOutput = $derived(hasLiveOutput || showStaticOutputs); |
61 | 62 |
|
| 63 | + function addLog(message: string, level: LogEntry['level']) { |
| 64 | + consoleLogs = [...consoleLogs, { id: nextLogId++, level, message }]; |
| 65 | + } |
| 66 | +
|
| 67 | + function clearLogs() { |
| 68 | + consoleLogs = []; |
| 69 | + nextLogId = 0; |
| 70 | + } |
| 71 | +
|
62 | 72 | /** |
63 | 73 | * Execute this cell's code (called by store during prerequisite chain) |
64 | 74 | */ |
65 | 75 | async function executeCell(): Promise<void> { |
66 | 76 | const codeToRun = codeBlockRef?.getCurrentCode() ?? code; |
67 | 77 |
|
68 | 78 | // Clear previous output |
69 | | - stdout = ''; |
70 | | - stderr = ''; |
| 79 | + clearLogs(); |
71 | 80 | plots = []; |
72 | 81 | error = null; |
73 | 82 | duration = null; |
|
84 | 93 | // Execute with streaming callbacks for real-time output |
85 | 94 | const result = await execute(codeToRun, { |
86 | 95 | onStdout: (text) => { |
87 | | - stdout = stdout ? stdout + '\n' + text : text; |
| 96 | + addLog(text, 'output'); |
88 | 97 | }, |
89 | 98 | onStderr: (text) => { |
90 | | - stderr = stderr ? stderr + '\n' + text : text; |
| 99 | + addLog(text, 'warning'); |
91 | 100 | }, |
92 | 101 | onPlot: (data) => { |
93 | 102 | plots = [...plots, data]; |
94 | 103 | } |
95 | 104 | }); |
96 | 105 |
|
97 | | - // Final result (in case any output was missed) |
98 | | - stdout = result.stdout; |
99 | | - stderr = result.stderr; |
| 106 | + // Final result - add any output that wasn't streamed |
| 107 | + if (result.stdout && consoleLogs.every((log) => log.message !== result.stdout)) { |
| 108 | + addLog(result.stdout, 'output'); |
| 109 | + } |
| 110 | + if (result.stderr && consoleLogs.every((log) => log.message !== result.stderr)) { |
| 111 | + addLog(result.stderr, 'warning'); |
| 112 | + } |
100 | 113 | plots = result.plots; |
101 | 114 | duration = result.duration; |
102 | 115 |
|
|
133 | 146 | } |
134 | 147 |
|
135 | 148 | function clearOutput() { |
136 | | - stdout = ''; |
137 | | - stderr = ''; |
| 149 | + clearLogs(); |
138 | 150 | plots = []; |
139 | 151 | error = null; |
140 | 152 | duration = null; |
|
210 | 222 | </div> |
211 | 223 | {/if} |
212 | 224 |
|
213 | | - {#if stdout} |
| 225 | + {#if consoleLogs.length > 0} |
214 | 226 | <div class="output-panel"> |
215 | 227 | <div class="panel-header"> |
216 | 228 | <span>Output</span> |
217 | 229 | <div class="header-actions"> |
218 | 230 | {#if duration !== null} |
219 | 231 | <span class="duration">{duration}ms</span> |
220 | 232 | {/if} |
221 | | - <button class="icon-btn" onclick={clearOutput} use:tooltip={'Clear'}> |
222 | | - <Icon name="x" size={14} /> |
223 | | - </button> |
224 | | - </div> |
225 | | - </div> |
226 | | - <div class="panel-body"> |
227 | | - <div class="output-text">{stdout}</div> |
228 | | - </div> |
229 | | - </div> |
230 | | - {/if} |
231 | | - |
232 | | - {#if stderr && !error} |
233 | | - <div class="output-panel warning"> |
234 | | - <div class="panel-header"> |
235 | | - <span>Stderr</span> |
236 | | - <div class="header-actions"> |
237 | | - <button class="icon-btn" onclick={clearOutput} use:tooltip={'Clear'}> |
| 233 | + <button class="icon-btn" onclick={clearLogs} use:tooltip={'Clear'}> |
238 | 234 | <Icon name="x" size={14} /> |
239 | 235 | </button> |
240 | 236 | </div> |
241 | 237 | </div> |
242 | | - <div class="panel-body"> |
243 | | - <div class="output-text stderr">{stderr}</div> |
244 | | - </div> |
| 238 | + <ConsoleOutput logs={consoleLogs} maxHeight={200} /> |
245 | 239 | </div> |
246 | 240 | {/if} |
247 | 241 |
|
|
331 | 325 | padding: 0; |
332 | 326 | } |
333 | 327 |
|
334 | | - /* output-text styles are in app.css global rules */ |
335 | | -
|
336 | 328 | /* Duration in header */ |
337 | 329 | .output-panel .duration { |
338 | 330 | font-family: var(--font-ui); |
|
363 | 355 | border-top: 1px solid var(--border); |
364 | 356 | } |
365 | 357 |
|
366 | | - /* Warning panel (stderr) */ |
367 | | - .output-panel.warning { |
368 | | - background: var(--warning-bg); |
369 | | - } |
370 | | -
|
371 | | - .output-panel.warning .panel-header { |
372 | | - background: transparent; |
373 | | - color: var(--warning); |
374 | | - } |
375 | | -
|
376 | | - .output-panel.warning .output-text { |
377 | | - color: var(--warning); |
378 | | - } |
379 | | -
|
380 | 358 | /* Plots */ |
381 | 359 | .plots-body { |
382 | 360 | display: flex; |
|
391 | 369 | border-radius: var(--radius-sm); |
392 | 370 | background: transparent; |
393 | 371 | } |
394 | | -
|
395 | | - /* Output text styling */ |
396 | | - .output-text { |
397 | | - font-family: var(--font-mono); |
398 | | - font-size: var(--font-base); |
399 | | - font-weight: 400; |
400 | | - line-height: 1.5; |
401 | | - margin: 0; |
402 | | - padding: var(--space-md); |
403 | | - color: var(--text-muted); |
404 | | - white-space: pre-wrap; |
405 | | - word-break: break-word; |
406 | | - } |
407 | | -
|
408 | | - .output-text.stderr { |
409 | | - color: var(--warning); |
410 | | - } |
411 | 372 | </style> |
0 commit comments