HextaAI

Une application de chat IA complète construite avec tous les blocks IA de HextaUI. Des conversations dans une barre latérale, la réflexion avec sources, des appels d’outils avec diffs et approbations, un plan à relire avant l’exécution de l’agent, du Markdown et du code en streaming, et un mode vocal silencieux, le tout piloté par les message parts d’AI SDK.

HextaAI est une application de chat IA complète faite de tous les blocks IA de HextaUI. Chat Sidebar contient l’historique, Chat Thread la conversation, Prompt Input est le champ de saisie, et chaque réponse est dessinée à partir de ses parts : Thinking pour le raisonnement, Tool Calls pour ce que l’agent a fait, Agent Todos pour son plan, Code Block pour les diffs et terminaux, et Streaming Markdown pour la réponse. Artifact ouvre les pages web, documents et fichiers produits par le modèle dans un panneau à côté du chat, avec des versions, et les petits changements sont tapés dans le fichier ouvert au lieu de le réécrire. Voice Mode s’ouvre depuis le champ de saisie et dépose la conversation dans le chat quand vous raccrochez.

Le cœur est HextaMessageParts. Il prend les mêmes message parts que renvoie useChat d’AI SDK, regroupe les raisonnements et appels d’outils voisins, garde un seul plan actif en haut de la réponse et affiche le texte avec des citations liées aux sources trouvées par le modèle. Lectures, modifications, écritures, recherches, requêtes web et commandes shell ont chacune une vue adaptée, et vous pouvez ajouter des moteurs de rendu pour vos propres outils.

La démo repose sur ScriptedChats, un petit moteur qui diffuse des parts réalistes hors ligne. Essayez une suggestion : créer une page de tarifs et la regarder s’ouvrir à côté du chat, rechercher une question et voir arriver les sources, demander une fonctionnalité et relire le plan avant son exécution, approuver un lancement de tests qui échoue puis est corrigé, ou demander une explication avec du code et des tableaux. Arrêtez, régénérez, modifiez et changez de chat pendant que les réponses continuent d’arriver en arrière-plan, puis ouvrez le mode vocal et parlez.

  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/hexta-ai

Avec AI SDK

Passez directement les messages de useChat à HextaMessageParts. Les approbations d’outils et les relectures de plan répondent via addToolApprovalResponse, et un plan modifié retourne au modèle comme motif d’approbation.

"use client"

import { useChat } from "@ai-sdk/react"
import { lastAssistantMessageIsCompleteWithApprovalResponses } from "ai"

import { ChatAssistantMessage, ChatThread, ChatUserMessage } from "../chat-thread/chat-thread"
import { PromptInput, PromptInputActions, PromptInputBody, PromptInputSubmit, PromptInputTextarea } from "../prompt-input/prompt-input"
import { HextaMessageParts } from "@/components/blocks/hexta-ai/message-parts"
import { textOf, type HextaMessage, type ToolPart } from "@/components/blocks/hexta-ai/parts"

export function Assistant() {
  const { messages, status, sendMessage, stop, regenerate, addToolApprovalResponse } = useChat({
    sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithApprovalResponses,
  })
  const busy = status === "submitted" || status === "streaming"
  const chat = messages as HextaMessage[]

  const approvalId = (toolCallId: string) =>
    chat
      .flatMap((message) => message.parts)
      .find((part): part is ToolPart => "toolCallId" in part && part.toolCallId === toolCallId)
      ?.approval?.id

  return (
    <ChatThread
      busy={busy}
      composer={
        <PromptInput status={status} onStop={stop} onSubmit={({ text }) => sendMessage({ text })}>
          <PromptInputBody>
            <PromptInputTextarea />
            <PromptInputActions>
              <PromptInputSubmit />
            </PromptInputActions>
          </PromptInputBody>
        </PromptInput>
      }
    >
      {chat.map((message, index) => {
        const last = index === chat.length - 1
        if (message.role === "user") {
          return <ChatUserMessage key={message.id} id={message.id} text={textOf(message)} />
        }
        return (
          <ChatAssistantMessage
            key={message.id}
            id={message.id}
            text={textOf(message)}
            streaming={last && busy}
            last={last}
            onRetry={last ? () => regenerate() : undefined}
          >
            <HextaMessageParts
              parts={message.parts}
              streaming={last && busy}
              onApproval={(toolCallId, { approved, reason }) => {
                const id = approvalId(toolCallId)
                if (id) addToolApprovalResponse({ id, approved, reason })
              }}
              onPlanApprove={(toolCallId, todos) => {
                const id = approvalId(toolCallId)
                if (!id) return
                addToolApprovalResponse({
                  id,
                  approved: true,
                  reason: `Run this plan:\n${todos.map((todo, step) => `${step + 1}. ${todo.content}`).join("\n")}`,
                })
              }}
              onPlanCancel={(toolCallId) => {
                const id = approvalId(toolCallId)
                if (id) addToolApprovalResponse({ id, approved: false })
              }}
            />
          </ChatAssistantMessage>
        )
      })}
    </ChatThread>
  )
}

Vos propres outils

Associez un nom d’outil à un moteur de rendu qui renvoie son kind, son subject et son content. Les outils non associés utilisent les vues intégrées ou une vue JSON.

"use client"

import { CodeBlock } from "../code-block/code-block"
import { HextaMessageParts, type ToolRenderer } from "@/components/blocks/hexta-ai/message-parts"
import type { MessagePart } from "@/components/blocks/hexta-ai/parts"

const tools: Record<string, ToolRenderer> = {
  create_chart: (part) => {
    const input = part.input as { title?: string; spec?: string } | undefined
    return {
      kind: "write",
      subject: input?.title ?? "Chart",
      content: input?.spec ? <CodeBlock code={input.spec} language="json" filename="chart.json" /> : undefined,
    }
  },
}

export function Reply({ parts, streaming }: { parts: MessagePart[]; streaming: boolean }) {
  return <HextaMessageParts parts={parts} streaming={streaming} tools={tools} />
}

Démos scriptées

ScriptedChats diffuse en streaming des parts au format AI SDK à partir d’un script, avec un vrai rythme, des approbations et l’arrêt. Utilisez-le pour des visites de produit, de l’onboarding et des démos hors ligne.

"use client"

import * as React from "react"

import { HextaMessageParts } from "@/components/blocks/hexta-ai/message-parts"
import { textOf } from "@/components/blocks/hexta-ai/parts"
import { ScriptedChats, useScriptedChat, type Script } from "@/components/blocks/hexta-ai/scripted-chat"

const onboarding: Script = async (run) => {
  await run.think("**Picking a first step**\nShow them the one thing that makes the product click.")
  const search = await run.tool("web_search", { query: "best onboarding checklists" })
  await run.pause(600)
  search.finish({ results: 4 })
  await run.say("Start with **one task they can finish in a minute**, then show what it unlocked.")
}

export function ProductTour() {
  const [store] = React.useState(() => new ScriptedChats(() => onboarding))
  const { messages, status } = useScriptedChat(store, "tour")
  const busy = status === "submitted" || status === "streaming"

  return (
    <div className="flex flex-col gap-4">
      <button onClick={() => void store.send("tour", "How should onboarding start?")}>Play</button>
      {messages.map((message, index) =>
        message.role === "user" ? (
          <p key={message.id}>{textOf(message)}</p>
        ) : (
          <HextaMessageParts
            key={message.id}
            parts={message.parts}
            streaming={busy && index === messages.length - 1}
            reasoningDurations={message.metadata?.thoughts}
            onApproval={(id, approval) => store.decide(id, approval)}
          />
        )
      )}
    </div>
  )
}

Anatomie

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

PartieDescription
HextaMessagePartsAffiche un message de l’assistant à partir de ses parts, dans l’ordre : raisonnement, plan, appels d’outils et Markdown.
defaultToolsMoteurs de rendu intégrés pour read_file, list_files, grep, web_search, fetch, edit_file, write_file et bash.
ScriptedChats, useScriptedChatUn moteur hors ligne qui diffuse en streaming des parts au format AI SDK à partir de scripts, avec arrêt, régénération, modification, approbations et relecture du plan.
textOf, sourcesOf, toBlocksDes helpers qui lisent le texte et les sources d’un message et regroupent ses parts comme le fait le moteur de rendu.
HextaAIDemoL’application complète : Chat Sidebar, Chat Thread, Prompt Input et Voice Mode reliés à ScriptedChats.
PropTypePar défaut
partsLes parts du message : texte, raisonnement, source-url, fichier et outil, telles que les renvoie useChat.
MessagePart[]–
streamingIndique si ce message est encore en cours d’arrivée. Le dernier bloc de raisonnement ou de texte s’anime, et les outils en cours restent actifs.
booleanfalse
stoppedMarque les outils en cours comme annulés et masque une relecture de plan en attente.
booleanfalse
toolsMoteurs de rendu par nom d’outil. Chacun renvoie kind, subject, title, meta, content, exitCode ou error, et remplace la vue intégrée.
Record<string, ToolRenderer>{}
onApprovalAppelé quand quelqu’un autorise ou refuse un appel d’outil en attente d’approbation.
(toolCallId, { approved, reason?, always? }) => void–
onPlanApproveAppelé avec le plan modifié quand quelqu’un l’exécute depuis la relecture.
(toolCallId, todos) => void–
onPlanCancelAppelé quand la relecture du plan est annulée.
(toolCallId) => void–
reasoningDurationsSecondes passées sur chaque bloc de raisonnement, pour que « Thought for » reste exact après un rechargement.
number[]–
PropTypePar défaut
new ScriptedChats(respond)Choisit le script qui répond à chaque message.
(prompt, files) => Script–
send(chatId, text, files?, options?)Ajoute un message de l’utilisateur et diffuse la réponse en streaming. options : model, thinking et instant, qui supprime toute attente.
Promise<void>–
stop, regenerate, edit, append, removeArrêtez la réponse, refaites la dernière, renvoyez depuis un message modifié, ajoutez des messages terminés ou supprimez un chat.
methods–
decide(toolCallId, decision)Répond à une approbation ou une relecture de plan en attente. Passez todos pour exécuter un plan modifié.
void–
useScriptedChat(store, chatId)Abonne un composant à un chat.
{ messages, status, pinned }–
PropTypePar défaut
think(text)Diffuse une part de raisonnement en streaming. Ignoré quand thinking est désactivé.
Promise<void>–
say(markdown)Diffuse une part de texte en blocs irréguliers, comme un vrai modèle.
Promise<void>–
tool(name, input, { stream? })Ajoute un appel d’outil, avec éventuellement un champ d’entrée en streaming. Le handle peut l’approuver, le faire progresser, le terminer ou le faire échouer.
Promise<ToolHandle>–
plan(todos)Affiche un plan à relire et attend. Renvoie le plan modifié, ou null s’il est annulé.
Promise<Todo[] | null>–
todos(todos), source(page), pause(ms)Met à jour le plan, cite une source ou attend.
methods–
ToucheAction
⌘KRecherche dans vos chats.
⌘⇧ODémarre un nouveau chat.
⌘BRéduit la barre latérale en icônes, ou l’ouvre en plein écran sur mobile.
EnterEnvoie le message. Shift+Enter ajoute une ligne.
⌘↵Exécute le plan pendant sa relecture, ou autorise un appel d’outil en attente.
⌘⌫Refuse l’appel d’outil en attente.
EscArrête la réponse, ferme un menu ou termine le mode vocal.
  • Chaque part conserve l’accessibilité du block qui la dessine : le fil annonce poliment les nouvelles réponses, les appels d’outils et les plans exposent leur état en texte, les citations nomment leur source, et les approbations sont de vrais boutons avec raccourcis clavier.
  • Rien d’important n’est montré uniquement par le mouvement ou la couleur. Les spinners ont des libellés comme « Replying », les chats terminés en arrière-plan reçoivent un libellé « Unread reply », et les diffs marquent les lignes ajoutées et supprimées par des signes en plus de la couleur.
  • Les animations de streaming, de shader et de sheet respectent toutes la réduction des animations. Le mode vocal fonctionne sans lire les réponses à voix haute, affiche des sous-titres et joue une conversation d’exemple quand il n’y a pas de microphone.

Construit avec

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

Code

5 fichiers, ajoutés à components/blocks/hexta-ai.