From bc44b0eee2dd9c99e949592fa2cfd7aad8698d71 Mon Sep 17 00:00:00 2001 From: davegilligan Date: Tue, 19 May 2026 09:37:19 +0200 Subject: [PATCH] Add My Documents panel to workbench + user-docs API - api/user-docs.php: GET/DELETE shared dbn_user_docs table (SSO users only) connects to dobetternorge DB via DBN_DB_* env vars - workbench.php: My Documents panel (section 05) for SSO/free-tier users; shows docs uploaded from either AI chat or tools, links to AI Chat for upload - workbench.js: fetch + render doc list, delete with Qdrant cleanup - tools.css: workbench-docs panel + item styles - i18n.php: my_docs_* strings in all 4 languages Co-Authored-By: Claude Sonnet 4.6 --- api/user-docs.php | 123 +++++++++++++++++++++++++++++++++++++++++ assets/css/tools.css | 75 ++++++++++++++++++++++++- assets/js/workbench.js | 77 ++++++++++++++++++++++++++ includes/i18n.php | 32 +++++++++++ workbench.php | 18 ++++++ 5 files changed, 324 insertions(+), 1 deletion(-) create mode 100644 api/user-docs.php diff --git a/api/user-docs.php b/api/user-docs.php new file mode 100644 index 0000000..6f1e2c4 --- /dev/null +++ b/api/user-docs.php @@ -0,0 +1,123 @@ + false, 'error' => 'Not authenticated.']); + exit; +} + +// Only SSO users have shared docs +$ssoUid = (string)($_SESSION['dbn_tools_sso_uid'] ?? ''); +if ($ssoUid === '') { + header('Content-Type: application/json'); + echo json_encode(['ok' => true, 'docs' => [], 'reason' => 'sso_only']); + exit; +} + +header('Content-Type: application/json; charset=utf-8'); + +// ── Connect to the shared dobetternorge DB (dbn_user_docs lives here) ───────── +function dbnSharedDb(): ?PDO +{ + static $pdo = null; + if ($pdo !== null) return $pdo; + $host = dbnToolsEnv('DBN_DB_HOST', dbnToolsEnv('DBNM_DB_HOST', 'localhost')); + $name = dbnToolsEnv('DBN_DB_NAME', dbnToolsEnv('DBNM_DB_NAME', 'dobetternorge')); + $user = dbnToolsEnv('DBN_DB_USER', dbnToolsEnv('DBNM_DB_USER', 'root')); + $pass = dbnToolsEnv('DBN_DB_PASS', dbnToolsEnv('DBNM_DB_PASS', '')); + try { + $pdo = new PDO("mysql:host={$host};dbname={$name};charset=utf8mb4", $user, $pass, [ + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + PDO::ATTR_TIMEOUT => 5, + ]); + return $pdo; + } catch (Throwable) { return null; } +} + +$method = $_SERVER['REQUEST_METHOD']; + +// ── DELETE ──────────────────────────────────────────────────────────────────── +if ($method === 'DELETE') { + $docId = trim($_GET['id'] ?? ''); + if ($docId === '') { + http_response_code(400); + echo json_encode(['ok' => false, 'error' => 'Missing id parameter.']); + exit; + } + + $db = dbnSharedDb(); + if ($db) { + $stmt = $db->prepare('SELECT id FROM dbn_user_docs WHERE id = ? AND user_id = ?'); + $stmt->execute([$docId, $ssoUid]); + if ($stmt->fetch()) { + $db->prepare('DELETE FROM dbn_user_docs WHERE id = ? AND user_id = ?') + ->execute([$docId, $ssoUid]); + + // Delete Qdrant points for this doc + $qdrantUrl = 'http://10.0.2.10:6333'; + $body = [ + 'filter' => [ + 'must' => [ + ['key' => 'doc_id', 'match' => ['value' => $docId]], + ['key' => 'user_id', 'match' => ['value' => $ssoUid]], + ], + ], + ]; + $ch = curl_init("$qdrantUrl/collections/dbn_user_docs/points/delete"); + curl_setopt_array($ch, [ + CURLOPT_POST => true, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => 10, + CURLOPT_HTTPHEADER => ['Content-Type: application/json'], + CURLOPT_POSTFIELDS => json_encode($body), + ]); + curl_exec($ch); + curl_close($ch); + } + } + echo json_encode(['ok' => true]); + exit; +} + +// ── GET ─────────────────────────────────────────────────────────────────────── +$db = dbnSharedDb(); +if (!$db) { + echo json_encode(['ok' => true, 'docs' => []]); + exit; +} + +$stmt = $db->prepare( + 'SELECT id, filename, file_type, chunk_count, source, created_at + FROM dbn_user_docs + WHERE user_id = ? AND status = ? + ORDER BY created_at DESC + LIMIT 50' +); +$stmt->execute([$ssoUid, 'ready']); +$rows = $stmt->fetchAll(); + +$docs = array_map(static fn($r) => [ + 'doc_id' => $r['id'], + 'filename' => $r['filename'], + 'file_type' => $r['file_type'], + 'chunk_count' => (int)$r['chunk_count'], + 'source' => $r['source'], + 'created_at' => $r['created_at'], +], $rows); + +echo json_encode(['ok' => true, 'docs' => $docs]); diff --git a/assets/css/tools.css b/assets/css/tools.css index 07f9439..e836014 100644 --- a/assets/css/tools.css +++ b/assets/css/tools.css @@ -6196,10 +6196,83 @@ body.lt-landing { } .workbench-panel--flow, -.workbench-panel--outputs { +.workbench-panel--outputs, +.workbench-panel--docs { grid-column: 1 / -1; } +.workbench-docs__desc { + font-size: 0.875rem; + color: var(--dbn-text-2, #555); + margin-bottom: 16px; +} + +.workbench-docs__list { + display: flex; + flex-direction: column; + gap: 8px; + margin-bottom: 16px; + min-height: 40px; +} + +.workbench-docs__item { + display: flex; + align-items: center; + gap: 10px; + padding: 8px 12px; + border-radius: 8px; + border: 1px solid var(--dbn-line, #e0ddd8); + background: rgba(255,255,255,0.9); + font-size: 0.875rem; +} + +.workbench-docs__icon { flex-shrink: 0; font-size: 1rem; } + +.workbench-docs__name { + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-weight: 500; +} + +.workbench-docs__meta { + font-size: 0.75rem; + color: var(--dbn-text-2, #888); + white-space: nowrap; + flex-shrink: 0; +} + +.workbench-docs__remove { + flex-shrink: 0; + background: none; + border: 1px solid var(--dbn-line, #e0ddd8); + border-radius: 6px; + padding: 3px 10px; + font-size: 0.75rem; + cursor: pointer; + color: var(--dbn-text-2, #777); + transition: background 0.15s, color 0.15s; +} + +.workbench-docs__remove:hover { + background: #fee2e2; + color: #b91c1c; + border-color: #fca5a5; +} + +.workbench-docs__empty, +.workbench-docs__loading { + font-size: 0.875rem; + color: var(--dbn-text-2, #888); + font-style: italic; +} + +.workbench-docs__cta { + display: inline-block; + font-size: 0.875rem; +} + .workbench-section-head { display: flex; align-items: baseline; diff --git a/assets/js/workbench.js b/assets/js/workbench.js index 7143559..f297bf2 100644 --- a/assets/js/workbench.js +++ b/assets/js/workbench.js @@ -94,3 +94,80 @@ }); } }()); + +// ── My Documents panel ──────────────────────────────────────────────────────── +(function () { + 'use strict'; + + var panel = document.querySelector('[data-my-docs="true"]'); + var list = document.getElementById('myDocsList'); + if (!panel || !list) return; + + var lang = (window.DBN_TOOLS_LANG || 'en'); + + var i18n = { + empty: { en: 'No documents uploaded yet. Upload PDFs, DOCX, or TXT files in the AI Chat sidebar.', no: 'Ingen dokumenter lastet opp ennå. Last opp PDF, DOCX eller TXT i AI-chattens sidepanel.', uk: 'Документів ще немає. Завантажте файли у бічній панелі AI-чату.', pl: 'Brak dokumentów. Prześlij pliki w panelu bocznym czatu AI.' }, + remove: { en: 'Remove', no: 'Fjern', uk: 'Видалити', pl: 'Usuń' }, + ai: { en: 'AI Chat', no: 'AI-chat', uk: 'AI-чат', pl: 'Czat AI' }, + tools: { en: 'Tools', no: 'Verktøy', uk: 'Інструменти', pl: 'Narzędzia' }, + error: { en: 'Could not load documents.', no: 'Kunne ikke laste dokumenter.', uk: 'Не вдалося завантажити документи.', pl: 'Nie można załadować dokumentów.' }, + }; + + function t(key) { + return (i18n[key] && i18n[key][lang]) || (i18n[key] && i18n[key]['en']) || key; + } + + function formatDate(str) { + if (!str) return ''; + try { return new Date(str).toLocaleDateString(); } catch (e) { return str; } + } + + function renderDocs(docs) { + if (!docs.length) { + list.innerHTML = '

' + t('empty') + '

'; + return; + } + list.innerHTML = docs.map(function (doc) { + return '
' + + '📄' + + '' + esc(doc.filename) + '' + + '' + esc(formatDate(doc.created_at)) + ' · ' + (doc.source === 'ai_chat' ? t('ai') : t('tools')) + '' + + '' + + '
'; + }).join(''); + + list.querySelectorAll('.workbench-docs__remove').forEach(function (btn) { + btn.addEventListener('click', function () { + var docId = btn.getAttribute('data-doc-id'); + if (!docId) return; + btn.disabled = true; + fetch('/api/user-docs.php?id=' + encodeURIComponent(docId), { method: 'DELETE', credentials: 'include' }) + .then(function () { + var item = list.querySelector('[data-doc-id="' + docId + '"]'); + if (item) item.remove(); + if (!list.querySelector('.workbench-docs__item')) { + list.innerHTML = '

' + t('empty') + '

'; + } + }) + .catch(function () { btn.disabled = false; }); + }); + }); + } + + function esc(str) { + return String(str).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); + } + + fetch('/api/user-docs.php', { credentials: 'include' }) + .then(function (r) { return r.json(); }) + .then(function (data) { + if (data.ok) { + renderDocs(data.docs || []); + } else { + list.innerHTML = '

' + t('error') + '

'; + } + }) + .catch(function () { + list.innerHTML = '

' + t('error') + '

'; + }); +}()); diff --git a/includes/i18n.php b/includes/i18n.php index 9dda3b0..77d4f93 100644 --- a/includes/i18n.php +++ b/includes/i18n.php @@ -127,6 +127,14 @@ function dbnToolsTranslations(): array 'register_card_note' => 'Free for families navigating Norwegian child-welfare cases.', 'register_google' => 'Register with Google', 'register_email' => 'Register with email', + 'my_docs_title' => 'My Documents', + 'my_docs_desc' => 'Reference documents you uploaded in the AI Chat are available here. Use them across tools or remove them when no longer needed.', + 'my_docs_upload_cta' => 'Upload documents in AI Chat', + 'my_docs_empty' => 'No documents uploaded yet. Upload PDFs, DOCX, or TXT files in the AI Chat sidebar.', + 'my_docs_remove' => 'Remove', + 'my_docs_source_ai' => 'AI Chat', + 'my_docs_source_tools' => 'Tools', + 'loading' => 'Loading…', ], 'no' => [ 'meta_title' => 'Do Better Norge - juridiske AI-verktøy', @@ -187,6 +195,14 @@ function dbnToolsTranslations(): array 'register_card_note' => 'Gratis for familier i norske barnevernssaker.', 'register_google' => 'Registrer med Google', 'register_email' => 'Registrer med e-post', + 'my_docs_title' => 'Mine dokumenter', + 'my_docs_desc' => 'Referansedokumenter du lastet opp i AI-chatten er tilgjengelige her. Bruk dem på tvers av verktøy eller slett dem når de ikke lenger trengs.', + 'my_docs_upload_cta' => 'Last opp dokumenter i AI-chat', + 'my_docs_empty' => 'Ingen dokumenter lastet opp ennå. Last opp PDF, DOCX eller TXT i AI-chattens sidepanel.', + 'my_docs_remove' => 'Fjern', + 'my_docs_source_ai' => 'AI-chat', + 'my_docs_source_tools' => 'Verktøy', + 'loading' => 'Laster…', ], 'uk' => [ 'meta_title' => 'Do Better Norge - юридичні AI інструменти', @@ -247,6 +263,14 @@ function dbnToolsTranslations(): array 'register_card_note' => 'Безкоштовно для сімей у норвезьких справах із захисту дітей.', 'register_google' => 'Зареєструватися через Google', 'register_email' => 'Зареєструватися через email', + 'my_docs_title' => 'Мої документи', + 'my_docs_desc' => 'Довідкові документи, завантажені в AI-чаті, доступні тут.', + 'my_docs_upload_cta' => 'Завантажити документи в AI-чаті', + 'my_docs_empty' => 'Документів ще немає. Завантажте PDF, DOCX або TXT у бічній панелі AI-чату.', + 'my_docs_remove' => 'Видалити', + 'my_docs_source_ai' => 'AI-чат', + 'my_docs_source_tools' => 'Інструменти', + 'loading' => 'Завантаження…', ], 'pl' => [ 'meta_title' => 'Do Better Norge - prawne narzędzia AI', @@ -307,6 +331,14 @@ function dbnToolsTranslations(): array 'register_card_note' => 'Bezpłatnie dla rodzin w norweskich sprawach dotyczących ochrony dzieci.', 'register_google' => 'Zarejestruj przez Google', 'register_email' => 'Zarejestruj przez email', + 'my_docs_title' => 'Moje dokumenty', + 'my_docs_desc' => 'Dokumenty referencyjne przesłane w czacie AI są dostępne tutaj.', + 'my_docs_upload_cta' => 'Prześlij dokumenty w czacie AI', + 'my_docs_empty' => 'Brak dokumentów. Prześlij pliki PDF, DOCX lub TXT na pasku bocznym czatu AI.', + 'my_docs_remove' => 'Usuń', + 'my_docs_source_ai' => 'Czat AI', + 'my_docs_source_tools' => 'Narzędzia', + 'loading' => 'Ładowanie…', ], ]; } diff --git a/workbench.php b/workbench.php index 1e2ac05..1cab9bc 100644 --- a/workbench.php +++ b/workbench.php @@ -198,6 +198,24 @@ foreach ($evidenceFields as [$name, $label, $hint]): + + +
+
+

05

+

+
+

+
+

+
+ + ↗ + +
+ +