Repository navigation
Getting Started
npm i @machfivetechchicago/machvive-chat-syncopation-ai// Everything, registered on import.
import '@machfivetechchicago/machvive-chat-syncopation-ai';
// Or cherry-pick; each subpath self-registers its own tag.
import '@machfivetechchicago/machvive-chat-syncopation-ai/canvas';
import '@machfivetechchicago/machvive-chat-syncopation-ai/prompt';Registration happens on import, not on first use. Import order does not matter.
Bundler note. Never mark this package
sideEffects: false, and do not let a tree-shaker treatimport 'pkg/canvas'as dead code — the import is the registration, and dropping it means the tag silently never upgrades. The package's own manifest declaressideEffects: trueand a test enforces it.
The files are published as authored ESM, so a plain <script type="module"> works:
<script type="module">
import 'https://esm.sh/@machfivetechchicago/machvive-chat-syncopation-ai';
</script>One tag. It creates its own services element and defaults to the echo transport,
which replies by repeating what you said — enough to build and style the entire
surface before any model exists.
<machvive-chat-syncopation>
<machvive-chat-syncopation-canvas></machvive-chat-syncopation-canvas>
<machvive-chat-syncopation-prompt slot="footer"></machvive-chat-syncopation-prompt>
</machvive-chat-syncopation>The container is layout and nothing else. It gives you three slots:
| Slot | Typical contents |
|---|---|
header |
A title bar, a close button, a model selector |
| (default) | The canvas, and anything above it such as nudges |
footer |
The composer, the CLI, the voice controls |
<machvive-chat-syncopation theme="dark">
<h2 slot="header">Support</h2>
<machvive-chat-syncopation-nudge
suggestions="Where's my order?|I'd like to return something|Talk to a human"
idle-ms="45000"
idle-text="Still here if you need anything.">
</machvive-chat-syncopation-nudge>
<machvive-chat-syncopation-canvas></machvive-chat-syncopation-canvas>
<machvive-chat-syncopation-prompt slot="footer" placeholder="How can we help?">
</machvive-chat-syncopation-prompt>
</machvive-chat-syncopation>That is a complete support surface: suggested openings for the empty state, a streaming transcript, a composer, and a once-only idle prompt.
Wrap the surface in <machvive-chat-syncopation-services> when the page needs to
reach the bus, wants persistence, or runs two conversations that must stay separate.
<machvive-chat-syncopation-services id="chat" transport="local" model="Llama-3.2-1B" persist>
<machvive-chat-syncopation>
<machvive-chat-syncopation-canvas></machvive-chat-syncopation-canvas>
<machvive-chat-syncopation-prompt slot="footer"></machvive-chat-syncopation-prompt>
</machvive-chat-syncopation>
<machvive-chat-syncopation-history></machvive-chat-syncopation-history>
</machvive-chat-syncopation-services>const chat = document.getElementById('chat');
chat.bus.on('record:added', (record) => {
analytics.track('chat_turn', { role: record.role });
});
await chat.send('Hello');Set these on the services element — or on the container, when you let it create its own and it will pass them through.
| Attribute | Default | Meaning |
|---|---|---|
transport |
echo |
echo, local, remote, or your own registered name |
model |
— | Handed to the transport |
endpoint |
— | Handed to the remote transport |
persist |
off | Keep conversations in IndexedDB. Opt-in, deliberately |
max-turns |
200 |
Oldest turns are trimmed past this |
streaming |
true |
Whether transports stream |
theme |
OS |
light or dark, overriding prefers-color-scheme
|
A bare attribute means on: persist and persist="true" are the same, and
persist="false" turns it off.
Because discovery is by DOM position, this just works — two conversations, two caches, no shared state, no configuration:
<machvive-chat-syncopation-services transport="local" model="Llama-3.2-1B">
<machvive-chat-syncopation><!-- the private one --></machvive-chat-syncopation>
</machvive-chat-syncopation-services>
<machvive-chat-syncopation-services transport="remote" endpoint="/api/chat">
<machvive-chat-syncopation><!-- the hosted one --></machvive-chat-syncopation>
</machvive-chat-syncopation-services>The echo transport exists so you can finish the interface first. When you are
ready, see Writing a Transport — it is an async generator
that yields strings, and that is the entire contract.
These modules are browser-only. class X extends HTMLElement is evaluated at
import, so a server-side import throws ReferenceError: HTMLElement is not defined.
That is inherent to custom elements, not a bug to work around.
| Framework | Import from |
|---|---|
| React / Next.js | inside useEffect, or next/dynamic with ssr: false
|
| Vue / Nuxt |
onMounted, or <ClientOnly>
|
| Svelte / SvelteKit | onMount |
| Astro | client:load |
| Anything else |
await import() behind typeof window !== 'undefined'
|
React also needs the usual custom-element caveats: pass data via attributes or
imperative properties on a ref, and subscribe to custom events with
addEventListener rather than an onEvent prop.
TypeScript needs nothing — every entry point ships a .d.ts and
HTMLElementTagNameMap is augmented, so querySelector is typed.
Next: Component Reference.