Chat Thread

Die ganze Unterhaltung rund um den Composer. Deine Frage bleibt oben angeheftet, während die Antwort streamt, Checkpoints an der Seite springen zwischen Nachrichten, und jede Antwort lässt sich kopieren, bearbeiten, wiederholen, bewerten und zwischen Versionen umschalten. Antworten rendern Markdown mit Codeblöcken, Tabellen und Zitaten, und Thinking, Tool-Aufrufe und Prompt Input lassen sich direkt einsetzen.

Chat Thread ist die Unterhaltung rund um den Composer: deine Nachrichten, die Antworten des Modells und was du damit tun kannst. Er beginnt mit deinem Leerzustand und dem zentrierten Composer und dockt den Composer unten an, sobald es Nachrichten gibt, ohne ihn neu zu mounten, sodass Fokus und Entwurfstext erhalten bleiben.

Die Nachricht, die du gerade gesendet hast, rückt nach oben, während die Antwort darunter streamt, und die Ansicht folgt neuem Text, bis du wegscrollst. Eine Leiste mit Checkpoints rechts lässt dich zwischen deinen Nachrichten springen, mit Vorschau beim Hover. Älterer Verlauf lädt von oben, ohne dass sich die Ansicht bewegt.

Fahre über eine Nachricht, um sie zu kopieren, zu bearbeiten oder erneut zu versuchen. Bearbeitungen und Wiederholungen werden zu Versionen, zwischen denen du wechseln kannst, und nichts geht verloren. Fehlgeschlagene Antworten bieten Try again, gestoppte Antworten sagen das, und Daumen hoch oder runter ist ein Klick. In jeder Antwort lassen sich Reasoning, ToolCalls und Markdown aus den anderen Blocks direkt einsetzen.

  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/chat-thread

Mit dem AI SDK

Ordne useChat-Nachrichten dem Thread zu. Reasoning-Parts gehen an Reasoning, Tool-Parts an ToolCalls und Text an Markdown. Hefte die neueste Nutzernachricht an, nutze regenerate für Try again und sende eine Bearbeitung ab diesem Punkt erneut.

"use client"

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

import { PromptInput, PromptInputActions, PromptInputBody, PromptInputSubmit, PromptInputTextarea } from "../prompt-input/prompt-input"
import { Reasoning } from "../thinking/reasoning"
import { ToolCalls } from "../tool-calls/tool-group"
import { getToolPartStatus } from "../tool-calls/tool-part"
import { Markdown } from "../markdown/markdown"
import { ChatAssistantMessage, ChatThread, ChatUserMessage } from "@/components/blocks/chat-thread/chat-thread"

function textOf(message: UIMessage) {
  return message.parts.flatMap((part) => (part.type === "text" ? [part.text] : [])).join("")
}

export function Chat() {
  const { messages, status, sendMessage, regenerate, stop, setMessages } = useChat()
  const busy = status === "submitted" || status === "streaming"
  const lastUser = messages.findLastIndex((message) => message.role === "user")

  return (
    <ChatThread
      busy={busy}
      composer={
        <PromptInput status={status} onStop={stop} onSubmit={({ text }) => sendMessage({ text })}>
          <PromptInputBody>
            <PromptInputTextarea />
            <PromptInputActions>
              <PromptInputSubmit />
            </PromptInputActions>
          </PromptInputBody>
        </PromptInput>
      }
    >
      {messages.map((message, index) => {
        const last = index === messages.length - 1
        if (message.role === "user") {
          return (
            <ChatUserMessage
              key={message.id}
              id={message.id}
              text={textOf(message)}
              pin={index === lastUser}
              onEdit={(text) => {
                setMessages(messages.slice(0, index))
                sendMessage({ text })
              }}
            />
          )
        }
        const reasoning = message.parts.flatMap((part) => (part.type === "reasoning" ? [part.text] : [])).join("\n\n")
        const tools = message.parts.filter(isToolUIPart)
        const streaming = last && busy
        return (
          <ChatAssistantMessage
            key={message.id}
            id={message.id}
            text={textOf(message)}
            streaming={streaming}
            last={last}
            error={last && status === "error" ? "Something went wrong." : null}
            onRetry={last ? () => regenerate() : undefined}
          >
            {reasoning ? <Reasoning text={reasoning} streaming={streaming && !textOf(message)} /> : null}
            {tools.length > 0 ? (
              <ToolCalls
                calls={tools.map((part) => ({
                  id: part.toolCallId,
                  name: getToolName(part),
                  status: getToolPartStatus(part, { stopped: !streaming }),
                }))}
              />
            ) : null}
            <Markdown
              text={textOf(message)}
              streaming={streaming}
              sources={message.parts.flatMap((part) =>
                part.type === "source-url"
                  ? [{ id: part.sourceId, url: part.url, title: part.title ?? part.url }]
                  : []
              )}
            />
          </ChatAssistantMessage>
        )
      })}
    </ChatThread>
  )
}

Aufbau

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

PartBeschreibung
ChatThreadDer Rahmen: Scroller, Checkpoints, Leerzustand und Composer-Slot.
ChatUserMessageDeine Nachricht: Anhänge, Bubble, Zeit, Versionen, Kopieren und Bearbeiten.
ChatAssistantMessageEine Antwort. Setze Reasoning, ToolCalls und Markdown hinein; sie fügt die Aktionen sowie Fehler- und Stopp-Zustände hinzu.
ChatDividerEine beschriftete Linie zwischen Nachrichtengruppen.
ChatAttachments, ChatAction, ChatCopy, ChatBranchSwitch, ChatEditorDie Bausteine, die die Nachrichten verwenden, für eigene Layouts exportiert.
PropTypStandard
childrenNachrichten und Trenner, der Reihe nach.
ReactNode–
composerMeist Prompt Input. Zentriert, wenn leer, sonst unten angedockt.
ReactNode–
emptyWird über dem Composer angezeigt, wenn es keine Nachrichten gibt.
ReactNode–
busyMarkiert die Unterhaltung für assistive Technologien als beschäftigt, während eine Antwort streamt.
booleanfalse
hasOlderZeigt oben „Load earlier messages“.
booleanfalse
loadingOlderZeigt auf diesem Button einen Spinner. Die Scrollposition wird wiederhergestellt, sobald es false wird.
booleanfalse
onLoadOlderÄltere Nachrichten hier voranstellen.
() => void–
PropTypStandard
idStabile ID. Behalte sie über Versionen hinweg bei, damit die Ansicht nicht springt.
string–
textDie Nachricht. Wird auch für die Checkpoint-Vorschau verwendet.
string–
pinScrollt diese Nachricht nach oben, wenn sie erscheint. Setze es bei der gerade gesendeten Nachricht.
booleanfalse
files{ id, name, size?, type?, url? }. Bilder mit einer url werden als Thumbnails angezeigt.
ChatFile[]–
timeWird beim Hover angezeigt, in der Locale des Lesers.
Date–
branch{ index, count, onChange } für die Versionspfeile.
ChatBranch–
onEditZeigt Edit. Wird mit dem neuen Text aufgerufen, wenn er sich geändert hat.
(text: string) => void–
attachmentsEigener Inhalt über der Bubble.
ReactNode–
PropTypStandard
idStabile ID.
string–
childrenReasoning, ToolCalls, Markdown oder irgendetwas anderes.
ReactNode–
textReiner Text für Copy.
string–
streamingBlendet die Aktionen aus, bis die Antwort fertig ist.
booleanfalse
lastHält die Aktionen sichtbar, statt sie beim Hover zu zeigen.
booleanfalse
metaRechte Seite der Aktionszeile, etwa das Modell und der Effort.
ReactNode–
branchVersionspfeile für Wiederholungen.
ChatBranch–
feedbackDie aktuelle Bewertung.
"up" | "down" | nullnull
onFeedbackZeigt die Daumen. Wird mit null aufgerufen, wenn eine Bewertung zurückgenommen wird.
(feedback) => void–
onRetryZeigt Try again.
() => void–
errorZeigt die Nachricht mit Try again statt der Aktionen.
string | null–
stoppedMarkiert eine Antwort, die vorzeitig gestoppt wurde.
booleanfalse
PropTypStandard
childrenDas Label, etwa Heute oder „Zu Nova 3 Max gewechselt“.
ReactNode–
TasteAktion
TabWechselt durch die Aktionen der Nachrichten. Sie erscheinen bei Fokus, nicht nur beim Hover.
EnterSendet eine bearbeitete Nachricht.
ShiftEnterFügt beim Bearbeiten eine Zeile hinzu.
EscBricht eine Bearbeitung ab.
↑↓PgUpPgDnScrollt die Unterhaltung, wenn sie den Fokus hat.
  • Die Unterhaltung ist eine beschriftete Region mit einem Log, sodass neue Nachrichten beim Eintreffen angesagt werden und die Region als beschäftigt markiert ist, während eine Antwort streamt.
  • Jede Icon-Aktion hat ein Label und einen Tooltip. Daumen nutzen aria-pressed, Copy und Feedback bestätigen über eine höfliche Live-Region, und Fehler nutzen role=alert.
  • Checkpoints sind eine Navigationsmarke aus Buttons namens „Nachricht 2: …“, und der aktuelle hat aria-current.
  • Versionspfeile sind eine Gruppe namens „Version 1 von 2“.
  • Der Editor fokussiert sich selbst mit dem Cursor am Ende und kehrt bei Cancel zur Nachricht zurück. Enter wartet, solange eine Eingabemethode komponiert.
  • Fokusringe werden vom Scroller nie abgeschnitten, und Aktionen bleiben auf Touchscreens sichtbar.

Gebaut mit

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

Code

2 Dateien, hinzugefügt zu components/blocks/chat-thread.