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

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

View File

@@ -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[]) => {

View File

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

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