From 86b7ff377bb3b7169d392a72ef3b92dbd5780987 Mon Sep 17 00:00:00 2001 From: andupetcu <47487320+andupetcu@users.noreply.github.com> Date: Mon, 22 Sep 2025 11:33:36 +0300 Subject: [PATCH] Implement AI chat history system with enhanced memory (Phases 1 & 2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 1 - Database Schema & Basic API: - Add ChatConversation and ChatMessage tables with proper relationships - Create conversation CRUD API endpoints with authentication - Update chat API to support persistent conversations - Implement auto-generated conversation titles and language separation - Add conversation soft delete and user-specific access control Phase 2 - Enhanced Memory System: - Implement intelligent context selection beyond simple chronological order - Add relevance scoring for older messages based on keyword overlap and biblical references - Create automatic message summarization for very long conversations - Optimize token usage with smart context management (1500 token budget) - Add biblical awareness with Romanian/English book name detection - Implement time-based relevance decay for better context prioritization Frontend Improvements: - Add chat history button to floating chat header - Create basic history panel UI with placeholder content - Maintain backward compatibility for anonymous users Database Changes: - Enhanced schema with conversation relationships and message roles - Proper indexing for performance and user-specific queries - Migration completed successfully with prisma db push πŸ€– Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- app/api/chat/conversations/[id]/route.ts | 266 +++++++++++++++++ app/api/chat/conversations/route.ts | 206 +++++++++++++ app/api/chat/route.ts | 268 ++++++++++++++++- components/chat/floating-chat.tsx | 109 ++++++- prisma/schema.prisma | 43 ++- temp/ai-chat-improvements-plan.md | 364 +++++++++++++++++++++++ 6 files changed, 1229 insertions(+), 27 deletions(-) create mode 100644 app/api/chat/conversations/[id]/route.ts create mode 100644 app/api/chat/conversations/route.ts create mode 100644 temp/ai-chat-improvements-plan.md diff --git a/app/api/chat/conversations/[id]/route.ts b/app/api/chat/conversations/[id]/route.ts new file mode 100644 index 0000000..671348c --- /dev/null +++ b/app/api/chat/conversations/[id]/route.ts @@ -0,0 +1,266 @@ +import { NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { PrismaClient } from '@prisma/client' +import { verifyToken } from '@/lib/auth' + +const prisma = new PrismaClient() + +export const runtime = 'nodejs' + +const updateConversationSchema = z.object({ + title: z.string().min(1).max(200).optional(), + isActive: z.boolean().optional(), +}) + +// GET /api/chat/conversations/[id] - Get conversation with messages +export async function GET( + request: NextRequest, + { params }: { params: { id: string } } +) { + try { + // Get user from authentication + const authHeader = request.headers.get('authorization') + if (!authHeader?.startsWith('Bearer ')) { + return NextResponse.json( + { success: false, error: 'Authentication required' }, + { status: 401 } + ) + } + + const token = authHeader.substring(7) + const payload = verifyToken(token) + + if (!payload) { + return NextResponse.json( + { success: false, error: 'Invalid token' }, + { status: 401 } + ) + } + + const userId = payload.userId as string + const conversationId = params.id + + // Get conversation with messages + const conversation = await prisma.chatConversation.findUnique({ + where: { + id: conversationId, + userId, // Ensure user owns this conversation + isActive: true + }, + include: { + messages: { + orderBy: { timestamp: 'asc' }, + select: { + id: true, + role: true, + content: true, + timestamp: true, + metadata: true + } + } + } + }) + + if (!conversation) { + return NextResponse.json( + { success: false, error: 'Conversation not found' }, + { status: 404 } + ) + } + + return NextResponse.json({ + success: true, + conversation: { + id: conversation.id, + title: conversation.title, + language: conversation.language, + createdAt: conversation.createdAt, + lastMessageAt: conversation.lastMessageAt, + messages: conversation.messages.map(msg => ({ + id: msg.id, + role: msg.role.toLowerCase(), // Convert enum to lowercase for frontend compatibility + content: msg.content, + timestamp: msg.timestamp, + metadata: msg.metadata + })) + } + }) + + } catch (error) { + console.error('Error fetching conversation:', error) + return NextResponse.json( + { + success: false, + error: 'Failed to fetch conversation' + }, + { status: 500 } + ) + } +} + +// PUT /api/chat/conversations/[id] - Update conversation +export async function PUT( + request: NextRequest, + { params }: { params: { id: string } } +) { + try { + // Get user from authentication + const authHeader = request.headers.get('authorization') + if (!authHeader?.startsWith('Bearer ')) { + return NextResponse.json( + { success: false, error: 'Authentication required' }, + { status: 401 } + ) + } + + const token = authHeader.substring(7) + const payload = verifyToken(token) + + if (!payload) { + return NextResponse.json( + { success: false, error: 'Invalid token' }, + { status: 401 } + ) + } + + const userId = payload.userId as string + const conversationId = params.id + + // Parse request body + const body = await request.json() + const updateData = updateConversationSchema.parse(body) + + // Verify conversation ownership + const existingConversation = await prisma.chatConversation.findUnique({ + where: { + id: conversationId, + userId + } + }) + + if (!existingConversation) { + return NextResponse.json( + { success: false, error: 'Conversation not found' }, + { status: 404 } + ) + } + + // Update conversation + const conversation = await prisma.chatConversation.update({ + where: { id: conversationId }, + data: { + ...updateData, + updatedAt: new Date() + }, + include: { + _count: { + select: { messages: true } + } + } + }) + + return NextResponse.json({ + success: true, + conversation: { + id: conversation.id, + title: conversation.title, + language: conversation.language, + messageCount: conversation._count.messages, + lastMessageAt: conversation.lastMessageAt, + createdAt: conversation.createdAt, + isActive: conversation.isActive + } + }) + + } catch (error) { + console.error('Error updating conversation:', error) + + if (error instanceof z.ZodError) { + return NextResponse.json( + { + success: false, + error: 'Invalid request format', + details: error.errors + }, + { status: 400 } + ) + } + + return NextResponse.json( + { + success: false, + error: 'Failed to update conversation' + }, + { status: 500 } + ) + } +} + +// DELETE /api/chat/conversations/[id] - Delete conversation +export async function DELETE( + request: NextRequest, + { params }: { params: { id: string } } +) { + try { + // Get user from authentication + const authHeader = request.headers.get('authorization') + if (!authHeader?.startsWith('Bearer ')) { + return NextResponse.json( + { success: false, error: 'Authentication required' }, + { status: 401 } + ) + } + + const token = authHeader.substring(7) + const payload = verifyToken(token) + + if (!payload) { + return NextResponse.json( + { success: false, error: 'Invalid token' }, + { status: 401 } + ) + } + + const userId = payload.userId as string + const conversationId = params.id + + // Verify conversation ownership + const existingConversation = await prisma.chatConversation.findUnique({ + where: { + id: conversationId, + userId + } + }) + + if (!existingConversation) { + return NextResponse.json( + { success: false, error: 'Conversation not found' }, + { status: 404 } + ) + } + + // Soft delete (mark as inactive) instead of hard delete to preserve data + await prisma.chatConversation.update({ + where: { id: conversationId }, + data: { + isActive: false, + updatedAt: new Date() + } + }) + + return NextResponse.json({ + success: true, + message: 'Conversation deleted successfully' + }) + + } catch (error) { + console.error('Error deleting conversation:', error) + return NextResponse.json( + { + success: false, + error: 'Failed to delete conversation' + }, + { status: 500 } + ) + } +} \ No newline at end of file diff --git a/app/api/chat/conversations/route.ts b/app/api/chat/conversations/route.ts new file mode 100644 index 0000000..d0ebbf0 --- /dev/null +++ b/app/api/chat/conversations/route.ts @@ -0,0 +1,206 @@ +import { NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { PrismaClient } from '@prisma/client' +import { verifyToken } from '@/lib/auth' + +const prisma = new PrismaClient() + +export const runtime = 'nodejs' + +const createConversationSchema = z.object({ + title: z.string().min(1).max(200), + language: z.enum(['ro', 'en']).default('ro'), +}) + +const getConversationsSchema = z.object({ + language: z.enum(['ro', 'en']).optional(), + limit: z.string().transform(Number).pipe(z.number().min(1).max(50)).optional().default(20), + offset: z.string().transform(Number).pipe(z.number().min(0)).optional().default(0), +}) + +// GET /api/chat/conversations - List user's conversations +export async function GET(request: NextRequest) { + try { + // Get user from authentication + const authHeader = request.headers.get('authorization') + if (!authHeader?.startsWith('Bearer ')) { + return NextResponse.json( + { success: false, error: 'Authentication required' }, + { status: 401 } + ) + } + + const token = authHeader.substring(7) + const payload = verifyToken(token) + + if (!payload) { + return NextResponse.json( + { success: false, error: 'Invalid token' }, + { status: 401 } + ) + } + + const userId = payload.userId as string + + // Parse query parameters + const url = new URL(request.url) + const queryParams = Object.fromEntries(url.searchParams.entries()) + const { language, limit, offset } = getConversationsSchema.parse(queryParams) + + // Build where clause + const where: any = { + userId, + isActive: true + } + + if (language) { + where.language = language + } + + // Get conversations with message count + const conversations = await prisma.chatConversation.findMany({ + where, + orderBy: { lastMessageAt: 'desc' }, + take: limit, + skip: offset, + include: { + _count: { + select: { messages: true } + }, + messages: { + take: 1, + orderBy: { timestamp: 'desc' }, + select: { + content: true, + role: true, + timestamp: true + } + } + } + }) + + // Get total count for pagination + const total = await prisma.chatConversation.count({ where }) + + return NextResponse.json({ + success: true, + conversations: conversations.map(conv => ({ + id: conv.id, + title: conv.title, + language: conv.language, + messageCount: conv._count.messages, + lastMessage: conv.messages[0] || null, + lastMessageAt: conv.lastMessageAt, + createdAt: conv.createdAt + })), + pagination: { + total, + limit, + offset, + hasMore: offset + limit < total + } + }) + + } catch (error) { + console.error('Error fetching conversations:', error) + + if (error instanceof z.ZodError) { + return NextResponse.json( + { + success: false, + error: 'Invalid request parameters', + details: error.errors + }, + { status: 400 } + ) + } + + return NextResponse.json( + { + success: false, + error: 'Failed to fetch conversations' + }, + { status: 500 } + ) + } +} + +// POST /api/chat/conversations - Create new conversation +export async function POST(request: NextRequest) { + try { + // Get user from authentication + const authHeader = request.headers.get('authorization') + if (!authHeader?.startsWith('Bearer ')) { + return NextResponse.json( + { success: false, error: 'Authentication required' }, + { status: 401 } + ) + } + + const token = authHeader.substring(7) + const payload = verifyToken(token) + + if (!payload) { + return NextResponse.json( + { success: false, error: 'Invalid token' }, + { status: 401 } + ) + } + + const userId = payload.userId as string + + // Parse request body + const body = await request.json() + const { title, language } = createConversationSchema.parse(body) + + // Create new conversation + const conversation = await prisma.chatConversation.create({ + data: { + userId, + title, + language, + lastMessageAt: new Date() + }, + include: { + _count: { + select: { messages: true } + } + } + }) + + return NextResponse.json({ + success: true, + conversation: { + id: conversation.id, + title: conversation.title, + language: conversation.language, + messageCount: conversation._count.messages, + lastMessage: null, + lastMessageAt: conversation.lastMessageAt, + createdAt: conversation.createdAt + } + }, { status: 201 }) + + } catch (error) { + console.error('Error creating conversation:', error) + + if (error instanceof z.ZodError) { + return NextResponse.json( + { + success: false, + error: 'Invalid request format', + details: error.errors + }, + { status: 400 } + ) + } + + return NextResponse.json( + { + success: false, + error: 'Failed to create conversation' + }, + { status: 500 } + ) + } +} \ No newline at end of file diff --git a/app/api/chat/route.ts b/app/api/chat/route.ts index 383fdcd..3822c09 100644 --- a/app/api/chat/route.ts +++ b/app/api/chat/route.ts @@ -1,12 +1,18 @@ import { NextRequest, NextResponse } from 'next/server' import { z } from 'zod' +import { PrismaClient, ChatMessageRole } from '@prisma/client' import { searchBibleHybrid, BibleVerse } from '@/lib/vector-search' +import { verifyToken } from '@/lib/auth' + +const prisma = new PrismaClient() export const runtime = 'nodejs' const chatRequestSchema = z.object({ message: z.string().min(1), + conversationId: z.string().optional(), locale: z.string().optional().default('ro'), + // Keep history for backward compatibility with frontend history: z.array(z.object({ id: z.string(), role: z.enum(['user', 'assistant']), @@ -18,15 +24,115 @@ const chatRequestSchema = z.object({ export async function POST(request: NextRequest) { try { const body = await request.json() - const { message, locale, history } = chatRequestSchema.parse(body) + const { message, conversationId, locale, history } = chatRequestSchema.parse(body) - // Generate response using Azure OpenAI with vector search - const response = await generateBiblicalResponse(message, locale, history) + // Try to get user from authentication (optional for backward compatibility) + let userId: string | null = null + const authHeader = request.headers.get('authorization') + if (authHeader?.startsWith('Bearer ')) { + try { + const token = authHeader.substring(7) + const payload = verifyToken(token) + userId = payload.userId + } catch (error) { + // Continue without authentication for backward compatibility + console.log('Chat without authentication') + } + } + + // Handle conversation logic + let finalConversationId = conversationId + let conversationHistory: any[] = [] + + if (userId) { + // User is authenticated - use conversation system + if (conversationId) { + // Load existing conversation + const conversation = await prisma.chatConversation.findUnique({ + where: { + id: conversationId, + userId, + isActive: true + }, + include: { + messages: { + orderBy: { timestamp: 'desc' }, + take: 15, // Last 15 messages for context + select: { + role: true, + content: true, + timestamp: true + } + } + } + }) + + if (conversation) { + conversationHistory = conversation.messages + .reverse() // Oldest first for context + .map(msg => ({ + role: msg.role.toLowerCase(), + content: msg.content, + timestamp: msg.timestamp.toISOString() + })) + } + } else { + // Create new conversation + const conversation = await prisma.chatConversation.create({ + data: { + userId, + title: generateConversationTitle(message), + language: locale, + lastMessageAt: new Date() + } + }) + finalConversationId = conversation.id + } + } else { + // Anonymous user - use provided history for backward compatibility + conversationHistory = history + } + + // Generate AI response + const aiResponse = await generateBiblicalResponse(message, locale, conversationHistory) + + // Save messages to database if user is authenticated + if (userId && finalConversationId) { + await prisma.$transaction([ + // Save user message + prisma.chatMessage.create({ + data: { + conversationId: finalConversationId, + userId, + role: ChatMessageRole.USER, + content: message, + timestamp: new Date() + } + }), + // Save AI response + prisma.chatMessage.create({ + data: { + conversationId: finalConversationId, + userId, + role: ChatMessageRole.ASSISTANT, + content: aiResponse, + timestamp: new Date() + } + }), + // Update conversation last message time + prisma.chatConversation.update({ + where: { id: finalConversationId }, + data: { lastMessageAt: new Date() } + }) + ]) + } return NextResponse.json({ success: true, - response + response: aiResponse, + conversationId: finalConversationId }) + } catch (error) { console.error('Error in chat API:', error) @@ -61,11 +167,8 @@ async function generateBiblicalResponse(message: string, locale: string, history .map(verse => `${verse.ref}: "${verse.text_raw}"`) .join('\n\n') - // Create conversation history for context - const conversationHistory = history - .slice(-3) // Last 3 messages for context - .map(msg => `${msg.role}: ${msg.content}`) - .join('\n') + // Intelligent context selection for conversation history + const conversationHistory = buildSmartContext(history, message, locale) // Create language-specific system prompts const systemPrompts = { @@ -126,7 +229,7 @@ Current question: ${message}` content: message } ], - max_tokens: 800, + max_tokens: 2000, temperature: 0.7, top_p: 0.9 }), @@ -161,3 +264,148 @@ Current question: ${message}` return fallbackResponses[locale as keyof typeof fallbackResponses] || fallbackResponses.en } } + +function generateConversationTitle(message: string): string { + // Generate a title from the first message (max 50 characters) + const title = message.length > 47 + ? message.substring(0, 47) + '...' + : message + + return title +} + +function buildSmartContext(history: any[], currentMessage: string, locale: string): string { + if (history.length === 0) return '' + + const MAX_CONTEXT_TOKENS = 1500 // Reserve tokens for context + const RECENT_MESSAGES_COUNT = 6 // Always include last 6 messages + + // Step 1: Always include the most recent messages for immediate context + const recentMessages = history.slice(-RECENT_MESSAGES_COUNT) + + // Step 2: Calculate relevance scores for older messages + const olderMessages = history.slice(0, -RECENT_MESSAGES_COUNT) + const relevantOlderMessages = findRelevantMessages(olderMessages, currentMessage, locale) + + // Step 3: Combine recent + relevant older messages + const selectedMessages = [...relevantOlderMessages, ...recentMessages] + + // Step 4: Apply token-based truncation if needed + const optimizedContext = optimizeContextForTokens(selectedMessages, MAX_CONTEXT_TOKENS) + + // Step 5: Format for AI consumption + return formatContextForAI(optimizedContext) +} + +function findRelevantMessages(messages: any[], currentMessage: string, locale: string): any[] { + if (messages.length === 0) return [] + + // Score messages based on relevance to current question + const scoredMessages = messages.map(msg => ({ + ...msg, + relevanceScore: calculateMessageRelevance(msg, currentMessage, locale) + })) + + // Sort by relevance and take top 3-5 most relevant older messages + return scoredMessages + .filter(msg => msg.relevanceScore > 0.3) // Only include somewhat relevant messages + .sort((a, b) => b.relevanceScore - a.relevanceScore) + .slice(0, 5) // Max 5 relevant older messages +} + +function calculateMessageRelevance(message: any, currentMessage: string, locale: string): number { + const msgContent = message.content.toLowerCase() + const currentContent = currentMessage.toLowerCase() + + let score = 0 + + // Keyword overlap scoring + const currentWords = currentContent.split(/\s+/).filter(word => word.length > 3) + const messageWords = msgContent.split(/\s+/) + + for (const word of currentWords) { + if (messageWords.some(mWord => mWord.includes(word) || word.includes(mWord))) { + score += 0.2 + } + } + + // Biblical reference detection (higher relevance) + const biblicalPatterns = locale === 'ro' + ? [/\b(geneza|exod|levitic|numeri|deuteronom|iosua|judecΔƒtori|rut|samuel|regi|cronici|ezra|neemia|estera|iov|psalmi|proverbe|ecclesiast|cΓ’ntarea|isaia|ieremia|plΓ’ngeri|ezechiel|daniel|osea|ioel|amos|obadia|iona|mica|naum|habacuc|Θ›efania|hagai|zaharia|maleahi|matei|marcu|luca|ioan|faptele|romani|corinteni|galateni|efeseni|filipeni|coloseni|tesaloniceni|timotei|tit|filimon|evrei|iacov|petru|ioan|iuda|apocalipsa)\s*\d+/gi] + : [/\b(genesis|exodus|leviticus|numbers|deuteronomy|joshua|judges|ruth|samuel|kings|chronicles|ezra|nehemiah|esther|job|psalm|proverbs|ecclesiastes|song|isaiah|jeremiah|lamentations|ezekiel|daniel|hosea|joel|amos|obadiah|jonah|micah|nahum|habakkuk|zephaniah|haggai|zechariah|malachi|matthew|mark|luke|john|acts|romans|corinthians|galatians|ephesians|philippians|colossians|thessalonians|timothy|titus|philemon|hebrews|james|peter|jude|revelation)\s*\d+/gi] + + if (biblicalPatterns.some(pattern => pattern.test(msgContent))) { + score += 0.4 + } + + // Recent user questions get higher relevance + if (message.role === 'user') { + score += 0.1 + } + + // Time decay (older messages get slightly lower scores) + const messageAge = Date.now() - new Date(message.timestamp).getTime() + const hoursAge = messageAge / (1000 * 60 * 60) + const timeDecay = Math.max(0.5, 1 - (hoursAge / 168)) // Decay over a week + + return Math.min(1.0, score * timeDecay) +} + +function optimizeContextForTokens(messages: any[], maxTokens: number): any[] { + // Rough token estimation (1 token β‰ˆ 4 characters in English, 3 in Romanian) + let currentTokens = 0 + const optimizedMessages = [] + + // Start from most recent and work backwards + for (let i = messages.length - 1; i >= 0; i--) { + const message = messages[i] + const estimatedTokens = Math.ceil(message.content.length / 3.5) + + if (currentTokens + estimatedTokens <= maxTokens) { + optimizedMessages.unshift(message) // Add to beginning to maintain order + currentTokens += estimatedTokens + } else { + // If this message would exceed limit, try to include a summary instead + if (i > 0 && optimizedMessages.length < 3) { + const summary = summarizeMessage(message) + const summaryTokens = Math.ceil(summary.length / 3.5) + if (currentTokens + summaryTokens <= maxTokens) { + optimizedMessages.unshift({ + ...message, + content: summary, + isSummary: true + }) + currentTokens += summaryTokens + } + } + break + } + } + + return optimizedMessages +} + +function summarizeMessage(message: any): string { + const content = message.content + if (content.length <= 100) return content + + // Extract key points and questions + const sentences = content.split(/[.!?]+/).filter(s => s.trim().length > 10) + if (sentences.length <= 2) return content + + // Keep first and last sentence, or most important parts + const summary = sentences.length > 3 + ? `${sentences[0].trim()}... ${sentences[sentences.length - 1].trim()}` + : sentences.slice(0, 2).join('. ').trim() + + return `[Summary] ${summary}` +} + +function formatContextForAI(messages: any[]): string { + if (messages.length === 0) return '' + + return messages.map(msg => { + const prefix = msg.isSummary ? '[Summary] ' : '' + return `${prefix}${msg.role}: ${msg.content}` + }).join('\n') +} diff --git a/components/chat/floating-chat.tsx b/components/chat/floating-chat.tsx index 134d1e4..0d1a08c 100644 --- a/components/chat/floating-chat.tsx +++ b/components/chat/floating-chat.tsx @@ -30,9 +30,11 @@ import { ThumbDown, Minimize, Launch, + History, } from '@mui/icons-material' import { useState, useRef, useEffect } from 'react' import { useTranslations, useLocale } from 'next-intl' +import ReactMarkdown from 'react-markdown' interface ChatMessage { id: string @@ -48,6 +50,7 @@ export default function FloatingChat() { const [isOpen, setIsOpen] = useState(false) const [isMinimized, setIsMinimized] = useState(false) const [isFullscreen, setIsFullscreen] = useState(false) + const [showHistory, setShowHistory] = useState(false) const [messages, setMessages] = useState([ { id: '1', @@ -240,6 +243,14 @@ export default function FloatingChat() { + setShowHistory(!showHistory)} + sx={{ color: 'white', mr: 0.5 }} + title="Chat History" + > + + {!isMinimized && ( + <> + {/* Chat History Panel */} + {showHistory && ( + + + Chat History + + + + πŸ“ Chat history will appear here + + + ✨ Sign in to save your conversations + + + + + )} + <> {/* Suggested Questions */} @@ -338,15 +382,62 @@ export default function FloatingChat() { maxWidth: '100%', }} > - - {message.content} - + {message.role === 'assistant' ? ( + ( + + {children} + + ), + h3: ({ children }) => ( + + {children} + + ), + strong: ({ children }) => ( + + {children} + + ), + ul: ({ children }) => ( + + {children} + + ), + li: ({ children }) => ( + + {children} + + ), + }} + > + {message.content} + + ) : ( + + {message.content} + + )} {message.role === 'assistant' && ( diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 71e9e93..685b3f9 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -24,6 +24,7 @@ model User { chapterBookmarks ChapterBookmark[] notes Note[] chatMessages ChatMessage[] + chatConversations ChatConversation[] prayerRequests PrayerRequest[] userPrayers UserPrayer[] readingHistory ReadingHistory[] @@ -126,17 +127,43 @@ model BiblePassage { @@index([testament]) } +model ChatConversation { + id String @id @default(uuid()) + userId String? // Optional for anonymous users + title String // Auto-generated from first message + language String // 'ro' or 'en' + isActive Boolean @default(true) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + lastMessageAt DateTime @default(now()) + + user User? @relation(fields: [userId], references: [id], onDelete: Cascade) + messages ChatMessage[] + + @@index([userId, language, lastMessageAt]) + @@index([isActive, lastMessageAt]) +} + model ChatMessage { - id String @id @default(uuid()) - userId String - role String // 'user' or 'assistant' - content String @db.Text - metadata Json? // Store verse references, etc. - createdAt DateTime @default(now()) + id String @id @default(uuid()) + conversationId String + userId String? // Keep for backward compatibility + role ChatMessageRole + content String @db.Text + metadata Json? // Store verse references, etc. + timestamp DateTime @default(now()) - user User @relation(fields: [userId], references: [id], onDelete: Cascade) + conversation ChatConversation @relation(fields: [conversationId], references: [id], onDelete: Cascade) + user User? @relation(fields: [userId], references: [id], onDelete: Cascade) - @@index([userId, createdAt]) + @@index([conversationId, timestamp]) + @@index([userId, timestamp]) +} + +enum ChatMessageRole { + USER + ASSISTANT + SYSTEM } model Bookmark { diff --git a/temp/ai-chat-improvements-plan.md b/temp/ai-chat-improvements-plan.md new file mode 100644 index 0000000..7c5d277 --- /dev/null +++ b/temp/ai-chat-improvements-plan.md @@ -0,0 +1,364 @@ +# AI Chat Improvements Plan + +## Overview +Enhance the AI chat system with persistent chat history and conversation memory to provide a more engaging and contextual user experience. + +## Current State Analysis +- βœ… Basic AI chat with Azure OpenAI integration +- βœ… ReactMarkdown rendering for formatted responses +- βœ… Floating chat component with fullscreen mode +- βœ… Language-specific responses (Romanian/English) +- ❌ No chat persistence between sessions +- ❌ Limited conversation memory (only last 3 messages) +- ❌ No user-specific chat history +- ❌ No chat management UI + +## Goals +1. **Persistent Chat History**: Store chat conversations in database per user and language +2. **Enhanced Memory**: Maintain longer conversation context for better AI responses +3. **Chat Management**: Allow users to view, organize, and manage their chat history +4. **Multi-language Support**: Separate chat histories for Romanian and English + +## Technical Implementation Plan + +### 1. Database Schema Extensions + +#### 1.1 Chat Conversations Table +```prisma +model ChatConversation { + id String @id @default(cuid()) + userId String? // Optional for anonymous users + title String // Auto-generated from first message + language String // 'ro' or 'en' + isActive Boolean @default(true) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + lastMessageAt DateTime @default(now()) + + user User? @relation(fields: [userId], references: [id], onDelete: Cascade) + messages ChatMessage[] + + @@index([userId, language, lastMessageAt]) + @@index([isActive, lastMessageAt]) +} + +model ChatMessage { + id String @id @default(cuid()) + conversationId String + role ChatMessageRole + content String @db.Text + timestamp DateTime @default(now()) + metadata Json? // For storing additional context + + conversation ChatConversation @relation(fields: [conversationId], references: [id], onDelete: Cascade) + + @@index([conversationId, timestamp]) +} + +enum ChatMessageRole { + USER + ASSISTANT + SYSTEM +} +``` + +#### 1.2 Update User Model +```prisma +model User { + // ... existing fields + chatConversations ChatConversation[] +} +``` + +### 2. API Endpoints + +#### 2.1 Chat Conversations API (`/api/chat/conversations`) +- **GET**: List user's conversations (paginated, filtered by language) +- **POST**: Create new conversation +- **DELETE /:id**: Delete conversation + +#### 2.2 Enhanced Chat API (`/api/chat`) +- **Modify existing POST**: Include conversation management +- **GET /:conversationId**: Get conversation history +- **PUT /:conversationId**: Update conversation (rename, etc.) + +#### 2.3 Chat Messages API (`/api/chat/:conversationId/messages`) +- **GET**: Get all messages in conversation (paginated) +- **POST**: Add message to conversation + +### 3. Frontend Components + +#### 3.1 Enhanced Floating Chat Component +```typescript +interface FloatingChatProps { + conversationId?: string + initialMessage?: string +} + +// New state management +const [conversations, setConversations] = useState([]) +const [activeConversationId, setActiveConversationId] = useState(null) +const [messages, setMessages] = useState([]) +const [isHistoryOpen, setIsHistoryOpen] = useState(false) +``` + +#### 3.2 Chat History Sidebar +```typescript +interface ChatHistorySidebarProps { + conversations: Conversation[] + activeConversationId: string | null + onSelectConversation: (id: string) => void + onNewConversation: () => void + onDeleteConversation: (id: string) => void + language: string +} +``` + +#### 3.3 Conversation List Item +```typescript +interface ConversationListItemProps { + conversation: Conversation + isActive: boolean + onClick: () => void + onDelete: () => void + onRename: (newTitle: string) => void +} +``` + +### 4. Implementation Phases + +#### Phase 1: Database Schema & Basic API +1. **Create Prisma migrations** for new tables +2. **Implement conversation CRUD APIs** +3. **Add database seed scripts** for testing +4. **Update existing chat API** to work with conversations + +#### Phase 2: Enhanced Memory System +1. **Modify chat API** to include more conversation context +2. **Implement intelligent context selection** (last 10-15 messages) +3. **Add conversation summarization** for very long chats +4. **Optimize token usage** with smart context management + +#### Phase 3: Frontend Chat Management +1. **Add conversation state management** to floating chat +2. **Implement chat history sidebar** +3. **Add conversation creation/deletion** functionality +4. **Implement conversation switching** within chat + +#### Phase 4: Advanced Features +1. **Auto-generate conversation titles** from first message +2. **Add conversation search/filtering** +3. **Implement conversation sharing** (optional) +4. **Add conversation export** functionality + +### 5. Detailed Implementation Steps + +#### 5.1 Database Setup +```bash +# Create migration +npx prisma migrate dev --name add-chat-conversations + +# Update database +npx prisma db push + +# Generate client +npx prisma generate +``` + +#### 5.2 API Implementation Strategy + +**Chat API Enhancement (`/api/chat/route.ts`)**: +```typescript +// Modified request schema +const chatRequestSchema = z.object({ + message: z.string().min(1), + conversationId: z.string().optional(), + locale: z.string().default('ro'), + userId: z.string().optional() +}) + +// Enhanced response +interface ChatResponse { + success: boolean + response: string + conversationId: string + messageId: string +} +``` + +**Conversation Management Logic**: +```typescript +async function handleChatMessage( + message: string, + conversationId?: string, + locale: string = 'ro', + userId?: string +) { + // 1. Get or create conversation + const conversation = conversationId + ? await getConversation(conversationId) + : await createConversation(userId, locale, message) + + // 2. Get conversation history (last 15 messages) + const history = await getConversationHistory(conversation.id, 15) + + // 3. Generate AI response with full context + const aiResponse = await generateBiblicalResponse(message, locale, history) + + // 4. Save both user message and AI response + await saveMessages(conversation.id, [ + { role: 'user', content: message }, + { role: 'assistant', content: aiResponse } + ]) + + // 5. Update conversation metadata + await updateConversationActivity(conversation.id) + + return { response: aiResponse, conversationId: conversation.id } +} +``` + +#### 5.3 Frontend State Management + +**Enhanced Floating Chat Hook**: +```typescript +function useFloatingChat() { + const [conversations, setConversations] = useState([]) + const [activeConversation, setActiveConversation] = useState(null) + const [messages, setMessages] = useState([]) + const [isLoading, setIsLoading] = useState(false) + + const loadConversations = useCallback(async () => { + // Fetch user's conversations + }, []) + + const switchConversation = useCallback(async (conversationId: string) => { + // Load conversation messages + }, []) + + const startNewConversation = useCallback(() => { + // Reset state for new conversation + }, []) + + const sendMessage = useCallback(async (message: string) => { + // Send message with conversation context + }, [activeConversation]) + + return { + conversations, + activeConversation, + messages, + isLoading, + loadConversations, + switchConversation, + startNewConversation, + sendMessage + } +} +``` + +### 6. UI/UX Enhancements + +#### 6.1 Chat History Sidebar Layout +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Chat History β”‚ Active Chat β”‚ +β”‚ β”‚ β”‚ +β”‚ β—‹ New Chat β”‚ β”Œβ”€ Messages ─────┐ β”‚ +β”‚ β”‚ β”‚ β”‚ β”‚ +β”‚ ╔═ Today ═══ β”‚ β”‚ User: Question β”‚ β”‚ +β”‚ β•‘ β—‹ Bible Q&A β”‚ β”‚ AI: Response β”‚ β”‚ +β”‚ β•‘ β—‹ Prayer Help β”‚ β”‚ ... β”‚ β”‚ +β”‚ β•šβ•β•β•β•β•β•β•β•β•β•β• β”‚ β”‚ β”‚ β”‚ +β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ ╔═ Yesterday ═ β”‚ β”Œβ”€ Input ──────────┐ β”‚ +β”‚ β•‘ β—‹ Verse Study β”‚ β”‚ [Type message... ]β”‚ β”‚ +β”‚ β•šβ•β•β•β•β•β•β•β•β•β•β• β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +#### 6.2 Mobile-Responsive Design +- **Mobile**: Stack history as overlay/drawer +- **Tablet**: Side-by-side with collapsed history +- **Desktop**: Full side-by-side layout + +### 7. Performance Considerations + +#### 7.1 Database Optimization +- **Indexes**: conversation lookups, message pagination +- **Pagination**: Limit conversation and message queries +- **Cleanup**: Archive old conversations after 6 months + +#### 7.2 Frontend Optimization +- **Lazy loading**: Load conversations on demand +- **Virtualization**: For large message lists +- **Caching**: Cache conversation metadata + +#### 7.3 AI Context Management +- **Smart truncation**: Summarize old messages if context too long +- **Relevance scoring**: Prioritize recent and relevant messages +- **Token optimization**: Balance context richness vs cost + +### 8. Security & Privacy + +#### 8.1 Data Protection +- **User isolation**: Strict user-based access control +- **Encryption**: Sensitive conversation content +- **Retention policy**: Auto-delete after configurable period + +#### 8.2 Anonymous Users +- **Session-based storage**: For non-authenticated users +- **Migration path**: Convert anonymous chats to user accounts + +### 9. Testing Strategy + +#### 9.1 Unit Tests +- Conversation CRUD operations +- Message history management +- AI context generation + +#### 9.2 Integration Tests +- Full chat flow with persistence +- Conversation switching +- Multi-language handling + +#### 9.3 User Testing +- Chat history navigation +- Conversation management +- Mobile responsiveness + +### 10. Deployment Considerations + +#### 10.1 Migration Strategy +- **Backward compatibility**: Existing chat continues to work +- **Data migration**: Convert existing session data if applicable +- **Feature flags**: Gradual rollout of new features + +#### 10.2 Monitoring +- **Conversation metrics**: Creation, length, engagement +- **Performance monitoring**: API response times +- **Error tracking**: Failed chat operations + +## Success Metrics + +1. **User Engagement**: Longer chat sessions, return conversations +2. **Conversation Quality**: Better AI responses with context +3. **User Satisfaction**: Positive feedback on chat experience +4. **Technical Performance**: Fast conversation loading, reliable persistence + +## Timeline Estimate + +- **Phase 1** (Database & API): 3-4 days +- **Phase 2** (Enhanced Memory): 2-3 days +- **Phase 3** (Frontend Management): 4-5 days +- **Phase 4** (Advanced Features): 3-4 days + +**Total**: ~2-3 weeks for complete implementation + +## Next Steps + +1. Review and approve this plan +2. Begin with Phase 1: Database schema and basic API +3. Implement incremental improvements +4. Test thoroughly at each phase +5. Gather user feedback and iterate \ No newline at end of file