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
393 lines
14 KiB
CSS
393 lines
14 KiB
CSS
/* Vanna chat theme — ClubPetro
|
|
* Paleta espelhada do widget de referência (test.html):
|
|
* - Brand orange: #F46A1F (primário / interativo)
|
|
* - Brand orange dark: #DB5510 (badges, hover, accent forte)
|
|
* - Purple deep: #4A1A56 (texto / estrutural)
|
|
* - Purple medium: #7B2D8E (info / divisores secundários)
|
|
* - Cream bg: #F8EFE2 (header, surfaces warm)
|
|
* - Cream border: #EADFCB (borders sobre cream)
|
|
* - Meta/muted: #8B7B6E
|
|
* - Font: Open Sans
|
|
*
|
|
* CSS custom properties pierce the Shadow DOM, so overriding them on the
|
|
* <vanna-chat> host element retemas tudo dentro do componente.
|
|
*
|
|
* Reference (todos os tokens disponíveis):
|
|
* vanna/frontends/webcomponent/src/styles/vanna-design-tokens.ts
|
|
*/
|
|
|
|
/* Estratégia dual:
|
|
* - vanna-chat: matchea o host element no documento (cascade externo).
|
|
* - :host: matchea o host de qualquer shadow root onde este sheet
|
|
* for adotado via adoptedStyleSheets (override em todo nested
|
|
* custom element como vanna-message, plotly-chart, etc., já que
|
|
* eles re-declaram os tokens via :host).
|
|
*
|
|
* Google Fonts é carregado via <link> no HTML — @import não funciona
|
|
* em CSSStyleSheet construída via replaceSync().
|
|
*/
|
|
:host,
|
|
vanna-chat {
|
|
/* === Brand accent (orange) === */
|
|
--vanna-accent-primary-default: #f46a1f;
|
|
--vanna-accent-primary-stronger: #db5510;
|
|
--vanna-accent-primary-strongest: #b8460e;
|
|
--vanna-accent-primary-subtle: rgba(244, 106, 31, 0.12);
|
|
--vanna-accent-primary-hover: #e55c13;
|
|
|
|
/* "Positive" também ancora no laranja pra manter coerência da marca */
|
|
--vanna-accent-positive-default: #f46a1f;
|
|
--vanna-accent-positive-stronger: #db5510;
|
|
--vanna-accent-positive-subtle: rgba(244, 106, 31, 0.12);
|
|
|
|
/* Negativo continua vermelho (semântica de erro deve diferir da marca) */
|
|
--vanna-accent-negative-default: #dc2626;
|
|
--vanna-accent-negative-stronger: #b91c1c;
|
|
--vanna-accent-negative-subtle: rgba(220, 38, 38, 0.1);
|
|
|
|
/* Warning em amber pra distinguir do laranja-primário */
|
|
--vanna-accent-warning-default: #d97706;
|
|
--vanna-accent-warning-stronger: #b45309;
|
|
--vanna-accent-warning-subtle: rgba(217, 119, 6, 0.1);
|
|
|
|
/* === Foreground (texto) — roxo deep do widget de referência === */
|
|
--vanna-foreground-default: #4a1a56;
|
|
--vanna-foreground-dimmer: #6b4673;
|
|
--vanna-foreground-dimmest: #8b7b6e;
|
|
|
|
/* === Backgrounds (cream warm pra ecoar fundo da logo + página) ===
|
|
* root/default ficam branco puro (legibilidade nas bolhas/tabelas);
|
|
* higher/subtle/lower puxam #F8EFE2 (cream do test.html) pra dar
|
|
* identidade em surfaces secundárias (sidebar, áreas de input,
|
|
* separadores).
|
|
*/
|
|
--vanna-background-root: #ffffff;
|
|
--vanna-background-default: #ffffff;
|
|
--vanna-background-higher: #f8efe2;
|
|
--vanna-background-highest: #f0e2c8;
|
|
--vanna-background-subtle: #fbf5ea;
|
|
--vanna-background-lower: #f8efe2;
|
|
|
|
/* === Outlines / borders — cream-tinted pro look warm + brand orange
|
|
* pro estado de hover/focus.
|
|
*/
|
|
--vanna-outline-default: rgba(244, 106, 31, 0.25);
|
|
--vanna-outline-dimmer: #eadfcb;
|
|
--vanna-outline-dimmest: #f3e8d2;
|
|
--vanna-outline-hover: #f46a1f;
|
|
|
|
/* === Typography === */
|
|
--vanna-font-family-default: "Open Sans", system-ui, -apple-system,
|
|
"Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
|
|
/* === Shape — bolhas um pouco menos arredondadas pra ar mais corporativo === */
|
|
--vanna-chat-bubble-radius: 14px;
|
|
--vanna-chat-bubble-radius-sm: 8px;
|
|
}
|
|
|
|
/* ============================================================================
|
|
* Containment fixes — impedem mensagem/tabela de estourar a largura do chat.
|
|
*
|
|
* Estes seletores casam elementos *dentro* dos shadow roots onde este sheet
|
|
* é adoptado (vanna-message, vanna-chat). Não dependem de :host.
|
|
* ========================================================================== */
|
|
|
|
/* Bolha de mensagem:
|
|
* 1) Texto puro mantém o limite original do upstream (~580px) — preserva
|
|
* a estética de bolha de chat. Só adicionamos min-width:0 + overflow-wrap
|
|
* pra texto longo quebrar dentro da bolha em vez de vazar.
|
|
* 2) Quando a mensagem do assistente tem tabela/chart embutido, ampliamos
|
|
* o max-width pra 100% — caso contrário a tabela empurraria a bolha
|
|
* além do limite (display:flex sem min-width:0 nos children).
|
|
*/
|
|
.message {
|
|
min-width: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
.message-content {
|
|
min-width: 0;
|
|
max-width: 100%;
|
|
overflow-wrap: anywhere;
|
|
word-break: break-word;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
/* Headers de markdown dentro do balão (### etc., gerados pelo mdToHtml
|
|
do embed-demo.html). Sem essas regras, o browser usa default que é
|
|
grande demais e quebra o ritmo do balão. */
|
|
.message-content h1,
|
|
.message-content h2,
|
|
.message-content h3,
|
|
.message-content h4,
|
|
.message-content h5,
|
|
.message-content h6 {
|
|
font-weight: 600;
|
|
line-height: 1.3;
|
|
margin: 0.7em 0 0.3em 0;
|
|
}
|
|
.message-content h1 { font-size: 1.25em; }
|
|
.message-content h2 { font-size: 1.15em; }
|
|
.message-content h3 { font-size: 1.05em; }
|
|
.message-content h4 { font-size: 1em; }
|
|
.message-content h5 { font-size: 0.95em; }
|
|
.message-content h6 { font-size: 0.9em; opacity: 0.85; }
|
|
|
|
/* Primeiro header sem margem-top — evita gap visível na borda do balão. */
|
|
.message-content > h1:first-child,
|
|
.message-content > h2:first-child,
|
|
.message-content > h3:first-child,
|
|
.message-content > h4:first-child,
|
|
.message-content > h5:first-child,
|
|
.message-content > h6:first-child {
|
|
margin-top: 0;
|
|
}
|
|
|
|
/* Esconder o FAB nativo quando minimizado — substituímos por um CTA
|
|
custom (.vanna-cta) renderizado fora do shadow DOM em embed-demo.html
|
|
/ no app React. A regra :host(.minimized) é a única que vence o
|
|
posicionamento fixed do componente.
|
|
*
|
|
* overflow:hidden é cinto-de-segurança: o host fica 0x0 com overflow
|
|
* clippando o .chat-layout interno (que mantemos com dimensões naturais
|
|
* — vê regra abaixo), evitando o crash do ResizeObserver do Plotly.
|
|
*/
|
|
:host(.minimized) {
|
|
background: transparent !important;
|
|
box-shadow: none !important;
|
|
width: 0 !important;
|
|
height: 0 !important;
|
|
overflow: hidden !important;
|
|
pointer-events: none !important;
|
|
}
|
|
:host(.minimized) .minimized-icon {
|
|
display: none !important;
|
|
}
|
|
|
|
/* Plotly ResizeObserver crash defense. Upstream (vanna-chat.ts:103-105)
|
|
* faz `:host(.minimized) .chat-layout { display: none }` quando o chat
|
|
* minimiza. Plotly tem um ResizeObserver no container do chart que dispara
|
|
* `_t [as relayout]` em cada mutação de tamanho; quando o container vai
|
|
* pra display:none, o `gd._fullLayout` interno fica undefined e o relayout
|
|
* quebra com `TypeError: Cannot read properties of undefined (reading
|
|
* 'width')` (kt at vanna-components.js:22888).
|
|
*
|
|
* Mantemos display:grid (cancela o display:none upstream) + dimensões
|
|
* naturais — o host já é 0x0 com overflow:hidden, então o chat-layout
|
|
* é clippado visualmente sem precisar mudar suas próprias dimensões.
|
|
* Plotly não vê o container colapsar pra 0x0 e o ResizeObserver fica
|
|
* quieto. pointer-events:none impede interação acidental.
|
|
*/
|
|
:host(.minimized) .chat-layout {
|
|
display: grid !important;
|
|
pointer-events: none !important;
|
|
}
|
|
|
|
/* CTA abre direto em maximized; estado intermediário "normal" não é
|
|
exposto ao usuário. Esconde os botões de restore (que voltariam de
|
|
maximized → normal) e maximize. Sobra só o .minimize, que dispara
|
|
windowState='minimized' e o CTA volta a aparecer. */
|
|
.window-control-btn.restore,
|
|
.window-control-btn.maximize {
|
|
display: none !important;
|
|
}
|
|
|
|
.message.assistant:has(.rich-dataframe, .rich-component, .plotly-chart) {
|
|
max-width: 100%;
|
|
width: 100%;
|
|
}
|
|
|
|
/* Tabela de resultados (rich-dataframe):
|
|
* - .dataframe-table-container já tem overflow:auto, mas falta limitar
|
|
* o max-width do rich-component (wrapper) ao tamanho da bolha.
|
|
* - Forçando 100% + box-sizing, a tabela com N colunas ganha scroll
|
|
* horizontal interno em vez de empurrar a bolha.
|
|
*/
|
|
.rich-component,
|
|
.rich-dataframe,
|
|
.dataframe-table-container {
|
|
max-width: 100%;
|
|
width: 100%;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
.dataframe-table-container {
|
|
overflow-x: auto;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
/* Cabeçalhos longos quebram em duas linhas em vez de forçar coluna larga;
|
|
* células de dado não quebram (preserva alinhamento numérico). */
|
|
.dataframe-table th {
|
|
white-space: normal;
|
|
overflow-wrap: anywhere;
|
|
}
|
|
|
|
.dataframe-table td {
|
|
white-space: nowrap;
|
|
}
|
|
|
|
/* Avatar do header — substitui as iniciais "AV" pela logo ClubPetro.
|
|
* Upstream renderiza <div class="chat-avatar">${initials}</div> num shadow
|
|
* root; este sheet é adotado lá dentro via attachShadow patch (ver
|
|
* embed-demo.html), então o seletor de classe vence o :host upstream em
|
|
* specificity igual + ordem (adotado vai por último). font-size:0 esconde
|
|
* as iniciais sem mudar o layout grid 44x44 do upstream.
|
|
*/
|
|
.chat-avatar {
|
|
background-color: transparent;
|
|
background-image: url(/clubpetro-logo.svg);
|
|
background-repeat: no-repeat;
|
|
background-size: 100%;
|
|
background-position: center;
|
|
backdrop-filter: none;
|
|
border: none;
|
|
overflow: visible;
|
|
font-size: 0;
|
|
color: transparent;
|
|
}
|
|
|
|
/* Bolha de mensagem do usuário — laranja brand sólido (sem gradient).
|
|
* Cor única alinhada ao #F46A1F do widget de referência. Texto branco
|
|
* mantém contraste suficiente em semibold (≈3.4:1 WCAG AA Large).
|
|
*
|
|
* align-self: flex-end + width: fit-content fazem a bolha encolher ao
|
|
* tamanho do conteúdo (em vez de esticar até o max-width default do
|
|
* upstream). O parent .chat-messages é flex-direction:column com
|
|
* align-items:stretch implícito, então sem o override a bolha enche
|
|
* a coluna inteira mesmo com texto curto.
|
|
*
|
|
* max-width replica o do upstream (vanna-message.ts:57). Necessário
|
|
* porque `width: fit-content` sozinho pode empurrar a bolha além do
|
|
* limite quando o conteúdo tem strings incompressíveis (URL longa,
|
|
* palavra grudada) — overflow-wrap: anywhere já está em .message-content
|
|
* mas o cap explícito é cinto + suspensório. min-width: 0 garante que
|
|
* flex children obedeçam ao max-width.
|
|
*/
|
|
.message.user {
|
|
background: #f46a1f;
|
|
border-color: rgba(255, 255, 255, 0.18);
|
|
align-self: flex-end;
|
|
width: fit-content;
|
|
max-width: min(80%, 500px);
|
|
min-width: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
:host([theme="dark"]) .message.user {
|
|
background: #f46a1f;
|
|
}
|
|
|
|
/* ============================================================================
|
|
* Header — cream bg, roxo deep no título, laranja no subtítulo.
|
|
*
|
|
* Upstream renderiza o header com gradient laranja cheio + overlay
|
|
* radial branco e texto branco. Aqui invertemos: fundo cream #F8EFE2,
|
|
* título em roxo #4A1A56 e subtítulo injetado via ::after no .header-text
|
|
* (upstream tem property `subtitle` mas nunca renderiza no template —
|
|
* pseudo-element é mais simples que rebuild).
|
|
* ========================================================================== */
|
|
.chat-header {
|
|
background: #f8efe2 !important;
|
|
border-bottom: 1px solid #eadfcb !important;
|
|
color: #4a1a56 !important;
|
|
}
|
|
|
|
.chat-header::before {
|
|
display: none;
|
|
}
|
|
|
|
.chat-title {
|
|
color: #4a1a56 !important;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.header-text {
|
|
gap: 2px;
|
|
}
|
|
|
|
.header-text::after {
|
|
content: "inteligência para seu posto";
|
|
color: #f46a1f;
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
letter-spacing: 0.01em;
|
|
line-height: 1.2;
|
|
}
|
|
|
|
/* Window control buttons (refresh / minimize / maximize / X) — repintar
|
|
* com tinta roxa pra contrastar com o cream em vez do laranja original.
|
|
*
|
|
* .header-top-actions / .window-controls precisam de flex-shrink:0 porque
|
|
* upstream (vanna-chat.ts:204-209) só seta `margin-left:auto` no actions,
|
|
* sem proteção de shrink. Em larguras justas (chat ocupando metade da
|
|
* viewport via :host(.maximized){left:50vw} + título/avatar consumindo o
|
|
* .header-left flex:1) o squeeze do flexbox comprime os botões a 0 e o
|
|
* minimize some. Travar shrink mantém os 32x32 sempre.
|
|
*
|
|
* .window-control-btn ganha `flex-shrink:0` pelo mesmo motivo + width
|
|
* explícito no shadow (upstream põe width:32px sem flex-basis, então em
|
|
* algumas combinações o shrink ainda toca).
|
|
*/
|
|
.header-top-actions,
|
|
.window-controls {
|
|
flex-shrink: 0 !important;
|
|
}
|
|
|
|
.window-control-btn {
|
|
background: rgba(74, 26, 86, 0.05) !important;
|
|
border-color: rgba(74, 26, 86, 0.1) !important;
|
|
color: #4a1a56 !important;
|
|
flex-shrink: 0 !important;
|
|
}
|
|
|
|
.window-control-btn:hover {
|
|
background: rgba(74, 26, 86, 0.12) !important;
|
|
}
|
|
|
|
/* Maximizado ocupa só a metade direita da tela em vez da viewport inteira.
|
|
* Upstream (vanna-chat.ts:62-75) usa top/left/right/bottom = space-6 (24px),
|
|
* cobrindo full-screen com margem. Override de `left` pra 50vw mantém a
|
|
* sensação de "side panel" — usuário ainda vê o app por trás à esquerda.
|
|
*
|
|
* overflow:hidden no host é cinto-de-segurança contra qualquer descendente
|
|
* (bolha, chart, tabela) que rompa as constraints internas — nada visível
|
|
* vaza além das bordas arredondadas.
|
|
*/
|
|
:host(.maximized) {
|
|
left: 50vw !important;
|
|
overflow: hidden !important;
|
|
}
|
|
|
|
/* Containment do .chat-main — é filho direto do grid .chat-layout
|
|
* (1fr quando .compact). min-width:0 + overflow:hidden impedem que um
|
|
* descendente largo (ex.: tabela com muitas colunas, chart Plotly que
|
|
* recalculou layout estranho) empurre o próprio chat-main além da
|
|
* coluna do grid, o que arrastaria o header e cortaria os botões.
|
|
*/
|
|
.chat-main {
|
|
min-width: 0 !important;
|
|
overflow: hidden !important;
|
|
}
|
|
|
|
/* Spinner do status bar — repintar de teal upstream pra roxo da marca.
|
|
* Upstream (vanna-status-bar.ts:190-198) usa border teal #15A8A8 + glow
|
|
* teal via @keyframes spinnerGlow. Como o sheet é adotado em todo shadow
|
|
* root (incluindo o do <vanna-status-bar>), tanto a regra de border
|
|
* quanto o redefine do @keyframes vencem o upstream (mesma especificity,
|
|
* adotado depois).
|
|
*/
|
|
.spinner {
|
|
border-color: rgba(74, 26, 86, 0.18) !important;
|
|
border-top-color: #4a1a56 !important;
|
|
}
|
|
|
|
@keyframes spinnerGlow {
|
|
0%, 100% {
|
|
filter: drop-shadow(0 0 2px rgba(74, 26, 86, 0.45));
|
|
}
|
|
50% {
|
|
filter: drop-shadow(0 0 6px rgba(74, 26, 86, 0.75));
|
|
}
|
|
}
|