HextaAI

HextaUIのすべてのAIブロックで作られた、完全なAIチャットアプリです。サイドバーのチャット、出典つきの思考、差分と承認つきのツール呼び出し、エージェントの実行前に確認するプラン、ストリーミングされるMarkdownとコード、無音の音声モードを備え、すべてAI SDKのメッセージパーツで動作します。

HextaAIは、HextaUIのすべてのAIブロックで作られた、完全なAIチャットアプリです。Chat Sidebarが履歴を、Chat Threadが会話を、Prompt Inputがコンポーザーを担い、各返信はそのパーツから描画されます。推論にはThinking、エージェントの行動にはTool Calls、プランにはAgent Todos、差分とターミナルにはCode Block、回答にはStreaming Markdownが使われます。Artifactは、モデルが作成したWebページ、ドキュメント、ファイルをチャットの横のパネルで開き、バージョンを持ち、小さな変更は書き換えずに開いているファイルにタイプされます。Voice Modeはコンポーザーから開き、通話を終えると会話をチャットに落とし込みます。

中核はHextaMessageParts です。AI SDKのuseChatが返すものと同じメッセージパーツを受け取り、隣り合う推論とツール呼び出しをグループ化し、返信の先頭に1つのライブなプランを保ち、モデルが見つけた出典にリンクされた引用つきでテキストを描画します。読み取り、編集、書き込み、検索、Web取得、シェルコマンドにはそれぞれ適したビューがあり、独自のツール用のレンダラーも追加できます。

デモは、リアルなパーツをオフラインでストリーミングする小さなエンジンであるScriptedChatsで動いています。提案を試してみてください。料金ページを作ってチャットの横で開く様子を見る、質問を調べて出典が集まる様子を見る、機能を依頼して実行前にプランをレビューする、失敗して修正されるテスト実行を承認する、コードとテーブルつきの説明を求める、といったことができます。返信がバックグラウンドでストリーミングされ続ける間に、停止、再生成、編集、チャットの切り替えを行い、その後音声モードを開いて話しかけてみてください。

  1. Proレジストリをcomponents.jsonに追加する

    components.json
    {
      "registries": {
        "@hextaui-pro": {
          "url": "https://hextaui.com/r/pro/{name}.json",
          "headers": {
            "Authorization": "Bearer ${HEXTAUI_PRO_TOKEN}"
          }
        }
      }
    }
  2. トークンを追加する

    アカウントページでトークンを作成し、.env.local に HEXTAUI_PRO_TOKEN として設定してください。

  3. ブロックを追加する

    pnpm dlx shadcn@latest add @hextaui-pro/hexta-ai

AI SDKと使う

useChatのメッセージをそのままHextaMessagePartsに渡してください。ツールの承認とプランのレビューは、addToolApprovalResponseを通じて応答され、編集されたプランは承認の理由としてモデルに返されます。

"use client"

import { useChat } from "@ai-sdk/react"
import { lastAssistantMessageIsCompleteWithApprovalResponses } from "ai"

import { ChatAssistantMessage, ChatThread, ChatUserMessage } from "../chat-thread/chat-thread"
import { PromptInput, PromptInputActions, PromptInputBody, PromptInputSubmit, PromptInputTextarea } from "../prompt-input/prompt-input"
import { HextaMessageParts } from "@/components/blocks/hexta-ai/message-parts"
import { textOf, type HextaMessage, type ToolPart } from "@/components/blocks/hexta-ai/parts"

export function Assistant() {
  const { messages, status, sendMessage, stop, regenerate, addToolApprovalResponse } = useChat({
    sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithApprovalResponses,
  })
  const busy = status === "submitted" || status === "streaming"
  const chat = messages as HextaMessage[]

  const approvalId = (toolCallId: string) =>
    chat
      .flatMap((message) => message.parts)
      .find((part): part is ToolPart => "toolCallId" in part && part.toolCallId === toolCallId)
      ?.approval?.id

  return (
    <ChatThread
      busy={busy}
      composer={
        <PromptInput status={status} onStop={stop} onSubmit={({ text }) => sendMessage({ text })}>
          <PromptInputBody>
            <PromptInputTextarea />
            <PromptInputActions>
              <PromptInputSubmit />
            </PromptInputActions>
          </PromptInputBody>
        </PromptInput>
      }
    >
      {chat.map((message, index) => {
        const last = index === chat.length - 1
        if (message.role === "user") {
          return <ChatUserMessage key={message.id} id={message.id} text={textOf(message)} />
        }
        return (
          <ChatAssistantMessage
            key={message.id}
            id={message.id}
            text={textOf(message)}
            streaming={last && busy}
            last={last}
            onRetry={last ? () => regenerate() : undefined}
          >
            <HextaMessageParts
              parts={message.parts}
              streaming={last && busy}
              onApproval={(toolCallId, { approved, reason }) => {
                const id = approvalId(toolCallId)
                if (id) addToolApprovalResponse({ id, approved, reason })
              }}
              onPlanApprove={(toolCallId, todos) => {
                const id = approvalId(toolCallId)
                if (!id) return
                addToolApprovalResponse({
                  id,
                  approved: true,
                  reason: `Run this plan:\n${todos.map((todo, step) => `${step + 1}. ${todo.content}`).join("\n")}`,
                })
              }}
              onPlanCancel={(toolCallId) => {
                const id = approvalId(toolCallId)
                if (id) addToolApprovalResponse({ id, approved: false })
              }}
            />
          </ChatAssistantMessage>
        )
      })}
    </ChatThread>
  )
}

独自のツール

ツール名を、その種類、対象、内容を返すレンダラーに対応付けます。対応付けていないツールは、組み込みのビューまたはJSONビューにフォールバックします。

"use client"

import { CodeBlock } from "../code-block/code-block"
import { HextaMessageParts, type ToolRenderer } from "@/components/blocks/hexta-ai/message-parts"
import type { MessagePart } from "@/components/blocks/hexta-ai/parts"

const tools: Record<string, ToolRenderer> = {
  create_chart: (part) => {
    const input = part.input as { title?: string; spec?: string } | undefined
    return {
      kind: "write",
      subject: input?.title ?? "Chart",
      content: input?.spec ? <CodeBlock code={input.spec} language="json" filename="chart.json" /> : undefined,
    }
  },
}

export function Reply({ parts, streaming }: { parts: MessagePart[]; streaming: boolean }) {
  return <HextaMessageParts parts={parts} streaming={streaming} tools={tools} />
}

スクリプトによるデモ

ScriptedChatsは、スクリプトからAI SDK形式のパーツを、実際のペース配分、承認、停止とともにストリーミングします。プロダクトツアー、オンボーディング、オフラインデモに使えます。

"use client"

import * as React from "react"

import { HextaMessageParts } from "@/components/blocks/hexta-ai/message-parts"
import { textOf } from "@/components/blocks/hexta-ai/parts"
import { ScriptedChats, useScriptedChat, type Script } from "@/components/blocks/hexta-ai/scripted-chat"

const onboarding: Script = async (run) => {
  await run.think("**Picking a first step**\nShow them the one thing that makes the product click.")
  const search = await run.tool("web_search", { query: "best onboarding checklists" })
  await run.pause(600)
  search.finish({ results: 4 })
  await run.say("Start with **one task they can finish in a minute**, then show what it unlocked.")
}

export function ProductTour() {
  const [store] = React.useState(() => new ScriptedChats(() => onboarding))
  const { messages, status } = useScriptedChat(store, "tour")
  const busy = status === "submitted" || status === "streaming"

  return (
    <div className="flex flex-col gap-4">
      <button onClick={() => void store.send("tour", "How should onboarding start?")}>Play</button>
      {messages.map((message, index) =>
        message.role === "user" ? (
          <p key={message.id}>{textOf(message)}</p>
        ) : (
          <HextaMessageParts
            key={message.id}
            parts={message.parts}
            streaming={busy && index === messages.length - 1}
            reasoningDurations={message.metadata?.thoughts}
            onApproval={(id, approval) => store.decide(id, approval)}
          />
        )
      )}
    </div>
  )
}

構造

外側から内側へ組み合わせるパーツ。

パーツ説明
HextaMessageParts1つのアシスタントメッセージを、そのパーツから順に描画します。推論、プラン、ツール呼び出し、Markdownの順です。
defaultToolsread_file、list_files、grep、web_search、fetch、edit_file、write_file、bash用の組み込みレンダラー。
ScriptedChats, useScriptedChatスクリプトからAI SDK形式のパーツをストリーミングする、オフラインのエンジンです。停止、再生成、編集、承認、プランのレビューに対応します。
textOf, sourcesOf, toBlocksメッセージからテキストと出典を読み取り、レンダラーと同じ方法でパーツをグループ化するヘルパー。
HextaAIDemoアプリ全体。ScriptedChatsに接続されたChat Sidebar、Chat Thread、Prompt Input、Voice Mode。
プロパティ型デフォルト
partsメッセージのパーツ。useChatが返す、text、reasoning、source-url、file、toolのパーツ。
MessagePart[]–
streamingこのメッセージがまだ届いている途中かどうか。最後の推論またはテキストのブロックはアニメーションし、実行中のツールはライブのままです。
booleanfalse
stopped実行中のツールをキャンセル済みとしてマークし、保留中のプランレビューを非表示にします。
booleanfalse
toolsツール名ごとのレンダラー。それぞれがkind、subject、title、meta、content、exitCode、errorを返し、組み込みのビューを上書きします。
Record<string, ToolRenderer>{}
onApproval承認待ちのツール呼び出しが許可または拒否されたときに呼ばれます。
(toolCallId, { approved, reason?, always? }) => void–
onPlanApproveレビューからプランが実行されたとき、編集されたプランとともに呼ばれます。
(toolCallId, todos) => void–
onPlanCancelプランのレビューがキャンセルされたときに呼ばれます。
(toolCallId) => void–
reasoningDurations各推論ブロックにかかった秒数。再読み込みしても「Thought for」が正しく保たれます。
number[]–
プロパティ型デフォルト
new ScriptedChats(respond)各メッセージに応答するスクリプトを選びます。
(prompt, files) => Script–
send(chatId, text, files?, options?)ユーザーメッセージを追加し、返信をストリーミングします。options: model、thinking、そしてすべての待機をスキップするinstant。
Promise<void>–
stop, regenerate, edit, append, remove返信を停止する、直前の返信をやり直す、編集したメッセージから再送信する、完了したメッセージを追加する、チャットを破棄する。
methods–
decide(toolCallId, decision)保留中の承認またはプランのレビューに応答します。編集したプランを実行するには、todosを渡します。
void–
useScriptedChat(store, chatId)コンポーネントを1つのチャットに購読させます。
{ messages, status, pinned }–
プロパティ型デフォルト
think(text)推論パーツをストリーミングします。thinkingがオフの場合はスキップされます。
Promise<void>–
say(markdown)本物のモデルのように、不揃いなチャンクでテキストパーツをストリーミングします。
Promise<void>–
tool(name, input, { stream? })ツール呼び出しを追加します。入力フィールドを1つ、ストリーミングさせることもできます。ハンドルで、承認、進捗、完了、失敗を操作できます。
Promise<ToolHandle>–
plan(todos)レビュー用にプランを表示して待機します。編集されたプランを返し、キャンセルされた場合はnullを返します。
Promise<Todo[] | null>–
todos(todos), source(page), pause(ms)プランを更新するか、出典を引用するか、待機します。
methods–
キーアクション
⌘Kチャットを検索します。
⌘⇧O新しいチャットを開始します。
⌘Bサイドバーをアイコンに折りたたむか、スマートフォンでは全画面で開きます。
Enterメッセージを送信します。Shift+Enterで改行します。
⌘↵レビュー中のプランを実行するか、待機中のツール呼び出しを許可します。
⌘⌫待機中のツール呼び出しを拒否します。
Esc返信を停止するか、メニューを閉じるか、音声モードを終了します。
  • すべてのパーツは、それを描画するブロックのアクセシビリティを引き継ぎます。スレッドは新しい返信をpoliteに読み上げ、ツール呼び出しとプランは状態をテキストで公開し、引用は出典を名前で示し、承認はキーボードショートカットを持つ本物のボタンです。
  • 重要な情報が、モーションや色だけで示されることはありません。スピナーには「Replying」のようなラベルが付き、バックグラウンドで完了したチャットには「Unread reply」のラベルが付き、差分では追加された行と削除された行が色に加えて記号でも示されます。
  • ストリーミング、シェーダー、シートのアニメーションはすべて、モーション軽減の設定を尊重します。音声モードは返信を読み上げなくても動作し、字幕を表示し、マイクがない場合はサンプルの会話を再生します。

使用技術

HextaAI を構成する無料のHextaUIコンポーネントです。それぞれ単独でインストールできます。

コード

5 個のファイルを components/blocks/hexta-ai に追加しました。