diff --git a/web/src/App.tsx b/web/src/App.tsx index 7600d224..8c38490d 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -12,6 +12,7 @@ import { AuthGuard } from './components/auth'; import { ErrorBoundary } from './components/shared/ErrorBoundary'; import { SkipToContent } from './components/shared/SkipToContent'; import { LiveAnnouncerProvider } from './components/shared/LiveAnnouncer'; +import { FloatingChat } from './components/chat/FloatingChat'; // Main app content (only rendered when authenticated) function AppContent() { @@ -38,6 +39,7 @@ function AppContent() { + diff --git a/web/src/components/chat/FloatingChat.tsx b/web/src/components/chat/FloatingChat.tsx new file mode 100644 index 00000000..a2bbc1ab --- /dev/null +++ b/web/src/components/chat/FloatingChat.tsx @@ -0,0 +1,69 @@ +import { useState, useEffect } from 'react'; +import { Button } from '@/components/ui/button'; +import { MessageSquare } from 'lucide-react'; +import { ChatPanel } from './ChatPanel'; +import { chatEventTarget } from '@/hooks/useTaskSync'; +import { cn } from '@/lib/utils'; + +/** + * Floating chat bubble — bottom-right corner. + * Opens a board-level ChatPanel (no taskId). + * Pulses when a new response arrives while closed. + */ +export function FloatingChat() { + const [open, setOpen] = useState(false); + const [hasUnread, setHasUnread] = useState(false); + + // Listen for incoming chat messages when panel is closed + useEffect(() => { + const handler = (e: Event) => { + const msg = (e as CustomEvent).detail; + // Only pulse for board-level chat messages (no taskId in sessionId) + if ( + !open && + (msg.type === 'chat:message' || msg.type === 'chat:delta') && + msg.sessionId && + !msg.sessionId.includes('task_') + ) { + setHasUnread(true); + } + }; + + chatEventTarget.addEventListener('chat', handler); + return () => chatEventTarget.removeEventListener('chat', handler); + }, [open]); + + // Clear unread when opening + const handleOpen = () => { + setOpen(true); + setHasUnread(false); + }; + + return ( + <> + {/* Floating button */} + + + {/* Chat panel — board-level (no taskId) */} + + + ); +}