feat(dashboard): export current view
This commit is contained in:
@@ -216,6 +216,28 @@ body {
|
|||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#exportFormat {
|
||||||
|
background: #1e1e28;
|
||||||
|
border: 1px solid #2a2a3a;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
color: #e8e8ee;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-export {
|
||||||
|
background: #1e1e28;
|
||||||
|
border: 1px solid #2a2a3a;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
color: #cfd3ff;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.btn-export:active { transform: scale(0.98); }
|
||||||
|
|
||||||
/* ─── New Engram ──────────────────────────────────────────────────────────── */
|
/* ─── New Engram ──────────────────────────────────────────────────────────── */
|
||||||
.new-engram {
|
.new-engram {
|
||||||
padding: 0 12px 8px;
|
padding: 0 12px 8px;
|
||||||
|
|||||||
@@ -34,6 +34,11 @@
|
|||||||
<option value="rejected">Rejected</option>
|
<option value="rejected">Rejected</option>
|
||||||
<option value="errors">Errors</option>
|
<option value="errors">Errors</option>
|
||||||
</select>
|
</select>
|
||||||
|
<select id="exportFormat" title="Export format">
|
||||||
|
<option value="jsonl">JSONL</option>
|
||||||
|
<option value="csv">CSV</option>
|
||||||
|
</select>
|
||||||
|
<button class="btn-export" onclick="exportCurrent()">Export</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- New Engram -->
|
<!-- New Engram -->
|
||||||
@@ -167,14 +172,7 @@ function setView(view) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function loadCards() {
|
async function loadCards() {
|
||||||
let url = `/api/engrams?limit=${state.limit}&offset=${state.offset}`;
|
const data = await api(buildEngramsUrl(state.limit, state.offset));
|
||||||
if (state.search) url += `&search=${encodeURIComponent(state.search)}`;
|
|
||||||
if (state.filter === 'confirmed') url += '&confirmed=1';
|
|
||||||
if (state.filter === 'pending') url += '&confirmed=0';
|
|
||||||
if (state.filter === 'rejected') url += '&verdict=confirmed_false';
|
|
||||||
if (state.filter === 'errors') url += '&tag=error';
|
|
||||||
|
|
||||||
const data = await api(url);
|
|
||||||
state.items = data.items;
|
state.items = data.items;
|
||||||
renderCardsWithSuggestions();
|
renderCardsWithSuggestions();
|
||||||
document.getElementById('pageNum').textContent = Math.floor(state.offset / state.limit) + 1;
|
document.getElementById('pageNum').textContent = Math.floor(state.offset / state.limit) + 1;
|
||||||
@@ -182,6 +180,68 @@ async function loadCards() {
|
|||||||
document.getElementById('btnNext').disabled = data.items.length < state.limit;
|
document.getElementById('btnNext').disabled = data.items.length < state.limit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildEngramsUrl(limit, offset) {
|
||||||
|
let url = `/api/engrams?limit=${limit}&offset=${offset}`;
|
||||||
|
if (state.search) url += `&search=${encodeURIComponent(state.search)}`;
|
||||||
|
if (state.filter === 'confirmed') url += '&confirmed=1';
|
||||||
|
if (state.filter === 'pending') url += '&confirmed=0';
|
||||||
|
if (state.filter === 'rejected') url += '&verdict=confirmed_false';
|
||||||
|
if (state.filter === 'errors') url += '&tag=error';
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function exportCurrent() {
|
||||||
|
const fmt = (document.getElementById('exportFormat')?.value || 'jsonl').toLowerCase();
|
||||||
|
const limit = 100;
|
||||||
|
const max = 5000;
|
||||||
|
let offset = 0;
|
||||||
|
let all = [];
|
||||||
|
while (all.length < max) {
|
||||||
|
const data = await api(buildEngramsUrl(limit, offset));
|
||||||
|
const items = data.items || [];
|
||||||
|
all = all.concat(items);
|
||||||
|
if (items.length < limit) break;
|
||||||
|
offset += limit;
|
||||||
|
}
|
||||||
|
|
||||||
|
const safe = (s) => String(s ?? '').replace(/[\\r\\n]+/g, ' ').trim();
|
||||||
|
let payload = '';
|
||||||
|
let mime = 'text/plain';
|
||||||
|
if (fmt === 'csv') {
|
||||||
|
mime = 'text/csv';
|
||||||
|
const esc = (v) => '\"' + String(v ?? '').replace(/\"/g, '\"\"') + '\"';
|
||||||
|
payload += ['id','created','source','confidence','verdict','tags','content'].join(',') + '\\n';
|
||||||
|
for (const it of all) {
|
||||||
|
payload += [
|
||||||
|
esc(it.id),
|
||||||
|
esc(it.created),
|
||||||
|
esc(it.source),
|
||||||
|
esc(it.confidence),
|
||||||
|
esc(it.verdict),
|
||||||
|
esc((it.tags || []).join('|')),
|
||||||
|
esc((it.content || '').replace(/\\r?\\n/g, '\\\\n')),
|
||||||
|
].join(',') + '\\n';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
mime = 'application/x-ndjson';
|
||||||
|
payload = all.map(x => JSON.stringify(x)).join('\\n') + (all.length ? '\\n' : '');
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
const ymd = `${now.getFullYear()}-${String(now.getMonth()+1).padStart(2,'0')}-${String(now.getDate()).padStart(2,'0')}`;
|
||||||
|
const filename = `second-brain_${ymd}_${safe(state.filter || 'all')}_${fmt}.${fmt}`;
|
||||||
|
|
||||||
|
const blob = new Blob([payload], {type: mime});
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = filename;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
a.remove();
|
||||||
|
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
||||||
|
}
|
||||||
|
|
||||||
async function loadStatus() {
|
async function loadStatus() {
|
||||||
const reqs = await Promise.allSettled([
|
const reqs = await Promise.allSettled([
|
||||||
api('/api/config'),
|
api('/api/config'),
|
||||||
|
|||||||
Reference in New Issue
Block a user