mirror of
https://github.com/BradGroux/veritas-kanban.git
synced 2026-10-09 05:07:53 +00:00
feat: floating chat bubble with pulse indicator
- Floating chat icon in bottom-right corner - Opens board-level chat (not tied to any task) - Pulsing green dot when new response arrives while closed - Hides bubble when chat panel is open - Clears unread indicator on open
This commit is contained in:
parent
89a5fd8b20
commit
974255bff3
2 changed files with 71 additions and 0 deletions
|
|
@ -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() {
|
|||
</main>
|
||||
<Toaster />
|
||||
<KeyboardShortcutsDialog />
|
||||
<FloatingChat />
|
||||
</div>
|
||||
</TaskConfigProvider>
|
||||
</BulkActionsProvider>
|
||||
|
|
|
|||
69
web/src/components/chat/FloatingChat.tsx
Normal file
69
web/src/components/chat/FloatingChat.tsx
Normal file
|
|
@ -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 */}
|
||||
<Button
|
||||
onClick={handleOpen}
|
||||
size="icon"
|
||||
className={cn(
|
||||
'fixed bottom-6 right-6 z-40 h-14 w-14 rounded-full shadow-lg',
|
||||
'bg-primary hover:bg-primary/90 text-primary-foreground',
|
||||
'transition-all duration-200 hover:scale-105',
|
||||
open && 'hidden'
|
||||
)}
|
||||
aria-label="Open chat"
|
||||
>
|
||||
<MessageSquare className="h-6 w-6" />
|
||||
{hasUnread && (
|
||||
<span className="absolute -top-1 -right-1 flex h-4 w-4">
|
||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75" />
|
||||
<span className="relative inline-flex rounded-full h-4 w-4 bg-emerald-500" />
|
||||
</span>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
{/* Chat panel — board-level (no taskId) */}
|
||||
<ChatPanel open={open} onOpenChange={setOpen} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue