2025-11-20 11:00:42 +02:00
|
|
|
import {
|
|
|
|
|
type CreateDocumentRequest,
|
|
|
|
|
createDocumentRequest,
|
|
|
|
|
createDocumentResponse,
|
|
|
|
|
type DeleteDocumentRequest,
|
|
|
|
|
deleteDocumentRequest,
|
|
|
|
|
deleteDocumentResponse,
|
|
|
|
|
type GetDocumentByChunkRequest,
|
|
|
|
|
type GetDocumentRequest,
|
|
|
|
|
type GetDocumentsRequest,
|
2026-02-09 16:46:54 -08:00
|
|
|
type GetDocumentsStatusRequest,
|
2025-11-20 11:00:42 +02:00
|
|
|
type GetDocumentTypeCountsRequest,
|
2026-01-13 01:15:33 +02:00
|
|
|
type GetSurfsenseDocsRequest,
|
2025-11-20 11:00:42 +02:00
|
|
|
getDocumentByChunkRequest,
|
|
|
|
|
getDocumentByChunkResponse,
|
|
|
|
|
getDocumentRequest,
|
|
|
|
|
getDocumentResponse,
|
|
|
|
|
getDocumentsRequest,
|
2026-02-09 16:49:11 -08:00
|
|
|
getDocumentsResponse,
|
2026-02-09 16:46:54 -08:00
|
|
|
getDocumentsStatusRequest,
|
|
|
|
|
getDocumentsStatusResponse,
|
2025-11-20 11:00:42 +02:00
|
|
|
getDocumentTypeCountsRequest,
|
|
|
|
|
getDocumentTypeCountsResponse,
|
2026-01-12 18:06:51 +02:00
|
|
|
getSurfsenseDocsByChunkResponse,
|
2026-01-13 01:45:58 -08:00
|
|
|
getSurfsenseDocsRequest,
|
2026-01-13 01:15:33 +02:00
|
|
|
getSurfsenseDocsResponse,
|
2025-11-20 11:00:42 +02:00
|
|
|
type SearchDocumentsRequest,
|
2026-01-17 20:45:10 +05:30
|
|
|
type SearchDocumentTitlesRequest,
|
2025-11-20 11:00:42 +02:00
|
|
|
searchDocumentsRequest,
|
|
|
|
|
searchDocumentsResponse,
|
2026-01-17 20:45:10 +05:30
|
|
|
searchDocumentTitlesRequest,
|
|
|
|
|
searchDocumentTitlesResponse,
|
2025-11-20 11:00:42 +02:00
|
|
|
type UpdateDocumentRequest,
|
|
|
|
|
type UploadDocumentRequest,
|
|
|
|
|
updateDocumentRequest,
|
|
|
|
|
updateDocumentResponse,
|
|
|
|
|
uploadDocumentRequest,
|
|
|
|
|
uploadDocumentResponse,
|
|
|
|
|
} from "@/contracts/types/document.types";
|
|
|
|
|
import { ValidationError } from "../error";
|
|
|
|
|
import { baseApiService } from "./base-api.service";
|
|
|
|
|
|
|
|
|
|
class DocumentsApiService {
|
|
|
|
|
/**
|
|
|
|
|
* Get a list of documents with optional filtering and pagination
|
|
|
|
|
*/
|
|
|
|
|
getDocuments = async (request: GetDocumentsRequest) => {
|
|
|
|
|
const parsedRequest = getDocumentsRequest.safeParse(request);
|
|
|
|
|
|
|
|
|
|
if (!parsedRequest.success) {
|
|
|
|
|
console.error("Invalid request:", parsedRequest.error);
|
|
|
|
|
|
2025-12-20 05:10:47 -08:00
|
|
|
const errorMessage = parsedRequest.error.issues.map((issue) => issue.message).join(", ");
|
2025-11-20 11:00:42 +02:00
|
|
|
throw new ValidationError(`Invalid request: ${errorMessage}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Transform query params to be string values
|
|
|
|
|
const transformedQueryParams = parsedRequest.data.queryParams
|
|
|
|
|
? Object.fromEntries(
|
|
|
|
|
Object.entries(parsedRequest.data.queryParams).map(([k, v]) => {
|
|
|
|
|
// Handle array values (document_type)
|
|
|
|
|
if (Array.isArray(v)) {
|
|
|
|
|
return [k, v.join(",")];
|
|
|
|
|
}
|
|
|
|
|
return [k, String(v)];
|
|
|
|
|
})
|
|
|
|
|
)
|
|
|
|
|
: undefined;
|
|
|
|
|
|
|
|
|
|
const queryParams = transformedQueryParams
|
|
|
|
|
? new URLSearchParams(transformedQueryParams).toString()
|
|
|
|
|
: "";
|
|
|
|
|
|
|
|
|
|
return baseApiService.get(`/api/v1/documents?${queryParams}`, getDocumentsResponse);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get a single document by ID
|
|
|
|
|
*/
|
|
|
|
|
getDocument = async (request: GetDocumentRequest) => {
|
|
|
|
|
const parsedRequest = getDocumentRequest.safeParse(request);
|
|
|
|
|
|
|
|
|
|
if (!parsedRequest.success) {
|
|
|
|
|
console.error("Invalid request:", parsedRequest.error);
|
|
|
|
|
|
2025-12-20 05:10:47 -08:00
|
|
|
const errorMessage = parsedRequest.error.issues.map((issue) => issue.message).join(", ");
|
2025-11-20 11:00:42 +02:00
|
|
|
throw new ValidationError(`Invalid request: ${errorMessage}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return baseApiService.get(`/api/v1/documents/${request.id}`, getDocumentResponse);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Create documents (extension, crawled URL, or YouTube video)
|
|
|
|
|
*/
|
|
|
|
|
createDocument = async (request: CreateDocumentRequest) => {
|
|
|
|
|
const parsedRequest = createDocumentRequest.safeParse(request);
|
|
|
|
|
|
|
|
|
|
if (!parsedRequest.success) {
|
|
|
|
|
console.error("Invalid request:", parsedRequest.error);
|
|
|
|
|
|
2025-12-20 05:10:47 -08:00
|
|
|
const errorMessage = parsedRequest.error.issues.map((issue) => issue.message).join(", ");
|
2025-11-20 11:00:42 +02:00
|
|
|
throw new ValidationError(`Invalid request: ${errorMessage}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return baseApiService.post(`/api/v1/documents`, createDocumentResponse, {
|
|
|
|
|
body: parsedRequest.data,
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
2026-02-28 17:22:34 -08:00
|
|
|
* Upload document files in batches to avoid proxy/LB timeouts.
|
|
|
|
|
* Files are split into chunks of UPLOAD_BATCH_SIZE and sent as separate
|
|
|
|
|
* requests. Results are aggregated into a single response.
|
2025-11-20 11:00:42 +02:00
|
|
|
*/
|
|
|
|
|
uploadDocument = async (request: UploadDocumentRequest) => {
|
|
|
|
|
const parsedRequest = uploadDocumentRequest.safeParse(request);
|
|
|
|
|
|
|
|
|
|
if (!parsedRequest.success) {
|
|
|
|
|
console.error("Invalid request:", parsedRequest.error);
|
|
|
|
|
|
2025-12-20 05:10:47 -08:00
|
|
|
const errorMessage = parsedRequest.error.issues.map((issue) => issue.message).join(", ");
|
2025-11-20 11:00:42 +02:00
|
|
|
throw new ValidationError(`Invalid request: ${errorMessage}`);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-28 17:22:34 -08:00
|
|
|
const { files, search_space_id, should_summarize } = parsedRequest.data;
|
|
|
|
|
const UPLOAD_BATCH_SIZE = 5;
|
2025-11-20 11:00:42 +02:00
|
|
|
|
2026-02-28 17:22:34 -08:00
|
|
|
const batches: File[][] = [];
|
|
|
|
|
for (let i = 0; i < files.length; i += UPLOAD_BATCH_SIZE) {
|
|
|
|
|
batches.push(files.slice(i, i + UPLOAD_BATCH_SIZE));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const allDocumentIds: number[] = [];
|
|
|
|
|
const allDuplicateIds: number[] = [];
|
|
|
|
|
let totalFiles = 0;
|
|
|
|
|
let pendingFiles = 0;
|
|
|
|
|
let skippedDuplicates = 0;
|
|
|
|
|
|
|
|
|
|
for (const batch of batches) {
|
|
|
|
|
const formData = new FormData();
|
2026-03-27 03:17:05 -07:00
|
|
|
for (const file of batch) formData.append("files", file);
|
2026-02-28 17:22:34 -08:00
|
|
|
formData.append("search_space_id", String(search_space_id));
|
|
|
|
|
formData.append("should_summarize", String(should_summarize));
|
|
|
|
|
|
|
|
|
|
const controller = new AbortController();
|
|
|
|
|
const timeoutId = setTimeout(() => controller.abort(), 120_000);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const result = await baseApiService.postFormData(
|
|
|
|
|
`/api/v1/documents/fileupload`,
|
|
|
|
|
uploadDocumentResponse,
|
|
|
|
|
{ body: formData, signal: controller.signal }
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
allDocumentIds.push(...(result.document_ids ?? []));
|
|
|
|
|
allDuplicateIds.push(...(result.duplicate_document_ids ?? []));
|
|
|
|
|
totalFiles += result.total_files ?? batch.length;
|
|
|
|
|
pendingFiles += result.pending_files ?? 0;
|
|
|
|
|
skippedDuplicates += result.skipped_duplicates ?? 0;
|
|
|
|
|
} finally {
|
|
|
|
|
clearTimeout(timeoutId);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
message: "Files uploaded for processing" as const,
|
|
|
|
|
document_ids: allDocumentIds,
|
|
|
|
|
duplicate_document_ids: allDuplicateIds,
|
|
|
|
|
total_files: totalFiles,
|
|
|
|
|
pending_files: pendingFiles,
|
|
|
|
|
skipped_duplicates: skippedDuplicates,
|
|
|
|
|
};
|
2025-11-20 11:00:42 +02:00
|
|
|
};
|
|
|
|
|
|
2026-02-09 16:46:54 -08:00
|
|
|
/**
|
|
|
|
|
* Batch document status for async processing tracking
|
|
|
|
|
*/
|
|
|
|
|
getDocumentsStatus = async (request: GetDocumentsStatusRequest) => {
|
|
|
|
|
const parsedRequest = getDocumentsStatusRequest.safeParse(request);
|
|
|
|
|
|
|
|
|
|
if (!parsedRequest.success) {
|
|
|
|
|
console.error("Invalid request:", parsedRequest.error);
|
|
|
|
|
const errorMessage = parsedRequest.error.issues.map((issue) => issue.message).join(", ");
|
|
|
|
|
throw new ValidationError(`Invalid request: ${errorMessage}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const { search_space_id, document_ids } = parsedRequest.data.queryParams;
|
|
|
|
|
const params = new URLSearchParams({
|
|
|
|
|
search_space_id: String(search_space_id),
|
|
|
|
|
document_ids: document_ids.join(","),
|
|
|
|
|
});
|
|
|
|
|
|
2026-02-09 16:49:11 -08:00
|
|
|
return baseApiService.get(
|
|
|
|
|
`/api/v1/documents/status?${params.toString()}`,
|
|
|
|
|
getDocumentsStatusResponse
|
|
|
|
|
);
|
2026-02-09 16:46:54 -08:00
|
|
|
};
|
|
|
|
|
|
2025-11-20 11:00:42 +02:00
|
|
|
/**
|
|
|
|
|
* Search documents by title
|
|
|
|
|
*/
|
|
|
|
|
searchDocuments = async (request: SearchDocumentsRequest) => {
|
|
|
|
|
const parsedRequest = searchDocumentsRequest.safeParse(request);
|
|
|
|
|
|
|
|
|
|
if (!parsedRequest.success) {
|
|
|
|
|
console.error("Invalid request:", parsedRequest.error);
|
|
|
|
|
|
2025-12-20 05:10:47 -08:00
|
|
|
const errorMessage = parsedRequest.error.issues.map((issue) => issue.message).join(", ");
|
2025-11-20 11:00:42 +02:00
|
|
|
throw new ValidationError(`Invalid request: ${errorMessage}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Transform query params to be string values
|
|
|
|
|
const transformedQueryParams = parsedRequest.data.queryParams
|
|
|
|
|
? Object.fromEntries(
|
|
|
|
|
Object.entries(parsedRequest.data.queryParams).map(([k, v]) => {
|
|
|
|
|
// Handle array values (document_type)
|
|
|
|
|
if (Array.isArray(v)) {
|
|
|
|
|
return [k, v.join(",")];
|
|
|
|
|
}
|
|
|
|
|
return [k, String(v)];
|
|
|
|
|
})
|
|
|
|
|
)
|
|
|
|
|
: undefined;
|
|
|
|
|
|
|
|
|
|
const queryParams = transformedQueryParams
|
|
|
|
|
? new URLSearchParams(transformedQueryParams).toString()
|
|
|
|
|
: "";
|
|
|
|
|
|
|
|
|
|
return baseApiService.get(`/api/v1/documents/search?${queryParams}`, searchDocumentsResponse);
|
|
|
|
|
};
|
|
|
|
|
|
2026-01-17 20:45:10 +05:30
|
|
|
/**
|
|
|
|
|
* Search document titles (lightweight, optimized for mention picker)
|
|
|
|
|
* Returns only id, title, document_type - no content or metadata
|
2026-01-17 21:44:10 +05:30
|
|
|
* @param request - The search request with query params
|
|
|
|
|
* @param signal - Optional AbortSignal for request cancellation
|
2026-01-17 20:45:10 +05:30
|
|
|
*/
|
2026-01-17 21:44:10 +05:30
|
|
|
searchDocumentTitles = async (request: SearchDocumentTitlesRequest, signal?: AbortSignal) => {
|
2026-01-17 20:45:10 +05:30
|
|
|
const parsedRequest = searchDocumentTitlesRequest.safeParse(request);
|
|
|
|
|
|
|
|
|
|
if (!parsedRequest.success) {
|
|
|
|
|
console.error("Invalid request:", parsedRequest.error);
|
|
|
|
|
|
|
|
|
|
const errorMessage = parsedRequest.error.issues.map((issue) => issue.message).join(", ");
|
|
|
|
|
throw new ValidationError(`Invalid request: ${errorMessage}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Transform query params to be string values
|
|
|
|
|
const transformedQueryParams = Object.fromEntries(
|
|
|
|
|
Object.entries(parsedRequest.data.queryParams)
|
|
|
|
|
.filter(([, v]) => v !== undefined)
|
|
|
|
|
.map(([k, v]) => [k, String(v)])
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const queryParams = new URLSearchParams(transformedQueryParams).toString();
|
|
|
|
|
|
|
|
|
|
return baseApiService.get(
|
|
|
|
|
`/api/v1/documents/search/titles?${queryParams}`,
|
2026-01-17 21:44:10 +05:30
|
|
|
searchDocumentTitlesResponse,
|
|
|
|
|
{ signal }
|
2026-01-17 20:45:10 +05:30
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
2025-11-20 11:00:42 +02:00
|
|
|
/**
|
|
|
|
|
* Get document type counts
|
|
|
|
|
*/
|
|
|
|
|
getDocumentTypeCounts = async (request: GetDocumentTypeCountsRequest) => {
|
|
|
|
|
// Validate the request
|
|
|
|
|
const parsedRequest = getDocumentTypeCountsRequest.safeParse(request);
|
|
|
|
|
|
|
|
|
|
if (!parsedRequest.success) {
|
|
|
|
|
console.error("Invalid request:", parsedRequest.error);
|
|
|
|
|
|
|
|
|
|
// Format a user friendly error message
|
2025-12-20 05:10:47 -08:00
|
|
|
const errorMessage = parsedRequest.error.issues.map((issue) => issue.message).join(", ");
|
2025-11-20 11:00:42 +02:00
|
|
|
throw new ValidationError(`Invalid request: ${errorMessage}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Transform query params to be string values
|
|
|
|
|
const transformedQueryParams = parsedRequest.data.queryParams
|
|
|
|
|
? Object.fromEntries(
|
|
|
|
|
Object.entries(parsedRequest.data.queryParams).map(([k, v]) => [k, String(v)])
|
|
|
|
|
)
|
|
|
|
|
: undefined;
|
|
|
|
|
|
|
|
|
|
const queryParams = transformedQueryParams
|
|
|
|
|
? new URLSearchParams(transformedQueryParams).toString()
|
|
|
|
|
: "";
|
|
|
|
|
|
|
|
|
|
return baseApiService.get(
|
|
|
|
|
`/api/v1/documents/type-counts?${queryParams}`,
|
|
|
|
|
getDocumentTypeCountsResponse
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get document by chunk ID (includes all chunks)
|
|
|
|
|
*/
|
|
|
|
|
getDocumentByChunk = async (request: GetDocumentByChunkRequest) => {
|
|
|
|
|
// Validate the request
|
|
|
|
|
const parsedRequest = getDocumentByChunkRequest.safeParse(request);
|
|
|
|
|
|
|
|
|
|
if (!parsedRequest.success) {
|
|
|
|
|
console.error("Invalid request:", parsedRequest.error);
|
|
|
|
|
|
|
|
|
|
// Format a user friendly error message
|
2025-12-20 05:10:47 -08:00
|
|
|
const errorMessage = parsedRequest.error.issues.map((issue) => issue.message).join(", ");
|
2025-11-20 11:00:42 +02:00
|
|
|
throw new ValidationError(`Invalid request: ${errorMessage}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return baseApiService.get(
|
|
|
|
|
`/api/v1/documents/by-chunk/${request.chunk_id}`,
|
|
|
|
|
getDocumentByChunkResponse
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
2026-01-09 18:21:59 +02:00
|
|
|
/**
|
|
|
|
|
* Get Surfsense documentation by chunk ID
|
|
|
|
|
* Used for resolving [citation:doc-XXX] citations
|
|
|
|
|
*/
|
|
|
|
|
getSurfsenseDocByChunk = async (chunkId: number) => {
|
|
|
|
|
return baseApiService.get(
|
|
|
|
|
`/api/v1/surfsense-docs/by-chunk/${chunkId}`,
|
2026-01-12 18:06:51 +02:00
|
|
|
getSurfsenseDocsByChunkResponse
|
2026-01-09 18:21:59 +02:00
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
2026-01-13 01:15:33 +02:00
|
|
|
/**
|
|
|
|
|
* List all Surfsense documentation documents
|
2026-01-17 21:44:10 +05:30
|
|
|
* @param request - The request with query params
|
|
|
|
|
* @param signal - Optional AbortSignal for request cancellation
|
2026-01-13 01:15:33 +02:00
|
|
|
*/
|
2026-01-17 21:44:10 +05:30
|
|
|
getSurfsenseDocs = async (request: GetSurfsenseDocsRequest, signal?: AbortSignal) => {
|
2026-01-13 06:14:58 +02:00
|
|
|
const parsedRequest = getSurfsenseDocsRequest.safeParse(request);
|
2026-01-13 01:15:33 +02:00
|
|
|
|
2026-01-13 06:14:58 +02:00
|
|
|
if (!parsedRequest.success) {
|
|
|
|
|
console.error("Invalid request:", parsedRequest.error);
|
|
|
|
|
|
|
|
|
|
const errorMessage = parsedRequest.error.issues.map((issue) => issue.message).join(", ");
|
|
|
|
|
throw new ValidationError(`Invalid request: ${errorMessage}`);
|
2026-01-13 01:15:33 +02:00
|
|
|
}
|
|
|
|
|
|
2026-01-13 06:14:58 +02:00
|
|
|
// Transform query params to be string values
|
|
|
|
|
const transformedQueryParams = parsedRequest.data.queryParams
|
|
|
|
|
? Object.fromEntries(
|
|
|
|
|
Object.entries(parsedRequest.data.queryParams).map(([k, v]) => [k, String(v)])
|
|
|
|
|
)
|
|
|
|
|
: undefined;
|
|
|
|
|
|
|
|
|
|
const queryParams = transformedQueryParams
|
|
|
|
|
? new URLSearchParams(transformedQueryParams).toString()
|
|
|
|
|
: "";
|
|
|
|
|
|
|
|
|
|
const url = `/api/v1/surfsense-docs?${queryParams}`;
|
2026-01-13 01:15:33 +02:00
|
|
|
|
2026-01-17 21:44:10 +05:30
|
|
|
return baseApiService.get(url, getSurfsenseDocsResponse, { signal });
|
2026-01-13 01:15:33 +02:00
|
|
|
};
|
|
|
|
|
|
2025-11-20 11:00:42 +02:00
|
|
|
/**
|
|
|
|
|
* Update a document
|
|
|
|
|
*/
|
|
|
|
|
updateDocument = async (request: UpdateDocumentRequest) => {
|
|
|
|
|
// Validate the request
|
|
|
|
|
const parsedRequest = updateDocumentRequest.safeParse(request);
|
|
|
|
|
|
|
|
|
|
if (!parsedRequest.success) {
|
|
|
|
|
console.error("Invalid request:", parsedRequest.error);
|
|
|
|
|
|
|
|
|
|
// Format a user friendly error message
|
2025-12-20 05:10:47 -08:00
|
|
|
const errorMessage = parsedRequest.error.issues.map((issue) => issue.message).join(", ");
|
2025-11-20 11:00:42 +02:00
|
|
|
throw new ValidationError(`Invalid request: ${errorMessage}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const { id, data } = parsedRequest.data;
|
|
|
|
|
|
|
|
|
|
return baseApiService.put(`/api/v1/documents/${id}`, updateDocumentResponse, {
|
|
|
|
|
body: data,
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Delete a document
|
|
|
|
|
*/
|
|
|
|
|
deleteDocument = async (request: DeleteDocumentRequest) => {
|
|
|
|
|
// Validate the request
|
|
|
|
|
const parsedRequest = deleteDocumentRequest.safeParse(request);
|
|
|
|
|
|
|
|
|
|
if (!parsedRequest.success) {
|
|
|
|
|
console.error("Invalid request:", parsedRequest.error);
|
|
|
|
|
|
|
|
|
|
// Format a user friendly error message
|
2025-12-20 05:10:47 -08:00
|
|
|
const errorMessage = parsedRequest.error.issues.map((issue) => issue.message).join(", ");
|
2025-11-20 11:00:42 +02:00
|
|
|
throw new ValidationError(`Invalid request: ${errorMessage}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return baseApiService.delete(`/api/v1/documents/${request.id}`, deleteDocumentResponse);
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const documentsApiService = new DocumentsApiService();
|