import PdfSheet from '@/components/pdf-drawer'; import { useClickDrawer } from '@/components/pdf-drawer/hooks'; import { MessageType, SharedFrom } from '@/constants/chat'; import { useFetchExternalAgentInputs } from '@/hooks/use-agent-request'; import { useFetchExternalChatInfo } from '@/hooks/use-chat-request'; import i18n from '@/locales/config'; import { useSendNextSharedMessage } from '@/pages/agent/hooks/use-send-shared-message'; import { MessageCircle, Minimize2, Send, X } from 'lucide-react'; import React, { useCallback, useEffect, useRef, useState } from 'react'; import { useGetSharedChatSearchParams, useSendSharedMessage, } from '../pages/next-chats/hooks/use-send-shared-message'; import FloatingChatWidgetMarkdown from './floating-chat-widget-markdown'; const FloatingChatWidget = () => { const [isOpen, setIsOpen] = useState(false); const [isMinimized, setIsMinimized] = useState(false); const [inputValue, setInputValue] = useState(''); const [lastResponseId, setLastResponseId] = useState(null); const [displayMessages, setDisplayMessages] = useState([]); const [isLoaded, setIsLoaded] = useState(false); const messagesEndRef = useRef(null); const { sharedId: conversationId, locale, from, } = useGetSharedChatSearchParams(); const isFromAgent = from === SharedFrom.Agent; // Check if we're in button-only mode or window-only mode const urlParams = new URLSearchParams(window.location.search); const mode = urlParams.get('mode') || 'full'; // 'button', 'window', or 'full' const enableStreaming = urlParams.get('streaming') === 'true'; // Only enable if explicitly set to true const { handlePressEnter, handleInputChange, value: hookValue, sendLoading, derivedMessages, hasError, } = (isFromAgent ? useSendNextSharedMessage : useSendSharedMessage)(() => {}); // Sync our local input with the hook's value when needed useEffect(() => { if (hookValue && hookValue !== inputValue) { setInputValue(hookValue); } }, [hookValue, inputValue]); const { data } = ( isFromAgent ? useFetchExternalAgentInputs : useFetchExternalChatInfo )(); const title = data.title; const { visible, hideModal, documentId, selectedChunk, clickDocumentButton } = useClickDrawer(); // PDF drawer state tracking useEffect(() => { // Drawer state management }, [visible, documentId, selectedChunk]); // Play sound when opening const playNotificationSound = useCallback(() => { try { const audioContext = new (window.AudioContext || (window as any).webkitAudioContext)(); const oscillator = audioContext.createOscillator(); const gainNode = audioContext.createGain(); oscillator.connect(gainNode); gainNode.connect(audioContext.destination); oscillator.frequency.value = 800; oscillator.type = 'sine'; gainNode.gain.setValueAtTime(0.3, audioContext.currentTime); gainNode.gain.exponentialRampToValueAtTime( 0.01, audioContext.currentTime + 0.3, ); oscillator.start(audioContext.currentTime); oscillator.stop(audioContext.currentTime + 0.3); } catch (error) { // Silent fail if audio not supported } }, []); // Play sound for AI responses (Intercom-style) const playResponseSound = useCallback(() => { try { const audioContext = new (window.AudioContext || (window as any).webkitAudioContext)(); const oscillator = audioContext.createOscillator(); const gainNode = audioContext.createGain(); oscillator.connect(gainNode); gainNode.connect(audioContext.destination); oscillator.frequency.value = 600; oscillator.type = 'sine'; gainNode.gain.setValueAtTime(0.2, audioContext.currentTime); gainNode.gain.exponentialRampToValueAtTime( 0.01, audioContext.currentTime + 0.2, ); oscillator.start(audioContext.currentTime); oscillator.stop(audioContext.currentTime + 0.2); } catch (error) { // Silent fail if audio not supported } }, []); // Set loaded state and locale useEffect(() => { // Set component as loaded after a brief moment to prevent flash const timer = setTimeout(() => { setIsLoaded(true); // Tell parent window that we're ready to be shown window.parent.postMessage( { type: 'WIDGET_READY', }, '*', ); }, 50); if (locale && i18n.language !== locale) { i18n.changeLanguage(locale); } return () => clearTimeout(timer); }, [locale]); // Handle message display based on streaming preference useEffect(() => { if (!derivedMessages) { setDisplayMessages([]); return; } if (enableStreaming) { // Show messages as they stream setDisplayMessages(derivedMessages); } else { // Only show complete messages (non-streaming mode) const completeMessages = derivedMessages.filter((msg, index) => { // Always show user messages immediately if (msg.role === MessageType.User) return true; // For AI messages, only show when response is complete (not loading) if (msg.role === MessageType.Assistant) { return !sendLoading || index < derivedMessages.length - 1; } return true; }); setDisplayMessages(completeMessages); } }, [derivedMessages, enableStreaming, sendLoading]); // Auto-scroll to bottom when display messages change useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [displayMessages]); // Render different content based on mode // Master mode - handles everything and creates second iframe dynamically useEffect(() => { if (mode !== 'master') return; // Create the chat window iframe dynamically when needed const createChatWindow = () => { // Check if iframe already exists in parent document window.parent.postMessage( { type: 'CREATE_CHAT_WINDOW', src: window.location.href.replace('mode=master', 'mode=window'), }, '*', ); }; createChatWindow(); // Listen for our own toggle events to show/hide the dynamic iframe const handleToggle = (e: MessageEvent) => { if (e.source === window) return; // Ignore our own messages const chatWindow = document.getElementById( 'dynamic-chat-window', ) as HTMLIFrameElement; if (chatWindow && e.data.type === 'TOGGLE_CHAT') { chatWindow.style.display = e.data.isOpen ? 'block' : 'none'; } }; window.addEventListener('message', handleToggle); return () => window.removeEventListener('message', handleToggle); }, [mode]); // Play sound only when AI response is complete (not streaming chunks) useEffect(() => { if (derivedMessages && derivedMessages.length > 0 && !sendLoading) { const lastMessage = derivedMessages[derivedMessages.length - 1]; if ( lastMessage.role === MessageType.Assistant && lastMessage.id !== lastResponseId && derivedMessages.length > 1 ) { setLastResponseId(lastMessage.id || ''); playResponseSound(); } } }, [derivedMessages, sendLoading, lastResponseId, playResponseSound]); const toggleChat = useCallback(() => { if (mode === 'button') { // In button mode, communicate with parent window to show/hide chat window window.parent.postMessage( { type: 'TOGGLE_CHAT', isOpen: !isOpen, }, '*', ); setIsOpen(!isOpen); if (!isOpen) { playNotificationSound(); } } else { // In full mode, handle locally if (!isOpen) { setIsOpen(true); setIsMinimized(false); playNotificationSound(); } else { setIsOpen(false); setIsMinimized(false); } } }, [isOpen, mode, playNotificationSound]); const minimizeChat = useCallback(() => { setIsMinimized(true); }, []); const handleSendMessage = useCallback(() => { if (!inputValue.trim() || sendLoading) return; // Update the hook's internal state first const syntheticEvent = { target: { value: inputValue }, currentTarget: { value: inputValue }, preventDefault: () => {}, } as any; handleInputChange(syntheticEvent); // Wait for state to update, then send setTimeout(() => { handlePressEnter([]); // Clear our local input after sending setInputValue(''); }, 50); }, [inputValue, sendLoading, handleInputChange, handlePressEnter]); const handleKeyPress = useCallback( (e: React.KeyboardEvent) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleSendMessage(); } }, [handleSendMessage], ); if (!conversationId) { return (
Error: No conversation ID provided
); } // Remove the blocking return - we'll handle visibility with CSS instead const messageCount = displayMessages?.length || 0; // Show just the button in master mode if (mode === 'master') { return (
{/* Unread Badge */} {!isOpen && messageCount > 0 && (
{messageCount > 9 ? '9+' : messageCount}
)}
); } if (mode === 'button') { // Only render the floating button return (
{/* Unread Badge */} {!isOpen && messageCount > 0 && (
{messageCount > 9 ? '9+' : messageCount}
)}
); } if (mode === 'window') { // Only render the chat window (always open) return ( <>
{/* Header */}

{title || 'Chat Support'}

We typically reply instantly

{/* Messages and Input */}
{ const element = e.currentTarget; const isAtTop = element.scrollTop === 0; const isAtBottom = element.scrollTop + element.clientHeight >= element.scrollHeight - 1; // Allow scroll to pass through to parent when at boundaries if ((isAtTop && e.deltaY < 0) || (isAtBottom && e.deltaY > 0)) { e.preventDefault(); // Let the parent handle the scroll window.parent.postMessage( { type: 'SCROLL_PASSTHROUGH', deltaY: e.deltaY, }, '*', ); } }} > {displayMessages?.map((message, index) => (
{message.role === MessageType.User ? (

{message.content}

) : ( )}
))} {/* Clean Typing Indicator */} {sendLoading && !enableStreaming && (
)}
{/* Input Area */}