Initial commit

This commit is contained in:
2026-02-21 09:44:18 -03:00
commit 5dfc2cbd85
65 changed files with 11989 additions and 0 deletions

411
frontend/src/lib/api.ts Normal file
View 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>;
}