'use client'
import React, { useState, useEffect } from 'react'
import { useRouter, usePathname } from 'next/navigation'
import { useAuth } from '@/components/AuthProvider'
import { Chat } from '@/lib/types/database'
import { ChatContext, type ChatContextType } from '@/lib/contexts/ChatContext'
import { IoAddOutline, IoTrashOutline, IoPencilOutline, IoCheckmarkOutline, IoMenuOutline, IoCloseOutline, IoChatbubblesOutline } from 'react-icons/io5'
import DeleteConfirmationDialog from '@/components/DeleteConfirmationDialog'
interface ChatLayoutProps {
children: React.ReactNode
}
export default function ChatLayout({ children }: ChatLayoutProps) {
const { user } = useAuth()
const router = useRouter()
const pathname = usePathname()
const [chats, setChats] = useState<Chat[]>([])
const [currentChatId, setCurrentChatId] = useState<string | null>(null)
const [isLoading, setIsLoading] = useState(true)
const [editingChatId, setEditingChatId] = useState<string | null>(null)
const [editingTitle, setEditingTitle] = useState('')
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false)
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false)
const [chatToDelete, setChatToDelete] = useState<Chat | null>(null)
// Extract chat ID from pathname
useEffect(() => {
const pathParts = pathname.split('/')
const chatIdFromPath = pathParts[pathParts.length - 1]
// Only set if it's a valid UUID-like string (not 'chat')
if (chatIdFromPath && chatIdFromPath !== 'chat' && chatIdFromPath.length > 10) {
setCurrentChatId(chatIdFromPath)
} else {
setCurrentChatId(null)
}
}, [pathname])
// Fetch standalone chats on mount and when user changes
useEffect(() => {
if (user?.id) {
fetchChats()
}
}, [user?.id])
const fetchChats = async () => {
try {
setIsLoading(true)
const response = await fetch(`/api/standalone-chat?userId=${user?.id}`)
const data = await response.json()
if (response.ok) {
setChats(data.chats || [])
} else {
console.error('Failed to fetch chats:', data.error)
}
} catch (error) {
console.error('Error fetching chats:', error)
} finally {
setIsLoading(false)
}
}
// Optimized function to add new chat to the list without full refresh
const addNewChatToList = (newChat: Chat) => {
setChats(prevChats => [newChat, ...prevChats])
}
// Optimized function to update chat timestamp without full refresh
const updateChatTimestamp = (chatId: string) => {
setChats(prevChats =>
prevChats.map(chat =>
chat.id === chatId
? { ...chat, updated_at: new Date().toISOString() }
: chat
).sort((a, b) => new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime())
)
}
const createNewChat = () => {
router.push('/dashboard/chat')
}
const selectChat = (chatId: string) => {
router.push(`/dashboard/chat/${chatId}`)
}
const deleteChat = async (chatId: string) => {
try {
const response = await fetch(`/api/standalone-chat?chatId=${chatId}&userId=${user?.id}`, {
method: 'DELETE'
})
if (response.ok) {
setChats(chats.filter(chat => chat.id !== chatId))
if (currentChatId === chatId) {
router.push('/dashboard/chat')
}
} else {
console.error('Failed to delete chat')
}
} catch (error) {
console.error('Error deleting chat:', error)
}
}
const handleDeleteConfirm = async () => {
if (chatToDelete) {
await deleteChat(chatToDelete.id)
setIsDeleteDialogOpen(false)
setChatToDelete(null)
}
}
const updateChatTitle = async (chatId: string, newTitle: string) => {
try {
// Update locally first for immediate feedback
setChats(chats.map(chat =>
chat.id === chatId ? { ...chat, title: newTitle } : chat
))
// Then update in database
const { supabase } = await import('@/lib/supabase')
await supabase
.from('chats')
.update({ title: newTitle })
.eq('id', chatId)
} catch (error) {
console.error('Error updating chat title:', error)
// Revert local change on error
fetchChats()
}
}
const handleStartEdit = (chat: Chat) => {
setEditingChatId(chat.id)
setEditingTitle(chat.title)
}
const handleSaveEdit = async (chatId: string) => {
if (editingTitle.trim()) {
await updateChatTitle(chatId, editingTitle.trim())
}
setEditingChatId(null)
setEditingTitle('')
}
const handleCancelEdit = () => {
setEditingChatId(null)
setEditingTitle('')
}
const formatDate = (dateString: string) => {
const date = new Date(dateString)
const now = new Date()
const diffTime = Math.abs(now.getTime() - date.getTime())
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24))
if (diffDays === 1) {
// Check if it's actually today (same calendar day)
const isToday = date.toDateString() === now.toDateString()
if (isToday) {
return `Today at ${date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}`
}
return 'Today'
} else if (diffDays === 2) {
return 'Yesterday'
} else if (diffDays <= 7) {
return `${diffDays - 1} days ago`
} else {
return date.toLocaleDateString()
}
}
const contextValue: ChatContextType = {
currentChatId,
setCurrentChatId,
chats,
fetchChats: fetchChats,
addNewChatToList,
updateChatTimestamp
}
return (
<ChatContext.Provider value={contextValue}>
<div className="flex h-screen bg-black">
{/* Mobile Menu Button */}
<div className="md:hidden fixed top-4 right-20 z-50">
<button
onClick={() => setIsMobileMenuOpen(true)}
className="p-3 rounded-2xl bg-black/20 backdrop-blur-xl border border-white/10 text-white hover:bg-black/40 transition-all duration-200 shadow-2xl"
>
<IoChatbubblesOutline className="h-5 w-5" />
</button>
</div>
{/* Mobile Sidebar Overlay */}
{isMobileMenuOpen && (
<div
className="md:hidden fixed inset-0 bg-black/50 backdrop-blur-sm z-40"
onClick={() => setIsMobileMenuOpen(false)}
/>
)}
{/* Chat History Sidebar */}
<div className={`
fixed md:relative inset-y-0 left-0 z-50
w-64 lg:w-72 bg-black/40 backdrop-blur-2xl border-r border-white/10 flex flex-col
transform transition-transform duration-300 ease-in-out
${isMobileMenuOpen ? 'translate-x-0' : '-translate-x-full md:translate-x-0'}
`}>
{/* Mobile Close Button */}
<div className="md:hidden flex justify-end p-3">
<button
onClick={() => setIsMobileMenuOpen(false)}
className="p-1 rounded-lg bg-white/10 text-white hover:bg-white/20 transition-colors"
>
<IoCloseOutline className="w-5 h-5" />
</button>
</div>
{/* Header */}
<div className="p-3 border-b border-white/10">
<button
onClick={createNewChat}
className="w-full flex items-center justify-center gap-2 px-3 py-2 bg-white/5 text-slate-200 rounded-lg hover:bg-white/10 transition-colors text-sm border border-white/10"
>
<IoAddOutline className="w-4 h-4" />
New Chat
</button>
</div>
{/* Chat List */}
<div className="flex-1 overflow-y-auto p-2 space-y-1">
{isLoading ? (
<div className="flex items-center justify-center py-8">
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-500"></div>
</div>
) : chats.length === 0 ? (
<div className="text-center py-8 text-slate-400 text-sm">
<p>No chats yet</p>
<p className="text-xs mt-1">Start a conversation to see it here</p>
</div>
) : (
chats.map((chat) => (
<div
key={chat.id}
className={`group relative p-2 rounded-md transition-all cursor-pointer ${
currentChatId === chat.id
? 'bg-white/10 text-white'
: 'text-slate-300 hover:bg-white/5'
}`}
onClick={() => !editingChatId && selectChat(chat.id)}
>
{/* Chat Title */}
<div className="flex items-center justify-between">
{editingChatId === chat.id ? (
<div className="flex items-center gap-1 flex-1">
<input
type="text"
value={editingTitle}
onChange={(e) => setEditingTitle(e.target.value)}
className="flex-1 bg-slate-700 text-slate-200 text-xs px-2 py-1 rounded border border-slate-600 focus:outline-none focus:border-slate-400"
autoFocus
onKeyDown={(e) => {
if (e.key === 'Enter') {
handleSaveEdit(chat.id)
} else if (e.key === 'Escape') {
handleCancelEdit()
}
}}
/>
<button
onClick={(e) => {
e.stopPropagation()
handleSaveEdit(chat.id)
}}
className="p-1 text-slate-400 hover:text-slate-200"
>
<IoCheckmarkOutline className="w-3 h-3" />
</button>
</div>
) : (
<>
<h3 className="text-sm font-medium line-clamp-1 flex-1 pr-2">
{chat.title}
</h3>
{/* Action Buttons */}
<div className="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
<button
onClick={(e) => {
e.stopPropagation()
handleStartEdit(chat)
}}
className="p-1 text-slate-400 hover:text-slate-200 hover:bg-white/10 rounded"
title="Rename chat"
>
<IoPencilOutline className="w-3 h-3" />
</button>
<button
onClick={(e) => {
e.stopPropagation()
setChatToDelete(chat)
setIsDeleteDialogOpen(true)
}}
className="p-1 text-slate-400 hover:text-red-400 hover:bg-white/10 rounded"
title="Delete chat"
>
<IoTrashOutline className="w-3 h-3" />
</button>
</div>
</>
)}
</div>
{/* Chat metadata */}
<div className="text-xs text-slate-400 mt-1">
{formatDate(chat.updated_at)}
</div>
</div>
))
)}
</div>
</div>
{/* Chat Content */}
<div className="flex-1 flex flex-col md:ml-0">
{children}
</div>
</div>
{/* Delete Confirmation Dialog */}
<DeleteConfirmationDialog
isOpen={isDeleteDialogOpen}
onClose={() => {
setIsDeleteDialogOpen(false)
setChatToDelete(null)
}}
onConfirm={handleDeleteConfirm}
loading={false}
title="Delete Chat"
message={`Delete "${chatToDelete?.title}"? This action cannot be undone.`}
confirmText="Delete Chat"
/>
</ChatContext.Provider>
)
}