# rony-chat-widget
A drop-in vanilla-JS chat widget that talks to the Rony Chat Bot backend over Server-Sent Events. No build step, no runtime dependencies, no global CSS pollution.
## Files
| File | Purpose |
|---|---|
| `chat-widget.js` | The widget. Self-contained ~12 KB. |
| `chat-widget.css` | Scoped styles, themable via CSS custom properties. |
| `example.html` | Standalone demo page (use with `python3 -m http.server`). |
## Quick start (any site)
```html
```
The bubble appears bottom-right (or bottom-left), opens a 380×560 panel, and talks to `data-api-url/api/chat` over SSE.
## Configuration (all via `data-*` attributes on the `
```
## Theming (override without forking)
All visual tokens are CSS custom properties on the root element. Set them in your site's stylesheet:
```css
.rony-chat-widget-root {
--rony-accent: #ff6b35; /* bubble + send button + links */
--rony-radius: 4px; /* tighter corners */
--rony-font: "Inter", sans-serif;
}
```
See the full list in `chat-widget.css` (search for `--rony-`).
## Astro integration
The simplest path is the drop-in. Add this to your `Layout.astro` (or any shared layout):
```astro
---
// src/layouts/ChatLayout.astro
import "../path/to/chat-widget.css";
const apiUrl = import.meta.env.PUBLIC_CHAT_API_URL || "http://localhost:7331";
---
```
Notes:
- `is:inline` keeps Astro from hashing/transforming the script tag, so the `data-*` attributes survive.
- `PUBLIC_CHAT_API_URL` is an Astro env var; set it in `.env` per environment.
- The bot's `cors_origins` in YAML must include your Astro dev origin (`http://localhost:4321`).
## React/Next.js
Mount the same script tag in your root layout:
```tsx
// app/layout.tsx
import Script from "next/script";
export default function RootLayout({ children }) {
return (
{children}
);
}
```
## Backend requirements
The widget expects the bot to:
1. Expose `POST /api/chat` accepting `{ messages, stream, conversation_id? }` (see `docs/architecture.md` §3.1).
2. Stream SSE events: `start` (with `conversation_id`), `chunk`, `sources`, `done`, `error` (see `docs/architecture.md` §3.2).
3. Expose `GET /api/conversations/{id}` for history restore (returns 404 if unknown).
4. Allow the page's origin via `cors_origins` in the bot's config.
## Running the example locally
```bash
# 1. Start the bot
./bin/chat-bot serve
# 2. Serve the widget (in another terminal)
cd web
python3 -m http.server 8000
# 3. Open http://localhost:8000/example.html in a browser
```
> Note: `localhost:8000` must be in the bot's `cors_origins` for the demo to work. The default config already includes it.
## Browser support
Modern browsers (Chrome/Edge 90+, Firefox 90+, Safari 15+). Uses:
- `fetch` + `ReadableStream` (for SSE)
- `AbortController`
- CSS custom properties + `prefers-color-scheme`
No polyfills, no transpilation.
## Conversation persistence
The bot persists conversations on the server side (SQLite, see
`docs/architecture.md` §3.4). The widget handles the client side
automatically:
1. **First message** — the server mints a new `conversation_id` and returns
it in the `start` SSE event. The widget saves it to
`localStorage["rony-chat-conv"]`.
2. **Subsequent messages** — the widget sends the saved ID with every
request, so the server keeps appending to the same thread.
3. **Page reload** — on load, the widget reads the stored ID and calls
`GET /api/conversations/{id}` to restore the full history.
4. **Server lost the conversation** (e.g. DB was wiped) — the GET returns
404. The widget clears `localStorage` and starts a fresh thread on the
next message.
**Browser-scoped**: `localStorage` is per-origin, so the same browser
keeps the thread across visits, but a different browser starts fresh.
Clearing site data resets the conversation.
**Server-scoped across devices**: not automatic. The conversation lives
in the SQLite DB but only the browser that created it knows its ID. If
you want cross-device continuity, persist the ID in your user profile
(e.g. after login) and pass it on initial load instead of relying on
`localStorage`. The backend already supports this — see
`docs/architecture.md` §3.4 for the protocol.
**To opt out** (start a fresh conversation on every page load):
```html
```
Or expose a "new chat" button in your UI that calls
`DELETE /api/conversations/{id}` then clears the localStorage key.
## What's not in the widget (yet)
- **Markdown images / tables** — the renderer handles paragraphs, lists, code, links, bold/italic. Tables and images render as raw text. For richer output, swap `renderMarkdown` for `marked` or `markdown-it`.
- **Typing indicators beyond the streaming caret** — the caret at the end of the streaming response is the only indicator. Good enough for short answers.
- **Mobile sheet drag-to-dismiss** — the panel goes full-screen on phones, but can't be swiped away. Add a swipe handler if it matters.
- **Conversation history sidebar** — only the active conversation is shown in the panel. The backend exposes `GET /api/conversations` for a future sidebar.