Thinking

Muestra lo que hace un modelo mientras trabaja. Una esfera de shader y una etiqueta en vivo que sigue cada paso, un rastro colapsable de búsquedas, chips de fuentes y razonamiento, y luego una respuesta con citas justo al lado de las afirmaciones que respaldan.

Las esperas largas se sienten más cortas cuando puedes ver que el trabajo avanza. Thinking coloca una línea de estado en vivo al principio de una respuesta: una pequeña esfera de shader, el paso actual en palabras sencillas como «Searching the web» y un temporizador en marcha. Ábrela para leer todo el rastro de búsquedas, fuentes y razonamiento.

El razonamiento escrito como un encabezado en negrita seguido de un párrafo se divide en secciones, y los encabezados a medio escribir se retienen hasta que se cierran. Los pasos de herramienta muestran un spinner y una etiqueta en presente mientras se ejecutan, y luego pasan a lo que hicieron, con chips de fuentes debajo.

El rastro se abre mientras el modelo trabaja y se colapsa a «Thought for 12s» cuando termina, salvo que alguien lo haya abierto o cerrado por su cuenta. La respuesta cita entonces sus fuentes en línea: CitedText convierte [1] y [2][3] en chips compactos con el título, el dominio y un fragmento al pasar el cursor.

  1. Añade el registro Pro a components.json

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

    Crea un token en tu página de cuenta y colócalo en .env.local como HEXTAUI_PRO_TOKEN.

  3. Añade el bloque

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

Con AI SDK

Asigna las partes de un mensaje de useChat: las partes de razonamiento a Reasoning y el texto a CitedText, con las partes source-url como sus fuentes. Pide al modelo que cite con [1], [2] al final de la frase que respaldan.

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

Con pasos de herramienta

Convierte las llamadas a herramientas en pasos. Una llamada que sigue en ejecución muestra un spinner y un shimmer, y se marca como hecha cuando llega su salida.

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

Anatomía

Las partes que compones, de fuera hacia dentro.

ParteDescripción
ReasoningLa línea de estado y el rastro colapsables. Aceptan texto de razonamiento, pasos, o ambos.
ThinkingOrbLa esfera de shader animada. Úsala sola como indicador compacto de ocupado.
CitedTextLa respuesta, con las marcas [n] convertidas en chips de cita.
CitationUn chip con una hover card que enumera sus fuentes.
SourceChipsUna fila de chips de fuentes, usada bajo un paso.

Reasoning

También acepta todas las props de div excepto children.

PropTipoPredeterminado
textEl razonamiento hasta ahora. Las líneas en **negrita** se convierten en encabezados de sección.
string""
streamingSi el modelo sigue trabajando. Dirige la esfera, el shimmer y el temporizador.
booleanfalse
stepsPasos de herramienta mostrados sobre el razonamiento.
ReasoningStep[][]
durationSegundos que mostrar al terminar. Omítelo para usar el tiempo medido.
number–
labelSustituye la etiqueta en vivo durante el streaming.
string–
defaultOpenEstado abierto inicial. Por defecto está abierto mientras llega el streaming.
boolean–
autoCollapseSe colapsa cuando termina el streaming, salvo que alguien lo haya alternado.
booleantrue

ReasoningStep

Una entrada de steps.

PropTipoPredeterminado
idClave estable.
string–
labelLo que está haciendo, por ejemplo «Searching for debounce».
string–
doneLabelLo que hizo, mostrado cuando status es done.
string–
statusLos pasos activos muestran un spinner y dirigen la etiqueta en vivo.
"active" | "done"–
kindElige el icono y el texto de fase.
"search" | "read" | "think" | "code""think"
phaseTexto de fase personalizado mientras el rastro está abierto.
string–
detailContenido adicional bajo el paso.
ReactNode–
sourcesChips mostrados bajo el paso.
Source[]–

CitedText

También acepta todas las props de div excepto children.

PropTipoPredeterminado
textLa respuesta, con marcas al estilo [1] tras las afirmaciones.
string–
sourcesFuentes en orden de cita, de modo que [1] es sources[0].
Source[]–
streamingMuestra el cursor y regula el ritmo del texto.
booleanfalse
smoothDesactiva el ritmo.
booleantrue
PropTipoPredeterminado
idClave estable.
string–
urlSe abre en una pestaña nueva.
string–
titleSe muestra en la hover card.
string–
nameNombre corto del sitio para el chip. Por defecto, el dominio.
string–
snippetLa cita mostrada bajo el título.
string–
iconURL del favicon. Recurre a la primera letra.
string–

ThinkingOrb

También acepta todas las props de span.

PropTipoPredeterminado
activeSe anima mientras es true y descansa cuando es false.
boolean–
colorsTres colores hexadecimales para el shader.
{ deep, mid, light }blue
speedMultiplicador de velocidad de la animación.
number–
KeyAcción
TabPasa a la línea de estado, luego al rastro y luego a cada cita. Una cita enfocada abre su hover card.
EnterSpaceAbre o cierra el rastro.
EscCierra una tarjeta de cita abierta.
  • La línea de estado es un botón que anuncia su resumen mediante una región live polite, de modo que los usuarios de lectores de pantalla oyen «Searching the web», luego «Thought for 12s», sin cada tic del temporizador.
  • Cada chip de cita es un enlace real etiquetado «Source: MDN: Debounce», para que se lea como una fuente y no como un número.
  • Con movimiento reducido, la esfera renderiza un fotograma fijo, el shimmer se detiene y las filas aparecen sin deslizarse.

Construido con

Los componentes gratuitos de HextaUI con los que está hecho Thinking. Cada uno se instala por separado.

Código

5 archivos, añadidos a components/blocks/thinking.