@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.
592 lines (555 loc) • 19.8 kB
text/typescript
import type { ExampleUsage, StoryUsage } from './types';
/**
* Build the web-component snippets. `<kc-context>` takes a single `context`
* object PROPERTY (`usedTokens`, `maxTokens`, plus optional input/output/
* reasoning/cache token counts + `estimatedCost`) and renders the trigger,
* popover, and breakdown for you.
*
* Color thresholds are configurable via numeric properties:
* - `warnThreshold` (default 0.7) — fraction above which the bar turns yellow
* - `dangerThreshold` (default 0.9) — fraction above which the bar turns red
*
* When the computed severity level changes (`ok` → `warn` → `danger` or back),
* `<kc-context>` fires a **`kc-threshold-change`** CustomEvent with
* `detail.level` set to `'ok'`, `'warn'`, or `'danger'`.
*
* Token counts come from the API response `usage` field after each turn:
* inputTokens ← usage.input_tokens
* outputTokens ← usage.output_tokens
* cacheTokens ← usage.cache_read_input_tokens (+ cache_creation_input_tokens)
* reasoningTokens← usage.reasoning_tokens (extended thinking models)
* estimatedCost is calculated by the app; there is no built-in cost computation.
*/
const htmlSnippet = (obj: string) => `<!-- Register the elements once (CDN or bundler) -->
<script type="module">
import 'https://cdn.jsdelivr.net/npm/@kitn.ai/chat/dist/kitn-chat.es.js';
</script>
<kc-context id="ctx"></kc-context>
<script type="module">
const ctx = document.getElementById('ctx');
// Set the data as a PROPERTY (not an attribute — attributes only take strings).
ctx.context = ${obj};
</script>`;
const reactSnippet = (obj: string) => `import { Context } from '@kitn.ai/chat/react';
export function UsageIndicator() {
return (
<Context context={${obj}} />
);
}`;
const vueSnippet = (obj: string) => `<script setup>
import '@kitn.ai/chat/elements'; // register once (e.g. in main.ts)
const context = ${obj};
</script>
<template>
<!-- .prop binds the object as a property (attributes only take strings). -->
<kc-context :context.prop="context" />
</template>`;
const svelteSnippet = (obj: string) => `<script>
import '@kitn.ai/chat/elements'; // register once
let el;
const context = ${obj};
// Objects are set as properties via a binding (attributes only take strings).
$: if (el) el.context = context;
</script>
<kc-context bind:this={el} />`;
const angularSnippet = (obj: string) => `// main.ts: import '@kitn.ai/chat/elements' before bootstrapApplication,
// and add CUSTOM_ELEMENTS_SCHEMA to the component/module.
import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
@Component({
selector: 'app-usage',
standalone: true,
schemas: [CUSTOM_ELEMENTS_SCHEMA],
template: \`
<kc-context [context]="context"></kc-context>
\`,
})
export class UsageComponent {
context = ${obj};
}`;
/**
* Low Usage (Green) — early in a conversation; input + output rows only.
* The threshold colour (green/yellow/red) is derived from used/max by the
* element: green ≤ 70%, yellow > 70%, red > 90%. Thresholds are hardcoded —
* there is no `warnThreshold`/`dangerThreshold` prop yet (planned gap).
*/
const lowUsage: StoryUsage = {
intro:
"Show how much of the model's context window is used. Pass a `context` object with `usedTokens` / `maxTokens` (plus optional `inputTokens` / `outputTokens` / `estimatedCost`) to `<kc-context>` as a JS **property** — the trigger colour shifts green → yellow → red as usage climbs. Override the thresholds with `warnThreshold` (default `0.7`) and `dangerThreshold` (default `0.9`) numeric properties. Listen for `kc-threshold-change` to react when severity shifts. (The live demo composes the SolidJS `Context` primitives.)",
snippets: {
html: htmlSnippet(`{
usedTokens: 4200,
maxTokens: 200000,
inputTokens: 2800,
outputTokens: 1400,
estimatedCost: 0.012,
}`),
react: reactSnippet(`{
usedTokens: 4200,
maxTokens: 200000,
inputTokens: 2800,
outputTokens: 1400,
estimatedCost: 0.012,
}`),
vue: vueSnippet(`{
usedTokens: 4200,
maxTokens: 200000,
inputTokens: 2800,
outputTokens: 1400,
estimatedCost: 0.012,
}`),
svelte: svelteSnippet(`{
usedTokens: 4200,
maxTokens: 200000,
inputTokens: 2800,
outputTokens: 1400,
estimatedCost: 0.012,
}`),
angular: angularSnippet(`{
usedTokens: 4200,
maxTokens: 200000,
inputTokens: 2800,
outputTokens: 1400,
estimatedCost: 0.012,
}`),
solid: `import {
Context, ContextTrigger, ContextContent,
ContextContentHeader, ContextContentBody, ContextContentFooter,
ContextInputUsage, ContextOutputUsage,
} from '@kitn.ai/chat';
export function UsageIndicator() {
return (
// Low usage -> green trigger. Pass the counts as individual props.
<Context usedTokens={4200} maxTokens={200000} inputTokens={2800} outputTokens={1400} estimatedCost={0.012}>
<ContextTrigger />
<ContextContent>
<ContextContentHeader />
<ContextContentBody>
<div class="space-y-1.5">
<ContextInputUsage />
<ContextOutputUsage />
</div>
</ContextContentBody>
<ContextContentFooter />
</ContextContent>
</Context>
);
}`,
},
};
/**
* Medium Usage (Yellow) — extended conversation with reasoning; adds the
* reasoning row and crosses the 70% warning threshold.
*/
const mediumUsage: StoryUsage = {
intro:
"Same element, higher numbers. As `usedTokens / maxTokens` crosses `warnThreshold` (default 70%) the trigger turns yellow. Pass `warnThreshold` as a numeric property to override. Include `reasoningTokens` to surface a reasoning row in the breakdown — comes from `usage.reasoning_tokens` on extended-thinking models. (The live demo composes the SolidJS `Context` primitives.)",
snippets: {
html: htmlSnippet(`{
usedTokens: 150000,
maxTokens: 200000,
inputTokens: 85000,
outputTokens: 42000,
reasoningTokens: 23000,
estimatedCost: 0.89,
}`),
react: reactSnippet(`{
usedTokens: 150000,
maxTokens: 200000,
inputTokens: 85000,
outputTokens: 42000,
reasoningTokens: 23000,
estimatedCost: 0.89,
}`),
vue: vueSnippet(`{
usedTokens: 150000,
maxTokens: 200000,
inputTokens: 85000,
outputTokens: 42000,
reasoningTokens: 23000,
estimatedCost: 0.89,
}`),
svelte: svelteSnippet(`{
usedTokens: 150000,
maxTokens: 200000,
inputTokens: 85000,
outputTokens: 42000,
reasoningTokens: 23000,
estimatedCost: 0.89,
}`),
angular: angularSnippet(`{
usedTokens: 150000,
maxTokens: 200000,
inputTokens: 85000,
outputTokens: 42000,
reasoningTokens: 23000,
estimatedCost: 0.89,
}`),
solid: `import {
Context, ContextTrigger, ContextContent,
ContextContentHeader, ContextContentBody, ContextContentFooter,
ContextInputUsage, ContextOutputUsage, ContextReasoningUsage,
} from '@kitn.ai/chat';
export function UsageIndicator() {
return (
// ~75% used -> yellow trigger. reasoningTokens adds the reasoning row.
<Context usedTokens={150000} maxTokens={200000} inputTokens={85000} outputTokens={42000} reasoningTokens={23000} estimatedCost={0.89}>
<ContextTrigger />
<ContextContent>
<ContextContentHeader />
<ContextContentBody>
<div class="space-y-1.5">
<ContextInputUsage />
<ContextOutputUsage />
<ContextReasoningUsage />
</div>
</ContextContentBody>
<ContextContentFooter />
</ContextContent>
</Context>
);
}`,
},
};
/**
* High Usage (Red) — near the context limit; numbers pushed past the 90%
* danger threshold so the trigger goes red.
*/
const highUsage: StoryUsage = {
intro:
"Near the limit. Push `usedTokens` past `dangerThreshold` (default 90%) of `maxTokens` and the trigger goes red — a cue for the user to start a new conversation. Pass `dangerThreshold` as a numeric property to override. Same markup as Medium — only the counts differ. (The live demo composes the SolidJS `Context` primitives.)",
snippets: {
html: htmlSnippet(`{
usedTokens: 189000,
maxTokens: 200000,
inputTokens: 110000,
outputTokens: 54000,
reasoningTokens: 25000,
estimatedCost: 1.42,
}`),
react: reactSnippet(`{
usedTokens: 189000,
maxTokens: 200000,
inputTokens: 110000,
outputTokens: 54000,
reasoningTokens: 25000,
estimatedCost: 1.42,
}`),
vue: vueSnippet(`{
usedTokens: 189000,
maxTokens: 200000,
inputTokens: 110000,
outputTokens: 54000,
reasoningTokens: 25000,
estimatedCost: 1.42,
}`),
svelte: svelteSnippet(`{
usedTokens: 189000,
maxTokens: 200000,
inputTokens: 110000,
outputTokens: 54000,
reasoningTokens: 25000,
estimatedCost: 1.42,
}`),
angular: angularSnippet(`{
usedTokens: 189000,
maxTokens: 200000,
inputTokens: 110000,
outputTokens: 54000,
reasoningTokens: 25000,
estimatedCost: 1.42,
}`),
solid: `import {
Context, ContextTrigger, ContextContent,
ContextContentHeader, ContextContentBody, ContextContentFooter,
ContextInputUsage, ContextOutputUsage, ContextReasoningUsage,
} from '@kitn.ai/chat';
export function UsageIndicator() {
return (
// ~95% used -> red trigger; time to start a new conversation.
<Context usedTokens={189000} maxTokens={200000} inputTokens={110000} outputTokens={54000} reasoningTokens={25000} estimatedCost={1.42}>
<ContextTrigger />
<ContextContent>
<ContextContentHeader />
<ContextContentBody>
<div class="space-y-1.5">
<ContextInputUsage />
<ContextOutputUsage />
<ContextReasoningUsage />
</div>
</ContextContentBody>
<ContextContentFooter />
</ContextContent>
</Context>
);
}`,
},
};
/**
* Full Breakdown with Cache — detailed usage including cache-hit tokens; adds
* `cacheTokens` (and the cache row in the Solid composition).
*
* From the API: `cacheTokens` = `usage.cache_read_input_tokens` from the
* response (tokens served from prompt cache). Cache-write tokens
* (`usage.cache_creation_input_tokens`) are also counted toward `usedTokens`
* but shown under the same row here for simplicity.
*/
const withCache: StoryUsage = {
intro:
"Show the full breakdown including cache-hit tokens. Add `cacheTokens` to the `context` object (sourced from `usage.cache_read_input_tokens` in the API response) and `<kc-context>` includes it in the popover breakdown. (The live demo composes the SolidJS `Context` primitives, adding a `ContextCacheUsage` row.)",
snippets: {
html: htmlSnippet(`{
usedTokens: 82000,
maxTokens: 200000,
inputTokens: 45000,
outputTokens: 22000,
reasoningTokens: 15000,
cacheTokens: 32000,
estimatedCost: 0.38,
}`),
react: reactSnippet(`{
usedTokens: 82000,
maxTokens: 200000,
inputTokens: 45000,
outputTokens: 22000,
reasoningTokens: 15000,
cacheTokens: 32000,
estimatedCost: 0.38,
}`),
vue: vueSnippet(`{
usedTokens: 82000,
maxTokens: 200000,
inputTokens: 45000,
outputTokens: 22000,
reasoningTokens: 15000,
cacheTokens: 32000,
estimatedCost: 0.38,
}`),
svelte: svelteSnippet(`{
usedTokens: 82000,
maxTokens: 200000,
inputTokens: 45000,
outputTokens: 22000,
reasoningTokens: 15000,
cacheTokens: 32000,
estimatedCost: 0.38,
}`),
angular: angularSnippet(`{
usedTokens: 82000,
maxTokens: 200000,
inputTokens: 45000,
outputTokens: 22000,
reasoningTokens: 15000,
cacheTokens: 32000,
estimatedCost: 0.38,
}`),
solid: `import {
Context, ContextTrigger, ContextContent,
ContextContentHeader, ContextContentBody, ContextContentFooter,
ContextInputUsage, ContextOutputUsage, ContextReasoningUsage, ContextCacheUsage,
} from '@kitn.ai/chat';
export function UsageIndicator() {
return (
// cacheTokens lights up the ContextCacheUsage row.
<Context usedTokens={82000} maxTokens={200000} inputTokens={45000} outputTokens={22000} reasoningTokens={15000} cacheTokens={32000} estimatedCost={0.38}>
<ContextTrigger />
<ContextContent>
<ContextContentHeader />
<ContextContentBody>
<div class="space-y-1.5">
<ContextInputUsage />
<ContextOutputUsage />
<ContextReasoningUsage />
<ContextCacheUsage />
</div>
</ContextContentBody>
<ContextContentFooter />
</ContextContent>
</Context>
);
}`,
},
};
/**
* In a Header Bar — `<kc-context>` sitting next to a model switcher in an app
* header. The header chrome and `<kc-model-switcher>` are siblings; the usage
* indicator itself is the same `context` object as the other stories.
*/
const inHeaderBar: StoryUsage = {
intro:
"Drop the usage indicator into your app header next to the model switcher. `<kc-context>` is just an inline element — lay it out with your own header markup. The `<kc-model-switcher>` beside it takes a `models` array property and fires `modelchange`. (The live demo composes the SolidJS `Context` + `ModelSwitcher` primitives.)",
snippets: {
html: `<!-- Register the elements once (CDN or bundler) -->
<script type="module">
import 'https://cdn.jsdelivr.net/npm/@kitn.ai/chat/dist/kitn-chat.es.js';
</script>
<header style="display:flex;align-items:center;gap:0.5rem">
<kc-model-switcher id="models"></kc-model-switcher>
<kc-context id="ctx"></kc-context>
</header>
<script type="module">
// Set object/array data as PROPERTIES (not attributes).
const ctx = document.getElementById('ctx');
ctx.context = {
usedTokens: 67000,
maxTokens: 200000,
inputTokens: 38000,
outputTokens: 29000,
estimatedCost: 0.31,
};
const models = document.getElementById('models');
models.models = [
{ id: 'claude-4', name: 'Claude 4 Opus', provider: 'Anthropic' },
{ id: 'claude-4-sonnet', name: 'Claude 4 Sonnet', provider: 'Anthropic' },
];
models.currentModel = 'claude-4';
models.addEventListener('kc-model-change', (e) => console.log(e.detail));
</script>`,
react: `import { Context, ModelSwitcher } from '@kitn.ai/chat/react';
export function ChatHeader() {
const models = [
{ id: 'claude-4', name: 'Claude 4 Opus', provider: 'Anthropic' },
{ id: 'claude-4-sonnet', name: 'Claude 4 Sonnet', provider: 'Anthropic' },
];
return (
<header style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
<ModelSwitcher models={models} currentModel="claude-4" onModelChange={(e) => console.log(e.detail)} />
<Context
context={{
usedTokens: 67000,
maxTokens: 200000,
inputTokens: 38000,
outputTokens: 29000,
estimatedCost: 0.31,
}}
/>
</header>
);
}`,
vue: `<script setup>
import '@kitn.ai/chat/elements'; // register once (e.g. in main.ts)
const context = {
usedTokens: 67000,
maxTokens: 200000,
inputTokens: 38000,
outputTokens: 29000,
estimatedCost: 0.31,
};
const models = [
{ id: 'claude-4', name: 'Claude 4 Opus', provider: 'Anthropic' },
{ id: 'claude-4-sonnet', name: 'Claude 4 Sonnet', provider: 'Anthropic' },
];
</script>
<template>
<header style="display:flex;align-items:center;gap:0.5rem">
<kc-model-switcher :models.prop="models" current-model="claude-4" @kc-model-change="(e) => console.log(e.detail)" />
<kc-context :context.prop="context" />
</header>
</template>`,
svelte: `<script>
import '@kitn.ai/chat/elements'; // register once
let ctxEl, modelsEl;
const context = {
usedTokens: 67000,
maxTokens: 200000,
inputTokens: 38000,
outputTokens: 29000,
estimatedCost: 0.31,
};
const models = [
{ id: 'claude-4', name: 'Claude 4 Opus', provider: 'Anthropic' },
{ id: 'claude-4-sonnet', name: 'Claude 4 Sonnet', provider: 'Anthropic' },
];
// Objects/arrays are set as properties (attributes only take strings).
$: if (ctxEl) ctxEl.context = context;
$: if (modelsEl) modelsEl.models = models;
</script>
<header style="display:flex;align-items:center;gap:0.5rem">
<kc-model-switcher bind:this={modelsEl} current-model="claude-4" on:kc-model-change={(e) => console.log(e.detail)} />
<kc-context bind:this={ctxEl} />
</header>`,
angular: `// main.ts: import '@kitn.ai/chat/elements' before bootstrapApplication,
// and add CUSTOM_ELEMENTS_SCHEMA to the component/module.
import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
@Component({
selector: 'app-header',
standalone: true,
schemas: [CUSTOM_ELEMENTS_SCHEMA],
template: \`
<header style="display:flex;align-items:center;gap:0.5rem">
<kc-model-switcher [models]="models" current-model="claude-4" (kc-model-change)="onModelChange($event)"></kc-model-switcher>
<kc-context [context]="context"></kc-context>
</header>
\`,
})
export class HeaderComponent {
context = {
usedTokens: 67000,
maxTokens: 200000,
inputTokens: 38000,
outputTokens: 29000,
estimatedCost: 0.31,
};
models = [
{ id: 'claude-4', name: 'Claude 4 Opus', provider: 'Anthropic' },
{ id: 'claude-4-sonnet', name: 'Claude 4 Sonnet', provider: 'Anthropic' },
];
onModelChange(e: CustomEvent) { console.log(e.detail); }
}`,
solid: `import { createSignal } from 'solid-js';
import {
Context, ContextTrigger, ContextContent,
ContextContentHeader, ContextContentBody, ContextContentFooter,
ContextInputUsage, ContextOutputUsage,
ModelSwitcher,
} from '@kitn.ai/chat';
import type { ModelOption } from '@kitn.ai/chat';
export function ChatHeader() {
const [modelId, setModelId] = createSignal('claude-4');
const models: ModelOption[] = [
{ id: 'claude-4', name: 'Claude 4 Opus', provider: 'Anthropic' },
{ id: 'claude-4-sonnet', name: 'Claude 4 Sonnet', provider: 'Anthropic' },
];
return (
<div class="flex items-center gap-2">
<ModelSwitcher models={models} currentModelId={modelId()} onModelChange={setModelId} />
<Context usedTokens={67000} maxTokens={200000} inputTokens={38000} outputTokens={29000} estimatedCost={0.31}>
<ContextTrigger />
<ContextContent>
<ContextContentHeader />
<ContextContentBody>
<div class="space-y-1.5">
<ContextInputUsage />
<ContextOutputUsage />
</div>
</ContextContentBody>
<ContextContentFooter />
</ContextContent>
</Context>
</div>
);
}`,
},
};
/**
* Example: Context & Token Usage — show how much of the model's context window
* is consumed by the conversation. `<kc-context>` takes a single `context`
* object property (`usedTokens`, `maxTokens`, plus optional input/output/
* reasoning/cache token counts + `estimatedCost`); it renders the trigger,
* popover, and breakdown and has no events. SolidJS composes the `Context`
* primitives for full control over the popover.
*
* Per-story: the Usage tab shows the snippet for the story you're on; the
* example-level fields below (spread from the primary story) are the fallback.
*/
const contextUsage: ExampleUsage = {
title: 'Examples/Context & Token Usage',
...lowUsage, // example-level fallback = the primary "Low Usage (Green)" story
stories: {
'Low Usage (Green)': lowUsage,
'Medium Usage (Yellow)': mediumUsage,
'High Usage (Red)': highUsage,
'Full Breakdown with Cache': withCache,
'In a Header Bar': inHeaderBar,
},
};
export default contextUsage;