rony-chat-bot/web
Victor Hugo Vargas 18e555e338 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 00:56:35 -07:00
..
chat-widget.css feat: bootstrap rony-chat-bot Go module 2026-07-17 00:56:06 -07:00
chat-widget.js feat: persistent conversation storage (Phase 4) 2026-07-17 00:56:35 -07:00
example.html feat: bootstrap rony-chat-bot Go module 2026-07-17 00:56:06 -07:00
README.md feat: persistent conversation storage (Phase 4) 2026-07-17 00:56:35 -07: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)

<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):

<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:

.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):

---
// 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:

// 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, 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

# 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):

<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.

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.