From 223336c606630c10a0a1219fb48ae899d8319e99 Mon Sep 17 00:00:00 2001 From: dtoro Date: Sat, 28 Mar 2026 22:20:19 +0100 Subject: [PATCH] fix: improvements --- ARCHITECTURE.md | 259 +++++++++++++----- CONTRIBUTING.md | 61 +++-- PERFORMANCE_IMPROVEMENTS.md | 130 +++++---- README.md | 142 +++++----- frontend/src/app/canvas/useCanvasGraph.ts | 13 +- .../app/recollections/layout/TreeBrowser.tsx | 8 +- .../components/nodes/config/renderingLogic.ts | 33 ++- .../nodes/render/useRenderingNodeState.ts | 8 +- frontend/src/lib/graph/configTypes.ts | 85 ++++-- frontend/src/main.tsx | 16 +- 10 files changed, 506 insertions(+), 249 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index ef3f6f7..c38431e 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -2,104 +2,215 @@ ## 1. Executive Summary -This document provides a high-level overview of the ZUI application's architecture, focusing on component organization, data flow, technology stack, and contribution guidelines for junior developers and AI agents. +Zui is a node-based visual editor for composing configs, templates, variables, and AI-generated content. The frontend is a React 18 SPA backed by a thin Express API for AI agent calls. All graph data is persisted in `localStorage`; the backend is stateless. + +--- ## 2. Technology Stack -- **Frontend**: React 18, TypeScript, Vite, Tailwind CSS, Shadcn UI, Zustand (canvasStore), custom hooks, and canvas rendering engine. -- **Backend**: Node.js 18, TypeScript, Express-like routing, Docker, and environment variable configuration. -- **Database/Storage**: In-memory state management with optional persistence via cacheRepository. -- **Testing**: Vitest, React Testing Library, and Jest-like utilities. +| Layer | Technology | +| --- | --- | +| Frontend framework | React 18, TypeScript (strict), Vite 7 | +| Graph canvas | `@xyflow/react` v12 (React Flow) | +| State management | Zustand 5 (command/reducer pattern) | +| Styling | Tailwind CSS + Shadcn UI (`@radix-ui/*`) | +| Rich text | BlockNote 0.36 with custom blocks | +| Templating | Nunjucks | +| Graph traversal | `graphology` + `graphology-traversal` | +| Routing | React Router v6 | +| Backend | Node.js 20+, TypeScript, Express 4 | +| AI SDK | Vercel AI SDK v4 + `@ai-sdk/openai` | +| Testing | Vitest, React Testing Library | -## 3. High-Level Structure +--- -The workspace is divided into two primary directories: +## 3. Project Structure -- **frontend/**: Contains the React application, component library, pages, and styling. -- **backend/**: Contains server-side code, services, repositories, models, and middleware. +```text +frontend/src/ +├── main.tsx # Entry: registers node types & config types, mounts app +├── app/ +│ ├── canvas/ # CanvasPage, canvasStore (Zustand), canvasStore.test.ts +│ ├── kosmos/ # KosmosPage, sidebar, AI settings, KosmosContext +│ └── recollections/ +│ ├── RecollectionsPage # Workspace list +│ ├── RecollectionLayout # Shared layout (sidebar + outlet) +│ ├── flux/ # Flux view (graph canvas) +│ ├── logos/ # Logos view (BlockNote rich text) +│ └── katalogos/ # Katalogos view (artifact gallery) +├── components/ +│ ├── graph/ # AnimatedEdge, BaseNode, handles, keyboard shortcuts +│ ├── nodes/ # One folder per node type +│ └── ui/ # Shadcn primitives +├── hooks/ # useGraphStateWithHistory, useResizeHeight, … +└── lib/ + └── graph/ + ├── nodeRegistry.ts # Mutable plugin registry + ├── registerBuiltinNodes.tsx + ├── nodeTypeBuilder.ts # Fluent builder for node descriptors + ├── configTypes.ts # Output type registry (plantuml, markdown, wireframe) + ├── rendering.ts # 3-step rendering pipeline contract + ├── state.ts # Graph state types + └── canvasStore/ # Reducer + selectors -### 3.1 Frontend Modules +backend/src/ +├── index.ts # Express app, CORS, routes, error handler +├── routes/agentRoutes.ts # POST /api/agent, POST /api/agent/stream +├── services/agentService.ts # LLM call logic (Vercel AI SDK) +├── repositories/cacheRepository.ts +├── models/index.ts +└── middleware/rateLimiter.ts +``` -| Module | Purpose | Key Files | -|--------|---------|-----------| -| **canvas** | Core graph/canvas rendering, nodes, edges, and interactive graph features. | `src/app/canvas/*`, `src/components/graph/*`, `src/components/nodes/*` | -| **recollections** | Collection management, browsing, editing, and related UI. | `src/app/recollections/*`, `src/components/ui/*` | -| **kosmos** | Knowledge organization system, settings, and user preferences. | `src/app/kosmos/*` | -| **layout** | Layout and navigation components shared across pages. | `src/app/recollections/layout/*` | -| **components** | Reusable UI components (buttons, dialogs, avatars, etc.). | `src/components/ui/*` | -| **hooks** | Custom React hooks for state, effects, and utilities. | `src/hooks/*` | -| **lib** | Shared utilities, types, and low-level helpers. | `src/lib/*` | +--- -### 3.2 Backend Modules +## 4. Key Architectural Patterns -| Module | Purpose | Key Files | -|--------|---------|-----------| -| **services** | Business logic, external API interactions, and complex computations. | `backend/src/services/*` | -| **repositories** | Data access abstraction, caching, and persistence. | `backend/src/repositories/*` | -| **models** | TypeScript interfaces and type definitions for domain objects. | `backend/src/models/*` | -| **routes** | HTTP route definitions and controllers. | `backend/src/routes/*` | -| **middleware** | Request processing pipeline (e.g., rate limiting, authentication). | `backend/src/middleware/*` | -| **index.ts** | Application entry point that composes services, routes, and middleware. | `backend/src/index.ts` | +### 4.1 Command / Reducer Store (canvasStore) -## 4. Data Flow +The Zustand canvas store uses a pure reducer (`canvasStoreReducer`) with a discriminated union of `CanvasCommand` types. All mutations go through `dispatch(command)`. In dev mode every command is logged to the console. -1. **User Interaction** (React components) → dispatches actions → updates local state (Zustand) → triggers re-render. -2. **State Changes** may trigger async calls to **services** (frontend) → which call **backend APIs** → responses are cached via **cacheRepository**. -3. **Backend** processes requests via **routes**, applies **middleware**, interacts with **repositories** and **models**, and returns JSON responses. -4. **Caching** layer reduces repeated expensive computations or DB queries, improving performance. +The store has three slices: -## 5. Component Interaction Patterns +- **`graph`** — nodes and edges (the React Flow state) +- **`path`** — trigger pulse for edge animation +- **`ui`** — renaming state, fullscreen node, pending connection -- **Container Components** (e.g., `CanvasPage`, `RecollectionPage`) manage layout and state provision. -- **Presentational Components** (e.g., `TreeBrowser`, `AgentNode`) focus on UI rendering only. -- **Custom Hooks** encapsulate logic (e.g., `useResizeHeight`, `useStreamingContent`) and are reused across components. -- **Context Providers** (`KosmosContext`, `RecollectionSidebarContext`) supply global state to deeply nested component trees. +### 4.2 Delta-based Undo / Redo -## 6. Extensibility & Plug‑in Architecture +`useGraphStateWithHistory` computes *inverse deltas* (only the nodes/edges that changed) rather than snapshotting the full graph. Maximum 100 history entries. Drag operations commit a pre-drag snapshot on `onNodeDragStop`. -- **Node Types** are registered via `nodeRegistry.ts` and extended through descriptor files. -- **New node types** can be added by creating a descriptor, implementing rendering logic, and registering it in `registerBuiltinNodes.tsx`. -- **Extensibility points** are documented in `NODE_TYPE_EXTENSIBILITY_PROPOSAL.md`. +### 4.3 Node Plugin Registry -## 7. Performance Considerations +Node types are registered via a mutable registry in `nodeRegistry.ts`. The built-ins are registered at app init from `registerBuiltinNodes.tsx`. Adding a new node type requires: -- **Large Render Trees**: `CanvasPage` and `RecollectionPage` handle thousands of nodes; memoization (`useMemo`, `useCallback`) and lazy loading are essential. -- **State Updates**: Prefer granular state updates in `canvasStore` to avoid full tree re-renders. -- **Code Splitting**: Dynamic imports are used for heavy modules (e.g., rendering views, graph utilities). +1. Implement a React component for the node. +2. Create a descriptor using `NodeTypeBuilder`. +3. Call `registerNodeType(descriptor)` — no edits to core code. -## 8. Contribution Workflow +### 4.4 Fluent Builder for Node Descriptors -1. Fork the repository and clone the workspace. -2. Install dependencies: `pnpm install` (or `npm ci`). -3. Run the dev server: `pnpm dev` (frontend) and `pnpm start` (backend). +`NodeTypeBuilder` provides a type-safe method-chaining API (`idPrefix`, `withInputOutput`, `classification`, `allowedSourceTypes`, `help`, `menu`, `withFullscreen`, `sourceRenderingLogic`, …). Required fields are validated at `.build()`. + +### 4.5 Three-Step Rendering Pipeline + +Defined as an explicit contract in `rendering.ts`: + +1. **Resolve** — source node (Config/Agent) produces `{ resolved, outputTypeId, reasoning? }` +2. **Render** — output type handler converts the resolved string (PlantUML → Kroki SVG, Markdown → HTML, Wireframe → SVG) +3. **Display** — the Rendering node shows an image viewport or scrollable HTML + +### 4.6 Node Classification System + +Four metaphorical classes drive the create menu grouping: + +| Class | Node type | Metaphor | +| --- | --- | --- | +| `psyche` | Variable | source / input | +| `physis` | Data (CSV) | physical data | +| `pneuma` | Config / Render | transformation | +| `archon` | Agent | AI / LLM | + +### 4.7 Recollections (Workspaces) + +Each workspace (recollection) is stored in `localStorage`: + +| Key | Content | +| --- | --- | +| `zui_platform_recollections` | Metadata index (id, name, icon, timestamps) | +| `zui_graph_` | Flux graph (nodes + edges) | +| `zui_logos_` | BlockNote rich text document | +| `zui_render_cache_` | Rendered artifact cache | + +Legacy key prefix `emanations` is migrated to `recollections` on first load. + +### 4.8 User-configurable AI Connection + +`KosmosContext` persists `aiConnection` (provider, baseURL, model, apiKey) in `localStorage` under `zui_ai_connection`. The Agent node forwards this to the backend with each request, so users can switch between OpenAI and local LLMs (LM Studio, Ollama) from the UI without env variable changes. + +### 4.9 Diagram Rendering via Kroki + +PlantUML diagrams are rendered by proxying to `https://kroki.io` via `/api/kroki/plantuml/svg`. This proxy is configured in both the Vite dev server and Nginx production config. Timeout: 15 seconds. + +--- + +## 5. Data Flow + +```text +User interaction + → dispatch(CanvasCommand) [canvasStore reducer] + → Zustand state update [graph / path / ui slices] + → React re-render [React Flow nodes/edges] + +Rendering node triggered + → Resolve source node [Config/Agent → resolved string] + → Render output type [plantuml/markdown/wireframe] + → Kroki proxy (PlantUML) [GET /api/kroki/plantuml/svg] + → Agent backend (AI) [POST /api/agent or /api/agent/stream] + → Display artifact [image / HTML / SVG viewport] + +Agent node (streaming) + → POST /api/agent/stream [Express → agentService] + → Vercel AI SDK streams SSE [backend → frontend] + → useStreamingContent hook consumes [updates node data] +``` + +--- + +## 6. Backend Architecture + +The backend is intentionally thin and stateless. It has three responsibilities: + +1. **Proxy AI calls** — forwards prompts to the configured LLM (OpenAI or OpenAI-compatible). +2. **Stream responses** — `POST /api/agent/stream` uses `pipeTextStreamToResponse` from the Vercel AI SDK. +3. **Health check** — `GET /health` for Docker/orchestration readiness probes. + +The `InMemoryCache` and `rateLimiter` middleware are implemented but not yet wired into the agent routes. + +--- + +## 7. Contribution Workflow + +1. Fork the repository and clone locally. +2. Install dependencies: `cd frontend && npm install` and `cd backend && npm install`. +3. Run the dev servers (see README). 4. Create a feature branch: `git checkout -b feat/your-feature`. -5. Follow the **Coding Standards** (see `CONTRIBUTING.md` for details). -6. Submit a Pull Request with: - - Descriptive title. - - Linked issue(s). - - Updated tests (if applicable). - - Documentation updates (if relevant). -7. Code Review Checklist: - - [ ] Readability: clear naming, minimal nesting. - - [ ] Maintainability: extracted utilities, JSDoc, TypeScript typings. - - [ ] Performance: no unnecessary re-renders, proper memoization. - - [ ] Accessibility: semantic HTML, ARIA attributes. - - [ ] Security: no hardcoded secrets, proper input validation. +5. Follow the Coding Standards (see [CONTRIBUTING.md](CONTRIBUTING.md)). +6. Submit a Pull Request with a descriptive title, linked issue(s), updated tests, and documentation changes if relevant. -## 9. Coding Standards +### PR Review Checklist -- **TypeScript**: Strict mode (`strict`, `noImplicitAny`, `noUnusedVars`). +- [ ] Readability: clear naming, minimal nesting +- [ ] TypeScript: no `any`, strict types throughout +- [ ] Performance: no unnecessary re-renders, proper memoization +- [ ] Accessibility: semantic HTML, ARIA attributes where needed +- [ ] Security: no hardcoded secrets, input validated at boundaries + +--- + +## 8. Coding Standards + +- **TypeScript**: strict mode (`strict`, `noImplicitAny`, `noUnusedLocals`). - **Naming**: PascalCase for components, camelCase for functions/variables, UPPER_SNAKE_CASE for constants. -- **File Structure**: Feature‑first (`features//`) or domain‑grouped (`src/app/recollections/...`). -- **Documentation**: JSDoc for all public APIs, TSDoc for types, and inline comments for complex logic. -- **Formatting**: Prettier configured via `frontend/prettier.config.cjs`. +- **File structure**: domain-grouped under `src/app//` and `src/components//`. +- **Formatting**: Prettier via `frontend/prettier.config.cjs`; line length ≤ 120. +- **Comments**: JSDoc for public APIs; inline only where logic is non-obvious. -## 10. Quick Reference for AI Agents +--- -- **Key Entry Points**: `frontend/src/main.tsx`, `backend/src/index.ts`. -- **State Management**: `src/lib/graph/state.ts`, `src/app/canvas/canvasStore.*`. -- **Routing**: `backend/src/routes/agentRoutes.ts`. -- **Caching**: `backend/src/repositories/cacheRepository.ts`. -- **Performance Hotspots**: `CanvasPage.tsx`, `RecollectionPage.tsx`, large utility functions in `src/lib/*`. +## 9. Quick Reference for AI Agents -*This overview is intended to serve as a living document; future sections will expand on each module, performance audit findings, and junior developer quickstart guides.* +| Topic | Location | +| --- | --- | +| Entry point | `frontend/src/main.tsx` | +| Canvas store | `frontend/src/app/canvas/canvasStore.ts` | +| Store reducer | `frontend/src/app/canvas/canvasStoreReducer.ts` | +| Node registry | `frontend/src/lib/graph/nodeRegistry.ts` | +| Node builder | `frontend/src/lib/graph/nodeTypeBuilder.ts` | +| Rendering pipeline | `frontend/src/lib/graph/rendering.ts` | +| Built-in node registration | `frontend/src/lib/graph/registerBuiltinNodes.tsx` | +| AI settings context | `frontend/src/app/kosmos/KosmosContext.tsx` | +| Backend entry | `backend/src/index.ts` | +| Agent service | `backend/src/services/agentService.ts` | + +*This document is a living reference; update it when architectural decisions change.* diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4c4218e..80db775 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,49 +2,60 @@ ## 1. Coding Standards -- Use TypeScript with `strict` mode enabled. -- Follow the import order: core, library, project, then relative. +- Use TypeScript with `strict` mode enabled; no `any` types. +- Import order: core libs → third-party → project aliases (`@/`) → relative. - Naming: PascalCase for components, camelCase for functions/variables, UPPER_SNAKE_CASE for constants. -- Add JSDoc comments for all public APIs. -- Keep line length <= 120 characters, use Prettier for formatting. -- Lint with `pnpm lint` and fix warnings. +- Add JSDoc comments for all public APIs and exported types. +- Line length ≤ 120 characters; use Prettier for formatting (`frontend/prettier.config.cjs`). +- Run `npm run lint` and fix all warnings before opening a PR. ## 2. Branch Workflow - Create a feature branch: `git checkout -b feat/` -- Keep branches up to date with `main` via `git rebase` or `git merge --ff-only`. +- Keep branches up to date with `main` via `git rebase`. - Submit a Pull Request with a clear title and description. -- Ensure PR passes CI (tests, lint, type-check). +- Ensure the PR passes CI (tests, lint, type-check). - Address review comments promptly. ## 3. Running Tests -- Install dependencies: `pnpm install`. -- Run tests in watch mode: `pnpm test --watch`. -- Run tests with coverage: `pnpm test --coverage`. -- Run lint: `pnpm lint`. -- Run type-check: `pnpm typecheck`. +```bash +# Install dependencies +cd frontend && npm install + +# Run tests in watch mode +npm run test + +# Run tests once (CI) +npm run test:run + +# Type-check +npx tsc --noEmit +``` ## 4. Making Your First Contribution 1. Pick a beginner-friendly issue labeled `good first issue`. -2. Follow the steps in the Quickstart guide. -3. Make a small change (e.g., fix typo, add JSDoc). -4. Run the relevant tests. -5. Commit and push, then open a PR. +2. Read [ARCHITECTURE.md](ARCHITECTURE.md) to understand the relevant module. +3. Make a small, focused change; avoid refactoring unrelated code. +4. Run the relevant tests and confirm they pass. +5. Commit and push, then open a PR with a clear description of what and why. -## 5. Where to Extend (AI Agent Guidance) +## 5. Where to Extend -- **Canvas rendering**: Extend `src/app/canvas/CanvasPage.tsx` by adding new node types or customizing the context menu. -- **Recollection management**: Modify `src/app/recollections/RecollectionsPage.tsx` to add new views or actions. -- **State management**: Update `src/app/canvas/canvasStore.ts` for new graph features. -- **Performance**: Optimize expensive renders in `src/app/recollections/RecollectionsPage.tsx` using memoization. +| Goal | Where to look | +| --- | --- | +| Add a new node type | `src/lib/graph/nodeTypeBuilder.ts` + `registerBuiltinNodes.tsx` | +| Add a new output/render type | `src/lib/graph/configTypes.ts` + `rendering.ts` | +| Change canvas state shape | `src/app/canvas/canvasStore.ts` + reducer + tests | +| Add a new Logos block | `src/app/recollections/logos/logosSchema.ts` | +| Add a backend endpoint | `backend/src/routes/` → `services/` → `index.ts` | ## 6. Useful Links -- [ARCHITECTURE.md](ARCHITECTURE.md) – High-level architecture overview. -- [PERFORMANCE_IMPROVEMENTS.md](PERFORMANCE_IMPROVEMENTS.md) – Performance improvement plan. -- [QUICKSTART_FOR_JUNIORS.md](QUICKSTART_FOR_JUNIORS.md) – Junior developer quickstart. -- [CODE_REVIEW_CHECKLIST.md](CODE_REVIEW_CHECKLIST.md) – Checklist for reviewers. +- [ARCHITECTURE.md](ARCHITECTURE.md) — architecture overview, key patterns, module map +- [PERFORMANCE_IMPROVEMENTS.md](PERFORMANCE_IMPROVEMENTS.md) — bottleneck analysis and improvement plan +- [docs/CODE_REVIEW_CHECKLIST.md](docs/CODE_REVIEW_CHECKLIST.md) — PR review checklist +- [docs/NODE_TYPE_EXTENSIBILITY_PROPOSAL.md](docs/NODE_TYPE_EXTENSIBILITY_PROPOSAL.md) — node plugin system design *Thank you for contributing!* diff --git a/PERFORMANCE_IMPROVEMENTS.md b/PERFORMANCE_IMPROVEMENTS.md index 8f98002..744071c 100644 --- a/PERFORMANCE_IMPROVEMENTS.md +++ b/PERFORMANCE_IMPROVEMENTS.md @@ -1,70 +1,108 @@ # Performance Improvements Plan -## 1. Executive Summary +## 1. Current State -This document outlines identified performance bottlenecks in the ZUI application and proposes concrete actions to improve render efficiency, state management, and code splitting. The plan is targeted at enabling junior developers to contribute measurable performance gains while maintaining code quality. +The core rendering and state management architecture is already well-structured for performance: -## 2. Current Performance Issues +- **Command/reducer store** — all mutations go through a pure reducer; individual selectors can prevent unnecessary re-renders. +- **Delta-based undo/redo** — only diffs are stored, not full graph snapshots (max 100 entries). +- **Plugin node registry** — node types are loaded once at app init, not dynamically on each render. +- **Streaming AI responses** — `POST /api/agent/stream` uses SSE so the UI updates incrementally. -| Issue | Location | Impact | Root Cause | -|-------|----------|--------|------------| -| **Large Component Renders** | `frontend/src/app/canvas/CanvasPage.tsx`, `frontend/src/app/recollections/RecollectionPage.tsx` | High CPU usage, frame drops | These components render thousands of nodes without memoization; state updates trigger full re-renders. | -| **Unmemoized Expensive Calculations** | Various utility functions in `src/lib/*` (e.g., graph layout calculations) | Delayed response to user interactions | Calculations recomputed on every render cycle. | -| **Frequent State Updates** | `canvasStore` mutations in response to mouse/touch events | Unnecessary re-renders of unrelated nodes | State updates not granular; multiple mutations in quick succession. | -| **Lack of Code Splitting** | Heavy modules imported globally (e.g., rendering views, graph utilities) | Initial bundle size > 2MB, slow load | All modules loaded upfront even if not used. | -| **Inefficient List Rendering** | Lists of nodes in sidebar components | Scrolling lag | No virtualization; all items rendered simultaneously. | -| **Repeated API Calls** | Agent service calls without proper caching | Latency spikes | Calls bypass `cacheRepository` in some paths. | +The sections below identify remaining bottlenecks and concrete next steps. + +--- + +## 2. Known Bottlenecks + +| # | Issue | Location | Impact | Root Cause | +| --- | --- | --- | --- | --- | +| 1 | Unguarded re-renders on canvas | `CanvasPage.tsx` | Frame drops on large graphs | Components not subscribed to granular store slices | +| 2 | Nunjucks template resolution on every render | `rendering.ts` resolve step | Slow Config node updates | No memoization of template output keyed to input hash | +| 3 | Kroki SVG requests not deduplicated | `rendering.ts` render step | Redundant network calls | No in-flight request deduplication or client-side cache | +| 4 | Sidebar tree renders all items | `KosmosPage` recollection tree | Scrolling lag with many workspaces | No list virtualization | +| 5 | Backend cache not wired to agent routes | `agentRoutes.ts` | Repeated identical LLM calls | `InMemoryCache` and `rateLimiter` exist but are unused | +| 6 | Full graph serialized to localStorage on every change | `useGraphStateWithHistory` | Storage I/O on every keypress | No debounce on the persistence write | +| 7 | Initial bundle size | Vite build | Slow first load | Heavy deps (BlockNote, React Flow, Nunjucks) loaded eagerly | + +--- ## 3. Recommended Improvements -### 3.1 Component Refactoring +### 3.1 Granular Store Subscriptions -- **Split `CanvasPage` and `RecollectionPage`** into smaller, feature‑specific sub‑components. -- **Extract pure logic** (e.g., node layout calculations) into standalone utility functions with `useMemo`. -- **Apply `React.memo`** to pure presentational components that receive static props. +Zustand supports slice-level subscriptions. Node components should select only their own data slice: -### 3.2 State Management Optimization +```ts +// Instead of subscribing to the entire graph: +const node = useCanvasStore(s => s.graph.nodes.find(n => n.id === id)) +``` -- **Granular State Updates**: Use `canvasStore` selectors to update only the affected node slices. -- **Batch Updates**: Wrap multiple mutations in `runWithTiming` or `unstable_batchedUpdates` to reduce render cycles. +This prevents all nodes from re-rendering when a single node changes. -### 3.3 Memoization & Lazy Loading +### 3.2 Memoize Template Resolution -- **Memoize Callbacks**: Replace inline event handlers with `useCallback` references stored in context or hooks. -- **Dynamic Imports**: Use `import()` for heavy modules (e.g., `RenderingNode`, `AnimatedEdge`) to split the bundle. -- **Virtualized Lists**: Integrate `react-window` or `react-virtualized` for large node lists in sidebars. +Cache the Nunjucks resolution output keyed to a hash of the template source plus variable inputs. Invalidate only when those inputs change: -### 3.4 Code Splitting & Bundle Optimization +```ts +const resolved = useMemo( + () => resolveTemplate(template, variables), + [templateHash, variableHash] +) +``` -- **Remove Unused Dependencies**: Audit `package.json` for deprecated libraries. -- **Enable `vite-plugin-dynamic-import`** for on‑demand loading of route‑specific components. -- **Compress Assets**: Configure `gzip`/`brotli` in `nginx.conf` for large SVG and texture assets. +### 3.3 Deduplicate Kroki Requests -### 3.5 Caching Strategy Enhancements +Add a simple in-flight map in the render step: if a request for the same PlantUML source is already pending, reuse its promise. Cache successful responses keyed to the source string with a short TTL (e.g. 5 minutes). -- **Centralize Caching**: Ensure all external API calls route through `cacheRepository`. -- **Add TTL** to cached responses to avoid stale data while still reducing repeat calls. +### 3.4 Wire Backend Cache and Rate Limiter -### 3.6 Testing & Verification +`InMemoryCache` and `rateLimiter` middleware are implemented in `backend/src/`. Connect them to `agentRoutes.ts`: -- **Performance Tests**: Add Vitest benchmarks for render times using `performance.now()`. -- **Profile with React DevTools**: Capture flame graphs before and after each optimization. -- **CI Gate**: Enforce that PRs must include a performance regression test if changes affect rendering. +1. Add cache lookup before calling the AI service. +2. Store the response on cache miss. +3. Apply rate limiting per IP to prevent abuse. -## 4. Contribution Path for Junior Developers +### 3.5 Debounce localStorage Writes -1. **Familiarize** with the `canvasStore` architecture and the `CanvasPage` component structure. -2. **Pick** a low‑risk optimization (e.g., memoizing a utility function). -3. **Implement** the change, add JSDoc comments, and write a simple benchmark. -4. **Submit** a PR with: - - Description of the performance gain. - - Updated tests/benchmarks. - - Documentation in `PERFORMANCE_IMPROVEMENTS.md`. +Wrap the graph persistence call in a debounce (e.g., 300 ms) to avoid a write on every keystroke or node drag. The delta-based history already computes minimal diffs; the bottleneck is the frequency of writes. -## 5. Success Metrics +### 3.6 Virtualize the Sidebar Tree -- **Target**: Reduce average frame time from ~16 ms to < 10 ms for `CanvasPage`. -- **Bundle Size**: Decrease initial load by ≥ 15 %. -- **API Latency**: Cut repeated call overhead by ≥ 30 %. +Integrate `react-arborist` (already installed) with virtualization enabled for the recollection sidebar when item count exceeds a threshold (~50). -*This plan is living; subsequent sections will track progress and update targets.* +### 3.7 Code Split Heavy Routes + +Add lazy imports for the three heavy route components so the initial bundle only loads what the user navigates to: + +```ts +const FluxRoute = lazy(() => import('./app/recollections/flux/FluxRoute')) +const LogosPage = lazy(() => import('./app/recollections/logos/LogosPage')) +const KatalogosPage = lazy(() => import('./app/recollections/katalogos/KatalogosPage')) +``` + +### 3.8 Enable Brotli Compression in Nginx + +Add brotli/gzip compression to `frontend/nginx.conf` for JS, CSS, and SVG assets. This can cut transfer size by 60–70% for the JS bundle. + +--- + +## 4. Success Metrics + +| Metric | Current (estimated) | Target | +| --- | --- | --- | +| Frame time on 50-node canvas | ~16 ms | < 10 ms | +| Initial JS bundle (gzipped) | ~800 KB | < 600 KB | +| Repeated identical LLM calls | uncached | 0 network round-trips | +| localStorage write frequency | every change | debounced 300 ms | + +--- + +## 5. Contribution Path + +1. Read [ARCHITECTURE.md](ARCHITECTURE.md) to understand the module you're optimizing. +2. Pick one item from section 2. +3. Add a Vitest benchmark (`performance.now()` before/after) alongside your change. +4. Submit a PR with the benchmark results in the description and update this file's "Current" column. + +*This plan is a living document; update the metrics table when improvements land.* diff --git a/README.md b/README.md index 7010fa4..097722c 100644 --- a/README.md +++ b/README.md @@ -1,42 +1,61 @@ # Zui -Node-based editor (React Flow) for configs, variables, and rendering (PlantUML, Markdown, etc.). Optional Node.js backend API for demos or future features (e.g. todos CRUD). +Node-based visual editor (React Flow) for composing configs, variables, templates, and AI-generated content. Three views per workspace: **Flux** (graph canvas), **Logos** (rich text), and **Katalogos** (artifact gallery). Optional Node.js backend for AI agent calls. ## Project layout -``` -my-app/ -├── frontend/ # React app (Vite, TypeScript) -│ ├── Dockerfile # Multi-stage for prod (build → Nginx) -│ ├── Dockerfile.dev # Dev with hot reload +```text +zui/ +├── frontend/ # React SPA (Vite, TypeScript) │ ├── src/ -│ ├── public/ +│ │ ├── main.tsx # Entry point: registers node/config types, mounts React +│ │ ├── app/ +│ │ │ ├── canvas/ # Core graph editor (React Flow + Zustand) +│ │ │ ├── kosmos/ # Platform shell: sidebar, recollection list, AI settings +│ │ │ └── recollections/ # Logos (rich text), Flux (canvas), Katalogos (gallery) +│ │ ├── components/ +│ │ │ ├── graph/ # AnimatedEdge, BaseNode, handles, keyboard shortcuts +│ │ │ ├── nodes/ # One folder per node type (agent, config, variable, …) +│ │ │ └── ui/ # Shadcn-based primitives +│ │ ├── hooks/ # Custom React hooks +│ │ └── lib/ +│ │ └── graph/ # Registry, types, state, rendering pipeline, Nunjucks utils +│ ├── Dockerfile # Multi-stage: Vite build → Nginx +│ ├── Dockerfile.dev # Dev with hot reload │ ├── nginx.conf │ └── package.json -├── backend/ # Node.js/Express API -│ ├── Dockerfile +├── backend/ # Node.js/Express API │ ├── src/ -│ │ └── index.js +│ │ ├── index.ts # Express entry: registers routes, CORS, error handler +│ │ ├── routes/agentRoutes.ts +│ │ ├── services/agentService.ts +│ │ ├── repositories/cacheRepository.ts +│ │ ├── models/index.ts +│ │ └── middleware/rateLimiter.ts │ └── package.json +├── docs/ +│ ├── ARCHITECTURE.md +│ ├── PERFORMANCE.md +│ ├── NODE_TYPE_EXTENSIBILITY_PROPOSAL.md +│ └── CODE_REVIEW_CHECKLIST.md ├── docker-compose.yml -├── .dockerignore └── .gitignore ``` -Ignored by git: `node_modules`, `dist`, `.env`, `.env.*` (see [.gitignore](.gitignore)). Local Docker overrides: `docker-compose.override.yml` (optional, not committed). +Ignored by git: `node_modules`, `dist`, `.env`, `.env.*`. Local Docker overrides: `docker-compose.override.yml` (optional, not committed). --- ## Run locally (dev) -**Frontend only:** +**Frontend only** (no AI agent): ```bash cd frontend && npm install && npm run dev # → http://localhost:3000 ``` -**Frontend + backend** (for AI agent and health): +**Frontend + backend** (for AI agent and health check): ```bash # Terminal 1 – backend @@ -45,7 +64,7 @@ cd backend && npm install && npm run dev # Terminal 2 – frontend cd frontend && npm install && npm run dev -# → http://localhost:3000 (Vite proxies /api/* and /health to backend) +# → http://localhost:3000 (Vite proxies /api/* and /health to backend) ``` --- @@ -61,77 +80,74 @@ docker compose up --build Environment variables (backend service): -| Variable | Default | Description | -|-------------|----------------------------|-------------| -| `PORT` | `8080` | Backend listen port. | -| `CORS_ORIGIN` | `http://localhost:3000` | Allowed origin for CORS. | +| Variable | Default | Description | +|------------------|---------------------------|--------------------------------------------| +| `PORT` | `8080` | Backend listen port. | +| `CORS_ORIGIN` | `http://localhost:3000` | Allowed origin for CORS. | +| `AI_BASE_URL` | *(unset)* | OpenAI-compatible base URL (local LLMs). | +| `AI_MODEL` | `gpt-4o-mini` | Model ID for the AI agent. | +| `OPENAI_API_KEY` | *(unset)* | Required when using OpenAI directly. | -For self-hosting (e.g. Tailscale/HTTPS): set `CORS_ORIGIN` to your frontend URL; put Caddy or Nginx in front for TLS if needed. - -**Dev with Docker (frontend hot reload):** use `frontend/Dockerfile.dev` and mount `./frontend` as a volume, or run `cd frontend && npm run dev` locally. +For self-hosting (e.g., Tailscale/HTTPS): set `CORS_ORIGIN` to your frontend URL and put Caddy or Nginx in front for TLS. --- ## Scripts -## Contribute - -For contribution guidelines, see: - -- [ARCHITECTURE.md](ARCHITECTURE.md) -- [PERFORMANCE_IMPROVEMENTS.md](PERFORMANCE_IMPROVEMENTS.md) -- [QUICKSTART_FOR_JUNIORS.md](QUICKSTART_FOR_JUNIORS.md) -- [CONTRIBUTING.md](CONTRIBUTING.md) -- [CODE_REVIEW_CHECKLIST.md](CODE_REVIEW_CHECKLIST.md) - -*Thank you for contributing!* - -| Command | Description | -|--------|-------------| -| `cd frontend && npm run dev` | Vite dev server. | -| `cd frontend && npm run build` | Build frontend for production. | -| `cd frontend && npm run preview` | Preview production build. | -| `cd backend && npm run dev` | Backend with `--watch`. | -| `cd backend && npm start` | Backend production run. | -| `docker compose up --build` | Run frontend + backend in Docker. | +| Command | Description | +|--------------------------------------|----------------------------------------| +| `cd frontend && npm run dev` | Vite dev server (port 3000). | +| `cd frontend && npm run build` | Build frontend for production. | +| `cd frontend && npm run preview` | Preview production build. | +| `cd frontend && npm run test` | Run tests in watch mode (Vitest). | +| `cd frontend && npm run test:run` | Run tests once (CI). | +| `cd backend && npm run dev` | Backend with `tsx --watch`. | +| `cd backend && npm start` | Backend production run. | +| `docker compose up --build` | Run full stack in Docker. | --- -## Backend API (no DB) +## Backend API -| Method | Path | Description | -|--------|------|-------------| -| GET | `/health` | Health check (e.g. for Docker). | -| POST | `/api/agent` | Run AI agent; body `{ "prompt", "context?", "contextNodes?" }` → `{ "markdown" }`. | +| Method | Path | Body / Response | +|--------|-----------------------|---------------------------------------------------------------------------------| +| GET | `/health` | `{ ok: true, timestamp: number }` — health check for Docker/orchestration. | +| POST | `/api/agent` | Body `{ prompt, context?, contextNodes? }` → `{ markdown }` (one-shot). | +| POST | `/api/agent/stream` | Body `{ prompt, context?, contextNodes? }` → SSE text stream (streaming). | --- ## Agent node (local LLM or OpenAI) -The **Agent** node uses an OpenAI-compatible API. You can use: +AI connection settings are configured **directly in the UI** — open the sidebar and go to **AI Settings**. You can switch between providers without restarting the server. -**1. Local LLM (e.g. LM Studio)** +The backend reads its AI config from environment variables as a fallback: -1. Install [LM Studio](https://lmstudio.ai/) and load a model. -2. Start the local server: in LM Studio open the **Developer** tab and run the **Local Server** (default: `http://localhost:1234`). -3. In the project root or `backend/`, set: +### Local LLM (e.g. LM Studio) + +1. Install [LM Studio](https://lmstudio.ai/), load a model, and start the local server (default: `http://localhost:1234`). +2. Set env vars (backend): ```bash export AI_BASE_URL=http://localhost:1234/v1 - # Optional: set to the model name shown in LM Studio (e.g. the loaded model id). Default is "local-model". - export AI_MODEL=your-model-name + export AI_MODEL=your-model-name # optional; matches the name shown in LM Studio ``` -4. Start the backend (`cd backend && npm run dev`). The Agent node will use your local model. +### OpenAI -**2. OpenAI** +```bash +export OPENAI_API_KEY=sk-... +export AI_MODEL=gpt-4o-mini # optional; defaults to gpt-4o-mini +``` -Set `OPENAI_API_KEY` to your API key. The backend will use `gpt-4o-mini` unless you set `AI_MODEL`. +--- -**Env summary (backend)** +## Contribute -| Variable | When to use | Description | -|----------|--------------|-------------| -| `AI_BASE_URL` | Local LLM (LM Studio, Ollama, etc.) | OpenAI-compatible base URL, e.g. `http://localhost:1234/v1`. | -| `AI_MODEL` | Optional | Model id (for local: use the name shown in LM Studio; for OpenAI: e.g. `gpt-4o-mini`). | -| `OPENAI_API_KEY` | OpenAI only | Your OpenAI API key. Not required when using `AI_BASE_URL` only. | +- [ARCHITECTURE.md](ARCHITECTURE.md) — architecture overview, key patterns, module map +- [CONTRIBUTING.md](CONTRIBUTING.md) — coding standards, branch workflow, test commands +- [PERFORMANCE_IMPROVEMENTS.md](PERFORMANCE_IMPROVEMENTS.md) — bottleneck analysis and improvement plan +- [docs/CODE_REVIEW_CHECKLIST.md](docs/CODE_REVIEW_CHECKLIST.md) — PR review checklist +- [docs/NODE_TYPE_EXTENSIBILITY_PROPOSAL.md](docs/NODE_TYPE_EXTENSIBILITY_PROPOSAL.md) — node plugin system design + +*Thank you for contributing!* diff --git a/frontend/src/app/canvas/useCanvasGraph.ts b/frontend/src/app/canvas/useCanvasGraph.ts index 845263f..a60fcea 100644 --- a/frontend/src/app/canvas/useCanvasGraph.ts +++ b/frontend/src/app/canvas/useCanvasGraph.ts @@ -3,7 +3,7 @@ * with initial graph from recollection storage (or example). Save is explicit via save(). */ -import { useCallback, useMemo, useRef, useState } from 'react' +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useGraphStateWithHistory } from '@/hooks/useGraphStateWithHistory' import { getInitialGraph } from '@/app/canvas/canvasGraphUtils' import { saveGraphToStorage, RECOLLECTION_VERSION } from '@/app/recollections/state/recollectionGraphStorage' @@ -59,5 +59,16 @@ export function useCanvasGraph(recollectionId: string | undefined): UseCanvasGra }, SAVING_DISPLAY_MS) }, [recollectionId]) + // Auto-save after 5 seconds of inactivity when there are unsaved changes. + // Prevents data loss if the user closes the tab without pressing Ctrl+S. + const AUTO_SAVE_DELAY_MS = 5000 + const saveRef = useRef(save) + saveRef.current = save + useEffect(() => { + if (!isDirty || !recollectionId) return + const timer = setTimeout(() => saveRef.current(), AUTO_SAVE_DELAY_MS) + return () => clearTimeout(timer) + }, [isDirty, recollectionId, currentSerialized]) + return { ...result, save, saveStatus } } diff --git a/frontend/src/app/recollections/layout/TreeBrowser.tsx b/frontend/src/app/recollections/layout/TreeBrowser.tsx index 0bc0a64..36466bc 100644 --- a/frontend/src/app/recollections/layout/TreeBrowser.tsx +++ b/frontend/src/app/recollections/layout/TreeBrowser.tsx @@ -14,6 +14,7 @@ */ import React, { useCallback, useMemo, useRef, useState } from 'react' +import { useResizeHeight } from '@/hooks/useResizeHeight' import { Tree, NodeApi, @@ -422,6 +423,7 @@ export function TreeBrowser() { const { tree, handleTreeChange } = useRecollectionSidebar() const [searchQuery, setSearchQuery] = useState('') const treeRef = useRef | null>(null) + const [treeContainerHeight, treeContainerRef] = useResizeHeight(600) // Build tree from flat structure const treeNodes = useTreeNodes(tree) @@ -533,7 +535,7 @@ export function TreeBrowser() { idAccessor: 'id' as const, childrenAccessor: 'children' as const, width: '100%' as const, - height: 600, + height: Math.max(treeContainerHeight, 100), rowHeight: 32, indent: ROW_INDENT_PX, renderRow: TreeRow, @@ -545,7 +547,7 @@ export function TreeBrowser() { renderCursor: LogosDropCursor, children: LogosTreeNode, }), - [filteredTree, initialOpenState, handleMove, searchQuery] + [filteredTree, initialOpenState, handleMove, searchQuery, treeContainerHeight] ) return ( @@ -599,7 +601,7 @@ export function TreeBrowser() { {/* Tree */} -
+
diff --git a/frontend/src/components/nodes/config/renderingLogic.ts b/frontend/src/components/nodes/config/renderingLogic.ts index c8ef0ad..65f5cfb 100644 --- a/frontend/src/components/nodes/config/renderingLogic.ts +++ b/frontend/src/components/nodes/config/renderingLogic.ts @@ -9,6 +9,13 @@ import type { ResolvedContentResult, SourceRenderingLogicContext } from '@/lib/g import { getConfigContent, getConfigType, getConfigTypeId, type ConfigTypeId } from '@/lib/graph/rendering' import { isReachable, resolveExtendsRef, getTemplateRefs, type NodeLike } from '@/lib/graph/templateRefs' +// Module-level cache for resolved Nunjucks output. +// Keyed by a fingerprint of all inputs (template contents + variable/data values + function bodies). +// Avoids re-running the Nunjucks environment when the same inputs are seen again (e.g. undo/redo, +// multiple rendering nodes sharing the same config, rapid edits cycling back to a prior value). +const MAX_RESOLVE_CACHE = 200 +const resolveCache = new Map() + export async function getResolvedContentForConfig(context: SourceRenderingLogicContext): Promise { const { nodes, edges, sourceNodeId, renderNodeId } = context const srcId = sourceNodeId @@ -107,6 +114,24 @@ export async function getResolvedContentForConfig(context: SourceRenderingLogicC } } + // Build cache key from all inputs that affect the resolved output. + const allTemplateContents = [...configIdsUsed] + .map((cid) => { + const node = nodes.find((n) => n.id === cid) + return getConfigContent((node?.data ?? undefined) as Record | undefined) + }) + .join('\x00') + const allFunctionBodies = [...functionIdsToRegister] + .map((fid) => { + const node = nodes.find((n) => n.id === fid) + return ((node?.data as Record)?.body as string) ?? '' + }) + .join('\x00') + const cacheKey = `${srcId}\x01${JSON.stringify(nunjucksContext)}\x01${allTemplateContents}\x01${allFunctionBodies}` + + const cached = resolveCache.get(cacheKey) + if (cached) return cached + const env = new nunjucks.Environment([configLoader], { autoescape: false }) const formatFilterResult = (r: unknown): string => { @@ -248,7 +273,13 @@ export async function getResolvedContentForConfig(context: SourceRenderingLogicC return } const resolved = afterNunjucks.replace(/(\r?\n)(\s*\r?\n)+/g, '$1').trim() - resolve({ resolved, outputTypeId }) + const result: ResolvedContentResult = { resolved, outputTypeId } + // FIFO eviction when cache is full + if (resolveCache.size >= MAX_RESOLVE_CACHE) { + resolveCache.delete(resolveCache.keys().next().value as string) + } + resolveCache.set(cacheKey, result) + resolve(result) }) }) } diff --git a/frontend/src/components/nodes/render/useRenderingNodeState.ts b/frontend/src/components/nodes/render/useRenderingNodeState.ts index ad9b4ed..a4e7d37 100644 --- a/frontend/src/components/nodes/render/useRenderingNodeState.ts +++ b/frontend/src/components/nodes/render/useRenderingNodeState.ts @@ -56,6 +56,7 @@ */ import { useCallback, useDeferredValue, useEffect, useMemo, useRef, useState } from 'react' +import { useShallow } from 'zustand/react/shallow' import { useAbstractNode } from '@/lib/graph/abstractNode' import { useSyncConnectionStatus } from '@/lib/graph/nodeLifecycle' import { useCanvasStore, dispatchCanvasCommand } from '@/app/canvas/canvasStore' @@ -175,8 +176,11 @@ export function useRenderingNodeState( // Subscribe to store graph so we re-render when any node/edge changes (e.g. ascendant data). // Context only exposes a ref, so we wouldn't re-render when another node updates otherwise. - const storeNodes = useCanvasStore((s) => s.graph.nodes) - const storeEdges = useCanvasStore((s) => s.graph.edges) + // Single combined subscription (vs two separate) halves listener overhead; useShallow prevents + // re-renders when non-graph slices (ui/path) change. + const { storeNodes, storeEdges } = useCanvasStore( + useShallow((s) => ({ storeNodes: s.graph.nodes, storeEdges: s.graph.edges })) + ) const nodes = storeNodes.length > 0 ? storeNodes : contextNodes const edges = storeEdges.length > 0 ? storeEdges : contextEdges diff --git a/frontend/src/lib/graph/configTypes.ts b/frontend/src/lib/graph/configTypes.ts index 023200f..6a5e220 100644 --- a/frontend/src/lib/graph/configTypes.ts +++ b/frontend/src/lib/graph/configTypes.ts @@ -53,6 +53,13 @@ export type ConfigType = { const KROKI_PLANTUML_SVG = '/api/kroki/plantuml/svg' const KROKI_TIMEOUT_MS = 15000 +const KROKI_CACHE_TTL_MS = 5 * 60 * 1000 // 5 minutes + +// In-flight deduplication: if the same PlantUML content is already being fetched, +// reuse the existing promise instead of firing a duplicate request. +const krokiInflight = new Map>() +// TTL result cache: avoids re-fetching identical content within the TTL window. +const krokiCache = new Map() const PLANTUML_INSERT_BLOCKS: InsertBlockOrGroup[] = [ { label: 'Diagram start/end', snippet: '@startuml\n\n@enduml' }, @@ -172,36 +179,56 @@ const BUILTIN_CONFIG_TYPES: ConfigType[] = [ language: 'plantuml', insertBlocks: PLANTUML_INSERT_BLOCKS, render: async (content: string) => { - const controller = new AbortController() - const timeoutId = setTimeout(() => controller.abort(), KROKI_TIMEOUT_MS) - try { - const res = await fetch(KROKI_PLANTUML_SVG, { - method: 'POST', - headers: { 'Content-Type': 'text/plain' }, - body: content, - signal: controller.signal, - }) - clearTimeout(timeoutId) - if (!res.ok) { - const err = await res.text() - if (res.status >= 500) { - throw new Error('Diagram service unavailable. Try again later.') - } - throw new Error(res.status === 400 ? err || 'Invalid PlantUML' : `Kroki error: ${res.status}`) - } - return res.text() - } catch (err: unknown) { - clearTimeout(timeoutId) - if (err instanceof Error) { - if (err.name === 'AbortError') { - throw new Error('Diagram request timed out. The service may be slow or unavailable.') - } - if (err.message.includes('fetch') || err.message.includes('network') || err.message.includes('Failed to fetch')) { - throw new Error('Diagram service unavailable. Check your connection or try again later.') - } - } - throw err + // 1. TTL cache hit + const cached = krokiCache.get(content) + if (cached && Date.now() - cached.cachedAt < KROKI_CACHE_TTL_MS) { + return cached.result } + + // 2. In-flight deduplication: reuse an existing request for the same content + const inflight = krokiInflight.get(content) + if (inflight) return inflight + + // 3. New request + const fetchPromise = (async () => { + const controller = new AbortController() + const timeoutId = setTimeout(() => controller.abort(), KROKI_TIMEOUT_MS) + try { + const res = await fetch(KROKI_PLANTUML_SVG, { + method: 'POST', + headers: { 'Content-Type': 'text/plain' }, + body: content, + signal: controller.signal, + }) + clearTimeout(timeoutId) + if (!res.ok) { + const err = await res.text() + if (res.status >= 500) { + throw new Error('Diagram service unavailable. Try again later.') + } + throw new Error(res.status === 400 ? err || 'Invalid PlantUML' : `Kroki error: ${res.status}`) + } + const svg = await res.text() + krokiCache.set(content, { result: svg, cachedAt: Date.now() }) + return svg + } catch (err: unknown) { + clearTimeout(timeoutId) + if (err instanceof Error) { + if (err.name === 'AbortError') { + throw new Error('Diagram request timed out. The service may be slow or unavailable.') + } + if (err.message.includes('fetch') || err.message.includes('network') || err.message.includes('Failed to fetch')) { + throw new Error('Diagram service unavailable. Check your connection or try again later.') + } + } + throw err + } finally { + krokiInflight.delete(content) + } + })() + + krokiInflight.set(content, fetchPromise) + return fetchPromise }, outputMenuDescriptor: { submenuLabel: 'Export', diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 4d31e98..20ab741 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -1,21 +1,25 @@ -import React from 'react' +import React, { lazy, Suspense } from 'react' import { createRoot } from 'react-dom/client' import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom' import { Toaster } from 'sonner' import { ThemeProvider } from './lib/themeContext' import { registerBuiltinNodes } from './lib/graph/registerBuiltinNodes' import { registerBuiltinConfigTypes } from './lib/graph/configTypes' -import { KosmosPage } from './app/kosmos/KosmosPage' -import { RecollectionsPage } from './app/recollections/RecollectionsPage' import { RecollectionLayout } from './app/recollections/RecollectionLayout' -import { LogosPage } from './app/recollections/logos/LogosPage' -import { FluxRoute } from './app/recollections/flux/FluxRoute' import { NotFoundPage } from './app/NotFoundPage' + import './lib/prismSetup' import 'prismjs/themes/prism.css' import './styles.css' import '@xyflow/react/dist/style.css' +// Lazy-load heavy route components so their bundles are fetched on first navigation, +// not at initial app load. RecollectionLayout and NotFoundPage stay eager (lightweight). +const KosmosPage = lazy(() => import('./app/kosmos/KosmosPage').then(m => ({ default: m.KosmosPage }))) +const RecollectionsPage = lazy(() => import('./app/recollections/RecollectionsPage').then(m => ({ default: m.RecollectionsPage }))) +const LogosPage = lazy(() => import('./app/recollections/logos/LogosPage').then(m => ({ default: m.LogosPage }))) +const FluxRoute = lazy(() => import('./app/recollections/flux/FluxRoute').then(m => ({ default: m.FluxRoute }))) + registerBuiltinConfigTypes() registerBuiltinNodes() @@ -23,6 +27,7 @@ createRoot(document.getElementById('root')!).render( + Loading…}> }> } /> @@ -37,6 +42,7 @@ createRoot(document.getElementById('root')!).render( } /> +