# Chat

> Las piezas de una conversación con un asistente: cabecera, mensajes, sugerencias, el campo para escribir y sus estados. No sabe de modelos ni de streaming: eso es de la app.

```tsx
import { Chat, ChatActions, ChatDisclaimer, ChatEmpty, … } from "sebs7n-ui/chat"
```

## Ejemplos

### El asistente entero

El lanzador abre un panel de vidrio con la conversación. Apretá una sugerencia: mientras responde, el borde del panel se enciende y el canto del lanzador gira.

```tsx
import { AiLauncher } from "sebs7n-ui/ai-button"
import { Popover, PopoverContent, PopoverTrigger } from "sebs7n-ui/popover"
import { XIcon } from "lucide-react"
import { useState } from "react"

function Asistente() {
  const [abierto, setAbierto] = useState(false)
  const [respondiendo, setRespondiendo] = useState(false)
  return (
    <div className="flex h-24 w-full items-center justify-end pr-4">
      <Popover onOpenChange={setAbierto} open={abierto}>
        <PopoverTrigger render={<AiLauncher active={respondiendo} label={abierto ? "Cerrar asistente" : "Asistente"} />}>
          {abierto ? <XIcon /> : undefined}
        </PopoverTrigger>
        <PopoverContent
          align="end"
          aria-label="Asistente"
          // Material grueso: es de lo más grande que flota. `p-0` y `gap-0` porque el aire lo
          // ponen las piezas del chat, que llegan hasta el borde con sus líneas divisorias.
          className="h-[min(560px,var(--available-height))] w-[min(400px,var(--available-width))] gap-0 overflow-hidden rounded-panel p-0"
          side="top"
          sideOffset={12}
        >
          <Conversacion onBusyChange={setRespondiendo} onClose={() => setAbierto(false)} />
        </PopoverContent>
      </Popover>
    </div>
  )
}
```

### En una tarjeta

Las mismas piezas, fijas en la página. El `Chat` no dibuja superficie: acá la pone la `Card`.

```tsx
import { Card } from "sebs7n-ui/card"

function EnTarjeta() {
  return (
    <Card className="h-[480px] w-full max-w-md gap-0 overflow-hidden py-0">
      <Conversacion />
    </Card>
  )
}
```

### Estados

Una respuesta en curso: el borde encendido, «escribiendo» y el botón de detener. Y el error con su reintento.

```tsx
import { Card } from "sebs7n-ui/card"
import { Chat, ChatError, ChatFooter, ChatInput, ChatMessage, ChatMessages, ChatTyping } from "sebs7n-ui/chat"

function Estados() {
  return (
    <Card className="w-full max-w-md gap-0 overflow-hidden py-0">
      <Chat busy>
        <ChatMessages className="max-h-80">
          <ChatMessage from="user">¿Cuál fue el cliente que más compró?</ChatMessage>
          <ChatTyping />
          <ChatMessage from="user">¿Y el mes pasado?</ChatMessage>
          <ChatError onRetry={() => {}}>No pude responder ahora. Probá de nuevo en un rato.</ChatError>
        </ChatMessages>
        <ChatFooter>
          <ChatInput onStop={() => {}} />
        </ChatFooter>
      </Chat>
    </Card>
  )
}
```

## Props

### Chat

Hereda las props de `<div>`.

| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
| `busy` | `boolean` | `false` | Hay una respuesta en curso. Enciende el borde de la IA alrededor de la conversación, y le avisa a `ChatMessages` y a `ChatInput`, que ya no necesitan su propio `busy`. |
| `className` | `string` | — | Se fusiona con las clases del componente vía `cn()` (tailwind-merge): lo que pongas gana. |

### ChatActions

Hereda las props de `<div>`.

| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
| `className` | `string` | — | Se fusiona con las clases del componente vía `cn()` (tailwind-merge): lo que pongas gana. |

### ChatDisclaimer

Hereda las props de `<p>`.

| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
| `className` | `string` | — | Se fusiona con las clases del componente vía `cn()` (tailwind-merge): lo que pongas gana. |

### ChatEmpty

Hereda las props de `<div>`.

| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
| `className` | `string` | — | Se fusiona con las clases del componente vía `cn()` (tailwind-merge): lo que pongas gana. |

### ChatError

Hereda las props de `<div>`.

| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
| `labels` | `Partial<ChatLabels>` | — | Textos de la interfaz, para traducir o ajustar el tono. |
| `onRetry` | `() => void` | — | Con esto aparece el botón de reintentar. |
| `className` | `string` | — | Se fusiona con las clases del componente vía `cn()` (tailwind-merge): lo que pongas gana. |

### ChatFooter

Hereda las props de `<div>`.

| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
| `className` | `string` | — | Se fusiona con las clases del componente vía `cn()` (tailwind-merge): lo que pongas gana. |

### ChatHeader

Hereda las props de `<div>`.

| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
| `className` | `string` | — | Se fusiona con las clases del componente vía `cn()` (tailwind-merge): lo que pongas gana. |

### ChatInput

Hereda las props de `<form>`.

| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
| `busy` | `boolean` | — | Hay una respuesta en curso: el botón de enviar pasa a ser el de detener. Si no se pasa, toma el del `Chat`. |
| `defaultValue` | `string \| (readonly string[] & string)` | `""` | El valor inicial. Es la versión no controlada de `value`. |
| `disabled` | `boolean` | `false` | Apaga el campo y el botón: sin conexión, límite de mensajes alcanzado. |
| `enterKey` | `"auto" \| "send" \| "newline"` | `"auto"` | Qué hace Enter. `auto` (el default) envía con mouse o trackpad y hace un salto de línea en una pantalla táctil, donde el teclado en pantalla no tiene otra forma de bajar de renglón. |
| `inputRef` | `React.Ref<HTMLTextAreaElement>` | — | El `<textarea>`, para enfocarlo desde afuera: al abrir el panel, después de una respuesta. |
| `labels` | `Partial<ChatLabels>` | — | Textos de la interfaz, para traducir o ajustar el tono. |
| `maxLength` | `number` | — | El largo máximo del mensaje. |
| `onSend` | `(message: string) => void` | — | Manda el mensaje, ya sin espacios a los costados. Sin controlar, el campo se vacía solo. |
| `onStop` | `() => void` | — | Corta la respuesta en curso. Sin esto, mientras `busy` el botón queda apagado. |
| `onValueChange` | `(value: string) => void` | — | Avisa cada cambio del texto. |
| `value` | `string` | — | El texto, controlado. |
| `className` | `string` | — | Se fusiona con las clases del componente vía `cn()` (tailwind-merge): lo que pongas gana. |

### ChatMessage

Hereda las props de `<div>`.

| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
| `from` * | `"user" \| "assistant"` | — | Quién habla. El usuario va a la derecha, en un globo; el asistente a la izquierda, sin globo. |
| `className` | `string` | — | Se fusiona con las clases del componente vía `cn()` (tailwind-merge): lo que pongas gana. |

`*` obligatoria.

### ChatMessageActions

Hereda las props de `<div>`.

| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
| `className` | `string` | — | Se fusiona con las clases del componente vía `cn()` (tailwind-merge): lo que pongas gana. |

### ChatMessages

Hereda las props de `<div>`.

| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
| `busy` | `boolean` | — | Hay una respuesta en curso: lo anuncia con `aria-busy`. Si no se pasa, toma el del `Chat`. |
| `labels` | `Partial<ChatLabels>` | — | Textos de la interfaz, para traducir o ajustar el tono. |
| `onScroll` | `UIEventHandler<T>` | — | Se llama después de que la lista anota si el usuario sigue abajo. Sirve para mostrar un botón de «ir al final». |
| `className` | `string` | — | Se fusiona con las clases del componente vía `cn()` (tailwind-merge): lo que pongas gana. |

### ChatSuggestion

Hereda las props de `<button>`.

| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
| `type` | `"button" \| "submit" \| "reset"` | `"button"` | Por defecto `button`, no `submit`: una sugerencia adentro de un `<form>` no tiene que enviarlo. |
| `className` | `string` | — | Se fusiona con las clases del componente vía `cn()` (tailwind-merge): lo que pongas gana. |
| `disabled` | `boolean` | — | **Heredada de Base UI.** Apaga la interacción y lo marca con `data-disabled`, que es el atributo del que cuelgan los estilos de apagado. |

### ChatSuggestions

Hereda las props de `<div>`.

| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
| `className` | `string` | — | Se fusiona con las clases del componente vía `cn()` (tailwind-merge): lo que pongas gana. |

### ChatTitle

Hereda las props de `<div>`.

| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
| `className` | `string` | — | Se fusiona con las clases del componente vía `cn()` (tailwind-merge): lo que pongas gana. |

### ChatTyping

Hereda las props de `<div>`.

| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
| `labels` | `Partial<ChatLabels>` | — | Textos de la interfaz, para traducir o ajustar el tono. |
| `className` | `string` | — | Se fusiona con las clases del componente vía `cn()` (tailwind-merge): lo que pongas gana. |

## Teclado

| Tecla | Qué hace |
|---|---|
| Enter | Envía el mensaje. En una pantalla táctil, baja de renglón. |
| Shift + Enter | Baja de renglón. |
| Tab | Recorre la lista de mensajes, el campo y el botón. |
| ↑ ↓ · Re Pág · Av Pág | Con el foco en la lista, la scrollean. |

## Accesibilidad

- La lista es un `role="log"` con `aria-live="polite"`: lo que se agrega se anuncia sin mover el foco. `busy` lo marca con `aria-busy`.
- La lista es tabulable: una zona que scrollea sin nada enfocable adentro no se puede recorrer con el teclado.
- `ChatTyping` es un `role="status"` con nombre; `ChatError`, un `role="alert"`.
- El campo tiene su `<label>`, oculto a la vista. El botón de enviar y el de detener, su `aria-label`.
- En táctil el campo mide 44px y la letra 16px: con menos, iOS hace zoom al enfocar.
- El borde de la IA es decoración: lo que anuncia la espera es el `aria-busy` de la lista. Con `prefers-reduced-motion` se enciende pero no gira.
- Mientras se arma una palabra con un IME (japonés, coreano), Enter la confirma y no envía.

## Reglas de uso

- No dibuja superficie: va adentro de un `Popover`, un `Sheet` o una `Card`.
- El texto de un mensaje va como `children`. Si la respuesta trae formato, convertila a elementos de React. **Nunca la inyectes como HTML**: lo que escribe un modelo es texto de un tercero.
- `ChatMessages` sigue al último mensaje mientras el usuario esté abajo. Si subió a leer, no lo arrastra.
- `busy` va en el `Chat`: enciende el borde de la IA alrededor de la conversación y les avisa a la lista y al campo. Es el borde que recorre la pantalla del iPhone cuando se activa Siri, y acá hace de spinner.
- Con una respuesta en curso, el botón de enviar pasa a ser el de detener, en el mismo lugar.
- Sin controlar, `ChatInput` se vacía solo al enviar. Controlado (`value`), vaciarlo es de la app, que es quien sabe si el envío salió.
- La letra chica (`ChatDisclaimer`) va debajo del campo, no en la cabecera: ahí es donde se mira antes de enviar.

## Relacionados

[ai-button](/docs/components/ai-button.md) · [popover](/docs/components/popover.md) · [sheet](/docs/components/sheet.md) · [textarea](/docs/components/textarea.md)
