i18n all /dashboard/ corpus pages for en/no/uk/pl

- Add require_once bootstrap.php to all 6 dashboard page files so
  dbnToolsT() is available before layout_dashboard.php is included
- Add dash_upload_category_lbl key to no/uk/pl sections of i18n.php
  (was only in English); Kategori/Категорія/Kategoria
- Fix broken ternary on upload.php Category label — replace with
  dbnToolsT('dash_upload_category_lbl', $uiLang)
- layout_dashboard.php outputs window.DBN_I18N with all js_* keys
  so dashboard JS reads locale-aware strings from PHP translations

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-23 20:10:57 +02:00
parent 90117fa9de
commit a9e64b65ce
8 changed files with 886 additions and 197 deletions
+24 -22
View File
@@ -1,21 +1,22 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/../includes/bootstrap.php';
$dashboardPage = 'chat';
$dashboardTitle = 'Spør korpuset';
$dashboardLead = 'Still juridiske spørsmål. Svar streames med kildehenvisninger til ditt eget korpus og delt Do Better Norge-pakke.';
$dashboardTitle = dbnToolsT('dash_title_chat', dbnToolsCurrentLanguage());
$dashboardLead = dbnToolsT('dash_lead_chat', dbnToolsCurrentLanguage());
require_once __DIR__ . '/../includes/layout_dashboard.php';
?>
<div class="disclaimer" role="note" style="margin-bottom:1rem;">
⚖ Juridisk informasjon og forberedelsesstøtte — ikke endelig juridisk rådgivning. Bekreft alltid med advokat.
<?= htmlspecialchars(dbnToolsT('dash_chat_disclaimer', $uiLang)) ?>
</div>
<section class="dash-card" style="display:flex; flex-direction:column; min-height:60vh;">
<div id="chatLog" class="chat-log" aria-live="polite">
<div class="chat-empty">Still ditt første spørsmål. Det kan handle om barnerett, barnevern, EMD, arbeidsrett — alt som finnes i ditt korpus.</div>
<div class="chat-empty" id="chatEmptyMsg"></div>
</div>
<form id="chatForm" class="chat-form" autocomplete="off">
<textarea id="chatInput" rows="2" required placeholder="f.eks. «Hva sier barnevernloven § 4-12 om plassering uten samtykke?»"></textarea>
<textarea id="chatInput" rows="2" required placeholder="<?= htmlspecialchars(dbnToolsT('dash_chat_placeholder', $uiLang)) ?>"></textarea>
<button type="submit" class="dash-btn dash-btn--primary" id="chatSendBtn">Send</button>
</form>
</section>
@@ -53,13 +54,19 @@ require_once __DIR__ . '/../includes/layout_dashboard.php';
<script>
(function () {
'use strict';
const api = window.DBN_DASHBOARD.apiBase;
const $log = document.getElementById('chatLog');
const I18N = window.DBN_I18N || {};
const api = window.DBN_DASHBOARD.apiBase;
const $log = document.getElementById('chatLog');
const $form = document.getElementById('chatForm');
const $input = document.getElementById('chatInput');
const $send = document.getElementById('chatSendBtn');
const $empty = document.getElementById('chatEmptyMsg');
const history = []; // [{role, content}]
if ($empty) $empty.textContent = I18N.ask_btn
? I18N.ask_btn.replace(/^💬\s*/, '')
: 'Ask your first question. It can be about child welfare, family law, ECHR — anything in your corpus.';
const history = [];
function safe(s) { return String(s ?? '').replace(/[&<>"]/g, c => ({ '&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;' }[c])); }
@@ -79,11 +86,11 @@ require_once __DIR__ . '/../includes/layout_dashboard.php';
const wrap = document.createElement('div');
wrap.className = 'chat-msg chat-msg--ai';
wrap.innerHTML =
'<div class="chat-bubble"><span class="chat-stream"></span><span class="chat-thinking">tenker…</span></div>'
'<div class="chat-bubble"><span class="chat-stream"></span><span class="chat-thinking">' + (I18N.chat_thinking || 'thinking…') + '</span></div>'
+ '<div class="chat-sources" hidden></div>'
+ '<div class="chat-actions" hidden>'
+ '<button class="dash-btn chat-save" type="button">💾 Lagre i korpus</button>'
+ '<button class="dash-btn chat-copy" type="button">📋 Kopier</button>'
+ '<button class="dash-btn chat-save" type="button">' + (I18N.chat_save || '💾 Save to corpus') + '</button>'
+ '<button class="dash-btn chat-copy" type="button">' + (I18N.chat_copy || '📋 Copy') + '</button>'
+ '</div>'
+ '<div class="chat-meta" hidden></div>';
$log.appendChild(wrap);
@@ -94,7 +101,7 @@ require_once __DIR__ . '/../includes/layout_dashboard.php';
async function ask(question) {
appendUser(question);
history.push({ role: 'user', content: question });
const aiNode = appendAi();
const aiNode = appendAi();
const stream = aiNode.querySelector('.chat-stream');
const thinking = aiNode.querySelector('.chat-thinking');
const sources = aiNode.querySelector('.chat-sources');
@@ -123,7 +130,6 @@ require_once __DIR__ . '/../includes/layout_dashboard.php';
if (done) break;
buffer += dec.decode(value, { stream: true });
// Parse SSE frames: blocks separated by "\n\n"
let idx;
while ((idx = buffer.indexOf('\n\n')) !== -1) {
const frame = buffer.slice(0, idx);
@@ -146,15 +152,13 @@ require_once __DIR__ . '/../includes/layout_dashboard.php';
} else if (evName === 'done') {
history.push({ role: 'assistant', content: answer });
renderSources(sources, payload.sources || []);
const chunksTmpl = (I18N.chat_passages_meta || '{n} passages').replace('{n}', payload.chunks_used || 0);
meta.hidden = false;
meta.textContent =
(payload.chunks_used || 0) + ' passasjer · '
+ (payload.model || 'auto') + ' · '
+ (payload.response_time_ms || 0) + ' ms';
meta.textContent = chunksTmpl + ' · ' + (payload.model || 'auto') + ' · ' + (payload.response_time_ms || 0) + ' ms';
actions.hidden = false;
wireActions(aiNode, question, answer);
} else if (evName === 'fail') {
thinking.textContent = '❌ ' + (payload.message || 'Feil');
thinking.textContent = '❌ ' + (payload.message || 'Error');
thinking.style.color = 'var(--dbn-red)';
}
}
@@ -191,22 +195,20 @@ require_once __DIR__ . '/../includes/layout_dashboard.php';
navigator.clipboard.writeText(answer).then(() => {
const btn = node.querySelector('.chat-copy');
const orig = btn.textContent;
btn.textContent = '✓ Kopiert';
btn.textContent = I18N.chat_copied || '✓ Copied';
setTimeout(() => btn.textContent = orig, 1400);
});
});
node.querySelector('.chat-save').addEventListener('click', () => {
// Re-uses existing corpus-save.js dialog (loaded by layout footer)
const dialog = document.getElementById('save-corpus-dialog');
const titleField = document.getElementById('save-corpus-title');
const tagsField = document.getElementById('save-corpus-tags');
if (!dialog || !titleField) {
alert('Lagre-dialog ikke tilgjengelig.');
alert(I18N.chat_save_unavail || 'Save dialog unavailable.');
return;
}
titleField.value = question.slice(0, 80);
tagsField.value = 'chat,answer';
// Hand-off contract used by corpus-save.js: data-pending-content
dialog.dataset.pendingContent = 'Q: ' + question + '\n\nA: ' + answer;
dialog.dataset.pendingTool = 'dashboard-chat';
dialog.showModal();
+50 -38
View File
@@ -1,80 +1,91 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/../includes/bootstrap.php';
$dashboardPage = 'documents';
$dashboardTitle = 'Dokument';
$dashboardTitle = dbnToolsT('dash_title_document', dbnToolsCurrentLanguage());
$dashboardLead = '';
require_once __DIR__ . '/../includes/layout_dashboard.php';
$docId = (int)($_GET['id'] ?? 0);
?>
<div id="docViewRoot" data-doc-id="<?= $docId ?>">
<p class="dash-loading">Laster dokument…</p>
<p class="dash-loading" id="docLoadingMsg"></p>
</div>
<script>
(function () {
'use strict';
const I18N = window.DBN_I18N || {};
const root = document.getElementById('docViewRoot');
const docId = parseInt(root.dataset.docId, 10);
const api = window.DBN_DASHBOARD.apiBase;
const loc = I18N.locale || 'en-GB';
const loadMsg = document.getElementById('docLoadingMsg');
if (loadMsg) loadMsg.textContent = I18N.loading || 'Loading…';
if (!docId) {
root.innerHTML = '<div class="dash-error">Mangler dokument-id.</div>';
root.innerHTML = '<div class="dash-error">' + (I18N.missing_doc_id || 'Missing document ID.') + '</div>';
return;
}
function safe(s) { return String(s ?? '').replace(/[&<>"]/g, c => ({ '&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;' }[c])); }
function fmtDate(s) {
if (!s) return '—';
try { return new Date(s.replace(' ', 'T') + 'Z').toLocaleString('nb-NO', { dateStyle:'medium', timeStyle:'short' }); }
try { return new Date(s.replace(' ', 'T') + 'Z').toLocaleString(loc, { dateStyle:'medium', timeStyle:'short' }); }
catch (_) { return s; }
}
function fmtNum(n) { return n == null ? '—' : Number(n).toLocaleString('nb-NO'); }
function fmtNum(n) { return n == null ? '—' : Number(n).toLocaleString(loc); }
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 lbl = { ready:'Klar', pending:'Venter', processing:'Behandler', error:'Feil' }[status] || status;
const lbl = {
ready: I18N.status_ready || 'Ready', pending: I18N.status_pending || 'Pending',
processing: I18N.status_processing || 'Processing', error: I18N.status_error || 'Error',
}[status] || status;
return '<span class="dash-status ' + cls + '">' + lbl + '</span>';
}
fetch(api + '/documents.php?action=get&id=' + docId, { credentials: 'same-origin' })
.then(r => r.json())
.then(data => {
if (!data.ok) throw new Error(data.error?.message || 'Dokument ikke funnet');
if (!data.ok) throw new Error(data.error?.message || (I18N.doc_not_found || 'Document not found'));
render(data.document, data.chunks || []);
})
.catch(err => {
root.innerHTML = '<div class="dash-error">' + safe(err.message)
+ '</div><p><a href="/dashboard/documents.php" class="dash-btn">← Tilbake</a></p>';
+ '</div><p><a href="/dashboard/documents.php" class="dash-btn">← ' + (I18N.back || 'Back') + '</a></p>';
});
function render(doc, chunks) {
const back = I18N.back || '← Back';
const delBtn = I18N.delete_btn || 'Delete';
const html =
'<section class="dash-card">'
+ '<div class="dash-doc-head">'
+ '<div>'
+ '<h2>' + safe(doc.title) + '</h2>'
+ '<p class="dash-doc-meta">' + statusPill(doc.status)
+ ' · ' + fmtNum(doc.word_count) + ' ord'
+ ' · ' + fmtNum(doc.chunk_count) + ' passasjer'
+ ' · lagt til ' + fmtDate(doc.created_at) + '</p>'
+ (doc.tags ? '<p class="dash-doc-meta">Tagger: ' + safe(doc.tags) + '</p>' : '')
+ (doc.source_url ? '<p class="dash-doc-meta"><a href="' + safe(doc.source_url) + '" target="_blank" rel="noopener">Original kilde ↗</a></p>' : '')
+ ' · ' + fmtNum(doc.word_count) + ' ' + (I18N.words || 'words')
+ ' · ' + fmtNum(doc.chunk_count) + ' ' + (I18N.passages_label || 'passages')
+ ' · ' + (I18N.added_at || 'added') + ' ' + fmtDate(doc.created_at) + '</p>'
+ (doc.tags ? '<p class="dash-doc-meta">' + (I18N.tags_label || 'Tags:') + ' ' + safe(doc.tags) + '</p>' : '')
+ (doc.source_url ? '<p class="dash-doc-meta"><a href="' + safe(doc.source_url) + '" target="_blank" rel="noopener">' + (I18N.source_url_label || 'Original source ↗') + '</a></p>' : '')
+ '</div>'
+ '<div>'
+ '<a href="/dashboard/documents.php" class="dash-btn">← Tilbake</a> '
+ '<button class="dash-btn dash-btn--danger" id="docDelete">Slett</button>'
+ '<a href="/dashboard/documents.php" class="dash-btn">' + back + '</a> '
+ '<button class="dash-btn dash-btn--danger" id="docDelete">' + delBtn + '</button>'
+ '</div>'
+ '</div>'
+ '<nav class="dash-tabs" role="tablist">'
+ '<button class="dash-tab is-active" data-tab="preview" role="tab">Forhåndsvisning</button>'
+ '<button class="dash-tab" data-tab="chunks" role="tab">Passasjer (' + fmtNum(doc.chunk_count) + ')</button>'
+ '<button class="dash-tab" data-tab="related" role="tab">Relatert</button>'
+ '<button class="dash-tab" data-tab="edit" role="tab">Rediger</button>'
+ '<button class="dash-tab is-active" data-tab="preview" role="tab">' + (I18N.tab_preview || 'Preview') + '</button>'
+ '<button class="dash-tab" data-tab="chunks" role="tab">' + (I18N.tab_chunks || 'Passages') + ' (' + fmtNum(doc.chunk_count) + ')</button>'
+ '<button class="dash-tab" data-tab="related" role="tab">' + (I18N.tab_related || 'Related') + '</button>'
+ '<button class="dash-tab" data-tab="edit" role="tab">' + (I18N.tab_edit || 'Edit') + '</button>'
+ '</nav>'
+ '<div class="dash-tab-panel is-active" data-panel="preview">'
+ '<div class="dash-preview">' + safe(doc.content || '(tom)') + '</div>'
+ '<div class="dash-preview">' + safe(doc.content || (I18N.content_empty || '(empty)')) + '</div>'
+ '</div>'
+ '<div class="dash-tab-panel" data-panel="chunks">'
@@ -83,22 +94,22 @@ $docId = (int)($_GET['id'] ?? 0);
'<article class="dash-chunk">'
+ (c.section_title ? '<p class="dash-chunk__section">' + safe(c.section_title) + '</p>' : '')
+ safe(c.content) + '</article>').join('')
: '<div class="dash-empty">Ingen passasjer indeksert ennå.</div>')
: '<div class="dash-empty">' + (I18N.no_chunks || 'No passages indexed yet.') + '</div>')
+ '</div>'
+ '<div class="dash-tab-panel" data-panel="related">'
+ '<div class="dash-loading" id="relatedLoading">Laster relaterte autoriteter fra graphen…</div>'
+ '<div class="dash-loading" id="relatedLoading">' + (I18N.loading_related || 'Loading related authorities from the graph…') + '</div>'
+ '<div class="dash-related" id="relatedList" hidden></div>'
+ '</div>'
+ '<div class="dash-tab-panel" data-panel="edit">'
+ '<form id="docEditForm" style="display:grid; gap:0.85rem; max-width:560px;">'
+ '<label>Tittel<input name="title" value="' + safe(doc.title) + '" style="width:100%;padding:0.5rem;border:1px solid var(--dbn-line);border-radius:8px;"></label>'
+ '<label>Kategori<input name="category" value="' + safe(doc.category || '') + '" style="width:100%;padding:0.5rem;border:1px solid var(--dbn-line);border-radius:8px;"></label>'
+ '<label>Tagger (komma-separert)<input name="tags" value="' + safe(doc.tags || '') + '" style="width:100%;padding:0.5rem;border:1px solid var(--dbn-line);border-radius:8px;"></label>'
+ '<label>Språk<input name="language" value="' + safe(doc.language || 'no') + '" maxlength="10" style="width:120px;padding:0.5rem;border:1px solid var(--dbn-line);border-radius:8px;"></label>'
+ '<label>Forfatter<input name="author" value="' + safe(doc.author || '') + '" style="width:100%;padding:0.5rem;border:1px solid var(--dbn-line);border-radius:8px;"></label>'
+ '<button type="submit" class="dash-btn dash-btn--primary" style="justify-self:start;">Lagre endringer</button>'
+ '<label>' + (I18N.field_title || 'Title') + '<input name="title" value="' + safe(doc.title) + '" style="width:100%;padding:0.5rem;border:1px solid var(--dbn-line);border-radius:8px;"></label>'
+ '<label>' + (I18N.field_category || 'Category') + '<input name="category" value="' + safe(doc.category || '') + '" style="width:100%;padding:0.5rem;border:1px solid var(--dbn-line);border-radius:8px;"></label>'
+ '<label>' + (I18N.field_tags || 'Tags (comma-separated)') + '<input name="tags" value="' + safe(doc.tags || '') + '" style="width:100%;padding:0.5rem;border:1px solid var(--dbn-line);border-radius:8px;"></label>'
+ '<label>' + (I18N.field_lang || 'Language') + '<input name="language" value="' + safe(doc.language || 'no') + '" maxlength="10" style="width:120px;padding:0.5rem;border:1px solid var(--dbn-line);border-radius:8px;"></label>'
+ '<label>' + (I18N.field_author || 'Author') + '<input name="author" value="' + safe(doc.author || '') + '" style="width:100%;padding:0.5rem;border:1px solid var(--dbn-line);border-radius:8px;"></label>'
+ '<button type="submit" class="dash-btn dash-btn--primary" style="justify-self:start;">' + (I18N.save_changes || 'Save changes') + '</button>'
+ '<span id="docEditStatus" style="color:rgba(22,19,15,0.6);font-size:0.85rem;"></span>'
+ '</form>'
+ '</div>'
@@ -137,19 +148,19 @@ $docId = (int)($_GET['id'] ?? 0);
list.hidden = false;
const items = data.results || [];
if (!items.length) {
list.innerHTML = '<div class="dash-empty">Dokumentet har ingen kjente siteringer i graf-databasen ennå.</div>';
list.innerHTML = '<div class="dash-empty">' + (I18N.no_citations || 'No known citations in the graph database yet.') + '</div>';
return;
}
list.innerHTML = items.map(it =>
'<div class="dash-related__edge">'
+ '<span class="dash-related__rel">' + safe(it.rel_type || '—') + '</span>'
+ '<span class="dash-related__title">' + safe(it.title || it.ref || 'Ukjent') + '</span>'
+ '<span class="dash-related__title">' + safe(it.title || it.ref || 'Unknown') + '</span>'
+ '</div>').join('');
})
.catch(_ => {
loading.hidden = true;
list.hidden = false;
list.innerHTML = '<div class="dash-empty">Graf-databasen er ikke tilgjengelig akkurat nå.</div>';
list.innerHTML = '<div class="dash-empty">' + (I18N.graph_unavailable || 'Graph database is not available right now.') + '</div>';
});
}
@@ -157,7 +168,7 @@ $docId = (int)($_GET['id'] ?? 0);
const btn = document.getElementById('docDelete');
if (!btn) return;
btn.addEventListener('click', () => {
if (!confirm('Slette dette dokumentet permanent?')) return;
if (!confirm(I18N.delete_doc_confirm || 'Delete this document permanently?')) return;
btn.disabled = true;
fetch(api + '/documents.php?action=delete', {
method: 'POST', credentials: 'same-origin',
@@ -166,10 +177,10 @@ $docId = (int)($_GET['id'] ?? 0);
})
.then(r => r.json())
.then(data => {
if (!data.ok) throw new Error(data.error?.message || 'Slett feilet');
if (!data.ok) throw new Error(data.error?.message || 'Delete failed');
location.href = '/dashboard/documents.php';
})
.catch(err => { alert('Feil: ' + err.message); btn.disabled = false; });
.catch(err => { alert(err.message); btn.disabled = false; });
});
}
@@ -184,7 +195,7 @@ $docId = (int)($_GET['id'] ?? 0);
const el = form.elements[name];
if (el) payload[name] = el.value;
});
status.textContent = 'Lagrer…';
status.textContent = I18N.saving || 'Saving…';
fetch(api + '/documents.php?action=update', {
method: 'POST', credentials: 'same-origin',
headers: { 'Content-Type': 'application/json' },
@@ -192,10 +203,11 @@ $docId = (int)($_GET['id'] ?? 0);
})
.then(r => r.json())
.then(data => {
if (!data.ok) throw new Error(data.error?.message || 'Lagring feilet');
status.textContent = 'Lagret ' + new Date().toLocaleTimeString('nb-NO');
if (!data.ok) throw new Error(data.error?.message || 'Save failed');
const tmpl = I18N.saved_at || 'Saved {time}';
status.textContent = tmpl.replace('{time}', new Date().toLocaleTimeString(loc));
})
.catch(err => status.textContent = 'Feil: ' + err.message);
.catch(err => status.textContent = err.message);
});
}
})();
+50 -28
View File
@@ -1,31 +1,32 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/../includes/bootstrap.php';
$dashboardPage = 'documents';
$dashboardTitle = 'Dokumenter';
$dashboardLead = 'Alle dokumenter i din private korpus. Klikk for å åpne, eller velg flere for bulk-handlinger.';
$dashboardTitle = dbnToolsT('dash_title_docs', dbnToolsCurrentLanguage());
$dashboardLead = dbnToolsT('dash_lead_docs', dbnToolsCurrentLanguage());
require_once __DIR__ . '/../includes/layout_dashboard.php';
?>
<section class="dash-card">
<div class="dash-filters">
<input type="search" id="docFilterQ" placeholder="Søk i titler eller tagger…" autocomplete="off">
<input type="search" id="docFilterQ" placeholder="<?= htmlspecialchars(dbnToolsT('dash_filter_q_ph', $uiLang)) ?>" autocomplete="off">
<select id="docFilterStatus">
<option value="">Alle statuser</option>
<option value="ready">Klar</option>
<option value="pending">Venter</option>
<option value="processing">Behandler</option>
<option value="error">Feil</option>
<option value=""><?= htmlspecialchars(dbnToolsT('dash_filter_all_status', $uiLang)) ?></option>
<option value="ready" id="optReady"></option>
<option value="pending" id="optPending"></option>
<option value="processing" id="optProcessing"></option>
<option value="error" id="optError"></option>
</select>
<button id="docBulkDelete" class="dash-btn dash-btn--danger" disabled>Slett valgte</button>
<a href="/dashboard/upload.php" class="dash-btn dash-btn--primary" style="margin-left: auto;">+ Last opp</a>
<button id="docBulkDelete" class="dash-btn dash-btn--danger" disabled><?= htmlspecialchars(dbnToolsT('dash_delete_selected', $uiLang)) ?></button>
<a href="/dashboard/upload.php" class="dash-btn dash-btn--primary" style="margin-left: auto;"><?= htmlspecialchars(dbnToolsT('dash_upload_btn_short', $uiLang)) ?></a>
</div>
<div id="docListWrap"><p class="dash-loading">Laster dokumenter…</p></div>
<div id="docListWrap"><p class="dash-loading"></p></div>
<div class="dash-pager" id="docPager" hidden>
<span id="docPagerLabel"></span>
<div class="dash-pager__actions">
<button class="dash-btn" id="docPagerPrev">← Forrige</button>
<button class="dash-btn" id="docPagerNext">Neste →</button>
<button class="dash-btn" id="docPagerPrev"><?= htmlspecialchars(dbnToolsT('dash_prev', $uiLang)) ?></button>
<button class="dash-btn" id="docPagerNext"><?= htmlspecialchars(dbnToolsT('dash_next', $uiLang)) ?></button>
</div>
</div>
</section>
@@ -33,9 +34,20 @@ require_once __DIR__ . '/../includes/layout_dashboard.php';
<script>
(function () {
'use strict';
const api = window.DBN_DASHBOARD.apiBase;
const I18N = window.DBN_I18N || {};
const api = window.DBN_DASHBOARD.apiBase;
const loc = I18N.locale || 'en-GB';
const PAGE = 25;
const optReady = document.getElementById('optReady');
const optPend = document.getElementById('optPending');
const optProc = document.getElementById('optProcessing');
const optErr = document.getElementById('optError');
if (optReady) optReady.textContent = I18N.status_ready || 'Ready';
if (optPend) optPend.textContent = I18N.status_pending || 'Pending';
if (optProc) optProc.textContent = I18N.status_processing || 'Processing';
if (optErr) optErr.textContent = I18N.status_error || 'Error';
const state = { offset: 0, total: 0, selected: new Set(), q: '', status: '' };
const $wrap = document.getElementById('docListWrap');
@@ -50,13 +62,16 @@ require_once __DIR__ . '/../includes/layout_dashboard.php';
function safe(s) { return String(s ?? '').replace(/[&<>"]/g, c => ({ '&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;' }[c])); }
function fmtDate(s) {
if (!s) return '—';
try { return new Date(s.replace(' ', 'T') + 'Z').toLocaleDateString('nb-NO', { day:'numeric', month:'short', year:'numeric' }); }
try { return new Date(s.replace(' ', 'T') + 'Z').toLocaleDateString(loc, { day:'numeric', month:'short', year:'numeric' }); }
catch (_) { return s; }
}
function fmtNum(n) { return n == null ? '—' : Number(n).toLocaleString('nb-NO'); }
function fmtNum(n) { return n == null ? '—' : Number(n).toLocaleString(loc); }
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 lbl = { ready:'Klar', pending:'Venter', processing:'Behandler', error:'Feil' }[status] || status;
const lbl = {
ready: I18N.status_ready || 'Ready', pending: I18N.status_pending || 'Pending',
processing: I18N.status_processing || 'Processing', error: I18N.status_error || 'Error',
}[status] || status;
return '<span class="dash-status ' + cls + '">' + lbl + '</span>';
}
@@ -65,7 +80,7 @@ require_once __DIR__ . '/../includes/layout_dashboard.php';
if (state.q) qs.set('q', state.q);
if (state.status) qs.set('status', state.status);
$wrap.innerHTML = '<p class="dash-loading">Laster dokumenter…</p>';
$wrap.innerHTML = '<p class="dash-loading">' + (I18N.loading_docs || 'Loading documents…') + '</p>';
fetch(api + '/documents.php?' + qs, { credentials:'same-origin' })
.then(r => r.json())
.then(data => {
@@ -81,8 +96,9 @@ require_once __DIR__ . '/../includes/layout_dashboard.php';
function render(docs) {
if (!docs.length) {
$wrap.innerHTML = '<div class="dash-empty"><span class="dash-empty__icon">📭</span>'
+ (state.q || state.status ? 'Ingen treff for valgt filter.'
: 'Ingen dokumenter ennå. <a href="/dashboard/upload.php">Last opp ditt første</a>.')
+ (state.q || state.status
? (I18N.empty_filter || 'No results for selected filter.')
: (I18N.empty_docs || 'No documents yet.') + ' <a href="/dashboard/upload.php">' + (I18N.empty_docs_link || 'Upload your first') + '</a>.')
+ '</div>';
$pager.hidden = true;
return;
@@ -93,8 +109,11 @@ require_once __DIR__ . '/../includes/layout_dashboard.php';
table.innerHTML =
'<thead><tr>'
+ '<th style="width:36px;"><input type="checkbox" id="docSelectAll"></th>'
+ '<th>Tittel</th><th>Kategori</th><th>Status</th>'
+ '<th>Passasjer</th><th>Lagt til</th>'
+ '<th>' + (I18N.th_title || 'Title') + '</th>'
+ '<th>' + (I18N.th_category || 'Category') + '</th>'
+ '<th>' + (I18N.th_status || 'Status') + '</th>'
+ '<th>' + (I18N.th_chunks || 'Passages') + '</th>'
+ '<th>' + (I18N.th_added || 'Added') + '</th>'
+ '</tr></thead>';
const tbody = document.createElement('tbody');
@@ -120,7 +139,6 @@ require_once __DIR__ . '/../includes/layout_dashboard.php';
$wrap.innerHTML = '';
$wrap.appendChild(table);
// Wire selection
const all = document.getElementById('docSelectAll');
all.addEventListener('change', () => {
tbody.querySelectorAll('.doc-check').forEach(c => {
@@ -140,7 +158,8 @@ require_once __DIR__ . '/../includes/layout_dashboard.php';
const from = state.offset + 1;
const to = Math.min(state.offset + docs.length, state.total);
$pl.textContent = 'Viser ' + from + '' + to + ' av ' + state.total;
const tmpl = I18N.pager_showing || 'Showing {from}{to} of {total}';
$pl.textContent = tmpl.replace('{from}', from).replace('{to}', to).replace('{total}', state.total);
$prev.disabled = state.offset === 0;
$next.disabled = state.offset + PAGE >= state.total;
$pager.hidden = false;
@@ -148,7 +167,9 @@ require_once __DIR__ . '/../includes/layout_dashboard.php';
function updateBulk() {
$bulk.disabled = state.selected.size === 0;
$bulk.textContent = state.selected.size > 0 ? 'Slett valgte (' + state.selected.size + ')' : 'Slett valgte';
$bulk.textContent = state.selected.size > 0
? (I18N.delete_n_selected || 'Delete selected ({n})').replace('{n}', state.selected.size)
: (I18N.delete_selected || 'Delete selected');
}
$prev.addEventListener('click', () => { state.offset = Math.max(0, state.offset - PAGE); load(); });
@@ -163,7 +184,8 @@ require_once __DIR__ . '/../includes/layout_dashboard.php';
$bulk.addEventListener('click', () => {
if (!state.selected.size) return;
if (!confirm('Slette ' + state.selected.size + ' dokumenter? Dette kan ikke angres.')) return;
const msg = (I18N.delete_docs_confirm || 'Delete {n} documents? This cannot be undone.').replace('{n}', state.selected.size);
if (!confirm(msg)) return;
const ids = Array.from(state.selected);
$bulk.disabled = true;
fetch(api + '/documents.php?action=delete', {
@@ -174,12 +196,12 @@ require_once __DIR__ . '/../includes/layout_dashboard.php';
})
.then(r => r.json())
.then(data => {
if (!data.ok) throw new Error(data.error?.message || 'Slett feilet');
if (!data.ok) throw new Error(data.error?.message || 'Delete failed');
state.selected.clear();
updateBulk();
load();
})
.catch(err => alert('Feil: ' + err.message));
.catch(err => alert(err.message));
});
load();
+58 -32
View File
@@ -1,76 +1,96 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/../includes/bootstrap.php';
$dashboardPage = 'index';
$dashboardTitle = 'Min korpus';
$dashboardLead = 'Privat juridisk kunnskapsbase. Last opp, organiser, spør — alt holdes til din konto.';
$dashboardTitle = dbnToolsT('dash_title_overview', dbnToolsCurrentLanguage());
$dashboardLead = dbnToolsT('dash_lead_overview', dbnToolsCurrentLanguage());
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__label" id="kpiLabelDocs"></p>
<p class="dash-kpi__value" data-kpi="documents">—</p>
<p class="dash-kpi__hint">av kvote</p>
<p class="dash-kpi__hint" id="kpiHintQuota"></p>
</div>
<div class="dash-kpi">
<p class="dash-kpi__label">Passasjer indeksert</p>
<p class="dash-kpi__label" id="kpiLabelChunks"></p>
<p class="dash-kpi__value" data-kpi="chunks">—</p>
<p class="dash-kpi__hint">søkbare biter</p>
<p class="dash-kpi__hint" id="kpiHintSearchable"></p>
</div>
<div class="dash-kpi">
<p class="dash-kpi__label">Klare</p>
<p class="dash-kpi__label" id="kpiLabelReady"></p>
<p class="dash-kpi__value" data-kpi="ready">—</p>
<p class="dash-kpi__hint">av totalt</p>
<p class="dash-kpi__hint" id="kpiHintTotal"></p>
</div>
<div class="dash-kpi">
<p class="dash-kpi__label">Siste opplasting</p>
<p class="dash-kpi__label" id="kpiLabelLast"></p>
<p class="dash-kpi__value" data-kpi="last_upload" style="font-size: 1.05rem;">—</p>
<p class="dash-kpi__hint">dato</p>
<p class="dash-kpi__hint" id="kpiHintDate"></p>
</div>
</section>
<section class="dash-card">
<div class="dash-card__head">
<h2>Kom i gang</h2>
<h2 id="getStartedTitle"></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>
<a class="dash-btn dash-btn--primary" href="/dashboard/upload.php" id="btnUpload"></a>
<a class="dash-btn" href="/dashboard/chat.php" id="btnAsk"></a>
<a class="dash-btn" href="/dashboard/documents.php" id="btnBrowse"></a>
</div>
</section>
<section class="dash-card">
<div class="dash-card__head">
<h2>Nylig aktivitet</h2>
<h2 id="recentTitle"></h2>
<div class="dash-card__actions">
<a href="/dashboard/documents.php" class="dash-btn">Se alle →</a>
<a href="/dashboard/documents.php" class="dash-btn" id="btnSeeAll"></a>
</div>
</div>
<div id="dashRecent" class="dash-loading">Laster…</div>
<div id="dashRecent" class="dash-loading"></div>
</section>
<script>
(function () {
'use strict';
const api = window.DBN_DASHBOARD.apiBase;
const I18N = window.DBN_I18N || {};
const api = window.DBN_DASHBOARD.apiBase;
const loc = I18N.locale || 'en-GB';
document.getElementById('kpiLabelDocs').textContent = I18N.kpi_docs || 'Documents';
document.getElementById('kpiHintQuota').textContent = I18N.kpi_of_quota || 'of quota';
document.getElementById('kpiLabelChunks').textContent = I18N.kpi_chunks || 'Passages indexed';
document.getElementById('kpiHintSearchable').textContent = I18N.kpi_searchable || 'searchable pieces';
document.getElementById('kpiLabelReady').textContent = I18N.kpi_ready || 'Ready';
document.getElementById('kpiHintTotal').textContent = I18N.kpi_of_total || 'of total';
document.getElementById('kpiLabelLast').textContent = I18N.kpi_last || 'Last upload';
document.getElementById('kpiHintDate').textContent = I18N.kpi_date_label || 'date';
document.getElementById('getStartedTitle').textContent = I18N.get_started || 'Get started';
document.getElementById('recentTitle').textContent = I18N.recent_activity || 'Recent activity';
document.getElementById('btnUpload').textContent = I18N.upload_docs_btn || '📥 Upload documents';
document.getElementById('btnAsk').textContent = I18N.ask_btn || '💬 Ask a legal question';
document.getElementById('btnBrowse').textContent = I18N.browse_btn || '📚 Browse corpus';
document.getElementById('btnSeeAll').textContent = I18N.see_all || 'See all →';
document.getElementById('dashRecent').textContent = I18N.loading || 'Loading…';
function fmtDate(s) {
if (!s) return '—';
try {
return new Date(s.replace(' ', 'T') + 'Z').toLocaleDateString('nb-NO',
return new Date(s.replace(' ', 'T') + 'Z').toLocaleDateString(loc,
{ day: 'numeric', month: 'short', year: 'numeric' });
} catch (_) { return s; }
}
function fmtNum(n) { return n == null ? '—' : Number(n).toLocaleString('nb-NO'); }
function fmtNum(n) { return n == null ? '—' : Number(n).toLocaleString(loc); }
function statusPill(status) {
const cls = {
ready: 'dash-status--ready',
pending: 'dash-status--pending',
processing: 'dash-status--processing',
error: 'dash-status--error',
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;
const label = {
ready: I18N.status_ready || 'Ready', pending: I18N.status_pending || 'Pending',
processing: I18N.status_processing || 'Processing', error: I18N.status_error || 'Error',
}[status] || status;
return '<span class="dash-status ' + cls + '">' + label + '</span>';
}
@@ -84,23 +104,29 @@ require_once __DIR__ . '/../includes/layout_dashboard.php';
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="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>.';
recent.innerHTML = '<span class="dash-empty__icon">📭</span>'
+ (I18N.empty_docs || 'No documents yet.') + ' '
+ '<a href="/dashboard/upload.php">' + (I18N.empty_docs_link || 'Upload your first') + '</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>';
table.innerHTML = '<thead><tr>'
+ '<th>' + (I18N.th_title || 'Title') + '</th>'
+ '<th>' + (I18N.th_status || 'Status') + '</th>'
+ '<th>' + (I18N.th_chunks || 'Passages') + '</th>'
+ '<th>' + (I18N.th_added || 'Added') + '</th>'
+ '</tr></thead>';
const tbody = document.createElement('tbody');
docs.slice(0, 8).forEach(doc => {
const tr = document.createElement('tr');
@@ -120,7 +146,7 @@ require_once __DIR__ . '/../includes/layout_dashboard.php';
.catch(err => {
const recent = document.getElementById('dashRecent');
recent.className = 'dash-error';
recent.textContent = 'Kunne ikke laste: ' + err.message;
recent.textContent = (I18N.error_loading || 'Could not load: ') + err.message;
});
})();
</script>
+20 -20
View File
@@ -1,54 +1,54 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/../includes/bootstrap.php';
$dashboardPage = 'settings';
$dashboardTitle = 'Innstillinger';
$dashboardLead = 'Innstillinger for ditt private korpus.';
$dashboardTitle = dbnToolsT('dash_title_settings', dbnToolsCurrentLanguage());
$dashboardLead = dbnToolsT('dash_lead_settings', dbnToolsCurrentLanguage());
require_once __DIR__ . '/../includes/layout_dashboard.php';
?>
<section class="dash-card">
<div class="dash-card__head">
<h2>Konto</h2>
<h2><?= htmlspecialchars(dbnToolsT('dash_section_account', $uiLang)) ?></h2>
</div>
<dl style="display:grid; grid-template-columns: max-content 1fr; gap: 0.4rem 1.5rem; font-size:0.92rem;">
<dt style="color:rgba(22,19,15,0.55);">Klient-ID</dt>
<dt style="color:rgba(22,19,15,0.55);"><?= htmlspecialchars(dbnToolsT('dash_label_client_id', $uiLang)) ?></dt>
<dd><code id="setClientId">—</code></dd>
<dt style="color:rgba(22,19,15,0.55);">Korpus-ID</dt>
<dt style="color:rgba(22,19,15,0.55);"><?= htmlspecialchars(dbnToolsT('dash_label_corpus_id', $uiLang)) ?></dt>
<dd><code id="setCorpusId">—</code></dd>
<dt style="color:rgba(22,19,15,0.55);">Bruker-ID</dt>
<dt style="color:rgba(22,19,15,0.55);"><?= htmlspecialchars(dbnToolsT('dash_label_user_id', $uiLang)) ?></dt>
<dd><code id="setUserId">—</code></dd>
</dl>
</section>
<section class="dash-card">
<div class="dash-card__head">
<h2>RAG-pipeline</h2>
<h2><?= htmlspecialchars(dbnToolsT('dash_section_rag', $uiLang)) ?></h2>
</div>
<dl style="display:grid; grid-template-columns: max-content 1fr; gap: 0.4rem 1.5rem; font-size:0.92rem;">
<dt style="color:rgba(22,19,15,0.55);">Chunking</dt>
<dd>600 ord pr. passasje, 75 ords overlapp, heading-aware</dd>
<dt style="color:rgba(22,19,15,0.55);">Embedding-modell</dt>
<dd><code>nomic-embed-text</code> (768-dim) via LiteLLM Colin</dd>
<dd>600 words/passage, 75-word overlap, heading-aware</dd>
<dt style="color:rgba(22,19,15,0.55);">Embedding model</dt>
<dd><code>nomic-embed-text</code> (768-dim) via LiteLLM on Colin</dd>
<dt style="color:rgba(22,19,15,0.55);">Vector DB</dt>
<dd><code>bnl_client_chunks</code> i Qdrant (Colin Docker)</dd>
<dt style="color:rgba(22,19,15,0.55);">Søkemetode</dt>
<dd><code>bnl_client_chunks</code> in Qdrant (Colin Docker)</dd>
<dt style="color:rgba(22,19,15,0.55);">Search method</dt>
<dd>Hybrid (vector + keyword), reciprocal rank fusion, private boost 1.5×</dd>
<dt style="color:rgba(22,19,15,0.55);">Graf-database</dt>
<dd><code>bnl_legal</code> i FalkorDB (Colin) — siterings-edges</dd>
<dt style="color:rgba(22,19,15,0.55);">Graph DB</dt>
<dd><code>bnl_legal</code> in FalkorDB (Colin) — citation edges</dd>
</dl>
</section>
<section class="dash-card">
<div class="dash-card__head">
<h2>Personvern</h2>
<h2><?= htmlspecialchars(dbnToolsT('dash_section_privacy', $uiLang)) ?></h2>
</div>
<p style="margin-top:0; max-width:64ch; line-height:1.6;">
Alt du laster opp eller lagrer her holdes til din konto. Andre brukere kan ikke se eller søke i dine dokumenter.
Felles-pakken <code>family-legal</code> (~220K passasjer av norsk lovverk og rettspraksis) er delt og brukes
for å berike svar med autoritative kilder, men du eier alt du selv legger inn.
<?= htmlspecialchars(dbnToolsT('dash_privacy_p1', $uiLang)) ?>
</p>
<p style="max-width:64ch; line-height:1.6;">
Slett enkelt-dokumenter fra <a href="/dashboard/documents.php">Dokumenter</a>. Trenger du å slette hele
kontoen, kontakt support.
<?= htmlspecialchars(dbnToolsT('dash_privacy_p2a', $uiLang)) ?>
<a href="/dashboard/documents.php"><?= htmlspecialchars(dbnToolsT('dash_nav_documents', $uiLang)) ?></a>.
<?= htmlspecialchars(dbnToolsT('dash_privacy_p2b', $uiLang)) ?>
</p>
</section>
+54 -51
View File
@@ -1,16 +1,17 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/../includes/bootstrap.php';
$dashboardPage = 'upload';
$dashboardTitle = 'Last opp dokumenter';
$dashboardLead = 'Last opp PDF, DOCX, eller TXT — eller lim inn tekst eller en URL. Innholdet chunkes, embedes og indekseres i din private korpus.';
$dashboardTitle = dbnToolsT('dash_title_upload', dbnToolsCurrentLanguage());
$dashboardLead = dbnToolsT('dash_lead_upload', dbnToolsCurrentLanguage());
require_once __DIR__ . '/../includes/layout_dashboard.php';
?>
<section class="dash-card">
<div class="dash-card__head">
<h2>Velg kilde</h2>
<h2><?= htmlspecialchars(dbnToolsT('dash_upload_source', $uiLang)) ?></h2>
<div class="dash-card__actions">
<button class="dash-btn upload-mode is-active" data-mode="file">Fil</button>
<button class="dash-btn upload-mode" data-mode="text">Lim inn tekst</button>
<button class="dash-btn upload-mode is-active" data-mode="file"><?= htmlspecialchars(dbnToolsT('dash_upload_file_btn', $uiLang)) ?></button>
<button class="dash-btn upload-mode" data-mode="text"><?= htmlspecialchars(dbnToolsT('dash_upload_text_btn', $uiLang)) ?></button>
<button class="dash-btn upload-mode" data-mode="url">URL</button>
</div>
</div>
@@ -19,39 +20,39 @@ require_once __DIR__ . '/../includes/layout_dashboard.php';
<label class="upload-drop" id="upDrop">
<input type="file" name="file" id="upFile" accept=".pdf,.docx,.txt" hidden>
<span class="upload-drop__icon">📥</span>
<strong>Slipp filen her, eller klikk for å bla</strong>
<small>PDF · DOCX · TXT · maks 8 MB · automatisk OCR for skannede PDF-er</small>
<strong id="upDropHint"><?= htmlspecialchars(dbnToolsT('dash_upload_drop_strong', $uiLang)) ?></strong>
<small><?= htmlspecialchars(dbnToolsT('dash_upload_drop_small', $uiLang)) ?></small>
</label>
<div class="upload-meta">
<label>Tittel<input name="title" placeholder="Auto fra filnavn om tom"></label>
<label>Kategori<input name="category" placeholder="f.eks. barnevern, familierett"></label>
<label>Tagger<input name="tags" placeholder="komma,separert,liste"></label>
<label>Språk<input name="language" value="no" maxlength="10"></label>
<label><?= htmlspecialchars(dbnToolsT('dash_upload_title_lbl', $uiLang)) ?><input name="title" placeholder="<?= htmlspecialchars(dbnToolsT('dash_upload_title_ph', $uiLang)) ?>"></label>
<label><?= htmlspecialchars(dbnToolsT('dash_upload_category_lbl', $uiLang)) ?><input name="category" placeholder="<?= htmlspecialchars(dbnToolsT('dash_upload_category_ph', $uiLang)) ?>"></label>
<label><?= htmlspecialchars(dbnToolsT('dash_upload_tags_lbl', $uiLang)) ?><input name="tags" placeholder="comma,separated,list"></label>
<label><?= htmlspecialchars(dbnToolsT('dash_upload_lang_lbl', $uiLang)) ?><input name="language" value="no" maxlength="10"></label>
</div>
<button type="submit" class="dash-btn dash-btn--primary" style="justify-self:start;">Last opp og indekser</button>
<button type="submit" class="dash-btn dash-btn--primary" style="justify-self:start;"><?= htmlspecialchars(dbnToolsT('dash_upload_btn_file', $uiLang)) ?></button>
</form>
<form id="upTextForm" class="upload-form" hidden style="display:grid; gap:0.85rem;">
<label>Tittel<input name="title" required placeholder="Gi notatet en tittel"></label>
<label>Innhold<textarea name="content" rows="12" required placeholder="Lim inn tekst her — minst 30 tegn"></textarea></label>
<label><?= htmlspecialchars(dbnToolsT('dash_upload_title_lbl', $uiLang)) ?><input name="title" required placeholder="<?= htmlspecialchars(dbnToolsT('dash_upload_note_title_ph', $uiLang)) ?>"></label>
<label><?= htmlspecialchars(dbnToolsT('dash_upload_content_lbl', $uiLang)) ?><textarea name="content" rows="12" required placeholder="<?= htmlspecialchars(dbnToolsT('dash_upload_content_ph', $uiLang)) ?>"></textarea></label>
<div class="upload-meta">
<label>Kategori<input name="category" placeholder="f.eks. notat"></label>
<label>Tagger<input name="tags"></label>
<label>Språk<input name="language" value="no" maxlength="10"></label>
<label><?= htmlspecialchars(dbnToolsT('dash_upload_tags_lbl', $uiLang)) ?><input name="category" placeholder="e.g. note"></label>
<label>Tags<input name="tags"></label>
<label><?= htmlspecialchars(dbnToolsT('dash_upload_lang_lbl', $uiLang)) ?><input name="language" value="no" maxlength="10"></label>
</div>
<button type="submit" class="dash-btn dash-btn--primary" style="justify-self:start;">Lagre i korpus</button>
<button type="submit" class="dash-btn dash-btn--primary" style="justify-self:start;"><?= htmlspecialchars(dbnToolsT('dash_upload_btn_text', $uiLang)) ?></button>
</form>
<form id="upUrlForm" class="upload-form" hidden style="display:grid; gap:0.85rem;">
<label>URL<input name="url" type="url" required placeholder="https://lovdata.no/dokument/…"></label>
<label>Tittel<input name="title" placeholder="Tom = bruker URL"></label>
<label>URL<input name="url" type="url" required placeholder="<?= htmlspecialchars(dbnToolsT('dash_upload_url_ph', $uiLang)) ?>"></label>
<label><?= htmlspecialchars(dbnToolsT('dash_upload_title_lbl', $uiLang)) ?><input name="title" placeholder="<?= htmlspecialchars(dbnToolsT('dash_upload_url_title_ph', $uiLang)) ?>"></label>
<div class="upload-meta">
<label>Kategori<input name="category"></label>
<label>Tagger<input name="tags"></label>
<label>Språk<input name="language" value="no" maxlength="10"></label>
<label>Category<input name="category"></label>
<label>Tags<input name="tags"></label>
<label><?= htmlspecialchars(dbnToolsT('dash_upload_lang_lbl', $uiLang)) ?><input name="language" value="no" maxlength="10"></label>
</div>
<button type="submit" class="dash-btn dash-btn--primary" style="justify-self:start;">Hent og indekser</button>
<small style="color:rgba(22,19,15,0.55);">URLer kjøres i bakgrunnen — sjekk «Dokumenter» for status.</small>
<button type="submit" class="dash-btn dash-btn--primary" style="justify-self:start;"><?= htmlspecialchars(dbnToolsT('dash_upload_btn_url', $uiLang)) ?></button>
<small style="color:rgba(22,19,15,0.55);"><?= htmlspecialchars(dbnToolsT('dash_upload_url_note', $uiLang)) ?></small>
</form>
<div id="upStatus" class="upload-status" hidden></div>
@@ -85,7 +86,9 @@ require_once __DIR__ . '/../includes/layout_dashboard.php';
<script>
(function () {
'use strict';
const api = window.DBN_DASHBOARD.apiBase;
const I18N = window.DBN_I18N || {};
const api = window.DBN_DASHBOARD.apiBase;
const dropHintDefault = I18N.upload_drop_hint || 'Drop file here, or click to browse';
const forms = {
file: document.getElementById('upFileForm'),
@@ -104,9 +107,9 @@ require_once __DIR__ . '/../includes/layout_dashboard.php';
});
});
// Drag-drop wiring for file
const drop = document.getElementById('upDrop');
const fileInput = document.getElementById('upFile');
const dropHintEl = document.getElementById('upDropHint');
if (drop && fileInput) {
drop.addEventListener('click', () => fileInput.click());
['dragenter', 'dragover'].forEach(ev => drop.addEventListener(ev, e => {
@@ -118,12 +121,12 @@ require_once __DIR__ . '/../includes/layout_dashboard.php';
drop.addEventListener('drop', e => {
if (e.dataTransfer.files.length) {
fileInput.files = e.dataTransfer.files;
drop.querySelector('strong').textContent = e.dataTransfer.files[0].name;
if (dropHintEl) dropHintEl.textContent = e.dataTransfer.files[0].name;
}
});
fileInput.addEventListener('change', () => {
if (fileInput.files.length) {
drop.querySelector('strong').textContent = fileInput.files[0].name;
if (fileInput.files.length && dropHintEl) {
dropHintEl.textContent = fileInput.files[0].name;
}
});
}
@@ -139,54 +142,54 @@ require_once __DIR__ . '/../includes/layout_dashboard.php';
forms.file.addEventListener('submit', (e) => {
e.preventDefault();
const fd = new FormData(forms.file);
if (!fileInput.files.length) { setStatus('Velg en fil først.', 'err'); return; }
setStatus('Laster opp og indekserer…');
if (!fileInput.files.length) { setStatus(I18N.upload_select_file || 'Select a file first.', 'err'); return; }
setStatus(I18N.upload_indexing || 'Uploading and indexing…');
fetch(api + '/upload.php', { method: 'POST', credentials: 'same-origin', body: fd })
.then(r => r.json()).then(handleResult).catch(err => setStatus('Feil: ' + safe(err.message), 'err'));
.then(r => r.json()).then(handleResult).catch(err => setStatus(' ' + safe(err.message), 'err'));
});
forms.text.addEventListener('submit', (e) => {
e.preventDefault();
const payload = { kind: 'text' };
new FormData(forms.text).forEach((v, k) => payload[k] = v);
setStatus('Indekserer…');
setStatus(I18N.upload_saving || 'Indexing…');
fetch(api + '/upload.php', {
method: 'POST', credentials: 'same-origin',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
}).then(r => r.json()).then(handleResult).catch(err => setStatus('Feil: ' + safe(err.message), 'err'));
}).then(r => r.json()).then(handleResult).catch(err => setStatus(' ' + safe(err.message), 'err'));
});
forms.url.addEventListener('submit', (e) => {
e.preventDefault();
const payload = { kind: 'url' };
new FormData(forms.url).forEach((v, k) => payload[k] = v);
setStatus('Køer URL for henting…');
setStatus(I18N.upload_queuing || 'Queuing URL for fetching…');
fetch(api + '/upload.php', {
method: 'POST', credentials: 'same-origin',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
}).then(r => r.json()).then(handleResult).catch(err => setStatus('Feil: ' + safe(err.message), 'err'));
}).then(r => r.json()).then(handleResult).catch(err => setStatus(' ' + safe(err.message), 'err'));
});
function handleResult(data) {
if (data.status === 'ready') {
setStatus('✅ Indeksert! '
+ (data.chunks || 0) + ' passasjer lagt til. '
+ '<a href="/dashboard/document.php?id=' + data.document_id + '">Åpne dokument</a> · '
+ '<a href="/dashboard/documents.php">Se alle</a>', 'ok');
const msg = (I18N.upload_indexed || '✅ Indexed! {n} passages added.').replace('{n}', data.chunks || 0);
setStatus(msg
+ ' <a href="/dashboard/document.php?id=' + data.document_id + '">' + (I18N.upload_open_doc || 'Open document') + '</a>'
+ ' · <a href="/dashboard/documents.php">' + (I18N.see_all || 'See all') + '</a>', 'ok');
['file', 'text', 'url'].forEach(m => forms[m].reset());
if (drop) drop.querySelector('strong').textContent = 'Slipp filen her, eller klikk for å bla';
if (dropHintEl) dropHintEl.textContent = dropHintDefault;
} else if (data.status === 'pending') {
setStatus('📥 Lagt i kø. '
+ '<a href="/dashboard/documents.php">Følg fremdriften i Dokumenter</a>.', 'ok');
setStatus((I18N.upload_queued || '📥 Queued.')
+ ' <a href="/dashboard/documents.php">' + (I18N.upload_follow_docs || 'Follow progress in Documents') + '</a>.', 'ok');
pollUntilDone(data.document_id);
} else if (data.status === 'error') {
setStatus('❌ ' + safe(data.error?.message || 'Indeksering feilet.'), 'err');
setStatus('❌ ' + safe(data.error?.message || 'Indexing failed.'), 'err');
} else if (!data.ok) {
setStatus('❌ ' + safe(data.error?.message || 'Opplasting feilet.'), 'err');
setStatus('❌ ' + safe(data.error?.message || 'Upload failed.'), 'err');
} else {
setStatus('Uventet svar.', 'err');
setStatus('Unexpected response.', 'err');
}
}
@@ -200,13 +203,13 @@ require_once __DIR__ . '/../includes/layout_dashboard.php';
const s = (data.statuses || []).find(x => x.id === docId);
if (!s) return;
if (s.status === 'ready') {
setStatus('✅ Bakgrunnsjobb ferdig. '
+ s.chunk_count + ' passasjer indeksert. '
+ '<a href="/dashboard/document.php?id=' + docId + '">Åpne dokument</a>', 'ok');
const msg = (I18N.upload_bg_done || '✅ Background job done. {n} passages indexed.').replace('{n}', s.chunk_count);
setStatus(msg
+ ' <a href="/dashboard/document.php?id=' + docId + '">' + (I18N.upload_open_doc || 'Open document') + '</a>', 'ok');
return;
}
if (s.status === 'error') {
setStatus('❌ ' + safe(s.error_message || 'Bakgrunnsjobb feilet.'), 'err');
setStatus('❌ ' + safe(s.error_message || 'Background job failed.'), 'err');
return;
}
setTimeout(tick, 3000);