mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-15 10:43:07 +00:00
- Converse (useChat) lifts to the provider: survives desktop panel <-> mobile drawer swaps, internal /campanha navigation and resize (new tab still resets). - Chat Panel/Separator always mounted (RRP panel-count change forces a full navigation); footprint hidden via elementRef + .b167-ai-chat-hidden (display:none !important), avoiding RRP's imperative collapse/expand races. - Single 'open' flag drives both surfaces (open && viewport); reconciled with the panel at desktop settle and on user drag-collapse (gated by isUserInteraction). - Mobile Drawer extracted to CampaignAIDrawer; FAB/setOpen unified; focus moves into the chat on the mobile->desktop swap. - e2e campaignAiChatResize: desktop->mobile, mobile->desktop, closed stays closed, fresh mobile visit closed, navigation survival, new tab resets. - Impl plan: docs/plans/migrar-chat-painel-drawer-resize-impl.md Closes #416
8.2 KiB
8.2 KiB
Impl: Migrar o chat Sollinha entre painel e drawer ao redimensionar
Status: aprovado — em execução (design final conforme entregue) Atualizado em: 2026-08-08 Issue: #416 Intenção: docs/plans/migrar-chat-painel-drawer-resize.md Appetite restante: herdado (~1–1,5 dia eng; sem migration)
Leitura da intenção
- Outcome: ao cruzar a borda 768 px com o chat aberto, o chat migra aberto para a outra superfície (painel↔drawer) com a conversa atual; fechado permanece fechado; a conversa dura a sessão do site (navegação interna, abrir/fechar, resize) e reinicia em nova aba.
- O que NÃO negociar: não fechar um chat aberto; não abrir um chat fechado;
opené a fonte da verdade (sem flag nova); sem histórico entre sessões (LGPD); abrir/fechar em tela fixa como hoje (painel desktop continua abrindo em 25% no load); sem tocar a largura default (irmão B166); sem migration/collection/Consent. - O que foi reavaliado (tudo reproduzido em dev + e2e):
useChatsem store porid: em@ai-sdk/react@4.0.50as mensagens vivem na instânciaChat—idestável NÃO sobrevive a desmonte. A conversa foi erguida para o provider (persiste no layout(app)), eCampaignAIChatvirou consumidor.react-resizable-panels@4aplicadisplay:flexinline no elemento doPanele roteia className/style para o wrapper interno — ohidden md:blockantigo não zerava a pegada flex (coluna fantasma no mobile). Além disso, mudar a contagem de painéis doGroupem runtime força uma navegação full-page nesta app. Solução: panel+separator sempre montados; a pegada do panel é zerada por uma classe comdisplay:none !importantalternada viaelementRefno elemento OUTER (sobrevive aos re-renders async do RRP).collapse()/expand()imperativos do RRP são sobrepostos pelo próprio re-layout do RRP no cruzamento (raças). O design nunca os usa no caminho do cruzamento; só um drag-collapse do usuário (gated pormeta.isUserInteraction) fecha o chat.
Abordagem final entregue
flowchart LR
P[CampaignAISidebarProvider<br/>open · isMobile · measured · useChat · reconcile/sync] --> S[CampaignAISidebarShell]
S -->|desktop| G[Group<br/>Panel main + Separator + Panel chat sempre montados]
S -->|mobile| D[CampaignAIDrawer<br/>portal, fora do Group]
G --> C[CampaignAISidebar<br/>desktop surface]
D --> C2[CampaignAIDrawer + header]
C --> CH[CampaignAIChat<br/>consumidor do contexto]
C2 --> CH
Sem "migração" no cruzamento: as duas superfícies derivam do MESMO open + viewport (chatVisible = measured && !isMobile && open). Cruzar a borda só re-deriva a superfície. O trabalho é manter open fiel:
- Reconcile no settle desktop: o panel abre no default RRP (25%) com
open=false; no primeiro settle medido desktop,open := !panelRef.isCollapsed()— um chat visível é "aberto" (X fecha, drawer abre no resize). Visita mobile mantémopen=false(gated por!isMobile). - Drag-collapse:
Group.onLayoutChanged((_, meta) => sync(meta.isUserInteraction))— só o drag do usuário (isUserInteraction) fecha; o auto-collapse que o RRP faz no próprio re-layout (panel display:none mede 0) NÃO fecha o drawer recém-aberto no lado mobile. - Reopen pós-collapse: effect
[open, isMobile]expande seopen && !isMobile && isCollapsed()(no-op nos caminhos normais — sem raça). - Pré-medição:
chatVisible = measured ? … : true— o frame de hidratação não esconde o panel (evita o piscar 100%→75% no load desktop).
Componentes / mudanças
CampaignAISidebarContext.tsx: dono único —open,isMobile/measured(useIsMobileMeasured),useChaterguido (transport memoizado,id: 'campaign-sollinha'), reconcile no settle,syncPanelVisibility(isUserInteraction), effect de reopen. RemoveusetOpenMobile(unificado emsetOpen).CampaignAISidebarShell.tsx:Group/Separator/Panelchat sempre montados;elementRef+useLayoutEffectalterna a classeb167-ai-chat-hidden;CampaignAIDrawercomo irmão no mobile; foco para o textarea ao migrar mobile→desktop com chat aberto.src/app/(frontend)/styles.css: utility.b167-ai-chat-hidden { display:none !important }(vence o inlinedisplay:flexdo RRP).src/hooks/use-mobile.ts: refatorado —useMobileMeasured()interno +useIsMobile()(compat) +useIsMobileMeasured()(expoemeasured).CampaignAIDrawer.tsx(novo): surface mobile (Drawer + header + chat), portado do branch removido deCampaignAISidebar.CampaignAISidebar.tsx: só a surface desktop (header + chat).CampaignAIChat.tsx: consumidor do contexto (removeuseChatlocal e oviewportRefmorto).CampaignAIFab.tsx:setOpen(true)(erasetOpenMobile(true)).- Migration: nenhuma. Access/Consent/RBAC/cache: intocados.
Abordagens rejeitadas (registradas)
- Migração explícita por resolver puro (
resolveAISidebarOpenOnResize+ unit tests): descartada quando se provou que, comopenreconciliado, o cruzamento é automático (open && viewport). O resolver virou código morto e foi removido. - Panel condicional (desmonta no mobile): força navegação full-page do RRP nesta app.
display:nonevia className/style do RRP: vão no wrapper interno, não zeram a pegada.collapse()/expand()no cruzamento: sobrepostos pelo re-layout do RRP (raças).collapsibleno Panel do chat: o RRP auto-colapsa um paineldisplay:none(largura 0) durante o próprio re-layout, e no build de produção o painel voltava ao desktop colapsado (largura 0) sem nada para expandir de forma confiável. Removido — mostrar/esconder é 100% CSS; o painel sempre mantém um tamanho RRP real quando visível.
Dados → forma
- Apresento dados? Não (N/A — correção de estado de UI). Item 3 de data-presentation não se aplica.
Fases verificáveis
- Núcleo + surfaces — provider (open/isMobile/measured/useChat/reconcile/sync), shell montado-constante,
CampaignAIDrawer, chat consumidor, FAB unificado, utility CSS. - Verificação no navegador — 4 fluxos da intenção por viewport (1280↔500) em dev real.
- Gates + e2e — unit (full), lint/tsc/prettier/cycles; e2e
campaignAiChatResize.e2e.spec.tscom mock SSE: desktop→mobile (drawer + conversa + sem coluna), mobile→desktop (painel + conversa, sem reload), fechado-fica-fechado (2 direções), visita mobile nova fechada, conversa sobrevive à navegação interna, nova aba recomeça vazia. Entrega compnpm push.
Rabbit holes / Não escopo (engenharia)
- sessionStorage/localStorage p/ reload na mesma aba: reload não está no aceite; o provider cobre navegação interna/abrir-fechar/resize sem storage. Deferir (gatilho: produto pedir reload-persistência).
- Largura default / persistência de tamanho / drag handle: terreno do B166 (#414, in-progress separada) — aqui só migrar, não remodelar.
- Debounce de resize rápido: intenção resolveu "aceitar".
- Extrair
AISidebarHeadercompartilhado: 2 call sites com classes diferentes = módulo raso; mantido inline. - Foco/AT no drawer×painel: foco do textarea coberto no mobile→desktop; anúncio AT de troca de superfície deferido (gate) se o produto quiser.
Riscos e mitigação
- RRP + panel sempre montado + display:none: utility
!importantsobrevive aos re-renders async do RRP; auto-collapse do RO não fecha o chat (gated por isUserInteraction). Verificado em e2e. - Load desktop sem piscar 100%→75%:
chatVisiblecai paratruepré-medição. - Flakiness local (dev server 2 workers): conhecida no repo; CI roda e2e sobre o build de produção (
pnpm start), sem compilações a frio nem hard-nav — stável. - Foco ao migrar: foco vai para o textarea do painel no mobile→desktop com chat aberto.
Aceite de engenharia
- Aceite de produto da intenção coberto (migração aberto→aberto; fechado→fechado; conversa por sessão da aba; tela fixa intacta; visita mobile fecha)
- Invariantes AGENTS/engineering-standards (sem migration; sem Consent;
openfonte da verdade; identificadores EN / copy pt-BR; transações não tocadas) - Testes: e2e com mock SSE (6 cenários) + full unit/lint/tsc/prettier/cycles