import React, { useEffect, useState, useRef, useCallback } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import { motion } from 'framer-motion'; import Typography from '../components/ui/Typography'; import { Button } from '../components/ui/Button'; import { useAgentStore } from '../stores/agentStore'; import { useConfigStore } from '../stores/configStore'; import { Logger } from '@hashgraphonline/standards-sdk'; import { fetchUserProfile as fetchProfileViaFactory } from '../services/hcs10ClientFactory'; import { useWalletStore } from '../stores/walletStore'; import { FiSettings, FiRefreshCw, FiMessageSquare, FiWifi, FiWifiOff, FiShield, FiAlertCircle, FiHash, FiUser, } from 'react-icons/fi'; import { cn } from '../lib/utils'; import type { Message, AgentStatus } from '../stores/agentStore'; import type { ChatSession } from '../../main/db/schema'; import MessageList from '../components/chat/MessageList'; import ChatComposer from '../components/chat/ChatComposer'; import ModeBadge from '../components/chat/status/ModeBadge'; import LoadingConversation from '../components/chat/status/LoadingConversation'; import EmptyChatState from '../components/chat/status/EmptyChatState'; import ChatHeader from '../components/chat/headers/ChatHeader'; import { Disclaimer } from '../components/chat/Disclaimer'; import { AgentSelectorModal } from '../components/chat/AgentSelectorModal'; import { SessionCreationModal } from '../components/chat/SessionCreationModal'; import { AgentProfileModal } from '../components/AgentProfileModal'; import { toast } from 'sonner'; import { useNotificationStore } from '../stores/notificationStore'; import { useHCS10 } from '../contexts/HCS10Context'; import type { Agent } from '../contexts/HCS10Context'; import ClearChatDialog from '../components/chat/modals/ClearChatDialog'; import DeleteSessionDialog from '../components/chat/modals/DeleteSessionDialog'; import useFileAttachments from '../hooks/useFileAttachments'; import useAgentInit from '../hooks/useAgentInit'; import useHcs10Polling from '../hooks/useHcs10Polling'; import useChatSessions from '../hooks/useChatSessions'; import useChatContextInit from '../hooks/useChatContextInit'; interface ChatPageProps {} interface UserProfile { display_name?: string; alias?: string; bio?: string; profileImage?: string; type?: number; aiAgent?: { type: number; capabilities?: number[]; model?: string; creator?: string; }; } const logger = new Logger({ module: 'ChatPage' }); const ChatPage: React.FC = () => { const navigate = useNavigate(); const { agentId } = useParams<{ agentId?: string }>(); const { status, isConnected, connectionError, connect, sendMessage, clearMessages, getMessages, loadConversationMessages, refreshConversationMessages, chatContext, setChatContext, hcs10LoadingMessages, sessions, currentSession, createSession, loadSession, saveSession, deleteSession, isTyping, processingContext, } = useAgentStore(); let derivedTopicId: string | undefined; if (currentSession?.mode === 'hcs10' && currentSession.topicId) { derivedTopicId = currentSession.topicId; } else if (chatContext.mode === 'hcs10' && chatContext.topicId) { derivedTopicId = chatContext.topicId; } else { derivedTopicId = undefined; } const messages = getMessages(derivedTopicId); const { config, isConfigured } = useConfigStore(); const { addNotification } = useNotificationStore(); const [isLoading, setIsLoading] = useState(false); const [inputValue, setInputValue] = useState(''); const [isSubmitting, setIsSubmitting] = useState(false); const { files: selectedFiles, fileError, addFiles, removeFile, reset, toBase64, } = useFileAttachments(5, 10); const [userProfile, setUserProfile] = useState(null); const [isLoadingProfile, setIsLoadingProfile] = useState(false); const [showClearDialog, setShowClearDialog] = useState(false); const { agents: hcs10Agents } = useHCS10(); const agents = hcs10Agents; const [showAgentSelector, setShowAgentSelector] = useState(false); const [showSessionSelector, setShowSessionSelector] = useState(false); const [agentProfileModal, setAgentProfileModal] = useState<{ accountId: string; agentName: string; network: string; } | null>(null); const { isLoadingSessions, showSessionCreationModal, sessionDeleteConfirm, setSessionDeleteConfirm, openCreateSession: openCreateSessionHook, closeCreateSession, handleSessionSelect: handleSessionSelectHook, handleSessionCreated: handleSessionCreatedHook, handleDeleteSession: handleDeleteSessionHook, } = useChatSessions({ loadSession, createSession, deleteSession, setChatContext, currentSession, }); const messagesEndRef = React.useRef(null); const isManuallySelectingSession = useRef(false); const toggleSessionSelector = useCallback(() => { setShowSessionSelector((prev) => !prev); }, []); const openCreateSession = useCallback(() => { setShowSessionSelector(false); openCreateSessionHook(); }, [openCreateSessionHook]); const requestDeleteSession = useCallback((id: string) => { setSessionDeleteConfirm(id); }, []); const isConfigComplete = isConfigured(); let currentTopicId: string | undefined; if (currentSession?.mode === 'hcs10' && currentSession.topicId) { currentTopicId = currentSession.topicId; } else if (chatContext.mode === 'hcs10' && chatContext.topicId) { currentTopicId = chatContext.topicId; } else { currentTopicId = undefined; } let isLoadingHCS10Messages = false; if (currentTopicId) { isLoadingHCS10Messages = Boolean(hcs10LoadingMessages[currentTopicId]); } const wallet = useWalletStore(); const fetchUserProfile = React.useCallback(async () => { const effectiveAccountId = wallet.isConnected ? wallet.accountId : config?.hedera?.accountId; const effectiveNetwork = (wallet.isConnected ? wallet.network : config?.hedera?.network) as ('mainnet' | 'testnet' | undefined); if (effectiveAccountId && effectiveNetwork && !isLoadingProfile) { setIsLoadingProfile(true); try { const resp = await fetchProfileViaFactory( effectiveAccountId, effectiveNetwork, { walletConnected: wallet.isConnected, operatorId: config?.hedera?.accountId, privateKey: config?.hedera?.privateKey, } ); if (resp.success) { setUserProfile(resp.profile as unknown as UserProfile); } else if (wallet.isConnected) { setUserProfile({ display_name: 'Wallet Account' }); } } catch (error) { } finally { setIsLoadingProfile(false); } } }, [wallet.isConnected, wallet.accountId, wallet.network, config?.hedera?.accountId, config?.hedera?.network, config?.hedera?.privateKey]); useEffect(() => { fetchUserProfile(); }, [fetchUserProfile]); useEffect(() => { const handleFocus = () => { fetchUserProfile(); }; window.addEventListener('focus', handleFocus); return () => { window.removeEventListener('focus', handleFocus); }; }, [fetchUserProfile]); const walletReady = wallet.isConnected || wallet.isInitializing === false; useAgentInit({ isConfigured, config, isConnected, status, connect, ready: walletReady }); useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [messages]); useChatContextInit({ agentId, agents, sessions, currentSession, currentContext: chatContext, setChatContext, loadSession, createSession, isManualSelecting: () => isManuallySelectingSession.current === true, clearManualSelecting: () => { isManuallySelectingSession.current = false; }, }); useHcs10Polling({ topicId: currentTopicId, isConnected, hasMessages: messages.length > 0, }); const handleAddFiles = (files: FileList) => addFiles(files); const handleRemoveFile = (index: number) => removeFile(index); const fileToBase64 = toBase64; const handleSendMessage = async () => { const message = inputValue.trim(); if ( (!message && selectedFiles.length === 0) || isSubmitting || isTyping || !isConnected ) return; setIsSubmitting(true); setIsLoading(true); try { if (!currentSession) { let sessionName: string; if (chatContext.mode === 'personal') { sessionName = message.length > 50 ? `${message.substring(0, 47)}...` : message || `Personal Chat - ${new Date().toLocaleDateString()}`; } else { sessionName = chatContext.agentName || `HCS-10 Chat - ${chatContext.topicId}`; } isManuallySelectingSession.current = true; const newSession = await createSession( sessionName, chatContext.mode, chatContext.mode === 'hcs10' ? chatContext.topicId : undefined ); await loadSession(newSession.id); setTimeout(() => { isManuallySelectingSession.current = false; }, 1500); const { currentSession: verifiedSession } = useAgentStore.getState(); if (!verifiedSession) { throw new Error('Failed to create active session for sending message'); } } const { currentSession: activeSession } = useAgentStore.getState(); if (!activeSession) { throw new Error('No active session available for sending message'); } const attachments: Array<{ name: string; data: string; type: string; size: number; }> = []; if (selectedFiles.length > 0) { const failedFiles: string[] = []; for (const file of selectedFiles) { try { const base64Content = await fileToBase64(file); attachments.push({ name: file.name, data: base64Content, type: file.type || 'application/octet-stream', size: file.size, }); } catch (error) { logger.error('File conversion failed:', error); failedFiles.push(file.name); } } if (failedFiles.length > 0) { const allFilesFailed = failedFiles.length === selectedFiles.length; const someFilesFailed = failedFiles.length > 0 && failedFiles.length < selectedFiles.length; if (allFilesFailed) { addNotification({ type: 'error', title: 'File Attachment Failed', message: failedFiles.length === 1 ? `Failed to process file: ${failedFiles[0]}` : `Failed to process ${failedFiles.length} files: ${failedFiles.join(', ')}`, duration: 8000 }); return; } else if (someFilesFailed) { addNotification({ type: 'warning', title: 'Some Files Failed to Attach', message: failedFiles.length === 1 ? `Failed to attach: ${failedFiles[0]}` : `Failed to attach ${failedFiles.length} files: ${failedFiles.join(', ')}`, duration: 6000 }); } } } let sendTopicId: string | undefined; if (activeSession?.mode === 'hcs10') { sendTopicId = activeSession.topicId; } else { sendTopicId = undefined; } await sendMessage(message, attachments, sendTopicId); if (activeSession) { let nextSessionName = activeSession.name; const isPersonalDefault = activeSession.name.includes('Personal Chat -'); const isHcsDefault = activeSession.name.includes('HCS-10 Chat -'); const canUseMessageAsName = message.length > 0 && message.length <= 50; if (isPersonalDefault && canUseMessageAsName) { nextSessionName = message; } else if ( isHcsDefault && Boolean(chatContext.agentName) && activeSession.name !== chatContext.agentName ) { nextSessionName = chatContext.agentName as string; } const updatedSession = { ...activeSession, lastMessageAt: new Date(), updatedAt: new Date(), name: nextSessionName, } as ChatSession; try { await saveSession(updatedSession); } catch (error) { logger.error('Failed to save session:', error); } } setInputValue(''); } catch (error) { logger.error('Failed to send message:', error); addNotification({ type: 'error', title: 'Message Send Failed', message: 'Unable to send your message. Please check your connection and try again.', duration: 6000 }); } finally { reset(); setIsSubmitting(false); setIsLoading(false); } }; const handleConnect = async () => { try { await connect(); } catch (error) {} }; const handleGoToSettings = () => { navigate('/settings'); }; const handleConnectToAgent = useCallback(async (agent: Agent) => { try { const result = await window.electron.invoke( 'hcs10:send-connection-request', { targetAccountId: agent.accountId, } ); if (result.success) { toast.success('Connection Request Sent', { description: 'Your connection request has been sent to the agent', }); } else { toast.error('Connection Failed', { description: result.error || 'Failed to send connection request', }); } } catch (error) { toast.error('Error', { description: 'Failed to send connection request', }); } }, []); const handleSessionSelect = useCallback( async (session: ChatSession) => { setShowSessionSelector(false); isManuallySelectingSession.current = true; await handleSessionSelectHook(session); setTimeout(() => { isManuallySelectingSession.current = false; }, 1500); }, [handleSessionSelectHook] ); const handleSessionCreated = useCallback( async (session: ChatSession) => { isManuallySelectingSession.current = true; await handleSessionCreatedHook(session); setTimeout(() => { isManuallySelectingSession.current = false; }, 1500); }, [handleSessionCreatedHook] ); const handleDeleteSession = useCallback( async (sessionId: string) => { await handleDeleteSessionHook(sessionId); }, [handleDeleteSessionHook] ); const handleAgentProfileClick = useCallback((accountId: string, agentName: string, network: string) => { setAgentProfileModal({ accountId, agentName, network }); }, []); const handleAgentConnect = useCallback(async (accountId: string) => { try { const result = await window.electron.invoke( 'hcs10:send-connection-request', { targetAccountId: accountId, } ); if (result.success) { toast.success('Connection Request Sent', { description: 'Your connection request has been sent to the agent', }); } else { toast.error('Connection Failed', { description: result.error || 'Failed to send connection request', }); } } catch (error) { toast.error('Error', { description: 'Failed to send connection request', }); } setAgentProfileModal(null); }, []); if (!isConfigComplete) { return (
AI Agent Chat
Welcome to Agent Chat To start chatting, you'll need to set up your account and API credentials. This ensures your conversations are secure and private.
); } if (!isConnected) { return (
AI Agent Chat
{status === 'connecting' || status === 'disconnecting' ? (
{status === 'disconnecting' ? 'Switching Mode' : 'Connecting to Agent'} {status === 'disconnecting' ? 'Reconfiguring your assistant for the new operational mode...' : 'Getting your assistant ready. This may take a moment...'}
Loading extensions...
Connecting to network...
Setting up your assistant...
) : (
Ready to Connect {connectionError ? `Connection failed: ${connectionError}. Please check your settings and try again.` : 'Your assistant is ready to start. Click below to connect and begin chatting.'}
)}
); } return (
{(() => { let content; if (messages.length === 0 && !isLoadingHCS10Messages) { content = ; } else if (isLoadingHCS10Messages) { content = ; } else { content = (
{isTyping && (
{processingContext === 'form' ? 'Processing your form...' : 'Processing your message...'}
)}
); } return content; })()}
setShowClearDialog(false)} onConfirm={() => { clearMessages(); setShowClearDialog(false); }} /> setShowAgentSelector(false)} onConnect={handleConnectToAgent} currentNetwork={config?.hedera?.network || 'testnet'} /> setSessionDeleteConfirm(null)} onConfirm={handleDeleteSession} /> setAgentProfileModal(null)} onConnect={handleAgentConnect} />
); }; export default ChatPage;