💬 Local Chat

A floating, resizable chat widget powered entirely by Chrome's built-in on-device AI, grounded in context you provide.

Overview

<local-chat> is a self-contained custom element: it renders its own toggle button, transcript, input box, and controls. There's no separate element to attach it to and no server round-trip — every response comes from Chrome's on-device Prompt API (window.LanguageModel), running entirely on the visitor's machine.

Answers are grounded in whatever Context you provide: raw text, a CSS selector that extracts matching page elements, or both combined. If the on-device model API isn't available in the visitor's browser, the element is a complete no-op — it renders nothing and does nothing.

Install

npm install @internetarchive/local-chat

Usage

<script type="module" src="@internetarchive/local-chat"></script>

<local-chat
  context-selector="main"
  starters='["What can you help with?"]'
  icebreakers
  max-followups="3"
></local-chat>

The chat widget on this page is a live example: it's grounded in this very documentation via context-selector="main", so you can ask it questions about <local-chat> itself.

Attributes

Attribute Description
instructions Text establishing the assistant's persona/behavior. Has a sensible built-in default (answer only from the provided Context, say so plainly when something isn't covered). Also available as a property, which takes priority over the attribute.
context Raw text, or a JSON array/object of structured context, to ground answers in. Combines with context-selector when both are set. Also available as a property, which takes priority over the attribute — a string is parsed the same way, or pass an array/object directly (no need to JSON.stringify it yourself).
context-selector A CSS selector. Every matching element's .innerText is concatenated into the Context.
starters A host-provided opening suggestion, or a JSON array of several, shown before any message has been sent. Purely declarative — never generated by the model. Also available as a property, which accepts a single string the same way the attribute does, or an array of strings directly.
icebreakers Boolean. When present, the model generates its own opening suggestions (grounded in Context) once ready, shown alongside any starters.
empty-message Plain text shown before the first message, only while neither a starters entry nor a generated Icebreaker exists yet to show instead. Has a sensible built-in default describing the Widget as an on-device AI chat. Also available as a property, which takes priority over the attribute.
max-followups Non-negative integer capping how many Follow-ups get suggested after each response. Default: 3. Set to 0 to disable Follow-ups (and Icebreakers).
max-history Non-negative integer capping how many past Exchanges (a user message and its reply, saved as a pair once the reply finishes) are persisted and restored across page loads. Default: 5. Set to 0 to disable History entirely.
storage-key The storage scope shared by History and by the widget's own Visual state (open/closed, dragged position, resized size). "url" (default) scopes to the current path and query string; "path" scopes to the path only (shared across query variations); "origin" shares both across the entire site. Any other value is used verbatim as a custom scope, shared by every page that sets the same storage-key.
collapsed The widget's initial visibility state, used only the very first time nothing's been persisted yet for the resolved storage-key scope (see Visual state below). Defaults to collapsed (a floating button) unless set to the literal string "false", which starts it expanded.
trigger-selector A CSS selector. Every matching element (queried once, at render time) becomes a trigger — clicking it toggles the widget open/closed, the same as calling toggle(). Useful for invoking the widget from your own existing button instead of the floating toggle.
hide-toggle Boolean. Hides the built-in floating toggle button — useful alongside trigger-selector or the methods below, if you want only your own trigger.
logo Overrides the default 💬 glyph, used both for the collapsed toggle button and, in the panel header, right before the title. Plain text/emoji renders as-is; a value that looks like an image reference (a data:image/ URI, a URL, or a filename ending in .png/.jpg/ .gif/.svg/.webp/.avif) renders as an image instead. Also available as a property, which takes priority over the attribute.
title Renders as a heading in the panel header, right after the logo. This is the native title attribute/property every HTML element already has — no separate accessor needed. Defaults to "Local Chat" when unset.
color-scheme Forces "light" or "dark" mode regardless of the OS/browser's dark-mode preference. Unset (or any other value) follows that preference instead, via a built-in dark palette on every themed custom property below — see Styling. Also available as a property, directly reflected onto the attribute.

Methods

Method Description
expand() Expands the widget (a no-op if already expanded). Call this from your own trigger element's click handler.
collapse() Collapses the widget (a no-op if already collapsed).
toggle() Expands if collapsed, collapses if expanded. This is what trigger-selector wires up automatically.

Events

Event Detail
local-chat-message-sent { text } — dispatched when the user sends a message.
local-chat-response-received { text } — dispatched with the full rendered response once streaming finishes.
local-chat-error { error } — dispatched if generating a response fails.
local-chat-expanded Dispatched when the widget transitions to expanded, however it was triggered.
local-chat-collapsed Dispatched when the widget transitions to collapsed, however it was triggered.

Styling

The widget renders in a Shadow DOM, themeable from the host page via CSS custom properties and ::part(). Every custom property below overrides its value in both light and dark mode at once — the values shown are the light-mode defaults; each already has a built-in dark-mode default too, auto-switching with the OS/browser's preference (or forced either way via the color-scheme attribute) unless you override it here:

local-chat {
  --local-chat-font-family: system-ui, sans-serif;
  --local-chat-font-size: 0.9rem;
  --local-chat-accent: #2563eb;
  --local-chat-accent-color: #fff;
  --local-chat-background: #fff;
  --local-chat-color: #111;
  --local-chat-border-color: #ccc;
  --local-chat-radius: 0.5rem;
  --local-chat-assistant-background: #f2f2f2;
  --local-chat-code-background: #e5e5e5;
  --local-chat-status-color: #777;
  --local-chat-empty-state-color: #777;
  --local-chat-shadow-color: rgba(0, 0, 0, 0.25);
}

local-chat::part(panel) {
  /* every part is also addressable directly: toggle, panel, resize-handle,
     panel-header, logo, title, clear, panel-close, transcript, message,
     message-user, message-assistant, status, status-download, input-row,
     input, send, starter(s), icebreaker(s), followup(s), empty-state */
}

Browser support

Requires a Chrome build with the on-device Prompt API (window.LanguageModel) available. Everywhere else, <local-chat> renders nothing and does nothing — it never errors or shows a broken widget.