Skip to content

GeoNews: floating bubble chat UI #21

Description

@sshahriar

Improve GeoNews chat UI: floating bubble + smooth overlay

Summary

The AI analyst is currently a docked strip under the intel drawer. It steals vertical space from events, messages are plain You: / Analyst: paragraphs in a max-h-36 scroller, and collapse is a crude / header. It does not feel like a modern chat product.

Redesign chat as a floating bubble over the map: a compact FAB when closed, a smooth expanding panel when open. Keep every existing chat behavior (send, brief, mock badge, errors, retry, place-scoped context). This is frontend UI only — no API, ingest, or LLM contract changes.

Related: #20 light/dark theme — the new chrome must use CSS variables so both themes look correct.


Why this is worth doing

GeoNews is map-first (plan.md §1, §13). The map and intel drawer are the primary surfaces. Chat is a helper, not a second column of equal weight.

Today:

  1. The right column is EventDrawer + AiPanel stacked. On desktop the drawer is squeezed; on tablet (h-[42vh]) chat and events fight for a short strip.
  2. AiPanel starts open (useState(true)), so first paint always shrinks the event list.
  3. The transcript is not chat UI — color-coded paragraphs, no bubbles, no timestamps, no auto-scroll.
  4. Loading is the text Thinking…. No pulse on a launcher, no skeleton, no send-state.
  5. The composer is a single-row textarea + Send with no Enter-to-send, no suggestion chips.

The product should feel like an OSINT workstation with a quiet analyst you can summon, not a chat app glued under a sidebar.


Current state (code)

Piece Today
Layout GeoNewsApp.tsx: right column lg:w-[360px] xl:w-[400px] contains drawer (flex-1) then <AiPanel />
Component frontend/src/components/AiPanel.tsx — collapsible <section data-testid="ai-panel">
Closed state Title still occupies a header row; body unmounts
Messages <p> with You: / Analyst: labels, max-h-36
Brief Card inside the same cramped column
Mock Dashed ai-mock-badge banner
Error ai-error + ai-error-retry
Input data-testid="ai-input" textarea, submit button named Send
Tests AiPanel.test.tsx, GeoNewsApp.place-chat.test.tsx, Playwright uses ai-input

Do not regress those testids or the mock/error/retry copy. Restyle around them.


Target UX

Closed — floating bubble (FAB)

  • Circular (or squircle) button floating over the map, not inside the intel drawer.
  • Default position: bottom-right of the map canvas, with enough margin that it does not sit under Leaflet attribution or the drawer edge.
    • Desktop: over the map, ~24px from the map’s bottom and from the drawer’s left edge (so it reads as “on the map”).
    • If that collides with attribution, sit just above the attribution control.
    • Tablet / stacked layout: bottom-right of the viewport, above the drawer, with safe-area padding.
  • Icon: analyst / spark / chat (inline SVG, no new icon font). Accent fill using var(--accent).
  • Accessible name: Open AI analyst / Close AI analyst.
  • data-testid="ai-fab" on the launcher (new). data-testid="ai-panel" stays on the expanded surface.
  • Default closed on first visit (map-first). Optional persist geonews.chatOpen in localStorage so a user who left it open gets it open again — nice-to-have, not required.
  • Unread / activity: while loading, the FAB shows a soft pulse / spinner. After a new assistant message while closed, a small unread dot until opened.
  • z-index above the map (pins, clusters, popups) but below any modal we do not have yet; popups should still be clickable when chat is closed.

Open — floating panel

A card that springs out of the bubble (scale + fade + slight translate, 180–240ms, ease-out, respect prefers-reduced-motion).

Suggested frame:

  • Width: min(380px, calc(100vw - 24px))
  • Height: min(560px, 70vh) (taller than today’s max-h-36 transcript)
  • Shape: rounded-2xl, border var(--border), fill var(--panel) with backdrop blur, shadow using a --shadow token (see GeoNews: implement light mode / dark mode #20)
  • Layout inside:
    1. Header — “AI analyst”, current place name (from chat context / focus), close (X) + optional collapse-to-bubble
    2. Brief place action (keep existing button)
    3. Mock badge / error / retry (same testids, tighter visual)
    4. Optional brief card (ai-brief) as a compact pinned block at the top of the scroll area, or as the first assistant message — either is fine if tests still find ai-brief
    5. Transcript — flex-1, overflow-y auto, auto-scroll to latest on send and on reply
    6. Composer — sticky bottom: textarea + send

Do not cover the entire map. The user must still pan the map and use the intel drawer while chat is open.

Close via: FAB toggle, header X, Escape. Do not close on map click (map interaction would fight the overlay). Backdrop is optional and should be non-blocking (pointer-events none) if used for dimming; prefer no dim so the situation room stays visible.

Message bubbles

Replace labeled paragraphs with real bubbles:

Role Alignment Style
User right Accent-tinted fill (var(--accent)), dark/light-aware text
Analyst left var(--panel-2) fill, var(--text)
System / empty centered muted “Ask for a brief, hotspot summary, or watchlist suggestion.”
  • Wrap long URLs; preserve newlines from the model.
  • Loading: typing indicator (three dots) as an analyst placeholder, not the word Thinking… alone. Keep a visually hidden or test-friendly loading state so tests can still wait on loading.
  • After send: user bubble appears immediately (already true in GeoNewsApp — keep that).
  • Suggestion chips above the composer when messages.length === 0, e.g. Brief this place, What should I watch?, Summarize risk. Clicking a chip sends that string via onSend. Do not invent new API actions.

Composer

  • ai-input remains the textarea.
  • Enter sends (Shift+Enter newline).
  • Send button: icon or “Send”, still a button with accessible name Send so AiPanel.test.tsx (getByRole("button", { name: "Send" })) keeps working.
  • Disable send while loading or empty (already done).
  • Focus the input when the panel opens.

Motion

  • Open: opacity 0→1, transform: translateY(8px) scale(0.96) → identity, ~200ms.
  • Close: reverse, then unmount or hidden after transition.
  • FAB: hover scale 1.05, active 0.96; loading pulse on the ring.
  • New bubbles: short fade/slide (120ms).
  • @media (prefers-reduced-motion: reduce): no transform, instant show/hide.

Layout change in GeoNewsApp

  1. Remove <AiPanel /> from the right-column stack so intel drawer uses the full column height.
  2. Render the chat widget as a sibling overlay inside the map pane (relative wrapper already on the map column) or as a page-level portal (fixed) with coordinates that avoid the drawer.
  3. Keep passing the same props: brief, messages, loading, mockMode, error, onSend, onLoadBrief, onRetry.
  4. Place name in the chat header should reflect current focus (e.g. Dhaka / selected search). Pass placeName if needed — display only, no extra fetch.

The intel drawer must not shrink when chat is open.


Behavior that must not change

These were hard-won in planning/FRONTEND_PLACE_CHAT_HANDOFF.md. Restyle only.

  • Mock badge whenever health llm: mock or response mock: true (ai-mock-badge, OpenRouter / LLM_MOCK=false copy).
  • Mock chat still sends and still shows briefs.
  • postChat / getBrief failures → ai-error + ai-error-retry. Retry does not duplicate the user message.
  • Place ingest, watchlist, map, filters, SSE — untouched.
  • No new backend routes. POST /api/chat and GET /api/brief stay as they are.
  • Category pins, heatmap, and leaflet popups still work with the overlay z-index.
  • Frontend never reads API keys.

Suggested component split

Keep AiPanel as the public component (tests import it) or split internally:

File Role
frontend/src/components/AiPanel.tsx Orchestrates open/close; renders FAB + panel; preserves props and testids
frontend/src/components/ChatBubble.tsx (optional) One message
frontend/src/components/ChatComposer.tsx (optional) Form + Enter-to-send
frontend/src/components/GeoNewsApp.tsx Unstack from drawer; overlay placement; pass placeName
frontend/src/app/globals.css Chat-specific tokens (bubble radius, shadow, FAB size) using existing CSS variables

Do not add a UI library. Tailwind + CSS variables only.


Theme

Use var(--bg), --panel, --panel-2, --border, --text, --text-muted, --accent, --alert, --warning. No hardcoded #0b1220 / #a9b6cd. If #20 lands first, chat must look correct in light mode (FAB contrast on light Carto tiles). If #20 has not landed, still use variables so the later theme work is free.

Light-mode FAB on a pale map: solid panel fill + border, not a faint ghost.


Accessibility

  • FAB is a real <button>.
  • Expanded panel: role="dialog" or role="complementary" with aria-label="AI analyst". Prefer complementary if we do not want focus trap (map must stay usable). If dialog, do not trap focus — the map and drawer stay interactive.
  • aria-expanded on the FAB.
  • Focus input on open; restore focus to FAB on close.
  • Escape closes.
  • Contrast: analyst bubbles and muted empty-state text meet WCAG AA on both themes.
  • Typing indicator has aria-live="polite".

Files to touch (expected)

File Change
frontend/src/components/AiPanel.tsx FAB + overlay panel, bubbles, motion, Enter-to-send, default closed
frontend/src/components/GeoNewsApp.tsx Move chat out of drawer stack; overlay on map column
frontend/src/app/globals.css Optional chat animation / reduced-motion helpers
frontend/src/__tests__/AiPanel.test.tsx Open panel before typing if default is closed; FAB toggle
frontend/src/__tests__/GeoNewsApp.place-chat.test.tsx Same — open chat if needed; selectors stay
planning/FRONTEND_HANDOFF.md Document ai-fab, overlay layout
plan.md §13 “AI panel” → floating analyst bubble + overlay chat

Playwright (test/e2e/geonews.spec.ts) currently does not drive chat deeply; if a spec focuses ai-input, open the FAB first. Integration Tester owns E2E — frontend unit tests must still pass.


Testing

Unit (Vitest)

  • Default: FAB visible, transcript/composer not blocking the drawer (panel closed). ai-input may be in the DOM but hidden — tests that type into ai-input should click ai-fab first or keep ai-input always mounted and visually hidden when closed (prefer always mounted if it simplifies tests).
  • Click FAB → panel visible (ai-panel), aria-expanded=true, input focused.
  • Click FAB / X / Escape → closed.
  • Existing: mock badge copy, mock send via Send, error + retry.
  • Suggestion chip (if implemented) calls onSend with the chip string.
  • Enter sends; Shift+Enter does not.

Playwright (add or extend if Integration Tester is in the loop)

  1. Map and intel drawer visible with chat closed (more drawer height than today).
  2. Click ai-fabai-input visible.
  3. Optional: send “Brief this place” under LLM_MOCK still yields ai-brief (plan §17 already expects this mock path).

Existing map/search/drawer E2E must pass with the FAB present.

Manual visual QA

  • FAB does not cover Leaflet attribution, zoom (if any), or the last event card
  • Open panel does not cover place search / filters
  • Drawer scrolls fully with chat open
  • Map pan/zoom works with chat open
  • Pin popup still clickable when chat is closed
  • Long assistant message scrolls; new reply auto-scrolls
  • Loading pulse on FAB + typing dots in transcript
  • Reduced motion: no janky scale
  • Tablet stacked layout: FAB above the 42vh drawer
  • Light and dark (if GeoNews: implement light mode / dark mode #20 is in) both readable

Acceptance criteria

  • Chat launches from a floating bubble over the map; default closed so the intel drawer gets the full right column.
  • Opening/closing is animated and feels smooth; reduced-motion honored.
  • Open state is a floating card (not a docked footer): bubbles, header, brief, composer.
  • User/analyst messages are visually distinct bubbles; transcript auto-scrolls.
  • Enter sends; Send button accessible name remains Send.
  • data-testids preserved: ai-panel, ai-input, ai-brief, ai-mock-badge, ai-error, ai-error-retry. New: ai-fab.
  • Mock badge, live/mock send, error + retry behavior unchanged.
  • Map, drawer, filters, watchlist remain fully usable with chat open.
  • CSS variables only (theme-ready). No backend/env/secret changes.
  • Vitest suite green; existing E2E still pass.

Out of scope

  • Streaming tokens / SSE for chat tokens (events SSE is unrelated)
  • Voice input, attachments, markdown mermaid, syntax highlighting
  • Chat history persisted in SQLite UI (backend already stores chat_messages; no history browser required here)
  • Changing POST /api/chat schema, prompts, or OpenRouter wiring (#19 is the live-LLM card)
  • Mobile bottom-nav redesign
  • Drag-to-reposition the FAB (nice later, not this card)

Context / pointers

  • UI today: GeoNews/frontend/src/components/AiPanel.tsx
  • Layout: GeoNews/frontend/src/components/GeoNewsApp.tsx (right column ~L327–347)
  • Honest mock/error contract: GeoNews/planning/FRONTEND_PLACE_CHAT_HANDOFF.md
  • Plan: GeoNews/plan.md §13 “AI panel — collapsible chat”
  • Tests: frontend/src/__tests__/AiPanel.test.tsx, GeoNewsApp.place-chat.test.tsx
  • Theme tokens: frontend/src/app/globals.css (and issue GeoNews: implement light mode / dark mode #20)

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions