import { useEffect, useMemo, useRef, useState } from 'react'; import { Bell, X } from 'lucide-react'; import type { Campaign } from '@/lib/schemas'; import { buildSignals } from '@/lib/assistant/signals'; import type { Suggestion } from '@/lib/assistant/advisors'; import { useActiveCampaign } from '@/features/campaigns/hooks'; import { useCharacters } from '@/features/characters/hooks'; import { useNotes, useQuests } from '@/features/world/hooks'; import { useEncounters } from '@/features/combat/hooks'; import { Button } from '@/components/ui/Button'; import { cn } from '@/lib/cn'; import { useSignalAction } from './useSignalAction'; const DOT: Record = { danger: 'bg-danger', warn: 'bg-warning', info: 'bg-info', }; /** * Ambient, app-wide "what needs my attention" surface. Watches the active * campaign's state and surfaces one-click signals from anywhere — the proactive * core of the assistant. Renders nothing without an active campaign. */ export function SignalsBell() { const campaign = useActiveCampaign(); if (!campaign) return null; return ; } function SignalsBellInner({ campaign }: { campaign: Campaign }) { const characters = useCharacters(campaign.id); const notes = useNotes(campaign.id); const quests = useQuests(campaign.id); const encounters = useEncounters(campaign.id); const activeEnc = encounters.find((e) => e.status === 'active'); const [dismissed, setDismissed] = useState>(new Set()); const [open, setOpen] = useState(false); const [msg, setMsg] = useState(null); const runAction = useSignalAction(campaign.id, setMsg); const ref = useRef(null); const signals = useMemo( () => buildSignals({ activeEncounter: activeEnc, characters, notes, quests }).filter((s) => !dismissed.has(s.id)), [activeEnc, characters, notes, quests, dismissed], ); // The badge counts only things that genuinely want attention. const attention = signals.filter((s) => s.severity !== 'info').length; // Close the popover on an outside click or Escape. useEffect(() => { if (!open) return; const onDown = (e: MouseEvent) => { if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false); }; const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') setOpen(false); }; document.addEventListener('mousedown', onDown); document.addEventListener('keydown', onKey); return () => { document.removeEventListener('mousedown', onDown); document.removeEventListener('keydown', onKey); }; }, [open]); return (
{open && (
Signals {signals.length > 0 && ( )}
{msg &&

{msg}

} {signals.length === 0 ? (

Nothing needs your attention.

) : (
    {signals.map((s) => (
  • {s.title}
    {s.detail &&
    {s.detail}
    } {s.action && ( )}
  • ))}
)}
)}
); }