Files
daveadmin b014638f39 feat(corpus): add save-to-corpus + private corpus search scope
- POST /api/save-to-corpus.php — saves tool output text to user's default CaveauAI corpus via ClientRagPipeline
- api/case/upload.php — dual-writes uploaded PDFs to CaveauAI client_documents (best-effort)
- assets/js/corpus-save.js — shared <dialog> handler for .js-save-corpus buttons on all tool pages
- includes/layout_footer.php — injects corpus-save.js + shared save dialog markup
- korrespond/deep-research/barnevernet/discrepancy JS — save-to-corpus buttons on output sections
- api/search.php + LegalTools::search() — corpus_scope param ('shared'|'private'|'both'), merges personal CaveauAI corpus with shared legal library when 'both'
- includes/tool_form.php + assets/js/tools.js — corpus scope radio toggle shown on search tab
- api/user-docs.php — add POST upload method for non-SSO authenticated users

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 17:50:32 +02:00

91 lines
3.3 KiB
PHP

<?php
declare(strict_types=1);
require_once __DIR__ . '/../../includes/bootstrap.php';
require_once __DIR__ . '/../../includes/FreeTier.php';
require_once __DIR__ . '/../../includes/CaseStore.php';
dbnToolsRequireMethod('POST');
dbnToolsRequireAuth();
$userId = (int)($_SESSION['dbn_tools_sso_uid'] ?? 0);
if ($userId <= 0) {
dbnToolsError('Auth required.', 401, 'auth_required');
}
if (empty($_FILES['file']) || ($_FILES['file']['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) {
dbnToolsError('No file uploaded or upload error.', 400, 'no_file');
}
$file = $_FILES['file'];
$size = (int)$file['size'];
$tmp = (string)$file['tmp_name'];
$name = (string)$file['name'];
if ($size <= 0 || $size > 25 * 1024 * 1024) {
dbnToolsError('Filen må være mellom 1 byte og 25 MB.', 413, 'bad_size');
}
// Validate it's actually a PDF (magic number check)
$fh = @fopen($tmp, 'rb');
if ($fh === false) {
dbnToolsError('Kunne ikke lese filen.', 500, 'read_fail');
}
$head = (string)fread($fh, 5);
fclose($fh);
if (strncmp($head, '%PDF-', 5) !== 0) {
dbnToolsError('Filen er ikke en gyldig PDF.', 415, 'not_pdf');
}
try {
$doc = CaseStore::registerUpload($userId, $name, $tmp, $size);
CaseStore::caseEnqueueIngest((int)$doc['doc_id'], $userId);
} catch (Throwable $e) {
dbnToolsError($e->getMessage(), 400, 'upload_failed');
}
// Dual-write to CaveauAI corpus (best-effort — never fails the upload)
$caveauDocId = null;
$clientId = (int)($_SESSION['dbn_tools_client_id'] ?? 0);
if ($clientId > 0 && !empty($doc['storage_path'])) {
try {
dbnToolsBootCaveau();
$aiPortalRoot = dbnToolsAiPortalRoot();
$textExtractFile = $aiPortalRoot . '/platform/includes/text_extract.php';
if (is_file($textExtractFile)) {
require_once $textExtractFile;
$content = extractPdfText($doc['storage_path']);
if ($content !== '' && strlen($content) > 30) {
$caveauDb = getDb();
$corpusSt = $caveauDb->prepare(
'SELECT id FROM client_corpora WHERE client_id = ? AND is_default = 1 LIMIT 1'
);
$corpusSt->execute([$clientId]);
$corpusId = (int)($corpusSt->fetchColumn() ?: 0);
if ($corpusId > 0) {
$title = pathinfo($doc['filename'], PATHINFO_FILENAME);
$caveauDb->prepare("
INSERT INTO client_documents
(client_id, corpus_id, title, source_type, content, category,
import_method, word_count, status)
VALUES (?, ?, ?, 'pdf', ?, 'user-upload', 'dbn_upload', ?, 'pending')
")->execute([$clientId, $corpusId, $title, $content, str_word_count($content)]);
$caveauDocId = (int)$caveauDb->lastInsertId();
$rag = new ClientRagPipeline($clientId);
$rag->ingestDocument($caveauDocId);
}
}
}
} catch (Throwable $e) {
// Non-fatal: log and continue
error_log('[upload] CaveauAI dual-write failed for doc ' . ($doc['doc_id'] ?? '?') . ': ' . $e->getMessage());
}
}
dbnToolsRespond([
'ok' => true,
'doc_id' => $doc['doc_id'],
'filename' => $doc['filename'],
'caveau_doc_id' => $caveauDocId,
]);