Architecture and Technology Stack¶
This page describes the React / Python / Electron application on custom-base. main tracks
upstream, while flutter maintains the separate Flutter / Rust application. Their implementations
are outside this page's scope. See Local Development for commands and
AGENTS.md for branch rules.
Technology stack¶
| Responsibility | Technology and purpose |
|---|---|
| Frontend and build | React, TypeScript, Vite; Tailwind CSS and project styles, Motion animations, Lucide icons |
| Client business state | Zustand stores for the document, settings, preview, preferences, and notifications |
| Server data | TanStack Query; native Fetch requests and Valibot validation of responses and saved formats |
| Mathematics and text conversion | KaTeX parsing and project converters; Markdown cleanup, letter layout, image placeholders |
| Local storage | localStorage + IndexedDB on the Web; a restricted Electron IPC bridge and application data files on desktop |
| Generation service | Python and FastAPI; Uvicorn in development, Gunicorn / Uvicorn Worker in production |
| Layout and export | handrightbeta, Pillow, project math glyph and layout modules; OpenCV background detection and PyMuPDF export |
| Backend tasks | A persistent SQLite queue; WebSocket updates with HTTP polling as fallback |
| Desktop and offline entry | Electron + PyInstaller; a project TypeScript Service Worker caches the Web application shell |
| Engineering and validation | Playwright, Node tests, pytest; Oxlint / Oxfmt, Ruff / Pyright, Lefthook, semantic-release |
Consult package manifests and lockfiles for exact versions. UI text uses LXGW WenKai. Text rendered on paper uses the selected handwriting font and the backend's missing-glyph fallback logic.
Module responsibilities¶
The main UI is a single workspace; help and desktop-download information open in dialogs.
Business modules are organized under features/.
| Location | Responsibility |
|---|---|
frontend/src/main.tsx |
Restore the workspace before installing persistence subscriptions, Query Provider, and business actions |
frontend/src/App.tsx, components/ |
Screen composition, forms, and interactions with selective state subscriptions |
frontend/src/shared/ |
Shared UI, icons, and animation primitives |
frontend/src/features/*/store.ts |
Domain memory state and synchronous state updates |
frontend/src/features/workspace/ |
Cross-domain document, settings, and generation actions; workspace hydration and save subscriptions |
frontend/src/features/settings/ |
Configuration parsing, legacy conversion, and the preset repository |
frontend/src/features/generation/ |
Generation operations, task sessions, Query cache subscriptions, and retry countdowns |
frontend/src/api/ |
Transport, response validation, Query options, task endpoints, and download adaptation |
frontend/src/persistence/ |
Record contracts, Web / desktop adapters, asset snapshots, and reference protection |
frontend/src/utils/, i18n.ts |
Pure text / math conversion and centralized Chinese / English strings |
frontend/worker/ |
Application-shell caching; API requests bypass offline caching |
backend/app.py |
HTTP / WebSocket routes, task scheduling, and the generation flow |
backend/task_store.py, task_types.py |
Task persistence and data contracts |
backend/symbol_fallback.py, math_*.py |
Math glyph fallback, geometry, and page layout |
backend/identify.py, pdf.py, logging_setup.py |
Background detection, PDF export, and shared logging |
desktop/main.js, preload.js |
Backend process, windows, downloads, and the isolated renderer bridge |
desktop/studio-storage.js |
Fixed-key storage IPC, source validation, atomic file writes, and queue draining |
e2e/ |
Web and real packaged-desktop validation |
The UI delegates operations to workspace actions. Actions combine stores, APIs, repositories, and resource services. APIs own transport and data contracts; adapters own persistence. Stores hold state and synchronous updates rather than requests, file writes, or object-URL creation and disposal.
State ownership¶
| State | Owner | Persistence |
|---|---|---|
| Text, image map, imported filename | Document store | Workspace snapshot and binary assets |
| Handwriting parameters, fonts, backgrounds, generation options | Settings store | Workspace snapshot; uploaded files become assets |
| Language, theme, table conversion mode | Preferences store | Workspace snapshot |
| Preview URLs, current page, download display information | Preview store | Memory only |
| Font catalog, version, task state, raw generation results | TanStack Query | Memory cache; the whole Query cache is not persisted |
| Generation task ID and export mode | Generation flow / studio.generation |
Resume observation after refresh |
| Notifications and save status | Separate stores | Memory only |
| Dialogs, accordions, selection, temporary number-input text | Component state and refs | Component lifetime |
Components subscribe through field selectors or useShallow. Editing text does not propagate a
large Context value through the page; status components subscribe to task progress separately.
Context provides the language environment and stable workspace actions. QueryClientProvider
provides server-data caching.
Generation flow¶
- Workspace actions read a document / settings snapshot and validate it. Image placeholders are expanded when building the request payload.
- The API submits a task. The backend persists it in SQLite and returns a task ID.
- The client saves the ID and export mode, then observes progress through WebSocket with HTTP polling as fallback.
- Both transports update the same Query task cache. On completion, the client fetches images, ZIP, or PDF and passes them to preview / download adapters.
- After refresh, the client resumes the saved task without another POST, provided that task remains available on the server.
Generation POSTs are not retried automatically. A full queue disables submission until the returned retry deadline. Cancellation, unmounting, or leaving the page aborts client requests and releases observation connections; it does not delete the backend task. Temporary network errors and timeouts retain task identity. Success, explicit task failure, or an unavailable task clears it. The uploaded snapshot reference is released when the POST finishes.
Local persistence¶
Separate metadata and binary assets¶
| Content | Web | Desktop |
|---|---|---|
studio.workspace, presets, active task records |
localStorage for the current origin | studio/records/ under the application data directory |
| Illustrations, uploaded fonts, paper backgrounds | IndexedDB database handwriting-studio, store assets |
studio/assets/ under the application data directory |
The desktop main process selects the data directory: %LOCALAPPDATA%/HandwritingWeb on Windows,
or ~/Library/Application Support/HandwritingWeb on macOS. Storage is independent of the backend's
random port. The backend SQLite queue and editor drafts have separate data and lifecycles.
studio.workspace combines text, configuration, preferences, and asset references in one snapshot.
Presets have a separate record. Assets are deduplicated by content hash. Records contain asset IDs
and file metadata rather than blob: URLs or full Base64 illustrations. Restoration recreates
Files, the illustration map, and required object URLs.
Save and restore boundaries¶
- Ordinary edits are debounced; blur, visibility changes, and page exit flush pending changes. Prepared assets allow synchronous Web metadata writes.
- Uploads persist their binary content before publishing the corresponding UI state. If storage fails, editing remains available in memory and the UI reports that saving failed.
- Binary assets commit before the complete referencing metadata snapshot. A partial new snapshot does not replace the previous complete draft.
- Desktop writes replace files through a temporary file, sync, and rename. Writes to the same record are serialized; reads wait for previously accepted writes.
- Startup prefers the current snapshot, then falls back to legacy
studio.*, camelCase settings, andmyPresetwhen no snapshot exists. Failed restoration is reported and automatic overwriting of the unreadable workspace is blocked. - Resource services revoke old object URLs when previews or backgrounds change. Late asynchronous results must not overwrite newer edits.
Web storage is origin-scoped and does not merge edits across tabs. Web asset reclamation is disabled to avoid deleting another tab's in-flight resources. Desktop reclamation only considers application assets identified during this run and unreferenced by the workspace, preset, or in-flight operations. Read or save failures disable automatic reclamation for the session.
Desktop shutdown¶
A normal window close or application quit keeps the renderer alive, requests a save, waits for its acknowledgement and accepted file writes, then exits. A failed or timed-out save allows cancelling the quit. Forced termination or power loss can only rely on previously committed snapshots. The bridge accepts only the current main window's main frame at its exact origin and restricts record keys, asset IDs, sizes, and in-flight calls. It exposes no arbitrary filesystem paths.
Extension rules¶
- Assign client state to a business domain; keep temporary interactions in components and server data in the Query cache.
- Define new endpoint contracts, validation, and Query keys in
api/. Compose cross-domain side effects through workspace actions. - Extend persistence through its adapters, including migration and failure handling. Keep the renderer contract and desktop record whitelist aligned.
- Changes to refresh recovery, task observation, or shutdown saving require the relevant unit and end-to-end coverage.
- Consult source and tests for exact fields, limits, timeouts, and cache parameters. This page does not duplicate release versions or a work log.