'use client'; import { useState, useRef, useEffect } from 'react'; import { Box, TextField, IconButton, Paper, Typography, Avatar, CircularProgress, Chip, Drawer, List, ListItem, ListItemButton, ListItemText, ListItemIcon, Divider, Button, Dialog, DialogTitle, DialogContent, DialogActions, useMediaQuery, useTheme, Collapse, Menu, MenuItem, InputAdornment, } from '@mui/material'; import { Send, SmartToy, Person, AutoAwesome, Chat, Delete, Add, Menu as MenuIcon, Close, ExpandMore, ExpandLess, Folder, FolderOpen, MoreVert, DriveFileMove, CreateNewFolder, } from '@mui/icons-material'; import { motion, AnimatePresence } from 'framer-motion'; import { useAuth } from '@/lib/auth/AuthContext'; import apiClient from '@/lib/api/client'; import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; interface Message { id: string; role: 'user' | 'assistant'; content: string; timestamp: Date; } interface Conversation { id: string; title: string; messages: Array<{ role: string; content: string; timestamp: string; }>; totalTokens: number; createdAt: string; updatedAt: string; metadata?: { groupName?: string | null; [key: string]: any; }; } interface ConversationGroup { name: string; conversations: Conversation[]; isCollapsed: boolean; } const suggestedQuestions = [ 'How much should my baby sleep at 3 months?', 'What are normal feeding patterns?', 'When should I introduce solid foods?', 'Tips for better sleep routine', ]; const thinkingMessages = [ 'Gathering baby wisdom...', 'Consulting the baby books...', 'Mixing up the perfect answer...', 'Warming up some advice...', 'Preparing your bottle of knowledge...', 'Counting tiny fingers and toes...', 'Connecting the building blocks...', 'Peeking into the toy box...', 'Arranging the puzzle pieces...', 'Stirring the baby food jar...', 'Polishing the pacifier of wisdom...', 'Tiptoeing through naptime...', 'Organizing the diaper bag...', 'Wrapping up your answer with love...', 'Brewing a warm cup of guidance...', 'Knitting together some thoughts...', 'Tucking in the details...', 'Sprinkling some magic dust...', 'Humming a lullaby while I think...', ]; // Get a random selection of 3-5 thinking messages const getRandomThinkingMessages = () => { const count = Math.floor(Math.random() * 3) + 3; // 3 to 5 const shuffled = [...thinkingMessages].sort(() => Math.random() - 0.5); return shuffled.slice(0, count); }; export const AIChatInterface: React.FC = () => { const [messages, setMessages] = useState([]); const [input, setInput] = useState(''); const [isLoading, setIsLoading] = useState(false); const [currentThinkingMessages, setCurrentThinkingMessages] = useState([]); const [currentThinkingIndex, setCurrentThinkingIndex] = useState(0); const [conversations, setConversations] = useState([]); const [currentConversationId, setCurrentConversationId] = useState(null); const [drawerOpen, setDrawerOpen] = useState(false); const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const [conversationToDelete, setConversationToDelete] = useState(null); const [collapsedGroups, setCollapsedGroups] = useState>(new Set()); const [contextMenu, setContextMenu] = useState<{ mouseX: number; mouseY: number; conversationId: string } | null>(null); const [moveToGroupDialog, setMoveToGroupDialog] = useState<{ open: boolean; conversationId: string | null }>({ open: false, conversationId: null }); const [newGroupDialog, setNewGroupDialog] = useState(false); const [newGroupName, setNewGroupName] = useState(''); const messagesEndRef = useRef(null); const thinkingIntervalRef = useRef(null); const { user } = useAuth(); const theme = useTheme(); const isMobile = useMediaQuery(theme.breakpoints.down('md')); const scrollToBottom = () => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }; useEffect(() => { scrollToBottom(); }, [messages]); // Load conversations on mount useEffect(() => { loadConversations(); }, []); // Cycle through thinking messages while loading useEffect(() => { if (isLoading) { const randomMessages = getRandomThinkingMessages(); setCurrentThinkingMessages(randomMessages); setCurrentThinkingIndex(0); thinkingIntervalRef.current = setInterval(() => { setCurrentThinkingIndex((prev) => (prev + 1) % randomMessages.length); }, 2000); // Change message every 2 seconds } else { if (thinkingIntervalRef.current) { clearInterval(thinkingIntervalRef.current); thinkingIntervalRef.current = null; } setCurrentThinkingMessages([]); setCurrentThinkingIndex(0); } return () => { if (thinkingIntervalRef.current) { clearInterval(thinkingIntervalRef.current); } }; }, [isLoading]); const loadConversations = async () => { try { const response = await apiClient.get('/api/v1/ai/conversations'); setConversations(response.data.data.conversations); } catch (error) { console.error('Failed to load conversations:', error); } }; const loadConversation = async (conversationId: string) => { try { const response = await apiClient.get(`/api/v1/ai/conversations/${conversationId}`); const conversation = response.data.data.conversation; // Convert conversation messages to Message format const loadedMessages: Message[] = conversation.messages.map((msg: any, index: number) => ({ id: `${conversationId}-${index}`, role: msg.role === 'user' ? 'user' : 'assistant', content: msg.content, timestamp: new Date(msg.timestamp), })); setMessages(loadedMessages); setCurrentConversationId(conversationId); if (isMobile) { setDrawerOpen(false); } } catch (error) { console.error('Failed to load conversation:', error); } }; const handleNewConversation = () => { setMessages([]); setCurrentConversationId(null); if (isMobile) { setDrawerOpen(false); } }; const handleDeleteConversation = async () => { if (!conversationToDelete) return; try { await apiClient.delete(`/api/v1/ai/conversations/${conversationToDelete}`); // If deleting current conversation, start new one if (conversationToDelete === currentConversationId) { handleNewConversation(); } // Reload conversations list await loadConversations(); setDeleteDialogOpen(false); setConversationToDelete(null); } catch (error) { console.error('Failed to delete conversation:', error); } }; // Group conversations by their group name const organizeConversations = (): ConversationGroup[] => { const groups: { [key: string]: Conversation[] } = {}; // Separate conversations by group conversations.forEach((conv) => { const groupName = conv.metadata?.groupName || 'Ungrouped'; if (!groups[groupName]) { groups[groupName] = []; } groups[groupName].push(conv); }); // Convert to array and sort const groupArray: ConversationGroup[] = Object.entries(groups) .map(([name, convs]) => ({ name, conversations: convs.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()), isCollapsed: collapsedGroups.has(name), })) .sort((a, b) => { // Ungrouped always last if (a.name === 'Ungrouped') return 1; if (b.name === 'Ungrouped') return -1; return a.name.localeCompare(b.name); }); return groupArray; }; const toggleGroupCollapse = (groupName: string) => { setCollapsedGroups((prev) => { const newSet = new Set(prev); if (newSet.has(groupName)) { newSet.delete(groupName); } else { newSet.add(groupName); } return newSet; }); }; const handleMoveToGroup = async (conversationId: string, groupName: string | null) => { try { await apiClient.patch(`/api/v1/ai/conversations/${conversationId}/group`, { groupName: groupName, }); await loadConversations(); setMoveToGroupDialog({ open: false, conversationId: null }); setContextMenu(null); } catch (error) { console.error('Failed to move conversation to group:', error); } }; const handleCreateNewGroup = async () => { if (!newGroupName.trim()) return; const conversationId = moveToGroupDialog.conversationId; if (conversationId) { await handleMoveToGroup(conversationId, newGroupName.trim()); } setNewGroupName(''); setNewGroupDialog(false); }; const handleContextMenu = (event: React.MouseEvent, conversationId: string) => { event.preventDefault(); setContextMenu({ mouseX: event.clientX - 2, mouseY: event.clientY - 4, conversationId, }); }; const handleCloseContextMenu = () => { setContextMenu(null); }; const getExistingGroups = (): string[] => { const groups = new Set(); conversations.forEach((conv) => { if (conv.metadata?.groupName) { groups.add(conv.metadata.groupName); } }); return Array.from(groups).sort(); }; const handleSend = async (message?: string) => { const messageText = message || input.trim(); if (!messageText || isLoading) return; const userMessage: Message = { id: Date.now().toString(), role: 'user', content: messageText, timestamp: new Date(), }; setMessages((prev) => [...prev, userMessage]); setInput(''); setIsLoading(true); try { const response = await apiClient.post('/api/v1/ai/chat', { message: messageText, conversationId: currentConversationId, }); const responseData = response.data.data; const assistantMessage: Message = { id: (Date.now() + 1).toString(), role: 'assistant', content: responseData.message, timestamp: new Date(responseData.timestamp), }; setMessages((prev) => [...prev, assistantMessage]); // Update current conversation ID if it's a new conversation if (!currentConversationId && responseData.conversationId) { setCurrentConversationId(responseData.conversationId); } // Reload conversations to update the list await loadConversations(); } catch (error) { console.error('AI chat error:', error); const errorMessage: Message = { id: (Date.now() + 1).toString(), role: 'assistant', content: 'Sorry, I encountered an error. Please try again.', timestamp: new Date(), }; setMessages((prev) => [...prev, errorMessage]); } finally { setIsLoading(false); } }; const handleSuggestedQuestion = (question: string) => { handleSend(question); }; const drawerContent = ( Chat History {isMobile && ( setDrawerOpen(false)} size="small" aria-label="Close drawer"> )} {conversations.length === 0 ? ( No conversations yet ) : ( organizeConversations().map((group) => ( {/* Group Header */} toggleGroupCollapse(group.name)} sx={{ py: 1, px: 2, bgcolor: 'background.default', '&:hover': { bgcolor: 'action.hover' }, }} > {group.isCollapsed ? : } {group.isCollapsed ? : } {/* Conversations in Group */} {group.conversations.map((conversation) => ( loadConversation(conversation.id)} onContextMenu={(e) => handleContextMenu(e, conversation.id)} sx={{ mx: 1, pl: 5, borderRadius: 2, mb: 0.5, '&.Mui-selected': { bgcolor: 'primary.light', '&:hover': { bgcolor: 'primary.light', }, }, }} > { e.stopPropagation(); if (isMobile) { handleContextMenu(e, conversation.id); } else { setConversationToDelete(conversation.id); setDeleteDialogOpen(true); } }} sx={{ ml: 1 }} aria-label={isMobile ? 'More options' : 'Delete conversation'} > {isMobile ? : } ))} )) )} ); return ( {/* Conversation History Sidebar - Desktop */} {!isMobile && ( {drawerContent} )} {/* Mobile Drawer */} setDrawerOpen(false)} ModalProps={{ keepMounted: true, // Better mobile performance }} > {drawerContent} {/* Chat Area */} {/* Header */} {isMobile && ( setDrawerOpen(true)} edge="start"> )} AI Parenting Assistant Ask me anything about parenting and childcare {/* Messages Container */} {messages.length === 0 && ( Hi {user?.name}! How can I help you today? {suggestedQuestions.map((question, index) => ( handleSuggestedQuestion(question)} sx={{ borderRadius: 3, '&:hover': { bgcolor: 'primary.light', color: 'white', }, }} /> ))} )} {messages.map((message) => ( {message.role === 'assistant' && ( )} {message.role === 'assistant' ? ( {message.content} ) : ( {message.content} )} {message.timestamp.toLocaleTimeString()} {message.role === 'user' && ( )} ))} {isLoading && ( {currentThinkingMessages[currentThinkingIndex] || 'Thinking...'} )}
{/* Input Area */} setInput(e.target.value)} onKeyPress={(e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleSend(); } }} disabled={isLoading} sx={{ '& .MuiOutlinedInput-root': { borderRadius: 3, }, }} /> handleSend()} disabled={!input.trim() || isLoading} sx={{ width: 48, height: 48, bgcolor: 'primary.main', color: 'white', '&:hover': { bgcolor: 'primary.dark', }, '&:disabled': { bgcolor: 'action.disabledBackground', }, }} > This AI assistant provides general information. Always consult healthcare professionals for medical advice. {/* Delete Confirmation Dialog */} setDeleteDialogOpen(false)}> Delete Conversation Are you sure you want to delete this conversation? This action cannot be undone. {/* Context Menu for Conversation Actions */} { if (contextMenu) { setMoveToGroupDialog({ open: true, conversationId: contextMenu.conversationId }); } handleCloseContextMenu(); }} > Move to Group { if (contextMenu) { setConversationToDelete(contextMenu.conversationId); setDeleteDialogOpen(true); } handleCloseContextMenu(); }} > Delete {/* Move to Group Dialog */} setMoveToGroupDialog({ open: false, conversationId: null })} maxWidth="xs" fullWidth > Move to Group { if (moveToGroupDialog.conversationId) { handleMoveToGroup(moveToGroupDialog.conversationId, null); } }} > {getExistingGroups().map((groupName) => ( { if (moveToGroupDialog.conversationId) { handleMoveToGroup(moveToGroupDialog.conversationId, groupName); } }} > ))} setNewGroupDialog(true)}> {/* Create New Group Dialog */} { setNewGroupDialog(false); setNewGroupName(''); }} maxWidth="xs" fullWidth > Create New Group setNewGroupName(e.target.value)} onKeyPress={(e) => { if (e.key === 'Enter') { handleCreateNewGroup(); } }} /> ); };