Thinking

Montrez ce que fait un modèle pendant qu’il travaille. Une orbe en shader et un libellé en direct qui suit chaque étape, une trace repliable de recherches, de chips de sources et de raisonnements, puis une réponse avec des citations juste à côté des affirmations qu’elles étayent.

Les longues attentes paraissent plus courtes quand on voit le travail se faire. Thinking place une ligne d’état en direct en haut d’une réponse : une petite orbe en shader, l’étape en cours en mots simples comme « Searching the web », et un minuteur. Ouvrez-la pour lire toute la trace des recherches, sources et raisonnements.

Un raisonnement écrit comme un titre en gras suivi d’un paragraphe se découpe en sections, et les titres à moitié saisis sont retenus jusqu’à leur fermeture. Les étapes d’outil affichent un spinner et un libellé au présent pendant leur exécution, puis passent à ce qu’elles ont fait, avec des chips de sources en dessous.

La trace s’ouvre pendant que le modèle travaille et se replie en « Thought for 12s » à la fin, sauf si quelqu’un l’a ouverte ou fermée lui-même. La réponse cite alors ses sources en ligne : CitedText transforme [1] et [2][3] en chips compactes avec le titre, le domaine et un extrait au survol.

  1. Ajouter le registre Pro à components.json

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

    Créez un token sur votre page de compte et placez-le dans .env.local sous le nom HEXTAUI_PRO_TOKEN.

  3. Ajouter le block

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

Avec AI SDK

Associez les parts d’un message useChat : les parts de raisonnement vers Reasoning et le texte vers CitedText, avec les parts source-url comme sources. Demandez au modèle de citer avec [1], [2] à la fin de la phrase qu’elles étayent.

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

Avec des étapes d’outil

Transforme les appels d’outils en étapes. Un appel encore en cours affiche un spinner et un shimmer, et se coche à l’arrivée de sa sortie.

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

Anatomie

Les parties à composer, de l’extérieur vers l’intérieur.

PartieDescription
ReasoningLa ligne d’état repliable et la trace. Accepte le texte de raisonnement, des steps, ou les deux.
ThinkingOrbL’orbe animée en shader. Utilisez-la seule comme indicateur d’occupation compact.
CitedTextLa réponse, avec les marques [n] transformées en chips de citation.
CitationUne chip avec une carte au survol listant ses sources.
SourceChipsUne rangée de chips de sources, utilisée sous une étape.

Reasoning

Accepte aussi toutes les props de div sauf children.

PropTypePar défaut
textRaisonnement jusqu’ici. Les lignes **en gras** deviennent des titres de section.
string""
streamingIndique si le modèle travaille encore. Pilote l’orbe, le shimmer et le minuteur.
booleanfalse
stepsÉtapes d’outil affichées au-dessus du raisonnement.
ReasoningStep[][]
durationSecondes à afficher une fois terminé. Omettez-le pour utiliser la durée mesurée.
number–
labelRemplace le libellé en direct pendant le streaming.
string–
defaultOpenÉtat ouvert initial. Ouvert par défaut pendant le streaming.
boolean–
autoCollapseSe replie à la fin du streaming, sauf si quelqu’un l’a basculé.
booleantrue

ReasoningStep

Une entrée de steps.

PropTypePar défaut
idClé stable.
string–
labelCe qu’il fait, par exemple « Searching for debounce ».
string–
doneLabelCe qu’il a fait, affiché une fois status à done.
string–
statusLes étapes actives affichent un spinner et pilotent le libellé en direct.
"active" | "done"–
kindChoisit l’icône et le texte de phase.
"search" | "read" | "think" | "code""think"
phaseTexte de phase personnalisé tant que la trace est ouverte.
string–
detailContenu supplémentaire sous l’étape.
ReactNode–
sourcesChips affichées sous l’étape.
Source[]–

CitedText

Accepte aussi toutes les props de div sauf children.

PropTypePar défaut
textLa réponse, avec des marques de style [1] après les affirmations.
string–
sourcesSources dans l’ordre des citations, donc [1] correspond à sources[0].
Source[]–
streamingAffiche le curseur et cadence le texte.
booleanfalse
smoothDésactive la cadence.
booleantrue
PropTypePar défaut
idClé stable.
string–
urlOuvert dans un nouvel onglet.
string–
titleAffiché dans la carte au survol.
string–
nameNom court du site pour la chip. Par défaut, le domaine.
string–
snippetLa citation affichée sous le titre.
string–
iconURL du favicon. Retombe sur la première lettre.
string–

ThinkingOrb

Accepte aussi toutes les props de span.

PropTypePar défaut
activeS’anime tant que la valeur est true et se repose quand elle est false.
boolean–
colorsTrois couleurs hexadécimales pour le shader.
{ deep, mid, light }blue
speedMultiplicateur de vitesse d’animation.
number–
ToucheAction
TabPasse à la ligne d’état, puis dans la trace, puis à chaque citation. Une citation focalisée ouvre sa carte au survol.
EnterSpaceOuvre ou ferme la trace.
EscFerme une carte de citation ouverte.
  • La ligne d’état est un bouton qui annonce son résumé via une région live polie, donc les utilisateurs de lecteur d’écran entendent « Searching the web », puis « Thought for 12s », sans chaque tic du minuteur.
  • Chaque chip de citation est un vrai lien libellé « Source: MDN: Debounce », donc elle se lit comme une source, pas comme un numéro.
  • Avec réduction des animations, l’orbe affiche une image fixe, le shimmer s’arrête et les lignes apparaissent sans glisser.

Construit avec

Les composants HextaUI gratuits dont Thinking est constitué. Chacun s’installe séparément.

Code

5 fichiers, ajoutés à components/blocks/thinking.