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.

228 lines (179 loc) 8.49 kB
import { Meta } from '@storybook/addon-docs/blocks'; <Meta title="Docs/Frameworks/React" /> # React The kit ships auto-generated, **typed React wrappers** under `@kitn.ai/chat/react`. They hide the custom-element plumbing — rich data goes in as React props (set as DOM **properties**, never stringified) and CustomEvents come out as `on<Event>` handlers — so you write idiomatic JSX without touching refs. There are **two ways to build with the kit**, and you can mix them: 1. **`<Chat>`** — the batteries-included shell: a whole chat experience in one tag. Fastest start. 2. **Compose the individual elements** (`<Conversations>`, `<Markdown>`, `<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 (a side-effect import), then use the typed wrappers anywhere: ```tsx // Once, near your app entryregisters the <kc-*> elements globally. import '@kitn.ai/chat/elements'; // Then import the wrappers you need (tree-shaken). import { Chat, Conversations, Markdown } from '@kitn.ai/chat/react'; ``` - **`@kitn.ai/chat/react`** — the typed wrappers (recommended). Component names are the PascalCase of the tag: `kc-chat → Chat`, `kc-prompt-input → PromptInput`. - **`@kitn.ai/chat/elements`** — the raw custom elements, if you'd rather wire refs yourself (see [Without the wrappers](#without-the-wrappers-raw-custom-element)). - 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**). > One name collides with a browser global: `kc-image → Image`. Alias it if you import it: > `import { Image as KcImage } from '@kitn.ai/chat/react'`. ## Quick start — the all-in-one shell `<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. ```tsx import '@kitn.ai/chat/elements'; import { Chat } from '@kitn.ai/chat/react'; import { useState } from 'react'; type Message = { id: string; role: 'user' | 'assistant'; content: string }; export function App() { const [messages, setMessages] = useState<Message[]>([ { id: '1', role: 'assistant', content: 'Hello! How can I help?' }, ]); const handleSubmit = async (e: CustomEvent<{ value: string }>) => { const history = [...messages, { id: crypto.randomUUID(), role: 'user' as const, content: e.detail.value }]; setMessages(history); const aid = crypto.randomUUID(); setMessages([...history, { id: aid, role: 'assistant', content: '' }]); let answer = ''; for await (const token of streamFromYourAPI(history)) { answer += token; // Assign a NEW array (and a new object for the message you change) to trigger a re-render. setMessages((prev) => prev.map((m) => (m.id === aid ? { ...m, content: answer } : m))); } }; // 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. return ( <div style={{ display: 'flex', flexDirection: 'column', height: '100dvh' }}> <Chat messages={messages} suggestions={['Summarize the chat', 'Start fresh']} onSubmit={handleSubmit} style={{ flex: 1, minHeight: 0 }} /> </div> ); } ``` ## Go further — compose the pieces `<Chat>` is one option, not the only one. Every element has its own wrapper, so you can assemble your own layout. Here's a multi-conversation shell — a `<Conversations>` sidebar next to the `<Chat>` thread: ```tsx import '@kitn.ai/chat/elements'; import { Chat, Conversations } from '@kitn.ai/chat/react'; import { useState } from 'react'; export function Workspace() { const [conversations, setConversations] = useState(myConversations); const [activeId, setActiveId] = useState(conversations[0]?.id); const [messages, setMessages] = useState(loadMessages(activeId)); // 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. return ( <div style={{ display: 'flex', height: '100dvh' }}> <Conversations conversations={conversations} activeId={activeId} onConversationSelect={(e) => { setActiveId(e.detail.id); setMessages(loadMessages(e.detail.id)); }} onNewChat={() => startNewConversation()} style={{ width: 300, flexShrink: 0 }} /> <Chat messages={messages} onSubmit={(e) => sendMessage(e.detail.value)} style={{ flex: 1, minWidth: 0 }} /> </div> ); } ``` ### Make the panels resizable Want a draggable divider between the sidebar and the thread? Wrap the panels in `<Resizable>` with one `<ResizableItem>` each — the handles are inserted for you (up to 3 panels). Each item takes a `size` (px or `%`) plus optional `min`/`max`; listen for `onChange` (`detail.sizes`) to persist the layout. ```tsx import { Chat, Conversations, Resizable, ResizableItem } from '@kitn.ai/chat/react'; <div style={{ display: 'flex', flexDirection: 'column', height: '100dvh' }}> <Resizable orientation="horizontal" style={{ flex: 1, minHeight: 0 }}> <ResizableItem size="25%" min="200px"> <Conversations conversations={conversations} activeId={activeId} onConversationSelect={handleSelect} /> </ResizableItem> <ResizableItem> <Chat messages={messages} onSubmit={handleSubmit} /> </ResizableItem> </Resizable> </div> ``` You can also drop **standalone display elements** anywhere in your own UI — `<Markdown>`, `<CodeBlock>`, `<Artifact>`, `<Reasoning>`, `<Tool>` — to render rich AI content without adopting the whole chat. Each fills its container and is controlled via props and events. > **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 React (and every other framework). ## Props & events The rule for all elements: **rich data goes in as properties, interactions come out as events.** The wrappers do this for you — pass arrays/objects as props and they're assigned as live DOM properties (not stringified), while CustomEvents surface as `on<Event>` handlers. Event props are `on` + the event name with the `kc-` prefix stripped and each hyphen-segment PascalCased: | DOM event | React prop | |---|---| | `kc-submit` | `onSubmit` | | `kc-value-change` | `onValueChange` | | `kc-message-action` | `onMessageAction` | | `kc-model-change` | `onModelChange` | | `kc-conversation-select` | `onConversationSelect` | | `kc-suggestion-click` | `onSuggestionClick` | All **40+ elements** have a typed wrapper: ```tsx import { Chat, Conversations, PromptInput, Message, Markdown, CodeBlock, Reasoning, Tool, Context, ModelSwitcher, Attachments, Loader, // …every kc-* element } from '@kitn.ai/chat/react'; ``` ## Without the wrappers (raw custom element) Prefer the raw element (e.g. with React 19's improved custom-element support)? Use a `ref` to set object props and wire events: ```tsx import { useEffect, useRef, useState } from 'react'; import '@kitn.ai/chat/elements'; export function RawChat() { const ref = useRef<HTMLElement>(null); const [messages, setMessages] = useState([{ id: '1', role: 'assistant', content: 'Hello!' }]); useEffect(() => { const el = ref.current; if (!el) return; (el as any).messages = messages; // object/array props can't go through attributes }, [messages]); useEffect(() => { const el = ref.current; if (!el) return; const onSubmit = (e: Event) => { const { value } = (e as CustomEvent<{ value: string }>).detail; setMessages((prev) => [...prev, { id: crypto.randomUUID(), role: 'user', content: value }]); }; el.addEventListener('kc-submit', onSubmit); return () => el.removeEventListener('kc-submit', onSubmit); }, []); return <kc-chat ref={ref} style={{ display: 'block', height: '100vh' }} />; } ```