'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>
)
}