BareGit
# Web UI Design

## Purpose

This document describes how `diffusion-cli` should expose a simple
browser-based web UI on top of server mode.

The UI is for a local user who wants to generate images without typing a
full command line request each time. It should reuse the existing server
mode and the existing generation service. It should not become a new
image generation backend, a replacement for compatibility API profiles,
or a separate frontend build project.

The UI must be written with Preact and plain JavaScript that runs
directly in the browser. There should be no Node.js toolchain, no
bundler, no transpiler, and no JSX transform. The browser should load
ordinary static files served by Flask.

## Goals

The feature must:

- Add a simple local web UI to `diffusion-cli serve`.
- Keep the UI separate from API profiles such as `sillytavern-sdcpp`.
- Use Preact in browser-native JavaScript module files.
- Avoid a frontend build step.
- Avoid Bootstrap.
- Reuse `GenerationService` for all actual image generation.
- Use the same internal request model as the rest of the server code.
- Return generated PNG images to the browser for immediate display.
- Show clear validation, running, success, and failure states.
- Keep the first implementation small enough to test and maintain.

The feature should not:

- Add image-to-image support.
- Add LoRA support.
- Add prompt history persistence.
- Add user accounts, login, or multi-user authorization.
- Add async job queues, cancellation, or progress streaming.
- Add a Node.js package manager workflow.
- Call the SillyTavern compatibility endpoint from the UI.
- Change the existing SillyTavern API response shape.
- Change the generation algorithm.

The reason to keep the first UI small is that image generation requests
are already expensive and stateful. The value of the first UI is making
the existing local generator easier to drive. Features such as queues,
progress bars, persistent galleries, and cancellation require additional
server semantics and should be designed separately.

## Current State

Server mode already exists in:

```text
diffusion_cli/server.py
```

The server currently creates a Flask app, logs requests, constructs a
`GenerationService`, and registers routes for the selected API profile.

API profile routes live in:

```text
diffusion_cli/api_profiles.py
```

The first profile is `sillytavern-sdcpp`. It exists to match the route
and payload expectations of SillyTavern's stable-diffusion.cpp adapter.
That profile currently registers:

```text
GET     /health
OPTIONS /v1/images/generations
GET     /v1/models
POST    /sdapi/v1/txt2img
```

The generation boundary lives in:

```text
diffusion_cli/generation_service.py
```

`GenerationService.generateImages()` accepts an
`ImageGenerationRequest` and returns PNG bytes wrapped in
`GeneratedImage` objects. This is the correct layer for the UI to call
because it is independent of Flask request details and independent of
external API compatibility quirks.

## Design Summary

Add a project-owned UI layer that is always available when server mode
is running.

Recommended routes:

```text
GET  /ui/
GET  /ui/app.js
GET  /ui/style.css
GET  /ui/vendor/preact.module.js
GET  /ui/api/defaults
GET  /ui/api/health
POST /ui/api/txt2img
```

The `/ui/` routes serve static browser assets. The `/ui/api/...` routes
are first-party JSON endpoints used only by the web UI. They are not
compatibility API routes and should not be placed in `api_profiles.py`.

The browser app should call `/ui/api/txt2img`, not
`/sdapi/v1/txt2img`. The two endpoints may accept similar fields, but
they have different responsibilities:

- `/sdapi/v1/txt2img` is a compatibility endpoint for external clients.
- `/ui/api/txt2img` is a local UI endpoint controlled by this project.

This distinction matters because compatibility endpoints often preserve
client-specific behavior that is not ideal for a first-party UI. For
example, the SillyTavern endpoint returns A1111-style fields such as
`images`, `parameters`, and `info`. The UI can use a clearer response
shape because no external client depends on it.

## External References

The implementation should use these references:

- [Preact no-build tools guide][preact-no-build] for browser-native
  Preact usage without a build step.
- [MDN JavaScript modules][mdn-modules] for native browser module
  loading with `type="module"`.
- [MDN Fetch API guide][mdn-fetch] for browser JSON requests.
- [Flask `send_from_directory` documentation][flask-send] for serving
  static UI files from a package directory.
- [Flask file upload documentation][flask-upload] for the security
  rationale behind constraining file serving paths, even though this
  design does not require uploads.

[preact-no-build]: https://preactjs.com/guide/v10/getting-started/#no-build-tools-route
[mdn-modules]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules
[mdn-fetch]: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch
[flask-send]: https://flask.palletsprojects.com/en/stable/api/#flask.send_from_directory
[flask-upload]: https://flask.palletsprojects.com/en/stable/patterns/fileuploads/

## Frontend Asset Layout

Add static assets under the Python package:

```text
diffusion_cli/static/ui/index.html
diffusion_cli/static/ui/app.js
diffusion_cli/static/ui/style.css
diffusion_cli/static/ui/vendor/preact.module.js
diffusion_cli/static/ui/vendor/LICENSE.preact
```

The assets live under `diffusion_cli/static` so they can be included in
the Python package and served by Flask at runtime. Keeping the UI inside
the package also means users do not need a second checkout, a build
artifact directory, or a separate web server.

The Preact file should be vendored instead of loaded from a CDN. This
project already assumes local model files and explicitly avoids
downloading model files at runtime. The UI should follow the same
operational style: once installed, server mode should work without
network access.

`preact.module.js` should be an unmodified browser ESM distribution of
Preact 10.x. Its license must be included next to the vendored file.
The exact source URL and version should be documented in a short comment
or in the license file so the dependency can be updated deliberately.

## Python Packaging

`pyproject.toml` currently uses setuptools package discovery. Static UI
files must be included in the installed package.

Add package data for the UI files:

```toml
[tool.setuptools.package-data]
diffusion_cli = [
    "static/ui/*",
    "static/ui/vendor/*",
]
```

The package-data entry matters because Python packages do not
automatically include arbitrary static files in every build backend
configuration. Without package data, tests may pass from the source tree
while an installed wheel misses `index.html` or `app.js`.

## Route Registration

UI routes should be registered from `server.py`, not from an API
profile.

Recommended structure:

```text
createApp(...)
    registerUiRoutes(app, context)
    API_PROFILES[server_config.api_profile].register_routes(app, context)
```

`registerUiRoutes()` can live in a new module:

```text
diffusion_cli/ui.py
```

That keeps the UI-specific request parsing, response formatting, and
static file path handling out of the API compatibility module.

Recommended public function:

```python
def registerUiRoutes(app, context) -> None:
    """Register first-party browser UI routes."""
```

This function should be the only public entry point in `ui.py` for the
first implementation. It receives the same app and context objects used
by API profiles, so it can call `context.generation_service` without
creating a second service instance.

## Static File Routes

The UI should use explicit routes rather than exposing an arbitrary
static directory.

Recommended routes:

```text
GET /ui/
GET /ui/app.js
GET /ui/style.css
GET /ui/vendor/preact.module.js
GET /ui/vendor/LICENSE.preact
```

Each route should return a file from:

```text
diffusion_cli/static/ui
```

Use Flask's `send_from_directory()` or `send_file()` with a path derived
from the package directory, not from user input. Avoid a catch-all route
such as `/ui/<path:name>` in the first implementation. A catch-all route
is convenient, but explicit routes make it clear which files are public
and avoid serving files that were not meant to become part of the HTTP
surface.

The root route should support a trailing slash:

```text
GET /ui/
```

The implementation may also redirect:

```text
GET /ui -> /ui/
```

That redirect is a usability detail. It is not part of the API contract.

## UI API Routes

### `GET /ui/api/health`

Return basic server and profile information.

Response:

```json
{
    "status": "ok",
    "api_profile": "sillytavern-sdcpp",
    "model_residency": "cpu-cache"
}
```

This endpoint lets the UI verify that it is talking to the expected
server. It is separate from `/health` because `/health` currently belongs
to the selected compatibility profile. A future profile might not expose
`/health`, or it might expose a different response shape.

### `GET /ui/api/defaults`

Return the default values that the form should show before the user
edits anything.

Response:

```json
{
    "negative_prompt": "",
    "width": 832,
    "height": 1248,
    "batch_size": 1,
    "steps": 10,
    "cfg": 1.0,
    "seed": -1
}
```

The values should be derived from the loaded `UserConfig` plus built-in
generation defaults. They should not be copied into JavaScript as a
second source of truth.

If producing these defaults requires a new helper in `config.py`, prefer
that helper over duplicating constants in `ui.py`. A reasonable helper
shape is:

```python
def buildDefaultGenerationRequest(user_config: UserConfig):
    """Build UI-visible generation defaults from config and fallbacks."""
```

The helper should not require a prompt because the UI form starts empty.
Prompt is request-specific user input, not a persistent default.

### `POST /ui/api/txt2img`

Generate images from a browser form request.

Request:

```json
{
    "prompt": "a ceramic mug on a desk",
    "negative_prompt": "text, watermark",
    "width": 832,
    "height": 1248,
    "batch_size": 1,
    "steps": 10,
    "cfg": 1.0,
    "seed": -1
}
```

Field meanings:

`prompt`

: Required positive prompt. It must be a non-empty string after
  trimming whitespace.

`negative_prompt`

: Optional negative prompt. Empty string should be treated like no
  negative prompt if the internal request model expects `None`.

`width`

: Optional output width. It must be a positive multiple of 8.

`height`

: Optional output height. It must be a positive multiple of 8.

`batch_size`

: Optional number of images. It must be at least 1.

`steps`

: Optional denoising step count. It must be at least 1.

`cfg`

: Optional classifier-free guidance scale. It must be non-negative.

`seed`

: Optional seed. A value of `-1` means random seed and should become
  `None` in `ImageGenerationRequest`.

Response:

```json
{
    "images": [
        {
            "mime_type": "image/png",
            "data": "iVBORw0KGgo...",
            "seed": 123
        }
    ]
}
```

The response should use bare base64 image data plus an explicit MIME
type. The frontend can build a display URL with:

```text
data:image/png;base64,<data>
```

The API should not write UI results to disk. File output is part of the
CLI path. The UI path is interactive and should return image bytes to
the browser.

## Request Validation

The UI API should validate JSON before calling `GenerationService`.

Invalid requests should return:

```json
{
    "error": "bad_request",
    "message": "prompt required"
}
```

with HTTP status:

```text
400 Bad Request
```

Generation failures should return:

```json
{
    "error": "generation_failed",
    "message": "generation failed"
}
```

with HTTP status:

```text
500 Internal Server Error
```

When the failure is a `DiffusionCliError`, the response may include the
exception message. For unexpected exceptions, log the exception on the
server and return a generic message to the browser. This matches the
existing API profile pattern and avoids exposing Python tracebacks in the
UI.

Validation should reuse existing helpers where possible. In particular,
dimension checks should use `validateDimensions()` from `config.py` so
the UI does not drift away from CLI behavior.

## Frontend Implementation

The browser app should use Preact without JSX.

`index.html` should load the app as a module:

```html
<script type="module" src="/ui/app.js"></script>
```

`app.js` should import Preact from the vendored module:

```js
import { h, render } from "/ui/vendor/preact.module.js";
```

The app can define small components with plain `h()` calls. This is more
verbose than JSX, but it keeps the implementation honest: the file is
plain JavaScript that the browser can execute directly.

Recommended component split:

```text
App
GenerationForm
NumberField
TextAreaField
ResultGallery
StatusMessage
```

The split should stay practical. Components should exist when they make
state and markup easier to understand, not to imitate a larger frontend
framework structure.

### Frontend State

Recommended top-level state:

```js
{
    defaults: null,
    form: {
        prompt: "",
        negative_prompt: "",
        width: 832,
        height: 1248,
        batch_size: 1,
        steps: 10,
        cfg: 1.0,
        seed: -1
    },
    is_loading_defaults: true,
    is_generating: false,
    error_message: "",
    images: []
}
```

`is_generating` should disable the generate button and input fields
while a request is in flight. The server serializes generation with a
lock, so allowing multiple rapid submissions from the same UI would only
queue HTTP requests and make the user experience less clear.

### Frontend Request Flow

The submit flow should be:

1. Prevent the browser's default form submit.
2. Clear the previous error.
3. Set `is_generating` to `true`.
4. Send `POST /ui/api/txt2img` with JSON.
5. If the response is not OK, parse the JSON error message when present.
6. If the response is OK, replace `images` with the returned images.
7. Set `is_generating` to `false` in a `finally` block.

The `finally` step matters because failed network requests, server
errors, and successful generation must all return the UI to an editable
state.

## Visual Design

The UI should be a compact tool, not a marketing page.

Recommended layout:

- A full-width app shell with a narrow top bar.
- A two-column desktop layout.
- A single-column mobile layout.
- The form on the left or top.
- The result area on the right or below.
- No cards inside cards.
- No Bootstrap classes.
- No decorative gradients or ornamental backgrounds.

The target user is repeatedly generating local images. The layout should
therefore favor scanning, editing, and re-running requests. Controls
should stay close to the result area so the prompt-to-output loop is
short.

Recommended controls:

- Textarea for `prompt`.
- Textarea for `negative_prompt`.
- Number inputs for `width`, `height`, `steps`, `cfg`, `batch_size`,
  and `seed`.
- Checkbox or small button for random seed behavior.
- Primary button for generation.

The UI should display generated images at a bounded size while allowing
the user to open the full image in a new tab. The simplest first version
is to wrap each image in an anchor whose `href` is the data URL.

## Security Model

The server default host remains:

```text
127.0.0.1
```

This means the UI is local by default. If a user binds the server to
`0.0.0.0`, the UI becomes reachable by other machines on the network.
That is already true for the API routes. The UI design should not add
features that make remote exposure more dangerous, such as arbitrary
file browsing, file uploads, shell command execution, or config editing.

The first UI should not implement authentication. Authentication only
makes sense with a broader remote-use design, including transport
security and deployment guidance. A local-only first version is simpler
and matches the current server's default behavior.

The UI must not accept file paths from the browser. Model paths and
output paths remain server-side configuration. This avoids exposing local
filesystem details and avoids turning the browser into a remote file
selection surface.

## Concurrency Model

`GenerationService` already serializes generation with a lock.

The UI should assume exactly one generation request is active per browser
tab. It should disable submission while waiting. This does not prevent a
second browser tab from submitting another request, but the service lock
will serialize those requests on the server.

The first UI should not display queue position because the server does
not have a queue abstraction. The Flask development server may have
multiple request threads, but the generation lock is the actual
generation concurrency boundary.

## Error Handling

Frontend error handling should distinguish:

- Validation errors returned by the server.
- Generation failures returned by the server.
- Network failures where no JSON response is available.

For validation errors, show the server message. For generation failures,
show the server message if present. For network failures, show a short
generic message such as:

```text
Request failed. Check the server log for details.
```

Do not show Python tracebacks in the browser. The server log is the
right place for tracebacks.

## Testing Strategy

### Python Route Tests

Add tests for UI route registration in a new file:

```text
tests/test_ui.py
```

Recommended tests:

- `GET /ui/` returns `200`.
- `GET /ui/app.js` returns JavaScript content.
- `GET /ui/style.css` returns CSS content.
- `GET /ui/api/health` returns profile and residency values.
- `GET /ui/api/defaults` returns numeric generation defaults.
- `POST /ui/api/txt2img` calls a fake generation service.
- Invalid prompt returns `400`.
- Generation service errors return `500`.

The fake generation service should mirror the existing server tests:

```python
class FakeGenerationService:
    def __init__(self):
        self.requests = []

    def generateImages(self, request):
        self.requests.append(request)
        return [GeneratedImage(b"\x89PNG\r\n\x1a\nimage", "png", 123)]
```

This keeps the UI route tests fast and avoids loading model files.

### Static Asset Tests

Static asset tests should verify that package paths are correct. They
should not execute Preact in Python.

Useful assertions:

- `index.html` contains `/ui/app.js`.
- `app.js` imports `/ui/vendor/preact.module.js`.
- `pyproject.toml` includes UI static package data.

### Browser Tests

Do not require browser automation for the first implementation unless
the project already has that dependency. The no-build Preact app is small
enough that server-side route tests plus manual browser verification are
acceptable for the first version.

If browser automation is added later, it should verify:

- The form renders.
- The generate button disables while a request is active.
- A successful fake response displays an image.
- A server error displays an error message.

## Implementation Plan

1. Add `diffusion_cli/ui.py`.
2. Add static files under `diffusion_cli/static/ui`.
3. Add vendored Preact and its license under `static/ui/vendor`.
4. Register UI routes from `createApp()` in `server.py`.
5. Add UI API request parsing and validation.
6. Add UI API response formatting.
7. Add package-data configuration in `pyproject.toml`.
8. Add tests in `tests/test_ui.py`.
9. Run the existing test suite.
10. Start `uv run diffusion-cli serve ...` and manually open `/ui/`.

The implementation should keep each step small. Static serving can be
tested before image generation is wired up. The UI API can be tested
with a fake generation service before the browser app is complete.

## Future Work

The following features are intentionally out of scope for the first
version:

- Async job IDs.
- Cancellation.
- Progress updates.
- Server-sent events or WebSockets.
- Prompt history.
- Persistent image gallery.
- Saving UI results to a configured output directory.
- Model and checkpoint selection from the browser.
- Authentication for remote access.

These features are useful, but each one changes server behavior enough
to deserve a separate design. The first UI should establish a clean local
control surface without changing the generation backend contract.