Thinking

モデルが作業中に何をしているかを表示します。各ステップに追従するシェーダーのオーブとライブのラベル、検索、出典チップ、推論を折りたためるトレイル、そして根拠となる主張のすぐ隣に引用が付いた回答です。

待ち時間は、作業が進んでいるのが見えると短く感じられます。Thinkingは、回答の上部にライブのステータス行を表示します。小さなシェーダーのオーブ、「Searching the web」のような平易な言葉による現在のステップ、経過時間のタイマーです。開くと、検索、出典、推論のすべての経過を読めます。

太字の見出しとそれに続く段落として書かれた推論はセクションに分割され、入力途中の見出しは閉じられるまで保留されます。ツールのステップは、実行中はスピナーと現在形のラベルを表示し、その後、実行した内容に切り替わり、下に出典チップが付きます。

トレイルはモデルの作業中は開き、完了すると「Thought for 12s」に折りたたまれます。ユーザー自身が開閉した場合は例外です。その後、回答は出典をインラインで引用します。CitedTextは [1] や [2][3] を、ホバーでタイトル、ドメイン、スニペットを表示するコンパクトなチップに変換します。

  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/thinking

AI SDKと使う

useChatメッセージのパーツを対応付けます。reasoningパーツはReasoningに、textはCitedTextに渡し、source-urlパーツをその出典とします。モデルには、根拠となる文の末尾に [1]、[2] で引用するよう指示してください。

"use client"

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

import { CitedText } from "@/components/blocks/thinking/cited-text"
import { Reasoning } from "@/components/blocks/thinking/reasoning"
import type { Source } from "@/components/blocks/thinking/sources"

function AssistantMessage({ message }: { message: UIMessage }) {
  const sources: Source[] = message.parts.flatMap((part) =>
    part.type === "source-url"
      ? [{ id: part.sourceId, url: part.url, title: part.title ?? part.url }]
      : []
  )

  return (
    <div className="flex flex-col gap-4">
      {message.parts.map((part, index) => {
        if (part.type === "reasoning") {
          return (
            <Reasoning
              key={index}
              text={part.text}
              streaming={part.state === "streaming"}
            />
          )
        }
        if (part.type === "text") {
          return (
            <CitedText
              key={index}
              text={part.text}
              sources={sources}
              streaming={part.state === "streaming"}
            />
          )
        }
        return null
      })}
    </div>
  )
}

export function Chat() {
  const { messages } = useChat()

  return messages.map((message) =>
    message.role === "assistant" ? (
      <AssistantMessage key={message.id} message={message} />
    ) : null
  )
}

ツールのステップつき

ツール呼び出しをステップに変換します。まだ実行中の呼び出しにはスピナーとシマーが表示され、出力が届くとチェックが付きます。

"use client"

import { getToolName, isToolUIPart, type UIMessage } from "ai"

import { Reasoning, type ReasoningStep } from "@/components/blocks/thinking/reasoning"

const labels: Record<string, Pick<ReasoningStep, "label" | "kind">> = {
  webSearch: { label: "Searching the web", kind: "search" },
  readPage: { label: "Reading sources", kind: "read" },
  runCode: { label: "Running code", kind: "code" },
}

export function Thoughts({
  message,
  streaming,
}: {
  message: UIMessage
  streaming: boolean
}) {
  const steps: ReasoningStep[] = message.parts
    .filter(isToolUIPart)
    .map((part) => {
      const name = getToolName(part)
      return {
        id: part.toolCallId,
        ...(labels[name] ?? { label: name, kind: "think" }),
        status: part.state.startsWith("output") ? "done" : "active",
      }
    })

  const text = message.parts
    .flatMap((part) => (part.type === "reasoning" ? [part.text] : []))
    .join("\n\n")

  return <Reasoning steps={steps} text={text} streaming={streaming} />
}

構造

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

パーツ説明
Reasoning折りたたみ可能なステータス行とトレイル。推論テキスト、ステップ、またはその両方を受け取ります。
ThinkingOrbアニメーションするシェーダーのオーブ。単独で、コンパクトなビジーインジケーターとして使います。
CitedText[n] のマークが引用チップに変換された回答。
Citation出典を一覧するホバーカードを持つ、1つのチップ。
SourceChipsステップの下で使われる、出典チップの列。

Reasoning

childrenを除くすべてのdivプロップも受け付けます。

プロパティ型デフォルト
textここまでの推論。**太字**の行はセクションの見出しになります。
string""
streamingモデルがまだ作業中かどうか。オーブ、シマー、タイマーを駆動します。
booleanfalse
steps推論の上に表示されるツールのステップ。
ReasoningStep[][]
duration完了後に表示する秒数。省略すると、計測された時間が使われます。
number–
labelストリーミング中のライブラベルを上書きします。
string–
defaultOpen初期の開閉状態。デフォルトでは、ストリーミング中は開いています。
boolean–
autoCollapseユーザーが切り替えていない限り、ストリーミングが終わったら折りたたみます。
booleantrue

ReasoningStep

stepsの1つの項目。

プロパティ型デフォルト
id安定したキー。
string–
label実行中の内容。たとえば「Searching for debounce」。
string–
doneLabelstatusがdoneになったときに表示される、実行した内容。
string–
statusアクティブなステップにはスピナーが表示され、ライブのラベルを駆動します。
"active" | "done"–
kindアイコンとフェーズテキストを選びます。
"search" | "read" | "think" | "code""think"
phaseトレイルが開いている間の、カスタムのフェーズテキスト。
string–
detailステップの下の追加コンテンツ。
ReactNode–
sourcesステップの下に表示されるチップ。
Source[]–

CitedText

childrenを除くすべてのdivプロップも受け付けます。

プロパティ型デフォルト
text主張の後ろに [1] 形式のマークが付いた回答。
string–
sources引用順の出典。[1] は sources[0] です。
Source[]–
streamingキャレットを表示し、テキストのペースを調整します。
booleanfalse
smoothペース調整をオフにします。
booleantrue
プロパティ型デフォルト
id安定したキー。
string–
url新しいタブで開きます。
string–
titleホバーカードに表示されます。
string–
nameチップに表示する短いサイト名。デフォルトはドメインです。
string–
snippetタイトルの下に表示される引用文。
string–
iconファビコンのURL。ない場合は最初の文字にフォールバックします。
string–

ThinkingOrb

すべてのspanプロップも受け付けます。

プロパティ型デフォルト
activetrueの間はアニメーションし、falseのときは静止します。
boolean–
colorsシェーダー用の3つの16進カラー。
{ deep, mid, light }blue
speedアニメーション速度の倍率。
number–
キーアクション
Tabステータス行、トレイル、各引用の順に移動します。フォーカスされた引用は、ホバーカードを開きます。
EnterSpaceトレイルを開閉します。
Esc開いている引用カードを閉じます。
  • ステータス行は、politeなライブリージョンを通じて要約を読み上げるボタンなので、スクリーンリーダーのユーザーは、タイマーの更新を毎回聞くことなく、「Searching the web」、続いて「Thought for 12s」と聞こえます。
  • 各引用チップは「Source: MDN: Debounce」というラベルを持つ本物のリンクなので、番号ではなく出典として読み上げられます。
  • モーション軽減時は、オーブは静止したフレームを描画し、シマーは止まり、行はスライドせずに表示されます。

使用技術

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

コード

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