Spaces:
Build error
Build error
| const BASE_URL: string = import.meta.env.VITE_API_URL ?? '' | |
| if (!BASE_URL && import.meta.env.PROD) { | |
| console.warn( | |
| '[IIIF-Studio] VITE_API_URL non dΓ©fini en production. ' + | |
| 'Les appels API utiliseront des chemins relatifs, ce qui peut Γ©chouer ' + | |
| 'si le frontend n\'est pas servi par le mΓͺme domaine que le backend.' | |
| ) | |
| } | |
| // ββ Types βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| export interface ProviderInfo { | |
| provider_type: string | |
| display_name: string | |
| available: boolean | |
| model_count: number | |
| } | |
| export interface ModelInfo { | |
| model_id: string | |
| display_name: string | |
| provider: string | |
| supports_vision: boolean | |
| input_token_limit: number | null | |
| output_token_limit: number | null | |
| } | |
| export interface IngestResponse { | |
| corpus_id: string | |
| manuscript_id: string | |
| pages_created: number | |
| pages_skipped: number | |
| page_ids: string[] | |
| } | |
| export interface CorpusRunResponse { | |
| corpus_id: string | |
| jobs_created: number | |
| job_ids: string[] | |
| } | |
| export type JobStatus = 'pending' | 'claimed' | 'running' | 'done' | 'failed' | |
| export interface Job { | |
| id: string | |
| corpus_id: string | |
| page_id: string | null | |
| status: JobStatus | |
| started_at: string | null | |
| finished_at: string | null | |
| error_message: string | null | |
| created_at: string | |
| } | |
| export interface CreateCorpusInput { | |
| slug: string | |
| title: string | |
| profile_id: string | |
| } | |
| export interface Corpus { | |
| id: string | |
| slug: string | |
| title: string | |
| profile_id: string | |
| created_at: string | |
| updated_at: string | |
| } | |
| export interface Manuscript { | |
| id: string | |
| corpus_id: string | |
| title: string | |
| shelfmark: string | null | |
| date_label: string | null | |
| total_pages: number | |
| } | |
| export interface Page { | |
| id: string | |
| manuscript_id: string | |
| folio_label: string | |
| sequence: number | |
| image_master_path: string | null | |
| iiif_service_url: string | null | |
| canvas_width: number | null | |
| canvas_height: number | null | |
| manifest_url: string | null | |
| processing_status: string | |
| confidence_summary: number | null | |
| } | |
| export type RegionType = | |
| | 'text_block' | |
| | 'miniature' | |
| | 'decorated_initial' | |
| | 'margin' | |
| | 'rubric' | |
| | 'other' | |
| export interface Region { | |
| id: string | |
| type: RegionType | |
| bbox: [number, number, number, number] | |
| confidence: number | |
| polygon?: number[][] | null | |
| parent_region_id?: string | null | |
| } | |
| export interface OCRResult { | |
| diplomatic_text: string | |
| blocks: Record<string, unknown>[] | |
| lines: Record<string, unknown>[] | |
| language: string | |
| confidence: number | |
| uncertain_segments: string[] | |
| } | |
| export interface Translation { | |
| fr: string | |
| en: string | |
| } | |
| export interface CommentaryClaim { | |
| claim: string | |
| evidence_region_ids: string[] | |
| certainty: 'high' | 'medium' | 'low' | 'speculative' | |
| } | |
| export interface Commentary { | |
| public: string | |
| scholarly: string | |
| claims: CommentaryClaim[] | |
| } | |
| export type EditorialStatus = | |
| | 'machine_draft' | |
| | 'needs_review' | |
| | 'reviewed' | |
| | 'validated' | |
| | 'published' | |
| export interface EditorialInfo { | |
| status: EditorialStatus | |
| validated: boolean | |
| validated_by: string | null | |
| version: number | |
| notes: string[] | |
| } | |
| export interface ImageInfo { | |
| master: string | |
| derivative_web?: string | null | |
| thumbnail?: string | null | |
| iiif_service_url?: string | null | |
| manifest_url?: string | null | |
| width: number | |
| height: number | |
| } | |
| export interface PageMaster { | |
| schema_version: string | |
| page_id: string | |
| corpus_profile: string | |
| manuscript_id: string | |
| folio_label: string | |
| sequence: number | |
| image: ImageInfo | |
| layout: { regions: Region[] } | |
| ocr: OCRResult | null | |
| translation: Translation | null | |
| summary: { short: string; detailed: string } | null | |
| commentary: Commentary | null | |
| extensions: Record<string, unknown> | |
| processing: Record<string, unknown> | null | |
| editorial: EditorialInfo | |
| } | |
| export interface CorpusProfile { | |
| profile_id: string | |
| label: string | |
| language_hints: string[] | |
| script_type: string | |
| active_layers: string[] | |
| uncertainty_config: { flag_below: number; min_acceptable: number } | |
| export_config: { mets: boolean; alto: boolean; tei: boolean } | |
| } | |
| // ββ Errors ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| export class ApiError extends Error { | |
| status: number | |
| constructor(status: number, message: string) { | |
| super(message) | |
| this.name = 'ApiError' | |
| this.status = status | |
| } | |
| } | |
| // ββ Fetch helpers βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| /** | |
| * Extract a human-readable error message from a FastAPI error response. | |
| * FastAPI may return { detail: "string" } or { detail: [{loc, msg, type}, ...] } | |
| * for validation errors (422). This function handles both cases. | |
| */ | |
| function extractDetail(payload: unknown, fallback: string): string { | |
| if (!payload || typeof payload !== 'object') return fallback | |
| const detail = (payload as Record<string, unknown>).detail | |
| if (typeof detail === 'string') return detail | |
| if (Array.isArray(detail)) { | |
| // FastAPI validation error: [{loc: [...], msg: "...", type: "..."}] | |
| const messages = detail | |
| .map((e: Record<string, unknown>) => { | |
| const loc = Array.isArray(e.loc) ? e.loc.join(' β ') : '' | |
| return loc ? `${loc} : ${e.msg}` : String(e.msg ?? '') | |
| }) | |
| .filter(Boolean) | |
| return messages.length > 0 ? messages.join(' ; ') : fallback | |
| } | |
| return fallback | |
| } | |
| async function get<T>(path: string): Promise<T> { | |
| const resp = await fetch(`${BASE_URL}${path}`) | |
| if (!resp.ok) { | |
| const payload = await resp.json().catch(() => null) | |
| throw new ApiError(resp.status, extractDetail(payload, `HTTP ${resp.status} β ${path}`)) | |
| } | |
| return resp.json() as Promise<T> | |
| } | |
| async function post<T>(path: string, body?: unknown): Promise<T> { | |
| const resp = await fetch(`${BASE_URL}${path}`, { | |
| method: 'POST', | |
| headers: body !== undefined ? { 'Content-Type': 'application/json' } : {}, | |
| body: body !== undefined ? JSON.stringify(body) : undefined, | |
| }) | |
| if (!resp.ok) { | |
| const payload = await resp.json().catch(() => null) | |
| throw new Error(extractDetail(payload, `HTTP ${resp.status} β ${path}`)) | |
| } | |
| return resp.json() as Promise<T> | |
| } | |
| async function put<T>(path: string, body?: unknown): Promise<T> { | |
| const resp = await fetch(`${BASE_URL}${path}`, { | |
| method: 'PUT', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: body !== undefined ? JSON.stringify(body) : undefined, | |
| }) | |
| if (!resp.ok) { | |
| const payload = await resp.json().catch(() => null) | |
| throw new Error(extractDetail(payload, `HTTP ${resp.status} β ${path}`)) | |
| } | |
| return resp.json() as Promise<T> | |
| } | |
| async function del(path: string): Promise<void> { | |
| const resp = await fetch(`${BASE_URL}${path}`, { method: 'DELETE' }) | |
| if (!resp.ok) { | |
| const payload = await resp.json().catch(() => null) | |
| throw new Error(extractDetail(payload, `HTTP ${resp.status} β ${path}`)) | |
| } | |
| } | |
| async function postForm<T>(path: string, data: FormData): Promise<T> { | |
| const resp = await fetch(`${BASE_URL}${path}`, { method: 'POST', body: data }) | |
| if (!resp.ok) { | |
| const payload = await resp.json().catch(() => null) | |
| throw new Error(extractDetail(payload, `HTTP ${resp.status} β ${path}`)) | |
| } | |
| return resp.json() as Promise<T> | |
| } | |
| // ββ API functions βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| export const fetchCorpora = (): Promise<Corpus[]> => | |
| get('/api/v1/corpora') | |
| export const fetchManuscripts = (corpusId: string): Promise<Manuscript[]> => | |
| get(`/api/v1/corpora/${corpusId}/manuscripts`) | |
| export const fetchPages = (manuscriptId: string): Promise<Page[]> => | |
| get(`/api/v1/manuscripts/${manuscriptId}/pages`) | |
| export const fetchMasterJson = (pageId: string): Promise<PageMaster> => | |
| get(`/api/v1/pages/${pageId}/master-json`) | |
| export const fetchManifest = (manuscriptId: string): Promise<Record<string, unknown>> => | |
| get(`/api/v1/manuscripts/${manuscriptId}/iiif-manifest`) | |
| export const fetchProfile = (profileId: string): Promise<CorpusProfile> => | |
| get(`/api/v1/profiles/${profileId}`) | |
| export const listProfiles = (): Promise<CorpusProfile[]> => | |
| get('/api/v1/profiles') | |
| export const createCorpus = (input: CreateCorpusInput): Promise<Corpus> => | |
| post('/api/v1/corpora', input) | |
| export const fetchProviders = (): Promise<ProviderInfo[]> => | |
| get('/api/v1/providers') | |
| export const fetchProviderModels = (providerType: string): Promise<ModelInfo[]> => | |
| get(`/api/v1/providers/${providerType}/models`) | |
| export const selectModel = ( | |
| corpusId: string, | |
| modelId: string, | |
| displayName: string, | |
| providerType: string, | |
| supportsVision: boolean = true, | |
| ): Promise<CorpusModelConfig> => | |
| put(`/api/v1/corpora/${corpusId}/model`, { | |
| model_id: modelId, | |
| display_name: displayName, | |
| provider_type: providerType, | |
| supports_vision: supportsVision, | |
| }) | |
| export const deleteCorpus = (id: string): Promise<void> => | |
| del(`/api/v1/corpora/${id}`) | |
| export interface CorpusModelConfig { | |
| corpus_id: string | |
| selected_model_id: string | |
| selected_model_display_name: string | |
| provider_type: string | |
| supports_vision: boolean | |
| updated_at: string | |
| } | |
| export const getCorpusModel = async (corpusId: string): Promise<CorpusModelConfig | null> => { | |
| try { | |
| return await get<CorpusModelConfig>(`/api/v1/corpora/${corpusId}/model`) | |
| } catch { | |
| return null | |
| } | |
| } | |
| export const ingestImages = ( | |
| corpusId: string, | |
| urls: string[], | |
| folioLabels: string[], | |
| ): Promise<IngestResponse> => | |
| post(`/api/v1/corpora/${corpusId}/ingest/iiif-images`, { | |
| urls, | |
| folio_labels: folioLabels, | |
| }) | |
| export const ingestManifest = ( | |
| corpusId: string, | |
| manifestUrl: string, | |
| ): Promise<IngestResponse> => | |
| post(`/api/v1/corpora/${corpusId}/ingest/iiif-manifest`, { | |
| manifest_url: manifestUrl, | |
| }) | |
| export const ingestFiles = ( | |
| corpusId: string, | |
| files: File[], | |
| ): Promise<IngestResponse> => { | |
| const data = new FormData() | |
| for (const f of files) data.append('files', f) | |
| return postForm(`/api/v1/corpora/${corpusId}/ingest/files`, data) | |
| } | |
| export const runCorpus = (corpusId: string): Promise<CorpusRunResponse> => | |
| post(`/api/v1/corpora/${corpusId}/run`) | |
| export const getJob = (jobId: string): Promise<Job> => | |
| get(`/api/v1/jobs/${jobId}`) | |
| export const retryJob = (jobId: string): Promise<Job> => | |
| post(`/api/v1/jobs/${jobId}/retry`) | |
| export interface VersionInfo { | |
| version: number | |
| saved_at: string | |
| status: string | |
| } | |
| export interface CorrectionsInput { | |
| ocr_diplomatic_text?: string | |
| editorial_status?: string | |
| commentary_public?: string | |
| commentary_scholarly?: string | |
| region_validations?: Record<string, string> | |
| restore_to_version?: number | |
| } | |
| export interface SearchResult { | |
| page_id: string | |
| folio_label: string | |
| manuscript_id: string | |
| excerpt: string | |
| score: number | |
| corpus_profile: string | |
| } | |
| export const fetchPage = (pageId: string): Promise<Page> => | |
| get(`/api/v1/pages/${pageId}`) | |
| export const applyCorrections = ( | |
| pageId: string, | |
| corrections: CorrectionsInput, | |
| ): Promise<PageMaster> => | |
| post(`/api/v1/pages/${pageId}/corrections`, corrections) | |
| export const getHistory = (pageId: string): Promise<VersionInfo[]> => | |
| get(`/api/v1/pages/${pageId}/history`) | |
| export const searchPages = (q: string): Promise<SearchResult[]> => | |
| get(`/api/v1/search?q=${encodeURIComponent(q)}`) | |