import { useState, useEffect, useCallback } from 'react'
import { supabase } from '@/lib/supabase'
import { Chat, Message, MessageUI } from '@/lib/types/database'
import { generateChatName } from '@/lib/utils/chatNaming'
interface UseChatProps {
bookId?: string
userId?: string
}
interface UseChatReturn {
// Current chat state
currentChat: Chat | null
messages: MessageUI[]
isLoading: boolean
error: string | null
// Chat history
chatHistory: Chat[]
isLoadingHistory: boolean
// Actions
createNewChat: (title?: string) => Promise<Chat | null>
loadChat: (chatId: string) => Promise<void>
saveMessage: (message: Omit<MessageUI, 'id'>) => Promise<void>
updateChatTitle: (title: string) => Promise<void>
deleteChat: (chatId: string) => Promise<void>
loadChatHistory: () => Promise<Chat[]>
updateChatModel: (model: string) => Promise<void>
}
export function useChat({ bookId, userId }: UseChatProps): UseChatReturn {
const [currentChat, setCurrentChat] = useState<Chat | null>(null)
const [messages, setMessages] = useState<MessageUI[]>([])
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const [hasInitialized, setHasInitialized] = useState(false)
const [chatHistory, setChatHistory] = useState<Chat[]>([])
const [isLoadingHistory, setIsLoadingHistory] = useState(false)
// Convert database message to UI message
const convertMessage = useCallback((dbMessage: Message): MessageUI => ({
id: dbMessage.id,
type: dbMessage.type,
content: dbMessage.content,
timestamp: new Date(dbMessage.created_at),
model: dbMessage.model || undefined,
tool_results: dbMessage.tool_results || undefined,
context_info: dbMessage.context_info || undefined,
}), [])
// Load chat history for the current book
const loadChatHistory = useCallback(async () => {
if (!bookId || !userId) {
setChatHistory([])
return []
}
setIsLoadingHistory(true)
try {
const { data, error } = await supabase
.from('chats')
.select('*')
.eq('book_id', bookId)
.eq('user_id', userId)
.order('updated_at', { ascending: false })
if (error) throw error
setChatHistory(data || [])
return data || []
} catch (err) {
console.error('Error loading chat history:', err)
setError('Failed to load chat history')
return []
} finally {
setIsLoadingHistory(false)
}
}, [bookId, userId])
// Load an existing chat
const loadChat = useCallback(async (chatId: string) => {
setIsLoading(true)
setError(null)
try {
// Load chat metadata
const { data: chatData, error: chatError } = await supabase
.from('chats')
.select('*')
.eq('id', chatId)
.single()
if (chatError) throw chatError
setCurrentChat(chatData as Chat)
// Load messages for this chat
const { data: messagesData, error: messagesError } = await supabase
.from('messages')
.select('*')
.eq('chat_id', chatId)
.order('sequence_number', { ascending: true })
if (messagesError) throw messagesError
const uiMessages = (messagesData || []).map(convertMessage)
setMessages(uiMessages)
} catch (err) {
console.error('Error loading chat:', err)
setError('Failed to load chat')
} finally {
setIsLoading(false)
}
}, [convertMessage])
// Create a new chat
const createNewChat = useCallback(async (title = 'New Chat'): Promise<Chat | null> => {
if (!bookId || !userId) {
setError('Book ID and User ID are required')
return null
}
try {
// Check if there's already an empty chat (0 messages)
const { data: existingChats, error: checkError } = await supabase
.from('chats')
.select('*')
.eq('book_id', bookId)
.eq('user_id', userId)
.eq('total_messages', 0)
.order('created_at', { ascending: false })
.limit(1)
if (checkError) throw checkError
// If there's already an empty chat, just switch to it instead of creating a new one
if (existingChats && existingChats.length > 0) {
const existingEmptyChat = existingChats[0] as Chat
console.log('Found existing empty chat, switching to it:', existingEmptyChat.title)
setCurrentChat(existingEmptyChat)
setMessages([]) // Clear any old messages
return existingEmptyChat
}
// Create a new chat only if no empty chat exists
const { data, error } = await supabase
.from('chats')
.insert({
book_id: bookId,
user_id: userId,
title,
})
.select()
.single()
if (error) throw error
const newChat = data as Chat
setCurrentChat(newChat)
setMessages([])
// Refresh chat history to include the new chat
await loadChatHistory()
return newChat
} catch (err) {
console.error('Error creating chat:', err)
setError('Failed to create new chat')
return null
}
}, [bookId, userId, loadChatHistory])
// Load the most recent chat on initialization
const loadMostRecentChat = useCallback(async () => {
if (!bookId || !userId || hasInitialized) return
setIsLoading(true)
try {
const chats = await loadChatHistory()
if (chats.length > 0) {
// Load the most recent chat
const mostRecentChat = chats[0]
await loadChat(mostRecentChat.id)
} else {
// If no chats exist, create an initial chat automatically
console.log('No chats found for book, creating initial chat...')
const initialChat = await createNewChat('New Chat')
if (initialChat) {
console.log('Initial chat created successfully:', initialChat.title)
}
}
setHasInitialized(true)
} catch (err) {
console.error('Error loading most recent chat:', err)
setHasInitialized(true)
} finally {
setIsLoading(false)
}
}, [bookId, userId, hasInitialized, loadChatHistory, loadChat, createNewChat])
// Save a new message
const saveMessage = useCallback(async (message: Omit<MessageUI, 'id'>) => {
if (!currentChat) {
setError('No active chat')
return
}
try {
// Calculate sequence number
const sequenceNumber = messages.length + 1
const { data, error } = await supabase
.from('messages')
.insert({
chat_id: currentChat.id,
type: message.type,
content: message.content,
sequence_number: sequenceNumber,
model: message.model || null,
tool_results: message.tool_results || null,
context_info: message.context_info || null,
})
.select()
.single()
if (error) throw error
// Add to local state
const newMessage = convertMessage(data as Message)
setMessages(prev => [...prev, newMessage])
// Generate AI-powered chat title based on first user message if it's still "New Chat"
if (currentChat.title === 'New Chat' && message.type === 'user' && messages.length === 0) {
console.log('Generating AI-powered chat name for:', message.content)
try {
const generatedTitle = await generateChatName(message.content)
console.log('Generated chat name:', generatedTitle)
await updateChatTitle(generatedTitle)
} catch (err) {
console.error('Error generating chat name, using fallback:', err)
// Fallback to truncated message if AI generation fails
const fallbackTitle = message.content.length > 50
? message.content.substring(0, 50) + '...'
: message.content
await updateChatTitle(fallbackTitle)
}
}
} catch (err) {
console.error('Error saving message:', err)
setError('Failed to save message')
}
}, [currentChat, messages.length, convertMessage])
// Update chat title
const updateChatTitle = useCallback(async (title: string) => {
if (!currentChat) return
try {
const { error } = await supabase
.from('chats')
.update({ title })
.eq('id', currentChat.id)
if (error) throw error
setCurrentChat(prev => prev ? { ...prev, title } : null)
// Refresh chat history to show updated title
await loadChatHistory()
} catch (err) {
console.error('Error updating chat title:', err)
setError('Failed to update chat title')
}
}, [currentChat, loadChatHistory])
// Delete a chat
const deleteChat = useCallback(async (chatId: string) => {
try {
const { error } = await supabase
.from('chats')
.delete()
.eq('id', chatId)
if (error) throw error
// If deleting current chat, clear it
if (currentChat?.id === chatId) {
setCurrentChat(null)
setMessages([])
}
// Refresh chat history
await loadChatHistory()
} catch (err) {
console.error('Error deleting chat:', err)
setError('Failed to delete chat')
}
}, [currentChat, loadChatHistory])
// Update chat model
const updateChatModel = useCallback(async (model: string) => {
if (!currentChat) return
try {
const { error } = await supabase
.from('chats')
.update({ model })
.eq('id', currentChat.id)
if (error) throw error
setCurrentChat(prev => prev ? { ...prev, model } : null)
// Refresh chat history to show updated model
await loadChatHistory()
} catch (err) {
console.error('Error updating chat model:', err)
setError('Failed to update chat model')
}
}, [currentChat, loadChatHistory])
// Initialize chat when bookId and userId are available
useEffect(() => {
if (bookId && userId && !hasInitialized) {
loadMostRecentChat()
}
}, [bookId, userId, hasInitialized, loadMostRecentChat])
return {
currentChat,
messages,
isLoading,
error,
chatHistory,
isLoadingHistory,
createNewChat,
loadChat,
saveMessage,
updateChatTitle,
deleteChat,
loadChatHistory,
updateChatModel,
}
}