Metadata-Version: 2.4
Name: fabricatio-webui
Version: 0.6.1
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Typing :: Typed
Requires-Dist: fabricatio-core
Requires-Dist: orjson
Requires-Dist: typer>=0.15.2 ; extra == 'cli'
Provides-Extra: cli
Summary: An extension of fabricatio
Author-email: Whth <zettainspector@foxmail.com>
License-Expression: MIT
Requires-Python: >=3.12, <3.15
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
Project-URL: Homepage, https://github.com/Whth/fabricatio
Project-URL: Issues, https://github.com/Whth/fabricatio/issues
Project-URL: Repository, https://github.com/Whth/fabricatio

# `fabricatio-webui`

[MIT](https://img.shields.io/badge/license-MIT-blue.svg)
![Python Versions](https://img.shields.io/pypi/pyversions/fabricatio-webui)
[![PyPI Version](https://img.shields.io/pypi/v/fabricatio-webui)](https://pypi.org/project/fabricatio-webui/)
[![PyPI Downloads](https://static.pepy.tech/badge/fabricatio-webui/week)](https://pepy.tech/projects/fabricatio-webui)
[![PyPI Downloads](https://static.pepy.tech/badge/fabricatio-webui)](https://pepy.tech/projects/fabricatio-webui)
[![Bindings: PyO3](https://img.shields.io/badge/bindings-pyo3-green)](https://github.com/PyO3/pyo3)
[![Build Tool: uv + maturin](https://img.shields.io/badge/built%20with-uv%20%2B%20maturin-orange)](https://github.com/astral-sh/uv)

Web UI service for the Fabricatio LLM application framework. Serves a Vue-based single-page application built with Vite over an axum HTTP server (Rust, bound via PyO3).

---

## Installation

```bash
pip install fabricatio[webui]
# or
pip install fabricatio-webui
```

The CLI entry point requires the `cli` extra:

```bash
pip install fabricatio-webui[cli]
```

## Quick Start

Start the service with the bundled frontend:

```bash
fc-webui
```

This serves the SPA at `http://127.0.0.1:9846`. Use `--frontend-dir` / `-d` to point at a custom build, and `--addr` / `-a` to change the bind address:

```bash
fc-webui --addr 0.0.0.0:3000 --frontend-dir ./dist
```

### Run something in under a minute (no LLM required)

The **Hello Fabricatio** blueprint is a pure-Python two-step pipeline that needs
no LLM, no API keys, and no configuration:

1. On the board canvas, drag **webui → Hello Fabricatio** from the blueprint
   rail onto a role (create one first via right-click → *Add role*).
2. Double-click the role card to open the workflow: a `TextStats` node wired
   into a `SummarizeStats` node.
3. Press `Ctrl+Enter`, keep the namespace (`hello-fabricatio`), and put your
   text in **Extra init context**: `{"text": "hello fabricatio"}`.
4. Run — the console streams `node_start/done` events and the task result is
   the summary line, e.g. `[demo] chars: 16, words: 2, lines: 1`.

Every other shipped blueprint (novel/typst pipelines) drives real LLM calls and
requires configured credentials before it can run.

## API

All functionality is exposed through the Rust-backed Python module `fabricatio_webui.rust`.

### `start_service(...)`

Starts an async HTTP server (axum + tokio) that serves static files from `frontend_dir` with SPA fallback (all unmatched routes serve `index.html`). CORS is permissive when `allowed_origins` is empty, otherwise restricted to the given origins.

| Parameter            | Type                | Description                                   |
|----------------------|---------------------|-----------------------------------------------|
| `frontend_dir`       | `str \| PathLike`   | Directory containing the built frontend       |
| `data_dir`           | `str \| PathLike`   | Workflow persistence directory                |
| `addr`               | `str`               | Bind address, e.g. `"127.0.0.1:9846"`        |
| `node_registry_json` | `str`               | JSON array of node type definitions           |
| `blueprints_json`    | `str`               | JSON array of package-defined blueprints      |
| `allowed_origins`    | `Sequence[str]`     | CORS allowed origins (empty = permissive)     |
| `submit_fn`          | `Callable`          | Worker: `submit(execution_id, task_json)`     |
| `cancel_fn`          | `Callable`          | Worker: `cancel_current() -> bool`            |
| `queue_snapshot_fn`  | `Callable`          | Worker: `queue_snapshot() -> str` (JSON)      |
| `history_snapshot_fn`| `Callable`          | Worker: `history_snapshot() -> str` (JSON)    |
| `rebuild_roles_fn`   | `Callable`          | Worker: re-dispatch roles after save/delete   |
| `persist_workflows`  | `bool`              | When false, save/delete keep in-memory state only and skip writing `workflows.json` |

The exact Python-visible signature lives in the generated stub
(`python/fabricatio_webui/rust/__init__.pyi`, regenerated by
`cargo run -p fabricatio-stubgen --features webui`).

## Execution pipeline

Submissions (`POST /api/execute` or a WS `submit` message) are forwarded to an in-process asyncio worker (`fabricatio_webui.worker.WorkflowWorker`). The worker instantiates `Action` nodes from the workflow graph, executes them in topological order, and streams lifecycle events back over WebSocket. `POST /api/interrupt` cancels the running execution (`execution_done` with `cancelled: true`). Queue and history are owned by the worker and exposed via `GET /api/queue` and `GET /api/history`.

The CLI wires everything together:

```python
# fc-webui — worker + server run on one event loop
import asyncio, json
from fabricatio_webui.blueprints import build_blueprints
from fabricatio_webui.config import webui_config
from fabricatio_webui.registry import build_node_registry
from fabricatio_webui.rust import rust_broadcast, start_service
from fabricatio_webui.worker import WorkflowWorker

async def main() -> None:
    worker = WorkflowWorker(
        rust_broadcast, "./workflows",
        queue_max=webui_config.queue_max, history_max=webui_config.history_max,
    )
    await asyncio.gather(
        start_service("./www", "./workflows", "127.0.0.1:9846",
                      json.dumps(build_node_registry()["node_types"]),
                      json.dumps(build_blueprints()["blueprints"]),
                      list(webui_config.allowed_origins),
                      worker.submit, worker.cancel_current,
                      worker.queue_snapshot, worker.history_snapshot,
                      worker.rebuild_roles,
                      bool(webui_config.persist_workflows)),
        worker.run(),
    )

asyncio.run(main())
```

## Board editor

The frontend is a node-based board editor for authoring role-driven workflows.
A **board** is the top-level saved document (`format_version: 2`) holding:

- **roles** — each with a name, description, and a list of workflows; every
  workflow is a node graph plus the namespace pattern it subscribes to
  (e.g. `"write::book"` → `"write::book::*::Pending"`).
- **actions** — optional board-level custom Action definitions (emitted as
  Python subclasses by the code generator).

Blueprints are collected at startup from the `workflows` subpackage of **every
installed `fabricatio-*` package** (the node catalog's actions likewise, from
each package's `actions` tree), so installing another ecosystem package is
enough for its content to appear — no configuration. They are served via
`GET /api/blueprints` and offered in the sidebar for one-click board
seeding. The introspected node catalog — every registered `Action` subclass with
its typed ports, config fields, widget hints, MRO groups, source code, and an
8-hex schema fingerprint — comes from `GET /api/nodes`. Boards are CRUD-managed
through `GET|POST /api/workflows` and `GET|DELETE /api/workflows/{id}`; every
save/delete re-dispatches roles onto the worker's event bus.

### Using the editor

- **Add nodes**: right-click or double-click the canvas → fuzzy-searchable node
  menu grouped by category. `Ctrl+F` opens the command palette for node/command
  search from anywhere.
- **Wire dataflow**: drag between port dots; connections are type-checked
  (`isValidConnection`), optional inputs render hollow handles.
- **Configure inline**: every config field renders an in-node widget derived
  from the Action's pydantic annotations — toggles, number steppers with
  min/max/step, combos fed by `Literal` options, text/textarea, JSON fields;
  fields are grouped by their owning class in the Action's MRO.
- **Run**: `Ctrl+Enter` opens the run dialog; publish a task by namespace and
  watch per-node status badges + the live console (`node_start/done/error`
  events). `POST /api/interrupt` cancels mid-run.
- **Save**: `Ctrl+S` persists the board server-side; autosave drafts go to
  browser localStorage.

### Themes

The UI ships dark (default) and light themes. Switch via **Settings sidebar →
Appearance → Theme**. The choice persists per-browser (localStorage) and is
applied before first paint (no flash on reload).

### Import / export boards

In the **Boards** sidebar:

- **Export all** (header ⤓): downloads `fabricatio-boards.json`, a JSON array of
  every saved board.
- **Per-board export** (row ⤓): downloads `<name>.json` for that one board.
- **Import** (header ⤓↑): pick one or more JSON files — each may hold a single
  board object or an array. Every entry must be `format_version: 2`; entries are
  upserted by name (the server derives the storage id from it), so re-importing
  an edited file updates the existing board. Invalid payloads raise an error
  toast and leave the stored boards untouched.

Boards exported this way are plain JSON — diff them, commit them, share them,
or hand-edit roles/workflows offline and import back.

## WebSocket protocol

One endpoint: `/ws`. Messages are JSON objects tagged by a `type` field.

Client → server:

| type | payload |
|------|---------|
| `submit` | `{ workflow: WorkflowJSON, task_input?: any }` |

Server → client:

| type | payload | notes |
|------|---------|-------|
| `execution_start` | `{ execution_id, timestamp? }` | run begins |
| `node_start` | `{ execution_id, node_id, node_type, timestamp? }` | node begins |
| `node_done` | `{ execution_id, node_id, output?, timestamp? }` | node succeeds |
| `node_error` | `{ execution_id, node_id, error, traceback?, timestamp? }` | node fails |
| `node_output` | `{ execution_id, node_id, output_key, data, timestamp? }` | per-output-key stream |
| `execution_done` | `{ execution_id, result?, error?, cancelled?, timestamp? }` | terminal event |
| `status` | `{ queue_length, running_count }` | emitted on enqueue/dequeue |
| `llm_token` | `{ execution_id, node_id, token, timestamp? }` | receive path implemented end-to-end but nothing emits it yet (future work) |

## Configuration

All options below are read through the fabricatio configuration chain (see the
[Configuration Guide](../../docs/source/configuration.rst)). Set them under the
`[ext.webui]` table in `fabricatio.toml`, equivalently under
`[tool.fabricatio.ext.webui]` in `pyproject.toml`, or via
`FABRICATIO_EXT__WEBUI__<FIELD_UPPER>` environment variables.

```toml
[ext.webui]
addr = "127.0.0.1:9846"
queue_max = 64
```

| Option | Type | Default | Description |
|---|---|---|---|
| `addr` | `str` | `"127.0.0.1:9846"` | — |
| `frontend_dir` | `str` | `""` | empty = use bundled www |
| `allowed_origins` | `tuple[str, ...]` | `("http://localhost:*", "http://127.0.0.1:*")` | — |
| `queue_max` | `int` | `64` | — |
| `history_max` | `int` | `256` | — |
| `persist_workflows` | `bool` | `True` | — |

Access at runtime: `from fabricatio_webui.config import webui_config`.

## Dependencies
- `fabricatio-core` — core interfaces and configuration
- `axum` + `tokio` + `tower-http` (Rust) — HTTP server and middleware
- `typer` (optional, for CLI) — `fc-webui` command

## Todos / Known Gaps

Re-verified 2026-08-21 against source. Grouped by category; checkboxes track completion.

### Functional gaps

- [ ] **`ComfyNode.vue` `open-source` emit is not wired** — the title-bar dblclick emits `open-source`, but VueFlow does not propagate custom events from custom node types, so `NodeCanvas.vue` uses the `onNodeClick` dblclick path instead. Remove the dead emit or document why it is kept.
- [ ] **`llm_token` is never emitted** — the full receive path exists (Rust variant, TS interface, execution-store token buffer) but no Python code emits token events from LLM calls. Future work: wire streaming tokens into the instrumented actions.

### Test gaps

- [ ] **`migrate_board` has no test coverage** — `test_registry.py` covers `migrate_workflow` only; the format 0/1 → 2 board migration is untested.
- [ ] **Frontend unit coverage is thin** — only `argGroups`, `autoLayout`, `board` store, and `NodeWidget` have specs. Missing: `workflow`/`ui`/`execution`/`loading`/`notifications` stores, all canvas/chrome/board components, and all composables (`useWebSocket`, `useHotkeys`, `useAppActions`, `useOutputPreview`).
- [ ] **No Rust tests beyond `types.rs`** — `api.rs`, `state.rs`, `ws.rs`, `webui.rs` have no unit/integration tests for the HTTP and WS endpoints.

### Code hygiene

- [ ] **Pre-existing ruff violations in `registry/_schema.py`** — `C901` (`_type_to_port_type` 13 > 10) and `PLR0912` (`_widget_hint` 15 > 12); carried over verbatim from the old `registry.py`. Refactor or extend the `# noqa` comments.
- [ ] **Vite `INEFFECTIVE_DYNAMIC_IMPORT` warning** — `src/api/client.ts` is dynamically imported by `stores/board.ts` but statically by other stores; the dynamic import never splits a chunk.

### Infra / DX

- [ ] **No E2E browser tests** — the workspace lacks a browser-test harness for the webui package (Puppeteer unavailable in the Bun JS VM); only live API checks are possible.
- [ ] **Python tests lack a package-local runner config** — `python/` has no `pyproject.toml`; tests must be invoked with an explicit path (`python -m pytest packages/fabricatio-webui/python/tests/`) and `uv run` attempts rebuilds and times out.

## License

This project is licensed under the MIT License.

