UNPKG

@kitn.ai/chat

Version:

Framework-agnostic, Shadow-DOM web components for building AI chat interfaces — works in React, Vue, Angular, Svelte, or plain HTML. Authored in SolidJS.

187 lines (146 loc) 9.29 kB
{/* src/stories/docs/generative-ui-overview.mdx */} import { Meta } from '@storybook/addon-docs/blocks'; <Meta title="Generative UI/Overview" /> # Generative UI Agents don't just reply with text — they ask the chat to **render typed, interactive cards**: a form to fill, an action to approve, a plan to pick from, a link to preview. `@kitn.ai/chat` does this with a small, typed **Card Contract** and a turnkey dispatcher. ## The loop ``` agent/server ──CardEnvelope(s)──▶ host sets <kc-cards>.cards ▲ │ │ dispatcher renders the right kc-* by `type` │ │ └── result / next envelopes ◀─ CardPolicy ◀─ kc-card event ◀─ user interacts ``` ## Envelopes vs. cards Two things are easy to conflate — keeping them straight makes the rest click: - An **envelope** is what the agent *sends*: an addressed data wrapper, not UI. Like a postal envelope, it *carries* a card spec but isn't the card itself. - A **card** is what the user *sees*: the rendered, interactive UI the dispatcher mounts for that envelope (a `kc-form`, `kc-confirm`, … inside the shared card chrome). The dispatcher's whole job is **envelope in → card out** — one envelope renders one card. Everything an agent sends is a `CardEnvelope`: ```ts interface CardEnvelope { type: string; // selects the card: form | confirm | choice | tasks | link | embed id: string; // correlates every event back to this card data: unknown; // follows that type's JSON Schema (shipped in dist/schemas) title?: string; // optional chrome heading resolution?: CardResolution; // set once the user acts → renders the read-only view } ``` `data` follows the `type`'s **JSON Schema**, so an agent/server can validate before sending; `id` correlates every event back to this card. (For **remote** cards, a transport `WireFrame` wraps this *same* envelope over `postMessage` — the envelope inside is identical; see Remote cards.) ## Turnkey: drop `<kc-cards>` ```html <kc-cards></kc-cards> <script type="module"> import '@kitn.ai/chat/elements'; const cards = document.querySelector('kc-cards'); cards.cards = [{ type: 'confirm', id: 'deploy', title: 'Deploy?', data: { body: 'Ship it?', actions: [{ id: 'go', label: 'Deploy', default: true }] } }]; cards.policy = { onAction: (id, action) => console.log(id, action) }; </script> ``` `<kc-cards>` renders one card per envelope and routes every interaction through `policy` (or listen for the raw bubbling `kc-card` event). In SolidJS, use `renderCard(envelope)` / `<CardRenderer>` inside a `<CardProvider>`. ## Streaming & follow-ups Swap the `cards` array to add, replace, or remove cards — e.g. replace a `confirm` with a result card once the user acts. For **live streaming into a single card**, the SolidJS `<CardRenderer>` re-renders reactively as its envelope's `data` changes, so an agent can stream into it as work progresses. (Keyed-by-`id` in-place updates within a `<kc-cards>` list are a planned refinement.) ## Resolved cards Interactive cards (`kc-confirm`, `kc-choice`, `kc-tasks`, `kc-form`) flip to a **chromed read-only** view the moment the user acts — optimistically, before any server round-trip. To make that survive a **reload or history re-hydration**, the card reads an optional `resolution` field on its `CardEnvelope`. Persist it by feeding the card's terminal event back into the array with the `applyResolution` helper: ```ts import { applyResolution } from '@kitn.ai/chat'; // <kc-cards> is controlled — persist the resolution so it survives reload: el.addEventListener('kc-card', (e) => { el.cards = applyResolution(el.cards, e.detail); // resolved cards re-hydrate read-only // …then save el.cards to your store/server. }); ``` `applyResolution` is pure and safe to call on every `kc-card` event — non-terminal events (`ready` / `error` / …) and unknown card ids return the same array unchanged. The terminal verbs are `action` (confirm / choice) and `submit` (tasks / form). ## Remote cards Everything above renders cards you **bundle yourself** — the dispatcher mounts a native `kc-*` element for each envelope. That native `types` seam stays the easy path: register a card type, ship its element, done. **Remote cards** are the power feature for **provider-owned, cross-origin UI** — a card whose code lives on a *different* origin (a partner, a plugin, your own card service) and that you don't want to bundle or trust in your DOM. `<kc-remote>` renders it inside a **sandboxed cross-origin `<iframe>`** and bridges it to the host over `postMessage`, carrying the **exact same** `CardEnvelope` / `CardContext` / `CardEvent` shapes and routing every event through the **same `CardPolicy`** as a native card. The envelope inside is identical — a transport `WireFrame` just wraps it on the wire. ```html <kc-remote provider-origin="https://cards.provider.example" src="https://cards.provider.example/card" ></kc-remote> <script type="module"> import '@kitn.ai/chat/elements'; // registers <kc-remote> const el = document.querySelector('kc-remote'); // The CardEnvelope is set as a JS property — it travels down the wire unchanged. el.envelope = { type: 'form', id: 'signup', title: 'Join the beta', data: { type: 'object', required: ['email'], properties: { email: { type: 'string', title: 'Email', format: 'email' } } } }; // Every routed event is re-emitted as a bubbling kc-card CustomEvent — persist the // resolution exactly like a native card: el.addEventListener('kc-card', (e) => { if (e.detail.kind === 'submit') save(e.detail.data); }); // …or pass a CardPolicy directly: el.policy = { onSubmit: (id, data) => save(data) }; </script> ``` Prefer the **imperative** SDK when you manage the lifecycle yourself (theme refresh, swapping cards in one warm frame): ```ts import { mountRemoteCard } from '@kitn.ai/chat'; const handle = mountRemoteCard({ container: document.querySelector('#slot'), providerOrigin: 'https://cards.provider.example', src: 'https://cards.provider.example/card', envelope, context: { theme: { mode: 'dark' }, locale: 'en', authToken }, policy: { onSubmit: (cardId, data) => { /* persist via applyResolution, advance the chat */ } }, }); handle.updateContext({ theme: { mode: 'light' } }); // re-themes the framed card live handle.update(nextEnvelope); // swap cards in the same iframe handle.destroy(); // teardown + remove the frame ``` **Inputs.** `providerOrigin` is the exact origin (`https://…`, or `http://localhost` for dev) the iframe is pinned to. `src` is the provider page URL (must be on `providerOrigin`). `envelope` is the `CardEnvelope` to render. `policy` is the optional `CardPolicy`. `context` carries theme/locale/`a11y` and a short-lived `authToken`. **Theme changes remount; token/locale refreshes are silent (v1).** A host **theme change re-renders (remounts) the remote card**, so any in-progress remote-card state (e.g. a half-filled form) **resets on a theme toggle** — renderers apply theme imperatively at mount, and v1 has no in-place renderer theme hook yet. **Token / locale / `a11y` refreshes are silent** (no remount — renderers read `host.context()` on demand). A future refinement is a renderer context subscription for live re-theme without remount. **Security model.** The host mints a per-instance **nonce** and pins every inbound frame on **origin + source window + nonce + negotiated protocol version** — a frame failing any check is dropped, so a wrong-origin or forged `postMessage` can't drive your policy. The iframe is sandboxed (`allow-scripts allow-forms allow-same-origin`, **no** `allow-popups`) and gets a `no-referrer` policy. Any `authToken` you pass should be **short-lived** (the provider sees it). The **host page must set `Content-Security-Policy: frame-ancestors 'self'`** (or `'none'`) so it can't itself be reframed; the **provider** locks who may embed it with its own `frame-ancestors` listing your exact host origin (no wildcards). **Reference runtime.** `examples/remote-provider/` is the runnable provider iframe a real provider copies — it calls `createCardBridge` (from `@kitn.ai/chat/provider`, a SolidJS-free bundle) and registers one renderer per card `type`. `examples/remote-host/` is the matching host page. The real cross-origin behavior is verified by the standalone Playwright suite (`tests/e2e/`). ## Card elements are `kc-*` components too Each card type (`kc-confirm`, `kc-choice`, `kc-tasks`, `kc-form`, `kc-link`, `kc-embed`, `kc-remote`) is a registered `kc-*` custom element with its own prop/event API — the same model as everything else under **Components**. They live here under **Generative UI / Cards** because they only make sense inside the dispatcher/envelope system, but they follow the same component contract: attributes, JS properties, a `kc-card` event, CSS parts. Browse **Generative UI → Cards** for the per-element API references and live demos. See **Cards** for each card type, and **SDK** for the dispatcher in action.