getMessage(), $e->status, $e->errorCode); } $clientId = (int)$tenant['client_id']; $corpusId = (int)$tenant['corpus_id']; $input = dbnToolsJsonInput(2_000_000); $title = trim((string)($input['title'] ?? '')); if ($title === '') dbnToolsError('title is required.', 400, 'missing_title'); if (mb_strlen($title, 'UTF-8') > 500) dbnToolsError('title too long (max 500).', 422, 'title_too_long'); $content = trim((string)($input['content'] ?? '')); if (mb_strlen($content, 'UTF-8') < 30) dbnToolsError('content too short (min 30 chars).', 400, 'content_too_short'); if (mb_strlen($content, 'UTF-8') > 1_900_000) dbnToolsError('content exceeds 2 MB.', 422, 'content_too_large'); $sourceTool = trim((string)($input['source_tool'] ?? 'dashboard-save')); $sourceTool = substr(preg_replace('/[^a-z0-9\-_]/', '', strtolower($sourceTool)) ?: 'dashboard-save', 0, 64); $rawTags = $input['tags'] ?? ''; $tagList = is_array($rawTags) ? array_map('strval', $rawTags) : array_map('trim', explode(',', (string)$rawTags)); $tagList = array_values(array_filter(array_map(fn($t) => substr(trim($t), 0, 32), $tagList))); $tagList = array_slice($tagList, 0, 20); $tagsCsv = implode(',', $tagList); $category = strtolower(trim((string)($input['category'] ?? 'tool-output'))); $category = substr(preg_replace('/[^a-z0-9\-_]/', '', $category) ?: 'tool-output', 0, 50); $language = trim((string)($input['language'] ?? 'no')) ?: 'no'; $author = trim((string)($input['author'] ?? '')) ?: null; $kind = (string)($input['kind'] ?? 'tool_output'); $importMethod = match ($kind) { 'chat_answer' => 'chat_answer', 'manual' => 'manual', default => 'tool_output', }; $preview = !empty($input['preview']); $wordCount = str_word_count($content); dbnToolsBootCaveau(); try { if ($preview) { require_once dbnToolsAiPortalRoot() . '/lib/ai/TextChunker.php'; $chunker = new TextChunker(); $chunks = $chunker->chunk($content); $sample = array_slice($chunks, 0, 8); dbnToolsRespond([ 'ok' => true, 'preview' => true, 'word_count' => $wordCount, 'chunks' => array_map(fn($c) => [ 'section_title' => (string)($c['section_title'] ?? ''), 'word_count' => (int)str_word_count((string)($c['content'] ?? '')), 'snippet' => mb_substr((string)($c['content'] ?? ''), 0, 240, 'UTF-8'), ], $sample), 'total_chunks' => count($chunks), ]); } $db = getDb(); $ins = $db->prepare(" INSERT INTO client_documents (client_id, corpus_id, title, source_type, content, category, language, tags, author, import_method, source_tool, word_count, status) VALUES (?, ?, ?, 'text', ?, ?, ?, ?, ?, ?, ?, ?, 'pending') "); $ins->execute([ $clientId, $corpusId, $title, $content, $category, $language, $tagsCsv, $author, $importMethod, $sourceTool, $wordCount, ]); $docId = (int)$db->lastInsertId(); $rag = new ClientRagPipeline($clientId); $chunks = $rag->ingestDocument($docId); dbnToolsRespond([ 'ok' => true, 'document_id' => $docId, 'chunks' => (int)$chunks, 'status' => 'ready', ], 201); } catch (Throwable $e) { if (isset($docId)) { try { $db->prepare("UPDATE client_documents SET status='error', error_message=? WHERE id=?") ->execute([substr($e->getMessage(), 0, 1000), $docId]); } catch (Throwable $ignored) { /* non-fatal */ } dbnToolsError( 'Saved to corpus but indexing failed: ' . $e->getMessage(), 500, 'index_failed', ['document_id' => $docId] ); } dbnToolsError('Save failed: ' . $e->getMessage(), 500, 'save_failed'); }