f383ad5b74
- 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>
256 lines
8.3 KiB
PHP
256 lines
8.3 KiB
PHP
<?php
|
|
/**
|
|
* /api/dashboard/documents.php — CRUD for the current user's CaveauAI documents.
|
|
*
|
|
* GET ?action=list&offset=0&limit=20&q=&status=&category=
|
|
* → { ok, total, documents: [...] }
|
|
* GET ?action=get&id=123
|
|
* → { ok, document: {...} }
|
|
* POST ?action=update body: { id, title?, category?, tags?, language?, author? }
|
|
* → { ok, document: {...} }
|
|
* POST ?action=delete body: { ids: [1,2,3] }
|
|
* → { ok, deleted: N }
|
|
*
|
|
* All filtered by client_id from the dashboard session — no cross-tenant access possible.
|
|
*/
|
|
|
|
declare(strict_types=1);
|
|
|
|
require_once dirname(__DIR__, 2) . '/includes/bootstrap.php';
|
|
|
|
dbnToolsRequireAuth();
|
|
|
|
try {
|
|
$tenant = dbnToolsEnsureDashboardTenant();
|
|
} catch (DbnToolsHttpException $e) {
|
|
dbnToolsError($e->getMessage(), $e->status, $e->errorCode);
|
|
}
|
|
$clientId = (int)$tenant['client_id'];
|
|
|
|
$method = strtoupper((string)($_SERVER['REQUEST_METHOD'] ?? 'GET'));
|
|
$action = (string)($_GET['action'] ?? ($method === 'POST' ? '' : 'list'));
|
|
|
|
$db = dbnToolsDb();
|
|
|
|
switch ($action) {
|
|
case 'list':
|
|
dbnToolsRequireMethod('GET');
|
|
respondList($db, $clientId);
|
|
break;
|
|
case 'get':
|
|
dbnToolsRequireMethod('GET');
|
|
respondGet($db, $clientId);
|
|
break;
|
|
case 'update':
|
|
dbnToolsRequireMethod('POST');
|
|
respondUpdate($db, $clientId);
|
|
break;
|
|
case 'delete':
|
|
dbnToolsRequireMethod('POST');
|
|
respondDelete($db, $clientId);
|
|
break;
|
|
default:
|
|
dbnToolsError('Unknown action.', 400, 'unknown_action');
|
|
}
|
|
|
|
function respondList(PDO $db, int $clientId): void
|
|
{
|
|
$offset = max(0, (int)($_GET['offset'] ?? 0));
|
|
$limit = max(1, min(100, (int)($_GET['limit'] ?? 20)));
|
|
$q = trim((string)($_GET['q'] ?? ''));
|
|
$status = trim((string)($_GET['status'] ?? ''));
|
|
$category = trim((string)($_GET['category'] ?? ''));
|
|
|
|
$where = ['client_id = ?'];
|
|
$params = [$clientId];
|
|
|
|
if ($q !== '') {
|
|
$where[] = '(title LIKE ? OR tags LIKE ?)';
|
|
$like = '%' . str_replace(['%', '_'], ['\%', '\_'], $q) . '%';
|
|
$params[] = $like;
|
|
$params[] = $like;
|
|
}
|
|
$allowedStatus = ['pending', 'processing', 'ready', 'error'];
|
|
if ($status !== '' && in_array($status, $allowedStatus, true)) {
|
|
$where[] = 'status = ?';
|
|
$params[] = $status;
|
|
}
|
|
if ($category !== '') {
|
|
$where[] = 'category = ?';
|
|
$params[] = $category;
|
|
}
|
|
$sourceType = trim((string)($_GET['source_type'] ?? ''));
|
|
$allowedSourceTypes = ['text', 'audio', 'url', 'tool-output', 'upload'];
|
|
if ($sourceType !== '' && in_array($sourceType, $allowedSourceTypes, true)) {
|
|
$where[] = 'source_type = ?';
|
|
$params[] = $sourceType;
|
|
}
|
|
|
|
$whereSql = 'WHERE ' . implode(' AND ', $where);
|
|
|
|
$countStmt = $db->prepare("SELECT COUNT(*) FROM client_documents {$whereSql}");
|
|
$countStmt->execute($params);
|
|
$total = (int)$countStmt->fetchColumn();
|
|
|
|
$sql = "SELECT id, title, source_type, language, category, tags, author,
|
|
source_tool, import_method, status, word_count, chunk_count,
|
|
file_size_bytes, source_url, error_message,
|
|
created_at, updated_at
|
|
FROM client_documents
|
|
{$whereSql}
|
|
ORDER BY id DESC
|
|
LIMIT {$limit} OFFSET {$offset}";
|
|
$stmt = $db->prepare($sql);
|
|
$stmt->execute($params);
|
|
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
|
|
dbnToolsRespond([
|
|
'ok' => true,
|
|
'total' => $total,
|
|
'offset' => $offset,
|
|
'limit' => $limit,
|
|
'documents' => array_map('shapeDoc', $rows),
|
|
]);
|
|
}
|
|
|
|
function respondGet(PDO $db, int $clientId): void
|
|
{
|
|
$id = (int)($_GET['id'] ?? 0);
|
|
if ($id <= 0) {
|
|
dbnToolsError('id is required.', 400, 'missing_id');
|
|
}
|
|
$stmt = $db->prepare(
|
|
'SELECT * FROM client_documents WHERE id = ? AND client_id = ? LIMIT 1'
|
|
);
|
|
$stmt->execute([$id, $clientId]);
|
|
$doc = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
if (!$doc) {
|
|
dbnToolsError('Document not found.', 404, 'not_found');
|
|
}
|
|
|
|
$chunks = $db->prepare(
|
|
'SELECT id, content, section_title
|
|
FROM client_chunks
|
|
WHERE client_id = ? AND document_id = ?
|
|
ORDER BY id ASC
|
|
LIMIT 200'
|
|
);
|
|
try {
|
|
$chunks->execute([$clientId, $id]);
|
|
$chunkRows = $chunks->fetchAll(PDO::FETCH_ASSOC);
|
|
} catch (Throwable $e) {
|
|
$chunkRows = [];
|
|
}
|
|
|
|
dbnToolsRespond([
|
|
'ok' => true,
|
|
'document' => shapeDoc($doc) + ['content' => (string)$doc['content']],
|
|
'chunks' => $chunkRows,
|
|
]);
|
|
}
|
|
|
|
function respondUpdate(PDO $db, int $clientId): void
|
|
{
|
|
$input = dbnToolsJsonInput(20_000);
|
|
$id = (int)($input['id'] ?? 0);
|
|
if ($id <= 0) {
|
|
dbnToolsError('id is required.', 400, 'missing_id');
|
|
}
|
|
|
|
$fields = [];
|
|
$params = [];
|
|
$allowed = [
|
|
'title' => ['VARCHAR', 500],
|
|
'category' => ['VARCHAR', 50],
|
|
'tags' => ['VARCHAR', 500],
|
|
'language' => ['VARCHAR', 10],
|
|
'author' => ['VARCHAR', 200],
|
|
];
|
|
foreach ($allowed as $col => [$kind, $max]) {
|
|
if (!array_key_exists($col, $input)) {
|
|
continue;
|
|
}
|
|
$val = trim((string)$input[$col]);
|
|
if (mb_strlen($val, 'UTF-8') > $max) {
|
|
dbnToolsError("Field {$col} exceeds {$max} chars.", 422, 'field_too_long');
|
|
}
|
|
$fields[] = "{$col} = ?";
|
|
$params[] = $val !== '' ? $val : null;
|
|
}
|
|
if (!$fields) {
|
|
dbnToolsError('No editable fields supplied.', 400, 'no_fields');
|
|
}
|
|
$params[] = $id;
|
|
$params[] = $clientId;
|
|
|
|
$stmt = $db->prepare(
|
|
'UPDATE client_documents SET ' . implode(', ', $fields)
|
|
. ', updated_at = NOW() WHERE id = ? AND client_id = ?'
|
|
);
|
|
$stmt->execute($params);
|
|
|
|
$stmt = $db->prepare('SELECT * FROM client_documents WHERE id = ? AND client_id = ? LIMIT 1');
|
|
$stmt->execute([$id, $clientId]);
|
|
$doc = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
|
|
dbnToolsRespond(['ok' => true, 'document' => shapeDoc($doc ?: [])]);
|
|
}
|
|
|
|
function respondDelete(PDO $db, int $clientId): void
|
|
{
|
|
$input = dbnToolsJsonInput(50_000);
|
|
$ids = $input['ids'] ?? [];
|
|
if (!is_array($ids) || !$ids) {
|
|
dbnToolsError('ids array is required.', 400, 'missing_ids');
|
|
}
|
|
$ids = array_values(array_unique(array_map('intval', $ids)));
|
|
$ids = array_filter($ids, fn($v) => $v > 0);
|
|
if (!$ids) {
|
|
dbnToolsError('No valid ids.', 400, 'invalid_ids');
|
|
}
|
|
if (count($ids) > 200) {
|
|
dbnToolsError('Cannot delete more than 200 documents at once.', 422, 'too_many');
|
|
}
|
|
|
|
$placeholders = implode(',', array_fill(0, count($ids), '?'));
|
|
$stmt = $db->prepare(
|
|
"DELETE FROM client_documents
|
|
WHERE client_id = ? AND id IN ({$placeholders})"
|
|
);
|
|
$stmt->execute(array_merge([$clientId], $ids));
|
|
|
|
try {
|
|
$chunks = $db->prepare(
|
|
"DELETE FROM client_chunks WHERE client_id = ? AND document_id IN ({$placeholders})"
|
|
);
|
|
$chunks->execute(array_merge([$clientId], $ids));
|
|
} catch (Throwable $e) {
|
|
// table may be filtered to client_id only; non-fatal
|
|
}
|
|
|
|
dbnToolsRespond(['ok' => true, 'deleted' => $stmt->rowCount()]);
|
|
}
|
|
|
|
function shapeDoc(array $row): array
|
|
{
|
|
return [
|
|
'id' => (int)($row['id'] ?? 0),
|
|
'title' => (string)($row['title'] ?? ''),
|
|
'source_type' => (string)($row['source_type'] ?? ''),
|
|
'language' => (string)($row['language'] ?? ''),
|
|
'category' => (string)($row['category'] ?? ''),
|
|
'tags' => (string)($row['tags'] ?? ''),
|
|
'author' => $row['author'] ?? null,
|
|
'source_url' => $row['source_url'] ?? null,
|
|
'source_tool' => $row['source_tool'] ?? null,
|
|
'import_method' => (string)($row['import_method'] ?? ''),
|
|
'status' => (string)($row['status'] ?? ''),
|
|
'word_count' => (int)($row['word_count'] ?? 0),
|
|
'chunk_count' => (int)($row['chunk_count'] ?? 0),
|
|
'file_size_bytes'=> (int)($row['file_size_bytes'] ?? 0),
|
|
'error_message' => $row['error_message'] ?? null,
|
|
'created_at' => (string)($row['created_at'] ?? ''),
|
|
'updated_at' => (string)($row['updated_at'] ?? ''),
|
|
];
|
|
}
|