| import React, { useState } from 'react';
|
| import { Plus, FileText, CheckCircle, Loader2, Search, Globe, BookOpen, Download } from 'lucide-react';
|
| import { ingestPDF, searchPapers, importPaper } from '../api';
|
|
|
| const DocumentsPanel = ({ documents, setDocuments, notebookId }) => {
|
| const [activeTab, setActiveTab] = useState('sources');
|
| const [isUploading, setIsUploading] = useState(false);
|
|
|
|
|
| const [searchQuery, setSearchQuery] = useState('');
|
| const [searchMode, setSearchMode] = useState('academic');
|
| const [searchResults, setSearchResults] = useState([]);
|
| const [isSearching, setIsSearching] = useState(false);
|
| const [importingIds, setImportingIds] = useState({});
|
|
|
|
|
| const handleFileUpload = async (e) => {
|
| const file = e.target.files[0];
|
| if (!file) return;
|
|
|
| setIsUploading(true);
|
| try {
|
| const result = await ingestPDF(file, notebookId);
|
| setDocuments(prev => [{
|
| id: result.paper_id,
|
| title: file.name,
|
| date: new Date().toLocaleDateString(),
|
| active: true,
|
| filename: file.name
|
| }, ...prev]);
|
| } catch (error) {
|
| console.error("Upload failed", error);
|
| alert("Failed to upload paper");
|
| } finally {
|
| setIsUploading(false);
|
| }
|
| };
|
|
|
| const handleSearch = async (e) => {
|
| e.preventDefault();
|
| if (!searchQuery.trim()) return;
|
|
|
| setIsSearching(true);
|
| try {
|
| const results = await searchPapers(searchQuery, searchMode);
|
| setSearchResults(results);
|
| } catch (error) {
|
| console.error("Search failed", error);
|
| } finally {
|
| setIsSearching(false);
|
| }
|
| };
|
|
|
| const handleImport = async (paper) => {
|
| setImportingIds(prev => ({ ...prev, [paper.id]: true }));
|
| try {
|
| const result = await importPaper(notebookId, paper);
|
| setDocuments(prev => [{
|
| id: result.doc_id,
|
| title: paper.title,
|
| date: new Date().toLocaleDateString(),
|
| active: true,
|
| filename: paper.title + ".txt"
|
| }, ...prev]);
|
|
|
| setActiveTab('sources');
|
| } catch (error) {
|
| console.error("Import failed", error);
|
| alert("Failed to import paper");
|
| } finally {
|
| setImportingIds(prev => ({ ...prev, [paper.id]: false }));
|
| }
|
| };
|
|
|
| return (
|
| <div className="h-full flex flex-col border-r border-slate-200 bg-[#f8f9fa]">
|
| {/* Tabs */}
|
| <div className="flex border-b border-slate-200 bg-white">
|
| <button
|
| onClick={() => setActiveTab('sources')}
|
| className={`flex-1 py-3 text-sm font-medium ${activeTab === 'sources' ? 'text-blue-600 border-b-2 border-blue-600' : 'text-slate-500 hover:text-slate-700'}`}
|
| >
|
| Sources
|
| </button>
|
| <button
|
| onClick={() => setActiveTab('search')}
|
| className={`flex-1 py-3 text-sm font-medium ${activeTab === 'search' ? 'text-blue-600 border-b-2 border-blue-600' : 'text-slate-500 hover:text-slate-700'}`}
|
| >
|
| Add Source
|
| </button>
|
| </div>
|
|
|
| {/* TAB: SOURCES */}
|
| {activeTab === 'sources' && (
|
| <div className="flex-1 flex flex-col p-4 overflow-hidden">
|
| <div className="mb-6">
|
| <label className="flex items-center gap-2 w-full p-3 rounded-full border border-slate-300 hover:bg-white hover:shadow-sm cursor-pointer transition-all group bg-white">
|
| <div className="bg-slate-100 p-1 rounded-full group-hover:bg-slate-200">
|
| {isUploading ? <Loader2 size={16} className="animate-spin text-slate-600" /> : <Plus size={16} className="text-slate-600" />}
|
| </div>
|
| <span className="text-sm font-medium text-slate-700">Upload PDF</span>
|
| <input type="file" accept=".pdf" className="hidden" onChange={handleFileUpload} disabled={isUploading} />
|
| </label>
|
| </div>
|
|
|
| <div className="flex-1 overflow-y-auto space-y-2">
|
| {documents.map((doc) => (
|
| <div key={doc.id} className="group flex items-start gap-3 p-3 rounded-xl hover:bg-white hover:shadow-sm transition-all cursor-pointer border border-transparent hover:border-slate-200">
|
| <div className="mt-1">
|
| <div className="w-8 h-8 rounded-lg bg-orange-50 flex items-center justify-center border border-orange-100 text-orange-600">
|
| <FileText size={16} />
|
| </div>
|
| </div>
|
| <div className="flex-1 min-w-0">
|
| <h3 className="text-sm font-medium text-slate-800 truncate leading-snug">
|
| {doc.title || doc.filename}
|
| </h3>
|
| <p className="text-xs text-slate-400 mt-1">
|
| {doc.date || (doc.uploaded_at ? new Date(doc.uploaded_at * 1000).toLocaleDateString() : 'Just now')}
|
| </p>
|
| </div>
|
| {doc.active && <CheckCircle size={14} className="text-blue-500 mt-1" />}
|
| </div>
|
| ))}
|
| {documents.length === 0 && (
|
| <div className="text-center mt-10 p-4">
|
| <p className="text-sm text-slate-400">No sources added.</p>
|
| </div>
|
| )}
|
| </div>
|
| </div>
|
| )}
|
|
|
| {/* TAB: SEARCH */}
|
| {activeTab === 'search' && (
|
| <div className="flex-1 flex flex-col p-4 overflow-hidden">
|
| <form onSubmit={handleSearch} className="mb-4 space-y-3">
|
| <div className="flex bg-slate-200 p-1 rounded-lg">
|
| <button type="button" onClick={() => setSearchMode('academic')} className={`flex-1 text-xs font-medium py-1.5 rounded-md transition-all ${searchMode === 'academic' ? 'bg-white shadow-sm text-slate-900' : 'text-slate-500'}`}>Academic</button>
|
| <button type="button" onClick={() => setSearchMode('web')} className={`flex-1 text-xs font-medium py-1.5 rounded-md transition-all ${searchMode === 'web' ? 'bg-white shadow-sm text-slate-900' : 'text-slate-500'}`}>Web</button>
|
| </div>
|
| <div className="relative">
|
| <Search className="absolute left-3 top-2.5 text-slate-400" size={16} />
|
| <input
|
| type="text"
|
| placeholder={searchMode === 'academic' ? "Search papers..." : "Search web..."}
|
| className="w-full pl-9 pr-4 py-2 bg-white border border-slate-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
|
| value={searchQuery}
|
| onChange={(e) => setSearchQuery(e.target.value)}
|
| />
|
| </div>
|
| <button type="submit" disabled={isSearching} className="w-full py-2 bg-blue-600 text-white rounded-lg text-sm font-medium hover:bg-blue-700 disabled:opacity-50">
|
| {isSearching ? 'Searching...' : 'Find Sources'}
|
| </button>
|
| </form>
|
|
|
| <div className="flex-1 overflow-y-auto space-y-4 pr-1">
|
| {searchResults.map((result) => (
|
| <div key={result.id} className="bg-white p-3 rounded-xl border border-slate-200 shadow-sm">
|
| <h3 className="text-sm font-bold text-slate-900 leading-tight mb-1 line-clamp-2">
|
| <a href={result.url} target="_blank" rel="noopener noreferrer" className="hover:underline">{result.title}</a>
|
| </h3>
|
| <p className="text-xs text-slate-500 mb-2">{result.venue} • {result.year}</p>
|
| <p className="text-xs text-slate-600 mb-3 line-clamp-3">{result.abstract}</p>
|
| <div className="flex items-center justify-between">
|
| <span className="text-[10px] bg-slate-100 px-2 py-1 rounded text-slate-500 font-medium border border-slate-200">
|
| Cited by {(result.citations || 0).toLocaleString()}
|
| </span>
|
| <button
|
| onClick={() => handleImport(result)}
|
| disabled={importingIds[result.id]}
|
| className="flex items-center gap-1.5 px-3 py-1.5 bg-slate-900 text-white text-xs font-medium rounded-lg hover:bg-slate-800 disabled:opacity-50"
|
| >
|
| {importingIds[result.id] ? <Loader2 size={12} className="animate-spin" /> : <Download size={12} />}
|
| Import
|
| </button>
|
| </div>
|
| </div>
|
| ))}
|
| </div>
|
| </div>
|
| )}
|
| </div>
|
| );
|
| };
|
|
|
| export default DocumentsPanel;
|
|
|