Previously the on-device Web Speech API (Chrome/Edge/Safari) was always preferred, and the configured external transcription endpoint was only used as a fallback when the browser had no speech API at all (Firefox). Flip the priority in Composer.tsx: if a transcription endpoint is configured in Settings, use it for dictation regardless of browser support, since it's generally far more accurate. Fall back to the browser's on-device SpeechRecognition only when no endpoint is set. Update the Settings hint text and README to describe the new behavior. Rebuilt and redeployed to the repo root.
237 lines
9.2 KiB
Markdown
237 lines
9.2 KiB
Markdown
# Hermes PWA
|
|
|
|
A mobile-first **Progressive Web App** front end for a self-hosted
|
|
[Hermes Agent](https://hermes-agent.nousresearch.com) (Nous Research). It talks
|
|
directly to the **Hermes Agent API Server** — the OpenAI-compatible HTTP + SSE
|
|
endpoint — so you can chat with your agent, browse sessions, and use text, voice,
|
|
and images from an installable app.
|
|
|
|
Branding (logo, colour scheme, app icons) follows [derez.ai](https://derez.ai/);
|
|
the logo in the sidebar links to the homepage.
|
|
|
|
---
|
|
|
|
## Which URL do I use? (dashboard vs API server)
|
|
|
|
Hermes exposes **two different servers**. This app needs the second one.
|
|
|
|
| URL | What it is | Auth | Works with this app? |
|
|
| --- | --- | --- | --- |
|
|
| `https://your-host/` (e.g. `beaver.derez.ai`) | The **web dashboard** | username + password → session cookie | ❌ No. It only allows CORS from its own origin and never sends `Access-Control-Allow-Credentials: true`, so a separately-hosted app can't use its cookie. |
|
|
| `https://api.your-host/` (e.g. `api.beaver.derez.ai`, default port `8642`) | The **API Server** | `Authorization: Bearer <API_SERVER_KEY>` | ✅ **Yes — this is the one.** |
|
|
|
|
So enter the **API server URL** and your **`API_SERVER_KEY`** on the config
|
|
screen. There is no QR login and no username/password here — the API server uses
|
|
a bearer token.
|
|
|
|
### Enabling the API server (on the Hermes host)
|
|
|
|
If it isn't already running, add to `~/.hermes/.env` and start the gateway:
|
|
|
|
```bash
|
|
API_SERVER_ENABLED=true
|
|
API_SERVER_KEY=choose-a-long-random-secret
|
|
# Only needed if a browser calls the API directly (see CORS below):
|
|
# API_SERVER_CORS_ORIGINS=https://your-pwa-origin
|
|
```
|
|
|
|
```bash
|
|
hermes gateway # → [API Server] API server listening on http://127.0.0.1:8642
|
|
```
|
|
|
|
Verify from anywhere:
|
|
|
|
```bash
|
|
curl -s https://api.your-host/health
|
|
# {"status": "ok", "platform": "hermes-agent", "version": "..."}
|
|
|
|
curl -s -H "Authorization: Bearer $API_SERVER_KEY" https://api.your-host/v1/capabilities
|
|
```
|
|
|
|
---
|
|
|
|
## Reaching the API server (CORS)
|
|
|
|
The API server has **no browser CORS by default**. Two options:
|
|
|
|
### Option A — same origin (recommended): use the bundled proxy
|
|
|
|
`server/proxy.mjs` serves the built app **and** proxies `/api/*` and `/v1/*` to
|
|
the API server, so everything is same-origin and CORS never applies.
|
|
|
|
**You normally paste the API key into the app in the browser** (it's saved in
|
|
that browser's `localStorage`). The proxy does not need the key:
|
|
|
|
```bash
|
|
HERMES_TARGET=https://api.beaver.derez.ai PORT=8787 ./run.sh serve
|
|
# open http://localhost:8787
|
|
```
|
|
|
|
Then in the app's config screen, tick **"Same origin (behind the bundled
|
|
proxy)"** and paste your API key. Done — no key on the server, no CORS.
|
|
|
|
> `HERMES_API_KEY` is **optional**. Only set it if you'd rather keep the key on
|
|
the proxy so it never reaches the browser:
|
|
>
|
|
> ```bash
|
|
> HERMES_TARGET=https://api.beaver.derez.ai HERMES_API_KEY=your-key PORT=8787 ./run.sh serve
|
|
> ```
|
|
|
|
### Option B — direct, cross-origin
|
|
|
|
Set an explicit allowlist on the Hermes host and enter the API URL + key in the
|
|
app's config screen:
|
|
|
|
```bash
|
|
API_SERVER_CORS_ORIGINS=https://your-pwa-origin
|
|
```
|
|
|
|
For local development you can skip CORS entirely with a dev proxy:
|
|
|
|
```bash
|
|
# .env
|
|
HERMES_DEV_TARGET=https://api.beaver.derez.ai
|
|
```
|
|
|
|
---
|
|
|
|
## Quick start
|
|
|
|
```bash
|
|
./run.sh # dev server -> http://localhost:5173
|
|
```
|
|
|
|
Then on the config screen enter your **API server URL** and **API key**.
|
|
|
|
### `run.sh` modes
|
|
|
|
```bash
|
|
./run.sh # dev server (http://localhost:5173)
|
|
./run.sh build # production build, deployed straight into the repo root
|
|
./run.sh serve # build if needed, then host + proxy /api/*, /v1/* (needs HERMES_TARGET)
|
|
./run.sh preview # build, then serve the build with vite preview
|
|
./run.sh icons # regenerate the PWA icon set
|
|
./run.sh typecheck # type-check without emitting
|
|
```
|
|
|
|
### Deployment model: the repo root *is* the web root
|
|
|
|
The Vite project lives in `app/`. Running a build (`./run.sh build` or
|
|
`cd app && npm run build`) compiles it and then copies the output
|
|
(`index.html`, `assets/`, `manifest.webmanifest`, `sw.js`, `workbox-*.js`,
|
|
`favicon.svg`, `icons/`, `LICENSE`) directly into the **repository root** —
|
|
see `app/scripts/deploy.mjs`. Those deployed files are committed to git.
|
|
|
|
This means a webserver that simply serves the raw repository checkout as
|
|
static files (no build step, no custom MIME types, no reverse-proxy config)
|
|
works out of the box: `index.html` in the repo root already references
|
|
hashed, plain `.js`/`.css` assets — nothing points at `/src/*.tsx` in
|
|
production, so there's no "unauthorized MIME type" issue.
|
|
|
|
To publish a new version:
|
|
|
|
```bash
|
|
cd app
|
|
npm run build # builds app/dist, then syncs it into the repo root
|
|
cd ..
|
|
git add -A
|
|
git commit -m "Deploy: <description>"
|
|
git push # or however the server pulls the repo
|
|
```
|
|
|
|
> Note: `app/` (source, dev config, `node_modules`) is still present in the
|
|
> checkout, so if the webserver serves the whole repo folder, the raw
|
|
> TypeScript source under `app/src` is technically reachable as plain text.
|
|
> It is never *executed* as a module by the deployed app (the deployed
|
|
> `index.html` only references the built `assets/*.js`), so this is not a
|
|
> functional issue — just something to be aware of if source privacy
|
|
> matters to you.
|
|
|
|
| Variable | Used by | Meaning |
|
|
| --- | --- | --- |
|
|
| `HERMES_TARGET` | `serve` | API server base URL to proxy to |
|
|
| `HERMES_API_KEY` | `serve` | Optional — injected as `Authorization: Bearer …` |
|
|
| `PORT` | `serve` | Port to listen on (default `8787`) |
|
|
| `HERMES_DEV_TARGET` | `dev` | API server base URL to proxy to during development |
|
|
|
|
---
|
|
|
|
## Features
|
|
|
|
- **Config screen** — add one or more API-server connections (URL, API key,
|
|
optional profile prefix, custom headers) with a "Test" probe.
|
|
- **Sessions** — list, open, create, rename, fork, and delete; filter by title.
|
|
- **Streaming chat** — token-by-token replies over SSE, with live tool-call
|
|
cards, cancel, and mid-turn steer.
|
|
- **Images** — attach/paste/drag-drop; sent inline as `data:` URLs.
|
|
- **Voice** — dictation via the browser's Web Speech API and spoken replies via
|
|
on-device speech synthesis. Tap the mic to speak your prompt; tap again to stop.
|
|
Requires a **secure context** (HTTPS or `localhost`) and a browser with the Web
|
|
Speech API (Chrome, Edge, Safari). Note: Chrome's dictation sends audio to
|
|
Google's servers.
|
|
- **Installable PWA** — offline app shell, home-screen install, standalone display.
|
|
|
|
---
|
|
|
|
## Notes & limitations
|
|
|
|
- **Voice is client-side.** The API server has no `/transcribe` or `/tts`
|
|
endpoint. If a transcription endpoint is configured in Settings (any
|
|
OpenAI-compatible `/v1/audio/transcriptions` server), it's used for
|
|
dictation; otherwise dictation falls back to the browser's on-device
|
|
`SpeechRecognition`. Spoken replies always use `speechSynthesis`. Support
|
|
varies by browser (best on Chrome/Edge/Safari).
|
|
- **Profiles.** The API server routes profiles via a `/p/<profile>/…` prefix, and
|
|
each profile has its own `API_SERVER_KEY`. Add one connection per profile to
|
|
see them all; the sidebar switches between connections.
|
|
- **Session ownership.** The API server can create and drive its own sessions,
|
|
but it refuses to write to a session owned by another surface (CLI, TUI,
|
|
dashboard, messaging) — those open read-only.
|
|
- **Response shapes.** The Sessions REST API's exact JSON envelope isn't fully
|
|
documented, so the client normalises several shapes (`sessions`/`data`/`items`,
|
|
`id`/`session_id`, string/array content). If a field looks off on your version,
|
|
it's a one-line tweak in `app/src/api/client.ts`.
|
|
|
|
## Project layout
|
|
|
|
```
|
|
index.html, assets/, manifest.webmanifest, # repo root = deployed build
|
|
sw.js, workbox-*.js, favicon.svg, icons/, # output (see Deployment model above)
|
|
LICENSE
|
|
app/ # Vite project (source, dev config)
|
|
index.html # dev entry (-> /src/main.tsx); NOT served in production
|
|
src/
|
|
api/
|
|
client.ts # HermesClient: bearer auth, sessions REST, SSE chat
|
|
types.ts # API server types + normalised UI types
|
|
hooks/
|
|
useSpeechRecognition.ts # browser dictation
|
|
useTts.ts # browser speech synthesis
|
|
lib/
|
|
config.ts # saved connections (localStorage)
|
|
format.ts # time / token formatting
|
|
markdown.tsx # tiny safe markdown renderer
|
|
state/
|
|
store.ts # zustand store: connections, sessions, streaming
|
|
components/ # ConfigScreen, ConnectionForm, Sidebar, ChatView, Composer, …
|
|
scripts/
|
|
gen-icons.mjs # dependency-free PNG icon generator
|
|
deploy.mjs # copies app/dist/* into the repo root after `vite build`
|
|
server/
|
|
proxy.mjs # optional static host + /api,/v1 reverse proxy (no deps)
|
|
```
|
|
|
|
## License
|
|
|
|
Licensed under the [PolyForm Internal Use License 1.0.0](./LICENSE) — internal
|
|
business use only; redistribution is not permitted. The license text is also
|
|
shipped in the build output as `dist/LICENSE`.
|
|
|
|
## API endpoints used
|
|
|
|
`GET /health`, `GET /v1/capabilities`, `GET /v1/models`,
|
|
`GET|POST /api/sessions`, `GET|PATCH|DELETE /api/sessions/{id}`,
|
|
`GET /api/sessions/{id}/messages`, `POST /api/sessions/{id}/fork`,
|
|
`POST /api/sessions/{id}/chat/stream` (SSE), and
|
|
`POST /v1/runs/{id}/stop|steer|approval|clarify`.
|