Chat Thread

Toute la conversation autour du champ de saisie. Votre question reste épinglée en haut pendant que la réponse arrive en streaming, des points de repère sur le côté permettent de sauter d’un message à l’autre, et chaque réponse peut être copiée, modifiée, relancée, notée et basculée entre versions. Les réponses affichent du Markdown avec blocs de code, tableaux et citations, et la réflexion, les appels d’outils et Prompt Input s’y insèrent directement.

Chat Thread est la conversation autour du champ de saisie : vos messages, les réponses du modèle et ce que vous pouvez en faire. Il démarre avec votre état vide et le champ de saisie centré, puis ancre ce champ en bas dès qu’il y a des messages, sans le remonter, si bien que le focus et le brouillon restent en place.

Le message que vous venez d’envoyer remonte en haut pendant que sa réponse arrive en dessous, et la vue suit le nouveau texte jusqu’à ce que vous défiliez ailleurs. Un rail de points de repère à droite permet de sauter d’un de vos messages à l’autre, avec un aperçu au survol. L’historique plus ancien se charge par le haut sans que la vue bouge.

Survolez un message pour le copier, le modifier ou réessayer. Les modifications et nouveaux essais deviennent des versions entre lesquelles vous pouvez basculer, et rien n’est perdu. Les réponses en échec proposent Try again, les réponses interrompues le disent, et le pouce levé ou baissé se donne en un clic. Dans chaque réponse, Reasoning, ToolCalls et Markdown des autres blocks s’insèrent directement.

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

Avec AI SDK

Associez les messages de useChat au fil. Les parts de raisonnement vont à Reasoning, les parts d’outil à ToolCalls et le texte à Markdown. Épinglez le dernier message de l’utilisateur, utilisez regenerate pour Try again, et renvoyez une modification à partir de ce point.

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

Anatomie

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

PartieDescription
ChatThreadLe cadre : scroller, points de repère, état vide et emplacement du champ de saisie.
ChatUserMessageVotre message : pièces jointes, bulle, heure, versions, copie et modification.
ChatAssistantMessageUne réponse. Placez-y Reasoning, ToolCalls et Markdown ; elle ajoute les actions et les états d’erreur et d’arrêt.
ChatDividerUne ligne libellée entre des groupes de messages.
ChatAttachments, ChatAction, ChatCopy, ChatBranchSwitch, ChatEditorLes éléments utilisés par les messages, exportés pour des mises en page personnalisées.
PropTypePar défaut
childrenMessages et séparateurs, dans l’ordre.
ReactNode–
composerGénéralement Prompt Input. Centré quand c’est vide, ancré en bas sinon.
ReactNode–
emptyAffiché au-dessus du champ de saisie quand il n’y a aucun message.
ReactNode–
busyMarque la conversation comme occupée pour les technologies d’assistance pendant qu’une réponse arrive en streaming.
booleanfalse
hasOlderAffiche « Load earlier messages » en haut.
booleanfalse
loadingOlderAffiche un spinner sur ce bouton. La position de défilement est restaurée quand la valeur repasse à false.
booleanfalse
onLoadOlderAjoutez ici les messages plus anciens en tête.
() => void–
PropTypePar défaut
idId stable. Gardez-le identique d’une version à l’autre pour que la vue ne saute pas.
string–
textLe message. Également utilisé pour l’aperçu du point de repère.
string–
pinFait défiler ce message jusqu’en haut quand il apparaît. À définir sur le message qui vient d’être envoyé.
booleanfalse
files{ id, name, size?, type?, url? }. Les images avec une url s’affichent en miniatures.
ChatFile[]–
timeAffiché au survol, dans la locale du lecteur.
Date–
branch{ index, count, onChange } pour les flèches de version.
ChatBranch–
onEditAffiche Edit. Appelé avec le nouveau texte lorsqu’il a changé.
(text: string) => void–
attachmentsContenu personnalisé au-dessus de la bulle.
ReactNode–
PropTypePar défaut
idId stable.
string–
childrenReasoning, ToolCalls, Markdown ou tout autre contenu.
ReactNode–
textTexte brut pour Copy.
string–
streamingMasque les actions jusqu’à la fin de la réponse.
booleanfalse
lastGarde les actions visibles au lieu de les afficher au survol.
booleanfalse
metaCôté droit de la rangée d’actions, comme le modèle et l’effort.
ReactNode–
branchFlèches de version pour les nouveaux essais.
ChatBranch–
feedbackLa notation actuelle.
"up" | "down" | nullnull
onFeedbackAffiche les pouces. Appelé avec null quand une notation est annulée.
(feedback) => void–
onRetryAffiche Try again.
() => void–
errorAffiche le message avec Try again à la place des actions.
string | null–
stoppedMarque une réponse interrompue prématurément.
booleanfalse
PropTypePar défaut
childrenLe libellé, comme Today ou « Switched to Nova 3 Max ».
ReactNode–
ToucheAction
TabParcourt les actions des messages. Elles apparaissent au focus, pas seulement au survol.
EnterEnvoie un message modifié.
ShiftEnterAjoute une ligne pendant la modification.
EscAnnule une modification.
↑↓PgUpPgDnFait défiler la conversation quand elle a le focus.
  • La conversation est une région libellée contenant un log, donc les nouveaux messages sont annoncés à leur arrivée et la région est marquée comme occupée pendant qu’une réponse arrive en streaming.
  • Chaque action d’icône a un libellé et une infobulle. Les pouces utilisent aria-pressed, Copy et le retour de notation confirment via une région live polie, et les erreurs utilisent role=alert.
  • Les points de repère forment une zone de navigation de boutons nommés « Message 2: … », et le point courant a aria-current.
  • Les flèches de version forment un groupe nommé « Version 1 of 2 ».
  • L’éditeur prend lui-même le focus avec le curseur à la fin et revient au message sur Cancel. Enter attend tant qu’une méthode de saisie est en cours de composition.
  • Les anneaux de focus ne sont jamais coupés par le scroller, et les actions restent visibles sur les écrans tactiles.

Construit avec

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

Code

2 fichiers, ajoutés à components/blocks/chat-thread.