# 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).

> **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' }} />;
}
```
