- 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)
64 lines
1.6 KiB
TypeScript
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)
|
|
}
|