k8ordo
  • UI
  • Form
  • State
  • Router
  • Static
  • Server
ダークモードに切り替え
  • Get Started
  • Theming
  • i18n
  • Components
  • Hooks
  • Helpers
  • AI
ナビゲーションを開く
AI
  • AI Chat
  • Generative UI
  • AI Agents
k8ordo

Baselineに入った機能を、制限なく使うReactのライブラリ群。

パッケージ
  • @k8ordo/ui
  • @k8ordo/form
  • @k8ordo/state
  • @k8ordo/router
  • @k8ordo/static
  • @k8ordo/server
UI
  • Get Started
  • Theming
  • i18n
  • Components
  • Hooks
  • Helpers
  • AIチャット
  • Generative UI
  • AIエージェント
  • Storybook
リソース
  • GitHub
  • npm

© 2026 k8o — MIT License

組版 — Noto Sans JP / M PLUS 2

AI

AI
  • AI Chat
  • Generative UI
  • AI Agents

AIチャット

AIチャットUIのためのpresentationalな部品です。通信やメッセージの状態は持たず、データを渡してmessages.map()で組み立てるだけ。AI SDKでも自前のバックエンドでも接続できます。@k8ordo/ui/aiからimportします。

デモ

実際のチャット画面です。サジェスチョンを選ぶか、メッセージを入力して送信すると、吹き出しが会話に積まれていきます。思考過程やツール呼び出しは折りたたみで確認できます。

会話を組み立てる

Conversationはstick-to-bottomと「最新へ」ボタン付きのスクロール領域、Messageはroleごとの吹き出し、PromptInputは入力欄です。メッセージ配列は利用側が持ちます。

入力欄(IME対応)

Enterで送信、Shift+Enterで改行、そしてIME変換を確定するEnterでは送信しません。statusに応じて送信 / 停止ボタンが切り替わります。

サジェスチョン

Suggestionは定型の質問をチップで並べ、選択された値をそのまま送信ハンドラに渡します。

ストリーミングMarkdown

Responseはストリーミング中のMarkdownを描画し、未クローズのブロックにも耐えます。別サブパスに分かれており、optional peerのstreamdownとそのスタイルシートが必要です。

ツール呼び出しと思考

ToolInvocationとReasoningはツールの実行や思考過程を折りたたみで表示します。stateの語彙はAI SDKのツールパートの状態に揃えています。

AI SDK連携

mapMessageParts(@k8ordo/ui/ai-sdk)はAI SDKのUIMessage.partsを、自分で描画しやすい素朴な配列に変換します。optional peerのaiが必要です。

吹き出しの中にGenerative UI

Message.Contentは任意のchildrenを取れるので、json-renderのregistryを使ってLLMが生成したUI specを吹き出しの中に描画できます。会話の中へそのままGenerative UIを届けられます。

Props

コンポーネントの型から生成したpropsの一覧です。開閉を持つコンポーネント(Reasoning / ToolInvocation)はisOpen / defaultOpen / onChangeのcontrolled / uncontrolled両対応です。

Conversation.Root

children
Type: ReactNode
Default: -
PropTypeDefault
childrenReactNode-

Conversation.Messages

children
Type: ReactNode
Default: -
isStreaming?
Type: boolean
Default: false
label?
Type: string
Default:
-
縦書きプレビューに切り替え
こんにちは。k8ordo UIのAIチャットについて、何でも聞いてください。
ReactでAIチャットを作るとき、何から始めればいい?
まず会話の器・吹き出し・入力欄の3つが土台。Markdownやツール表示は後段で足せる。

入力

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

出力

Conversation / Message / PromptInputの3つから始めるのが推奨です。
まずはConversation・Message・PromptInputの3つで会話の骨組みを作り、そのあとResponse(Markdown)やToolInvocationを足していくのがおすすめです。
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-

型ベース(内部で固定する一部attrsは除外): HTMLAttributes<HTMLDivElement>

Message.Content

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

型ベース(内部で固定する一部attrsは除外): 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>-

型ベース(内部で固定する一部attrsは除外): 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

型ベース(内部で固定する一部attrsは除外): 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>