Thinking

Zeige, was ein Modell tut, während es arbeitet. Eine Shader-Kugel und ein Live-Label, das jedem Schritt folgt, eine einklappbare Spur aus Suchen, Quellen-Chips und Reasoning, dann eine Antwort mit Zitaten direkt neben den Aussagen, die sie stützen.

Lange Wartezeiten wirken kürzer, wenn man sieht, dass gearbeitet wird. Thinking setzt oben in eine Antwort eine Live-Statuszeile: eine kleine Shader-Kugel, den aktuellen Schritt in einfachen Worten wie „Searching the web“ und einen laufenden Timer. Öffne sie, um die vollständige Spur aus Suchen, Quellen und Reasoning zu lesen.

Reasoning, das als fette Überschrift gefolgt von einem Absatz geschrieben ist, wird in Abschnitte geteilt, und halb getippte Überschriften werden zurückgehalten, bis sie schließen. Tool-Schritte zeigen, solange sie laufen, einen Spinner und ein Label in der Gegenwart, wechseln dann zu dem, was sie getan haben, mit Quellen-Chips darunter.

Die Spur öffnet sich, solange das Modell arbeitet, und klappt zu „Thought for 12s“ zusammen, wenn es fertig ist, es sei denn, jemand hat sie selbst geöffnet oder geschlossen. Die Antwort zitiert dann ihre Quellen inline: CitedText macht aus [1] und [2][3] kompakte Chips mit Titel, Domain und Ausschnitt beim Hover.

  1. Die Pro-Registry zu components.json hinzufügen

    components.json
    {
      "registries": {
        "@hextaui-pro": {
          "url": "https://hextaui.com/r/pro/{name}.json",
          "headers": {
            "Authorization": "Bearer ${HEXTAUI_PRO_TOKEN}"
          }
        }
      }
    }
  2. Token hinzufügen

    Erstelle auf deiner Kontoseite einen Token und trage ihn in .env.local als HEXTAUI_PRO_TOKEN ein.

  3. Den Block hinzufügen

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

Mit dem AI SDK

Ordne die Parts einer useChat-Nachricht zu: Reasoning-Parts an Reasoning und Text an CitedText, mit source-url-Parts als Quellen. Bitte das Modell, mit [1], [2] am Ende des Satzes zu zitieren, den sie stützen.

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

Mit Tool-Schritten

Mache aus Tool-Aufrufen Schritte. Ein noch laufender Aufruf zeigt einen Spinner und einen Shimmer und wird abgehakt, wenn seine Ausgabe eintrifft.

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

Aufbau

Die Teile, die du zusammensetzt, von außen nach innen.

PartBeschreibung
ReasoningDie einklappbare Statuszeile und Spur. Nimmt Reasoning-Text, Schritte oder beides.
ThinkingOrbDie animierte Shader-Kugel. Nutze sie allein als kompakten Beschäftigt-Indikator.
CitedTextDie Antwort, mit zu Zitat-Chips gemachten [n]-Markierungen.
CitationEin Chip mit einer Hover Card, die seine Quellen auflistet.
SourceChipsEine Reihe von Quellen-Chips, unter einem Schritt verwendet.

Reasoning

Akzeptiert auch alle div-Props außer children.

PropTypStandard
textBisheriges Reasoning. **Fette** Zeilen werden zu Abschnittsüberschriften.
string""
streamingOb das Modell noch arbeitet. Steuert die Kugel, den Shimmer und den Timer.
booleanfalse
stepsTool-Schritte, die über dem Reasoning angezeigt werden.
ReasoningStep[][]
durationNach Abschluss anzuzeigende Sekunden. Lass es weg, um die gemessene Zeit zu verwenden.
number–
labelÜberschreibt das Live-Label während des Streamings.
string–
defaultOpenAnfänglicher Öffnungszustand. Standardmäßig geöffnet, während gestreamt wird.
boolean–
autoCollapseKlappt ein, wenn das Streaming endet, es sei denn, jemand hat es umgeschaltet.
booleantrue

ReasoningStep

Ein Eintrag in steps.

PropTypStandard
idStabiler Schlüssel.
string–
labelWas gerade getan wird, zum Beispiel „Searching for debounce“.
string–
doneLabelWas getan wurde, angezeigt sobald status done ist.
string–
statusAktive Schritte zeigen einen Spinner und steuern das Live-Label.
"active" | "done"–
kindWählt das Icon und den Phasentext.
"search" | "read" | "think" | "code""think"
phaseEigener Phasentext, solange die Spur geöffnet ist.
string–
detailZusätzlicher Inhalt unter dem Schritt.
ReactNode–
sourcesChips, die unter dem Schritt angezeigt werden.
Source[]–

CitedText

Akzeptiert auch alle div-Props außer children.

PropTypStandard
textDie Antwort, mit Markierungen im Stil von [1] hinter den Aussagen.
string–
sourcesQuellen in Zitierreihenfolge, sodass [1] sources[0] ist.
Source[]–
streamingZeigt den Cursor und taktet den Text.
booleanfalse
smoothSchaltet das Taktgeben ab.
booleantrue
PropTypStandard
idStabiler Schlüssel.
string–
urlIn einem neuen Tab geöffnet.
string–
titleWird in der Hover Card angezeigt.
string–
nameKurzer Website-Name für den Chip. Standardmäßig die Domain.
string–
snippetDas Zitat, das unter dem Titel angezeigt wird.
string–
iconFavicon-URL. Fällt auf den ersten Buchstaben zurück.
string–

ThinkingOrb

Akzeptiert auch alle span-Props.

PropTypStandard
activeAnimiert bei true und ruht bei false.
boolean–
colorsDrei Hex-Farben für den Shader.
{ deep, mid, light }blue
speedMultiplikator für die Animationsgeschwindigkeit.
number–
TasteAktion
TabWechselt zur Statuszeile, dann in die Spur, dann zu jedem Zitat. Ein fokussiertes Zitat öffnet seine Hover Card.
EnterSpaceÖffnet oder schließt die Spur.
EscSchließt eine geöffnete Zitatkarte.
  • Die Statuszeile ist ein Button, der seine Zusammenfassung über eine höfliche Live-Region ansagt, sodass Screenreader-Nutzer „Searching the web“ und dann „Thought for 12s“ hören, ohne jeden Timer-Tick.
  • Jeder Zitat-Chip ist ein echter Link mit dem Label „Quelle: MDN: Debounce“, sodass er als Quelle gelesen wird, nicht als Zahl.
  • Bei reduzierter Bewegung rendert die Kugel ein Standbild, der Shimmer stoppt und Zeilen erscheinen ohne Gleiten.

Gebaut mit

Die kostenlosen HextaUI-Komponenten, aus denen Thinking besteht. Jede lässt sich einzeln installieren.

Code

5 Dateien, hinzugefügt zu components/blocks/thinking.