bookwiz.io / lib / hooks / useChat.ts
useChat.ts
Raw
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,
  }
}