import React, { forwardRef, useState, useEffect, useRef, JSX, useImperativeHandle } from 'react'; import { Box } from '@mui/material'; import { useAuth } from 'hooks/AuthContext'; import { ChatMessage, ChatSession, ChatMessageUser, ChatMessageError, ChatMessageStreaming, ChatMessageStatus, ChatMessageMetaData, Candidate, ChatQuery, } from 'types/types'; import { ConversationHandle } from 'components/Conversation'; import { Message } from 'components/Message'; import { useAppState } from 'hooks/GlobalContext'; import PropagateLoader from 'react-spinners/PropagateLoader'; import { Scrollable } from 'components/Scrollable'; const emptyMetadata: ChatMessageMetaData = { model: 'qwen2.5', temperature: 0, maxTokens: 0, topP: 0, frequencyPenalty: 0, presencePenalty: 0, stopSequences: [], usage: { evalCount: 0, evalDuration: 0, promptEvalCount: 0, promptEvalDuration: 0, }, }; const defaultMessage: ChatMessage = { status: 'done', type: 'text', sessionId: '', timestamp: new Date(), content: '', role: 'user', metadata: emptyMetadata, }; interface TransientChatProps { id: string; } const TransientChat = forwardRef( (props, ref): JSX.Element => { const { id } = props; const { apiClient, user } = useAuth(); const [processingMessage, setProcessingMessage] = useState< ChatMessageStatus | ChatMessageError | null >(null); const [streamingMessage, setStreamingMessage] = useState(null); const { setSnack } = useAppState(); const [chatSession, setChatSession] = useState(null); const [messages, setMessages] = useState([]); const [streaming, setStreaming] = useState(false); const messagesEndRef = useRef(null); useImperativeHandle(ref, () => ({ submitQuery: (query: ChatQuery): void => { sendMessage(query.prompt); }, fetchHistory: (): void => { console.log('fetchHistory called, but not implemented'); }, })); const chatHandlers = { onMessage: (msg: ChatMessage): void => { setMessages(prev => { const filtered = prev.filter(m => m.id !== msg.id); return [...filtered, msg] as ChatMessage[]; }); setStreamingMessage(null); setProcessingMessage(null); }, onError: (error: string | ChatMessageError): void => { console.log('onError:', error); // Type-guard to determine if this is a ChatMessageBase or a string if (typeof error === 'object' && error !== null && 'content' in error) { setProcessingMessage(error); } else { setProcessingMessage({ ...defaultMessage, status: 'error', content: error, }); } setStreaming(false); }, onStreaming: (chunk: ChatMessageStreaming): void => { // console.log("onStreaming:", chunk); setStreamingMessage({ ...chunk, role: 'assistant', metadata: emptyMetadata, }); }, onStatus: (status: ChatMessageStatus): void => { setProcessingMessage(status); }, onComplete: (): void => { console.log('onComplete'); setStreamingMessage(null); setProcessingMessage(null); setStreaming(false); }, }; // Send message const sendMessage = async (message: string): Promise => { if (!message.trim() || !chatSession?.id || streaming) return; const messageContent = message; setStreaming(true); const chatMessage: ChatMessageUser = { sessionId: chatSession.id, role: 'user', content: messageContent, status: 'done', type: 'text', timestamp: new Date(), }; setProcessingMessage({ ...defaultMessage, status: 'status', activity: 'info', content: `Establishing connection with chat session.`, }); setMessages(prev => { const filtered = prev.filter(m => m.id !== chatMessage.id); return [...filtered, chatMessage] as ChatMessage[]; }); try { apiClient.sendMessageStream(chatMessage, chatHandlers); } catch (error) { console.error('Failed to send message:', error); setStreaming(false); } }; // Auto-scroll to bottom when new messages arrive useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [messages]); // Load sessions when username changes useEffect(() => { if (!user) return; try { apiClient .getOrCreateChatSession(user as Candidate, `Transient chat - ${id}`, 'candidate_chat') .then(session => { setChatSession(session); }); } catch (error) { setSnack('Unable to load chat session', 'error'); } }, [user, apiClient, setSnack]); // Load messages when session changes useEffect(() => { const loadMessages = async (): Promise => { if (!chatSession?.id) return; try { const result = await apiClient.getChatMessages(chatSession.id); const chatMessages: ChatMessage[] = result.data; setMessages(chatMessages); setProcessingMessage(null); setStreamingMessage(null); console.log(`getChatMessages returned ${chatMessages.length} messages.`, chatMessages); } catch (error) { console.error('Failed to load messages:', error); } }; if (chatSession?.id) { loadMessages(); } }, [chatSession, apiClient]); return ( *:not(.Scrollable)': { flexShrink: 0 /* Prevent shrinking */, }, position: 'relative', }} > {/* Chat Interface */} {/* Scrollable Messages Area */} {chatSession && ( {messages.map((message: ChatMessage) => ( ))} {processingMessage !== null && ( )} {streamingMessage !== null && ( )} {streaming && ( )}
)} ); } ); TransientChat.displayName = 'TransientChat'; export { TransientChat };