feat: document & audio corpus picker for all tools

- Add "Select from My Docs" button to all text tool forms; free-tier
  users see an upgrade modal, paid (CaveauAI) users get a searchable
  multi-select modal backed by /api/dashboard/documents.php
- Add "Select from My Audio" picker on Transcribe with single-select
  and a "Save to My Audio" button for persisting uploaded clips
- New PHP helpers in bootstrap.php: dbnToolsFetchDocChunks,
  dbnToolsClientIdFromSession, dbnToolsInjectDocContent
- timeline, ask, redact APIs prepend selected document content
  (fetched from client_chunks SQL) before the textarea text
- api/dashboard/audio-upload.php stores audio files on server and
  creates a client_documents row with source_type='audio'
- api/transcribe.php falls back to stored audio via audio_doc_id POST
  field when no file is uploaded
- api/dashboard/documents.php supports ?source_type= filter
- tools.js: doc_ids added to JSON payload; stored-audio transcribe path
- New assets/css/doc-picker.css, assets/js/doc-picker.js
- SQL migration: scripts/sql/audio_docs_column.sql

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-23 21:38:04 +02:00
parent 58e1d1dae1
commit f383ad5b74
14 changed files with 857 additions and 15 deletions
+66
View File
@@ -1110,3 +1110,69 @@ function dbnToolsExtractCheckLegalBasis(string $text): string
}
return '';
}
// ── Document picker helpers ───────────────────────────────────────────────────
/** Fetch text content of selected client documents as labelled blocks. */
function dbnToolsFetchDocChunks(array $docIds, int $clientId): string
{
if (empty($docIds) || $clientId <= 0) {
return '';
}
$db = dbnToolsDb();
$placeholders = implode(',', array_fill(0, count($docIds), '?'));
$stmt = $db->prepare(
"SELECT c.content, d.title AS doc_title, c.document_id
FROM client_chunks c
JOIN client_documents d ON d.id = c.document_id
WHERE c.client_id = ? AND c.document_id IN ($placeholders)
AND d.source_type != 'audio'
ORDER BY c.document_id, c.id ASC
LIMIT 500"
);
$stmt->execute(array_merge([$clientId], $docIds));
$byDoc = [];
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
$id = (int)$row['document_id'];
$byDoc[$id] ??= ['title' => (string)$row['doc_title'], 'chunks' => []];
$byDoc[$id]['chunks'][] = (string)$row['content'];
}
$parts = [];
foreach ($byDoc as $doc) {
$parts[] = '=== ' . $doc['title'] . " ===\n" . implode("\n\n", $doc['chunks']);
}
return implode("\n\n---\n\n", $parts);
}
/** Resolve client_id for the current CaveauAI session; returns 0 for SSO/free-tier users. */
function dbnToolsClientIdFromSession(): int
{
try {
$tenant = dbnToolsEnsureDashboardTenant();
return (int)($tenant['client_id'] ?? 0);
} catch (Throwable) {
return 0;
}
}
/**
* Inject selected corpus document content into $text if doc_ids are in the request input.
* No-ops silently for free-tier (SSO) users who have no client_documents.
*/
function dbnToolsInjectDocContent(array $input, string $text): string
{
$raw = $input['doc_ids'] ?? [];
$ids = array_values(array_filter(array_map('intval', is_array($raw) ? $raw : explode(',', (string)$raw))));
if (empty($ids)) {
return $text;
}
$clientId = dbnToolsClientIdFromSession();
if ($clientId <= 0) {
return $text;
}
$docText = dbnToolsFetchDocChunks($ids, $clientId);
if ($docText === '') {
return $text;
}
return $docText . ($text !== '' ? "\n\n---\n\n" . $text : '');
}