k8ordo
  • UI
  • Form
  • State
  • Router
  • Static
  • Server
Switch to dark mode
  • Get Started
  • Theming
  • i18n
  • Components
  • Hooks
  • Helpers
  • AI
Open navigation
AI
  • AI Chat
  • Generative UI
  • AI Agents
k8ordo

React libraries that use Baseline features without holding back.

Packages
  • @k8ordo/ui
  • @k8ordo/form
  • @k8ordo/state
  • @k8ordo/router
  • @k8ordo/static
  • @k8ordo/server
UI
  • Get Started
  • Theming
  • i18n
  • Components
  • Hooks
  • Helpers
  • AI Chat
  • Generative UI
  • AI Agents
  • Storybook
Resources
  • GitHub
  • npm

© 2026 k8o — MIT License

Typeset in Noto Sans JP & M PLUS 2

AI

AI
  • AI Chat
  • Generative UI
  • AI Agents

AI Chat

Presentational building blocks for AI chat UIs. They hold no fetching or message state — you pass data in and compose them with messages.map(), so they connect to the AI SDK or your own backend. Import from @k8ordo/ui/ai.

Demo

A working chat screen. Pick a suggestion or type a message and send it — bubbles stack up in the conversation, and reasoning and tool calls fold into collapsible blocks.

Hi! Ask me anything about the k8ordo UI AI chat components.
Where should I start when building an AI chat in React?
The conversation area, message bubbles, and the input are the foundation. Markdown and tool views can come later.

Input

{
  "query": "k8ordo UI ai getting started"
}

Output

Starting with Conversation, Message, and PromptInput is recommended.
Start with Conversation, Message, and PromptInput to frame the conversation, then add Response (Markdown) and ToolInvocation on top.

Compose a conversation

Conversation provides the scroll region with stick-to-bottom and a scroll-to-latest button; Message renders the bubble per role; PromptInput is the composer. You own the message list.

Prompt input (IME-safe)

Enter sends, Shift+Enter inserts a newline, and the Enter that confirms an IME composition never submits. status switches the button between send and stop.

Suggestions

Suggestion lays out canned prompts as chips and passes the selected value straight to your send handler.

Streaming Markdown

Response renders streaming Markdown and tolerates unterminated blocks. It lives in a separate subpath and needs the streamdown optional peer plus its stylesheet.

Tool calls & reasoning

ToolInvocation and Reasoning show collapsible tool activity and thinking. Their state vocabulary matches the AI SDK tool part states.

AI SDK integration

mapMessageParts (from @k8ordo/ui/ai-sdk) turns an AI SDK UIMessage.parts array into a flat list you render yourself. It needs the ai optional peer.

Generative UI inside a bubble

Because Message.Content takes any children, you can render an LLM-generated UI spec inside a bubble with the json-render registry — generative UI delivered straight into the conversation.

Props

Props generated from the component types. Collapsible components (Reasoning / ToolInvocation) support both controlled and uncontrolled usage via isOpen / defaultOpen / onChange.

Conversation.Root

children
Type: ReactNode
Default: -
PropTypeDefault
childrenReactNode-

Conversation.Messages

children
Type: ReactNode
Default: -
isStreaming?
Type: boolean
Default: false
label?
Type: string
Default:
-
PropTypeDefault
childrenReactNode-
isStreaming?booleanfalse
label?string-

Conversation.ScrollButton

label?
Type: string
Default: -
PropTypeDefault
label?string-

Message.Root

children
Type: ReactNode
Default: -
from
Type: MessageRole
Default: -
PropTypeDefault
childrenReactNode-
fromMessageRole-

Type base (some attrs are managed internally): HTMLAttributes<HTMLDivElement>

Message.Content

children
Type: ReactNode
Default: -
isStreaming?
Type: boolean
Default: false
PropTypeDefault
childrenReactNode-
isStreaming?booleanfalse

Type base (some attrs are managed internally): HTMLAttributes<HTMLDivElement>

PromptInput.Root

children
Type: ReactNode
Default: -
defaultValue?
Type: string
Default: ''
onChange?
Type: (value: string) => void
Default: -
onStop?
Type: () => void
Default: -
onSubmit?
Type: (message: string) => void
Default: -
status?
Type: ChatStatus
Default: 'ready'

PromptInput.Textarea

children?
Type: ReactNode
Default: -
placeholder?
Type: string
Default: -
ref?
Type: Ref<HTMLTextAreaElement>
Default: -
PropTypeDefault
children?ReactNode-
placeholder?string-
ref?Ref<HTMLTextAreaElement>-

Type base (some attrs are managed internally): TextareaHTMLAttributes<HTMLTextAreaElement>

PromptInput.Submit

sendLabel?
Type: string
Default: -
stopLabel?
Type: string
Default: -
PropTypeDefault
sendLabel?string-
stopLabel?string-

Suggestion.List

children
Type: ReactNode
Default: -
label?
Type: string
Default: -
PropTypeDefault
childrenReactNode-
label?string-

Suggestion.Item

value
Type: string
Default: -
children?
Type: ReactNode
Default: -
onSelect?
Type: (value: string) => void
Default: -
PropTypeDefault
valuestring-
children?ReactNode-
onSelect?(value: string) => void-

Response

children
Type: string
Default: -
isStreaming?
Type: boolean
Default: false
PropTypeDefault
childrenstring-
isStreaming?booleanfalse

Type base (some attrs are managed internally): StreamdownProps

Reasoning

children
Type: ReactNode
Default: -
defaultOpen?
Type: boolean
Default: false
isOpen?
Type: boolean
Default: -
isStreaming?
Type: boolean
Default: false
onChange?
Type: (isOpen: boolean) => void
Default: -
PropTypeDefault
children

ToolInvocation

name
Type: string
Default: -
state
Type: ToolState
Default: -
defaultOpen?
Type: boolean
Default: false
deniedReason?
Type: string
Default: -
errorText?
Type: string
Default: -
input?
Type: unknown
Default: -
isOpen?
Type:
value?
Type: string
Default: -
PropTypeDefault
childrenReactNode-
defaultValue?string''
onChange?(value: string) => void-
onStop?() => void-
onSubmit?(message: string) => void-
status?ChatStatus'ready'
value?string-
ReactNode
-
defaultOpen?booleanfalse
isOpen?boolean-
isStreaming?booleanfalse
onChange?(isOpen: boolean) => void-
boolean
Default: -
onChange?
Type: (isOpen: boolean) => void
Default: -
output?
Type: ReactNode
Default: -
PropTypeDefault
namestring-
stateToolState-
defaultOpen?booleanfalse
deniedReason?string-
errorText?string-
input?unknown-
isOpen?boolean-
onChange?(isOpen: boolean) => void-
output?
ReactNode
-
'use client';
import { Avatar, AssistantIcon } from '@k8ordo/ui';
import {
  Conversation,
  Message,
  PromptInput,
  Suggestion,
} from '@k8ordo/ui/ai';

export function Chat({ messages, send }: Props) {
  return (
    <div className="flex h-full flex-col gap-3">
      <Conversation.Root>
        <Conversation.Messages>
          {messages.map((m) => (
            <Message.Root from={m.role} key={m.id}>
              {m.role === 'assistant' && (
                <Avatar color="primary" icon={<AssistantIcon />} name="AI" size="sm" />
              )}
              <Message.Content>{m.text}</Message.Content>
            </Message.Root>
          ))}
        </Conversation.Messages>
        <Conversation.ScrollButton />
      </Conversation.Root>

      <Suggestion.List>
        <Suggestion.Item onSelect={send} value="Tell me about IME support" />
      </Suggestion.List>

      <PromptInput.Root onSubmit={send}>
        <PromptInput.Textarea placeholder="Type a message…" />
        <PromptInput.Submit />
      </PromptInput.Root>
    </div>
  );
}
'use client';
import { Avatar, AssistantIcon } from '@k8ordo/ui';
import { Conversation, Message, PromptInput } from '@k8ordo/ui/ai';
import { useChat } from '@ai-sdk/react';

export function Chat() {
  const { messages, sendMessage, status, stop } = useChat();

  return (
    <div className="flex h-full flex-col gap-3">
      <Conversation.Root>
        <Conversation.Messages isStreaming={status === 'streaming'}>
          {messages.map((m) => (
            <Message.Root from={m.role === 'user' ? 'user' : 'assistant'} key={m.id}>
              {m.role !== 'user' && (
                <Avatar color="primary" icon={<AssistantIcon />} name="AI" size="sm" />
              )}
              <Message.Content>{textOf(m)}</Message.Content>
            </Message.Root>
          ))}
        </Conversation.Messages>
        <Conversation.ScrollButton />
      </Conversation.Root>

      <PromptInput.Root
        onStop={stop}
        onSubmit={(text) => sendMessage({ text })}
        status={status}
      >
        <PromptInput.Textarea placeholder="Type a message" />
        <PromptInput.Submit />
      </PromptInput.Root>
    </div>
  );
}
// Enter to send, Shift+Enter for a newline, IME-confirm Enter never submits.
// status: 'ready' | 'submitted' | 'streaming' | 'error' (matches AI SDK).
<PromptInput.Root status={status} onSubmit={send} onStop={stop}>
  <PromptInput.Textarea placeholder="Type a message" />
  <PromptInput.Submit /> {/* send when ready, stop while streaming */}
</PromptInput.Root>
import { Suggestion } from '@k8ordo/ui/ai';

<Suggestion.List>
  {suggestions.map((s) => (
    <Suggestion.Item key={s} onSelect={send} value={s} />
  ))}
</Suggestion.List>
// pnpm add streamdown
import { Response } from '@k8ordo/ui/ai/response';
import 'streamdown/styles.css';

<Message.Content>
  <Response isStreaming={isStreaming}>{markdown}</Response>
</Message.Content>
import { Reasoning, ToolInvocation } from '@k8ordo/ui/ai';

<Reasoning isStreaming={isThinking}>{reasoningText}</Reasoning>

<ToolInvocation
  name="search_web"
  state="output-available" // 'input-streaming' | 'input-available' | 'output-available' | 'output-error'
  input={{ query: 'k8ordo UI' }}
  output="…"
/>
import { mapMessageParts } from '@k8ordo/ui/ai-sdk';
import { Reasoning, ToolInvocation } from '@k8ordo/ui/ai';
import { Response } from '@k8ordo/ui/ai/response';

<Message.Content>
  {mapMessageParts(message).map((part, i) => {
    if (part.kind === 'reasoning') return <Reasoning key={i}>{part.text}</Reasoning>;
    if (part.kind === 'tool') return <ToolInvocation key={i} {...part} />;
    return <Response key={i}>{part.text}</Response>;
  })}
</Message.Content>
'use client';
import { Message } from '@k8ordo/ui/ai';
import { JsonRenderUI } from '@k8ordo/ui/json-render/registry';

// An LLM returned a UI spec as a tool result — render it inside the bubble.
<Message.Root from="assistant">
  <Message.Content>
    <JsonRenderUI spec={spec} />
  </Message.Content>
</Message.Root>