diff --git a/docs/implementation-gaps.md b/docs/implementation-gaps.md index da54c39..2794c34 100644 --- a/docs/implementation-gaps.md +++ b/docs/implementation-gaps.md @@ -41,6 +41,7 @@ This document identifies features specified in the documentation that are not ye - ✅ **LangChain Context Management** (October 2, 2025): 4000 token budget with priority weighting, automatic context pruning - ✅ **Conversation Memory** (October 2, 2025): Semantic search with embeddings, conversation summarization, memory retrieval - ✅ **Multi-Language AI** (October 2, 2025): 5 languages (en/es/fr/pt/zh) with localized prompts and safety responses +- ✅ **AI Chat Conversation History** (October 2, 2025): Full conversation management UI with sidebar, conversation switching, deletion, and persistence ### Key Gaps Identified - **Backend**: 35 features not implemented (19 completed ✅) @@ -273,7 +274,45 @@ This document identifies features specified in the documentation that are not ye - Priority: Medium ✅ **COMPLETE** - Impact: International user support achieved -5. **Prompt Injection Protection** ✅ COMPLETED (Previously) +5. **AI Chat Conversation History UI** ✅ COMPLETED (October 2, 2025) + - Status: **IMPLEMENTED** + - Current: Full conversation management interface with sidebar, switching, and deletion + - Implemented: + * **Conversation State Management** (AIChatInterface.tsx:107-111): + - Tracks conversations list from API + - Maintains currentConversationId state + - Mobile drawer and delete dialog state + * **Load Conversations** (AIChatInterface.tsx:157-164): + - Fetches all user conversations on component mount + - Displays conversation history in sidebar/drawer + * **Load Conversation** (AIChatInterface.tsx:166-187): + - Loads specific conversation messages by ID + - Converts API format to Message[] format + - Auto-closes drawer on mobile after selection + * **New Conversation** (AIChatInterface.tsx:189-195): + - Clears current messages + - Resets conversationId to null for fresh chat + * **Delete Conversation** (AIChatInterface.tsx:197-215): + - Delete confirmation dialog + - API call to DELETE /api/v1/ai/conversations/:id + - Starts new conversation if deleting current one + - Reloads conversation list after deletion + * **Updated Chat Flow** (AIChatInterface.tsx:217-252): + - Passes currentConversationId in POST /api/v1/ai/chat + - Updates conversationId from API response for new chats + - Reloads conversation list after each message + * **Responsive UI**: + - Desktop: 320px sidebar with conversation list + - Mobile: Drawer with hamburger menu icon + - Conversation list shows title, date, delete icon + - Selected conversation highlighted + - "New Chat" button at top of sidebar + - Files: `maternal-web/components/features/ai-chat/AIChatInterface.tsx` (420 → 663 lines) + - Backend APIs Used: GET /api/v1/ai/conversations, GET /api/v1/ai/conversations/:id, DELETE /api/v1/ai/conversations/:id + - Priority: Medium ✅ **COMPLETE** + - Impact: Users can access chat history, switch conversations, and manage past conversations + +6. **Prompt Injection Protection** ✅ COMPLETED (Previously) - Status: **IMPLEMENTED** - Current: Comprehensive security system with 25+ regex patterns - Implemented: System manipulation detection, role change blocking, data exfiltration prevention, sanitizeInput() called in chat flow (ai.service.ts:193) diff --git a/maternal-web/components/features/ai-chat/AIChatInterface.tsx b/maternal-web/components/features/ai-chat/AIChatInterface.tsx index 8b81bc8..9c2a7fa 100644 --- a/maternal-web/components/features/ai-chat/AIChatInterface.tsx +++ b/maternal-web/components/features/ai-chat/AIChatInterface.tsx @@ -10,8 +10,32 @@ import { Avatar, CircularProgress, Chip, + Drawer, + List, + ListItem, + ListItemButton, + ListItemText, + ListItemIcon, + Divider, + Button, + Dialog, + DialogTitle, + DialogContent, + DialogActions, + useMediaQuery, + useTheme, } from '@mui/material'; -import { Send, SmartToy, Person, AutoAwesome } from '@mui/icons-material'; +import { + Send, + SmartToy, + Person, + AutoAwesome, + Chat, + Delete, + Add, + Menu as MenuIcon, + Close, +} from '@mui/icons-material'; import { motion, AnimatePresence } from 'framer-motion'; import { useAuth } from '@/lib/auth/AuthContext'; import apiClient from '@/lib/api/client'; @@ -25,6 +49,19 @@ interface Message { timestamp: Date; } +interface Conversation { + id: string; + title: string; + messages: Array<{ + role: string; + content: string; + timestamp: string; + }>; + totalTokens: number; + createdAt: string; + updatedAt: string; +} + const suggestedQuestions = [ 'How much should my baby sleep at 3 months?', 'What are normal feeding patterns?', @@ -67,9 +104,16 @@ export const AIChatInterface: React.FC = () => { 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 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' }); @@ -79,6 +123,11 @@ export const AIChatInterface: React.FC = () => { scrollToBottom(); }, [messages]); + // Load conversations on mount + useEffect(() => { + loadConversations(); + }, []); + // Cycle through thinking messages while loading useEffect(() => { if (isLoading) { @@ -105,6 +154,66 @@ export const AIChatInterface: React.FC = () => { }; }, [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); + } + }; + const handleSend = async (message?: string) => { const messageText = message || input.trim(); if (!messageText || isLoading) return; @@ -123,17 +232,26 @@ export const AIChatInterface: React.FC = () => { try { const response = await apiClient.post('/api/v1/ai/chat', { message: messageText, - conversationId: null, + conversationId: currentConversationId, }); + const responseData = response.data.data; const assistantMessage: Message = { id: (Date.now() + 1).toString(), role: 'assistant', - content: response.data.data.message, - timestamp: new Date(), + 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 = { @@ -152,268 +270,393 @@ export const AIChatInterface: React.FC = () => { handleSend(question); }; - return ( - - {/* Header */} - - - - - - - - AI Parenting Assistant - - - Ask me anything about parenting and childcare + const drawerContent = ( + + + + + Chat History + + {isMobile && ( + setDrawerOpen(false)} size="small"> + + + )} + + + + + {conversations.length === 0 ? ( + + + + No conversations yet - - + ) : ( + conversations.map((conversation) => ( + + loadConversation(conversation.id)} + sx={{ + mx: 1, + borderRadius: 2, + mb: 0.5, + '&.Mui-selected': { + bgcolor: 'primary.light', + '&:hover': { + bgcolor: 'primary.light', + }, + }, + }} + > + + + + + { + e.stopPropagation(); + setConversationToDelete(conversation.id); + setDeleteDialogOpen(true); + }} + sx={{ ml: 1 }} + > + + + + + )) + )} + + + ); - {/* Messages Container */} + return ( + + {/* Conversation History Sidebar - Desktop */} + {!isMobile && ( + + {drawerContent} + + )} + + {/* Mobile Drawer */} + setDrawerOpen(false)} + ModalProps={{ + keepMounted: true, // Better mobile performance + }} + > + {drawerContent} + + + {/* Chat Area */} - {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 && ( - + {/* 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? + + - {currentThinkingMessages[currentThinkingIndex] || 'Thinking...'} - - - - )} + {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. + + - {/* 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. + + + + + + + ); };