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

InstancePersonaSiteAGENT_IDService unit
peter-customerPeter (cartoon lobster, Project Sparks mascot)projectsparks.aipeter-widgetpeter-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:

  1. This reporocky-widget.js (filename kept for compat). Backend serves it at /rocky-widget.js as a fallback.
  2. 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 of server.js and 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.