fix first time experience
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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 |
|
||||||
|
|||||||
@@ -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[]) => {
|
||||||
|
|||||||
@@ -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
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