You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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:
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.
AiPanel starts open (useState(true)), so first paint always shrinks the event list.
The transcript is not chat UI — color-coded paragraphs, no bubbles, no timestamps, no auto-scroll.
Loading is the text Thinking…. No pulse on a launcher, no skeleton, no send-state.
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 />
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)
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
Transcript — flex-1, overflow-y auto, auto-scroll to latest on send and on reply
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).
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
Remove <AiPanel /> from the right-column stack so intel drawer uses the full column height.
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.
Keep passing the same props: brief, messages, loading, mockMode, error, onSend, onLoadBrief, onRetry.
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
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"orrole="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
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.
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 amax-h-36scroller, 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:
EventDrawer+AiPanelstacked. On desktop the drawer is squeezed; on tablet (h-[42vh]) chat and events fight for a short strip.AiPanelstarts open (useState(true)), so first paint always shrinks the event list.Thinking…. No pulse on a launcher, no skeleton, no send-state.textarea+Sendwith 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)
GeoNewsApp.tsx: right columnlg:w-[360px] xl:w-[400px]contains drawer (flex-1) then<AiPanel />frontend/src/components/AiPanel.tsx— collapsible<section data-testid="ai-panel"><p>withYou:/Analyst:labels,max-h-36ai-mock-badgebannerai-error+ai-error-retrydata-testid="ai-input"textarea, submit button namedSendAiPanel.test.tsx,GeoNewsApp.place-chat.test.tsx, Playwright usesai-inputDo not regress those testids or the mock/error/retry copy. Restyle around them.
Target UX
Closed — floating bubble (FAB)
var(--accent).Open AI analyst/Close AI analyst.data-testid="ai-fab"on the launcher (new).data-testid="ai-panel"stays on the expanded surface.geonews.chatOpeninlocalStorageso a user who left it open gets it open again — nice-to-have, not required.loading, the FAB shows a soft pulse / spinner. After a new assistant message while closed, a small unread dot until opened.z-indexabove 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, respectprefers-reduced-motion).Suggested frame:
min(380px, calc(100vw - 24px))min(560px, 70vh)(taller than today’smax-h-36transcript)rounded-2xl,bordervar(--border), fillvar(--panel)with backdrop blur, shadow using a--shadowtoken (see GeoNews: implement light mode / dark mode #20)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 findai-briefDo 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:
var(--accent)), dark/light-aware textvar(--panel-2)fill,var(--text)Thinking…alone. Keep a visually hidden or test-friendly loading state so tests can still wait onloading.GeoNewsApp— keep that).messages.length === 0, e.g.Brief this place,What should I watch?,Summarize risk. Clicking a chip sends that string viaonSend. Do not invent new API actions.Composer
ai-inputremains the textarea.buttonwith accessible nameSendsoAiPanel.test.tsx(getByRole("button", { name: "Send" })) keeps working.loadingor empty (already done).Motion
opacity0→1,transform: translateY(8px) scale(0.96)→ identity, ~200ms.hiddenafter transition.@media (prefers-reduced-motion: reduce): no transform, instant show/hide.Layout change in
GeoNewsApp<AiPanel />from the right-column stack so intel drawer uses the full column height.relativewrapper already on the map column) or as a page-level portal (fixed) with coordinates that avoid the drawer.brief,messages,loading,mockMode,error,onSend,onLoadBrief,onRetry.placeNameif 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.llm: mockor responsemock: true(ai-mock-badge, OpenRouter /LLM_MOCK=falsecopy).postChat/getBrieffailures →ai-error+ai-error-retry. Retry does not duplicate the user message.POST /api/chatandGET /api/briefstay as they are.z-index.Suggested component split
Keep
AiPanelas the public component (tests import it) or split internally:frontend/src/components/AiPanel.tsxfrontend/src/components/ChatBubble.tsx(optional)frontend/src/components/ChatComposer.tsx(optional)frontend/src/components/GeoNewsApp.tsxplaceNamefrontend/src/app/globals.cssDo 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
<button>.role="dialog"orrole="complementary"witharia-label="AI analyst". Prefercomplementaryif we do not want focus trap (map must stay usable). Ifdialog, do not trap focus — the map and drawer stay interactive.aria-expandedon the FAB.Escapecloses.aria-live="polite".Files to touch (expected)
frontend/src/components/AiPanel.tsxfrontend/src/components/GeoNewsApp.tsxfrontend/src/app/globals.cssfrontend/src/__tests__/AiPanel.test.tsxfrontend/src/__tests__/GeoNewsApp.place-chat.test.tsxplanning/FRONTEND_HANDOFF.mdai-fab, overlay layoutplan.md§13Playwright (
test/e2e/geonews.spec.ts) currently does not drive chat deeply; if a spec focusesai-input, open the FAB first. Integration Tester owns E2E — frontend unit tests must still pass.Testing
Unit (Vitest)
ai-inputmay be in the DOM but hidden — tests that type intoai-inputshould clickai-fabfirst or keepai-inputalways mounted and visually hidden when closed (prefer always mounted if it simplifies tests).ai-panel),aria-expanded=true, input focused.Send, error + retry.onSendwith the chip string.Playwright (add or extend if Integration Tester is in the loop)
ai-fab→ai-inputvisible.LLM_MOCKstill yieldsai-brief(plan §17 already expects this mock path).Existing map/search/drawer E2E must pass with the FAB present.
Manual visual QA
Acceptance criteria
Send.data-testids preserved:ai-panel,ai-input,ai-brief,ai-mock-badge,ai-error,ai-error-retry. New:ai-fab.Out of scope
chat_messages; no history browser required here)POST /api/chatschema, prompts, or OpenRouter wiring (#19 is the live-LLM card)Context / pointers
GeoNews/frontend/src/components/AiPanel.tsxGeoNews/frontend/src/components/GeoNewsApp.tsx(right column ~L327–347)GeoNews/planning/FRONTEND_PLACE_CHAT_HANDOFF.mdGeoNews/plan.md§13 “AI panel — collapsible chat”frontend/src/__tests__/AiPanel.test.tsx,GeoNewsApp.place-chat.test.tsxfrontend/src/app/globals.css(and issue GeoNews: implement light mode / dark mode #20)