fix first time experience
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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[]) => {
|
||||
|
||||
@@ -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
22
scripts/dev.sh
Executable 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
|
||||
Reference in New Issue
Block a user