Saltar al contenido principal

Composer

La caja de abajo de un chat, en una sola pieza. Prompt input, adjuntos, sugerencias, selector de modelo y el medidor de contexto, cableados a un solo onSend.

1'use client';
2
3import { useState } from 'react';
4import { Composer, type ComposerMessage } from '@/components/ui/composer';
5
6const MODELS = [
7 { id: 'gemini-2.5-flash', label: 'Gemini 2.5 Flash', description: 'Fast, cheap, most turns' },
8 { id: 'gemini-2.5-pro', label: 'Gemini 2.5 Pro', description: 'Slower, for hard questions' },
9 { id: 'claude-sonnet-5', label: 'Claude Sonnet 5', description: 'Long context, careful' },
10];
11
12const SUGGESTIONS = ['What is NachUI?', 'Install the CLI', 'Show me a chat example'];
13
14export function Default() {
15 const [model, setModel] = useState(MODELS[0]?.id ?? '');
16 const [sent, setSent] = useState<ComposerMessage | null>(null);
17
18 return (
19 <div className="flex w-full max-w-lg flex-col gap-3">
20 <Composer
21 onSend={setSent}
22 suggestions={SUGGESTIONS}
23 models={MODELS}
24 model={model}
25 onModelChange={setModel}
26 context={{ used: 12400, max: 128000 }}
27 placeholder="Ask about any component…"
28 />
29 {sent && (
30 <p className="text-muted-foreground text-xs">
31 Sent “{sent.text}” with {sent.files.length} file{sent.files.length === 1 ? '' : 's'}
32 </p>
33 )}
34 </div>
35 );
36}

Instalación

pnpm dlx nachui add composer

Anatomía

1import { Composer } from '@/components/ui/composer';
1<Composer
2 onSend={({ text, files }) => send(text, files)}
3 onStop={stop}
4 status={status}
5 suggestions={['¿Qué es NachUI?', 'Instalar la CLI']}
6 models={models}
7 model={model}
8 onModelChange={setModel}
9 context={{ used: 12400, max: 128000 }}
10/>
Sin children, el composer se arma solo: sugerencias y chips de adjuntos arriba, el textarea en el medio, adjuntar y modelo a la izquierda del footer, el medidor de contexto y el botón de enviar a la derecha. Enter envía, Shift+Enter corta la línea, Escape limpia.
Pasale children para reordenar las partes, sacar una o meter algo tuyo en el medio. Todas leen del mismo contexto, así que el orden es tuyo y el cableado se mantiene.
1<Composer onSend={handleSend}>
2 <Composer.Attachments />
3 <Composer.Input />
4 <Composer.Footer>
5 <Composer.Send />
6 </Composer.Footer>
7</Composer>

Composición

Un hybrid es composición visible. Abrí composer.tsx y estos son los elements que lo forman, en este orden:
Composer
├── PromptInput
│ ├── Composer.Suggestions → Suggestion.Group + Suggestion
│ ├── Composer.Attachments → Attachments (inline)
│ ├── Composer.Input → PromptInput.Textarea
│ └── Composer.Footer
│ ├── PromptInput.AddAttachments
│ ├── DropdownMenu (selector de modelo)
│ ├── Context (medidor de tokens)
│ └── Composer.Send → PromptInput.Submit o un botón de stop

Sugerencias

suggestions muestra una fila de chips arriba del input mientras está vacío. Un click envía la sugerencia como mensaje. Con sendOnSuggestion={false} la sugerencia rellena el input y quien lee puede editarla antes de mandar.

Modelos

models agrega un selector al footer. Es un DropdownMenu común que muestra el label actual; elegir otra entrada llama a onModelChange con su id. El composer nunca guarda el modelo, lo guardás vos.

Contexto

context renderiza el medidor Context al lado del botón de enviar, con used y max tokens. Pasá el mouse o el foco por encima para ver el encabezado con el detalle.

Streaming

status sigue los estados de PromptInput. Mientras está en streaming, el botón de enviar se convierte en stop y llama a onStop, y onSend se ignora hasta que termina la respuesta. submitted muestra el spinner y también bloquea el envío.

Ready. The send button turns into stop for three seconds after you send.

1'use client';
2
3import { useEffect, useRef, useState } from 'react';
4import { Composer, type ComposerStatus } from '@/components/ui/composer';
5
6export function Streaming() {
7 const [status, setStatus] = useState<ComposerStatus>('ready');
8 const [last, setLast] = useState<string | null>(null);
9 const timer = useRef<ReturnType<typeof setTimeout> | null>(null);
10
11 useEffect(() => {
12 return () => {
13 if (timer.current) clearTimeout(timer.current);
14 };
15 }, []);
16
17 const stop = () => {
18 if (timer.current) clearTimeout(timer.current);
19 setStatus('ready');
20 };
21
22 return (
23 <div className="flex w-full max-w-lg flex-col gap-3">
24 <Composer
25 status={status}
26 onSend={(message) => {
27 setLast(message.text);
28 setStatus('streaming');
29 timer.current = setTimeout(() => setStatus('ready'), 3000);
30 }}
31 onStop={stop}
32 placeholder="Send something and watch the button…"
33 />
34 <p className="text-muted-foreground text-xs">
35 {status === 'streaming'
36 ? `Streaming an answer to “${last}”. Stop cuts it short.`
37 : 'Ready. The send button turns into stop for three seconds after you send.'}
38 </p>
39 </div>
40 );
41}

Archivos

El botón de adjuntar abre el diálogo de archivos y soltar archivos sobre el composer también funciona. Se muestran como chips en línea con botón para quitar, y onSend los recibe como un File[] plano. Limitalos con maxFiles y accept.

Referencia de API

Composer

PropTypeDefaultDescription
onSend(message: { text: string; files: File[] }) => void-Se llama con Enter, el botón de enviar o una sugerencia
onStop() => void-Lo llama el botón de stop durante el streaming
status'ready' | 'submitted' | 'streaming' | 'error''ready'Define el botón de enviar y bloquea el envío
placeholderstring'Ask anything…'Placeholder del textarea
disabledbooleanfalseDeshabilita todos los controles
maxFilesnumber-Tope de archivos adjuntos
acceptstring-Tipos de archivo aceptados, como en un input
suggestionsstring[][]Chips visibles mientras el input está vacío
sendOnSuggestionbooleantrueEnviar al click, o rellenar el input
models{ id: string; label: string; description?: string }[][]Entradas del selector de modelo
modelstring-Id del modelo actual
onModelChange(model: string) => void-Se llama con el id elegido
context{ used: number; max: number }-Renderiza el medidor de tokens
labelsPartial<{ send; stop; attach; model }>InglésNombres accesibles de los botones
valuestring-Texto controlado
defaultValuestring-Texto inicial cuando no es controlado
onValueChange(value: string) => void-Se llama cuando cambia el texto
classNamestring-Clases CSS adicionales

Composer.Suggestions

PropTypeDefaultDescription
classNamestring-Clases CSS adicionales

Composer.Attachments

PropTypeDefaultDescription
classNamestring-Clases CSS adicionales

Composer.Input

PropTypeDefaultDescription
classNamestring-Clases CSS adicionales
...--Cualquier prop de textarea salvo value y onChange

Composer.Footer

PropTypeDefaultDescription
childrenReactNode<Composer.Send />Lo que va en el extremo derecho
classNamestring-Clases CSS adicionales

Composer.Send

PropTypeDefaultDescription
classNamestring-Clases CSS adicionales
¿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