Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions src/lib/components/WelcomeModal.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,10 @@
interface Props {
onNew: () => void;
onClose: () => void;
onLoadExample: (url: string) => void;
}

let { onNew, onClose }: Props = $props();
let { onNew, onClose, onLoadExample }: Props = $props();

const examples: Example[] = [
{ filename: 'feedback-system.json', basename: 'feedback-system', name: 'Feedback System', description: 'Linear feedback system with delayed step excitation' },
Expand Down Expand Up @@ -97,7 +98,7 @@

<div class="banner-content">
<div class="version-info">
PathView {PATHVIEW_VERSION} · {Object.entries(EXTRACTED_VERSIONS).map(([pkg, ver]) => `${pkg.replace('_', '-')} ${ver}`).join(' · ')}
pathview {PATHVIEW_VERSION} · {Object.entries(EXTRACTED_VERSIONS).map(([pkg, ver]) => `${pkg.replace('_', '-')} ${ver}`).join(' · ')}
</div>

<div class="header">
Expand Down Expand Up @@ -165,12 +166,11 @@
<div class="examples-section">
<div class="examples-grid">
{#each examples as example}
<a
class="example-card"
href="?model={base}/examples/{example.filename}"
data-sveltekit-reload
onclick={onClose}
>
<button
type="button"
class="example-card"
onclick={() => onLoadExample(`${base}/examples/${example.filename}`)}
>
<div class="example-info">
<div class="example-name">{example.name}</div>
<div class="example-description">{example.description}</div>
Expand All @@ -184,7 +184,7 @@
onerror={(e) => { (e.currentTarget as HTMLImageElement).style.display = 'none'; }}
/>
</div>
</a>
</button>
{/each}
</div>
</div>
Expand Down
6 changes: 6 additions & 0 deletions src/lib/pyodide/backend/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,12 @@ export const replState = {
export async function init(): Promise<void> {
const backend = getBackend();

// Idempotent: several callers invoke init() (auto-detect, toolbox installer,
// first run, helper injection). The backend init itself is a no-op once
// ready, but logging/callback setup ran every time, producing repeated
// "Initializing Python REPL..." noise. Bail early when ready.
if (backend.isReady()) return;

// Set up console output callbacks
backend.onStdout((value) => consoleStore.output(value));
backend.onStderr((value) => consoleStore.error(value));
Expand Down
36 changes: 34 additions & 2 deletions src/routes/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
import { openNodeDialog } from '$lib/stores/nodeDialog';
import { openEventDialog } from '$lib/stores/eventDialog';
import type { MenuItemType } from '$lib/components/ContextMenu.svelte';
import { pyodideState, simulationState, initPyodide, stopSimulation, continueStreamingSimulation, stageMutations } from '$lib/pyodide/bridge';
import { pyodideState, simulationState, initPyodide, stopSimulation, continueStreamingSimulation, stageMutations, resetSimulation } from '$lib/pyodide/bridge';
import { pendingMutationCount } from '$lib/pyodide/mutationQueue';
import { initBackendFromUrl, autoDetectBackend } from '$lib/pyodide/backend';
import { runGraphStreamingSimulation, validateGraphSimulation, exportToPython } from '$lib/pyodide/pathsimRunner';
Expand Down Expand Up @@ -203,6 +203,11 @@
const urlModelConfig = getUrlModelConfig();
let showWelcomeModal = $state(!urlModelConfig); // Hide if loading from URL

// Backend-ready promise (assigned in onMount). Component-scoped so client-
// side example loading can gate its toolbox install on the running worker
// instead of forcing a full reload + Pyodide reinit.
let backendReady: Promise<unknown> | undefined = $state(undefined);

// Track widths directly - initialized on first dual-panel open
let consolePanelWidth = $state<number | undefined>(undefined);
let plotPanelWidth = $state<number | undefined>(undefined);
Expand Down Expand Up @@ -581,7 +586,7 @@
// to `registryVersion` bumps, so any (missing) placeholders upgrade
// themselves as soon as their toolbox registers.
seedPreloadedToolboxes();
const backendReady = (async () => {
backendReady = (async () => {
try {
await autoDetectBackend();
await initBackendFromUrl();
Expand Down Expand Up @@ -1231,6 +1236,32 @@
}
}

// Load an example/model client-side — no page reload, so the running
// Pyodide worker is reused (no reinit). Reflects the model in the URL via
// replaceState, so deep-links (?model=) still work and the URL stays
// shareable. Used by the welcome modal's example cards.
async function loadExample(url: string): Promise<void> {
showWelcomeModal = false;
// Clear previous results / REPL state; the worker stays up.
await resetSimulation();
const result = await importFromUrl(url, {
deferToolboxInstall: true,
backendReady: backendReady ?? Promise.resolve()
});
if (result.success) {
try {
history.replaceState(history.state, '', `?model=${encodeURIComponent(url)}`);
} catch {
/* replaceState can throw in odd embedding contexts; non-fatal */
}
setTimeout(() => triggerFitView(), 100);
} else if (result.error) {
consoleStore.error(`Failed to load example: ${url}`);
consoleStore.error(result.error);
showConsole = true;
}
}

// Track placement offset for stacking prevention
let placementOffset = 0;
let lastPlacementTime = 0;
Expand Down Expand Up @@ -1851,6 +1882,7 @@
<WelcomeModal
onNew={handleNew}
onClose={() => showWelcomeModal = false}
onLoadExample={loadExample}
/>
{/if}
</div>
Expand Down
Loading