Spaces:
Sleeping
Sleeping
| import type { | |
| ChatResponse, | |
| DeleteResponse, | |
| DocumentsResponse, | |
| IngestResponse, | |
| } from "./types"; | |
| // FastAPI backend base URL for the admin endpoints (called directly from the | |
| // browser; the backend enables CORS for this origin). Exposed to the client, | |
| // so it must be prefixed with NEXT_PUBLIC_. | |
| const API_BASE = ( | |
| process.env.NEXT_PUBLIC_API_URL || "http://127.0.0.1:8080" | |
| ).replace(/\/+$/, ""); | |
| const configuredIngestTimeoutMs = Number( | |
| process.env.NEXT_PUBLIC_INGEST_TIMEOUT_MS || 600_000, | |
| ); | |
| const INGEST_TIMEOUT_MS = | |
| Number.isFinite(configuredIngestTimeoutMs) && configuredIngestTimeoutMs > 0 | |
| ? configuredIngestTimeoutMs | |
| : 600_000; | |
| /** Parse a JSON response, throwing a readable error on failure. */ | |
| async function jsonOrThrow<T>(res: Response): Promise<T> { | |
| if (!res.ok) { | |
| let message = `Yêu cầu thất bại (mã ${res.status})`; | |
| try { | |
| const body = await res.json(); | |
| // Next.js routes use { error }, FastAPI uses { detail }. | |
| message = body?.error || body?.detail || message; | |
| } catch { | |
| // non-JSON error body — keep the status message | |
| } | |
| throw new Error(message); | |
| } | |
| return res.json() as Promise<T>; | |
| } | |
| /** Chat goes through the Next.js proxy (`/api/chat` → backend `POST /chat`). | |
| * Accepts an optional AbortSignal so the store can cancel an in-flight request | |
| * when its session is deleted. */ | |
| export async function sendChat( | |
| input: { | |
| user_id: string; | |
| session_id: string; | |
| query: string; | |
| }, | |
| signal?: AbortSignal, | |
| ): Promise<ChatResponse> { | |
| const res = await fetch("/api/chat", { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify(input), | |
| signal, | |
| }); | |
| return jsonOrThrow<ChatResponse>(res); | |
| } | |
| export async function listDocuments(): Promise<DocumentsResponse> { | |
| const res = await fetch(`${API_BASE}/documents`, { cache: "no-store" }); | |
| return jsonOrThrow<DocumentsResponse>(res); | |
| } | |
| export async function ingestPdf(file: File): Promise<IngestResponse> { | |
| const form = new FormData(); | |
| form.append("file", file); | |
| try { | |
| const res = await fetch(`${API_BASE}/ingest`, { | |
| method: "POST", | |
| body: form, | |
| signal: AbortSignal.timeout(INGEST_TIMEOUT_MS), | |
| }); | |
| return jsonOrThrow<IngestResponse>(res); | |
| } catch (err) { | |
| if (err instanceof Error && err.name === "TimeoutError") { | |
| const minutes = Math.round(INGEST_TIMEOUT_MS / 60_000); | |
| throw new Error( | |
| `Backend xử lý upload quá lâu (quá ${minutes} phút). Thử tăng NEXT_PUBLIC_INGEST_TIMEOUT_MS nếu tệp lớn hoặc Qdrant Cloud chậm.`, | |
| ); | |
| } | |
| throw err; | |
| } | |
| } | |
| export async function deleteDocument(id: string): Promise<DeleteResponse> { | |
| const res = await fetch(`${API_BASE}/documents/${encodeURIComponent(id)}`, { | |
| method: "DELETE", | |
| }); | |
| return jsonOrThrow<DeleteResponse>(res); | |
| } | |