api.ts (2345B)
1 export class ApiError extends Error { 2 constructor( 3 public status: number, 4 message: string, 5 ) { 6 super(message); 7 this.name = "ApiError"; 8 } 9 } 10 11 const BASE_PATH = "/api"; 12 13 async function request<T>( 14 method: string, 15 path: string, 16 body?: unknown, 17 ): Promise<T> { 18 const init: RequestInit = { 19 method, 20 credentials: "include", 21 headers: { 22 "Content-Type": "application/json", 23 }, 24 }; 25 26 if (body !== undefined) { 27 init.body = JSON.stringify(body); 28 } 29 30 const response = await fetch(`${BASE_PATH}${path}`, init); 31 32 if (response.status === 401) { 33 window.location.assign("/login"); 34 throw new ApiError(401, "Unauthorized"); 35 } 36 37 if (!response.ok) { 38 const data = await response.json().catch(() => ({ error: "Unknown error" })); 39 throw new ApiError(response.status, data.error ?? "Request failed"); 40 } 41 42 return response.json(); 43 } 44 45 export const api = { 46 get: <T>(path: string) => request<T>("GET", path), 47 post: <T>(path: string, body?: unknown) => request<T>("POST", path, body), 48 patch: <T>(path: string, body?: unknown) => request<T>("PATCH", path, body), 49 delete: <T>(path: string) => request<T>("DELETE", path), 50 }; 51 52 export interface AttachmentResponse { 53 id: string; 54 filename: string; 55 content_type: string; 56 size_bytes: number; 57 created_at: number; 58 data_url?: string; 59 } 60 61 export async function uploadAttachment( 62 file: File, 63 sessionId: string, 64 onProgress?: (percent: number) => void, 65 ): Promise<AttachmentResponse> { 66 const formData = new FormData(); 67 formData.append("file", file); 68 formData.append("session_id", sessionId); 69 70 return new Promise<AttachmentResponse>((resolve, reject) => { 71 const xhr = new XMLHttpRequest(); 72 xhr.open("POST", `${BASE_PATH}/attachments`); 73 74 xhr.upload.addEventListener("progress", (e) => { 75 if (e.lengthComputable && onProgress) { 76 onProgress(Math.round((e.loaded / e.total) * 100)); 77 } 78 }); 79 80 xhr.addEventListener("load", () => { 81 if (xhr.status >= 200 && xhr.status < 300) { 82 resolve(JSON.parse(xhr.responseText)); 83 } else { 84 reject(new ApiError(xhr.status, "Upload failed")); 85 } 86 }); 87 88 xhr.addEventListener("error", () => { 89 reject(new ApiError(0, "Network error during upload")); 90 }); 91 92 xhr.withCredentials = true; 93 xhr.send(formData); 94 }); 95 }