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:
@@ -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 : '');
|
||||
}
|
||||
|
||||
@@ -22,11 +22,44 @@
|
||||
</section><!-- /workspace -->
|
||||
</main><!-- /appShell -->
|
||||
<?php require_once __DIR__ . '/footer.php'; ?>
|
||||
<link rel="stylesheet" href="/assets/css/doc-picker.css">
|
||||
<script src="assets/js/tools.js" defer></script>
|
||||
<?php if (!empty($extraScripts) && is_array($extraScripts)): foreach ($extraScripts as $extraScript): ?>
|
||||
<script src="<?= htmlspecialchars((string)$extraScript) ?>" defer></script>
|
||||
<?php endforeach; endif; ?>
|
||||
<script src="assets/js/corpus-save.js" defer></script>
|
||||
<script src="/assets/js/doc-picker.js" defer></script>
|
||||
|
||||
<!-- Doc picker modal (shared across all tool pages) -->
|
||||
<div id="docPickerBackdrop" class="doc-picker-backdrop" hidden role="dialog" aria-modal="true" aria-labelledby="docPickerTitle">
|
||||
<div class="doc-picker-dialog">
|
||||
<div class="doc-picker-dialog__head">
|
||||
<h3 id="docPickerTitle">Select from My Docs</h3>
|
||||
<button class="doc-picker-dialog__close" aria-label="Close">×</button>
|
||||
</div>
|
||||
<input type="search" class="doc-picker-dialog__search" placeholder="Search documents…" aria-label="Search documents">
|
||||
<div class="doc-picker-list" role="listbox" aria-multiselectable="true">
|
||||
<p class="doc-picker-list__loading">Loading…</p>
|
||||
</div>
|
||||
<div class="doc-picker-dialog__foot">
|
||||
<span class="doc-picker-dialog__count"></span>
|
||||
<button class="doc-picker-dialog__confirm" disabled>Add to tool</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Upgrade modal for free-tier users -->
|
||||
<div id="docPickerUpgradeBackdrop" class="doc-picker-upgrade-backdrop" hidden role="dialog" aria-modal="true" aria-labelledby="docPickerUpgradeTitle">
|
||||
<div class="doc-picker-upgrade-card">
|
||||
<span class="doc-picker-upgrade-card__icon">📂</span>
|
||||
<h3 id="docPickerUpgradeTitle">Plus & Pro feature</h3>
|
||||
<p>Select documents from your uploaded corpus and feed them directly into any tool. Available on Plus and Pro plans.</p>
|
||||
<div class="doc-picker-upgrade-card__actions">
|
||||
<a href="/pricing.php" class="doc-picker-upgrade-card__cta">View plans</a>
|
||||
<button class="doc-picker-upgrade-card__dismiss">Maybe later</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Save-to-corpus dialog (shared across all tool pages) -->
|
||||
<dialog id="save-corpus-dialog" class="save-corpus-dialog">
|
||||
|
||||
@@ -72,6 +72,15 @@
|
||||
<p class="alias-hint">Replace a name with a bracketed alias, e.g. “David Jr” → [Junior]</p>
|
||||
</div>
|
||||
|
||||
<div id="docPickerSection" class="doc-picker-section">
|
||||
<button type="button" id="docPickerBtn" class="doc-picker-btn" aria-haspopup="dialog">
|
||||
<svg class="doc-picker-btn__icon" width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true"><path d="M3 2h7l3 3v9H3V2z" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"/><path d="M10 2v3h3" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"/><path d="M5 7h6M5 9.5h4" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/></svg>
|
||||
<span>Select from My Docs</span>
|
||||
</button>
|
||||
<div id="docPickerChips" class="doc-picker-chips" aria-label="Selected documents"></div>
|
||||
<input type="hidden" id="docPickerIds" name="doc_ids" value="">
|
||||
</div>
|
||||
|
||||
<label class="input-label" for="toolInput" id="inputLabel">Question</label>
|
||||
<textarea id="toolInput" name="toolInput" rows="10" required></textarea>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user