Sparks Widget
July 28, 2026 · View on GitHub
Configurable chat widget engine for Sparks-stack storefronts. One repo, many personas.
What this is
A small Node/Express backend (~190 LOC) + a vanilla-JS embeddable widget. Drops onto any static site as a <script> tag and talks directly to OpenRouter. Deliberately stateless: no visitor id, no cookies, no memory, no per-visitor storage. Peter answers from a markdown knowledge file and remembers nothing between conversations.
Engine vs persona
The widget is a generic engine. The customer-facing personality is configuration:
- Engine (this repo): server.js, the embed JS, the chat-log writer.
- Persona (per-customer): assistant name, avatar, system prompt, product knowledge file.
Today the persona for the peter-customer instance lives partially in server.js (the SYSTEM_PROMPT constant) and partially in ~/peter-customer/knowledge/products.md. The externalization to a full per-customer config file is roadmap work (see TODO below) — current shape ships as one bound instance.
Deployed instances
| Instance | Persona | Site | AGENT_ID | Service unit |
|---|---|---|---|---|
peter-customer | Peter (cartoon lobster, Project Sparks mascot) | projectsparks.ai | peter-widget | peter-widget.service (user) |
When a new customer comes online (e.g. Rocky's Gallery on Shopify, master-todo #15), expect a new row here, not a new repo.
Architecture notes
The two-copy trap (live wire format)
The embed JS exists in two places:
- This repo —
rocky-widget.js(filename kept for compat). Backend serves it at/rocky-widget.jsas a fallback. projectsparks-site/rocky-widget.js— Firebase-served, the version visitors actually load.
The site's copy is the live wire format. Its CSS class names (#rocky-widget-bubble, .rw-msg.rocky), the sessionStorage key (rocky_widget_state; the rocky_visitor_id and rocky_widget_memory_on localStorage keys were removed and are no longer written), and global (window.ROCKY_WIDGET_API) are baked into visitor browsers and the static site embed. Don't rename them in either copy without a coordinated site deploy. Drift between the two copies is a recurring footgun — fix one, fix both.
What the backend serves
Production only uses the /api/* routes:
POST /api/chat— chat completion. Takes{messages}, returns{reply}. No identifier accepted.POST /api/log-question— log a question Peter couldn't answer. Takes{question}.
That's the whole API. /api/save-visitor, /api/recall/:id and /api/wipe-visitor were removed 2026-07-28 with the memory system.
The /rocky-widget.js and /rocky-icon.svg routes are unused in production (site has its own copies). Kept as fallback for direct-access debugging.
No memory, by design
There is none, and there is no toggle. Visitor memory was switched OFF on 2026-07-07 and removed entirely on 2026-07-28 (Guy: "we no longer need mnemo or switching for Peter. Just basic cust service info").
The code had been sitting behind a MEMORY_ENABLED = false flag while pointing at a Mnemo instance on port 50002 that was decommissioned the same day it was switched off — a fully-built path to a dead port that no caller could reach. A disabled feature aimed at a decommissioned service is not a feature held in reserve.
The visitor id went with it. It was generated on every page load and read by nothing; an identifier with no purpose is a tracking artifact. Peter now cannot distinguish a returning visitor from a new one, on purpose.
Chat log
Every exchange is appended to ~/peter-customer/logs/chat-YYYY-MM-DD.log as JSONL. The lines carry no identifier of any kind — they record what was asked, not who asked it. Tail:
tail -f ~/peter-customer/logs/chat-$(date -u +%Y-%m-%d).log | jq .
Deploy / operate
npm install
node server.js # or via systemd: systemctl --user start peter-widget.service
Reads ~/.rockys-switch/keys.json for the OpenRouter API key, per request, so a key rotation takes effect without a restart. The directory name is historical — Rocky's Switch itself was retired 2026-07-08; the key file outlived it. Chat goes straight to https://openrouter.ai — no relay, no router, no local service dependency.
Roadmap
- Persona externalization — pull
SYSTEM_PROMPT,AGENT_ID,KNOWLEDGE_DIR, header name/avatar, system-prompt persona strings out ofserver.jsand into per-customer~/<customer>-customer/config.json. One systemd template (sparks-widget@<customer>.service) per instance. Required before adding a second persona. - Wire-format generification — eventually rename
rocky-widget.js/ CSS classes / sessionStorage key / global API var to neutral names (widget.js,sw-*,sparks_widget_*,SPARKS_WIDGET_API). Requires a coordinated site deploy. Not urgent. - Multi-tenant routing — when there are 2+ customers, route by host header or path prefix so one process serves multiple personas.
License
MIT.