Spaces:
Sleeping
Sleeping
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>RefManager</title> | |
| <style> | |
| @import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&family=IBM+Plex+Sans:wght@300;400;500&display=swap'); | |
| :root { | |
| --bg: #0e0e0e; | |
| --surface: #161616; | |
| --border: #2a2a2a; | |
| --accent: #c8f06e; | |
| --accent-dim: #8aaa40; | |
| --text: #e8e8e8; | |
| --text-dim: #888; | |
| --text-faint: #444; | |
| --red: #ff5f5f; | |
| --yellow: #f0c84a; | |
| --green: #6ef0a0; | |
| --mono: 'IBM Plex Mono', monospace; | |
| --sans: 'IBM Plex Sans', sans-serif; | |
| } | |
| * { box-sizing: border-box; margin: 0; padding: 0; } | |
| body { | |
| background: var(--bg); | |
| color: var(--text); | |
| font-family: var(--sans); | |
| font-size: 14px; | |
| line-height: 1.5; | |
| min-height: 100vh; | |
| } | |
| .app { display: grid; grid-template-rows: auto 1fr; height: 100vh; overflow: hidden; } | |
| header { | |
| padding: 12px 24px; border-bottom: 1px solid var(--border); | |
| display: flex; align-items: center; gap: 16px; background: var(--surface); | |
| } | |
| .logo { font-family: var(--mono); font-size: 13px; font-weight: 600; color: var(--accent); letter-spacing: 0.08em; text-transform: uppercase; } | |
| .tagline { color: var(--text-faint); font-size: 11px; font-family: var(--mono); } | |
| .header-stats { margin-left: auto; display: flex; gap: 14px; align-items: center; } | |
| .vpills { display: flex; gap: 7px; } | |
| .vpill { font-family: var(--mono); font-size: 10px; padding: 3px 9px; border-radius: 10px; display: flex; align-items: center; gap: 4px; } | |
| .vpill-green { background: rgba(110,240,160,0.1); border: 1px solid rgba(110,240,160,0.25); color: var(--green); } | |
| .vpill-yellow { background: rgba(240,200,74,0.1); border: 1px solid rgba(240,200,74,0.25); color: var(--yellow); } | |
| .vpill-red { background: rgba(255,95,95,0.1); border: 1px solid rgba(255,95,95,0.25); color: var(--red); } | |
| .stat { display: flex; flex-direction: column; align-items: flex-end; gap: 1px; } | |
| .stat-val { font-family: var(--mono); font-size: 15px; font-weight: 600; color: var(--accent); line-height: 1; } | |
| .stat-label { font-size: 9px; color: var(--text-dim); text-transform: uppercase; letter-spacing: 0.06em; } | |
| .main { display: grid; grid-template-columns: 340px 1fr; overflow: hidden; } | |
| /* LEFT */ | |
| .left-panel { border-right: 1px solid var(--border); display: flex; flex-direction: column; overflow: hidden; background: var(--surface); } | |
| .mode-tabs { display: flex; border-bottom: 1px solid var(--border); } | |
| .mode-tab { | |
| font-family: var(--mono); font-size: 10px; text-transform: uppercase; letter-spacing: 0.08em; | |
| padding: 8px 14px; cursor: pointer; color: var(--text-dim); border: none; background: transparent; | |
| border-bottom: 2px solid transparent; margin-bottom: -1px; transition: all 0.15s; | |
| } | |
| .mode-tab.active { color: var(--accent); border-bottom-color: var(--accent); } | |
| .mode-tab:hover:not(.active) { color: var(--text); } | |
| .panel-header { | |
| padding: 10px 16px 8px; border-bottom: 1px solid var(--border); | |
| font-family: var(--mono); font-size: 10px; text-transform: uppercase; letter-spacing: 0.1em; | |
| color: var(--text-dim); display: flex; align-items: center; justify-content: space-between; | |
| } | |
| .panel-header span { color: var(--accent); } | |
| textarea { | |
| flex: 1; resize: none; background: transparent; border: none; outline: none; | |
| color: var(--text); font-family: var(--mono); font-size: 11px; line-height: 1.6; | |
| padding: 12px 16px; overflow-y: auto; | |
| } | |
| textarea::placeholder { color: var(--text-faint); } | |
| textarea::-webkit-scrollbar { width: 4px; } | |
| textarea::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; } | |
| .options-row { padding: 10px 16px; border-top: 1px solid var(--border); display: flex; flex-direction: column; gap: 9px; } | |
| .toggle-group { display: flex; gap: 10px; flex-wrap: wrap; } | |
| .toggle { display: flex; align-items: center; gap: 6px; cursor: pointer; user-select: none; } | |
| .toggle input { display: none; } | |
| .toggle-pill { width: 28px; height: 15px; background: var(--border); border-radius: 8px; position: relative; transition: background 0.2s; } | |
| .toggle-pill::after { content: ''; position: absolute; width: 11px; height: 11px; background: var(--text-dim); border-radius: 50%; top: 2px; left: 2px; transition: left 0.2s, background 0.2s; } | |
| .toggle input:checked ~ .toggle-pill { background: var(--accent-dim); } | |
| .toggle input:checked ~ .toggle-pill::after { left: 15px; background: var(--accent); } | |
| .toggle-label { font-size: 11px; color: var(--text-dim); font-family: var(--mono); } | |
| .file-row { display: flex; align-items: center; gap: 8px; } | |
| .file-label { font-size: 11px; color: var(--text-dim); font-family: var(--mono); white-space: nowrap; } | |
| .file-input-wrap { flex: 1; position: relative; overflow: hidden; } | |
| .file-input-wrap input[type="file"] { position: absolute; opacity: 0; width: 100%; height: 100%; cursor: pointer; } | |
| .file-display { font-family: var(--mono); font-size: 10px; color: var(--text-faint); background: var(--bg); border: 1px solid var(--border); border-radius: 3px; padding: 4px 8px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; cursor: pointer; } | |
| .file-display.has-file { color: var(--accent); border-color: var(--accent-dim); } | |
| .action-row { padding: 10px 16px; border-top: 1px solid var(--border); display: flex; gap: 8px; } | |
| .btn { font-family: var(--mono); font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.08em; border: none; border-radius: 3px; cursor: pointer; padding: 8px 14px; transition: all 0.15s; } | |
| .btn-primary { background: var(--accent); color: #0e0e0e; flex: 1; } | |
| .btn-primary:hover { background: #d8ff7e; } | |
| .btn-primary:active { transform: scale(0.98); } | |
| .btn-primary:disabled { background: var(--border); color: var(--text-faint); cursor: not-allowed; } | |
| .btn-ghost { background: transparent; color: var(--text-dim); border: 1px solid var(--border); } | |
| .btn-ghost:hover { border-color: var(--text-dim); color: var(--text); } | |
| .btn-danger { background: transparent; color: var(--red); border: 1px solid #3a1a1a; } | |
| .btn-danger:hover { background: #1e0a0a; border-color: var(--red); } | |
| .btn-sm { padding: 4px 9px; font-size: 10px; } | |
| /* RIGHT */ | |
| .right-panel { display: flex; flex-direction: column; overflow: hidden; background: var(--bg); } | |
| .loading-bar { height: 2px; background: var(--border); display: none; overflow: hidden; flex-shrink: 0; } | |
| .loading-bar.active { display: block; } | |
| .loading-fill { height: 100%; background: var(--accent); animation: sweep 1.4s infinite ease-in-out; } | |
| @keyframes sweep { 0%{width:0;margin-left:0} 50%{width:60%;margin-left:20%} 100%{width:0;margin-left:100%} } | |
| .right-toolbar { padding: 8px 16px; border-bottom: 1px solid var(--border); display: flex; align-items: center; gap: 10px; flex-shrink: 0; flex-wrap: wrap; } | |
| .panel-label { font-family: var(--mono); font-size: 10px; text-transform: uppercase; letter-spacing: 0.1em; color: var(--text-dim); } | |
| .count-badge { font-family: var(--mono); font-size: 10px; color: var(--accent); background: rgba(200,240,110,0.08); border: 1px solid rgba(200,240,110,0.2); border-radius: 10px; padding: 2px 8px; } | |
| .toolbar-right { margin-left: auto; display: flex; gap: 8px; } | |
| .filter-toggle { display: flex; border: 1px solid var(--border); border-radius: 3px; overflow: hidden; } | |
| .ftab { font-family: var(--mono); font-size: 9px; text-transform: uppercase; letter-spacing: 0.06em; padding: 4px 9px; cursor: pointer; color: var(--text-dim); background: transparent; border: none; border-right: 1px solid var(--border); transition: all 0.12s; } | |
| .ftab:last-child { border-right: none; } | |
| .ftab.active { background: var(--border); color: var(--text); } | |
| .ftab:hover:not(.active) { color: var(--text); } | |
| /* TABLE */ | |
| .refs-header, .ref-row { | |
| display: grid; | |
| grid-template-columns: 36px 18px 58px 130px 1fr 110px 110px; | |
| border-bottom: 1px solid var(--border); | |
| align-items: center; | |
| } | |
| .refs-header { background: var(--surface); position: sticky; top: 0; z-index: 10; flex-shrink: 0; } | |
| .col-head { padding: 6px 8px; font-family: var(--mono); font-size: 9px; text-transform: uppercase; letter-spacing: 0.1em; color: var(--text-faint); border-right: 1px solid var(--border); } | |
| .col-head:last-child { border-right: none; } | |
| .ref-row { cursor: pointer; min-height: 36px; transition: background 0.1s; } | |
| .ref-row:hover { background: rgba(255,255,255,0.02); } | |
| .ref-row.selected { background: rgba(200,240,110,0.04); } | |
| .ref-cell { padding: 6px 8px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px; border-right: 1px solid var(--border); } | |
| .ref-cell:last-child { border-right: none; } | |
| .cell-idx { font-family: var(--mono); font-size: 10px; color: var(--text-faint); text-align: center; } | |
| .cell-status { display: flex; align-items: center; justify-content: center; } | |
| .status-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; } | |
| .dot-green { background: var(--green); box-shadow: 0 0 5px rgba(110,240,160,0.5); } | |
| .dot-yellow { background: var(--yellow); box-shadow: 0 0 5px rgba(240,200,74,0.4); } | |
| .dot-red { background: var(--red); box-shadow: 0 0 5px rgba(255,95,95,0.4); } | |
| .cell-year { font-family: var(--mono); font-size: 11px; color: var(--accent); font-weight: 500; } | |
| .cell-author { font-size: 11px; color: var(--text-dim); } | |
| .cell-title { color: var(--text); font-size: 12px; } | |
| .cell-journal{ color: var(--text-dim); font-size: 11px; font-style: italic; } | |
| .cell-key { font-family: var(--mono); font-size: 10px; color: var(--text-faint); } | |
| .sim-badge { font-family: var(--mono); font-size: 9px; padding: 1px 4px; border-radius: 2px; margin-left: 5px; vertical-align: middle; opacity: 0.8; } | |
| .sim-high { background: rgba(110,240,160,0.15); color: var(--green); } | |
| .sim-mid { background: rgba(240,200,74,0.15); color: var(--yellow); } | |
| .sim-low { background: rgba(255,95,95,0.15); color: var(--red); } | |
| .refs-list { flex: 1; overflow-y: auto; } | |
| .refs-list::-webkit-scrollbar { width: 4px; } | |
| .refs-list::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; } | |
| /* DETAIL PANEL β always visible below selected row */ | |
| .detail-panel { | |
| display: none; | |
| border-bottom: 1px solid var(--border); | |
| background: #111; | |
| } | |
| .detail-panel.open { display: block; } | |
| .detail-inner { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 0; | |
| border-top: 1px solid var(--border); | |
| } | |
| /* LEFT SIDE: current bibtex */ | |
| .detail-bib { | |
| padding: 12px 14px; | |
| border-right: 1px solid var(--border); | |
| } | |
| .detail-section-label { | |
| font-family: var(--mono); font-size: 9px; text-transform: uppercase; letter-spacing: 0.1em; | |
| color: var(--text-faint); margin-bottom: 8px; display: flex; align-items: center; justify-content: space-between; | |
| } | |
| .bibtex-pre { | |
| font-family: var(--mono); font-size: 11px; color: var(--text-dim); | |
| white-space: pre-wrap; line-height: 1.6; | |
| border-left: 2px solid var(--border); padding: 8px 10px; | |
| background: rgba(255,255,255,0.02); border-radius: 0 3px 3px 0; | |
| max-height: 160px; overflow-y: auto; | |
| } | |
| .bibtex-pre.bib-green { border-left-color: var(--green); } | |
| .bibtex-pre.bib-yellow { border-left-color: var(--yellow); } | |
| .bibtex-pre.bib-red { border-left-color: var(--red); } | |
| .bibtex-pre::-webkit-scrollbar { width: 3px; } | |
| .bibtex-pre::-webkit-scrollbar-thumb { background: var(--border); } | |
| .detail-actions { display: flex; gap: 6px; margin-top: 8px; flex-wrap: wrap; align-items: center; } | |
| .missing-hint { font-family: var(--mono); font-size: 10px; color: var(--red); opacity: 0.7; margin-left: auto; } | |
| /* RIGHT SIDE: paste patch */ | |
| .detail-patch { padding: 12px 14px; } | |
| .paste-hint { | |
| font-family: var(--mono); font-size: 10px; color: var(--text-faint); | |
| margin-bottom: 8px; line-height: 1.5; | |
| } | |
| .paste-hint .step { color: var(--text-dim); } | |
| .paste-hint .step-accent { color: var(--accent); } | |
| .patch-ta { | |
| width: 100%; height: 140px; | |
| background: var(--bg); border: 1px solid var(--border); border-radius: 3px; | |
| color: var(--text); font-family: var(--mono); font-size: 11px; | |
| padding: 8px 10px; resize: none; outline: none; line-height: 1.5; | |
| transition: border-color 0.15s; | |
| } | |
| .patch-ta:focus { border-color: var(--accent-dim); } | |
| .patch-ta::placeholder { color: var(--text-faint); } | |
| .patch-actions { display: flex; gap: 8px; margin-top: 8px; } | |
| /* patched indicator on right side */ | |
| .patched-banner { | |
| display: none; | |
| font-family: var(--mono); font-size: 10px; color: var(--green); | |
| background: rgba(110,240,160,0.07); border: 1px solid rgba(110,240,160,0.2); | |
| border-radius: 3px; padding: 6px 10px; margin-bottom: 8px; | |
| align-items: center; gap: 8px; | |
| } | |
| .patched-banner.show { display: flex; } | |
| /* EMPTY */ | |
| .empty-state { padding: 60px 20px; display: flex; flex-direction: column; align-items: center; justify-content: center; color: var(--text-faint); gap: 8px; font-family: var(--mono); font-size: 12px; } | |
| .empty-icon { font-size: 32px; opacity: 0.3; } | |
| /* TOAST */ | |
| .toast { position: fixed; bottom: 24px; right: 24px; background: var(--surface); border: 1px solid var(--border); border-radius: 4px; padding: 10px 16px; font-family: var(--mono); font-size: 11px; color: var(--text); opacity: 0; transform: translateY(8px); transition: all 0.2s; z-index: 100; pointer-events: none; max-width: 340px; } | |
| .toast.show { opacity: 1; transform: translateY(0); } | |
| .toast.success { border-color: var(--accent-dim); color: var(--accent); } | |
| .toast.error { border-color: var(--red); color: var(--red); } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="app"> | |
| <header> | |
| <div class="logo">RefManager</div> | |
| <div class="tagline">// bibtex pipeline</div> | |
| <div class="header-stats"> | |
| <div class="vpills"> | |
| <div class="vpill vpill-green"><span id="count-green">0</span> verified</div> | |
| <div class="vpill vpill-yellow"><span id="count-yellow">0</span> uncertain</div> | |
| <div class="vpill vpill-red"><span id="count-red">0</span> incomplete</div> | |
| </div> | |
| <div class="stat"> | |
| <div class="stat-val" id="stat-total">0</div> | |
| <div class="stat-label">total</div> | |
| </div> | |
| </div> | |
| </header> | |
| <div class="main"> | |
| <!-- LEFT --> | |
| <div class="left-panel"> | |
| <div class="mode-tabs"> | |
| <button class="mode-tab" data-mode="bibtex">BibTeX</button> | |
| <button class="mode-tab active" data-mode="title">Titles</button> | |
| </div> | |
| <div class="panel-header"> | |
| Input <span id="char-count">0 chars</span> | |
| </div> | |
| <textarea id="bibtex-input" placeholder="One title per line: | |
| Deep Learning for Energy Systems | |
| TinyBERT: Distilling BERT for NLU | |
| Attention Is All You Need"></textarea> | |
| <div class="options-row"> | |
| <div class="toggle-group"> | |
| <label class="toggle"><input type="checkbox" id="opt-enrich"><div class="toggle-pill"></div><span class="toggle-label">Crossref</span></label> | |
| <label class="toggle"><input type="checkbox" id="opt-abbreviate"><div class="toggle-pill"></div><span class="toggle-label">Abbreviate</span></label> | |
| <label class="toggle"><input type="checkbox" id="opt-protect"><div class="toggle-pill"></div><span class="toggle-label">Protect caps</span></label> | |
| <label class="toggle"><input type="checkbox" id="opt-save"><div class="toggle-pill"></div><span class="toggle-label">Save to DB</span></label> | |
| </div> | |
| <div class="file-row"> | |
| <span class="file-label">.tex file</span> | |
| <div class="file-input-wrap"> | |
| <input type="file" id="latex-file" accept=".tex"> | |
| <div class="file-display" id="file-display">no file selected</div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="action-row"> | |
| <button class="btn btn-primary" id="process-btn" onclick="processRefs()">Process</button> | |
| <button class="btn btn-ghost" onclick="clearInput()" title="Clear input">βΊ</button> | |
| <button class="btn btn-danger" onclick="clearAll()" title="Clear results">β</button> | |
| </div> | |
| </div> | |
| <!-- RIGHT --> | |
| <div class="right-panel"> | |
| <div class="loading-bar" id="loading-bar"><div class="loading-fill"></div></div> | |
| <div class="right-toolbar"> | |
| <span class="panel-label">References</span> | |
| <span class="count-badge" id="ref-count">0 entries</span> | |
| <div class="filter-toggle"> | |
| <button class="ftab active" onclick="setFilter('all',this)">All</button> | |
| <button class="ftab" onclick="setFilter('green',this)">β Verified</button> | |
| <button class="ftab" onclick="setFilter('yellow',this)">β Uncertain</button> | |
| <button class="ftab" onclick="setFilter('red',this)">β Incomplete</button> | |
| </div> | |
| <div class="toolbar-right"> | |
| <button class="btn btn-ghost btn-sm" id="copy-verified-btn" onclick="copyVerified()" style="display:none">Copy verified</button> | |
| <button class="btn btn-ghost btn-sm" id="copy-all-btn" onclick="copyAll()" style="display:none">Copy all</button> | |
| <button class="btn btn-ghost btn-sm" id="export-btn" onclick="exportBib()" style="display:none">Export .bib</button> | |
| </div> | |
| </div> | |
| <div class="refs-header" id="refs-header" style="display:none"> | |
| <div class="col-head">#</div> | |
| <div class="col-head">β</div> | |
| <div class="col-head">Year</div> | |
| <div class="col-head">Author</div> | |
| <div class="col-head">Title</div> | |
| <div class="col-head">Journal</div> | |
| <div class="col-head">BibKey</div> | |
| </div> | |
| <div class="refs-list" id="refs-list"> | |
| <div class="empty-state"> | |
| <div class="empty-icon">β</div> | |
| <div>enter titles or bibtex and press process</div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="toast" id="toast"></div> | |
| <script> | |
| let data = []; // processed entries | |
| let patches = {}; // idx -> manually pasted BibTeX | |
| let openRow = null; // currently expanded row index | |
| let curFilter = 'all'; | |
| let curMode = 'title'; | |
| // ββ MODE TABS ββββββββββββββββββββββββββββββββββββββββββββ | |
| document.querySelectorAll('.mode-tab').forEach(tab => { | |
| tab.addEventListener('click', () => { | |
| document.querySelectorAll('.mode-tab').forEach(t => t.classList.remove('active')); | |
| tab.classList.add('active'); | |
| curMode = tab.dataset.mode; | |
| const ta = document.getElementById('bibtex-input'); | |
| if (curMode === 'bibtex') { | |
| ta.placeholder = '@article{smith2024,\n author = {Smith, John},\n title = {...},\n journal = {Applied Energy},\n year = {2024}\n}\n\nPaste BibTeX entries...'; | |
| } else { | |
| ta.placeholder = 'One title per line:\n\nDeep Learning for Energy Systems\nTinyBERT: Distilling BERT for NLU\nAttention Is All You Need'; | |
| } | |
| }); | |
| }); | |
| document.getElementById('bibtex-input').addEventListener('input', function() { | |
| document.getElementById('char-count').textContent = this.value.length.toLocaleString() + ' chars'; | |
| }); | |
| document.getElementById('latex-file').addEventListener('change', function() { | |
| const d = document.getElementById('file-display'); | |
| if (this.files.length) { d.textContent = this.files[0].name; d.classList.add('has-file'); } | |
| else { d.textContent = 'no file selected'; d.classList.remove('has-file'); } | |
| }); | |
| // ββ SCORING ββββββββββββββββββββββββββββββββββββββββββββββ | |
| function score(r) { | |
| let s = 0, missing = []; | |
| const doi = (r.DOI||'').trim(); | |
| doi && doi.length > 5 ? s+=3 : missing.push('no DOI'); | |
| const key = (r.Key||r.Reference||'').trim(); | |
| key && !/^\d+$/.test(key) && key.length>2 ? s+=2 : missing.push('bad key'); | |
| const yr = (r.Year||'').trim(); | |
| yr && /\d{4}/.test(yr) ? s+=1 : missing.push('no year'); | |
| const au = (r.Authors||'').trim(); | |
| au && au.length>3 ? s+=1 : missing.push('no authors'); | |
| const ti = (r.Title||'').trim(); | |
| ti && ti.length>10 ? s+=1 : missing.push('no title'); | |
| const jo = (r['Journal/Booktitle']||'').trim(); | |
| jo && jo.length>2 ? s+=1 : missing.push('no journal'); | |
| const sim = r.Title_Similarity||0; | |
| if (sim>=85) s+=3; else if (sim>=60) s+=1; | |
| return { status: s>=7?'green':s>=4?'yellow':'red', score:s, missing }; | |
| } | |
| // ββ PROCESS ββββββββββββββββββββββββββββββββββββββββββββββ | |
| async function processRefs() { | |
| const content = document.getElementById('bibtex-input').value.trim(); | |
| if (!content) { showToast('No input provided','error'); return; } | |
| setLoading(true); | |
| patches = {}; openRow = null; | |
| const fd = new FormData(); | |
| fd.append('bibtex_content', content); | |
| fd.append('input_mode', curMode); | |
| fd.append('enrich', document.getElementById('opt-enrich').checked); | |
| fd.append('abbreviate', document.getElementById('opt-abbreviate').checked); | |
| fd.append('protect', document.getElementById('opt-protect').checked); | |
| fd.append('save_to_db', document.getElementById('opt-save').checked); | |
| const lf = document.getElementById('latex-file').files[0]; | |
| if (lf) fd.append('latex_file', lf); | |
| try { | |
| const res = await fetch('/api/process', {method:'POST', body:fd}); | |
| const json = await res.json(); | |
| if (!res.ok || !json.success) { showToast(json.error||'Processing failed','error'); setLoading(false); return; } | |
| data = json.full_data || json.data || []; | |
| reScore(); | |
| render(); | |
| showToast('Processed ' + data.length + ' references'); | |
| } catch(e) { | |
| showToast('Network error: '+e.message,'error'); | |
| } | |
| setLoading(false); | |
| } | |
| function reScore() { | |
| data.forEach((r,i) => { | |
| if (patches[i]) { r._status='green'; r._missing=[]; } | |
| else { const v=score(r); r._status=v.status; r._missing=v.missing; } | |
| }); | |
| } | |
| // ββ RENDER ββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function render() { | |
| const counts = {green:0,yellow:0,red:0}; | |
| data.forEach(r => counts[r._status]++); | |
| document.getElementById('count-green').textContent = counts.green; | |
| document.getElementById('count-yellow').textContent = counts.yellow; | |
| document.getElementById('count-red').textContent = counts.red; | |
| document.getElementById('stat-total').textContent = data.length; | |
| document.getElementById('ref-count').textContent = data.length+' entries'; | |
| const show = data.length > 0; | |
| document.getElementById('refs-header').style.display = show ? 'grid' : 'none'; | |
| ['copy-verified-btn','copy-all-btn','export-btn'].forEach(id => | |
| document.getElementById(id).style.display = show ? 'inline-block' : 'none'); | |
| const list = document.getElementById('refs-list'); | |
| const filtered = data.map((r,i)=>({r,i})).filter(({r})=> curFilter==='all'||r._status===curFilter); | |
| if (!filtered.length) { | |
| list.innerHTML = '<div class="empty-state"><div class="empty-icon">β</div><div>no entries match filter</div></div>'; | |
| return; | |
| } | |
| list.innerHTML = filtered.map(({r,i}) => rowHTML(r,i)).join(''); | |
| // re-open previously open row if still visible | |
| if (openRow !== null) { | |
| const panel = document.getElementById('detail-'+openRow); | |
| const row = document.getElementById('row-'+openRow); | |
| if (panel) { panel.classList.add('open'); if(row) row.classList.add('selected'); } | |
| else openRow = null; | |
| } | |
| } | |
| function rowHTML(r, i) { | |
| const year = r.Year || 'β'; | |
| const author = firstAuthor(r.Authors); | |
| const title = r.Title || 'β'; | |
| const journal = r['Journal/Booktitle'] || 'β'; | |
| const key = r.Key || r.Reference || 'β'; | |
| const status = r._status; | |
| const patched = !!patches[i]; | |
| const bib = getBib(i); | |
| const sim = r.Title_Similarity||0; | |
| const simBadge = sim>0 | |
| ? `<span class="sim-badge ${sim>=85?'sim-high':sim>=60?'sim-mid':'sim-low'}">${sim}%</span>` | |
| : ''; | |
| const bibClass = `bib-${status}`; | |
| const missingTxt = (r._missing||[]).join(', '); | |
| return ` | |
| <div class="ref-row${openRow===i?' selected':''}" id="row-${i}" onclick="toggleRow(${i})"> | |
| <div class="ref-cell cell-idx">${i+1}</div> | |
| <div class="ref-cell cell-status"><span class="status-dot dot-${status}" title="${status}${patched?' (patched)':''}"></span></div> | |
| <div class="ref-cell cell-year">${escHtml(year)}</div> | |
| <div class="ref-cell cell-author">${escHtml(author)}</div> | |
| <div class="ref-cell cell-title">${escHtml(title)}${simBadge}</div> | |
| <div class="ref-cell cell-journal">${escHtml(journal)}</div> | |
| <div class="ref-cell cell-key">${escHtml(key)}</div> | |
| </div> | |
| <div class="detail-panel${openRow===i?' open':''}" id="detail-${i}"> | |
| <div class="detail-inner"> | |
| <!-- LEFT: current BibTeX --> | |
| <div class="detail-bib"> | |
| <div class="detail-section-label"> | |
| <span>Current BibTeX</span> | |
| ${patched ? '<span style="color:var(--green);font-size:10px">β patched</span>' : ''} | |
| </div> | |
| <div class="bibtex-pre ${bibClass}" id="bibpre-${i}">${escHtml(bib)}</div> | |
| <div class="detail-actions"> | |
| <button class="btn btn-ghost btn-sm" onclick="copyBib(event,${i})">Copy BibTeX</button> | |
| ${status!=='green'||patched ? `<button class="btn btn-ghost btn-sm" onclick="copyTitleClick(event,${i})">Copy title</button>` : ''} | |
| ${patched ? `<button class="btn btn-danger btn-sm" onclick="removePatch(event,${i})">Remove patch</button>` : ''} | |
| ${missingTxt ? `<span class="missing-hint">${escHtml(missingTxt)}</span>` : ''} | |
| </div> | |
| </div> | |
| <!-- RIGHT: paste patch --> | |
| <div class="detail-patch"> | |
| <div class="detail-section-label">Paste from Google Scholar</div> | |
| ${patched | |
| ? `<div class="patched-banner show">β This entry has been manually verified. You can paste a new version below to replace it.</div>` | |
| : `<div class="paste-hint"><span class="step">1.</span> Click <span class="step-accent">Copy title</span> β search Google Scholar<br><span class="step">2.</span> Click "Cite" β BibTeX β copy<br><span class="step">3.</span> Paste here and apply</div>` | |
| } | |
| <textarea class="patch-ta" id="patch-${i}" placeholder="@article{key, author = {...}, title = {...}, year = {2024}, doi = {10.xxx/yyy} }">${patches[i]||''}</textarea> | |
| <div class="patch-actions"> | |
| <button class="btn btn-primary btn-sm" onclick="applyPatch(event,${i})">Apply & verify β</button> | |
| </div> | |
| </div> | |
| </div> | |
| </div>`; | |
| } | |
| // ββ ROW TOGGLE ββββββββββββββββββββββββββββββββββββββββββββ | |
| function toggleRow(i) { | |
| if (openRow === i) { | |
| // close | |
| const panel = document.getElementById('detail-'+i); | |
| const row = document.getElementById('row-'+i); | |
| if(panel) panel.classList.remove('open'); | |
| if(row) row.classList.remove('selected'); | |
| openRow = null; | |
| } else { | |
| // close old | |
| if (openRow !== null) { | |
| const old = document.getElementById('detail-'+openRow); | |
| const oldr = document.getElementById('row-'+openRow); | |
| if(old) old.classList.remove('open'); | |
| if(oldr) oldr.classList.remove('selected'); | |
| } | |
| openRow = i; | |
| const panel = document.getElementById('detail-'+i); | |
| const row = document.getElementById('row-'+i); | |
| if(panel) panel.classList.add('open'); | |
| if(row) row.classList.add('selected'); | |
| } | |
| } | |
| function setFilter(f, el) { | |
| curFilter = f; | |
| document.querySelectorAll('.ftab').forEach(t=>t.classList.remove('active')); | |
| el.classList.add('active'); | |
| render(); | |
| } | |
| // ββ PATCH βββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function applyPatch(e, i) { | |
| e.stopPropagation(); | |
| const val = (document.getElementById('patch-'+i).value||'').trim(); | |
| if (!val || !val.includes('@')) { showToast('Paste a valid BibTeX entry','error'); return; } | |
| patches[i] = val; | |
| // parse metadata from pasted BibTeX to update table row | |
| const parsed = quickParseBib(val); | |
| if (parsed.year) data[i].Year = parsed.year; | |
| if (parsed.authors) data[i].Authors = parsed.authors; | |
| if (parsed.title) data[i].Title = parsed.title; | |
| if (parsed.journal) data[i]['Journal/Booktitle'] = parsed.journal; | |
| if (parsed.key) data[i].Key = parsed.key; | |
| if (parsed.doi) data[i].DOI = parsed.doi; | |
| reScore(); | |
| render(); | |
| showToast('Verified β β table updated'); | |
| } | |
| function removePatch(e, i) { | |
| e.stopPropagation(); | |
| delete patches[i]; | |
| reScore(); | |
| render(); | |
| showToast('Patch removed'); | |
| } | |
| // quick BibTeX field extractor (no full parser needed) | |
| function quickParseBib(bib) { | |
| const get = (field) => { | |
| const m = bib.match(new RegExp(field+'\\s*=\\s*[{"]([^{}]*(?:\\{[^{}]*\\}[^{}]*)*)[}"]','i')); | |
| return m ? m[1].replace(/[{}]/g,'').trim() : ''; | |
| }; | |
| // extract key from first line @type{key, | |
| const keyM = bib.match(/@\w+\{([^,]+),/); | |
| return { | |
| key: keyM ? keyM[1].trim() : '', | |
| year: get('year'), | |
| authors: get('author'), | |
| title: get('title'), | |
| journal: get('journal') || get('booktitle'), | |
| doi: get('doi'), | |
| }; | |
| } | |
| // ββ COPY / EXPORT βββββββββββββββββββββββββββββββββββββββββ | |
| function copyTitleClick(e, i) { | |
| e.stopPropagation(); | |
| navigator.clipboard.writeText(data[i].Title||'').then(()=> | |
| showToast('Title copied β search Google Scholar')); | |
| } | |
| function copyBib(e, i) { | |
| e.stopPropagation(); | |
| navigator.clipboard.writeText(getBib(i)).then(()=>showToast('BibTeX copied')); | |
| } | |
| function copyVerified() { | |
| const bibs = data.filter(r=>r._status==='green').map((_,i)=>getBib(i)).filter(Boolean); | |
| // rebuild with correct indices | |
| const out = data.map((r,i)=> r._status==='green' ? getBib(i) : null).filter(Boolean); | |
| navigator.clipboard.writeText(out.join('\n\n')).then(()=> | |
| showToast('Copied '+out.length+' verified entries')); | |
| } | |
| function copyAll() { | |
| const out = data.map((_,i)=>getBib(i)).filter(Boolean); | |
| navigator.clipboard.writeText(out.join('\n\n')).then(()=> | |
| showToast('Copied '+out.length+' entries')); | |
| } | |
| function exportBib() { | |
| const out = data.map((_,i)=>getBib(i)).filter(Boolean).join('\n\n'); | |
| const a = Object.assign(document.createElement('a'),{ | |
| href: URL.createObjectURL(new Blob([out],{type:'text/plain'})), | |
| download: 'references.bib' | |
| }); | |
| a.click(); | |
| showToast('Exported references.bib'); | |
| } | |
| // ββ HELPERS βββββββββββββββββββββββββββββββββββββββββββββββ | |
| function getBib(i) { | |
| if (patches[i]) return patches[i]; | |
| const r = data[i]; | |
| return r.Crossref_BibTeX_Protected||r.Crossref_BibTeX_Abbrev|| | |
| r.Crossref_BibTeX_LocalKey||r.Crossref_BibTeX||r.BibTeX||''; | |
| } | |
| function firstAuthor(authors) { | |
| if (!authors) return 'β'; | |
| return (authors.split(/,|and\s/)[0].trim().split(/\s+/).pop())||'β'; | |
| } | |
| function clearInput() { | |
| document.getElementById('bibtex-input').value=''; | |
| document.getElementById('char-count').textContent='0 chars'; | |
| } | |
| function clearAll() { | |
| data=[]; patches={}; openRow=null; | |
| ['count-green','count-yellow','count-red'].forEach(id=>document.getElementById(id).textContent='0'); | |
| document.getElementById('stat-total').textContent='0'; | |
| document.getElementById('ref-count').textContent='0 entries'; | |
| document.getElementById('refs-header').style.display='none'; | |
| ['copy-verified-btn','copy-all-btn','export-btn'].forEach(id=> | |
| document.getElementById(id).style.display='none'); | |
| document.getElementById('refs-list').innerHTML= | |
| '<div class="empty-state"><div class="empty-icon">β</div><div>enter titles or bibtex and press process</div></div>'; | |
| showToast('Cleared'); | |
| } | |
| function setLoading(v) { | |
| document.getElementById('loading-bar').classList.toggle('active',v); | |
| const btn=document.getElementById('process-btn'); | |
| btn.disabled=v; btn.textContent=v?'Processingβ¦':'Process'; | |
| } | |
| function showToast(msg, type='success') { | |
| const t=document.getElementById('toast'); | |
| t.textContent=msg; t.className='toast '+type+' show'; | |
| setTimeout(()=>t.classList.remove('show'),3000); | |
| } | |
| function escHtml(s) { | |
| if (s==null) return ''; | |
| return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); | |
| } | |
| </script> | |
| </body> | |
| </html> |