@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.
246 lines (200 loc) • 8.9 kB
text/mdx
import { Meta } from '@storybook/addon-docs/blocks';
<Meta title="Docs/Frameworks/Angular" />
# Angular
Angular needs **no wrappers** — its property binding (`[prop]="value"`) assigns the DOM **property**
directly, so arrays and objects pass through unstringified. Register the elements once, allow the
custom tags with `CUSTOM_ELEMENTS_SCHEMA`, and handle events with `(event)="handler($event)"`.
There are **two ways to build with the kit**, and you can mix them:
1. **`<kc-chat>`** — the batteries-included shell: a whole chat experience in one tag. Fastest start.
2. **Compose the individual elements** (`<kc-conversations>`, `<kc-markdown>`, `<kc-artifact>`, …)
into your own layout when you want full control.
Both are shown below.
## Install & setup
```bash
npm i @kitn.ai/chat
```
Register the custom elements once as a static side-effect import in `main.ts`, **before**
`bootstrapApplication(...)`. Then add `CUSTOM_ELEMENTS_SCHEMA` to each standalone component that
uses `<kc-*>` tags:
```ts
// main.ts — import BEFORE bootstrapApplication so elements are defined first
import '@kitn.ai/chat/elements';
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
import { appConfig } from './app/app.config';
bootstrapApplication(AppComponent, appConfig);
```
```ts
// app.component.ts
import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
@Component({
selector: 'app-root',
standalone: true,
templateUrl: './app.component.html',
schemas: [CUSTOM_ELEMENTS_SCHEMA], // required: allows <kc-*> tags in the template
})
export class AppComponent { /* … */ }
```
- **`@kitn.ai/chat/elements`** — the side-effect import that registers every `<kc-*>` element
globally. Import it once, in `main.ts`.
- **`CUSTOM_ELEMENTS_SCHEMA`** — tells the Angular compiler to accept unknown element names (the
`<kc-*>` tags). Add it to every standalone component whose template uses them.
- No CSS to import: each element is styled inside its own Shadow DOM. Only pull in
`@kitn.ai/chat/theme.css` if you want to override design tokens (see **Theming**).
## Quick start — the all-in-one shell
`<kc-chat>` is **transport-agnostic**: give it a `messages` array, handle the `submit` event, and
stream your model's reply back into state. You own the request; the component owns the UI.
```ts
// app.component.ts
import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
type Message = { id: string; role: 'user' | 'assistant'; content: string };
@Component({
selector: 'app-root',
standalone: true,
templateUrl: './app.component.html',
schemas: [CUSTOM_ELEMENTS_SCHEMA],
})
export class AppComponent {
messages: Message[] = [
{ id: '1', role: 'assistant', content: 'Hello! How can I help?' },
];
async onSubmit(e: Event) {
const { value } = (e as CustomEvent<{ value: string }>).detail;
// Reassign a NEW array so Angular's change detection picks it up.
const history = [...this.messages, { id: crypto.randomUUID(), role: 'user' as const, content: value }];
this.messages = history;
const aid = crypto.randomUUID();
this.messages = [...history, { id: aid, role: 'assistant' as const, content: '' }];
let answer = '';
for await (const token of streamFromYourAPI(history)) {
answer += token;
// Reassign a new array — and a new object for the message being streamed.
this.messages = this.messages.map((m) =>
m.id === aid ? { ...m, content: answer } : m
);
}
}
}
```
```html
<!-- app.component.html -->
<!-- The elements are display:block and fill their container — lay them out with flex
and let the element grow with flex:1 rather than hard-coding a height. -->
<div style="display: flex; flex-direction: column; height: 100dvh">
<kc-chat
[messages]="messages"
(kc-submit)="onSubmit($event)"
style="flex: 1; min-height: 0"
></kc-chat>
</div>
```
## Go further — compose the pieces
`<kc-chat>` is one option, not the only one. You can assemble your own layout from individual
elements. Here is a multi-conversation shell — a `<kc-conversations>` sidebar next to the
`<kc-chat>` thread:
```ts
// workspace.component.ts
import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
@Component({
selector: 'app-workspace',
standalone: true,
templateUrl: './workspace.component.html',
schemas: [CUSTOM_ELEMENTS_SCHEMA],
})
export class WorkspaceComponent {
conversations = myConversations;
activeId = this.conversations[0]?.id;
messages = loadMessages(this.activeId);
onConversationSelect(e: Event) {
this.activeId = (e as CustomEvent<{ id: string }>).detail.id;
this.messages = loadMessages(this.activeId);
}
onNewChat() {
startNewConversation();
}
onSubmit(e: Event) {
const { value } = (e as CustomEvent<{ value: string }>).detail;
sendMessage(value);
}
}
```
```html
<!-- workspace.component.html -->
<!-- Lay panels out with flex: the sidebar is fixed-width, the thread takes the rest
with flex:1. The elements fill whatever box you give them. -->
<div style="display: flex; height: 100dvh">
<kc-conversations
[conversations]="conversations"
[activeId]="activeId"
(kc-conversation-select)="onConversationSelect($event)"
(kc-new-chat)="onNewChat()"
style="width: 300px; flex-shrink: 0"
></kc-conversations>
<kc-chat
[messages]="messages"
(kc-submit)="onSubmit($event)"
style="flex: 1; min-width: 0"
></kc-chat>
</div>
```
### Make the panels resizable
Want a draggable divider between the sidebar and the thread? Wrap the panels in `<kc-resizable>`
with one `<kc-resizable-item>` each — the handles are inserted for you (up to 3 panels). Each item
takes a `size` (px or `%`) plus optional `min`/`max`; listen for `(change)` (`$event.detail.sizes`)
to persist the layout.
```html
<div style="display: flex; flex-direction: column; height: 100dvh">
<kc-resizable
orientation="horizontal"
(kc-change)="onResize($event)"
style="flex: 1; min-height: 0"
>
<kc-resizable-item size="25%" min="200px">
<kc-conversations
[conversations]="conversations"
[activeId]="activeId"
(kc-conversation-select)="onConversationSelect($event)"
></kc-conversations>
</kc-resizable-item>
<kc-resizable-item>
<kc-chat [messages]="messages" (kc-submit)="onSubmit($event)"></kc-chat>
</kc-resizable-item>
</kc-resizable>
</div>
```
```ts
onResize(e: Event) {
const { sizes } = (e as CustomEvent<{ sizes: string[] }>).detail;
// persist sizes to localStorage, a service, etc.
console.log('panel sizes:', sizes);
}
```
You can also drop **standalone display elements** anywhere in your own UI — `<kc-markdown>`,
`<kc-code-block>`, `<kc-artifact>`, `<kc-reasoning>`, `<kc-tool>` — to render rich AI content
without adopting the whole chat. Each fills its container and is controlled via `[prop]` bindings
and `(event)` listeners.
> **See it all assembled:** **[Examples → Full Chat App](?path=/story/examples-full-chat-app--default)**
> wires a sidebar, threaded markdown, reasoning, a tool call, a model switcher, a context meter, and
> a rich prompt input into one screen — a working reference to crib from.
> **Find every element:** browse the **Components** section in the sidebar. Each element's **API**
> tab lists its props, events, and copy-paste usage for Angular (and every other framework).
## Props & events
The rule for all elements: **rich data goes in as properties, interactions come out as events.**
Angular's `[prop]="…"` binding writes to the element's DOM property (not an HTML attribute), so
arrays and objects pass through unstringified — no adapter or wrapper needed.
`(event)="handler($event)"` listens for a DOM CustomEvent. `$event` is the `CustomEvent` itself;
read the payload from `$event.detail`:
| Element | Property binding | Event | `$event.detail` |
|---|---|---|---|
| `kc-chat` | `[messages]="messages"` | `(submit)` | `{ value: string }` |
| `kc-conversations` | `[conversations]="…"` | `(conversationselect)` | `{ id: string }` |
| `kc-conversations` | `[groups]="…"` | `(newchat)` | — |
| `kc-conversations` | — | `(togglesidebar)` | — |
| `kc-resizable` | `orientation="horizontal"` | `(change)` | `{ sizes: string[] }` |
| `kc-resizable-item` | `size="25%"` `min="200px"` `max="…"` | — | — |
> **Scalar attributes** like `orientation="horizontal"` or `theme="dark"` can be plain HTML
> attributes (no brackets). Use `[prop]="…"` only when passing non-string values (arrays, objects,
> booleans) or Angular expressions.
> **`CUSTOM_ELEMENTS_SCHEMA` is required** in every standalone component (or NgModule) whose
> template contains `<kc-*>` tags. Without it, the Angular compiler rejects unknown element names at
> build time.