Files
doc_ai_frontend/src/components/LeftSidebar.tsx

346 lines
13 KiB
TypeScript
Raw Normal View History

2025-12-22 17:37:41 +08:00
import React, { useState, useRef, useCallback, useEffect } from 'react';
2026-03-06 14:30:30 +08:00
import { Upload, LogIn, LogOut, FileText, Clock, ChevronLeft, ChevronRight, History, MousePointerClick, FileUp, ClipboardPaste, Loader2 } from 'lucide-react';
2025-12-22 17:37:41 +08:00
import { useAuth } from '../contexts/AuthContext';
2026-01-24 13:53:50 +08:00
import { useLanguage } from '../contexts/LanguageContext';
2025-12-22 17:37:41 +08:00
import { FileRecord } from '../types';
import AuthModal from './AuthModal';
interface LeftSidebarProps {
files: FileRecord[];
selectedFileId: string | null;
onFileSelect: (fileId: string) => void;
onUploadClick: () => void;
2026-03-06 14:30:30 +08:00
canUploadAnonymously: boolean;
onRequireAuth: () => void;
2025-12-22 17:37:41 +08:00
isCollapsed: boolean;
onToggleCollapse: () => void;
onUploadFiles: (files: File[]) => void;
hasMore: boolean;
loadingMore: boolean;
onLoadMore: () => void;
}
export default function LeftSidebar({
files,
selectedFileId,
onFileSelect,
onUploadClick,
2026-03-06 14:30:30 +08:00
canUploadAnonymously,
onRequireAuth,
2025-12-22 17:37:41 +08:00
isCollapsed,
onToggleCollapse,
onUploadFiles,
hasMore,
loadingMore,
onLoadMore,
}: LeftSidebarProps) {
2026-03-06 14:30:30 +08:00
const { user, signOut, isAuthenticated } = useAuth();
2026-01-24 13:53:50 +08:00
const { t } = useLanguage();
2025-12-22 17:37:41 +08:00
const [showAuthModal, setShowAuthModal] = useState(false);
const [isDragging, setIsDragging] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
const listRef = useRef<HTMLDivElement>(null);
2026-03-06 14:30:30 +08:00
const displayName = user?.username?.trim() || user?.email || '';
useEffect(() => {
if (isAuthenticated) {
setShowAuthModal(false);
}
}, [isAuthenticated]);
2025-12-22 17:37:41 +08:00
2026-01-24 13:53:50 +08:00
// ... (rest of the logic remains the same)
2025-12-22 17:37:41 +08:00
// Handle scroll to load more
const handleScroll = useCallback(() => {
if (!listRef.current || loadingMore || !hasMore) return;
const { scrollTop, scrollHeight, clientHeight } = listRef.current;
// Trigger load more when scrolled to bottom (with 50px threshold)
if (scrollHeight - scrollTop - clientHeight < 50) {
onLoadMore();
}
}, [loadingMore, hasMore, onLoadMore]);
// Auto-load more if content doesn't fill the container
useEffect(() => {
if (!hasMore || loadingMore || !user || files.length === 0) return;
// Use requestAnimationFrame to ensure DOM has been updated
const checkAndLoadMore = () => {
requestAnimationFrame(() => {
if (!listRef.current) return;
const { scrollHeight, clientHeight } = listRef.current;
// If content doesn't fill container and there's more data, load more
if (scrollHeight <= clientHeight && hasMore && !loadingMore) {
onLoadMore();
}
});
};
// Small delay to ensure DOM is fully rendered
const timer = setTimeout(checkAndLoadMore, 100);
return () => clearTimeout(timer);
}, [files.length, hasMore, loadingMore, onLoadMore, user]);
const handleDragOver = (e: React.DragEvent) => {
e.preventDefault();
setIsDragging(true);
};
const handleDragLeave = (e: React.DragEvent) => {
e.preventDefault();
setIsDragging(false);
};
const handleDrop = (e: React.DragEvent) => {
e.preventDefault();
2026-03-06 14:30:30 +08:00
if (!user && !canUploadAnonymously) {
onRequireAuth();
return;
}
2025-12-22 17:37:41 +08:00
setIsDragging(false);
const droppedFiles = Array.from(e.dataTransfer.files);
const validFiles = droppedFiles.filter(file =>
file.type.startsWith('image/') || file.type === 'application/pdf'
);
if (validFiles.length > 0) {
onUploadFiles(validFiles);
}
};
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
2026-03-06 14:30:30 +08:00
if (!user && !canUploadAnonymously) {
onRequireAuth();
return;
}
2025-12-22 17:37:41 +08:00
if (e.target.files && e.target.files.length > 0) {
onUploadFiles(Array.from(e.target.files));
}
// Reset input
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
};
if (isCollapsed) {
return (
<div className="h-full flex flex-col items-center py-4 bg-gray-50/50">
<button
onClick={onToggleCollapse}
className="p-2 mb-6 text-gray-500 hover:text-gray-900 hover:bg-gray-200 rounded-md transition-colors"
>
<ChevronRight size={20} />
</button>
<button
2026-03-06 14:30:30 +08:00
onClick={() => {
if (!user && !canUploadAnonymously) {
onRequireAuth();
return;
}
onUploadClick();
}}
2025-12-22 17:37:41 +08:00
className="p-3 rounded-xl bg-blue-600 text-white hover:bg-blue-700 shadow-lg shadow-blue-600/20 transition-all mb-6"
2026-01-24 13:53:50 +08:00
title={t.common.upload}
2025-12-22 17:37:41 +08:00
>
<Upload size={20} />
</button>
<div className="flex-1 w-full flex flex-col items-center gap-4">
2026-01-24 13:53:50 +08:00
<button className="p-2 text-gray-400 hover:text-gray-900 transition-colors" title={t.common.history}>
2025-12-22 17:37:41 +08:00
<History size={20} />
</button>
</div>
<button
onClick={() => !user && setShowAuthModal(true)}
className="p-3 rounded-lg text-gray-600 hover:bg-gray-200 transition-colors mt-auto"
2026-01-24 13:53:50 +08:00
title={user ? 'Signed In' : t.common.login}
2025-12-22 17:37:41 +08:00
>
<LogIn size={20} />
</button>
</div>
);
}
return (
<>
<div className="flex flex-col h-full bg-white">
{/* Top Area: Title & Upload */}
<div className="p-6 pb-4">
<div className="flex items-start justify-between mb-6">
<div>
2026-01-24 13:53:50 +08:00
<h2 className="text-lg font-bold text-gray-900 leading-tight">{t.sidebar.title}</h2>
<p className="text-xs text-gray-500 mt-1">{t.sidebar.subtitle}</p>
2025-12-22 17:37:41 +08:00
</div>
<button
onClick={onToggleCollapse}
className="p-1.5 -mr-1.5 text-gray-400 hover:text-gray-900 hover:bg-gray-100 rounded-md transition-colors"
>
<ChevronLeft size={18} />
</button>
</div>
2026-01-24 13:53:50 +08:00
<div className="mb-2" id="sidebar-upload-area">
2025-12-22 17:37:41 +08:00
<div
onDragOver={handleDragOver}
2026-03-06 14:30:30 +08:00
onDragLeave={handleDragLeave}
onDrop={handleDrop}
onClick={() => {
if (!user && !canUploadAnonymously) {
onRequireAuth();
return;
}
fileInputRef.current?.click();
}}
2025-12-22 17:37:41 +08:00
className={`
border-2 border-dashed rounded-xl p-6 text-center cursor-pointer transition-all duration-200 group
${isDragging
? 'border-blue-500 bg-blue-50'
: 'border-gray-200 hover:border-blue-400 hover:bg-gray-50'
}
`}
>
<input
type="file"
ref={fileInputRef}
className="hidden"
onChange={handleFileChange}
accept="image/*"
multiple
/>
<div className="w-12 h-12 bg-gray-100 text-gray-600 rounded-full flex items-center justify-center mx-auto mb-3 group-hover:scale-110 transition-transform">
<Upload size={24} />
</div>
2026-01-24 13:53:50 +08:00
<p className="text-xs text-gray-500 mb-2">{t.sidebar.uploadInstruction}</p>
<div className="flex items-center justify-center gap-4 text-xs text-gray-400">
2025-12-22 17:37:41 +08:00
<div className="flex items-center gap-1">
<MousePointerClick className="w-3.5 h-3.5" />
2026-01-24 13:53:50 +08:00
<span>{t.common.click}</span>
2025-12-22 17:37:41 +08:00
</div>
<div className="flex items-center gap-1">
<FileUp className="w-3.5 h-3.5" />
2026-01-24 13:53:50 +08:00
<span>{t.common.drop}</span>
2025-12-22 17:37:41 +08:00
</div>
<div className="flex items-center gap-1">
<ClipboardPaste className="w-3.5 h-3.5" />
2026-01-24 13:53:50 +08:00
<span>{t.common.paste}</span>
2025-12-22 17:37:41 +08:00
</div>
</div>
</div>
</div>
</div>
{/* Middle Area: History */}
2026-01-24 13:53:50 +08:00
<div className="flex-1 overflow-hidden flex flex-col px-4" id="sidebar-history">
2025-12-22 17:37:41 +08:00
<div className="flex items-center gap-2 text-xs font-semibold text-gray-500 uppercase tracking-wider mb-3 px-2">
<Clock size={14} />
2026-01-24 13:53:50 +08:00
<span>{t.sidebar.historyHeader}</span>
2025-12-22 17:37:41 +08:00
</div>
<div
ref={listRef}
onScroll={handleScroll}
className="flex-1 overflow-y-auto space-y-1 pr-2 -mr-2 custom-scrollbar"
>
{!user ? (
<div className="text-center py-12 text-gray-400 text-sm">
<div className="mb-2 opacity-50"><FileText size={40} className="mx-auto" /></div>
2026-01-24 13:53:50 +08:00
{t.sidebar.pleaseLogin}
2025-12-22 17:37:41 +08:00
</div>
) : files.length === 0 ? (
<div className="text-center py-12 text-gray-400 text-sm">
<div className="mb-2 opacity-50"><FileText size={40} className="mx-auto" /></div>
2026-01-24 13:53:50 +08:00
{t.sidebar.noHistory}
2025-12-22 17:37:41 +08:00
</div>
) : (
<>
{files.map((file) => (
<button
key={file.id}
onClick={() => onFileSelect(file.id)}
className={`w-full p-3 rounded-lg text-left transition-all border group relative ${selectedFileId === file.id
? 'bg-blue-50 border-blue-200 shadow-sm'
: 'bg-white border-transparent hover:bg-gray-50 hover:border-gray-100'
}`}
>
<div className="flex items-start gap-3">
<div className={`p-2 rounded-lg ${selectedFileId === file.id ? 'bg-blue-100 text-blue-600' : 'bg-gray-100 text-gray-500 group-hover:bg-gray-200'}`}>
<FileText size={18} />
</div>
<div className="flex-1 min-w-0">
<p className={`text-sm font-medium truncate ${selectedFileId === file.id ? 'text-blue-900' : 'text-gray-700'}`}>
{file.filename}
</p>
<div className="flex items-center gap-2 mt-1">
<span className="text-xs text-gray-400">
{new Date(file.created_at).toLocaleDateString()}
</span>
<span className={`w-1.5 h-1.5 rounded-full ${file.status === 'completed' ? 'bg-green-500' :
file.status === 'processing' ? 'bg-yellow-500' : 'bg-red-500'
}`} />
</div>
</div>
</div>
</button>
))}
{/* Loading more indicator */}
{loadingMore && (
<div className="flex items-center justify-center py-3 text-gray-400">
<Loader2 size={18} className="animate-spin" />
2026-01-24 13:53:50 +08:00
<span className="ml-2 text-xs">{t.common.loading}</span>
2025-12-22 17:37:41 +08:00
</div>
)}
{/* End of list indicator */}
{!hasMore && files.length > 0 && (
<div className="text-center py-3 text-xs text-gray-400">
2026-01-24 13:53:50 +08:00
{t.sidebar.noMore}
2025-12-22 17:37:41 +08:00
</div>
)}
</>
)}
</div>
</div>
{/* Bottom Area: User/Login */}
<div className="p-4 border-t border-gray-100 bg-gray-50/30">
{user ? (
<div className="flex items-center gap-3 p-2 rounded-lg bg-white border border-gray-100 shadow-sm">
<div className="w-8 h-8 bg-gray-900 rounded-full flex items-center justify-center flex-shrink-0">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 12C14.21 12 16 10.21 16 8C16 5.79 14.21 4 12 4C9.79 4 8 5.79 8 8C8 10.21 9.79 12 12 12ZM12 14C9.33 14 4 15.34 4 18V20H20V18C20 15.34 14.67 14 12 14Z" fill="white" />
</svg>
</div>
<div className="flex-1 min-w-0">
2026-03-06 14:30:30 +08:00
<p className="text-sm font-medium text-gray-900 truncate">{displayName}</p>
2025-12-22 17:37:41 +08:00
</div>
<button
onClick={() => signOut()}
className="p-1.5 text-gray-400 hover:text-red-500 hover:bg-red-50 rounded-md transition-colors"
2026-01-24 13:53:50 +08:00
title={t.common.logout}
2025-12-22 17:37:41 +08:00
>
<LogOut size={16} />
</button>
</div>
) : (
<button
onClick={() => setShowAuthModal(true)}
className="w-full py-2.5 px-4 bg-gray-900 text-white rounded-lg hover:bg-gray-800 transition-colors flex items-center justify-center gap-2 text-sm font-medium shadow-lg shadow-gray-900/10"
>
<LogIn size={18} />
2026-01-24 13:53:50 +08:00
{t.common.login}
2025-12-22 17:37:41 +08:00
</button>
)}
</div>
</div>
{showAuthModal && (
<AuthModal onClose={() => setShowAuthModal(false)} />
)}
</>
);
}