From 32717916eaebefe148e7a58aff792d50742aacb1 Mon Sep 17 00:00:00 2001 From: Damjan Savic Date: Sun, 25 Jan 2026 00:51:23 +0100 Subject: [PATCH] auto-claude: subtask-4-3 - Create Supabase chat database operations module Co-Authored-By: Claude Opus 4.5 --- src/lib/supabase/chat.ts | 334 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 334 insertions(+) create mode 100644 src/lib/supabase/chat.ts diff --git a/src/lib/supabase/chat.ts b/src/lib/supabase/chat.ts new file mode 100644 index 0000000..1b10dc8 --- /dev/null +++ b/src/lib/supabase/chat.ts @@ -0,0 +1,334 @@ +import { createClient as createServerClient } from '@supabase/supabase-js'; + +// Type definitions for chat tables +export type ChatRole = 'user' | 'assistant' | 'system'; + +export interface ChatSession { + id: string; + created_at: string; + visitor_id: string; + metadata: Record; +} + +export interface ChatMessage { + id: string; + session_id: string; + role: ChatRole; + content: string; + created_at: string; +} + +// Input types for creating records +export interface CreateChatSessionInput { + visitor_id: string; + metadata?: Record; +} + +export interface CreateChatMessageInput { + session_id: string; + role: ChatRole; + content: string; +} + +// Create a Supabase client with service role key for server-side operations +// This bypasses RLS for administrative operations +function createServiceClient() { + const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; + const serviceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY; + + if (!supabaseUrl || !serviceRoleKey) { + throw new Error( + 'Missing Supabase environment variables: NEXT_PUBLIC_SUPABASE_URL or SUPABASE_SERVICE_ROLE_KEY' + ); + } + + return createServerClient(supabaseUrl, serviceRoleKey, { + auth: { + autoRefreshToken: false, + persistSession: false, + }, + }); +} + +// ============================================ +// Chat Session Operations +// ============================================ + +/** + * Create a new chat session for a visitor + */ +export async function createChatSession( + input: CreateChatSessionInput +): Promise { + const supabase = createServiceClient(); + + const { data, error } = await supabase + .from('chat_sessions') + .insert({ + visitor_id: input.visitor_id, + metadata: input.metadata ?? {}, + }) + .select() + .single(); + + if (error) { + throw new Error(`Failed to create chat session: ${error.message}`); + } + + return data as ChatSession; +} + +/** + * Get a chat session by ID + */ +export async function getChatSession( + sessionId: string +): Promise { + const supabase = createServiceClient(); + + const { data, error } = await supabase + .from('chat_sessions') + .select() + .eq('id', sessionId) + .single(); + + if (error) { + if (error.code === 'PGRST116') { + // No rows found + return null; + } + throw new Error(`Failed to get chat session: ${error.message}`); + } + + return data as ChatSession; +} + +/** + * Get or create a chat session for a visitor + * Useful for maintaining session continuity + */ +export async function getOrCreateChatSession( + visitorId: string, + metadata?: Record +): Promise { + const supabase = createServiceClient(); + + // First, try to find an existing active session for this visitor + // Get the most recent session from the last 24 hours + const twentyFourHoursAgo = new Date( + Date.now() - 24 * 60 * 60 * 1000 + ).toISOString(); + + const { data: existingSession, error: findError } = await supabase + .from('chat_sessions') + .select() + .eq('visitor_id', visitorId) + .gte('created_at', twentyFourHoursAgo) + .order('created_at', { ascending: false }) + .limit(1) + .single(); + + if (existingSession && !findError) { + return existingSession as ChatSession; + } + + // No existing session found, create a new one + return createChatSession({ + visitor_id: visitorId, + metadata, + }); +} + +/** + * Update session metadata + */ +export async function updateSessionMetadata( + sessionId: string, + metadata: Record +): Promise { + const supabase = createServiceClient(); + + const { data, error } = await supabase + .from('chat_sessions') + .update({ metadata }) + .eq('id', sessionId) + .select() + .single(); + + if (error) { + throw new Error(`Failed to update session metadata: ${error.message}`); + } + + return data as ChatSession; +} + +// ============================================ +// Chat Message Operations +// ============================================ + +/** + * Add a message to a chat session + */ +export async function addChatMessage( + input: CreateChatMessageInput +): Promise { + const supabase = createServiceClient(); + + const { data, error } = await supabase + .from('chat_messages') + .insert({ + session_id: input.session_id, + role: input.role, + content: input.content, + }) + .select() + .single(); + + if (error) { + throw new Error(`Failed to add chat message: ${error.message}`); + } + + return data as ChatMessage; +} + +/** + * Get all messages for a chat session + * Returns messages in chronological order + */ +export async function getChatMessages( + sessionId: string +): Promise { + const supabase = createServiceClient(); + + const { data, error } = await supabase + .from('chat_messages') + .select() + .eq('session_id', sessionId) + .order('created_at', { ascending: true }); + + if (error) { + throw new Error(`Failed to get chat messages: ${error.message}`); + } + + return (data ?? []) as ChatMessage[]; +} + +/** + * Get the last N messages for a session + * Useful for maintaining context window limits + */ +export async function getRecentMessages( + sessionId: string, + limit: number = 20 +): Promise { + const supabase = createServiceClient(); + + const { data, error } = await supabase + .from('chat_messages') + .select() + .eq('session_id', sessionId) + .order('created_at', { ascending: false }) + .limit(limit); + + if (error) { + throw new Error(`Failed to get recent messages: ${error.message}`); + } + + // Reverse to get chronological order + return ((data ?? []) as ChatMessage[]).reverse(); +} + +/** + * Delete all messages for a session (clear history) + */ +export async function clearChatMessages(sessionId: string): Promise { + const supabase = createServiceClient(); + + const { error } = await supabase + .from('chat_messages') + .delete() + .eq('session_id', sessionId); + + if (error) { + throw new Error(`Failed to clear chat messages: ${error.message}`); + } +} + +/** + * Delete a chat session and all its messages + * Messages are deleted automatically via ON DELETE CASCADE + */ +export async function deleteChatSession(sessionId: string): Promise { + const supabase = createServiceClient(); + + const { error } = await supabase + .from('chat_sessions') + .delete() + .eq('id', sessionId); + + if (error) { + throw new Error(`Failed to delete chat session: ${error.message}`); + } +} + +// ============================================ +// Utility Functions +// ============================================ + +/** + * Get conversation history formatted for AI context + * Converts database messages to the format expected by chat completion APIs + */ +export async function getConversationHistory( + sessionId: string, + maxMessages: number = 20 +): Promise> { + const messages = await getRecentMessages(sessionId, maxMessages); + + return messages.map((msg) => ({ + role: msg.role, + content: msg.content, + })); +} + +/** + * Save a conversation turn (user message + assistant response) + * Convenience function for typical chat flow + */ +export async function saveConversationTurn( + sessionId: string, + userMessage: string, + assistantResponse: string +): Promise<{ userMsg: ChatMessage; assistantMsg: ChatMessage }> { + const userMsg = await addChatMessage({ + session_id: sessionId, + role: 'user', + content: userMessage, + }); + + const assistantMsg = await addChatMessage({ + session_id: sessionId, + role: 'assistant', + content: assistantResponse, + }); + + return { userMsg, assistantMsg }; +} + +// Default export for convenience +export default { + // Session operations + createChatSession, + getChatSession, + getOrCreateChatSession, + updateSessionMetadata, + deleteChatSession, + // Message operations + addChatMessage, + getChatMessages, + getRecentMessages, + clearChatMessages, + // Utility functions + getConversationHistory, + saveConversationTurn, +};