import type { ChatMessageResponse, ChatSessionResponse, CourseResponse, DocumentSource, } from '@/lib/rag'; import type { AdminUser, GeneratedApiKey, LlmConfig, RagConfig, RetrievalConfig, VectorDbConfig, } from '@/types/admin'; export type { ChatMessageResponse, ChatSessionResponse, CourseResponse }; export type DocumentResponse = { id: string; filename: string; file_type: string; file_size: number; status: 'processing' | 'ready' | 'failed'; course_id: string; course_name?: string; uploaded_by: string; uploader_name?: string; summary?: string; chunk_count?: number; error?: string; created_at: string; updated_at: string; }; export type AuthUser = { id?: number | string; name?: string; email?: string; role?: string; is_active?: boolean; is_superuser?: boolean; [key: string]: unknown; }; export class RagApiError extends Error { constructor( message: string, public readonly status: number, public readonly data?: unknown, ) { super(message); this.name = 'RagApiError'; } } const BASE_URL = (import.meta.env.VITE_RAG_API_BASE_URL as string | undefined)?.replace( /\/$/, '', ) ?? ''; function getAuthToken(): string | null { if (typeof document === 'undefined') { return null; } const match = document.cookie.match( /(?:^|;\s*)sevima_raghub_auth_token=([^;]*)/, ); return match ? decodeURIComponent(match[1]) : null; } function makeHeaders( extra: Record = {}, ): Record { const token = getAuthToken(); return { Accept: 'application/json', ...(token ? { Authorization: `Bearer ${token}` } : {}), ...extra, }; } async function buildRagError(response: Response): Promise { let body: unknown = null; let message = `${response.status} ${response.statusText}`; if (response.status === 429) { message = 'Terlalu banyak percobaan. Silakan tunggu beberapa saat sebelum mencoba lagi.'; } try { body = await response.json(); if (body !== null && typeof body === 'object') { const b = body as Record; if (typeof b.detail === 'string' && b.detail) { message = b.detail; } else if (typeof b.message === 'string' && b.message) { message = b.message; } else if (typeof b.error === 'string' && b.error) { message = b.error; } } } catch { // ignore } return new RagApiError(message, response.status, body); } async function ragGet(path: string): Promise { const response = await fetch(`${BASE_URL}${path}`, { headers: makeHeaders(), }); if (!response.ok) { throw await buildRagError(response); } return response.json() as Promise; } async function ragJson( path: string, method: string, body: unknown, ): Promise { const response = await fetch(`${BASE_URL}${path}`, { body: JSON.stringify(body), headers: makeHeaders({ 'Content-Type': 'application/json' }), method, }); if (!response.ok) { throw await buildRagError(response); } return response.json() as Promise; } async function ragDelete(path: string): Promise { const response = await fetch(`${BASE_URL}${path}`, { headers: makeHeaders(), method: 'DELETE', }); if (!response.ok) { throw await buildRagError(response); } } // Courses export function listCourses( page = 1, limit = 100, ): Promise<{ data: CourseResponse[] }> { return ragGet(`/courses?page=${page}&limit=${limit}`); } export function getCourse(courseId: string): Promise { return ragGet(`/courses/${encodeURIComponent(courseId)}`); } export function createCourse(payload: { title: string; description?: string; }): Promise { return ragJson('/courses', 'POST', payload); } export function updateCourse( courseId: string, payload: { title?: string; description?: string }, ): Promise { return ragJson(`/courses/${encodeURIComponent(courseId)}`, 'PUT', payload); } export function deleteCourse(courseId: string): Promise { return ragDelete(`/courses/${encodeURIComponent(courseId)}`); } export function listCourseDocuments( courseId: string, ): Promise<{ data: DocumentResponse[] }> { return ragGet(`/courses/${encodeURIComponent(courseId)}/documents`); } export function deleteDocument( courseId: string, documentId: string, ): Promise { return ragDelete( `/courses/${encodeURIComponent(courseId)}/documents/${encodeURIComponent(documentId)}`, ); } export async function uploadDocument( courseId: string, file: File, ): Promise { const token = getAuthToken(); const formData = new FormData(); formData.append('file', file); formData.append('course_id', courseId); const response = await fetch(`${BASE_URL}/documents/upload`, { body: formData, headers: { ...(token ? { Authorization: `Bearer ${token}` } : {}), }, method: 'POST', }); if (!response.ok) { throw await buildRagError(response); } } // Chat Sessions export function listChatSessions( page = 1, limit = 20, ): Promise<{ data: ChatSessionResponse[] }> { return ragGet(`/chats/sessions?page=${page}&limit=${limit}`); } export function getChatSession( sessionId: string, ): Promise { return ragGet(`/chats/sessions/${encodeURIComponent(sessionId)}`); } export function createChatSession(payload: { course_id: string; title: string; }): Promise { return ragJson('/chats/sessions', 'POST', payload); } export function deleteChatSession(sessionId: string): Promise { return ragDelete(`/chats/sessions/${encodeURIComponent(sessionId)}`); } export function getChatHistory( sessionId: string, ): Promise<{ data: ChatMessageResponse[] }> { return ragGet( `/chats/sessions/${encodeURIComponent(sessionId)}/messages`, ); } export async function openStreamChatMessage( sessionId: string, content: string, ): Promise { const token = getAuthToken(); const response = await fetch( `${BASE_URL}/chats/sessions/${encodeURIComponent(sessionId)}/stream`, { body: JSON.stringify({ content }), headers: { Accept: 'text/event-stream', 'Content-Type': 'application/json', ...(token ? { Authorization: `Bearer ${token}` } : {}), }, method: 'POST', }, ); if (!response.ok) { throw await buildRagError(response); } return response; } // Auth export async function loginUser( email: string, password: string, ): Promise<{ token: string; user: AuthUser }> { return ragJson('/auth/login', 'POST', { email, password }); } export async function registerUser( payload: Record, ): Promise<{ message?: string }> { return ragJson('/auth/register', 'POST', payload); } function setRawCookie(name: string, value: string, remember: boolean): void { const secure = typeof window !== 'undefined' && window.location.protocol === 'https:'; const maxAge = remember ? 60 * 60 * 24 * 30 : undefined; const parts = [ `${name}=${encodeURIComponent(value)}`, 'path=/', 'SameSite=Lax', ]; if (secure) { parts.push('Secure'); } if (maxAge !== undefined) { parts.push(`max-age=${maxAge}`); } if (typeof document !== 'undefined') { document.cookie = parts.join('; '); } } export function setAuthCookies( token: string, user: AuthUser, remember: boolean, ): void { // Token cookie — dibutuhkan middleware server untuk autentikasi setRawCookie('sevima_raghub_auth_token', token, remember); // User cookie — hanya simpan role, cukup untuk pengecekan middleware server // Full user disimpan di localStorage agar tidak membebani request header setRawCookie('sevima_raghub_auth_user', JSON.stringify({ role: user.role }), remember); // Full user disimpan di storage lokal (tidak dikirim sebagai cookie/header) const storage = remember ? window.localStorage : window.sessionStorage; storage.setItem('sevima_raghub_auth_user', JSON.stringify(user)); } // Admin: RAG Config export function getRagConfig(): Promise { return ragGet('/admin/rag/config'); } export function getLlmConfig(): Promise { return ragGet('/admin/rag/config/llm'); } export function patchLlmConfig( payload: Record, ): Promise { return ragJson('/admin/rag/config/llm', 'PATCH', payload); } export function getVectorDbConfig(): Promise { return ragGet('/admin/rag/config/vector_db'); } export function patchVectorDbConfig( payload: Record, ): Promise { return ragJson('/admin/rag/config/vector_db', 'PATCH', payload); } export function getRetrievalConfig(): Promise { return ragGet('/admin/rag/config/retrieval'); } export function patchRetrievalConfig( payload: Record, ): Promise { return ragJson('/admin/rag/config/retrieval', 'PATCH', payload); } // Admin: API Keys export function generateApiKey(): Promise { return ragJson('/auth/api-keys', 'POST', {}); } // Admin: Users export function listAdminUsers( query: Record = {}, ): Promise<{ founds?: AdminUser[] }> { const params = new URLSearchParams(query).toString(); const path = params ? `/user?${params}` : '/user'; return ragGet(path); } export async function createAdminUser( payload: Record, ): Promise { const response = await ragJson<{ message: string; user: AdminUser }>( '/auth/register', 'POST', payload, ); return response.user; } export function updateAdminUser( userId: number | string, payload: Record, ): Promise { return ragJson(`/user/${encodeURIComponent(String(userId))}`, 'PATCH', payload); } export function deleteAdminUser(userId: number | string): Promise { return ragDelete(`/user/${encodeURIComponent(String(userId))}`); } // Embed: internal helpers (explicit token, not from cookie) function makeEmbedHeaders( token: string, extra: Record = {}, ): Record { return { Accept: 'application/json', Authorization: `Bearer ${token}`, ...extra, }; } async function embedGet(path: string, token: string): Promise { const response = await fetch(`${BASE_URL}${path}`, { headers: makeEmbedHeaders(token), }); if (!response.ok) { throw await buildRagError(response); } return response.json() as Promise; } async function embedJson( path: string, method: string, body: unknown, token: string, ): Promise { const response = await fetch(`${BASE_URL}${path}`, { body: JSON.stringify(body), headers: makeEmbedHeaders(token, { 'Content-Type': 'application/json' }), method, }); if (!response.ok) { throw await buildRagError(response); } return response.json() as Promise; } async function embedDelete(path: string, token: string): Promise { const response = await fetch(`${BASE_URL}${path}`, { headers: makeEmbedHeaders(token), method: 'DELETE', }); if (!response.ok) { throw await buildRagError(response); } } // Embed: Auth export function loginWithEmbedApiKey( apiKey: string, ): Promise<{ token: string; user: AuthUser }> { return ragJson('/auth/login/api-key', 'POST', { api_key: apiKey }); } // Embed: API export function listCoursesEmbed( token: string, page = 1, limit = 100, ): Promise<{ data: CourseResponse[] }> { return embedGet(`/courses?page=${page}&limit=${limit}`, token); } export function listChatSessionsEmbed( token: string, courseId?: string, page = 1, limit = 50, ): Promise<{ data: ChatSessionResponse[] }> { const params = new URLSearchParams({ limit: String(limit), page: String(page), }); if (courseId) { params.set('course_id', courseId); } return embedGet(`/chats/sessions?${params.toString()}`, token); } export function getChatHistoryEmbed( sessionId: string, token: string, ): Promise<{ data: ChatMessageResponse[] }> { return embedGet( `/chats/sessions/${encodeURIComponent(sessionId)}/messages`, token, ); } export function createChatSessionEmbed( payload: { course_id: string; title: string }, token: string, ): Promise { return embedJson('/chats/sessions', 'POST', payload, token); } export function deleteChatSessionEmbed( sessionId: string, token: string, ): Promise { return embedDelete( `/chats/sessions/${encodeURIComponent(sessionId)}`, token, ); } export function sendRestChatMessage( sessionId: string, content: string, token: string, ): Promise { return embedJson( `/chats/sessions/${encodeURIComponent(sessionId)}/messages`, 'POST', { content }, token, ); } export async function queryAiDirect( courseId: string, prompt: string, token: string, ): Promise { type DirectResponse = { message_id: string; role?: string; content: string; sources?: DocumentSource[]; created_at: string; }; const data = await embedJson( '/ai/query', 'POST', { course_id: courseId, prompt }, token, ); return { content: data.content, created_at: data.created_at, role: data.role ?? 'assistant', sources: data.sources ?? [], uuid_id: data.message_id, }; }