HextaAI

Eine vollständige KI-Chat-App, gebaut aus allen KI-Blocks von HextaUI. Chats in einer Seitenleiste, Denkprozess mit Quellen, Tool-Aufrufe mit Diffs und Freigaben, ein Plan, den du prüfst, bevor der Agent läuft, gestreamtes Markdown und gestreamter Code sowie ein stiller Sprachmodus, alles gesteuert über AI-SDK-Message-Parts.

HextaAI ist eine vollständige KI-Chat-App aus allen KI-Blocks von HextaUI. Chat Sidebar hält den Verlauf, Chat Thread die Unterhaltung, Prompt Input ist der Composer, und jede Antwort wird aus ihren Parts gezeichnet: Thinking für das Denken, Tool Calls für das, was der Agent getan hat, Agent Todos für seinen Plan, Code Block für Diffs und Terminals und Streaming Markdown für die Antwort. Artifact öffnet Webseiten, Dokumente und Dateien, die das Modell erzeugt, in einem Panel neben dem Chat, mit Versionen, und kleine Änderungen werden in die geöffnete Datei getippt, statt sie neu zu schreiben. Voice Mode öffnet sich aus dem Composer und legt die Unterhaltung in den Chat, wenn du auflegst.

Der Kern ist HextaMessageParts. Es nimmt dieselben Message-Parts, die useChat des AI SDK zurückgibt, gruppiert benachbarte Reasoning- und Tool-Aufrufe, hält einen aktuellen Plan oben in der Antwort und rendert Text mit Zitaten, die auf die vom Modell gefundenen Quellen verlinken. Lesen, Bearbeiten, Schreiben, Suchen, Web-Abrufe und Shell-Befehle erhalten jeweils eine passende Ansicht, und du kannst Renderer für deine eigenen Tools hinzufügen.

Die Demo läuft auf ScriptedChats, einer kleinen Engine, die realistische Parts offline streamt. Probiere einen Vorschlag: eine Preisseite erstellen und zusehen, wie sie neben dem Chat aufgeht, eine Frage recherchieren und Quellen eintreffen sehen, ein Feature anfragen und den Plan vor der Ausführung prüfen, einen Testlauf freigeben, der fehlschlägt und repariert wird, oder eine Erklärung mit Code und Tabellen erbitten. Stoppe, regeneriere, bearbeite und wechsle Chats, während Antworten im Hintergrund weiterstreamen, und öffne dann den Sprachmodus und sprich.

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

Mit dem AI SDK

Übergib useChat-Nachrichten direkt an HextaMessageParts. Tool-Freigaben und Plan-Reviews antworten über addToolApprovalResponse, und ein bearbeiteter Plan geht als Freigabegrund zurück an das Modell.

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

Deine eigenen Tools

Ordne einem Tool-Namen einen Renderer zu, der seine Art, sein Subjekt und seinen Inhalt zurückgibt. Tools, die du nicht zuordnest, fallen auf eingebaute Ansichten oder eine JSON-Ansicht zurück.

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

Geskriptete Demos

ScriptedChats streamt Parts in der Form des AI SDK aus einem Skript, mit echtem Timing, Freigaben und Stopp. Nutze es für Produkttouren, Onboarding und Offline-Demos.

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

Aufbau

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

PartBeschreibung
HextaMessagePartsRendert eine Assistenten-Nachricht aus ihren Parts, der Reihe nach: Reasoning, Plan, Tool-Aufrufe und Markdown.
defaultToolsEingebaute Renderer für read_file, list_files, grep, web_search, fetch, edit_file, write_file und bash.
ScriptedChats, useScriptedChatEine Offline-Engine, die aus Skripten Parts in der Form des AI SDK streamt, mit Stopp, Regenerieren, Bearbeiten, Freigaben und Plan-Review.
textOf, sourcesOf, toBlocksHelfer, die Text und Quellen aus einer Nachricht lesen und ihre Parts so gruppieren, wie es der Renderer tut.
HextaAIDemoDie ganze App: Chat Sidebar, Chat Thread, Prompt Input und Voice Mode, verbunden mit ScriptedChats.
PropTypStandard
partsDie Parts der Nachricht: text-, reasoning-, source-url-, file- und Tool-Parts, wie useChat sie zurückgibt.
MessagePart[]–
streamingOb diese Nachricht noch eintrifft. Der letzte Reasoning- oder Textblock animiert, und laufende Tools bleiben live.
booleanfalse
stoppedMarkiert laufende Tools als abgebrochen und blendet ein ausstehendes Plan-Review aus.
booleanfalse
toolsRenderer nach Tool-Name. Jeder gibt kind, subject, title, meta, content, exitCode oder error zurück und überschreibt die eingebaute Ansicht.
Record<string, ToolRenderer>{}
onApprovalWird aufgerufen, wenn jemand einen Tool-Aufruf erlaubt oder verweigert, der auf Freigabe wartet.
(toolCallId, { approved, reason?, always? }) => void–
onPlanApproveWird mit dem bearbeiteten Plan aufgerufen, wenn jemand ihn aus dem Review ausführt.
(toolCallId, todos) => void–
onPlanCancelWird aufgerufen, wenn das Plan-Review abgebrochen wird.
(toolCallId) => void–
reasoningDurationsSekunden, die auf jeden Reasoning-Block entfallen, damit „Thought for“ nach einem Reload korrekt bleibt.
number[]–
PropTypStandard
new ScriptedChats(respond)Wählt das Skript, das jede Nachricht beantwortet.
(prompt, files) => Script–
send(chatId, text, files?, options?)Fügt eine Nutzernachricht hinzu und streamt die Antwort. Optionen: model, thinking und instant, das jedes Warten überspringt.
Promise<void>–
stop, regenerate, edit, append, removeStoppe die Antwort, wiederhole die letzte, sende ab einer bearbeiteten Nachricht neu, füge fertige Nachrichten hinzu oder verwirf einen Chat.
methods–
decide(toolCallId, decision)Beantwortet eine ausstehende Freigabe oder ein Plan-Review. Übergib todos, um einen bearbeiteten Plan auszuführen.
void–
useScriptedChat(store, chatId)Abonniert eine Komponente auf einen Chat.
{ messages, status, pinned }–
PropTypStandard
think(text)Streamt einen Reasoning-Part. Wird übersprungen, wenn thinking aus ist.
Promise<void>–
say(markdown)Streamt einen Text-Part in unregelmäßigen Häppchen, wie ein echtes Modell.
Promise<void>–
tool(name, input, { stream? })Fügt einen Tool-Aufruf hinzu, optional mit einem streamenden Eingabefeld. Das Handle kann ihn freigeben, fortschreiten lassen, beenden oder fehlschlagen lassen.
Promise<ToolHandle>–
plan(todos)Zeigt einen Plan zur Prüfung und wartet. Gibt den bearbeiteten Plan zurück, oder null bei Abbruch.
Promise<Todo[] | null>–
todos(todos), source(page), pause(ms)Aktualisiere den Plan, zitiere eine Quelle oder warte.
methods–
TasteAktion
⌘KDurchsucht deine Chats.
⌘⇧OStartet einen neuen Chat.
⌘BKlappt die Seitenleiste zu Icons ein oder öffnet sie auf Smartphones im Vollbild.
EnterSendet die Nachricht. Shift+Enter fügt eine Zeile hinzu.
⌘↵Führt den Plan aus, während er geprüft wird, oder erlaubt einen wartenden Tool-Aufruf.
⌘⌫Verweigert den wartenden Tool-Aufruf.
EscStoppt die Antwort, schließt ein Menü oder beendet den Sprachmodus.
  • Jeder Part behält die Barrierefreiheit des Blocks, der ihn zeichnet: Der Thread sagt neue Antworten höflich an, Tool-Aufrufe und Pläne machen ihren Zustand als Text zugänglich, Zitate nennen ihre Quelle, und Freigaben sind echte Buttons mit Tastenkürzeln.
  • Nichts Wichtiges wird nur durch Bewegung oder Farbe gezeigt. Spinner haben Labels wie „Antwortet“, im Hintergrund fertige Chats bekommen das Label „Ungelesene Antwort“, und Diffs markieren hinzugefügte und entfernte Zeilen mit Zeichen und nicht nur mit Farbe.
  • Streaming-, Shader- und Sheet-Animationen respektieren alle reduzierte Bewegung. Der Sprachmodus funktioniert, ohne Antworten laut vorzulesen, zeigt Untertitel und spielt eine Beispielunterhaltung ab, wenn kein Mikrofon da ist.

Gebaut mit

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

Code

5 Dateien, hinzugefügt zu components/blocks/hexta-ai.