teqo/src/components/campaign/shell/ai/CampaignAISidebarContext.tsx
Francisco Ignácio Fontoura Solla f9cd1df7b2 feat: AI chat 'Sollinha' — campaign data assistant (v1 read-only)
- Streaming AI endpoint at /campanha/api/ai-chat with DeepSeek V4 Flash
- 8 tools: calculate, getMunicipalityVotes, getTopDeputies, getDobradinhas,
  getMunicipalityOverview, getLeaderships, getOrganizations, searchEntities
- In-memory rate limiter (50 msgs / 15 min per user)
- Desktop: resizable right sidebar via react-resizable-panels
- Mobile: full-screen Drawer opened by FAB (2/3 size of quick-actions FAB)
- Markdown rendering with react-markdown + remark-gfm (tables, bold, lists)
- Session-bound context (clean slate each open)
- Auth via existing campaign-token cookie, tools respect user RBAC
- Architecture doc: docs/plans/ai-chat-sollinha.md
- E2E tests: campaignAiChat.e2e.spec.ts (mocked endpoint)
2026-08-04 06:17:31 -03:00

64 lines
1.6 KiB
TypeScript

'use client'
import {
createContext,
useCallback,
useContext,
useMemo,
useState,
type MutableRefObject,
type ReactNode,
} from 'react'
import type { PanelImperativeHandle } from 'react-resizable-panels'
type AISidebarContextValue = {
open: boolean
/** Opens/closes the desktop Panel AND toggles the mobile Drawer. */
setOpen: (open: boolean) => void
/** Opens the mobile Drawer only — does NOT expand the desktop Panel. */
setOpenMobile: (open: boolean) => void
/** Toggles the desktop Panel. */
toggle: () => void
}
const AISidebarContext = createContext<AISidebarContextValue | null>(null)
export const CampaignAISidebarProvider = ({
panelRef,
children,
}: {
panelRef: MutableRefObject<PanelImperativeHandle | null>
children: ReactNode
}) => {
const [open, setOpen] = useState(false)
const setOpenWithPanel = useCallback(
(next: boolean) => {
setOpen(next)
if (next) {
panelRef.current?.expand()
} else {
panelRef.current?.collapse()
}
},
[panelRef],
)
const setOpenMobile = useCallback((next: boolean) => {
setOpen(next)
// Don't touch the Panel — it's hidden on mobile
}, [])
const toggle = useCallback(() => setOpenWithPanel(!open), [open, setOpenWithPanel])
const value = useMemo(
() => ({ open, setOpen: setOpenWithPanel, setOpenMobile, toggle }),
[open, setOpenWithPanel, setOpenMobile, toggle],
)
return <AISidebarContext.Provider value={value}>{children}</AISidebarContext.Provider>
}
export const useAISidebar = (): AISidebarContextValue | null => {
return useContext(AISidebarContext)
}