CAPS04-FE-REFACTOR / resources /js /lib /rag-client.ts
wepee's picture
fix: admin dashboard endpoint
45e7f4a
Raw
History Blame Contribute Delete
14.4 kB
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,
};
}