# Embedar `<vanna-chat>` (chat flutuante) num app React

Guia para o time frontend embedar o web component `<vanna-chat>` como botão flutuante (canto inferior direito) numa aplicação React. Cobre auth IDs, theming e gotchas comuns.

---

## 1. Pré-requisitos

| Item | Valor |
| --- | --- |
| Backend Vanna | `https://SEU-BACKEND-VANNA` (substituir pelas envs prod/staging) |
| Bundle JS | `https://SEU-BACKEND-VANNA/static/vanna-components.js` (~7.5 MB, ES module) |
| Tema CSS (opcional) | `https://SEU-BACKEND-VANNA/vanna-theme.css` |
| Endpoints chat | `/api/vanna/v2/chat_sse` (SSE), `/api/vanna/v2/chat_websocket` (WS), `/api/vanna/v2/chat_poll` (long poll) |
| IDs obrigatórios na URL | `program_id`, `store_id`, `user_id` (do usuário autenticado) |

**Sobre os IDs:** `program_id` + `store_id` controlam **RLS** (o backend filtra dados pela loja desse tenant — sem eles, o chat retorna erro). `user_id` é usado **só para auditoria** (log em `events.vanna_ai`); se ausente, a row é gravada com `user_id=''` e o chat funciona normal — mas perde rastreabilidade. Mande sempre os 3.

---

## 2. Setup em 3 etapas

### Etapa 1 — `index.html`: scripts que rodam ANTES do bundle

Cole **antes** do `<script type="module">` do bundle. Há 2 scripts, na ordem:

1. **Theme pierce** — necessário **só se você quer customizar cores/fontes** via `vanna-theme.css`. O bundle encapsula cada componente interno em Shadow DOM com tokens próprios; sem esse hack, um `<link rel="stylesheet">` no documento não atinge os filhos. O patch força o stylesheet a ser adoptado em todo `attachShadow` novo.
2. **i18n PT-BR** — traduz strings hardcoded do bundle (`Search...`, `X rows`, `Export`, etc.) e converte markdown literal nas mensagens (`**negrito**` → `<strong>`).

```html
<!-- index.html (Vite/CRA — public/index.html) -->
<head>
  <!-- Open Sans (font usado pelo tema) -->
  <link rel="preconnect" href="https://fonts.googleapis.com" />
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
  <link
    rel="stylesheet"
    href="https://fonts.googleapis.com/css2?family=Open+Sans:wght@400;500;600;700&display=swap"
  />

  <!-- 1. Theme pierce (opcional — só se for customizar cores) -->
  <script>
    (() => {
      const themeSheet = new CSSStyleSheet();
      const desc = Object.getOwnPropertyDescriptor(
        ShadowRoot.prototype, "adoptedStyleSheets"
      );
      if (desc && desc.set) {
        Object.defineProperty(ShadowRoot.prototype, "adoptedStyleSheets", {
          configurable: true, enumerable: true,
          get() { return desc.get.call(this); },
          set(value) {
            const without = (value || []).filter((s) => s !== themeSheet);
            desc.set.call(this, [...without, themeSheet]);
          },
        });
      }
      fetch("https://SEU-BACKEND-VANNA/vanna-theme.css")
        .then((r) => r.text())
        .then((css) => themeSheet.replaceSync(css))
        .catch((err) => console.error("[vanna-theme] load failed", err));
    })();
  </script>

  <!-- 2. i18n PT-BR + markdown parser — copiar literal do segundo <script>
       em static/embed-demo.html (o IIFE que termina em "[vanna-i18n] frontend
       translator armed"). Faz MutationObserver + tradução exact-match +
       parser markdown (bold/italic/code/links/listas/headers h1-h6).
       Não modifique sem entender o porquê — o bundle não tem hooks de tradução. -->
  <script>
    /* ... cole o IIFE de tradução de embed-demo.html aqui ... */
  </script>
</head>
```

### Etapa 2 — Carregar o bundle

```html
<!-- Final do <body>, ou via React effect (ver etapa 3) -->
<script type="module" src="https://SEU-BACKEND-VANNA/static/vanna-components.js"></script>
```

### Etapa 3 — Componente React `<VannaChat />`

```tsx
// src/components/VannaChat.tsx
import { useEffect, useRef } from "react";

const BACKEND_URL = "https://SEU-BACKEND-VANNA";

interface VannaChatProps {
  programId: string;
  storeId: string;
  userId: string;
  /** Override do título do header. Default: "ClubPetro IA" */
  title?: string;
}

export function VannaChat({ programId, storeId, userId, title = "ClubPetro IA" }: VannaChatProps) {
  const ref = useRef<HTMLElement>(null);

  // Constrói query string com os 3 IDs
  const qs = new URLSearchParams({
    program_id: programId,
    store_id: storeId,
    user_id: userId,
  }).toString();

  // Custom element exige property assignment pra boolean attrs.
  // showProgress=true por default — desligamos pra esconder a sidebar de progresso.
  useEffect(() => {
    customElements.whenDefined("vanna-chat").then(() => {
      if (ref.current) {
        (ref.current as any).showProgress = false;
      }
    });
  }, []);

  return (
    // @ts-expect-error — custom element não tem types nativos do React
    <vanna-chat
      ref={ref}
      api-base={BACKEND_URL}
      sse-endpoint={`/api/vanna/v2/chat_sse?${qs}`}
      ws-endpoint={`/api/vanna/v2/chat_websocket?${qs}`}
      poll-endpoint={`/api/vanna/v2/chat_poll?${qs}`}
      starting-state="minimized"
      theme="light"
      title={title}
    />
  );
}
```

**Tipagem** (apaga o `@ts-expect-error`) — adicione em `src/types/global.d.ts`:

```ts
declare namespace JSX {
  interface IntrinsicElements {
    "vanna-chat": React.DetailedHTMLProps<
      React.HTMLAttributes<HTMLElement> & {
        "api-base"?: string;
        "sse-endpoint"?: string;
        "ws-endpoint"?: string;
        "poll-endpoint"?: string;
        "starting-state"?: "minimized" | "expanded";
        theme?: "light" | "dark";
        title?: string;
      },
      HTMLElement
    >;
  }
}
```

---

## 3. Uso com auth context

Renderize **uma única vez** no layout raiz, depois do login. O componente já é flutuante (`starting-state="minimized"` mostra só o botão no canto inferior direito).

```tsx
// src/App.tsx
import { useAuth } from "./auth";
import { VannaChat } from "./components/VannaChat";

export default function App() {
  const { user, currentStore } = useAuth();

  return (
    <>
      <Routes>{/* ... suas rotas ... */}</Routes>

      {user && currentStore && (
        <VannaChat
          programId={currentStore.programId}
          storeId={currentStore.id}
          userId={user.id}
        />
      )}
    </>
  );
}
```

**Importante:** se o usuário trocar de loja (sem re-login), force remount adicionando `key`:

```tsx
<VannaChat
  key={`${currentStore.id}-${user.id}`}
  programId={currentStore.programId}
  storeId={currentStore.id}
  userId={user.id}
/>
```

Sem `key`, o React reusa o mesmo elemento DOM com URLs antigas — a SSE em curso continua apontando pro tenant anterior até o usuário fechar o chat.

---

## 4. Botão flutuante custom (CTA "Conversar com meus dados")

Por padrão, `<vanna-chat starting-state="minimized">` mostra um FAB redondo no canto. Substituímos por um CTA pill horizontal (logo + pill com gradient laranja→roxo).

### CSS (cole no CSS global ou módulo)

```css
.vanna-cta {
  position: fixed;
  right: 24px;
  top: 100px; /* ou bottom: 24px se preferir o canto inferior direito */
  z-index: 2147483000;
  display: inline-flex;
  align-items: center;
  gap: 10px;
  padding: 4px;
  background: #fff;
  border: 1px solid transparent;
  border-radius: 8px;
  background-image: linear-gradient(#fff, #fff),
    linear-gradient(90deg, #f2672a 5%, #680367 100%);
  background-origin: border-box;
  background-clip: padding-box, border-box;
  box-shadow:
    3px 5px 15px rgba(235, 98, 45, 0.19),
    13px 24px 27px rgba(235, 98, 45, 0.16),
    28px 53px 36px rgba(235, 98, 45, 0.10),
    50px 95px 43px rgba(235, 98, 45, 0.03);
  cursor: pointer;
  font-family: "Open Sans", system-ui, sans-serif;
  transition: transform 0.15s ease;
}
.vanna-cta:hover { transform: translateY(-1px); }
.vanna-cta__logo {
  flex: none; width: 42px; height: 42px;
  display: grid; place-items: center;
}
.vanna-cta__logo img,
.vanna-cta__logo svg { width: 100%; height: 100%; display: block; }
.vanna-cta__pill {
  flex: 1; padding: 10px 18px; border-radius: 5px;
  background: linear-gradient(90deg, #f2672a 5%, #680367 100%);
  color: #fff; font-size: 14px; font-weight: 500; line-height: 1.15;
  white-space: nowrap;
}
```

### Esconder o FAB nativo do `<vanna-chat>`

Adicionar ao `vanna-theme.css` (servido pelo backend, ou seu próprio override):

```css
:host(.minimized) {
  background: transparent !important;
  box-shadow: none !important;
  width: 0 !important; height: 0 !important;
  pointer-events: none !important;
}
:host(.minimized) .minimized-icon { display: none !important; }

/* CTA abre direto maximized — esconder restore e maximize do header
   pra não expor o estado intermediário "normal". Sobra só .minimize. */
.window-control-btn.restore,
.window-control-btn.maximize { display: none !important; }
```

Sem isso, o FAB nativo aparece sobreposto ao CTA, e o usuário consegue cair no estado "normal" (janela pequena flutuante). Requer o **theme pierce** da etapa 1 — sem ele, essas regras não atingem o shadow DOM.

### Componente React `<VannaCTA />`

```tsx
// src/components/VannaCTA.tsx
import { useEffect, useState } from "react";

interface VannaCTAProps {
  /** Ref opcional para o <vanna-chat>. Se omitido, busca por id="vanna-chat-instance". */
  chatId?: string;
  label?: string;
}

export function VannaCTA({ chatId = "vanna-chat-instance", label = "Conversar com meus dados" }: VannaCTAProps) {
  const [visible, setVisible] = useState(true);

  useEffect(() => {
    let chat: any = null;
    customElements.whenDefined("vanna-chat").then(() => {
      chat = document.getElementById(chatId);
      if (!chat) return;

      const sync = (state: string) => setVisible(state === "minimized");
      sync(chat.windowState);

      const handler = (e: any) => sync(e.detail?.state);
      chat.addEventListener("window-state-changed", handler);
      return () => chat.removeEventListener("window-state-changed", handler);
    });
  }, [chatId]);

  if (!visible) return null;

  return (
    <button
      type="button"
      className="vanna-cta"
      aria-label={label}
      onClick={() => {
        const chat = document.getElementById(chatId) as any;
        if (!chat) return;
        // Esconder o CTA explicitamente — o setter de windowState não
        // dispara window-state-changed (só os métodos privados do componente
        // disparam), então o useEffect só sincroniza quando o user minimiza
        // pelo header do chat.
        setVisible(false);
        chat.windowState = "maximized";
      }}
    >
      <span className="vanna-cta__logo" aria-hidden="true">
        {/* Hospede o SVG no seu CDN/public e troque o src.
            No backend Vanna, fica em /clubpetro-logo.svg. */}
        <img src="https://SEU-BACKEND-VANNA/clubpetro-logo.svg" alt="" />
      </span>
      <span className="vanna-cta__pill">{label}</span>
    </button>
  );
}
```

Renderize **junto** com o `<VannaChat />`:

```tsx
{user && currentStore && (
  <>
    <VannaChat
      key={`${currentStore.id}-${user.id}`}
      programId={currentStore.programId}
      storeId={currentStore.id}
      userId={user.id}
    />
    <VannaCTA />
  </>
)}
```

---

## 5. Theming (opcional)

O tema é um `vanna-theme.css` com ~50 CSS custom properties. Pra mudar cores/fontes:

1. Garanta que o **theme pierce script** (etapa 1) está rodando antes do bundle.
2. Servir seu próprio `vanna-theme.css` (pode ser o do backend Vanna, ou um override próprio).
3. Atualizar a URL do `fetch` no theme pierce.

Tokens disponíveis: ver `vanna/frontends/webcomponent/src/styles/vanna-design-tokens.ts` no repo do backend. Exemplos:

```css
:host, vanna-chat {
  --vanna-primary: #023d60;
  --vanna-accent: #15a8a8;
  --vanna-radius-md: 8px;
  --vanna-font-family: "Open Sans", system-ui, sans-serif;
}
```

**Sem o pierce** o `:host` não atinge filhos (Shadow DOM encapsulado) — vai parecer que o tema "não pegou".

---

## 6. Gotchas (cole na cabeça)

| Sintoma | Causa provável | Fix |
| --- | --- | --- |
| Chat aparece mas sem cores customizadas | Theme pierce não rodou antes do bundle | Verifique ordem dos scripts no `<head>`; o pierce DEVE preceder o `<script type="module">` |
| `showProgress=false` ignorado, sidebar de progresso aparece | Lit boolean attrs não desligam via HTML | Use a property em `useEffect` + `customElements.whenDefined` (ver componente acima) |
| Mensagens com `**foo**` literal em vez de **foo** em negrito | i18n IIFE não rodou (markdown parser dele que faz a conversão) | Garantir que o segundo script da etapa 1 está presente |
| `Search...`, `Export`, `X rows` em inglês | Mesmo motivo: i18n IIFE ausente | Idem |
| Chat retorna erro / não responde | `program_id` ou `store_id` ausentes/inválidos na URL — backend rejeita com `PermissionError` | Inspecione a URL do `sse-endpoint` no DOM (DevTools → Elements); confirme que os 3 IDs aparecem |
| Tudo funciona mas `events.vanna_ai.user_id` vem vazio | Apenas o `user_id` ausente (não bloqueante) | Adicionar `&user_id=...` na query string |
| Bundle não carrega — CORS error | Backend Vanna sem CORS pro domínio do frontend | Setar `VANNA_CORS_ORIGINS` no `.env` do backend pra incluir seu domínio |
| Bundle 7.5 MB → carregamento lento na home | É o tamanho do bundle Lit + Plotly | Usar `<link rel="preload" as="script" href="...vanna-components.js" />` no `<head>` ou lazy-load só quando o usuário faz login |

---

## 7. Checklist final pra produção

- [ ] Backend `VANNA_CORS_ORIGINS` inclui o domínio do frontend
- [ ] Bundle servido via HTTPS (mixed content bloqueia em prod)
- [ ] Auth flow garante `program_id`, `store_id`, `user_id` válidos antes de montar o componente
- [ ] `key` no `<VannaChat>` muda quando user/loja troca
- [ ] Open Sans pré-carregado via `<link rel="preconnect">`
- [ ] (Opcional) `vanna-theme.css` servido com cache-busting (`?v=hash`)
- [ ] Testou minimizado e expandido em mobile (o botão é fixo bottom-right)
- [ ] Verificou no console: `[vanna-theme] CSS loaded: NNNN bytes` e `[vanna-i18n] frontend translator armed`

---

## Referência

- Demo de dev (com tudo wireado): `static/embed-demo.html` no repo do backend.
- Web component upstream: `vanna/frontends/webcomponent/src/components/vanna-chat.ts`.
- Endpoints: `vanna/src/vanna/servers/fastapi/routes.py`.
