Skip to main content
Glama
northernvariables

FedMCP - Federal Parliamentary Information

ChatWidget.tsx9.94 kB
/** * ChatWidget Component - Main Floating Chat Interface * * Features: * - Floating bottom-right widget * - Keyboard shortcut (Cmd/Ctrl + K) * - Collapsed/Expanded states with animations * - Integrates all chat components * - Persists across navigation */ 'use client'; import React from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { X, HelpCircle, ExternalLink, Maximize2, Minimize2 } from 'lucide-react'; import { MapleLeafIcon } from '@canadagpt/design-system'; import { useChatOpen, useChatQuota, useChatExpanded } from '@/lib/stores/chatStore'; import { useChatStore } from '@/lib/stores/chatStore'; import { useUserPreferences } from '@/contexts/UserPreferencesContext'; import { useAuth } from '@/contexts/AuthContext'; import { ChatHistory } from './ChatHistory'; import { ChatInput } from './ChatInput'; import { ChatSuggestions } from './ChatSuggestions'; import { QuotaDisplay } from './QuotaDisplay'; import { ChatHelp } from './ChatHelp'; import { ChatError } from './ChatError'; export function ChatWidget() { const [isOpen, toggleOpen] = useChatOpen(); const [isExpanded, toggleExpanded] = useChatExpanded(); const [showHelp, setShowHelp] = React.useState(false); const { checkQuota, refreshUsageStats} = useChatQuota(); const { preferences, updatePreferences } = useUserPreferences(); const { user } = useAuth(); const { addMessage, messages, conversation, createConversation } = useChatStore(); const [hasShownWelcome, setHasShownWelcome] = React.useState(false); const [hasInitialized, setHasInitialized] = React.useState(false); // Handle pop-out to separate window const handlePopOut = () => { const width = 600; const height = 700; const left = window.screen.width - width - 50; const top = 50; window.open( '/chat/window', 'ChatWindow', `width=${width},height=${height},left=${left},top=${top},resizable=yes,scrollbars=no` ); // Close the widget when popping out toggleOpen(); }; // Initialize quota check on mount React.useEffect(() => { if (!user) return; // Skip if not authenticated checkQuota(); refreshUsageStats(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [user]); // checkQuota and refreshUsageStats are stable Zustand store functions // Welcome flow for first-time users - DISABLED (auto-open removed) React.useEffect(() => { const showWelcome = async () => { // Only mark as seen if user is logged in and hasn't seen it if (user && preferences && !preferences.has_seen_welcome && !hasShownWelcome) { // Mark as shown immediately to prevent re-triggering setHasShownWelcome(true); // Mark as seen in preferences (without auto-opening) await updatePreferences({ has_seen_welcome: true }); } }; showWelcome(); }, [user, preferences, hasShownWelcome, updatePreferences]); // Auto-open in sidebar mode on desktop (one-time initialization) React.useEffect(() => { if (!user || hasInitialized) return; // Detect desktop (screen width >= 1024px) const isDesktop = typeof window !== 'undefined' && window.innerWidth >= 1024; if (isDesktop) { // Get current store state const currentState = useChatStore.getState(); // Only auto-open if not already open/expanded (respect user's previous state) if (!currentState.isOpen && !currentState.isExpanded) { // Open chat in sidebar mode if (!currentState.isOpen) { toggleOpen(); } if (!currentState.isExpanded) { toggleExpanded(); } } } setHasInitialized(true); }, [user, hasInitialized, toggleOpen, toggleExpanded]); // Keyboard shortcut: Cmd/Ctrl + K React.useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if ((e.metaKey || e.ctrlKey) && e.key === 'k') { e.preventDefault(); toggleOpen(); } // Escape to close if (e.key === 'Escape' && isOpen) { toggleOpen(); } }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, [isOpen, toggleOpen]); // Don't show chat widget for users who are not signed in if (!user) { return null; } return ( <> {/* Floating button (shows when closed) */} <AnimatePresence> {!isOpen && ( <motion.button initial={{ scale: 0, opacity: 0 }} animate={{ scale: 1, opacity: 1 }} exit={{ scale: 0, opacity: 0 }} transition={{ type: 'spring', stiffness: 260, damping: 20 }} onClick={toggleOpen} className="fixed bottom-6 right-6 z-50 w-14 h-14 bg-accent-red text-white rounded-full shadow-lg hover:bg-red-700 active:scale-95 transition-colors flex items-center justify-center group" title="Open chat (⌘K)" > <MapleLeafIcon className="w-[52px] h-[52px]" size={52} /> {/* Keyboard hint tooltip */} <div className="absolute bottom-full right-0 mb-2 px-2 py-1 bg-gray-900 text-white text-xs rounded opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap pointer-events-none"> Press ⌘K to open </div> </motion.button> )} </AnimatePresence> {/* Chat widget (shows when open) */} <AnimatePresence> {isOpen && ( <motion.div initial={{ opacity: 0, scale: 0.95, y: 20 }} animate={{ opacity: 1, scale: 1, y: 0 }} exit={{ opacity: 0, scale: 0.95, y: 20 }} transition={{ type: 'spring', stiffness: 260, damping: 20 }} className="z-50 bg-gray-900 bg-opacity-30 backdrop-blur-md rounded-lg shadow-2xl border border-gray-700 border-opacity-30 overflow-hidden flex flex-col relative pointer-events-auto transition-all duration-300" style={{ position: 'fixed', bottom: isExpanded ? '0' : '24px', right: isExpanded ? '0' : '24px', top: isExpanded ? '0' : 'auto', width: isExpanded ? '25vw' : '600px', height: isExpanded ? '100vh' : '500px', maxHeight: isExpanded ? '100vh' : 'calc(100vh - 100px)', maxWidth: isExpanded ? '25vw' : 'calc(100vw - 100px)', borderRadius: isExpanded ? '0' : undefined, }} > {/* Content */} {( <div className="flex flex-col h-full"> {/* Error display (fixed position) */} <ChatError /> {/* Quota display */} <div className="flex-shrink-0"> <QuotaDisplay /> </div> {/* Chat history - takes remaining space */} <ChatHistory /> {/* Bottom section: suggestions + input + buttons */} <div className="flex-shrink-0"> {/* Suggested prompts */} <ChatSuggestions /> {/* Input */} <ChatInput /> {/* Control buttons */} <div className="flex items-center justify-between gap-2 px-4 py-3 border-t border-gray-700 bg-gray-900"> {/* Left side buttons */} <div className="flex items-center gap-2"> {/* Help button - just question mark */} <button onClick={(e) => { e.stopPropagation(); setShowHelp(true); }} className="p-2 hover:bg-gray-700 text-gray-300 hover:text-white rounded-lg transition-colors" title="Show help" > <HelpCircle className="w-4 h-4" /> </button> {/* Pop-out button */} <button onClick={(e) => { e.stopPropagation(); handlePopOut(); }} className="p-2 hover:bg-gray-700 text-gray-300 hover:text-white rounded-lg transition-colors" title="Pop out to window" > <ExternalLink className="w-4 h-4" /> </button> {/* Expand button */} <button onClick={(e) => { e.stopPropagation(); toggleExpanded(); }} className="p-2 hover:bg-gray-700 text-gray-300 hover:text-white rounded-lg transition-colors" title={isExpanded ? 'Collapse' : 'Expand'} > {isExpanded ? ( <Minimize2 className="w-4 h-4" /> ) : ( <Maximize2 className="w-4 h-4" /> )} </button> </div> {/* Close button */} <button onClick={(e) => { e.stopPropagation(); toggleOpen(); }} className="p-2 hover:bg-gray-700 text-gray-300 hover:text-white rounded-lg transition-colors" title="Close (Esc)" > <X className="w-4 h-4" /> </button> </div> </div> </div> )} </motion.div> )} </AnimatePresence> {/* Help modal */} {showHelp && <ChatHelp onClose={() => setShowHelp(false)} />} </> ); }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/northernvariables/FedMCP'

If you have feedback or need assistance with the MCP directory API, please join our Discord server