Wrapper application around upstream Vanna with: - Tenant-aware ChromaDB memory (per program/store) - ClickHouse RLS runner with introspection guards - PT-BR system prompt and chat translations - Custom Plotly chart generator (ranked bar, datetime coercion) - Embed bootstrap (theme pierce + i18n + markdown) shared by demo and React app - Event sink for chat turn observability
187 lines
6.7 KiB
HTML
187 lines
6.7 KiB
HTML
<!doctype html>
|
|
<html lang="pt-BR">
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<title>Vanna chat — demo</title>
|
|
<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"
|
|
/>
|
|
<link rel="stylesheet" href="/vanna-theme.css" />
|
|
<!-- Bootstrap único do <vanna-chat>: theme pierce + tradutor PT + markdown
|
|
+ load do bundle. Mesmo arquivo é consumido pela app React via
|
|
vannaChatLoader.ts, garantindo fonte única (sem duplicação). -->
|
|
<script src="/vanna-embed-bootstrap.js"></script>
|
|
<style>
|
|
body {
|
|
margin: 0;
|
|
min-height: 100vh;
|
|
font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
|
|
color: #111827;
|
|
background: url("/dashboard-bg.png") top left / cover no-repeat fixed,
|
|
#f8efe2;
|
|
}
|
|
.wrap {
|
|
display: none;
|
|
}
|
|
|
|
/* Floating CTA — substitui o FAB nativo do <vanna-chat>.
|
|
Posicionado fixed bottom-right; clique abre o chat e esconde
|
|
o próprio CTA. Listener no evento window-state-changed do
|
|
componente alterna a visibilidade. */
|
|
.vanna-cta {
|
|
position: fixed;
|
|
right: 24px;
|
|
top: 100px;
|
|
z-index: 2147483000;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
padding: 4px;
|
|
background: #ffffff;
|
|
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, box-shadow 0.15s ease;
|
|
}
|
|
.vanna-cta[hidden] {
|
|
display: none;
|
|
}
|
|
.vanna-cta:hover {
|
|
transform: translateY(-1px);
|
|
}
|
|
.vanna-cta:active {
|
|
transform: translateY(0);
|
|
}
|
|
.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;
|
|
}
|
|
|
|
/* O CTA custom é o único ponto de entrada. Esconde o componente
|
|
enquanto não estiver maximizado: oculta o FAB nativo (estado
|
|
minimized, círculo 64x64) E também previne o flash de centralizado
|
|
se o JS de minimização atrasar (estado normal antes do windowState
|
|
setter rodar). Quando o CTA dispara windowState='maximized', a regra
|
|
deixa de casar e o chat aparece. */
|
|
vanna-chat:not(.maximized) {
|
|
display: none !important;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="wrap">
|
|
<h1>Vanna chat — demo</h1>
|
|
<p>
|
|
Esta página embeda o web component oficial
|
|
<code><vanna-chat></code> com
|
|
<code>starting-state="minimized"</code> — o botão flutuante aparece no
|
|
canto inferior direito. Os IDs vêm do <code>.env</code> via
|
|
substituição no handler.
|
|
</p>
|
|
<p>
|
|
Em produção, o app cliente cola um snippet semelhante na página dele,
|
|
substituindo <code>__PROGRAM_ID__</code> / <code>__STORE_ID__</code>
|
|
pelos IDs do tenant logado.
|
|
</p>
|
|
</div>
|
|
|
|
<button
|
|
type="button"
|
|
id="vanna-cta"
|
|
class="vanna-cta"
|
|
aria-label="Conversar com meus dados"
|
|
>
|
|
<span class="vanna-cta__logo" aria-hidden="true">
|
|
<img src="/clubpetro-logo.svg" alt="" />
|
|
</span>
|
|
<span class="vanna-cta__pill">Conversar com meus dados</span>
|
|
</button>
|
|
|
|
<script>
|
|
// Dispara o bootstrap (theme pierce + tradutor + markdown + load do
|
|
// bundle). baseUrl vazio = mesma origem (server FastAPI local).
|
|
window.VannaEmbed.ensureLoaded({ baseUrl: "" });
|
|
</script>
|
|
<vanna-chat
|
|
id="vanna-chat-instance"
|
|
api-base=""
|
|
sse-endpoint="/api/vanna/v2/chat_sse?program_id=__PROGRAM_ID__&store_id=__STORE_ID__&user_id=__USER_ID__"
|
|
ws-endpoint="/api/vanna/v2/chat_websocket?program_id=__PROGRAM_ID__&store_id=__STORE_ID__&user_id=__USER_ID__"
|
|
poll-endpoint="/api/vanna/v2/chat_poll?program_id=__PROGRAM_ID__&store_id=__STORE_ID__&user_id=__USER_ID__"
|
|
startingstate="minimized"
|
|
theme="light"
|
|
title="ClubPetro IA">
|
|
</vanna-chat>
|
|
|
|
<script>
|
|
// showProgress default=true e Lit boolean attrs não desligam via HTML.
|
|
// Setar a propriedade JS direto após o registro do custom element.
|
|
customElements.whenDefined("vanna-chat").then(() => {
|
|
const chat = document.getElementById("vanna-chat-instance");
|
|
const cta = document.getElementById("vanna-cta");
|
|
if (!chat) return;
|
|
chat.showProgress = false;
|
|
// Reforço: garantir minimized mesmo se o atributo não pegar.
|
|
if (chat.windowState !== "minimized") chat.windowState = "minimized";
|
|
|
|
// Sync inicial — começamos minimized via attribute, então CTA visível.
|
|
// Ao clicar no CTA, expandimos o chat (windowState='normal') e
|
|
// escondemos o CTA. Quando o user minimiza pelo header do chat,
|
|
// o componente emite window-state-changed; aí mostramos o CTA de novo.
|
|
const sync = (state) => {
|
|
if (!cta) return;
|
|
cta.hidden = state !== "minimized";
|
|
};
|
|
sync(chat.windowState);
|
|
|
|
if (cta) {
|
|
cta.addEventListener("click", () => {
|
|
// Esconder explicitamente — o setter de windowState NÃO dispara
|
|
// window-state-changed (só os métodos privados disparam), então
|
|
// o listener abaixo só pega a transição quando o user minimiza
|
|
// pelo header. Pra abrir, escondemos manualmente aqui.
|
|
cta.hidden = true;
|
|
chat.windowState = "maximized";
|
|
});
|
|
}
|
|
chat.addEventListener("window-state-changed", (e) => {
|
|
sync(e.detail?.state);
|
|
});
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|