Initial commit
This commit is contained in:
411
frontend/src/lib/api.ts
Normal file
411
frontend/src/lib/api.ts
Normal file
@@ -0,0 +1,411 @@
|
||||
/**
|
||||
* API client for backend DMS endpoints.
|
||||
*/
|
||||
import type {
|
||||
AppSettings,
|
||||
AppSettingsUpdate,
|
||||
DocumentListResponse,
|
||||
DmsDocument,
|
||||
DmsDocumentDetail,
|
||||
ProcessingLogListResponse,
|
||||
SearchResponse,
|
||||
TypeListResponse,
|
||||
UploadResponse,
|
||||
} from '../types';
|
||||
|
||||
/**
|
||||
* Resolves backend base URL from environment with localhost fallback.
|
||||
*/
|
||||
const API_BASE = import.meta.env.VITE_API_BASE ?? 'http://localhost:8000/api/v1';
|
||||
|
||||
/**
|
||||
* Encodes query parameters while skipping undefined and null values.
|
||||
*/
|
||||
function buildQuery(params: Record<string, string | number | boolean | undefined | null>): string {
|
||||
const searchParams = new URLSearchParams();
|
||||
Object.entries(params).forEach(([key, value]) => {
|
||||
if (value === undefined || value === null || value === '') {
|
||||
return;
|
||||
}
|
||||
searchParams.set(key, String(value));
|
||||
});
|
||||
const encoded = searchParams.toString();
|
||||
return encoded ? `?${encoded}` : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts a filename from content-disposition headers with fallback support.
|
||||
*/
|
||||
function responseFilename(response: Response, fallback: string): string {
|
||||
const disposition = response.headers.get('content-disposition') ?? '';
|
||||
const match = disposition.match(/filename="?([^";]+)"?/i);
|
||||
if (!match || !match[1]) {
|
||||
return fallback;
|
||||
}
|
||||
return match[1];
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads documents from the backend list endpoint.
|
||||
*/
|
||||
export async function listDocuments(options?: {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
includeTrashed?: boolean;
|
||||
onlyTrashed?: boolean;
|
||||
pathPrefix?: string;
|
||||
pathFilter?: string;
|
||||
tagFilter?: string;
|
||||
typeFilter?: string;
|
||||
processedFrom?: string;
|
||||
processedTo?: string;
|
||||
}): Promise<DocumentListResponse> {
|
||||
const query = buildQuery({
|
||||
limit: options?.limit ?? 100,
|
||||
offset: options?.offset ?? 0,
|
||||
include_trashed: options?.includeTrashed,
|
||||
only_trashed: options?.onlyTrashed,
|
||||
path_prefix: options?.pathPrefix,
|
||||
path_filter: options?.pathFilter,
|
||||
tag_filter: options?.tagFilter,
|
||||
type_filter: options?.typeFilter,
|
||||
processed_from: options?.processedFrom,
|
||||
processed_to: options?.processedTo,
|
||||
});
|
||||
const response = await fetch(`${API_BASE}/documents${query}`);
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to load documents');
|
||||
}
|
||||
return response.json() as Promise<DocumentListResponse>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes free-text search against backend search endpoint.
|
||||
*/
|
||||
export async function searchDocuments(
|
||||
queryText: string,
|
||||
options?: {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
includeTrashed?: boolean;
|
||||
onlyTrashed?: boolean;
|
||||
pathFilter?: string;
|
||||
tagFilter?: string;
|
||||
typeFilter?: string;
|
||||
processedFrom?: string;
|
||||
processedTo?: string;
|
||||
},
|
||||
): Promise<SearchResponse> {
|
||||
const query = buildQuery({
|
||||
query: queryText,
|
||||
limit: options?.limit ?? 100,
|
||||
offset: options?.offset ?? 0,
|
||||
include_trashed: options?.includeTrashed,
|
||||
only_trashed: options?.onlyTrashed,
|
||||
path_filter: options?.pathFilter,
|
||||
tag_filter: options?.tagFilter,
|
||||
type_filter: options?.typeFilter,
|
||||
processed_from: options?.processedFrom,
|
||||
processed_to: options?.processedTo,
|
||||
});
|
||||
const response = await fetch(`${API_BASE}/search${query}`);
|
||||
if (!response.ok) {
|
||||
throw new Error('Search failed');
|
||||
}
|
||||
return response.json() as Promise<SearchResponse>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads processing logs for recent upload, OCR, summarization, routing, and indexing steps.
|
||||
*/
|
||||
export async function listProcessingLogs(options?: {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
documentId?: string;
|
||||
}): Promise<ProcessingLogListResponse> {
|
||||
const query = buildQuery({
|
||||
limit: options?.limit ?? 120,
|
||||
offset: options?.offset ?? 0,
|
||||
document_id: options?.documentId,
|
||||
});
|
||||
const response = await fetch(`${API_BASE}/processing/logs${query}`);
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to load processing logs');
|
||||
}
|
||||
return response.json() as Promise<ProcessingLogListResponse>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Trims persisted processing logs while keeping recent document sessions.
|
||||
*/
|
||||
export async function trimProcessingLogs(options?: {
|
||||
keepDocumentSessions?: number;
|
||||
keepUnboundEntries?: number;
|
||||
}): Promise<{ deleted_document_entries: number; deleted_unbound_entries: number }> {
|
||||
const query = buildQuery({
|
||||
keep_document_sessions: options?.keepDocumentSessions ?? 2,
|
||||
keep_unbound_entries: options?.keepUnboundEntries ?? 80,
|
||||
});
|
||||
const response = await fetch(`${API_BASE}/processing/logs/trim${query}`, {
|
||||
method: 'POST',
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to trim processing logs');
|
||||
}
|
||||
return response.json() as Promise<{ deleted_document_entries: number; deleted_unbound_entries: number }>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all persisted processing logs.
|
||||
*/
|
||||
export async function clearProcessingLogs(): Promise<{ deleted_entries: number }> {
|
||||
const response = await fetch(`${API_BASE}/processing/logs/clear`, {
|
||||
method: 'POST',
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to clear processing logs');
|
||||
}
|
||||
return response.json() as Promise<{ deleted_entries: number }>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns existing tags for suggestion UIs.
|
||||
*/
|
||||
export async function listTags(includeTrashed = false): Promise<string[]> {
|
||||
const query = buildQuery({ include_trashed: includeTrashed });
|
||||
const response = await fetch(`${API_BASE}/documents/tags${query}`);
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to load tags');
|
||||
}
|
||||
const payload = (await response.json()) as { tags: string[] };
|
||||
return payload.tags;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns existing logical paths for suggestion UIs.
|
||||
*/
|
||||
export async function listPaths(includeTrashed = false): Promise<string[]> {
|
||||
const query = buildQuery({ include_trashed: includeTrashed });
|
||||
const response = await fetch(`${API_BASE}/documents/paths${query}`);
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to load paths');
|
||||
}
|
||||
const payload = (await response.json()) as { paths: string[] };
|
||||
return payload.paths;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns distinct type values from extension, MIME, and image text categories.
|
||||
*/
|
||||
export async function listTypes(includeTrashed = false): Promise<string[]> {
|
||||
const query = buildQuery({ include_trashed: includeTrashed });
|
||||
const response = await fetch(`${API_BASE}/documents/types${query}`);
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to load document types');
|
||||
}
|
||||
const payload = (await response.json()) as TypeListResponse;
|
||||
return payload.types;
|
||||
}
|
||||
|
||||
/**
|
||||
* Uploads files with optional logical path and tags.
|
||||
*/
|
||||
export async function uploadDocuments(
|
||||
files: File[],
|
||||
options: {
|
||||
logicalPath: string;
|
||||
tags: string;
|
||||
conflictMode: 'ask' | 'replace' | 'duplicate';
|
||||
},
|
||||
): Promise<UploadResponse> {
|
||||
const formData = new FormData();
|
||||
files.forEach((file) => {
|
||||
formData.append('files', file, file.name);
|
||||
const relativePath = (file as File & { webkitRelativePath?: string }).webkitRelativePath ?? file.name;
|
||||
formData.append('relative_paths', relativePath);
|
||||
});
|
||||
formData.append('logical_path', options.logicalPath);
|
||||
formData.append('tags', options.tags);
|
||||
formData.append('conflict_mode', options.conflictMode);
|
||||
|
||||
const response = await fetch(`${API_BASE}/documents/upload`, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error('Upload failed');
|
||||
}
|
||||
return response.json() as Promise<UploadResponse>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates document metadata and optionally trains routing suggestions.
|
||||
*/
|
||||
export async function updateDocumentMetadata(
|
||||
documentId: string,
|
||||
payload: { original_filename?: string; logical_path?: string; tags?: string[] },
|
||||
): Promise<DmsDocument> {
|
||||
const response = await fetch(`${API_BASE}/documents/${documentId}`, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to update document metadata');
|
||||
}
|
||||
return response.json() as Promise<DmsDocument>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves a document to trash state without removing stored files.
|
||||
*/
|
||||
export async function trashDocument(documentId: string): Promise<DmsDocument> {
|
||||
const response = await fetch(`${API_BASE}/documents/${documentId}/trash`, { method: 'POST' });
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to trash document');
|
||||
}
|
||||
return response.json() as Promise<DmsDocument>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores a document from trash to active state.
|
||||
*/
|
||||
export async function restoreDocument(documentId: string): Promise<DmsDocument> {
|
||||
const response = await fetch(`${API_BASE}/documents/${documentId}/restore`, { method: 'POST' });
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to restore document');
|
||||
}
|
||||
return response.json() as Promise<DmsDocument>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Permanently deletes a document record and associated stored files.
|
||||
*/
|
||||
export async function deleteDocument(documentId: string): Promise<{ deleted_documents: number; deleted_files: number }> {
|
||||
const response = await fetch(`${API_BASE}/documents/${documentId}`, { method: 'DELETE' });
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to delete document');
|
||||
}
|
||||
return response.json() as Promise<{ deleted_documents: number; deleted_files: number }>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads full details for one document, including extracted text content.
|
||||
*/
|
||||
export async function getDocumentDetails(documentId: string): Promise<DmsDocumentDetail> {
|
||||
const response = await fetch(`${API_BASE}/documents/${documentId}`);
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to load document details');
|
||||
}
|
||||
return response.json() as Promise<DmsDocumentDetail>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-enqueues one document for extraction and classification processing.
|
||||
*/
|
||||
export async function reprocessDocument(documentId: string): Promise<DmsDocument> {
|
||||
const response = await fetch(`${API_BASE}/documents/${documentId}/reprocess`, {
|
||||
method: 'POST',
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to reprocess document');
|
||||
}
|
||||
return response.json() as Promise<DmsDocument>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds preview URL for a specific document.
|
||||
*/
|
||||
export function previewUrl(documentId: string): string {
|
||||
return `${API_BASE}/documents/${documentId}/preview`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds thumbnail URL for dashboard card rendering.
|
||||
*/
|
||||
export function thumbnailUrl(documentId: string): string {
|
||||
return `${API_BASE}/documents/${documentId}/thumbnail`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds download URL for a specific document.
|
||||
*/
|
||||
export function downloadUrl(documentId: string): string {
|
||||
return `${API_BASE}/documents/${documentId}/download`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds direct markdown-content download URL for one document.
|
||||
*/
|
||||
export function contentMarkdownUrl(documentId: string): string {
|
||||
return `${API_BASE}/documents/${documentId}/content-md`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Exports extracted content markdown files for selected documents or path filters.
|
||||
*/
|
||||
export async function exportContentsMarkdown(payload: {
|
||||
document_ids?: string[];
|
||||
path_prefix?: string;
|
||||
include_trashed?: boolean;
|
||||
only_trashed?: boolean;
|
||||
}): Promise<{ blob: Blob; filename: string }> {
|
||||
const response = await fetch(`${API_BASE}/documents/content-md/export`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to export markdown contents');
|
||||
}
|
||||
const blob = await response.blob();
|
||||
return {
|
||||
blob,
|
||||
filename: responseFilename(response, 'document-contents-md.zip'),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves persisted application settings from backend.
|
||||
*/
|
||||
export async function getAppSettings(): Promise<AppSettings> {
|
||||
const response = await fetch(`${API_BASE}/settings`);
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to load application settings');
|
||||
}
|
||||
return response.json() as Promise<AppSettings>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates provider and task settings for OpenAI-compatible model execution.
|
||||
*/
|
||||
export async function updateAppSettings(payload: AppSettingsUpdate): Promise<AppSettings> {
|
||||
const response = await fetch(`${API_BASE}/settings`, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to update settings');
|
||||
}
|
||||
return response.json() as Promise<AppSettings>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets persisted provider and task settings to backend defaults.
|
||||
*/
|
||||
export async function resetAppSettings(): Promise<AppSettings> {
|
||||
const response = await fetch(`${API_BASE}/settings/reset`, {
|
||||
method: 'POST',
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to reset settings');
|
||||
}
|
||||
return response.json() as Promise<AppSettings>;
|
||||
}
|
||||
Reference in New Issue
Block a user