# Embedar `` (chat flutuante) num app React Guia para o time frontend embedar o web component `` 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 ` ``` ### Etapa 2 — Carregar o bundle ```html ``` ### Etapa 3 — Componente React `` ```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(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 ); } ``` **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 & { "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 ( <> {/* ... suas rotas ... */} {user && currentStore && ( )} ); } ``` **Importante:** se o usuário trocar de loja (sem re-login), force remount adicionando `key`: ```tsx ``` 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, `` 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 `` 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 `` ```tsx // src/components/VannaCTA.tsx import { useEffect, useState } from "react"; interface VannaCTAProps { /** Ref opcional para o . 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 ( ); } ``` Renderize **junto** com o ``: ```tsx {user && currentStore && ( <> )} ``` --- ## 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 ``; o pierce DEVE preceder o `