| // src/components/authoring/ReferenceManagerPanel.tsx | |
| import { useState } from "react"; | |
| import { Search, Plus, Upload, Trash2, ExternalLink, Copy, Loader2, Check, BookOpen, Database } from "lucide-react"; | |
| interface ReferenceManagerPanelProps { | |
| references: any[]; | |
| loading: boolean; | |
| search: string; | |
| setSearch: (v: string) => void; | |
| sourceFilter: string; | |
| setSourceFilter: (v: string) => void; | |
| importing: boolean; | |
| importFromDOI: (doi: string) => void; | |
| importBibTeX: (bibtex: string) => void; | |
| connectZotero: (apiKey: string, userId: string) => void; | |
| connectMendeley: (token: string) => void; | |
| removeReference: (id: string) => void; | |
| generateBibliography: (style: string) => string; | |
| } | |
| export function ReferenceManagerPanel({ references, loading, search, setSearch, sourceFilter, setSourceFilter, importing, importFromDOI, importBibTeX, connectZotero, connectMendeley, removeReference, generateBibliography }: ReferenceManagerPanelProps) { | |
| const [tab, setTab] = useState<"browse" | "import">("browse"); | |
| const [doi, setDoi] = useState(""); | |
| const [bibtex, setBibtex] = useState(""); | |
| const [zoteroKey, setZoteroKey] = useState(""); | |
| const [zoteroUser, setZoteroUser] = useState(""); | |
| const [mendeleyToken, setMendeleyToken] = useState(""); | |
| return ( | |
| <div className="grid grid-cols-1 lg:grid-cols-4 gap-4 h-full"> | |
| {/* Sidebar */} | |
| <div className="space-y-3"> | |
| <div className="flex gap-1"> | |
| <button onClick={() => setTab("browse")} className={`flex-1 py-1.5 text-xs rounded-lg ${tab === "browse" ? "bg-primary/20 text-primary" : "hover:bg-white/10"}`}>Browse</button> | |
| <button onClick={() => setTab("import")} className={`flex-1 py-1.5 text-xs rounded-lg ${tab === "import" ? "bg-primary/20 text-primary" : "hover:bg-white/10"}`}>Import</button> | |
| </div> | |
| {tab === "browse" && ( | |
| <> | |
| <div className="relative"> | |
| <Search className="absolute left-2 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-gray-400" /> | |
| <input type="text" value={search} onChange={e => setSearch(e.target.value)} placeholder="Search references..." | |
| className="w-full pl-7 pr-3 py-1.5 text-sm border border-white/20 rounded-lg bg-white/10" /> | |
| </div> | |
| <select aria-label="Select option" value={sourceFilter} onChange={e => setSourceFilter(e.target.value)} | |
| className="w-full text-sm px-3 py-1.5 border border-white/20 rounded-lg bg-white/10"> | |
| <option value="all">All sources</option> | |
| <option value="doi">DOI</option> | |
| <option value="bibtex">BibTeX</option> | |
| <option value="zotero">Zotero</option> | |
| <option value="mendeley">Mendeley</option> | |
| <option value="manual">Manual</option> | |
| </select> | |
| </> | |
| )} | |
| {tab === "import" && ( | |
| <div className="space-y-3"> | |
| <div> | |
| <h5 className="text-xs font-semibold mb-1">Import by DOI</h5> | |
| <div className="flex gap-1"> | |
| <input type="text" value={doi} onChange={e => setDoi(e.target.value)} placeholder="10.xxxx/xxxxx" | |
| className="flex-1 text-xs px-3 py-1.5 bg-white/10 rounded-lg" /> | |
| <button onClick={() => { importFromDOI(doi.trim()); setDoi(""); }} disabled={!doi.trim() || importing} | |
| className="btn btn-primary btn-xs">{importing ? <Loader2 className="w-3 h-3 animate-spin"/> : "Add"}</button> | |
| </div> | |
| </div> | |
| <div> | |
| <h5 className="text-xs font-semibold mb-1">Import BibTeX</h5> | |
| <textarea value={bibtex} onChange={e => setBibtex(e.target.value)} placeholder="Paste BibTeX here..." | |
| rows={4} className="w-full text-xs px-3 py-1.5 bg-white/10 rounded-lg font-mono" /> | |
| <button onClick={() => { importBibTeX(bibtex); setBibtex(""); }} disabled={!bibtex.trim() || importing} | |
| className="btn btn-primary btn-xs mt-1 w-full">Import BibTeX</button> | |
| </div> | |
| <div> | |
| <h5 className="text-xs font-semibold mb-1 flex items-center gap-1"><Database className="w-3 h-3"/> Connect Zotero</h5> | |
| <input type="text" value={zoteroUser} onChange={e => setZoteroUser(e.target.value)} placeholder="User ID" className="w-full text-xs px-3 py-1.5 bg-white/10 rounded-lg mb-1"/> | |
| <input type="text" value={zoteroKey} onChange={e => setZoteroKey(e.target.value)} placeholder="API Key" className="w-full text-xs px-3 py-1.5 bg-white/10 rounded-lg mb-1"/> | |
| <button onClick={() => connectZotero(zoteroKey, zoteroUser)} disabled={!zoteroKey || !zoteroUser || importing} | |
| className="btn btn-secondary btn-xs w-full">Connect Zotero</button> | |
| </div> | |
| <div> | |
| <h5 className="text-xs font-semibold mb-1">Connect Mendeley</h5> | |
| <input type="text" value={mendeleyToken} onChange={e => setMendeleyToken(e.target.value)} placeholder="Access Token" className="w-full text-xs px-3 py-1.5 bg-white/10 rounded-lg mb-1"/> | |
| <button onClick={() => connectMendeley(mendeleyToken)} disabled={!mendeleyToken || importing} | |
| className="btn btn-secondary btn-xs w-full">Connect Mendeley</button> | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| {/* Reference list */} | |
| <div className="lg:col-span-3 overflow-y-auto space-y-1.5"> | |
| {loading ? ( | |
| <div className="flex justify-center py-8"><Loader2 className="w-6 h-6 animate-spin text-gray-400"/></div> | |
| ) : references.length === 0 ? ( | |
| <div className="text-center py-8 text-gray-400"><BookOpen className="w-8 h-8 mx-auto mb-2 opacity-30"/><p className="text-sm">No references yet</p></div> | |
| ) : ( | |
| references.map(ref => ( | |
| <div key={ref.id} className="flex items-start justify-between p-2.5 rounded-lg hover:bg-white/5 transition text-sm"> | |
| <div className="flex-1 min-w-0"> | |
| <p className="font-semibold truncate">{ref.title}</p> | |
| <p className="text-xs text-gray-500">{ref.authors.slice(0, 2).map((a: any) => `${a.given} ${a.family}`).join(", ")}{ref.authors.length > 2 ? " et al." : ""} ({ref.year})</p> | |
| {ref.journal && <p className="text-xs text-gray-400 italic">{ref.journal}</p>} | |
| <div className="flex items-center gap-2 mt-1"> | |
| <span className="text-[9px] px-1.5 py-0.5 rounded-full bg-white/10">{ref.source}</span> | |
| {ref.doi && <a href={`https://doi.org/${ref.doi}`} target="_blank" className="text-[9px] text-primary hover:underline"><ExternalLink className="w-2.5 h-2.5 inline"/></a>} | |
| </div> | |
| </div> | |
| <button onClick={() => removeReference(ref.id)} className="p-1 text-gray-400 hover:text-red-500"><Trash2 className="w-3.5 h-3.5"/></button> | |
| </div> | |
| )) | |
| )} | |
| </div> | |
| </div> | |
| ); | |
| } |
Xet Storage Details
- Size:
- 7.02 kB
- Xet hash:
- 7fbfd66c4089c23ee8a574280ae377dbdf06f1bf2f209893885ca1d3efc352d4
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.