Beautiful AI responses, beyond plain Markdown.
Turn a prompt into a live, streaming interface that can explain, visualize, and interact.
ChatHTML turns a normal message into a live, streaming HTML response. It progressively renders model-generated interfaces as they are written, so an answer can be explanatory, visual, interactive, or simply beautifully typeset.
It is focused on the response medium rather than reproducing a ChatGPT-style product or an app-builder workflow. The chat shell exists so any answer can become a sandboxed UI artifact.
- Beautiful by default β responsive HTML, CSS, typography, and motion instead of a fenced code block.
- Interactive β forms, controls, small vanilla scripts, and buttons that continue the conversation.
- Private and contained β server keys stay server-side; generated artifacts run in a guarded sandbox.
- Built for iteration β select, edit, regenerate, repair, copy, or export the result.
Plain Markdown vs. ChatHTML comparisons
Each row starts from the same request; the response medium is different. The
left side is the raw response from the same configured model after sending the
user prompt directly to OpenRouter, rendered as a plain Markdown document with
no product chrome. No system instruction or response-format instruction is
sent. The right side is the actual output of this repository's /api/chat
pipeline, rendered by the ChatHTML runtime. The exact submitted prompts are
stored in scripts/readme-comparison-prompts.mjs.
No ChatGPT screenshots or third-party product UI are used. External gallery
photos come from Wikimedia Commons and are credited next to the example.
| Plain Markdown | ChatHTML |
|---|---|
| Prompt 1 Β· Run a Pomodoro clock Build a working Pomodoro clock set to 25:00. Include Start/Pause, Reset, skip, Focus/Short Break/Long Break modes, four session progress dots, and a compact task field. Use a bold analog-inspired countdown with subtle motion and keyboard hints. Keep it polished and focused; no web search. |
|
![]() Markdown source |
![]() Live example Β· Generated source |
| Prompt 2 Β· Curate an image gallery Create a compact editorial gallery titled βEvolution / IβXβ celebrating the Mitsubishi Lancer Evolution. Keep the complete 2-by-2 gallery, filters, captions, and credits visible within a single viewport. Use the four provided Wikimedia Commons images and load every image eagerly. Show model generation, year, photographer, and license on each card, with filters for III, VI, IX, and X and a click-to-expand lightbox. Do not imply endorsement; no web search. |
|
![]() Markdown source |
![]() Live example Β· Generated source Photos: III Β· Charles Β· CC BY 2.0 Β· VI Β· Motoring Weapon R Β· CC BY-SA 3.0 Β· IX Β· FotoSleuth Β· CC BY 2.0 Β· X Β· IFCAR Β· Public domain |
| Prompt 3 Β· Make it playable Build a playable 2048 mini-game with arrow-key and swipe controls, score and best counters, a new game button, and a clear visual hierarchy. The initial HTML itself must contain all 16 visible board cells and a plausible mid-game seed; JavaScript may take over for moves after load. Do not access browser storage; keep the best score only in memory for the current artifact. Keep it compact and polished; no web search. |
|
![]() Markdown source |
![]() Live example Β· Generated source |
| Prompt 4 Β· Direct a visual Build an interactive typographic poster studio. Show a bold live poster preview with the editable headline βMOVE / WITH / INTENTβ, controls for palette, type scale, grain, alignment, and a shuffle button. Use only CSS shapes and typography; no external assets. Make it editorial, expressive, and polished; no web search. |
|
![]() Markdown source |
![]() Live example Β· Generated source |
| Prompt 5 Β· Explain it interactively Teach me how cubic Bezier curves work. Build an interactive playground with a large curve, adjustable control points, the formula, and three named easing presets. Keep it focused and polished; no web search. |
|
![]() Markdown source |
![]() Live example Β· Generated source |
| Prompt 6 Β· Build a working utility Build a tip and split calculator for a EUR 186.50 dinner shared by 4 people. Include editable bill, tip, and party-size controls, update totals live, and show the calculation clearly. Keep it compact and polished; no web search. |
|
![]() Markdown source |
![]() Live example Β· Generated source |
| Prompt 7 Β· Step through a process Explain what happens after I type https://example.com into a browser and press Enter. Make an annotated, animated-looking pipeline from DNS through TCP and TLS, HTTP, and rendering, with controls to step through each stage. No web search. |
|
![]() Markdown source |
![]() Live example Β· Generated source |
| Prompt 8 Β· Explore live data Build an accessible color palette lab. Include HSL sliders, five live swatches with hex values, a foreground/background contrast checker, and AA/AAA status badges. Make it vivid, compact, and polished; no web search. |
|
![]() Markdown source |
![]() Live example Β· Generated source |
Generate or refresh the stored OpenRouter responses:
node scripts/generate-readme-markdown-examples.mjsThe generator submits each stored prompt directly as the only input, without a
system instruction or response-format guidance. It defaults to
google/gemini-3.1-pro-preview; set CHATHTML_README_MARKDOWN_MODEL to override
it. This command invokes the provider and uses provider credits. To regenerate
one or more examples, pass their slugs as arguments.
Render the stored Markdown answers as plain screenshots without a provider call:
node scripts/render-readme-markdown-examples.mjsTo create or refresh a ChatHTML example while the dev server is running:
node scripts/generate-readme-example.mjs \
"example-slug" \
"Your prompt" \
"docs/images/example-slug.png"The ChatHTML generator uses the normal server prompt, provider request,
streaming protocol, persistence layer, and iframe renderer. It writes the raw
response to docs/examples/<slug>.chathtml.html. It defaults to
google/gemini-3.1-pro-preview; set CHATHTML_README_MODEL to override it. The
command invokes the provider and therefore uses provider credits.
Publish generated examples to stable hosted share links:
npx tsx scripts/share-readme-examples.ts example-slugThe publisher wraps each saved ChatHTML response in the same sandbox document
used by the app, then creates or updates its stable link on
https://chat.aietheia.com. Omit the slugs to publish every README example; set
CHATHTML_README_SHARE_ORIGIN to use another compatible host.
Requires Node.js 22.13 or newer, npm, and an OpenRouter API key.
git clone https://github.com/aietheia/ChatHTML.git
cd ChatHTML
npm installCopy .env.example to .env (cp .env.example .env on macOS/Linux or
Copy-Item .env.example .env in Windows PowerShell), then set at least:
OPENROUTER_API_KEY=your_openrouter_key_here
OPENROUTER_MODEL=google/gemini-3.5-flash
OPENROUTER_REASONING_EFFORT=lowThe backend loads the root .env and an optional overriding apps/web/.env.
Environment keys stay on the server and are never sent to the browser.
npm run devOpen http://127.0.0.1:5173. The Vite client runs on port 5173; its Express
API proxy runs on http://127.0.0.1:8787.
π If ChatHTML is useful to you, consider giving it a star. Bugs can be reported in the app or opened directly in GitHub Issues.
Everything below covers how ChatHTML works, how to configure it, and what to check before running it beyond local development. For the shortest production path, start with Configuration, choose a persistence backend, review the security model, and then use the checked-in production operations assets.
flowchart LR
A[User prompt] --> B[ChatHTML server]
B --> C{Responses tool loop}
C -->|retrieve, files, memory| C
C --> D[NDJSON reasoning + content stream]
D --> E[Protocol parser]
E --> F[Partial HTML completion]
F --> G[Sandboxed iframe]
- The client sends the conversation, canvas size, theme, files, and provider
settings to
POST /api/chat. - The backend adds ChatHTML's system, canvas, theme, legibility, memory, and file context before calling a Responses-compatible model endpoint.
- Model tool calls are executed in the same response loop. Reasoning and content are returned to the browser as sequenced NDJSON events.
- The client extracts the protocol tags and feeds
<streamui>content to the streaming renderer. Incomplete tags are safely completed for each preview frame. - Once the stream is complete, scripts and artifact actions are enabled in a fresh sandbox document.
| Area | What is included |
|---|---|
| Rendering | Progressive HTML/CSS rendering, speculative completion, light/dark theme context, responsive canvas sizing |
| Interaction | Vanilla JavaScript, local controls, conversation actions, copy/download/open-link capability requests |
| Authoring | Region selection, targeted artifact edits, regeneration branches, screenshot-based visual repair |
| Retrieval | Brave, Tavily, Serper, DuckDuckGo fallback, direct URL fetching, optional Playwright browsing |
| Visual sources | Openverse, Wikimedia-oriented search, NASA, Library of Congress, The Met, Art Institute of Chicago, plus optional Pexels, Unsplash, and Rijksmuseum providers |
| Files | Draft image uploads, multimodal image reads, session file tools, capability URLs, saved raw artifact source |
| Persistence | SQLite for zero-setup local use; PostgreSQL with per-user state and concurrent access for production |
| Export | Copy source/text, standalone HTML, PNG, SVG, diagnostics, and optional hosted share links |
| Mode | Request path | Secret handling | Best for |
|---|---|---|---|
| Environment | Browser to ChatHTML server to provider | Key remains in server environment | Local development and self-hosting |
| Manual | Browser directly to provider | Key is kept in browser sessionStorage |
Signed-out use with a CORS-capable provider |
| Managed | Browser to authenticated ChatHTML Service gateway | Opaque service session stays in an HttpOnly cookie | Hosted deployments |
Manual mode never falls back through the ChatHTML server. Non-secret provider settings are kept in local storage, while the key disappears with the browser session. Plain HTTP provider URLs are rejected except for loopback development endpoints.
The complete, annotated list lives in .env.example. The most
useful settings are:
| Variable | Default | Purpose |
|---|---|---|
OPENROUTER_API_KEY |
none | Server-side OpenRouter credential |
OPENROUTER_MODEL |
google/gemini-3.5-flash |
Default generation model |
OPENROUTER_REASONING_EFFORT |
low |
none, minimal, low, medium, or high |
STREAMUI_RETRIEVAL |
true |
Enable the model's native retrieval tool |
STREAMUI_SEARCH_PROVIDER |
auto |
Search provider selection |
STREAMUI_BROWSER_ENGINE |
fetch |
Use fetch or Playwright for page retrieval |
STREAMUI_SESSION_DB |
./sessions/state.sqlite |
SQLite state file |
CHATHTML_DATABASE_URL |
none | PostgreSQL connection string for production |
CHATHTML_AUTH_REQUIRED |
false outside production | Require a hosted account |
With STREAMUI_SEARCH_PROVIDER=auto, configured API providers are preferred
and DuckDuckGo can be used as a fallback. Several visual and museum sources work
without keys; optional provider keys expand coverage.
The retrieval policy blocks private network targets by default. Domain allow
and block lists, timeouts, page limits, and context budgets are configurable in
.env.
SQLite is the zero-setup default. It uses WAL mode and a serialized write queue, which is appropriate for local development and small single-instance installs. Place the database on persistent storage if the deployment filesystem is ephemeral:
STREAMUI_SESSION_DB=/data/chathtml/state.sqliteUse PostgreSQL for a hosted or horizontally scaled deployment:
CHATHTML_DATABASE_URL=postgresql://chathtml_app:password@127.0.0.1:5432/chathtml
CHATHTML_DATABASE_POOL_SIZE=10
CHATHTML_AUTH_REQUIRED=truePostgreSQL stores one atomic state row per authenticated user, serializes updates for the same user with a row lock, and allows different users to read and write concurrently.
To migrate legacy SQLite data into an existing account, back up the source and run:
STREAMUI_SESSION_DB=/data/chathtml/state.sqlite \
CHATHTML_DATABASE_URL=postgresql://... \
CHATHTML_MIGRATION_USER_ID=00000000-0000-4000-8000-000000000000 \
npm run migrate:sessionsThe migration is transactional and idempotent. It verifies counts and hashes, refuses to overwrite non-empty target data, and rotates stored file capabilities.
The wire protocol retains the legacy <streamui> name for compatibility with
existing sessions and renderer code:
<sessiontitle>Concise history title</sessiontitle>
<chat></chat>
<streamui>
<section class="streamui-response">
<div class="streamui-chat">
<p>All user-facing content lives in this artifact.</p>
</div>
</section>
<script>
// Optional small vanilla JavaScript. Runs after completion.
</script>
</streamui><sessiontitle>becomes the hidden history title.<chat>remains empty for visual responses; it is used as a fallback for non-artifact text.<streamui>is rendered chunk by chunk and must remain open until the artifact is finished.- Built-in
streamui-*classes and CSS variables provide accessible prose, buttons, muted text, actions, and theme-aware colors. - Scripts are ignored during streaming and run only after a complete artifact has been installed in the iframe.
If a response has no valid <streamui> block, it is displayed as a normal
assistant message.
Generated controls can ask the host to perform safe actions without calling sensitive browser APIs directly:
<button data-streamui-prompt="Show a simpler example">Simplify</button>
<button data-streamui-copy-target="#result">Copy result</button>
<button
data-streamui-download-target="#result"
data-streamui-filename="result.txt"
>Download</button>
<button data-streamui-open-url="https://example.com">Open source</button>The host confirms capability actions and performs them outside the artifact.
- Server-managed provider credentials are never serialized into frontend settings.
- Artifacts render with
sandbox="allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox". - The iframe CSP permits required HTTPS resources while disabling objects, forms, and base URL changes.
- Browser storage, cookies, parent/top/opener access, permissions APIs,
document.write, and clipboard reads are blocked or reported by the runtime. - Session files use stable IDs and unguessable capability URLs. Draft files are invisible to model file tools until the user sends the message.
- Hosted session ownership comes from the authenticated server identity, never from a browser-supplied client ID.
The sandbox is a defense boundary for generated artifacts, not permission to run untrusted modifications to the ChatHTML host itself. Review the policy and deployment configuration before exposing a customized runtime publicly.
Standalone HTML hosting is supplied by the separate
aietheia/oops service. A production proxy
should route POST /api/html-shares and /artifacts/* to that service. Share
Link is hidden by default; enable it only after those routes exist:
VITE_CHATHTML_ARTIFACT_SHARE_LINKS=trueLocal HTML, PNG, and SVG export works without the hosting service.
This repository is an npm workspace:
| Path | Purpose |
|---|---|
apps/web |
Full Vite/React client and Express backend |
apps/webdemo |
Browser-local, reduced Web Demo surface |
docs/cloud-api.md |
Hosted HTTP and native-auth contracts |
ops/production |
Backup, monitoring, systemd, and Nginx assets |
Useful commands:
npm run dev # full app on :5173, API on :8787
npm run dev:webdemo # reduced web demo on :5174
npm test # web client and server unit tests
npm run test:webdemo # web demo unit tests
npm run test:e2e # Playwright browser suite
npm run build # type-check and production buildImportant implementation entry points:
apps/web/src/server/systemPrompt.tsβ model behavior and output contractapps/web/server/openrouterChatExecution.tsβ Responses API tool loop and stream generationapps/web/src/runtime/streamui/protocol.tsβ protocol extractionapps/web/src/runtime/streamui/streamingRenderer.tsβ progressive renderer lifecycleapps/web/src/runtime/streamui/sandboxDocument.tsβ iframe document, CSP, and sandbox runtimeapps/web/server/retrieval.tsβ search, fetching, parsing, and retrieval contextapps/web/server/sessionRepository.tsβ SQLite/PostgreSQL persistence
The production browser audits create disposable accounts and the full audit runs one managed generation. They modify production data and therefore require an explicit flag:
node scripts/auth-relogin-repro.mjs --confirm-production-audit
node scripts/production-alpha-audit.mjs --confirm-production-auditReports are written to the ignored test-results/ directory. Both scripts
delete their test account before exiting and return a non-zero status if the
audit is not clean. Use CHATHTML_AUDIT_APP_BASE and
CHATHTML_AUDIT_SERVICE_BASE to target a non-default environment.
For hosted authentication, account, and native-wrapper integration, see the Cloud API contract. For operational assets and backup expectations, see Production operations.
ChatHTML is available under the MIT License.






















