# Chat

> Una interfaz de chat de equipo en Python: canales con no leídos, mensajes directos con presencia, mensajes agrupados con Markdown, adjuntos, reacciones y un compositor completo.

Source: https://albakore.github.io/pyzed/es/blocks/chat/

Una mensajería al estilo de Slack o Discord: canales con contador de no leídos, mensajes directos con puntos de presencia, un hilo que agrupa mensajes seguidos, Markdown, adjuntos, reacciones y un compositor con botones de formato. Para un chat con IA que transmite la respuesta mira [Crear apps con IA](https://albakore.github.io/pyzed/es/ai/ai-apps/).

La captura sigue el tema del sitio: cambia entre claro y oscuro arriba de la página para ver el bloque en One Light y One Dark.

## Ejecutarlo

Desde la raíz del repositorio:

```bash
python examples/blocks/chat.py
```

## Qué muestra

- Los mensajes seguidos de la misma persona se agrupan bajo un avatar.
- `ui.markdown` muestra negrita, código y enlaces.
- `pz.scrollable(anchor_bottom=True)` mantiene visible el último mensaje.
- <kbd>Enter</kbd> envía y baja hasta el final con `pz.Command.scroll_to_end`.

## Componentes que usa

| Componente | Para |
| --- | --- |
| [Markdown](https://albakore.github.io/pyzed/es/library/markdown/) | Texto de los mensajes |
| [Avatar](https://albakore.github.io/pyzed/es/library/avatar/) | Personas |
| [Indicator](https://albakore.github.io/pyzed/es/library/indicator/) | Presencia |
| [Count badge](https://albakore.github.io/pyzed/es/library/count-badge/) | No leídos |
| [List item](https://albakore.github.io/pyzed/es/library/list-item/) | Canales y personas |
| [List header](https://albakore.github.io/pyzed/es/library/list-header/) | Secciones de la barra |
| [Chip](https://albakore.github.io/pyzed/es/library/chip/) | Reacciones |
| [Input](https://albakore.github.io/pyzed/es/library/input/) | Compositor |
| [Facepile](https://albakore.github.io/pyzed/es/library/facepile/) | Miembros del canal |
| [Scrollable](https://albakore.github.io/pyzed/es/library/scrollable/) | El hilo |

## Código

El bloque completo, listo para copiar. Solo necesita `pip install pyzed-ui`.

```python title="examples/blocks/chat.py"
"""Chat block: a team messenger with channels, direct messages, a message thread and a composer."""

import pyzed as pz
from pyzed import ui

CHANNELS = [("general", 0), ("design", 3), ("engineering", 12), ("releases", 0), ("random", 1)]
PEOPLE = [("Maya López", "online"), ("Jackson Lee", "online"), ("Isabella Nguyen", "away"), ("William Kim", "offline")]
PRESENCE = {"online": "success", "away": "warning", "offline": "text_disabled"}

MESSAGES = [
    ("Maya López", "09:12", "Morning! The new onboarding flow is on staging. Can someone give it a spin?", None),
    ("Jackson Lee", "09:14", "On it. First impression: **much** faster than the old one.", None),
    ("Jackson Lee", "09:15", "One nit: the progress bar jumps from step 2 to 4 when you skip the team invite.", None),
    ("Isabella Nguyen", "09:21", "Here's the build size before and after the switch to Pyzed:",
     ("build-report.pdf", "2.4 MB")),
    ("Maya López", "09:24", "Good catch, I'll fix the skip logic. Shipping it with `v2.3.1` this afternoon.", None),
]

def initials(name):
    return "".join(part[0] for part in name.split())

class Chat(pz.App):
    title = "Messages"
    size = (1200, 760)

    def __init__(self):
        self.channel = "design"
        self.draft = "Looks great! Let's demo it in Friday's review"
        self.messages = list(MESSAGES)
        self.reactions = {2: {"👍": 3, "🎯": 1}, 4: {"🚀": 4}}

    def send(self):
        if self.draft.strip():
            self.messages.append(("Kevin Vargas", "09:30", self.draft.strip(), None))
            self.draft = ""
        return pz.Command.scroll_to_end("chat-thread")

    # -- sidebar ---------------------------------------------------------------------------
    def sidebar(self):
        channels = [ui.list_item(f"# {name}", lambda name=name: setattr(self, "channel", name),
                                 selected=name == self.channel, height=30,
                                 end=ui.count_badge(unread) if unread else None)
                    for name, unread in CHANNELS]
        people = [ui.list_item(name, None, height=30, start=pz.stack([
                      ui.avatar(initials(name), size=20, index=i),
                      pz.container(ui.indicator(PRESENCE[status], size=7), width=22, height=22, align_x="end",
                                   align_y="end")]))
                  for i, (name, status) in enumerate(PEOPLE)]
        return pz.column([
            pz.row([ui.label("Acme", size="lg", weight="semibold"), pz.hspace(),
                    ui.icon_button("square-pen", None, tooltip="New message")], align="center"),
            ui.search_input("", None, placeholder="Jump to…"),
            pz.column([ui.list_header("Channels", end=ui.icon_button("plus", None, size="sm")), *channels],
                      spacing=2),
            pz.column([ui.list_header("Direct messages"), *people], spacing=2),
        ], spacing=16, width=250, height="fill", padding=(16, 12)).style(background="panel")

    # -- thread ----------------------------------------------------------------------------
    def message(self, index, author, time, text, attachment):
        previous = self.messages[index - 1][0] if index else None
        body = [ui.markdown(text)]
        if attachment:
            name, size = attachment
            body.append(pz.row([
                pz.container(pz.icon("file-text", size=18, color="#ffffff"), padding=8).style(
                    background="error", radius=8),
                pz.column([ui.label(name, weight="medium"), ui.muted(f"PDF · {size}", size="sm")], spacing=1),
                pz.hspace(width=24),
                ui.icon_button("download", None, tooltip="Download"),
            ], spacing=10, align="center").style(background="surface", border=(1, "border_variant"), radius=10,
                                                 padding=10, width="shrink"))
        reactions = self.reactions.get(index)
        if reactions:
            body.append(pz.row([ui.chip(f"{emoji} {count}", on_press=None) for emoji, count in reactions.items()],
                               spacing=6))
        if author == previous:  # a follow-up message: no avatar or name
            return pz.row([pz.space(width=36), pz.column(body, spacing=6, width="fill")], spacing=12,
                          padding=(2, 20))
        names = [a for a, _ in PEOPLE]
        header = pz.row([ui.label(author, weight="semibold"), ui.muted(time, size="sm")], spacing=8, align="end")
        return pz.row([
            ui.avatar(initials(author), size=36, index=names.index(author) if author in names else 5),
            pz.column([header, *body], spacing=6, width="fill"),
        ], spacing=12, padding=(10, 20, 2, 20))

    def composer(self):
        return pz.container(
            pz.column([
                ui.input(self.draft, lambda v: setattr(self, "draft", v), placeholder=f"Message #{self.channel}",
                         on_submit=self.send, borderless=True, id="composer"),
                pz.row([
                    ui.icon_button("plus", None, tooltip="Attach"),
                    ui.icon_button("bold", None), ui.icon_button("italic", None), ui.icon_button("code", None),
                    ui.icon_button("smile", None, tooltip="Emoji"),
                    ui.icon_button("at-sign", None, tooltip="Mention"),
                    pz.hspace(),
                    ui.muted("Enter to send", size="sm"),
                    ui.icon_button("send-horizontal", self.send, variant="solid" if self.draft else "ghost",
                                   tooltip="Send"),
                ], spacing=2, align="center"),
            ], spacing=6),
            padding=10, width="fill",
        ).style(background="surface", border=(1, "border"), radius=12)

    def view(self):
        header = pz.row([
            ui.label(f"# {self.channel}", size="lg", weight="semibold"),
            ui.muted("Product design, reviews and handoff", size="sm"),
            pz.hspace(),
            ui.facepile(["ML", "JL", "IN", "WK", "KV", "SD", "AB"], size=24, max_shown=4),
            ui.icon_button("phone", None, tooltip="Call"),
            ui.icon_button("pin", None, tooltip="Pinned"),
            ui.icon_button("panel-right", None, tooltip="Details"),
        ], spacing=10, align="center", padding=(12, 20))
        divider = pz.row([ui.divider(), ui.badge("Today", tint="accent", variant="outline"), ui.divider()],
                         spacing=10, align="center", padding=(8, 20))
        thread = pz.scrollable(
            pz.column([divider, *[self.message(i, *m) for i, m in enumerate(self.messages)]], spacing=4),
            height="fill", id="chat-thread", anchor_bottom=True)
        main = pz.column([header, ui.divider(), thread, pz.container(self.composer(), padding=(8, 20, 20, 20))],
                         width="fill", height="fill").style(background="editor")
        return pz.row([self.sidebar(), ui.divider(vertical=True), main], height="fill")

if __name__ == "__main__":
    pz.run(Chat)
```

## Hazlo tuyo

Cambia `theme` (o `self.theme = pz.themes.get("One Dark").with_accent("#e11d48")`), pon tus datos en lugar de los de ejemplo y separa piezas en [componentes](https://albakore.github.io/pyzed/es/guides/components/) cuando crezcan. Un agente de IA con el [servidor MCP](https://albakore.github.io/pyzed/es/ai/mcp/) puede leer este bloque con `read_example("blocks/chat")` y adaptarlo por ti.
