import React, { forwardRef, useState, useEffect, useRef } from 'react'; import { Box, Paper, Typography, TextField, Button, List, ListItem, ListItemText, Chip, IconButton, CircularProgress, Divider, Card, CardContent, Avatar, Drawer, useTheme, useMediaQuery, Fab } from '@mui/material'; import { ChevronLeft, ChevronRight, Chat as ChatIcon } from '@mui/icons-material'; import { useAuth } from 'hooks/AuthContext'; import { ChatMessageBase, ChatMessage, ChatSession, ChatStatusType } from 'types/types'; import { ConversationHandle } from 'components/Conversation'; import { BackstoryPageProps } from 'components/BackstoryTab'; import { Message } from 'components/Message'; import { DeleteConfirmation } from 'components/DeleteConfirmation'; import { CandidateSessionsResponse } from 'services/api-client'; import { CandidateInfo } from 'components/CandidateInfo'; import { useNavigate } from 'react-router-dom'; import { useSelectedCandidate } from 'hooks/GlobalContext'; const DRAWER_WIDTH = 300; const HANDLE_WIDTH = 48; const CandidateChatPage = forwardRef((props: BackstoryPageProps, ref) => { const { apiClient } = useAuth(); const { selectedCandidate } = useSelectedCandidate() const navigate = useNavigate(); const theme = useTheme(); const isMdUp = useMediaQuery(theme.breakpoints.up('md')); const { setSnack, submitQuery, } = props; const [sessions, setSessions] = useState(null); const [chatSession, setChatSession] = useState(null); const [messages, setMessages] = useState([]); const [newMessage, setNewMessage] = useState(''); const [loading, setLoading] = useState(false); const [streaming, setStreaming] = useState(false); const messagesEndRef = useRef(null); // Drawer state - defaults to open on md+ screens or when no session is selected const [drawerOpen, setDrawerOpen] = useState(() => isMdUp || !chatSession); // Update drawer state when screen size or session changes useEffect(() => { setDrawerOpen(isMdUp || !chatSession); }, [isMdUp, chatSession]); // Load sessions for the selectedCandidate const loadSessions = async () => { if (!selectedCandidate) return; try { setLoading(true); const result = await apiClient.getCandidateChatSessions(selectedCandidate.username); setSessions(result); } catch (error) { console.error('Failed to load sessions:', error); } finally { setLoading(false); } }; // Load messages for current session const loadMessages = async () => { if (!chatSession?.id) return; try { const result = await apiClient.getChatMessages(chatSession.id); setMessages(result.data as any); } catch (error) { console.error('Failed to load messages:', error); } }; // Create new session const createNewSession = async () => { if (!selectedCandidate) { return } try { setLoading(true); const newSession = await apiClient.createCandidateChatSession( selectedCandidate.username, 'candidate_chat', `Interview Discussion - ${selectedCandidate.username}` ); setChatSession(newSession); setMessages([]); await loadSessions(); // Refresh sessions list } catch (error) { console.error('Failed to create session:', error); } finally { setLoading(false); } }; // Send message const sendMessage = async () => { if (!newMessage.trim() || !chatSession?.id || streaming) return; const messageContent = newMessage; setNewMessage(''); setStreaming(true); try { await apiClient.sendMessageStream( chatSession.id, { prompt: messageContent }, { onMessage: (msg: ChatMessage) => { console.log("onMessage:", msg); if (msg.type === "response") { setMessages(prev => { const filtered = prev.filter((m: any) => m.id !== msg.id); return [...filtered, msg].sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime() ) as any; }); } else { console.log(msg); } }, onError: (error: string | ChatMessageBase) => { console.log("onError:", error); setStreaming(false); }, onStreaming: (chunk: ChatMessageBase) => { console.log("onStreaming:", chunk); }, onStatusChange: (status: ChatStatusType) => { console.log("onStatusChange:", status); }, onComplete: () => { console.log("onComplete"); setStreaming(false); } }); } catch (error) { console.error('Failed to send message:', error); setStreaming(false); } }; // Auto-scroll to bottom when new messages arrive useEffect(() => { (messagesEndRef.current as any)?.scrollIntoView({ behavior: 'smooth' }); }, [messages]); // Load sessions when username changes useEffect(() => { loadSessions(); }, [selectedCandidate]); // Load messages when session changes useEffect(() => { if (chatSession?.id) { loadMessages(); } }, [chatSession]); if (!selectedCandidate) { navigate('/find-a-candidate'); } const drawerContent = ( Chat Sessions {sessions && ( )} {sessions ? ( {sessions.sessions.data.map((session: any) => ( { setChatSession(session); // Auto-close drawer on smaller screens when session is selected if (!isMdUp) { setDrawerOpen(false); } }} sx={{ mb: 1, borderRadius: 1, border: '1px solid', borderColor: chatSession?.id === session.id ? 'primary.main' : 'divider', cursor: 'pointer', '&:hover': { backgroundColor: 'action.hover' } }} > ))} ) : ( Enter a username and click "Load Sessions" )} ); const drawerHandle = ( setDrawerOpen(!drawerOpen)} sx={{ borderRadius: '0 50% 50% 0', width: 32, height: 64, minHeight: 64, boxShadow: 2, '&:hover': { boxShadow: 4, } }} > {drawerOpen ? : } ); return ( {selectedCandidate && } {/* Drawer */} {drawerContent} {/* Drawer Handle */} {drawerHandle} {/* Chat Interface */} {chatSession?.id ? ( <> {/* Messages Area */} {messages.map((message: ChatMessageBase) => ( ))} {streaming && ( 🤖 AI is typing... )}
{/* Message Input */} setNewMessage(e.target.value)} onKeyPress={(e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); } }} disabled={streaming} multiline maxRows={4} /> ) : ( 🤖 Select a session to start chatting Create a new session or choose from existing ones to begin discussing the candidate )} ); }); export { CandidateChatPage };