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
+46 -3
View File
@@ -1070,13 +1070,17 @@ async function runTool(event) {
const tool = tools[state.activeTool];
const text = els.input.value.trim();
if (!text) {
els.status.textContent = 'Add text before running the tool.';
els.input.focus();
const docIds = (document.getElementById('docPickerIds')?.value || '')
.split(',').map(Number).filter(Boolean);
if (!text && !docIds.length) {
els.status.textContent = 'Add text or select a document before running the tool.';
if (!docIds.length) els.input.focus();
return;
}
const payload = { [tool.payloadKey]: text };
if (docIds.length) payload.doc_ids = docIds;
if (tool.usesLanguage) {
payload.language = currentLanguage();
}
@@ -1812,6 +1816,45 @@ function showTranscribeProgress(clip, total) {
}
async function runTranscribe() {
const storedAudioDocId = parseInt(document.getElementById('audioPickerDocId')?.value || '0', 10);
// Stored audio path — no file in the queue required
if (storedAudioDocId > 0 && !audioQueue.length) {
setBusy(true);
els.status.textContent = 'Transcribing from corpus…';
try {
const fd = new FormData();
fd.append('audio_doc_id', String(storedAudioDocId));
fd.append('language', currentTranscribeLang ? currentTranscribeLang() : 'auto');
fd.append('task', currentTask ? currentTask() : 'transcribe');
const vadFilter = document.getElementById('vadFilterCheck')?.checked ?? false;
if (vadFilter) fd.append('vad_filter', '1');
const initPrompt = (document.getElementById('initPromptInput')?.value || '').trim();
if (initPrompt) fd.append('initial_prompt', initPrompt);
const whisperModel = document.getElementById('whisperModelSelect')?.value;
if (whisperModel) fd.append('model', whisperModel);
const postModel = document.querySelector('input[name="post_model"]:checked')?.value;
if (postModel) fd.append('post_model', postModel);
const diarize = document.getElementById('diarizeCheck')?.checked ?? false;
if (diarize) {
fd.append('diarize', '1');
const ns = parseInt(document.getElementById('numSpeakersInput')?.value || '', 10);
if (ns >= 2) fd.append('num_speakers', String(ns));
}
const resp = await fetch('api/transcribe.php', { method: 'POST', credentials: 'same-origin', body: fd });
const data = await resp.json().catch(() => ({}));
if (!resp.ok || !data.ok) throw new Error(data.error?.message || 'Transcription failed.');
if (typeof renderTranscribeResult === 'function') renderTranscribeResult(data);
else renderResults(data);
els.status.textContent = 'Done.';
} catch (err) {
els.status.textContent = err.message;
} finally {
setBusy(false);
}
return;
}
if (!audioQueue.length) {
els.status.textContent = currentUiT('noFileSelected');
return;