npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

assistant-chat

v0.3.1

Published

Purely visual chat web component (Lit) for any AI model — or fake models. No API calls: the host owns data and transport.

Readme

assistant-chat

A purely visual chat web component (Lit 3) for connecting to any AI model — or fake models. It is a 100% visual layer: the component never makes API calls. The host app owns data and transport; <assistant-chat> renders state and emits intent events.

Streaming architecture based on opencode patterns (dual snapshot+delta channel, conditional pacing, block-level markdown projection with caching, incomplete markdown healed with remend).

Features (v1)

  • Autosizing composer — Enter sends, Shift+Enter inserts a newline, IME guard
  • User message bubble (plain text, line breaks preserved) with copy and rewind conversation
  • Assistant answers rendered via streaming (XSS-safe markdown through DOMPurify, fences/links healed mid-stream, highlight.js code highlighting)
  • Graphics in answers: sanitized inline SVG (charts/diagrams straight from the model) and responsive images — while an SVG streams, a skeleton holds its place and the graphic lands atomically when the tag closes; the reveal typewriter skips the markup
  • Interactive charts: the model emits a ```chart fence with declarative JSON (data, never code) and the component renders it natively — bar/line/area/pie/donut/scatter, hover tooltips, legend with series toggling, keyboard navigation — plus a download menu exporting the chart's current state as an interactive HTML page, PNG or PDF (all dependency-free; invalid JSON degrades to a regular code block)
  • Pluggable diagram renderers (Mermaid, PlantUML, …): registerFenceRenderer("mermaid", …) teaches a fence dialect at runtime — the host loads the tool itself (its own dependency or a CDN import), assistant-chat never bundles or fetches it; an unregistered language (or a failed render) degrades to a regular code block
  • Native reveal speed (speed property: slow/normal/fast or 0100) — the component types the answer at a set pace, smoothing bursty streams
  • Citations: inline markers ([1]) + "Sources" footer with clickable pills
  • "Thinking…" indicator, empty state, error card
  • Bottom scroll anchoring while streaming + "jump to bottom" button
  • Morphing send/stop button (chat-stop intent event)
  • Light/dark theming (light-dark() following the page's color-scheme), CSS custom property tokens, CSS Shadow Parts, built-in i18n (en/pt/es/fr/de)

Quick start

npm install   # dependencies
npm run dev   # demo (/demo/) and docs (/docs/) at http://localhost:5173
npm run build # publishable dist/: bundles + d.ts + custom-elements.json + package.json
npm test      # adapter tests (SSE wire-format mocks)

Distribution (same model as the data-grid): npm run build produces a ready-to-publish package in dist/assistant-chat.js (Rollup, self-contained minified ESM), assistant-chat.min.js (esbuild, for no-bundler pages), .d.ts types, custom-elements.json and a flat package.json. Publish with npm publish ./dist. Without a bundler, an import map is enough (examples/standalone.html):

<script type="importmap">
  { "imports": { "assistant-chat": "./assistant-chat.min.js" } }
</script>
<script type="module">
  import { attachChatHost } from "assistant-chat" // identical to the npm import
</script>
<assistant-chat></assistant-chat>
<script type="module">
  import { attachChatHost } from "assistant-chat"

  const chat = document.querySelector("assistant-chat")

  // ANY answer source: fake model, fetch/SSE, WebSocket… you decide.
  attachChatHost(chat, async ({ text, messages, reply, signal }) => {
    for (const word of `You said: ${text}`.split(" ")) {
      reply.append(word + " ")
      await new Promise((r) => setTimeout(r, 30))
    }
    reply.setCitations([{ id: "1", title: "Source", url: "https://…", marker: "[1]" }])
    reply.done() // or reply.error("message")
  }, { onRewind: "truncate" })
</script>

OpenAI and Anthropic patterns, directly — the adapters parse the APIs' streaming result (the fetch is yours; the library never calls the API):

import { openAIStream, anthropicStream, pipeToReply } from "assistant-chat"

attachChatHost(chat, async ({ messages, reply, signal }) => {
  const res = await fetch(/* your OpenAI-compatible or Anthropic endpoint */)
  await pipeToReply(openAIStream(res), reply)   // or anthropicStream(res)
})

anthropicStream converts citations_delta into component citations automatically. Runnable documentation (mocks of the real wire formats): run npm run dev and open /docs/.

Without the attachChatHost sugar, the primitive API is just this:

chat.messages = [...]                                  // snapshot channel (idempotent replace)
chat.appendDelta({ messageId, partId, delta: "…" })    // live channel (append-only, droppable)
chat.addEventListener("chat-send",   (e) => e.detail.text)
chat.addEventListener("chat-rewind", (e) => e.detail.messageId)

Streaming protocol (dual channel)

  1. When the turn starts, push a snapshot with the assistant message containing an empty text part and status: "streaming".
  2. Call appendDelta per chunk (at any rate).
  3. When it finishes (or on any resync/replay/error), push the final snapshot with the full text and status: "complete".

A missed delta is healed by the next snapshot; a stale snapshot (a prefix of the live text) does not clobber the accumulated text (the preserveDelta rule); replays are no-ops. Ids must sort lexicographically in creation order (use newId()).

API

Propertiesmessages: ChatMessage[], busy?: boolean (derived by default), rewindMessageId?: string (dims id >= value), speed?: "slow"|"normal"|"fast"|number (native reveal typewriter, default "normal"; numbers 0 slowest → 100 fastest≈instant — the host feeds text, the component paces the on-screen reveal), language?: string (default: context → <html lang> → browser; built-in en/pt/es/fr/de translations, extensible via setChatTranslation; setChatLanguage(lang) sets the subtree language through context), attributes no-rewind, no-copy.

MethodsappendDelta(d), scrollToBottom(behavior?), focusComposer(), setDraft(text).

Events (all bubbles + composed):

| Event | detail | Notes | |---|---|---| | chat-send | { text } | cancelable — preventDefault() keeps the draft | | chat-stop | { messageId \| null } | the composer button morphs into stop while streaming | | chat-rewind | { messageId } | pure intent; the host mutates state | | chat-copy | { messageId, text, ok } | informational (clipboard already written) | | chat-citation-click | { messageId, partId, citation } | cancelable — default opens citation.url | | chat-chart-download | { messageId?, partId?, format, spec, hiddenEntries, filename } | cancelable — default generates and downloads the artifact |

Slotsslot="empty" (empty state), slot="composer" (replaces the composer).

Parts::part(container), ::part(content), ::part(user-message), ::part(agent-message), ::part(composer), ::part(composer-textarea), ::part(composer-button), ::part(citation), ::part(queue), ::part(queue-item) to restyle layout, messages, the default composer, citation pills and the send queue straight from the page, no tokens needed.

Send queue — messages typed while an answer is generating are queued as pills above the composer (RTL-safe, inline-end aligned) and auto-sent one per turn as it finishes. Each queued item can be removed before it's sent.

Citations — data on the part (citations: [{ id, title?, url?, snippet?, marker? }]), never markdown syntax. With marker: "[1]" the literal becomes an inline chip; without it, the source only shows in the footer.

Interactive charts — teach your model (via system prompt) to answer with a fenced chart block containing declarative JSON; the component does the rest:

```chart
{
  "type": "bar",
  "title": "Revenue by quarter",
  "x": ["Q1", "Q2", "Q3", "Q4"],
  "series": [
    { "name": "2024", "data": [10, 14, 13, 19] },
    { "name": "2025", "data": [12, 18, 15, 24] }
  ]
}
```

Types: bar (plus "stacked": true), line, area, pie, donut, scatter (data: [[x, y], …]). Optional: xLabel, yLabel, yMin, yMax, per-series color (#hex). While the fence streams, a skeleton holds its place; when it closes the chart mounts with tooltips, a toggleable legend and a download menu (interactive HTML page, PNG, PDF — always the chart's current state). Listen to chat-chart-download and call preventDefault() to handle the artifact yourself. Full schema in SPEC.md §4c.

Diagrams via Mermaid (or any other tool) — without shipping it: register a renderer once; the library only ever holds the function, never the diagram tool itself:

import { registerFenceRenderer } from "assistant-chat"

registerFenceRenderer("mermaid", async (source) => {
  const { default: mermaid } = await import("https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs") // or your own `import mermaid from "mermaid"`
  mermaid.initialize({ startOnLoad: false, securityLevel: "strict", flowchart: { htmlLabels: false } }) // plain <text>, not <foreignObject> — see SPEC.md §4d
  const { svg } = await mermaid.render(crypto.randomUUID(), source)
  return { html: svg } // sanitized by the component (same DOMPurify config as the rest of the markdown)
})

Now any ```mermaid fence in an answer renders as a diagram — a skeleton while the fence streams and while the renderer is loading/running, the diagram once it resolves. No renderer registered, a thrown error, or a declined (null) result all fall back to a regular highlighted code block. Details in SPEC.md §4d.

Theming

The component is transparent: outer border, background and theme belong to the host page — text inherits the page color and the internal tokens follow the color-scheme THE PAGE defines (e.g. html { color-scheme: dark }). Fine-tuning via tokens:

html { color-scheme: light dark; }        /* the theme is yours, not the component's */

assistant-chat {
  background: #fff;                        /* background/border: regular page CSS */
  border: 1px solid #e5e7eb;
  --assistant-chat-accent: #7c3aed;               /* internal tokens (bubble, code, pills…) */
  --assistant-chat-user-bubble-bg: #ede9fe;
  --assistant-chat-radius: 14px;
  /* …see SPEC.md §8 for the full table */
}

Documentation

SPEC.md — the full contract: data model, streaming pipeline, pacing contract, citations, rewind, theming, a11y and v2 seams. Runnable docs with live examples: /docs/.