HextaAI

Um app de chat de IA completo, construído com todos os blocos de IA do HextaUI. Conversas em uma barra lateral, raciocínio com fontes, chamadas de ferramentas com diffs e aprovações, um plano que você revisa antes de o agente executar, Markdown e código em streaming e modo de voz silencioso, tudo movido por partes de mensagem do AI SDK.

O HextaAI é um app de chat de IA completo feito com todos os blocos de IA do HextaUI. O Chat Sidebar guarda o histórico, o Chat Thread guarda a conversa, o Prompt Input é o composer, e cada resposta é desenhada a partir de suas partes: Thinking para o raciocínio, Tool Calls para o que o agente fez, Agent Todos para o plano, Code Block para diffs e terminais e Streaming Markdown para a resposta. O Artifact abre páginas web, documentos e arquivos que o modelo cria em um painel ao lado do chat, com versões, e pequenas mudanças são digitadas no arquivo aberto em vez de reescrevê-lo. O Voice Mode abre a partir do composer e joga a conversa no chat quando você desliga.

O núcleo é o HextaMessageParts. Ele recebe as mesmas partes de mensagem que o useChat do AI SDK retorna, agrupa raciocínios e chamadas de ferramenta vizinhos, mantém um plano ativo no topo da resposta e renderiza o texto com citações ligadas às fontes que o modelo encontrou. Leituras, edições, gravações, buscas, requisições web e comandos de shell ganham cada um uma visão adequada, e você pode adicionar renderizadores para as suas próprias ferramentas.

A demo roda no ScriptedChats, um pequeno motor que transmite partes realistas offline. Experimente uma sugestão: crie uma página de preços e veja-a abrir ao lado do chat, pesquise uma pergunta e veja as fontes chegarem, peça um recurso e revise o plano antes de ele rodar, aprove uma execução de testes que falha e é corrigida, ou peça uma explicação com código e tabelas. Pare, regenere, edite e troque de chat enquanto as respostas continuam chegando em segundo plano e, depois, abra o modo de voz e converse.

  1. Adicione o registro Pro ao components.json

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

    Crie um token na sua página de conta e coloque-o em .env.local como HEXTAUI_PRO_TOKEN.

  3. Adicione o bloco

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

Com o AI SDK

Passe as mensagens do useChat direto para o HextaMessageParts. As aprovações de ferramenta e as revisões de plano respondem por addToolApprovalResponse, e um plano editado volta ao modelo como o motivo da aprovação.

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

Suas próprias ferramentas

Mapeie o nome de uma ferramenta para um renderizador que retorna seu kind, subject e content. As ferramentas que você não mapear usam visões nativas ou uma visão 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 com script

O ScriptedChats transmite partes no formato do AI SDK a partir de um script, com ritmo real, aprovações e parar. Use para tours de produto, onboarding e demos offline.

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

Anatomia

As partes que você compõe, de fora para dentro.

ParteDescrição
HextaMessagePartsRenderiza uma mensagem do assistente a partir de suas partes, em ordem: raciocínio, plano, chamadas de ferramenta e Markdown.
defaultToolsRenderizadores nativos para read_file, list_files, grep, web_search, fetch, edit_file, write_file e bash.
ScriptedChats, useScriptedChatUm motor offline que transmite partes no formato do AI SDK a partir de scripts, com parar, regenerar, editar, aprovações e revisão do plano.
textOf, sourcesOf, toBlocksHelpers que leem texto e fontes de uma mensagem e agrupam suas partes da forma como o renderizador faz.
HextaAIDemoO app completo: Chat Sidebar, Chat Thread, Prompt Input e Voice Mode ligados ao ScriptedChats.
PropTipoPadrão
partsAs partes da mensagem: partes de texto, reasoning, source-url, file e ferramenta, como o useChat as retorna.
MessagePart[]–
streamingSe esta mensagem ainda está chegando. O último bloco de raciocínio ou texto é animado, e as ferramentas em execução permanecem ativas.
booleanfalse
stoppedMarca as ferramentas em execução como canceladas e oculta uma revisão de plano pendente.
booleanfalse
toolsRenderizadores por nome de ferramenta. Cada um retorna kind, subject, title, meta, content, exitCode ou error, e substitui a visão nativa.
Record<string, ToolRenderer>{}
onApprovalChamado quando alguém permite ou nega uma chamada de ferramenta que aguarda aprovação.
(toolCallId, { approved, reason?, always? }) => void–
onPlanApproveChamado com o plano editado quando alguém o executa a partir da revisão.
(toolCallId, todos) => void–
onPlanCancelChamado quando a revisão do plano é cancelada.
(toolCallId) => void–
reasoningDurationsSegundos gastos em cada bloco de raciocínio, para que "Thought for" continue correto após um recarregamento.
number[]–
PropTipoPadrão
new ScriptedChats(respond)Escolhe o script que responde a cada mensagem.
(prompt, files) => Script–
send(chatId, text, files?, options?)Adiciona uma mensagem do usuário e transmite a resposta. options: model, thinking e instant, que dispensa toda espera.
Promise<void>–
stop, regenerate, edit, append, removeInterrompe a resposta, refaz a última, reenvia a partir de uma mensagem editada, adiciona mensagens concluídas ou descarta um chat.
methods–
decide(toolCallId, decision)Responde a uma aprovação ou revisão de plano pendente. Passe todos para executar um plano editado.
void–
useScriptedChat(store, chatId)Inscreve um componente em um chat.
{ messages, status, pinned }–
PropTipoPadrão
think(text)Transmite uma parte de raciocínio. Ignorada quando o thinking está desligado.
Promise<void>–
say(markdown)Transmite uma parte de texto em pedaços irregulares, como um modelo real.
Promise<void>–
tool(name, input, { stream? })Adiciona uma chamada de ferramenta, com um campo de entrada em streaming opcional. O handle pode aprová-la, avançá-la, concluí-la ou fazê-la falhar.
Promise<ToolHandle>–
plan(todos)Mostra um plano para revisão e aguarda. Retorna o plano editado, ou null se cancelado.
Promise<Todo[] | null>–
todos(todos), source(page), pause(ms)Atualizar o plano, citar uma fonte ou aguardar.
methods–
TeclaAção
⌘KPesquisa nos seus chats.
⌘⇧OInicia um novo chat.
⌘BRecolhe a barra lateral para ícones, ou a abre em tela cheia no celular.
EnterEnvia a mensagem. Shift+Enter adiciona uma linha.
⌘↵Executa o plano enquanto ele está em revisão, ou permite uma chamada de ferramenta que está aguardando.
⌘⌫Nega a chamada de ferramenta que está aguardando.
EscInterrompe a resposta, fecha um menu ou encerra o modo de voz.
  • Cada parte mantém a acessibilidade do bloco que a desenha: a thread anuncia novas respostas de forma polite, as chamadas de ferramenta e os planos expõem seu estado em texto, as citações nomeiam sua fonte e as aprovações são botões de verdade com atalhos de teclado.
  • Nada importante é mostrado apenas por movimento ou cor. Os spinners têm rótulos como "Respondendo", os chats concluídos em segundo plano recebem o rótulo "Resposta não lida", e os diffs marcam as linhas adicionadas e removidas com sinais além da cor.
  • As animações de streaming, shader e sheet respeitam o movimento reduzido. O modo de voz funciona sem ler as respostas em voz alta, mostra legendas e reproduz uma conversa de exemplo quando não há microfone.

Construído com

Os componentes gratuitos do HextaUI de que HextaAI é feito. Cada um é instalado separadamente.

Código

5 arquivos, adicionados a components/blocks/hexta-ai.