import React, { useState, useEffect, useMemo } from 'react'
import { Search, X, MessageSquare, Clock, ChevronLeft, ChevronRight, ZoomIn, ZoomOut, Maximize, Download, RotateCcw, Settings, User, Sun, Edit, Archive } from 'lucide-react'
export const SearchModal = ({ onSelect, onClose, conversations = [], isRestored = false }) => {
// Lock in the initial isRestored value to prevent re-triggering animations if App re-renders
const [wasRestored] = useState(isRestored)
const [query, setQuery] = useState(() => {
return sessionStorage.getItem('searchQuery') || ''
})
const [results, setResults] = useState([])
const [isLoading, setIsLoading] = useState(false)
const [debouncedQuery, setDebouncedQuery] = useState('')
useEffect(() => {
sessionStorage.setItem('searchQuery', query)
const timer = setTimeout(() => setDebouncedQuery(query), 300)
return () => clearTimeout(timer)
}, [query])
useEffect(() => {
if (!debouncedQuery || debouncedQuery.trim().length < 1) {
setResults([])
return
}
const fetchResults = async () => {
setIsLoading(true)
try {
const res = await fetch(`/api/search?q=${encodeURIComponent(debouncedQuery)}`)
if (res.ok) {
const data = await res.json()
setResults(data)
}
} catch (error) {
console.error("Search failed", error)
} finally {
setIsLoading(false)
}
}
fetchResults()
}, [debouncedQuery])
const groupedConversations = React.useMemo(() => {
if (query.trim().length >= 1) return null;
const groups = { today: [], yesterday: [], last7Days: [], older: [] }
const now = new Date()
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate())
const yesterday = new Date(today)
yesterday.setDate(yesterday.getDate() - 1)
const last7Days = new Date(today)
last7Days.setDate(last7Days.getDate() - 7)
conversations.forEach(conv => {
const dateStr = conv.updated_at || conv.created_at
if (!dateStr) { groups.older.push(conv); return; }
const date = new Date(dateStr)
if (date >= today) groups.today.push(conv)
else if (date >= yesterday) groups.yesterday.push(conv)
else if (date >= last7Days) groups.last7Days.push(conv)
else groups.older.push(conv)
})
return groups
}, [conversations, query])
useEffect(() => {
const handleEsc = (e) => { if (e.key === 'Escape') onClose() }
window.addEventListener('keydown', handleEsc)
return () => window.removeEventListener('keydown', handleEsc)
}, [onClose])
const highlightText = (text, highlight) => {
if (!highlight) return text
// Function to strip Vietnamese accents for matching
const stripAccents = (str) => {
return str.normalize('NFD')
.replace(/[\u0300-\u036f]/g, "")
.replace(/đ/g, "d")
.replace(/Đ/g, "D");
}
const normalizedText = stripAccents(text.toLowerCase())
const normalizedQuery = stripAccents(highlight.toLowerCase())
if (!normalizedText.includes(normalizedQuery)) return text
const parts = []
let lastIdx = 0
let idx = normalizedText.indexOf(normalizedQuery)
while (idx !== -1) {
// Push non-matching part
parts.push(text.substring(lastIdx, idx))
// Push matching part with original formatting
const matchedContent = text.substring(idx, idx + highlight.length)
parts.push(
{text.substring(idx, idx + highlight.length)}
)
lastIdx = idx + highlight.length
idx = normalizedText.indexOf(normalizedQuery, lastIdx)
}
parts.push(text.substring(lastIdx))
return parts
}
const renderGroup = (title, items) => {
if (!items || items.length === 0) return null
return (
{title}
{items.map(conv => (
))}
)
}
return (
e.stopPropagation()}>
{query.trim().length >= 1 ? (
{results.length > 0 ? (
{results.map((item) => (
))}
) : (!isLoading && query.trim() === debouncedQuery.trim()) ? (
Không tìm thấy kết quả nào cho "{query}"
) : null}
) : (
{groupedConversations && Object.entries(groupedConversations).map(([key, items]) => {
const labels = { today: 'Hôm nay', yesterday: 'Hôm qua', last7Days: '7 ngày qua', older: 'Cũ hơn' }
return renderGroup(labels[key], items)
})}
)}
)
}
export const SettingsModal = ({
onClose,
darkMode,
onToggleTheme,
archivedSessions = [],
onRestoreSession,
onUpdateProfile,
userProfile,
initialTab = 'general',
isRestored = false,
onTabChange
}) => {
// Lock in the initial isRestored value to prevent re-triggering animations if App re-renders
const [wasRestored] = useState(isRestored)
const [activeTab, setActiveTab] = useState(initialTab)
const [editName, setEditName] = useState(userProfile.name)
const [editEmail, setEditEmail] = useState(userProfile.email)
const [editAvatar, setEditAvatar] = useState(userProfile.avatar)
const [isSaving, setIsSaving] = useState(false)
const [showUnsavedWarning, setShowUnsavedWarning] = useState(false)
const [pendingAction, setPendingAction] = useState(null) // { type: 'tab', value: '...' } or { type: 'close' }
const tabs = [
{ id: 'general', label: 'Chung', icon: Settings },
{ id: 'account', label: 'Tài khoản', icon: User },
{ id: 'appearance', label: 'Giao diện', icon: Sun },
{ id: 'archived', label: 'Lưu trữ', icon: Archive }
]
const hasUnsavedChanges = useMemo(() => {
return editName !== userProfile.name ||
editEmail !== userProfile.email ||
editAvatar !== userProfile.avatar
}, [editName, editEmail, editAvatar, userProfile])
const handleTabClick = (tabId) => {
if (activeTab === 'account' && hasUnsavedChanges) {
setPendingAction({ type: 'tab', value: tabId })
setShowUnsavedWarning(true)
} else {
setActiveTab(tabId)
if (onTabChange) onTabChange(tabId)
}
}
const handleCloseClick = () => {
if (activeTab === 'account' && hasUnsavedChanges) {
setPendingAction({ type: 'close' })
setShowUnsavedWarning(true)
} else {
onClose()
}
}
const handleDiscard = () => {
// Reset local state to original
setEditName(userProfile.name)
setEditEmail(userProfile.email)
setEditAvatar(userProfile.avatar)
setShowUnsavedWarning(false)
if (pendingAction.type === 'tab') {
setActiveTab(pendingAction.value)
if (onTabChange) onTabChange(pendingAction.value)
} else if (pendingAction.type === 'close') {
onClose()
}
}
const handleSaveAndContinue = async () => {
setIsSaving(true)
await onUpdateProfile({ name: editName, email: editEmail, avatar: editAvatar })
setIsSaving(false)
setShowUnsavedWarning(false)
if (pendingAction.type === 'tab') {
setActiveTab(pendingAction.value)
} else if (pendingAction.type === 'close') {
onClose()
}
}
return (
e.stopPropagation()}>
{/* Sidebar Tabs */}
Cài đặt
{tabs.map(tab => (
))}
{/* Main Content */}
{tabs.find(t => t.id === activeTab).label}
{activeTab === 'general' && (
Dữ liệu huấn luyện
Đóng góp các cuộc hội thoại để cải thiện mô hình AI cho mọi người.
Lưu trữ hội thoại
Tự động lưu lịch sử chat vào tài khoản của bạn.
)}
{activeTab === 'account' && (
{editAvatar ?

:
{editName.charAt(0)}
}
)}
{activeTab === 'appearance' && (
)}
{activeTab === 'archived' && (
{archivedSessions.length > 0 ? (
{archivedSessions.map(session => (
{session.title || 'Không có tiêu đề'}
{new Date(session.created_at).toLocaleDateString()}
))}
) : (
Không có hội thoại nào được lưu trữ
)}
)}
{/* Unsaved Changes Warning Overlay */}
{showUnsavedWarning && (
Bạn chưa lưu thay đổi
Các chỉnh sửa trong phần tài khoản của bạn chưa được lưu lại. Bạn muốn làm gì tiếp theo?
)}
)
}
// Helper needed for the above
const renderedGroups = (groups, renderer) => {
if (!groups) return null;
return (
<>
{renderer("Hôm nay", groups.today)}
{renderer("Hôm qua", groups.yesterday)}
{renderer("7 ngày trước", groups.last7Days)}
{renderer("Cũ hơn", groups.older)}
>
)
}
export const ImageViewer = ({ images = [], startIndex = 0, onClose, onIndexChange, isRestored = false }) => {
// Lock in the initial isRestored value to prevent re-triggering animations if App re-renders
const [wasRestored] = useState(isRestored)
const [currentIndex, setCurrentIndex] = useState(startIndex)
const [zoom, setZoom] = useState(1)
const [rotation, setRotation] = useState(0)
// Sync current index to parent for persistence
useEffect(() => {
if (onIndexChange) onIndexChange(currentIndex)
}, [currentIndex, onIndexChange])
useEffect(() => {
const handleKeyDown = (e) => {
if (e.key === 'Escape') onClose()
if (e.key === 'ArrowLeft') handlePrev()
if (e.key === 'ArrowRight') handleNext()
}
window.addEventListener('keydown', handleKeyDown)
return () => window.removeEventListener('keydown', handleKeyDown)
}, [currentIndex, images.length])
const handleNext = () => {
setCurrentIndex((prev) => (prev + 1) % images.length)
setZoom(1)
setRotation(0)
}
const handlePrev = () => {
setCurrentIndex((prev) => (prev - 1 + images.length) % images.length)
setZoom(1)
setRotation(0)
}
const handleZoomIn = () => setZoom(prev => Math.min(prev + 0.25, 3))
const handleZoomOut = () => setZoom(prev => Math.max(prev - 0.25, 0.5))
const handleReset = () => {
setZoom(1)
setRotation(0)
}
const handleDownload = () => {
const link = document.createElement('a')
link.href = images[currentIndex]
link.download = `image-${currentIndex + 1}.png`
link.click()
}
if (!images || images.length === 0) return null
return (
{/* Top Controls */}
e.stopPropagation()}>
{currentIndex + 1} / {images.length}
{/* Navigation Arrows */}
{images.length > 1 && (
<>
>
)}
{/* Image Container */}
e.stopPropagation()}>
{/* Thumbnails Strip */}
{images.length > 1 && (
e.stopPropagation()}>
{images.map((img, idx) => (
{
setCurrentIndex(idx)
setZoom(1)
setRotation(0)
}}
>
))}
)}
)
}