feat: add Asset Library panel with papers list and drag-to-canvas

This commit is contained in:
诺斯费拉图 2026-04-12 17:28:51 +08:00
parent 62956a16e8
commit efecc78f54
3 changed files with 140 additions and 0 deletions

View File

@ -108,3 +108,42 @@ body {
.empty-state { color: #858585; font-style: italic; padding: 1rem; }
.error { color: #f48771; padding: 1rem; }
/* Asset Panel */
.asset-panel { max-width: 1200px; margin: 0 auto; }
.asset-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem; }
.asset-header h2 { color: #fff; }
.asset-tabs { display: flex; gap: 0.25rem; }
.tab-btn {
background: transparent; border: none; color: #d4d4d4;
padding: 0.5rem 1rem; cursor: pointer; border-radius: 4px;
}
.tab-btn:hover { background: #3c3c3c; }
.tab-btn.active { background: #37373d; color: #fff; }
.asset-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 1rem; }
.asset-card {
background: #252526; border-radius: 6px; padding: 1rem;
cursor: grab;
}
.asset-card:active { cursor: grabbing; }
.asset-card h4 { color: #fff; margin-bottom: 0.5rem; }
.asset-meta { color: #858585; font-size: 0.85rem; }
.asset-badges { display: flex; gap: 0.5rem; margin: 0.5rem 0; }
.badge {
font-size: 0.7rem; padding: 0.2rem 0.4rem;
border-radius: 3px; background: #37373d; color: #d4d4d4;
}
.badge.unread { background: #d29922; color: #000; }
.badge.library { background: #2ea043; color: #fff; }
.badge.pending { background: #6c6c6c; color: #fff; }
.badge.enabled { background: #2ea043; color: #fff; }
.badge.disabled { background: #6c6c6c; color: #fff; }
.asset-actions { display: flex; gap: 0.5rem; margin-top: 0.75rem; }
.btn-quick-read, .btn-drag {
background: #37373d; border: none; color: #d4d4d4;
padding: 0.4rem 0.8rem; border-radius: 4px; cursor: pointer; font-size: 0.8rem;
}
.btn-quick-read:hover, .btn-drag:hover { background: #4f4f4f; }

View File

@ -1,5 +1,6 @@
/** Main Research Workbench app - Home and Project Workspace panels. */
import * as api from "./api_client.js";
import { renderAssetPanel } from "./panels/AssetPanel.js";
const mainContent = document.getElementById("main-content");
const navButtons = document.querySelectorAll(".nav-btn");
@ -38,6 +39,9 @@ async function render() {
}
} else if (currentPanel === "canvas") {
window.location.href = "/";
} else if (currentPanel === "assets") {
mainContent.innerHTML = "";
renderAssetPanel(mainContent, api);
}
}

View File

@ -0,0 +1,97 @@
// research_web/js/panels/AssetPanel.js
/**Asset Library panel - browse and manage paper assets.*/
export function renderAssetPanel(container, api) {
container.innerHTML = `
<div class="asset-panel">
<div class="asset-header">
<h2>Asset Library</h2>
<div class="asset-tabs">
<button class="tab-btn active" data-tab="papers">Papers</button>
<button class="tab-btn" data-tab="claims">Claims</button>
<button class="tab-btn" data-tab="sources">Sources</button>
</div>
</div>
<div class="asset-content">
<div class="asset-list" id="asset-list">
<p class="loading">Loading...</p>
</div>
</div>
</div>
`;
// Tab switching
container.querySelectorAll('.tab-btn').forEach(btn => {
btn.addEventListener('click', () => {
container.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
loadAssets(btn.dataset.tab);
});
});
// Initial load
loadAssets('papers');
function loadAssets(type) {
const list = container.querySelector('#asset-list');
if (type === 'papers') {
list.innerHTML = '<p class="loading">Loading papers...</p>';
api.listPapers().then(papers => {
if (!papers || papers.length === 0) {
list.innerHTML = '<p class="empty-state">No papers in library. Search papers in ComfyUI canvas to add them.</p>';
return;
}
list.innerHTML = papers.map(p => `
<div class="asset-card paper-card" data-id="${p.id}" draggable="true">
<h4>${p.title || 'Untitled'}</h4>
<p class="asset-meta">${p.authors_text || 'Unknown authors'}</p>
<p class="asset-meta">${p.journal_or_source || ''} ${p.published_at || ''}</p>
<div class="asset-badges">
<span class="badge ${p.read_status}">${p.read_status || 'unread'}</span>
<span class="badge ${p.library_status}">${p.library_status || 'pending'}</span>
</div>
<div class="asset-actions">
<button class="btn-quick-read" data-id="${p.id}">Quick Read</button>
<button class="btn-drag" data-id="${p.id}" title="Drag to canvas">Drag to Canvas</button>
</div>
</div>
`).join('');
// Make cards draggable for canvas
list.querySelectorAll('.asset-card').forEach(card => {
card.addEventListener('dragstart', (e) => {
e.dataTransfer.setData('application/json', JSON.stringify({
type: 'paper_asset',
id: card.dataset.id,
title: card.querySelector('h4').textContent
}));
e.dataTransfer.effectAllowed = 'copy';
});
});
}).catch(err => {
list.innerHTML = `<p class="error">Failed to load: ${err.message}</p>`;
});
} else if (type === 'claims') {
list.innerHTML = '<p class="empty-state">Claims panel coming in Phase 3.</p>';
} else if (type === 'sources') {
list.innerHTML = '<p class="loading">Loading sources...</p>';
api.listSources().then(sources => {
if (!sources || sources.length === 0) {
list.innerHTML = '<p class="empty-state">No sources configured. Add sources to start your feed.</p>';
return;
}
list.innerHTML = sources.map(s => `
<div class="asset-card source-card" data-id="${s.id}">
<h4>${s.name}</h4>
<p class="asset-meta">${s.category || ''} - ${s.intake_type}</p>
<div class="asset-badges">
<span class="badge ${s.enabled ? 'enabled' : 'disabled'}">${s.enabled ? 'enabled' : 'disabled'}</span>
</div>
</div>
`).join('');
}).catch(err => {
list.innerHTML = `<p class="error">Failed to load: ${err.message}</p>`;
});
}
}
}