From d4ca88f108277e0355be6a59f2b0a00820c6d293 Mon Sep 17 00:00:00 2001 From: matei jordache Date: Sat, 4 Apr 2026 21:48:08 -0700 Subject: [PATCH] fix first time experience --- README.md | 3 +-- docs/building.md | 13 +++++++++++-- frontend/src/App.tsx | 38 +++++++++++++++++++++++++++++++++----- package.json | 1 + scripts/dev.sh | 22 ++++++++++++++++++++++ 5 files changed, 68 insertions(+), 9 deletions(-) create mode 100755 scripts/dev.sh diff --git a/README.md b/README.md index 6accb62..4537b27 100644 --- a/README.md +++ b/README.md @@ -20,8 +20,7 @@ pip install -e ".[server,dev]" npm install # Running the servers -npm run backend # terminal 1 — Python server at http://127.0.0.1:8188 -npm run dev # terminal 2 — Vite dev server, open the URL it prints +npm run dev:all # one terminal — starts the Python backend and the Vite dev server together ``` ## Self-hosting diff --git a/docs/building.md b/docs/building.md index 4e655ab..e5e6397 100644 --- a/docs/building.md +++ b/docs/building.md @@ -35,6 +35,14 @@ pip install -e ".[server,dev,desktop]" Two servers run in development: the Vite frontend dev server and the Python backend. Vite proxies all API and WebSocket requests to the backend, so you only open the Vite URL in your browser. +The easiest way is the combined runner, which starts both with prefixed, colour-coded output and tears everything down on Ctrl-C: + +```bash +npm run dev:all +``` + +Or run them in separate terminals if you prefer independent logs: + ```bash # Terminal 1 — Python backend (http://127.0.0.1:8188) npm run backend @@ -157,8 +165,9 @@ TONO_APPDATA=/my/data/dir python desktop.py | Command | Description | |---|---| -| `npm run dev` | Start Vite dev server + Python backend | -| `npm run backend` | Start Python backend only | +| `npm run dev:all` | Start the Python backend and the Vite dev server together | +| `npm run dev` | Start the Vite dev server only | +| `npm run backend` | Start the Python backend only | | `npm run build` | Build frontend to `frontend/dist/` | | `npm run preview` | Preview the production frontend build | | `npm run desktop` | Build frontend + launch desktop app | diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index d6d4e73..6b76b84 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1441,32 +1441,56 @@ function Flow() { initializeDynamicNodes(hydrated.nodes); }, [initializeDynamicNodes, setNodes, setEdges]); + const frameWorkflowViewport = useCallback(() => { + const flowEl = document.querySelector('.react-flow') as HTMLElement | null; + if (!flowEl) return; + const width = flowEl.offsetWidth; + const height = flowEl.offsetHeight; + if (width <= 0 || height <= 0) return; + const allNodes = (reactFlow.getNodes() as TonoNode[]); + if (allNodes.length === 0) return; + const bounds = getRenderedNodeBounds(allNodes); + if (!bounds) return; + const vp = getViewportForBounds(bounds, width, height, 0.5, 1, 0.1); + reactFlow.setViewport(vp, { duration: 300 }); + }, [reactFlow]); + + const scheduleFrameWorkflowViewport = useCallback(() => { + // Two rAFs so ReactFlow has rendered and measured the freshly-applied nodes + // before we read their DOM dimensions. + requestAnimationFrame(() => requestAnimationFrame(() => frameWorkflowViewport())); + }, [frameWorkflowViewport]); + const applyMaybePackedWorkflow = useCallback(async (data: any) => { if (data.packed && data.packedFiles) { setStatus({ text: 'Unpacking files…', level: 'info' }); try { const { workflow, restoredPaths } = await unpackWorkflow(data); applyWorkflowData(workflow, { preservedPaths: restoredPaths }); + scheduleFrameWorkflowViewport(); // Auto-run after packed workflow loads so all previews populate requestAnimationFrame(() => requestAnimationFrame(() => scheduleAutoRun())); } catch { // Unpack failed (e.g. stale session) — load the workflow without file restoration const { packedFiles: _, packed: __, ...cleanWorkflow } = data; applyWorkflowData(cleanWorkflow); + scheduleFrameWorkflowViewport(); setStatus({ text: 'Workflow loaded but packed files could not be restored. Re-browse your input files.', level: 'error' }); return; } } else { applyWorkflowData(data); + scheduleFrameWorkflowViewport(); } - }, [applyWorkflowData, scheduleAutoRun]); + }, [applyWorkflowData, scheduleAutoRun, scheduleFrameWorkflowViewport]); const loadDefaultWorkflow = useCallback(async () => { if (defaultWorkflowLoadAttemptedRef.current) return; defaultWorkflowLoadAttemptedRef.current = true; - // Only auto-load the example workflow on first visit - if (localStorage.getItem('tono_visited')) return; + // First-visit gating is handled by the caller (see the mount useEffect + // below) so we avoid a race with /help-docs, which also flips the + // tono_visited flag and often resolves before api.getNodes(). const graphHasContent = () => { const currentNodes = (reactFlow.getNodes() as TonoNode[]); @@ -1507,16 +1531,20 @@ function Flow() { // ── Load node definitions ─────────────────────────────────────────── useEffect(() => { + // Capture first-visit state once at mount so the /help-docs fetch (which + // sets tono_visited as a side effect) cannot race with the default-workflow + // gate below. Both decisions must see the same value. + const isFirstVisit = !localStorage.getItem('tono_visited'); + api.getNodes().then((defs) => { nodeDefsRef.current = defs; setStatus({ text: `Loaded ${Object.keys(defs).length} nodes.`, level: 'info' }); - loadDefaultWorkflow(); + if (isFirstVisit) loadDefaultWorkflow(); }).catch((err) => { setStatus({ text: 'Failed to load nodes: ' + err.message, level: 'error' }); }); // Load any .md files from frontend/public/ as help tabs - const isFirstVisit = !localStorage.getItem('tono_visited'); fetch('/help-docs') .then((r) => r.ok ? r.json() : []) .then((docs: any[]) => { diff --git a/package.json b/package.json index 016de28..ede9cf1 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "clean:build": "node scripts/clean-build-artifacts.mjs", "clean:native": "node scripts/clean-build-artifacts.mjs --mode=native", "dev": "npm run clean:dev && npm --prefix frontend run dev", + "dev:all": "bash scripts/dev.sh", "build": "npm run clean:build && npm --prefix frontend run build", "preview": "npm --prefix frontend run preview", "test:frontend": "npm --prefix frontend test", diff --git a/scripts/dev.sh b/scripts/dev.sh new file mode 100755 index 0000000..1c1f8a3 --- /dev/null +++ b/scripts/dev.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +# Launch the Python backend and the Vite frontend dev server together. +# Press Ctrl-C to stop both. + +set -m +cd "$(dirname "$0")/.." + +cleanup() { + trap - INT TERM EXIT + for pid in $(jobs -p); do + kill -TERM "-$pid" 2>/dev/null || true + done + wait 2>/dev/null +} +trap cleanup INT TERM EXIT + +python -m backend.main & +npm run dev & + +while (( $(jobs -pr | wc -l) == 2 )); do + sleep 0.5 +done