bookwiz.io / components / ChatPanel.tsx
ChatPanel.tsx
Raw
'use client'

import React, { useState, useCallback } from 'react'
import { supabase } from '@/lib/supabase'
import { DEFAULT_MODEL } from '@/lib/config/models'

// Hooks
import { useAuth } from '@/components/AuthProvider'
import { useProfile } from '@/lib/hooks/useProfile'
import { useFileOperations } from '@/lib/hooks/useFileOperations'
import { useChatController } from '@/lib/hooks/useChatController'

// Components
import ChatHistory from './ChatHistory'
import ChatMessages from './chat/ChatMessages'
import ChatInput from './chat/ChatInput'
import ChatHeader from './chat/ChatHeader'
import UsageLimitWarning from './chat/UsageLimitWarning'

interface ChatPanelProps {
  bookId?: string
  onFileOperationComplete?: () => void
  onViewDiff?: (filePath: string, oldContent: string, newContent: string) => void
  onFileSelect?: (file: any) => void
}

export default function ChatPanel({ 
  bookId, 
  onFileOperationComplete, 
  onViewDiff, 
  onFileSelect 
}: ChatPanelProps) {
  const { user } = useAuth()
  const { profile } = useProfile()
  
  // File operations
  const {
    includedFiles,
    includedFileNames,
    isDragOver,
    removeIncludedFile,
    getDisplayName,
    handleDragOver,
    handleDragEnter,
    handleDragLeave,
    handleDrop,
  } = useFileOperations()

  // Chat controller - consolidates all chat logic
  const {
    currentChat,
    allDisplayMessages,
    chatHistory,
    isLoadingHistory,
    chatError,
    isStreaming,
    usageInfo,
    usageLimitError,
    sendMessage,
    createNewChat,
    loadChat,
    updateChatTitle,
    deleteChat,
    updateChatModel,
    deleteMessage,
    setUsageLimitError,
    stopExecution,
    isLoading
  } = useChatController({ bookId, includedFiles })

  // Local UI state - only what's needed for UI
  const [inputValue, setInputValue] = useState('')
  const [selectedModel, setSelectedModel] = useState(DEFAULT_MODEL.name)
  const [isHistoryOpen, setIsHistoryOpen] = useState(false)

  // Simplified handlers
  const handleSendMessage = useCallback(async (e: React.FormEvent) => {
    e.preventDefault()
    if (!inputValue.trim() || isLoading) return

    const messageContent = inputValue.trim()
    setInputValue('')

    try {
      await sendMessage(messageContent, selectedModel)
    } catch (error) {
      console.error('Send message error:', error)
      setInputValue(messageContent) // Restore input on error
    }
  }, [inputValue, isLoading, sendMessage, selectedModel])

  const handleKeyDown = useCallback((e: React.KeyboardEvent<HTMLTextAreaElement>) => {
    if (e.key === 'Enter' && !e.shiftKey) {
      e.preventDefault()
      if (!isLoading && inputValue.trim()) {
        handleSendMessage(e as any)
      }
    }
  }, [isLoading, inputValue, handleSendMessage])

  const handleModelChange = useCallback(async (modelName: string) => {
    setSelectedModel(modelName)
    if (currentChat) {
      await updateChatModel(modelName)
    }
  }, [currentChat, updateChatModel])

  const handleNewChat = useCallback(async () => {
    if (bookId) {
      await createNewChat()
    }
  }, [bookId, createNewChat])

  const handleLoadChat = useCallback(async (chatId: string) => {
    await loadChat(chatId)
    setIsHistoryOpen(false)
  }, [loadChat])

  const handleFileClick = useCallback(async (fileId: string, fileName: string) => {
    if (!bookId || !onFileSelect) return
    
    try {
      const { data: { session } } = await supabase.auth.getSession()
      if (!session) return

      const response = await fetch(`/api/books/${bookId}/files/${fileId}`, {
        headers: { 'Authorization': `Bearer ${session.access_token}` }
      })

      if (!response.ok) throw new Error('Failed to fetch file')

      const fileData = await response.json()
      onFileSelect({ ...fileData, name: fileName })
    } catch (error) {
      console.error('Error fetching file:', error)
    }
  }, [bookId, onFileSelect])

  // User avatar helpers
  const getAvatarUrl = useCallback(() => {
    return user?.user_metadata?.avatar_url || profile?.avatar_url || null
  }, [user?.user_metadata?.avatar_url, profile?.avatar_url])

  const getInitials = useCallback(() => {
    const name = user?.user_metadata?.full_name || profile?.full_name || user?.email || 'User'
    return name.split(' ').map((n: string) => n[0]).join('').toUpperCase().slice(0, 2)
  }, [user?.user_metadata?.full_name, profile?.full_name, user?.email])

  return (
    <div 
      className="flex flex-col flex-1 md:flex-none md:h-full bg-gray-50 dark:bg-gray-900 relative min-h-0 overflow-hidden"
      onDragOver={handleDragOver}
      onDragEnter={handleDragEnter}
      onDragLeave={handleDragLeave}
      onDrop={handleDrop}
    >
      {/* Drag overlay */}
      {isDragOver && (
        <div className="absolute inset-0 bg-blue-500/20 border-2 border-dashed border-blue-500 z-50 flex items-center justify-center">
          <div className="bg-white dark:bg-gray-800 p-4 rounded-lg shadow-lg">
            <p className="text-lg font-semibold text-blue-600 dark:text-blue-400">
              Drop files to include in your conversation
            </p>
          </div>
        </div>
      )}

      {/* Header */}
      <ChatHeader
        currentChatTitle={currentChat?.title}
        bookId={bookId}
        userId={user?.id}
        chatError={chatError || undefined}
        onNewChat={handleNewChat}
        onOpenHistory={() => setIsHistoryOpen(!isHistoryOpen)}
      />

      {/* Usage limit warning */}
      {usageLimitError && (
        <UsageLimitWarning 
          usageLimitError={usageLimitError}
          onDismiss={() => setUsageLimitError(null)}
        />
      )}

      {/* Messages */}
      <ChatMessages
        allDisplayMessages={allDisplayMessages}
        isStreaming={isStreaming}
        currentChat={currentChat}
        bookId={bookId}
        contextInfo={null} // Remove unused contextInfo
        toolResults={[]} // Remove unused toolResults
        onViewDiff={onViewDiff}
        onFileClick={handleFileClick}
        getAvatarUrl={getAvatarUrl}
        getInitials={getInitials}
        onDeleteMessage={deleteMessage}
      />

      {/* Input */}
      <ChatInput
        inputValue={inputValue}
        setInputValue={setInputValue}
        isLoading={isLoading}
        currentChat={currentChat}
        selectedModel={selectedModel}
        onModelChange={handleModelChange}
        usageInfo={usageInfo}
        bookId={bookId}
        includedFiles={includedFiles}
        includedFileNames={includedFileNames}
        getDisplayName={getDisplayName}
        removeIncludedFile={removeIncludedFile}
        isDragOver={isDragOver}
        handleDragOver={handleDragOver}
        handleDragEnter={handleDragEnter}
        handleDragLeave={handleDragLeave}
        handleDrop={handleDrop}
        onSubmit={handleSendMessage}
        onKeyDown={handleKeyDown}
        stopExecution={stopExecution}
      />

      {/* Chat History Sidebar */}
      {isHistoryOpen && (
        <div className="absolute top-0 left-0 w-80 h-full max-h-full bg-white dark:bg-gray-800 border-r border-gray-200 dark:border-gray-700 z-40 shadow-lg overflow-hidden">
          <ChatHistory
            chatHistory={chatHistory}
            currentChatId={currentChat?.id}
            isLoading={isLoadingHistory}
            isOpen={isHistoryOpen}
            onLoadChat={handleLoadChat}
            onDeleteChat={deleteChat}
            onUpdateChatTitle={(chatId: string, title: string) => updateChatTitle(title)}
            onClose={() => setIsHistoryOpen(false)}
          />
        </div>
      )}
    </div>
  )
}