Saltar al contenido principal

Chat

Un chat entero en un componente. El hilo, cada respuesta con su código, sus tool calls y sus fuentes, la fila de acciones debajo y el composer al pie, todo cableado.

You
How do I show a tool call inside a message?
Assistant

Use Tool. It folds the call into a card with its status, and opens to show the input and the output.

<Tool status="complete">
  <Tool.Header name="get_projects" />
  <Tool.Content>
    <Tool.Input value={{ locale: 'en' }} />
    <Tool.Output value={projects} />
  </Tool.Content>
</Tool>
You
Show me one with real data.
Assistant

Two projects came back. The card above is the call itself.

1'use client';
2
3import { Chat, type ChatMessage } from '@/components/ui/chat';
4
5const THREAD: ChatMessage[] = [
6 {
7 id: 'u1',
8 role: 'user',
9 parts: [{ type: 'text', text: 'How do I show a tool call inside a message?' }],
10 },
11 {
12 id: 'a1',
13 role: 'assistant',
14 parts: [
15 { type: 'reasoning', text: 'The question is about rendering, not about running the tool.' },
16 {
17 type: 'text',
18 text: 'Use Tool. It folds the call into a card with its status, and opens to show the input and the output.',
19 },
20 {
21 type: 'code',
22 language: 'tsx',
23 code: `<Tool status="complete">
24 <Tool.Header name="get_projects" />
25 <Tool.Content>
26 <Tool.Input value={{ locale: 'en' }} />
27 <Tool.Output value={projects} />
28 </Tool.Content>
29</Tool>`,
30 },
31 ],
32 },
33 { id: 'u2', role: 'user', parts: [{ type: 'text', text: 'Show me one with real data.' }] },
34 {
35 id: 'a2',
36 role: 'assistant',
37 parts: [
38 {
39 type: 'tool',
40 name: 'get_projects',
41 status: 'complete',
42 input: { locale: 'en', limit: 2 },
43 output: [
44 { slug: 'nachui', title: 'NachUI' },
45 { slug: 'portfolio', title: 'ignaciofigueroa.dev' },
46 ],
47 },
48 { type: 'text', text: 'Two projects came back. The card above is the call itself.' },
49 {
50 type: 'sources',
51 items: [
52 { href: 'https://nachui.tech/docs/elements/ai/tool', title: 'Tool · NachUI' },
53 {
54 href: 'https://ai-sdk.dev/docs/ai-sdk-ui/chatbot-tool-usage',
55 title: 'Chatbot tool usage',
56 },
57 ],
58 },
59 ],
60 },
61];
62
63export function Default() {
64 return (
65 <div className="border-border h-[28rem] w-full max-w-2xl overflow-hidden rounded-xl border">
66 <Chat
67 messages={THREAD}
68 onSend={() => {}}
69 onRetry={() => {}}
70 onFeedback={() => {}}
71 suggestions={['What is NachUI?', 'Show me the composer']}
72 />
73 </div>
74 );
75}

Instalación

pnpm dlx nachui add chat

Anatomía

1import { Chat } from '@/components/ui/chat';
1<Chat
2 messages={messages}
3 status={status}
4 onSend={({ text, files }) => send(text, files)}
5 onStop={stop}
6 onRetry={(id) => regenerate(id)}
7 onFeedback={(id, vote) => rate(id, vote)}
8 suggestions={['¿Qué es NachUI?', 'Mostrame el composer']}
9/>
Le das los mensajes y un estado, y dibuja el resto: el hilo pegado al mensaje más nuevo, un skeleton mientras la respuesta viene en camino, un cursor mientras llega, y el composer que envía. Ponelo dentro de cualquier contenedor con alto fijo y lo llena.

Composición

Abrís chat.tsx y encontrás los elements en el orden en que aparecen en pantalla. El chat no agrega lógica propia más allá de entregarle cada parte de un mensaje al element que sabe dibujarla.
Chat
├── Chat.Thread Conversation
│ ├── Conversation.Empty + Chat.Suggestions Suggestion
│ ├── Chat.Message Message
│ │ ├── user Bubble, Attachments
│ │ └── assistant Response
│ │ ├── reasoning Reasoning
│ │ ├── text renderMarkdown
│ │ ├── code CodeBlock
│ │ ├── tool Tool
│ │ └── sources Sources
│ │ └── footer Actions
│ ├── Chat.Pending Response.Skeleton
│ └── Conversation.ScrollButton
└── Chat.Composer PromptInput
Pasale children a Chat para armar el layout vos; cada parte lee los mensajes y los handlers del contexto.
1<Chat messages={messages} onSend={send}>
2 <header className="border-b p-3">Soporte</header>
3 <Chat.Thread />
4 <Chat.Composer accept="image/*" />
5</Chat>

Forma del mensaje

Un mensaje es un rol y una lista de partes. Las partes se renderizan en orden, así que una respuesta puede pensar, contestar, mostrar código, llamar una herramienta y citar sus fuentes, en esa secuencia.
1type ChatMessage = {
2 id: string;
3 role: 'user' | 'assistant';
4 parts: ChatPart[];
5};
6
7type ChatPart =
8 | { type: 'text'; text: string }
9 | { type: 'reasoning'; text: string }
10 | { type: 'code'; code: string; language?: string }
11 | { type: 'tool'; name: string; status: ToolStatus; input?: unknown; output?: unknown }
12 | { type: 'sources'; items: { href: string; title: string }[] }
13 | { type: 'file'; name: string; size?: number; url?: string; mediaType?: string };
Las partes de texto pasan por renderMarkdown. El default parte párrafos en las líneas en blanco; pasale react-markdown o tu propio renderer para tener el resto.
1<Chat renderMarkdown={(text) => <Markdown>{text}</Markdown>} … />

Adaptador

El chat no sabe qué SDK corre tu modelo. toChatMessages mapea el array de UIMessage que mantiene el Vercel AI SDK a la forma de arriba, sin importar el SDK: las partes de texto, razonamiento y archivo pasan directo, las partes tool-* se vuelven partes tool con su state plegado en un status, y las partes source-url se juntan en una sola parte sources.
1'use client';
2
3import { useChat } from '@ai-sdk/react';
4import { Chat, toChatMessages } from '@/components/ui/chat';
5
6export function Soporte() {
7 const { messages, sendMessage, status, stop, regenerate } = useChat();
8
9 return (
10 <Chat
11 messages={toChatMessages(messages)}
12 status={status}
13 onSend={({ text, files }) => sendMessage({ text, files })}
14 onStop={stop}
15 onRetry={() => regenerate()}
16 />
17 );
18}
Cualquier otra cosa que produzca mensajes funciona igual: armás el array y lo pasás.

Streaming

status maneja los dos estados de espera. submitted muestra un skeleton debajo del último mensaje del usuario hasta que cae el primer token; streaming pone un cursor después de la última respuesta y convierte el botón de enviar en uno de parar.
Start a conversationAsk a question or pick one of the suggestions.
1'use client';
2
3import { useEffect, useRef, useState } from 'react';
4import { Chat, type ChatMessage, type ChatStatus } from '@/components/ui/chat';
5
6const REPLY =
7 'Every element in this thread is one you can install on its own. The chat only decides the order they sit in, and hands each one the part of the message it knows how to draw.';
8
9export function Streaming() {
10 const [messages, setMessages] = useState<ChatMessage[]>([]);
11 const [status, setStatus] = useState<ChatStatus>('ready');
12 const timer = useRef<ReturnType<typeof setInterval> | null>(null);
13
14 const stop = () => {
15 if (timer.current) clearInterval(timer.current);
16 timer.current = null;
17 setStatus('ready');
18 };
19
20 useEffect(() => stop, []);
21
22 const send = ({ text }: { text: string }) => {
23 const id = String(Date.now());
24 setMessages((previous) => [
25 ...previous,
26 { id: `u-${id}`, role: 'user', parts: [{ type: 'text', text }] },
27 ]);
28 setStatus('submitted');
29
30 const words = REPLY.split(' ');
31 let count = 0;
32
33 setTimeout(() => {
34 setStatus('streaming');
35 setMessages((previous) => [
36 ...previous,
37 { id: `a-${id}`, role: 'assistant', parts: [{ type: 'text', text: '' }] },
38 ]);
39 timer.current = setInterval(() => {
40 count += 1;
41 const text = words.slice(0, count).join(' ');
42 setMessages((previous) =>
43 previous.map((message) =>
44 message.id === `a-${id}` ? { ...message, parts: [{ type: 'text', text }] } : message,
45 ),
46 );
47 if (count >= words.length) stop();
48 }, 60);
49 }, 700);
50 };
51
52 return (
53 <div className="border-border h-[26rem] w-full max-w-2xl overflow-hidden rounded-xl border">
54 <Chat
55 messages={messages}
56 status={status}
57 onSend={send}
58 onStop={stop}
59 suggestions={['What makes this a hybrid?', 'Send anything to see it stream']}
60 />
61 </div>
62 );
63}

Referencia de API

Chat

PropTipoDefaultDescripción
messagesChatMessage[]-El hilo, del más viejo al más nuevo
status'ready' | 'submitted' | 'streaming' | 'error''ready'Maneja el skeleton, el cursor y el botón de parar
onSend(message: { text: string; files?: File[] }) => void-Se llama cuando el composer envía
onStop() => void-Lo llama el botón de parar mientras una respuesta está en vuelo
onRetry(messageId: string) => void-Agrega una acción de reintentar debajo de cada respuesta
onFeedback(messageId: string, vote: 'up' | 'down') => void-Agrega pulgar arriba y abajo debajo de cada respuesta
suggestionsstring[][]Chips que se muestran en el estado vacío
onSuggestion(text: string) => void-Se llama al elegir un chip; si falta, usa onSend
placeholderstring-Placeholder del composer, atajo de labels.placeholder
emptyTitlestring-Título del estado vacío, atajo de labels.emptyTitle
emptyDescriptionstring-Texto del estado vacío, atajo de labels.emptyDescription
labelsPartial<ChatLabels>-Cada texto visible, con defaults en inglés
renderMarkdown(text: string) => ReactNode-Renderiza las partes de texto; el default parte párrafos
childrenReactNode-Reemplaza el layout default de hilo y composer
classNamestring-Clases CSS adicionales

Chat.Thread

PropTipoDefaultDescripción
childrenReactNode-Reemplaza los mensajes renderizados dentro del hilo
classNamestring-Clases CSS adicionales

Chat.Message

PropTipoDefaultDescripción
messageChatMessage-El mensaje a dibujar
isLastbooleanfalseMarca la respuesta que hace streaming y abre su razonamiento
classNamestring-Clases CSS adicionales

Chat.Pending

Renderiza Response.Skeleton mientras status es submitted y el último mensaje es del usuario; nada en otro caso.
PropTipoDefaultDescripción
classNamestring-Clases CSS adicionales

Chat.Suggestions

PropTipoDefaultDescripción
classNamestring-Clases CSS adicionales

Chat.Composer

Acepta todas las props de PromptInput menos onSubmit, así que accept, maxFiles y maxFileSize pasan directo.
PropTipoDefaultDescripción
childrenReactNode-Reemplaza el textarea, las herramientas y el enviar
classNamestring-Clases CSS adicionales

toChatMessages

FirmaDescripción
toChatMessages(messages: UIMessageLike[]): ChatMessage[]Mapea mensajes del Vercel AI SDK a la forma del chat, sin importar el SDK
¿Encontraste algo que mejorar?

¿Notaste un error, tipografía o detalle faltante en esta página? Ayúdanos a mejorar la documentación abriendo un issue en GitHub.

Crear un Issue