HextaAI

Una app de chat de IA completa construida con todos los bloques de IA de HextaUI. Chats en una barra lateral, razonamiento con fuentes, llamadas a herramientas con diffs y aprobaciones, un plan que revisas antes de que el agente se ejecute, Markdown y código en streaming y modo de voz silencioso, todo impulsado por las message parts de AI SDK.

HextaAI es una app de chat de IA completa hecha con todos los bloques de IA de HextaUI. Chat Sidebar contiene el historial, Chat Thread la conversación, Prompt Input es el compositor y cada respuesta se dibuja a partir de sus partes: Thinking para el razonamiento, Tool Calls para lo que hizo el agente, Agent Todos para su plan, Code Block para diffs y terminales, y Streaming Markdown para la respuesta. Artifact abre en un panel junto al chat las páginas web, documentos y archivos que crea el modelo, con versiones, y los cambios pequeños se escriben en el archivo abierto en lugar de reescribirlo. Voice Mode se abre desde el compositor y vuelca la conversación en el chat cuando cuelgas.

El núcleo es HextaMessageParts. Toma las mismas partes de mensaje que devuelve useChat de AI SDK, agrupa razonamientos y llamadas a herramientas contiguos, mantiene un plan en vivo arriba de la respuesta y renderiza el texto con citas enlazadas a las fuentes que encontró el modelo. Las lecturas, ediciones, escrituras, búsquedas, peticiones web y comandos de shell tienen cada uno una vista adecuada, y puedes añadir renderizadores para tus propias herramientas.

La demo se ejecuta sobre ScriptedChats, un pequeño motor que transmite partes realistas sin conexión. Prueba una sugerencia: crea una página de precios y mírala abrirse junto al chat, investiga una pregunta y mira cómo llegan las fuentes, pide una función y revisa el plan antes de que se ejecute, aprueba una ejecución de pruebas que falla y se corrige, o pide una explicación con código y tablas. Detén, regenera, edita y cambia de chat mientras las respuestas siguen llegando en segundo plano, y luego abre el modo de voz y habla.

  1. Añade el registro Pro a components.json

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

    Crea un token en tu página de cuenta y colócalo en .env.local como HEXTAUI_PRO_TOKEN.

  3. Añade el bloque

    pnpm dlx shadcn@latest add @hextaui-pro/hexta-ai

Con AI SDK

Pasa los mensajes de useChat directamente a HextaMessageParts. Las aprobaciones de herramientas y las revisiones de plan se responden mediante addToolApprovalResponse, y un plan editado vuelve al modelo como motivo de la aprobación.

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

Tus propias herramientas

Asigna un nombre de herramienta a un renderizador que devuelve su kind, subject y content. Las herramientas que no asignes recurren a vistas integradas o a una vista 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} />
}

Demos con guion

ScriptedChats transmite partes con la forma de AI SDK a partir de un guion, con ritmo, aprobaciones y detención reales. Úsalo para tours de producto, onboarding y demos sin conexión.

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

Anatomía

Las partes que compones, de fuera hacia dentro.

ParteDescripción
HextaMessagePartsRenderiza un mensaje del asistente a partir de sus partes, en orden: razonamiento, plan, llamadas a herramientas y Markdown.
defaultToolsRenderizadores integrados para read_file, list_files, grep, web_search, fetch, edit_file, write_file y bash.
ScriptedChats, useScriptedChatUn motor sin conexión que transmite partes con la forma de AI SDK a partir de guiones, con detener, regenerar, editar, aprobaciones y revisión de plan.
textOf, sourcesOf, toBlocksHelpers que leen texto y fuentes de un mensaje y agrupan sus partes como lo hace el renderizador.
HextaAIDemoLa app completa: Chat Sidebar, Chat Thread, Prompt Input y Voice Mode conectados a ScriptedChats.
PropTipoPredeterminado
partsLas partes del mensaje: texto, razonamiento, source-url, archivo y partes de herramienta, tal como las devuelve useChat.
MessagePart[]–
streamingSi este mensaje aún está llegando. El último bloque de razonamiento o texto se anima y las herramientas en ejecución siguen activas.
booleanfalse
stoppedMarca las herramientas en ejecución como canceladas y oculta una revisión de plan pendiente.
booleanfalse
toolsRenderizadores por nombre de herramienta. Cada uno devuelve kind, subject, title, meta, content, exitCode o error, y sustituye la vista integrada.
Record<string, ToolRenderer>{}
onApprovalSe llama cuando alguien permite o deniega una llamada a herramienta que espera aprobación.
(toolCallId, { approved, reason?, always? }) => void–
onPlanApproveSe llama con el plan editado cuando alguien lo ejecuta desde la revisión.
(toolCallId, todos) => void–
onPlanCancelSe llama cuando se cancela la revisión del plan.
(toolCallId) => void–
reasoningDurationsSegundos dedicados a cada bloque de razonamiento, para que «Thought for» siga correcto tras una recarga.
number[]–
PropTipoPredeterminado
new ScriptedChats(respond)Elige el guion que responde a cada mensaje.
(prompt, files) => Script–
send(chatId, text, files?, options?)Añade un mensaje de usuario y transmite la respuesta. options: model, thinking e instant, que omite toda espera.
Promise<void>–
stop, regenerate, edit, append, removeDetén la respuesta, repite la última, reenvía desde un mensaje editado, añade mensajes terminados o elimina un chat.
methods–
decide(toolCallId, decision)Responde a una aprobación o revisión de plan pendiente. Pasa todos para ejecutar un plan editado.
void–
useScriptedChat(store, chatId)Suscribe un componente a un chat.
{ messages, status, pinned }–
PropTipoPredeterminado
think(text)Transmite una parte de razonamiento. Se omite cuando thinking está desactivado.
Promise<void>–
say(markdown)Transmite una parte de texto en fragmentos irregulares, como un modelo real.
Promise<void>–
tool(name, input, { stream? })Añade una llamada a herramienta, con la opción de transmitir un campo de entrada. El handle puede aprobarla, hacerla avanzar, terminarla o hacerla fallar.
Promise<ToolHandle>–
plan(todos)Muestra un plan para revisión y espera. Devuelve el plan editado, o null si se cancela.
Promise<Todo[] | null>–
todos(todos), source(page), pause(ms)Actualiza el plan, cita una fuente o espera.
methods–
KeyAcción
⌘KBusca en tus chats.
⌘⇧OInicia un chat nuevo.
⌘BColapsa la barra lateral a iconos, o la abre a pantalla completa en móviles.
EnterEnvía el mensaje. Shift+Enter añade una línea.
⌘↵Ejecuta el plan mientras se revisa, o permite una llamada a herramienta que está en espera.
⌘⌫Deniega la llamada a herramienta que está en espera.
EscDetiene la respuesta, cierra un menú o termina el modo de voz.
  • Cada parte conserva la accesibilidad del bloque que la dibuja: el hilo anuncia las respuestas nuevas de forma polite, las llamadas a herramientas y los planes exponen su estado en texto, las citas nombran su fuente y las aprobaciones son botones reales con atajos de teclado.
  • Nada importante se muestra solo con movimiento o color. Los spinners tienen etiquetas como «Replying», los chats terminados en segundo plano reciben una etiqueta «Unread reply», y los diffs marcan las líneas añadidas y eliminadas con signos además de color.
  • Las animaciones de streaming, shader y hoja respetan el movimiento reducido. El modo de voz funciona sin leer las respuestas en voz alta, muestra subtítulos y reproduce una conversación de ejemplo cuando no hay micrófono.

Construido con

Los componentes gratuitos de HextaUI con los que está hecho HextaAI. Cada uno se instala por separado.

Código

5 archivos, añadidos a components/blocks/hexta-ai.