feat(dashboard): add corpus dashboard at /dashboard/
Full private corpus dashboard for tools.dobetternorge.no users — each SSO
account gets an auto-provisioned CaveauAI tenant (clients row, corpus) on
first visit. Includes upload (file/paste/URL), RAG chat with SSE streaming
and citation chips, document CRUD, FalkorDB graph relations tab, and
improved save-from-tool flow with tag/preview support.
- dashboard/{index,documents,document,upload,chat,settings}.php
- api/dashboard/{corpus-init,documents,upload,ingest-status,chat-stream,
save-from-tool,graph}.php
- includes/{CorpusProvision,layout_dashboard,layout_dashboard_footer}.php
- assets/css/dashboard.css assets/js/corpus-save.js (routing upgrade)
- includes/{bootstrap,layout}.php extended for dashboard provisioning
Migration 141 (clients.dbn_sso_uid + import_method enum) applied on chloe.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
$dashboardPage = 'index';
|
||||
$dashboardTitle = 'Min korpus';
|
||||
$dashboardLead = 'Privat juridisk kunnskapsbase. Last opp, organiser, spør — alt holdes til din konto.';
|
||||
require_once __DIR__ . '/../includes/layout_dashboard.php';
|
||||
?>
|
||||
<section class="dash-kpis" id="dashKpis" aria-label="Corpus statistics">
|
||||
<div class="dash-kpi">
|
||||
<p class="dash-kpi__label">Dokumenter</p>
|
||||
<p class="dash-kpi__value" data-kpi="documents">—</p>
|
||||
<p class="dash-kpi__hint">av kvote</p>
|
||||
</div>
|
||||
<div class="dash-kpi">
|
||||
<p class="dash-kpi__label">Passasjer indeksert</p>
|
||||
<p class="dash-kpi__value" data-kpi="chunks">—</p>
|
||||
<p class="dash-kpi__hint">søkbare biter</p>
|
||||
</div>
|
||||
<div class="dash-kpi">
|
||||
<p class="dash-kpi__label">Klare</p>
|
||||
<p class="dash-kpi__value" data-kpi="ready">—</p>
|
||||
<p class="dash-kpi__hint">av totalt</p>
|
||||
</div>
|
||||
<div class="dash-kpi">
|
||||
<p class="dash-kpi__label">Siste opplasting</p>
|
||||
<p class="dash-kpi__value" data-kpi="last_upload" style="font-size: 1.05rem;">—</p>
|
||||
<p class="dash-kpi__hint">dato</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="dash-card">
|
||||
<div class="dash-card__head">
|
||||
<h2>Kom i gang</h2>
|
||||
</div>
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 1rem;">
|
||||
<a class="dash-btn dash-btn--primary" href="/dashboard/upload.php">📥 Last opp dokumenter</a>
|
||||
<a class="dash-btn" href="/dashboard/chat.php">💬 Still et juridisk spørsmål</a>
|
||||
<a class="dash-btn" href="/dashboard/documents.php">📚 Bla gjennom korpus</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="dash-card">
|
||||
<div class="dash-card__head">
|
||||
<h2>Nylig aktivitet</h2>
|
||||
<div class="dash-card__actions">
|
||||
<a href="/dashboard/documents.php" class="dash-btn">Se alle →</a>
|
||||
</div>
|
||||
</div>
|
||||
<div id="dashRecent" class="dash-loading">Laster…</div>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
'use strict';
|
||||
const api = window.DBN_DASHBOARD.apiBase;
|
||||
|
||||
function fmtDate(s) {
|
||||
if (!s) return '—';
|
||||
try {
|
||||
return new Date(s.replace(' ', 'T') + 'Z').toLocaleDateString('nb-NO',
|
||||
{ day: 'numeric', month: 'short', year: 'numeric' });
|
||||
} catch (_) { return s; }
|
||||
}
|
||||
function fmtNum(n) { return n == null ? '—' : Number(n).toLocaleString('nb-NO'); }
|
||||
|
||||
function statusPill(status) {
|
||||
const cls = {
|
||||
ready: 'dash-status--ready',
|
||||
pending: 'dash-status--pending',
|
||||
processing: 'dash-status--processing',
|
||||
error: 'dash-status--error',
|
||||
}[status] || 'dash-status--pending';
|
||||
const label = { ready: 'Klar', pending: 'Venter', processing: 'Behandler', error: 'Feil' }[status] || status;
|
||||
return '<span class="dash-status ' + cls + '">' + label + '</span>';
|
||||
}
|
||||
|
||||
fetch(api + '/documents.php?action=list&limit=100', { credentials: 'same-origin' })
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (!data.ok) throw new Error(data.error?.message || 'Failed');
|
||||
const docs = data.documents || [];
|
||||
const total = data.total;
|
||||
const chunks = docs.reduce((s, d) => s + (d.chunk_count || 0), 0);
|
||||
const ready = docs.filter(d => d.status === 'ready').length;
|
||||
const last = docs[0] ? docs[0].created_at : null;
|
||||
|
||||
document.querySelector('[data-kpi="documents"]').textContent = fmtNum(total);
|
||||
document.querySelector('[data-kpi="chunks"]').textContent = fmtNum(chunks);
|
||||
document.querySelector('[data-kpi="ready"]').textContent = ready + ' / ' + docs.length;
|
||||
document.querySelector('[data-kpi="last_upload"]').textContent = fmtDate(last);
|
||||
|
||||
const recent = document.getElementById('dashRecent');
|
||||
if (!docs.length) {
|
||||
recent.className = 'dash-empty';
|
||||
recent.innerHTML = '<span class="dash-empty__icon">📭</span>Ingen dokumenter ennå. '
|
||||
+ '<a href="/dashboard/upload.php">Last opp ditt første</a>.';
|
||||
return;
|
||||
}
|
||||
recent.className = '';
|
||||
recent.innerHTML = '';
|
||||
const table = document.createElement('table');
|
||||
table.className = 'dash-doctable';
|
||||
table.innerHTML = '<thead><tr><th>Tittel</th><th>Status</th><th>Passasjer</th><th>Lagt til</th></tr></thead>';
|
||||
const tbody = document.createElement('tbody');
|
||||
docs.slice(0, 8).forEach(doc => {
|
||||
const tr = document.createElement('tr');
|
||||
tr.addEventListener('click', () => location.href = '/dashboard/document.php?id=' + doc.id);
|
||||
const safe = (s) => String(s ?? '').replace(/[&<>"]/g, c => ({ '&':'&','<':'<','>':'>','"':'"' }[c]));
|
||||
tr.innerHTML =
|
||||
'<td><span class="dash-doctable__title">' + safe(doc.title) + '</span>'
|
||||
+ (doc.source_tool ? '<div class="dash-doctable__meta">via ' + safe(doc.source_tool) + '</div>' : '') + '</td>'
|
||||
+ '<td>' + statusPill(doc.status) + '</td>'
|
||||
+ '<td>' + fmtNum(doc.chunk_count) + '</td>'
|
||||
+ '<td>' + fmtDate(doc.created_at) + '</td>';
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
table.appendChild(tbody);
|
||||
recent.appendChild(table);
|
||||
})
|
||||
.catch(err => {
|
||||
const recent = document.getElementById('dashRecent');
|
||||
recent.className = 'dash-error';
|
||||
recent.textContent = 'Kunne ikke laste: ' + err.message;
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
<?php require_once __DIR__ . '/../includes/layout_dashboard_footer.php'; ?>
|
||||
Reference in New Issue
Block a user