164 lines
5.7 KiB
Markdown
164 lines
5.7 KiB
Markdown
|
|
# 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
|
|||
|
|
<link rel="stylesheet" href="/path/to/chat-widget.css">
|
|||
|
|
<script src="/path/to/chat-widget.js"
|
|||
|
|
data-api-url="https://your-chatbot.example.com"
|
|||
|
|
data-title="Ask me anything"
|
|||
|
|
data-greeting="Hi! Ask me about the projects."
|
|||
|
|
data-position="bottom-right"
|
|||
|
|
data-theme="auto"
|
|||
|
|
defer></script>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
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 `<script>` tag)
|
|||
|
|
|
|||
|
|
| Attribute | Default | Notes |
|
|||
|
|
|---|---|---|
|
|||
|
|
| `data-api-url` | *(required)* | Base URL of the chat-bot, e.g. `https://chat.example.com`. No trailing slash. |
|
|||
|
|
| `data-title` | `"Chat"` | Header text. |
|
|||
|
|
| `data-greeting` | `""` | First message shown when the panel opens (no greeting if empty). |
|
|||
|
|
| `data-position` | `"bottom-right"` | `"bottom-right"` or `"bottom-left"`. |
|
|||
|
|
| `data-theme` | `"auto"` | `"auto"` (follows `prefers-color-scheme`), `"light"`, or `"dark"`. |
|
|||
|
|
|
|||
|
|
## Language
|
|||
|
|
|
|||
|
|
The widget UI is bilingual (English / Spanish) with a toggle in the header.
|
|||
|
|
|
|||
|
|
- **Initial language**: `localStorage["rony-chat-lang"]` if set, else detected from `navigator.language` (anything starting with `es` → Spanish, else English).
|
|||
|
|
- **Persisted** across page reloads via `localStorage`.
|
|||
|
|
- **Conversation language is independent**: the bot auto-detects the language of each user message and replies in that language. The toggle only changes the *interface* (placeholder, status, errors, send button).
|
|||
|
|
- **No build step**: strings live in a `STRINGS` object at the top of `chat-widget.js`. Add a new language by adding an entry.
|
|||
|
|
|
|||
|
|
To override the initial language (e.g., force English on a Spanish site):
|
|||
|
|
|
|||
|
|
```html
|
|||
|
|
<script>
|
|||
|
|
localStorage.setItem("rony-chat-lang", "en");
|
|||
|
|
</script>
|
|||
|
|
<script src="chat-widget.js" data-api-url="..." defer></script>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 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";
|
|||
|
|
---
|
|||
|
|
<html>
|
|||
|
|
<head>
|
|||
|
|
<head><slot name="head" /></head>
|
|||
|
|
</head>
|
|||
|
|
<body>
|
|||
|
|
<slot />
|
|||
|
|
<script src="/path/to/chat-widget.js"
|
|||
|
|
data-api-url={apiUrl}
|
|||
|
|
data-title="Ask me anything"
|
|||
|
|
data-position="bottom-right"
|
|||
|
|
data-theme="auto"
|
|||
|
|
defer is:inline></script>
|
|||
|
|
</body>
|
|||
|
|
</html>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
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 (
|
|||
|
|
<html>
|
|||
|
|
<head>
|
|||
|
|
<link rel="stylesheet" href="/chat-widget.css" />
|
|||
|
|
<Script src="/chat-widget.js"
|
|||
|
|
data-api-url={process.env.NEXT_PUBLIC_CHAT_API_URL}
|
|||
|
|
data-title="Ask me anything"
|
|||
|
|
data-position="bottom-right"
|
|||
|
|
data-theme="auto"
|
|||
|
|
strategy="afterInteractive" />
|
|||
|
|
</head>
|
|||
|
|
<body>{children}</body>
|
|||
|
|
</html>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## Backend requirements
|
|||
|
|
|
|||
|
|
The widget expects the bot to:
|
|||
|
|
|
|||
|
|
1. Expose `POST /api/chat` accepting `{ messages, stream }` (see `docs/architecture.md` §3.1).
|
|||
|
|
2. Stream SSE events: `start`, `chunk`, `sources`, `done`, `error` (see `docs/architecture.md` §3.2).
|
|||
|
|
3. 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.
|
|||
|
|
|
|||
|
|
## What's not in the widget (yet)
|
|||
|
|
|
|||
|
|
- **Conversation persistence** — each visit is a fresh conversation. The bot is stateless; add a `conversation_id` cookie + server-side history if you want continuity.
|
|||
|
|
- **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.
|