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()}>
setQuery(e.target.value)} autoFocus />
ESC
{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 ? Avatar :
{editName.charAt(0)}
}
setEditName(e.target.value)} placeholder="Nhập tên của bạn" />
setEditEmail(e.target.value)} placeholder="social@example.com" />
)} {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()}> {`Preview
{/* Thumbnails Strip */} {images.length > 1 && (
e.stopPropagation()}> {images.map((img, idx) => (
{ setCurrentIndex(idx) setZoom(1) setRotation(0) }} > {`Thumb
))}
)}
) }