npm package: @kognitivedev/ui
Description: React components for building agentic chat UIs with Kognitive
Latest version: 0.2.22

# @kognitivedev/ui

React components for building agentic chat UIs with Kognitive. Drop-in `<Thread />` for instant results, or use headless primitives for full control.

## Installation

```bash
bun add @kognitivedev/ui
```

**Peer dependencies:** `react`, `ai`, `@ai-sdk/react`, `zod`

## Quick Start: End-to-End

### 1. Define an Agent (Server)

```tsx
// lib/agents.ts
import { createAgent } from "@kognitivedev/agents";
import { createTool } from "@kognitivedev/tools";
import { openai } from "@ai-sdk/openai";
import { z } from "zod";

const weatherTool = createTool({
  id: "weather-lookup",
  description: "Get current weather for a city",
  inputSchema: z.object({ city: z.string() }),
  execute: async (input) => ({
    city: input.city, temperature: 22, condition: "Sunny",
  }),
});

export const assistant = createAgent({
  name: "assistant",
  instructions: "You are a helpful assistant. Use tools when needed.",
  model: openai("gpt-4o-mini"),
  tools: [weatherTool],
  maxSteps: 5,
});
```

### 2. Create an API Route (Server)

```tsx
// app/api/chat/route.ts
import { assistant } from "@/lib/agents";

export async function POST(req: Request) {
  const { messages, resourceId } = await req.json();
  const result = await assistant.stream({ messages, resourceId: resourceId ?? {} });
  return result.toUIMessageStreamResponse();
}
```

### 3. Build the Chat UI (Client)

```tsx
// app/page.tsx
"use client";
import { KognitiveUI } from "@kognitivedev/ui";

export default function ChatPage() {
  return (
    <KognitiveUI api="/api/chat" agentName="assistant">
      <KognitiveUI.Thread />
    </KognitiveUI>
  );
}
```

That's a working chat. `api` points to your route. `agentName` is sent in the request body.

## Features

### Tool UI Registration

Register custom React components for tool calls — type-safe when using `@kognitivedev/tools`:

```tsx
import { makeToolUI } from "@kognitivedev/ui";
import { weatherTool } from "./tools";

const WeatherUI = makeToolUI({
  tool: weatherTool,
  render: ({ input, output, state }) => (
    <div>{input.city}: {output?.temp}deg</div>
  ),
});

<KognitiveUI toolUIs={[WeatherUI]} ...>
```

Batch register by name with `toolkit()`:

```tsx
import { toolkit } from "@kognitivedev/ui";

const myToolkit = toolkit({
  "weather-lookup": { render: ({ input, output }) => <WeatherCard {...output} /> },
  "calculate":      { render: ({ input, output }) => <span>{output?.result}</span> },
});

<KognitiveUI toolkit={myToolkit} ...>
```

### Message Editing

Users can edit sent messages and regenerate from that point. The Edit button appears on user messages, and submitting the edit trims the conversation and re-sends.

### Feedback / Ratings

Thumbs up/down on assistant messages:

```tsx
<KognitiveUI
  onFeedback={(messageId, type) => {
    // type is "positive" or "negative"
    fetch("/api/feedback", { method: "POST", body: JSON.stringify({ messageId, type }) });
  }}
>
```

### Suggested Follow-ups

Show clickable suggestion chips after the assistant responds:

```tsx
<KognitiveUI suggestions={["What's the weather?", "Tell me more"]}>
```

### Tool Approval (Human-in-the-Loop)

When a tool enters `approval-requested` state, an Approve/Reject card renders automatically:

```tsx
<KognitiveUI
  onToolApproval={(toolCallId, approved) => {
    console.log(`Tool ${toolCallId}: ${approved ? "approved" : "rejected"}`);
  }}
>
```

### Error Display

Stream errors are shown as dismissible banners via `ThreadPrimitive.Error` and the `ErrorBanner` component.

### File Attachments

Click-to-upload, drag-and-drop, and paste-to-upload are all supported:

```tsx
<KognitiveUI.Thread allowAttachments />
```

### Keyboard Shortcuts

```tsx
import { useKeyboardShortcuts } from "@kognitivedev/ui";

useKeyboardShortcuts([
  { key: "Escape", handler: () => stop() },
  { key: "k", ctrl: true, handler: () => clearMessages() },
]);
```

### Events System

```tsx
import { useKognitiveEvent, KognitiveEventEmitter } from "@kognitivedev/ui";

const emitter = new KognitiveEventEmitter();
useKognitiveEvent(emitter, "message:received", (data) => {
  analytics.track("message_received", data);
});
```

Events: `message:send`, `message:received`, `status:change`, `tool:invoked`, `tool:completed`, `error`

### Thread Management

```tsx
<KognitiveUI baseUrl="http://localhost:3001" agentName="agent" threads>
  <div className="grid grid-cols-[280px_1fr]">
    <KognitiveUI.ThreadList />
    <KognitiveUI.Thread />
  </div>
</KognitiveUI>
```

## API Reference

### `<KognitiveUI>` Props

| Prop | Type | Description |
|------|------|-------------|
| `api` | `string` | Explicit API endpoint URL. Takes priority over baseUrl+agentName. |
| `agentName` | `string` | **Required.** Agent name. |
| `baseUrl` | `string` | Base URL of the Kognitive runtime. |
| `headers` | `Record<string, string>` | Auth headers. |
| `resourceId` | `{ userId?, sessionId?, organizationId? }` | User identity. |
| `sessionId` | `string` | Controlled session/thread ID. |
| `threads` | `boolean` | Enable thread management sidebar. |
| `threadApiBase` | `string` | Override thread API base URL. |
| `toolUIs` | `ToolUIRegistration[]` | Tool UI registrations from `makeToolUI()`. |
| `toolkit` | `ToolUIRegistration[]` | Tool UI registrations from `toolkit()`. |
| `body` | `Record<string, unknown>` | Extra request body fields. |
| `suggestions` | `string[]` | Follow-up prompt suggestions. |
| `onFeedback` | `(messageId, type) => void` | Feedback callback (positive/negative). |
| `onToolApproval` | `(toolCallId, approved) => void` | Tool approval callback. |

### Primitives

Headless, unstyled components. Compose them for full control.

- **ThreadPrimitive** — `Root`, `Messages`, `Empty`, `Loading`, `Error`, `Suggestions`, `ScrollToBottom`
- **MessagePrimitive** — `Root`, `Content`, `Role`
- **ComposerPrimitive** — `Root`, `Input`, `Send`, `Attachments`, `AttachmentTrigger`, `EditRoot`
- **ActionBarPrimitive** — `Root`, `Copy`, `Retry`, `Stop`, `Feedback`, `Edit`
- **ToolUIPrimitive** — `Root`, `Fallback`
- **ThreadListPrimitive** — `Root`, `Items`, `Item`, `New`

### Composed Components

Pre-styled with Tailwind. Every `className` is overridable.

- `Thread` — Full chat: messages + composer + status + error + suggestions
- `Message` — Bubble with avatar, content, edit mode, feedback, copy
- `Composer` — Input + send + attachment support (drag-drop, paste)
- `ThreadList` — Sidebar with thread items + new button
- `ToolInvocation` — Collapsible tool card with approval UI
- `ToolApproval` — Approve/reject card for tool calls
- `MarkdownContent` — Basic markdown renderer
- `ErrorBanner` — Dismissible error display
- `Suggestions` — Follow-up prompt chips
- `AttachmentPreview` — File/image thumbnails
- `StatusIndicator` — Streaming/ready/error dot

### Hooks

- `useKognitiveChat(config)` — Standalone chat hook (no provider needed)
- `useKognitive()` — Access full context (messages, send, status, error, etc.)
- `useThreadManager(config)` — Thread CRUD operations
- `useKeyboardShortcuts(shortcuts)` — Register keyboard shortcuts
- `useKognitiveEvent(emitter, event, handler)` — Subscribe to lifecycle events
- `useAutoScroll(deps)` — Auto-scroll on dependency change
- `useCopyToClipboard()` — Copy text with success state
- `useStreamingStatus()` — Get current ChatStatus

### Primitive Hooks

- `useThread()` — Thread-level state (messages, status, send, stop)
- `useMessage()` — Message-level state (message, isEditing, startEdit, submitEdit)
- `useComposer()` — Composer state (input, files, submit, isDisabled)
- `useThreadList()` — Thread list state (threads, activeSessionId, createThread)

## Custom Layout with Primitives

```tsx
import {
  KognitiveUI,
  ThreadPrimitive,
  MessagePrimitive,
  ComposerPrimitive,
  ActionBarPrimitive,
} from "@kognitivedev/ui";

<KognitiveUI api="/api/chat" agentName="agent">
  <ThreadPrimitive.Root>
    <ThreadPrimitive.Empty>No messages yet</ThreadPrimitive.Empty>
    <ThreadPrimitive.Error>{(err) => <p>{err.message}</p>}</ThreadPrimitive.Error>

    <ThreadPrimitive.Messages>
      {(message) => (
        <MessagePrimitive.Root key={message.id} message={message}>
          <MessagePrimitive.Content
            components={{
              Text: ({ text }) => <MyMarkdown>{text}</MyMarkdown>,
            }}
          />
          <ActionBarPrimitive.Root>
            <ActionBarPrimitive.Copy />
            <ActionBarPrimitive.Feedback />
            <ActionBarPrimitive.Edit />
          </ActionBarPrimitive.Root>
        </MessagePrimitive.Root>
      )}
    </ThreadPrimitive.Messages>

    <ThreadPrimitive.Suggestions />

    <ComposerPrimitive.Root>
      <ComposerPrimitive.Attachments />
      <ComposerPrimitive.Input placeholder="Ask..." />
      <ComposerPrimitive.Send>Send</ComposerPrimitive.Send>
    </ComposerPrimitive.Root>
  </ThreadPrimitive.Root>
</KognitiveUI>
```

## License

MIT