Spaces:
Sleeping
Sleeping
| 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<string, string> = {}, | |
| ): Record<string, string> { | |
| const token = getAuthToken(); | |
| return { | |
| Accept: 'application/json', | |
| ...(token ? { Authorization: `Bearer ${token}` } : {}), | |
| ...extra, | |
| }; | |
| } | |
| async function buildRagError(response: Response): Promise<RagApiError> { | |
| 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<string, unknown>; | |
| 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<T>(path: string): Promise<T> { | |
| const response = await fetch(`${BASE_URL}${path}`, { | |
| headers: makeHeaders(), | |
| }); | |
| if (!response.ok) { | |
| throw await buildRagError(response); | |
| } | |
| return response.json() as Promise<T>; | |
| } | |
| async function ragJson<T>( | |
| path: string, | |
| method: string, | |
| body: unknown, | |
| ): Promise<T> { | |
| 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<T>; | |
| } | |
| async function ragDelete(path: string): Promise<void> { | |
| 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<CourseResponse> { | |
| return ragGet(`/courses/${encodeURIComponent(courseId)}`); | |
| } | |
| export function createCourse(payload: { | |
| title: string; | |
| description?: string; | |
| }): Promise<CourseResponse> { | |
| return ragJson('/courses', 'POST', payload); | |
| } | |
| export function updateCourse( | |
| courseId: string, | |
| payload: { title?: string; description?: string }, | |
| ): Promise<CourseResponse> { | |
| return ragJson(`/courses/${encodeURIComponent(courseId)}`, 'PUT', payload); | |
| } | |
| export function deleteCourse(courseId: string): Promise<void> { | |
| 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<void> { | |
| return ragDelete( | |
| `/courses/${encodeURIComponent(courseId)}/documents/${encodeURIComponent(documentId)}`, | |
| ); | |
| } | |
| export async function uploadDocument( | |
| courseId: string, | |
| file: File, | |
| ): Promise<void> { | |
| 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<ChatSessionResponse> { | |
| return ragGet(`/chats/sessions/${encodeURIComponent(sessionId)}`); | |
| } | |
| export function createChatSession(payload: { | |
| course_id: string; | |
| title: string; | |
| }): Promise<ChatSessionResponse> { | |
| return ragJson('/chats/sessions', 'POST', payload); | |
| } | |
| export function deleteChatSession(sessionId: string): Promise<void> { | |
| 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<Response> { | |
| 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<string, unknown>, | |
| ): 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<RagConfig> { | |
| return ragGet('/admin/rag/config'); | |
| } | |
| export function getLlmConfig(): Promise<LlmConfig> { | |
| return ragGet('/admin/rag/config/llm'); | |
| } | |
| export function patchLlmConfig( | |
| payload: Record<string, unknown>, | |
| ): Promise<LlmConfig> { | |
| return ragJson('/admin/rag/config/llm', 'PATCH', payload); | |
| } | |
| export function getVectorDbConfig(): Promise<VectorDbConfig> { | |
| return ragGet('/admin/rag/config/vector_db'); | |
| } | |
| export function patchVectorDbConfig( | |
| payload: Record<string, unknown>, | |
| ): Promise<VectorDbConfig> { | |
| return ragJson('/admin/rag/config/vector_db', 'PATCH', payload); | |
| } | |
| export function getRetrievalConfig(): Promise<RetrievalConfig> { | |
| return ragGet('/admin/rag/config/retrieval'); | |
| } | |
| export function patchRetrievalConfig( | |
| payload: Record<string, unknown>, | |
| ): Promise<RetrievalConfig> { | |
| return ragJson('/admin/rag/config/retrieval', 'PATCH', payload); | |
| } | |
| // Admin: API Keys | |
| export function generateApiKey(): Promise<GeneratedApiKey> { | |
| return ragJson('/auth/api-keys', 'POST', {}); | |
| } | |
| // Admin: Users | |
| export function listAdminUsers( | |
| query: Record<string, string> = {}, | |
| ): Promise<{ founds?: AdminUser[] }> { | |
| const params = new URLSearchParams(query).toString(); | |
| const path = params ? `/user?${params}` : '/user'; | |
| return ragGet(path); | |
| } | |
| export async function createAdminUser( | |
| payload: Record<string, unknown>, | |
| ): Promise<AdminUser> { | |
| const response = await ragJson<{ message: string; user: AdminUser }>( | |
| '/auth/register', | |
| 'POST', | |
| payload, | |
| ); | |
| return response.user; | |
| } | |
| export function updateAdminUser( | |
| userId: number | string, | |
| payload: Record<string, unknown>, | |
| ): Promise<AdminUser> { | |
| return ragJson(`/user/${encodeURIComponent(String(userId))}`, 'PATCH', payload); | |
| } | |
| export function deleteAdminUser(userId: number | string): Promise<void> { | |
| return ragDelete(`/user/${encodeURIComponent(String(userId))}`); | |
| } | |
| // Embed: internal helpers (explicit token, not from cookie) | |
| function makeEmbedHeaders( | |
| token: string, | |
| extra: Record<string, string> = {}, | |
| ): Record<string, string> { | |
| return { | |
| Accept: 'application/json', | |
| Authorization: `Bearer ${token}`, | |
| ...extra, | |
| }; | |
| } | |
| async function embedGet<T>(path: string, token: string): Promise<T> { | |
| const response = await fetch(`${BASE_URL}${path}`, { | |
| headers: makeEmbedHeaders(token), | |
| }); | |
| if (!response.ok) { | |
| throw await buildRagError(response); | |
| } | |
| return response.json() as Promise<T>; | |
| } | |
| async function embedJson<T>( | |
| path: string, | |
| method: string, | |
| body: unknown, | |
| token: string, | |
| ): Promise<T> { | |
| 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<T>; | |
| } | |
| async function embedDelete(path: string, token: string): Promise<void> { | |
| 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<ChatSessionResponse> { | |
| return embedJson('/chats/sessions', 'POST', payload, token); | |
| } | |
| export function deleteChatSessionEmbed( | |
| sessionId: string, | |
| token: string, | |
| ): Promise<void> { | |
| return embedDelete( | |
| `/chats/sessions/${encodeURIComponent(sessionId)}`, | |
| token, | |
| ); | |
| } | |
| export function sendRestChatMessage( | |
| sessionId: string, | |
| content: string, | |
| token: string, | |
| ): Promise<ChatMessageResponse> { | |
| return embedJson( | |
| `/chats/sessions/${encodeURIComponent(sessionId)}/messages`, | |
| 'POST', | |
| { content }, | |
| token, | |
| ); | |
| } | |
| export async function queryAiDirect( | |
| courseId: string, | |
| prompt: string, | |
| token: string, | |
| ): Promise<ChatMessageResponse> { | |
| type DirectResponse = { | |
| message_id: string; | |
| role?: string; | |
| content: string; | |
| sources?: DocumentSource[]; | |
| created_at: string; | |
| }; | |
| const data = await embedJson<DirectResponse>( | |
| '/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, | |
| }; | |
| } | |