# React

Use AI/UI's typed React wrappers to drop kai-* web components into any React app with idiomatic JSX, typed props, and onX event handlers.

`@kitn.ai/ui/react` exports a typed React wrapper for every `kai-*` web component. Pass data as props, handle events with `onX` handlers — no refs, no manual `addEventListener`, no stringified objects.

## Install

```bash
npm install @kitn.ai/ui
```

## Set up

Register the web components once at your app entry point, then import the wrappers you need anywhere:

```tsx
// main.tsx (or wherever you mount your app)

```

```tsx
// Any component file

```

Component names are the PascalCase of the tag name: `kai-chat → Chat`, `kai-prompt-input → PromptInput`, `kai-chain-of-thought → ChainOfThought`.

> **tip:** 
Every element is styled inside its own Shadow DOM. Import `@kitn.ai/ui/theme.css` only if you want to override design tokens.

## The all-in-one shell

`<Chat>` renders a complete thread — message list, prompt input, suggestions, and header chrome — in a single element. It is **transport-agnostic**: pass a `messages` array, handle `onSubmit`, and stream the reply back into state.

A message is an **ordered `parts` array**, not a string: text, reasoning, tool calls, generative-UI cards and file attachments, in the order the model produced them. `ChatMessage` is the kit's own type, so import it rather than hand-rolling a text-only copy — the copy compiles and then hides every other part kind from you.

```tsx

export function App() {
  const [messages, setMessages] = useState<ChatMessage[]>([
    { id: '1', role: 'assistant', parts: [{ type: 'text', text: 'How can I help?' }] },
  ]);

  const handleSubmit = async (e: CustomEvent<{ value: string }>) => {
    const userMsg: ChatMessage = {
      id: crypto.randomUUID(),
      role: 'user',
      parts: [{ type: 'text', text: e.detail.value }],
    };
    const history = [...messages, userMsg];
    setMessages(history);

    const aid = crypto.randomUUID();
    setMessages([...history, { id: aid, role: 'assistant', parts: [] }]);

    for await (const token of streamFromYourAPI(history)) {
      // appendTextPart returns a NEW parts array and leaves any reasoning, tool
      // or card parts alone. Replacing `parts` wholesale would drop them.
      setMessages((prev) =>
        prev.map((m) => (m.id === aid ? { ...m, parts: appendTextPart(m.parts, token) } : m)),
      );
    }
  };

  return (
    <div style={{ display: 'flex', flexDirection: 'column', height: '100dvh' }}>
      
    </div>
  );
}
```

`<Chat>` fills its container. Use `flex: 1` (not a hard-coded height) so it adapts to your layout.

## Compose individual elements

Every element has its own wrapper, so you can assemble your own layout. This example pairs a `<Conversations>` sidebar with a `<Chat>` thread. `groups` carries the section headers and `conversations` the rows; pass `groups={[]}` and everything lands in one Ungrouped section.

```tsx

export function Workspace() {
  const [activeId, setActiveId] = useState(myConversations[0]?.id);
  const [messages, setMessages] = useState(loadMessages(activeId));

  return (
    <div style={{ display: 'flex', height: '100dvh' }}>
      <Conversations
        groups={[]}
        conversations={myConversations}
        activeId={activeId}
        onConversationSelect={(e) => {
          setActiveId(e.detail.id);
          setMessages(loadMessages(e.detail.id));
        }}
        onNewChat={() => startNewConversation()}
        style={{ width: 280, flexShrink: 0 }}
      />
      <Chat
        messages={messages}
        onSubmit={(e) => sendMessage(e.detail.value)}
        style={{ flex: 1, minWidth: 0 }}
      />
    </div>
  );
}
```

### Resizable panels

Wrap panels in `<Resizable>` with one `<ResizableItem>` each to add a draggable divider. Each item takes a `size` (px or `%`) and optional `min`/`max`. Listen for `onChange` to persist the layout.

```tsx

<Resizable orientation="horizontal" style={{ flex: 1, minHeight: 0 }}>
  <ResizableItem size="25%" min="200px">
    
  </ResizableItem>
  <ResizableItem>
    
  </ResizableItem>
</Resizable>
```

### Standalone display elements

Drop individual elements anywhere in your UI without adopting a full chat shell — `<Markdown>`, `<CodeBlock>`, `<Reasoning>`, `<Tool>`, `<Artifact>` all work standalone:

```tsx

```

## Props and events

**Rich data goes in as props; interactions come out as events.** The wrappers assign arrays and objects directly as DOM properties (not stringified), and surface CustomEvents as `onX` handlers.

Event prop names strip the `kai-` prefix and PascalCase each hyphen-segment:

| DOM event | React prop |
|---|---|
| `kai-submit` | `onSubmit` |
| `kai-value-change` | `onValueChange` |
| `kai-message-action` | `onMessageAction` |
| `kai-model-change` | `onModelChange` |
| `kai-conversation-select` | `onConversationSelect` |
| `kai-suggestion-click` | `onSuggestionClick` |

Every wrapper also accepts `className`, `style`, `id`, `theme` (`'light' | 'dark' | 'auto'`), and `children` (for slotted content).

### Updating an array

**Assign a new array, and replace any item you changed with a new object.** Both halves matter, and each does a different job:

- The **new array reference** is what tells the element something changed. Setting the same array back is a no-op, even if you replaced an item inside it.
- The **new item object** is what makes the change visible. These lists key their rows by item identity, so an item you mutated in place renders stale even inside a brand-new array.

Adding, removing and reordering need only the fresh array — each of those changes the list of item references, which is what the element diffs. Editing a field inside an item changes nothing about that list, which is why it needs the new object as well.

```js
// Stale: `title` really did change, but the item object did not, so the row never updates.
setConversations((prev) => { prev.find((c) => c.id === id)!.title = 'Renamed'; return [...prev]; });

// Renders: a new array, and the one item that changed is a new object.
setConversations((prev) => prev.map((c) => (c.id === id ? { ...c, title: 'Renamed' } : c)));
```

This is the same immutable update React already wants for its own state, so idiomatic React code satisfies it by default. The trap is reaching for a mutation to avoid a re-render, or handing the elements state from a store that mutates in place.

> **tip:** 
`kai-image` exports as `Image`, which shadows the browser global. Alias it on import: `import { Image as KaiImage } from '@kitn.ai/ui/react'`.

## All available wrappers

```tsx

  Artifact, Attachments, Card, Cards, ChainOfThought,
  Chat, Checkpoint, Choice, CodeBlock, Confirm,
  Context, Conversations, Embed, Empty, FeedbackBar,
  FileTree, FileUpload, Form, Image, LinkPreview,
  Loader, Markdown, Message, ModelSwitcher, PromptInput,
  Reasoning, Remote, Resizable, ResizableItem, ResponseStream,
  ScopePicker, ScrollButton, Skills, Source, Sources,
  Suggestions, Tasks, TextShimmer, ThinkingBar, Tool,
  VoiceInput, Workspace,
} from '@kitn.ai/ui/react';
```

## Raw web component usage

Prefer to use the `kai-*` element directly — for example with React 19's improved custom-element support? Use a `ref` to set object props and wire events manually:

```tsx

export function RawChat() {
  const ref = useRef<HTMLElement>(null);
  const [messages, setMessages] = useState([
    { id: '1', role: 'assistant', parts: [{ type: 'text', text: 'Hello!' }] },
  ]);

  useEffect(() => {
    const el = ref.current;
    if (!el) return;
    // Arrays/objects must be assigned as DOM properties, not attributes.
    (el as any).messages = messages;
  }, [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', parts: [{ type: 'text', text: value }] },
      ]);
    };
    el.addEventListener('kai-submit', onSubmit);
    return () => el.removeEventListener('kai-submit', onSubmit);
  }, []);

  return <kai-chat ref={ref} style={{ display: 'block', height: '100dvh' }} />;
}
```
