feat: bootstrap rony-chat-bot Go module
Initial implementation of the bot:
- cmd/chat-bot: CLI entrypoint (serve, reindex, ask, version)
- internal/agent: LLM provider client + agent runner with RAG injection
- internal/config: YAML config loader (providers, RAG, persona, server)
- internal/i18n: response-language detection (EN/ES)
- internal/persona: persona system prompt assembly from YAML
- internal/portfolio: heading-based chunker + SQLite FTS5 indexer
- internal/server: chi router with /api/chat (SSE), /api/health, /api/info,
/api/reindex, middleware (RequestID, Logging, CORS, RateLimit)
- internal/streaming: SSE protocol helpers (start, chunk, sources, done, error)
- web/: drop-in vanilla-JS chat widget (no build, no deps) + demo + README
- bench/: reproducible driver benchmark (modernc vs mattn SQLite)
- configs/portfolio-bot.yaml: llama.cpp default provider, SQLite RAG, canine persona
- docs/architecture.md / .es.md: aligned with SQLite FTS5 + llama.cpp decisions
- data/projects/README*.md: project data documentation
- README.md / .es.md: updated for current implementation
All tests pass (go test ./...). Bot is functional end-to-end with the
configured LLM provider.
2026-07-17 07:56:06 +00:00
# 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:
feat: persistent conversation storage (Phase 4)
Conversations survive page reloads and work for any frontend, not just
the widget. Server-side SQLite, conversation ID as bearer token, browser
identity via localStorage.
Backend
-------
- internal/portfolio/conversations.go: schema + CRUD. Conversations and
messages tables in the same SQLite DB as the RAG index, with
foreign-key cascade delete. Conv IDs are 16-byte random hex
(128 bits of entropy).
- internal/portfolio/indexer.go: applies conversation schema + enables
foreign_keys pragma in OpenStore.
- internal/server/handlers.go: POST /api/chat accepts an optional
conversation_id, mints one if absent, persists user message before
the LLM runs and assistant message (with sources) after the stream
completes. New handlers: GetConversation, ListConversations,
DeleteConversation.
- internal/server/server.go: routes for GET /api/conversations,
GET/DELETE /api/conversations/{id}.
- internal/server/conversations_test.go: 6 tests (round-trip, continue,
list, 404, delete, streaming).
Widget
------
- web/chat-widget.js: stores conv_id in localStorage["rony-chat-conv"],
includes it in the chat request body, captures new IDs from the
server's 'start' SSE event, and calls GET /api/conversations/{id} on
load to restore history. On 404 it clears the stored ID and starts
fresh.
Docs
----
- docs/architecture.md: §3.1 documents the conversation_id field and
new REST endpoints; new §3.4 covers persistence lifecycle, schema,
client responsibilities, and auth model. §5.6 updated; filetree
reflects the new files.
- web/README.md: new 'Conversation persistence' section explains the
browser-scoped behavior and how to opt out or persist across devices.
2026-07-17 07:56:35 +00:00
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.
feat: bootstrap rony-chat-bot Go module
Initial implementation of the bot:
- cmd/chat-bot: CLI entrypoint (serve, reindex, ask, version)
- internal/agent: LLM provider client + agent runner with RAG injection
- internal/config: YAML config loader (providers, RAG, persona, server)
- internal/i18n: response-language detection (EN/ES)
- internal/persona: persona system prompt assembly from YAML
- internal/portfolio: heading-based chunker + SQLite FTS5 indexer
- internal/server: chi router with /api/chat (SSE), /api/health, /api/info,
/api/reindex, middleware (RequestID, Logging, CORS, RateLimit)
- internal/streaming: SSE protocol helpers (start, chunk, sources, done, error)
- web/: drop-in vanilla-JS chat widget (no build, no deps) + demo + README
- bench/: reproducible driver benchmark (modernc vs mattn SQLite)
- configs/portfolio-bot.yaml: llama.cpp default provider, SQLite RAG, canine persona
- docs/architecture.md / .es.md: aligned with SQLite FTS5 + llama.cpp decisions
- data/projects/README*.md: project data documentation
- README.md / .es.md: updated for current implementation
All tests pass (go test ./...). Bot is functional end-to-end with the
configured LLM provider.
2026-07-17 07:56:06 +00:00
## 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.
feat: persistent conversation storage (Phase 4)
Conversations survive page reloads and work for any frontend, not just
the widget. Server-side SQLite, conversation ID as bearer token, browser
identity via localStorage.
Backend
-------
- internal/portfolio/conversations.go: schema + CRUD. Conversations and
messages tables in the same SQLite DB as the RAG index, with
foreign-key cascade delete. Conv IDs are 16-byte random hex
(128 bits of entropy).
- internal/portfolio/indexer.go: applies conversation schema + enables
foreign_keys pragma in OpenStore.
- internal/server/handlers.go: POST /api/chat accepts an optional
conversation_id, mints one if absent, persists user message before
the LLM runs and assistant message (with sources) after the stream
completes. New handlers: GetConversation, ListConversations,
DeleteConversation.
- internal/server/server.go: routes for GET /api/conversations,
GET/DELETE /api/conversations/{id}.
- internal/server/conversations_test.go: 6 tests (round-trip, continue,
list, 404, delete, streaming).
Widget
------
- web/chat-widget.js: stores conv_id in localStorage["rony-chat-conv"],
includes it in the chat request body, captures new IDs from the
server's 'start' SSE event, and calls GET /api/conversations/{id} on
load to restore history. On 404 it clears the stored ID and starts
fresh.
Docs
----
- docs/architecture.md: §3.1 documents the conversation_id field and
new REST endpoints; new §3.4 covers persistence lifecycle, schema,
client responsibilities, and auth model. §5.6 updated; filetree
reflects the new files.
- web/README.md: new 'Conversation persistence' section explains the
browser-scoped behavior and how to opt out or persist across devices.
2026-07-17 07:56:35 +00:00
## 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
< script >
localStorage.removeItem("rony-chat-conv");
< / script >
< script src = "chat-widget.js" data-api-url = "..." defer > < / script >
```
Or expose a "new chat" button in your UI that calls
`DELETE /api/conversations/{id}` then clears the localStorage key.
feat: bootstrap rony-chat-bot Go module
Initial implementation of the bot:
- cmd/chat-bot: CLI entrypoint (serve, reindex, ask, version)
- internal/agent: LLM provider client + agent runner with RAG injection
- internal/config: YAML config loader (providers, RAG, persona, server)
- internal/i18n: response-language detection (EN/ES)
- internal/persona: persona system prompt assembly from YAML
- internal/portfolio: heading-based chunker + SQLite FTS5 indexer
- internal/server: chi router with /api/chat (SSE), /api/health, /api/info,
/api/reindex, middleware (RequestID, Logging, CORS, RateLimit)
- internal/streaming: SSE protocol helpers (start, chunk, sources, done, error)
- web/: drop-in vanilla-JS chat widget (no build, no deps) + demo + README
- bench/: reproducible driver benchmark (modernc vs mattn SQLite)
- configs/portfolio-bot.yaml: llama.cpp default provider, SQLite RAG, canine persona
- docs/architecture.md / .es.md: aligned with SQLite FTS5 + llama.cpp decisions
- data/projects/README*.md: project data documentation
- README.md / .es.md: updated for current implementation
All tests pass (go test ./...). Bot is functional end-to-end with the
configured LLM provider.
2026-07-17 07:56:06 +00:00
## 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.
feat: persistent conversation storage (Phase 4)
Conversations survive page reloads and work for any frontend, not just
the widget. Server-side SQLite, conversation ID as bearer token, browser
identity via localStorage.
Backend
-------
- internal/portfolio/conversations.go: schema + CRUD. Conversations and
messages tables in the same SQLite DB as the RAG index, with
foreign-key cascade delete. Conv IDs are 16-byte random hex
(128 bits of entropy).
- internal/portfolio/indexer.go: applies conversation schema + enables
foreign_keys pragma in OpenStore.
- internal/server/handlers.go: POST /api/chat accepts an optional
conversation_id, mints one if absent, persists user message before
the LLM runs and assistant message (with sources) after the stream
completes. New handlers: GetConversation, ListConversations,
DeleteConversation.
- internal/server/server.go: routes for GET /api/conversations,
GET/DELETE /api/conversations/{id}.
- internal/server/conversations_test.go: 6 tests (round-trip, continue,
list, 404, delete, streaming).
Widget
------
- web/chat-widget.js: stores conv_id in localStorage["rony-chat-conv"],
includes it in the chat request body, captures new IDs from the
server's 'start' SSE event, and calls GET /api/conversations/{id} on
load to restore history. On 404 it clears the stored ID and starts
fresh.
Docs
----
- docs/architecture.md: §3.1 documents the conversation_id field and
new REST endpoints; new §3.4 covers persistence lifecycle, schema,
client responsibilities, and auth model. §5.6 updated; filetree
reflects the new files.
- web/README.md: new 'Conversation persistence' section explains the
browser-scoped behavior and how to opt out or persist across devices.
2026-07-17 07:56:35 +00:00
- **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.