Thinking

Mostre o que um modelo está fazendo enquanto trabalha. Um orbe de shader e um rótulo ao vivo que acompanha cada etapa, uma trilha recolhível de buscas, chips de fonte e raciocínio, e depois uma resposta com citações bem ao lado das afirmações que sustentam.

Esperas longas parecem mais curtas quando você vê o trabalho acontecendo. O Thinking coloca uma linha de status ao vivo no topo de uma resposta: um pequeno orbe de shader, a etapa atual em palavras simples como "Pesquisando na web" e um cronômetro em andamento. Abra-o para ler toda a trilha de buscas, fontes e raciocínio.

O raciocínio escrito como um título em negrito seguido de um parágrafo é dividido em seções, e títulos pela metade ficam retidos até serem fechados. As etapas de ferramenta mostram um spinner e um rótulo no presente enquanto rodam, depois passam para o que fizeram, com chips de fonte embaixo.

A trilha abre enquanto o modelo trabalha e se recolhe para "Pensou por 12s" quando ele termina, a menos que alguém a tenha aberto ou fechado por conta própria. A resposta então cita suas fontes inline: o CitedText transforma [1] e [2][3] em chips compactos com título, domínio e trecho no hover.

  1. Adicione o registro Pro ao components.json

    components.json
    {
      "registries": {
        "@hextaui-pro": {
          "url": "https://hextaui.com/r/pro/{name}.json",
          "headers": {
            "Authorization": "Bearer ${HEXTAUI_PRO_TOKEN}"
          }
        }
      }
    }
  2. Adicione seu token

    Crie um token na sua página de conta e coloque-o em .env.local como HEXTAUI_PRO_TOKEN.

  3. Adicione o bloco

    pnpm dlx shadcn@latest add @hextaui-pro/thinking

Com o AI SDK

Mapeie as partes de uma mensagem do useChat: partes de reasoning para Reasoning e texto para CitedText, com as partes source-url como suas fontes. Peça ao modelo que cite com [1], [2] ao final da frase que elas sustentam.

"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
  )
}

Com etapas de ferramenta

Transforme chamadas de ferramenta em etapas. Uma chamada que ainda está em execução mostra um spinner e um shimmer, e é marcada quando sua saída chega.

"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} />
}

Anatomia

As partes que você compõe, de fora para dentro.

ParteDescrição
ReasoningA linha de status e a trilha recolhíveis. Recebe o texto de raciocínio, etapas, ou ambos.
ThinkingOrbO orbe de shader animado. Use-o sozinho como um indicador de ocupado compacto.
CitedTextA resposta, com as marcas [n] transformadas em chips de citação.
CitationUm chip com um hover card que lista suas fontes.
SourceChipsUma linha de chips de fonte, usada sob uma etapa.

Reasoning

Também aceita todas as props de div, exceto children.

PropTipoPadrão
textO raciocínio até agora. Linhas em **negrito** viram títulos de seção.
string""
streamingSe o modelo ainda está trabalhando. Controla o orbe, o shimmer e o cronômetro.
booleanfalse
stepsEtapas de ferramenta exibidas acima do raciocínio.
ReasoningStep[][]
durationSegundos a exibir ao terminar. Omita para usar o tempo medido.
number–
labelSubstitui o rótulo ao vivo durante o streaming.
string–
defaultOpenEstado inicial de abertura. O padrão é aberto durante o streaming.
boolean–
autoCollapseRecolhe quando o streaming termina, a menos que alguém o tenha alternado.
booleantrue

ReasoningStep

Uma entrada em steps.

PropTipoPadrão
idChave estável.
string–
labelO que está fazendo, por exemplo "Pesquisando por debounce".
string–
doneLabelO que fez, exibido quando o status é done.
string–
statusAs etapas ativas mostram um spinner e conduzem o rótulo ao vivo.
"active" | "done"–
kindEscolhe o ícone e o texto da fase.
"search" | "read" | "think" | "code""think"
phaseTexto de fase personalizado enquanto a trilha está aberta.
string–
detailConteúdo extra sob a etapa.
ReactNode–
sourcesChips exibidos sob a etapa.
Source[]–

CitedText

Também aceita todas as props de div, exceto children.

PropTipoPadrão
textA resposta, com marcas no estilo [1] após as afirmações.
string–
sourcesFontes na ordem de citação, então [1] é sources[0].
Source[]–
streamingMostra o cursor e dá ritmo ao texto.
booleanfalse
smoothDesativa o ritmo.
booleantrue
PropTipoPadrão
idChave estável.
string–
urlAberto em uma nova aba.
string–
titleExibido no hover card.
string–
nameNome curto do site para o chip. O padrão é o domínio.
string–
snippetA citação exibida sob o título.
string–
iconURL do favicon. Usa a primeira letra como fallback.
string–

ThinkingOrb

Também aceita todas as props de span.

PropTipoPadrão
activeAnima enquanto true e descansa quando false.
boolean–
colorsTrês cores hex para o shader.
{ deep, mid, light }blue
speedMultiplicador de velocidade da animação.
number–
TeclaAção
TabVai para a linha de status, depois para dentro da trilha e depois para cada citação. Uma citação em foco abre seu hover card.
EnterSpaceAbre ou fecha a trilha.
EscFecha um card de citação aberto.
  • A linha de status é um botão que anuncia seu resumo por uma região live polite, então quem usa leitor de tela ouve "Pesquisando na web" e depois "Pensou por 12s", sem cada tick do cronômetro.
  • Cada chip de citação é um link de verdade com o rótulo "Fonte: MDN: Debounce", então é lido como uma fonte, e não como um número.
  • Com movimento reduzido, o orbe renderiza um quadro estático, o shimmer para e as linhas aparecem sem deslizar.

Construído com

Os componentes gratuitos do HextaUI de que Thinking é feito. Cada um é instalado separadamente.

Código

5 arquivos, adicionados a components/blocks/thinking.