fix first time experience

This commit is contained in:
2026-04-04 21:48:08 -07:00
parent 561501259b
commit d4ca88f108
5 changed files with 68 additions and 9 deletions

View File

@@ -20,8 +20,7 @@ pip install -e ".[server,dev]"
npm install npm install
# Running the servers # Running the servers
npm run backend # terminal 1 — Python server at http://127.0.0.1:8188 npm run dev:all # one terminal — starts the Python backend and the Vite dev server together
npm run dev # terminal 2 — Vite dev server, open the URL it prints
``` ```
## Self-hosting ## Self-hosting

View File

@@ -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. 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 ```bash
# Terminal 1 — Python backend (http://127.0.0.1:8188) # Terminal 1 — Python backend (http://127.0.0.1:8188)
npm run backend npm run backend
@@ -157,8 +165,9 @@ TONO_APPDATA=/my/data/dir python desktop.py
| Command | Description | | Command | Description |
|---|---| |---|---|
| `npm run dev` | Start Vite dev server + Python backend | | `npm run dev:all` | Start the Python backend and the Vite dev server together |
| `npm run backend` | Start Python backend only | | `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 build` | Build frontend to `frontend/dist/` |
| `npm run preview` | Preview the production frontend build | | `npm run preview` | Preview the production frontend build |
| `npm run desktop` | Build frontend + launch desktop app | | `npm run desktop` | Build frontend + launch desktop app |

View File

@@ -1441,32 +1441,56 @@ function Flow() {
initializeDynamicNodes(hydrated.nodes); initializeDynamicNodes(hydrated.nodes);
}, [initializeDynamicNodes, setNodes, setEdges]); }, [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) => { const applyMaybePackedWorkflow = useCallback(async (data: any) => {
if (data.packed && data.packedFiles) { if (data.packed && data.packedFiles) {
setStatus({ text: 'Unpacking files…', level: 'info' }); setStatus({ text: 'Unpacking files…', level: 'info' });
try { try {
const { workflow, restoredPaths } = await unpackWorkflow(data); const { workflow, restoredPaths } = await unpackWorkflow(data);
applyWorkflowData(workflow, { preservedPaths: restoredPaths }); applyWorkflowData(workflow, { preservedPaths: restoredPaths });
scheduleFrameWorkflowViewport();
// Auto-run after packed workflow loads so all previews populate // Auto-run after packed workflow loads so all previews populate
requestAnimationFrame(() => requestAnimationFrame(() => scheduleAutoRun())); requestAnimationFrame(() => requestAnimationFrame(() => scheduleAutoRun()));
} catch { } catch {
// Unpack failed (e.g. stale session) — load the workflow without file restoration // Unpack failed (e.g. stale session) — load the workflow without file restoration
const { packedFiles: _, packed: __, ...cleanWorkflow } = data; const { packedFiles: _, packed: __, ...cleanWorkflow } = data;
applyWorkflowData(cleanWorkflow); applyWorkflowData(cleanWorkflow);
scheduleFrameWorkflowViewport();
setStatus({ text: 'Workflow loaded but packed files could not be restored. Re-browse your input files.', level: 'error' }); setStatus({ text: 'Workflow loaded but packed files could not be restored. Re-browse your input files.', level: 'error' });
return; return;
} }
} else { } else {
applyWorkflowData(data); applyWorkflowData(data);
scheduleFrameWorkflowViewport();
} }
}, [applyWorkflowData, scheduleAutoRun]); }, [applyWorkflowData, scheduleAutoRun, scheduleFrameWorkflowViewport]);
const loadDefaultWorkflow = useCallback(async () => { const loadDefaultWorkflow = useCallback(async () => {
if (defaultWorkflowLoadAttemptedRef.current) return; if (defaultWorkflowLoadAttemptedRef.current) return;
defaultWorkflowLoadAttemptedRef.current = true; defaultWorkflowLoadAttemptedRef.current = true;
// Only auto-load the example workflow on first visit // First-visit gating is handled by the caller (see the mount useEffect
if (localStorage.getItem('tono_visited')) return; // 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 graphHasContent = () => {
const currentNodes = (reactFlow.getNodes() as TonoNode[]); const currentNodes = (reactFlow.getNodes() as TonoNode[]);
@@ -1507,16 +1531,20 @@ function Flow() {
// ── Load node definitions ─────────────────────────────────────────── // ── Load node definitions ───────────────────────────────────────────
useEffect(() => { 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) => { api.getNodes().then((defs) => {
nodeDefsRef.current = defs; nodeDefsRef.current = defs;
setStatus({ text: `Loaded ${Object.keys(defs).length} nodes.`, level: 'info' }); setStatus({ text: `Loaded ${Object.keys(defs).length} nodes.`, level: 'info' });
loadDefaultWorkflow(); if (isFirstVisit) loadDefaultWorkflow();
}).catch((err) => { }).catch((err) => {
setStatus({ text: 'Failed to load nodes: ' + err.message, level: 'error' }); setStatus({ text: 'Failed to load nodes: ' + err.message, level: 'error' });
}); });
// Load any .md files from frontend/public/ as help tabs // Load any .md files from frontend/public/ as help tabs
const isFirstVisit = !localStorage.getItem('tono_visited');
fetch('/help-docs') fetch('/help-docs')
.then((r) => r.ok ? r.json() : []) .then((r) => r.ok ? r.json() : [])
.then((docs: any[]) => { .then((docs: any[]) => {

View File

@@ -11,6 +11,7 @@
"clean:build": "node scripts/clean-build-artifacts.mjs", "clean:build": "node scripts/clean-build-artifacts.mjs",
"clean:native": "node scripts/clean-build-artifacts.mjs --mode=native", "clean:native": "node scripts/clean-build-artifacts.mjs --mode=native",
"dev": "npm run clean:dev && npm --prefix frontend run dev", "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", "build": "npm run clean:build && npm --prefix frontend run build",
"preview": "npm --prefix frontend run preview", "preview": "npm --prefix frontend run preview",
"test:frontend": "npm --prefix frontend test", "test:frontend": "npm --prefix frontend test",

22
scripts/dev.sh Executable file
View File

@@ -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