Claude
fix: comprehensive repo audit β€” 15 issues fixed
9097545 unverified
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)}`)