ishaq101 commited on
Commit
89e1114
Β·
1 Parent(s): ee46b9f

[KM-475][DED][FE] Align API services, fix document upload flow, add upload progress

Browse files

ticket: https://bukittechnology.atlassian.net/browse/KM-475

- Align api.ts and interviewApi.ts with latest API contract (endpoint paths,
request/response shapes, error envelope, DocumentStatus types)
- Revert document upload to multipart/form-data; remove putFileToAzure (SAS flow)
- Replace fetch with XMLHttpRequest in uploadDocument to support onProgress callback
- Add upload progress bar (% indicator) in KnowledgeManagement UI
- Add post-process polling: after /process returns 202, poll GET /documents
every 3s until status resolves to completed or failed
- Add timing logs for /upload and /process endpoints for performance diagnostics

src/app/components/KnowledgeManagement.tsx CHANGED
@@ -1,4 +1,4 @@
1
- import { useState, useEffect } from "react";
2
  import {
3
  Upload,
4
  Trash2,
@@ -62,6 +62,7 @@ export default function KnowledgeManagement({
62
  const [loadingDocs, setLoadingDocs] = useState(false);
63
  const [docsError, setDocsError] = useState<string | null>(null);
64
  const [uploading, setUploading] = useState(false);
 
65
  const [uploadError, setUploadError] = useState<string | null>(null);
66
  const [processing, setProcessing] = useState<string | null>(null);
67
  const [deleting, setDeleting] = useState<string | null>(null);
@@ -76,12 +77,40 @@ export default function KnowledgeManagement({
76
  const [loadingDbTypes, setLoadingDbTypes] = useState(false);
77
  const [ingesting, setIngesting] = useState<string | null>(null);
78
  const [deletingClient, setDeletingClient] = useState<string | null>(null);
 
79
 
80
  // ── DB credentials form state ───────────────────────────────────────────────
81
  const [connectionName, setConnectionName] = useState("");
82
  const [dbForm, setDbForm] = useState<Record<string, string | number | boolean>>({});
83
  const [connecting, setConnecting] = useState(false);
84
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
  useEffect(() => {
86
  if (!open) return;
87
  const userId = getUserId();
@@ -92,15 +121,15 @@ export default function KnowledgeManagement({
92
  .then((types) => setDocTypes(types.filter((t) => t.status === "active")))
93
  .catch(() =>
94
  setDocTypes([
95
- { doc_type: "pdf", max_size: 10, status: "active", message: null },
96
- { doc_type: "csv", max_size: 10, status: "active", message: null },
97
- { doc_type: "xlsx", max_size: 10, status: "active", message: null },
98
  ])
99
  );
100
  }, [open]);
101
 
102
- const acceptedExtensions = docTypes.map((t) => `.${t.doc_type}`).join(",");
103
- const supportedFormatsText = docTypes.map((t) => t.doc_type.toUpperCase()).join(", ");
104
 
105
  const loadDbData = async (userId: string) => {
106
  setLoadingDbTypes(true);
@@ -149,23 +178,25 @@ export default function KnowledgeManagement({
149
  if (!userId) return;
150
 
151
  setUploading(true);
 
152
  setUploadError(null);
153
 
154
  for (let i = 0; i < files.length; i++) {
155
  const file = files[i];
 
 
156
  try {
157
- const uploadRes = await uploadDocument(userId, file);
 
 
158
  const newDoc: ApiDocument = {
 
159
  id: uploadRes.data.id,
160
- filename: uploadRes.data.filename,
161
- status: "pending",
162
- file_size: file.size,
163
- file_type: file.name.split(".").pop() ?? "",
164
- created_at: new Date().toISOString(),
165
  };
166
  setDocuments((prev) => [newDoc, ...prev]);
167
- await processDocumentById(userId, uploadRes.data.id);
168
  } catch (err) {
 
169
  setUploadError(err instanceof Error ? err.message : "Upload failed");
170
  }
171
  }
@@ -181,13 +212,12 @@ export default function KnowledgeManagement({
181
  d.id === docId ? { ...d, status: "processing" as DocumentStatus } : d
182
  )
183
  );
 
184
  try {
 
185
  await processDocument(userId, docId);
186
- setDocuments((prev) =>
187
- prev.map((d) =>
188
- d.id === docId ? { ...d, status: "completed" as DocumentStatus } : d
189
- )
190
- );
191
  } catch {
192
  setDocuments((prev) =>
193
  prev.map((d) =>
@@ -398,11 +428,19 @@ export default function KnowledgeManagement({
398
  </div>
399
  <div className="text-center">
400
  <p className="text-sm font-medium text-slate-700">
401
- {uploading ? "Uploading…" : (
402
  <>Drop files, or <span className="text-[#FF8F00]">browse</span></>
403
  )}
404
  </p>
405
- <p className="text-xs text-slate-400 mt-0.5">{supportedFormatsText}</p>
 
 
 
 
 
 
 
 
406
  </div>
407
  <input
408
  id="file-upload"
 
1
+ import { useState, useEffect, useRef } from "react";
2
  import {
3
  Upload,
4
  Trash2,
 
62
  const [loadingDocs, setLoadingDocs] = useState(false);
63
  const [docsError, setDocsError] = useState<string | null>(null);
64
  const [uploading, setUploading] = useState(false);
65
+ const [uploadProgress, setUploadProgress] = useState<number>(0);
66
  const [uploadError, setUploadError] = useState<string | null>(null);
67
  const [processing, setProcessing] = useState<string | null>(null);
68
  const [deleting, setDeleting] = useState<string | null>(null);
 
77
  const [loadingDbTypes, setLoadingDbTypes] = useState(false);
78
  const [ingesting, setIngesting] = useState<string | null>(null);
79
  const [deletingClient, setDeletingClient] = useState<string | null>(null);
80
+ const pollingTimers = useRef<Map<string, ReturnType<typeof setInterval>>>(new Map());
81
 
82
  // ── DB credentials form state ───────────────────────────────────────────────
83
  const [connectionName, setConnectionName] = useState("");
84
  const [dbForm, setDbForm] = useState<Record<string, string | number | boolean>>({});
85
  const [connecting, setConnecting] = useState(false);
86
 
87
+ useEffect(() => {
88
+ return () => {
89
+ pollingTimers.current.forEach((timer) => clearInterval(timer));
90
+ pollingTimers.current.clear();
91
+ };
92
+ }, []);
93
+
94
+ const startPollingDocument = (userId: string, docId: string) => {
95
+ if (pollingTimers.current.has(docId)) return;
96
+ const timer = setInterval(async () => {
97
+ try {
98
+ const docs = await getDocuments(userId);
99
+ const updated = docs.find((d) => d.id === docId);
100
+ if (updated && (updated.status === "completed" || updated.status === "failed")) {
101
+ clearInterval(timer);
102
+ pollingTimers.current.delete(docId);
103
+ setDocuments((prev) =>
104
+ prev.map((d) => (d.id === docId ? { ...d, status: updated.status } : d))
105
+ );
106
+ }
107
+ } catch {
108
+ // ignore transient errors, keep polling
109
+ }
110
+ }, 3000);
111
+ pollingTimers.current.set(docId, timer);
112
+ };
113
+
114
  useEffect(() => {
115
  if (!open) return;
116
  const userId = getUserId();
 
121
  .then((types) => setDocTypes(types.filter((t) => t.status === "active")))
122
  .catch(() =>
123
  setDocTypes([
124
+ { type: "pdf", max_size_mb: 10, status: "active", message: null },
125
+ { type: "csv", max_size_mb: 10, status: "active", message: null },
126
+ { type: "xlsx", max_size_mb: 10, status: "active", message: null },
127
  ])
128
  );
129
  }, [open]);
130
 
131
+ const acceptedExtensions = docTypes.map((t) => `.${t.type}`).join(",");
132
+ const supportedFormatsText = docTypes.map((t) => t.type.toUpperCase()).join(", ");
133
 
134
  const loadDbData = async (userId: string) => {
135
  setLoadingDbTypes(true);
 
178
  if (!userId) return;
179
 
180
  setUploading(true);
181
+ setUploadProgress(0);
182
  setUploadError(null);
183
 
184
  for (let i = 0; i < files.length; i++) {
185
  const file = files[i];
186
+ console.log(`[upload] ── file selected: "${file.name}", ${(file.size / 1024).toFixed(1)} KB`);
187
+ const tClick = performance.now();
188
  try {
189
+ console.log(`[upload] ── calling uploadDocument (pre-fetch delay: ${(performance.now() - tClick).toFixed(0)} ms)`);
190
+ const uploadRes = await uploadDocument(userId, file, setUploadProgress);
191
+ console.log(`[upload] ── uploadDocument returned, updating UI`);
192
  const newDoc: ApiDocument = {
193
+ ...uploadRes.data,
194
  id: uploadRes.data.id,
 
 
 
 
 
195
  };
196
  setDocuments((prev) => [newDoc, ...prev]);
197
+ console.log(`[upload] ── done in ${(performance.now() - tClick).toFixed(0)} ms total`);
198
  } catch (err) {
199
+ console.error(`[upload] βœ— failed after ${(performance.now() - tClick).toFixed(0)} ms:`, err);
200
  setUploadError(err instanceof Error ? err.message : "Upload failed");
201
  }
202
  }
 
212
  d.id === docId ? { ...d, status: "processing" as DocumentStatus } : d
213
  )
214
  );
215
+ const tProcess = performance.now();
216
  try {
217
+ console.log(`[process] ── calling processDocument for doc: ${docId}`);
218
  await processDocument(userId, docId);
219
+ console.log(`[process] ── accepted in ${(performance.now() - tProcess).toFixed(0)} ms, polling started`);
220
+ startPollingDocument(userId, docId);
 
 
 
221
  } catch {
222
  setDocuments((prev) =>
223
  prev.map((d) =>
 
428
  </div>
429
  <div className="text-center">
430
  <p className="text-sm font-medium text-slate-700">
431
+ {uploading ? `Uploading… ${uploadProgress}%` : (
432
  <>Drop files, or <span className="text-[#FF8F00]">browse</span></>
433
  )}
434
  </p>
435
+ {uploading && (
436
+ <div className="w-32 h-1.5 bg-slate-200 rounded-full overflow-hidden mt-1.5 mx-auto">
437
+ <div
438
+ className="h-full bg-[#FF8F00] transition-all duration-300"
439
+ style={{ width: `${uploadProgress}%` }}
440
+ />
441
+ </div>
442
+ )}
443
+ {!uploading && <p className="text-xs text-slate-400 mt-0.5">{supportedFormatsText}</p>}
444
  </div>
445
  <input
446
  id="file-upload"
src/app/components/Main.tsx CHANGED
@@ -763,11 +763,11 @@ export default function Main() {
763
  )}
764
  </button>
765
  <h1 className="text-base text-slate-900 flex-1 truncate min-w-0">
766
- {currentChat?.title || "Chatbot"}
767
  </h1>
768
  <button
769
  onClick={() => setKnowledgeOpen(true)}
770
- className="flex items-center gap-2 bg-[#F59E0B] hover:bg-[#D97706] text-white px-3 py-2 rounded-lg transition-all duration-200 hover:scale-105 text-sm flex-shrink-0"
771
  >
772
  <Database className="w-4 h-4" />
773
  Knowledge
 
763
  )}
764
  </button>
765
  <h1 className="text-base text-slate-900 flex-1 truncate min-w-0">
766
+ {currentChat?.title || "Data Eyond"}
767
  </h1>
768
  <button
769
  onClick={() => setKnowledgeOpen(true)}
770
+ className="flex items-center gap-2 bg-[#F59E0B] hover:bg-[#D97706] text-white px-3 py-2 rounded-lg transition-all duration-200 hover:scale-105 active:scale-95 cursor-pointer text-sm flex-shrink-0 shadow-sm hover:shadow-md"
771
  >
772
  <Database className="w-4 h-4" />
773
  Knowledge
src/services/api.ts CHANGED
@@ -53,26 +53,39 @@ export interface RoomDetail extends Room {
53
  messages: RoomMessage[];
54
  }
55
 
56
- export type DocumentStatus = "uploaded" | "processing" | "completed" | "failed";
57
 
58
  export interface ApiDocument {
59
  id: string;
 
60
  filename: string;
 
61
  status: DocumentStatus;
62
  file_size: number;
63
  file_type: string;
64
  created_at: string;
 
 
65
  }
66
 
67
  export interface UploadDocumentResponse {
68
  status: string;
69
  message: string;
70
- data: { id: string; filename: string; status: DocumentStatus };
 
 
 
 
 
 
 
 
 
71
  }
72
 
73
  export interface DocTypeInfo {
74
- doc_type: string;
75
- max_size: number;
76
  status: "active" | "inactive";
77
  message: string | null;
78
  }
@@ -82,6 +95,8 @@ export interface DataCatalogSource {
82
  source_type: "schema" | "tabular" | "unstructured";
83
  name: string;
84
  location_ref: string;
 
 
85
  }
86
 
87
  export interface DataCatalog {
@@ -106,8 +121,8 @@ async function request<T>(baseUrl: string, path: string, options?: RequestInit):
106
  if (!res.ok) {
107
  const err = await res
108
  .json()
109
- .catch(() => ({ detail: `HTTP ${res.status}` }));
110
- throw new Error(err.detail ?? `HTTP ${res.status}`);
111
  }
112
  return res.json() as Promise<T>;
113
  }
@@ -123,22 +138,22 @@ export const login = (email: string, password: string) =>
123
  // ─── Rooms ────────────────────────────────────────────────────────────────────
124
 
125
  export const getRooms = (userId: string): Promise<Room[]> =>
126
- request<{ status: string; message: string; data: Room[] | null }>(ORCHESTRATION_BASE_URL, `/api/v1/rooms/${userId}`)
127
  .then(res => res.data ?? []);
128
 
129
  export const getRoom = (roomId: string): Promise<RoomDetail> =>
130
- request<{ status: string; message: string; data: RoomDetail }>(ORCHESTRATION_BASE_URL, `/api/v1/room/${roomId}`)
131
  .then(res => res.data);
132
 
133
  export const deleteRoom = (roomId: string, userId: string) =>
134
  request<{ status: string; message: string }>(
135
  ORCHESTRATION_BASE_URL,
136
- `/api/v1/room/${roomId}?user_id=${userId}`,
137
  { method: "DELETE" }
138
  );
139
 
140
  export const createRoom = (userId: string, title?: string) =>
141
- request<CreateRoomResponse>(ORCHESTRATION_BASE_URL, "/api/v1/room/create", {
142
  method: "POST",
143
  body: JSON.stringify({ user_id: userId, title }),
144
  });
@@ -149,38 +164,71 @@ export const getDocuments = (userId: string): Promise<ApiDocument[]> =>
149
  request<{ status: string; message: string; data: ApiDocument[] | null }>(ORCHESTRATION_BASE_URL, `/api/v1/documents/${userId}`)
150
  .then(res => res.data ?? []);
151
 
152
- export const uploadDocument = async (
153
  userId: string,
154
- file: File
155
- ): Promise<UploadDocumentResponse> => {
156
- const form = new FormData();
157
- form.append("user_id", userId);
158
- form.append("file", file);
159
- const res = await fetch(
160
- `${ORCHESTRATION_BASE_URL}/api/v1/document/upload`,
161
- { method: "POST", body: form }
162
- );
163
- if (!res.ok) {
164
- const err = await res
165
- .json()
166
- .catch(() => ({ detail: `HTTP ${res.status}` }));
167
- throw new Error(err.detail ?? `HTTP ${res.status}`);
168
- }
169
- return res.json() as Promise<UploadDocumentResponse>;
170
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
171
 
172
- export const processDocument = (_userId: string, documentId: string) =>
173
- request<{ status: string; message: string }>(
 
 
174
  ORCHESTRATION_BASE_URL,
175
  `/api/v1/document/process`,
176
- { method: "POST", body: JSON.stringify({ document_id: documentId }) }
177
  );
 
 
 
178
 
179
- export const deleteDocument = (_userId: string, documentId: string) =>
180
  request<{ status: string; message: string }>(
181
  ORCHESTRATION_BASE_URL,
182
  `/api/v1/document/delete`,
183
- { method: "DELETE", body: JSON.stringify({ document_id: documentId }) }
184
  );
185
 
186
  export const getDocumentTypes = (): Promise<DocTypeInfo[]> =>
@@ -221,10 +269,21 @@ export interface DatabaseClient {
221
  updated_at: string | null;
222
  }
223
 
 
 
 
 
 
 
 
 
 
 
 
 
 
224
  export interface IngestResponse {
225
- status: string;
226
- client_id: string;
227
- chunks_ingested: number;
228
  }
229
 
230
  export const getDatabaseClientTypes = (): Promise<DbTypeInfo[]> =>
@@ -253,10 +312,10 @@ export const deleteDatabaseClient = (clientId: string, userId: string) =>
253
  { method: "DELETE" }
254
  );
255
 
256
- export const ingestDatabaseClient = (clientId: string, _userId: string): Promise<IngestResponse> =>
257
  request<{ status: string; message: string; data: IngestResponse }>(
258
  ORCHESTRATION_BASE_URL,
259
- `/api/v1/database-clients/${clientId}/ingest`,
260
  { method: "POST" }
261
  ).then(res => res.data);
262
 
 
53
  messages: RoomMessage[];
54
  }
55
 
56
+ export type DocumentStatus = "uploading" | "uploaded" | "processing" | "completed" | "failed";
57
 
58
  export interface ApiDocument {
59
  id: string;
60
+ user_id?: string;
61
  filename: string;
62
+ blob_name?: string;
63
  status: DocumentStatus;
64
  file_size: number;
65
  file_type: string;
66
  created_at: string;
67
+ processed_at?: string;
68
+ error_message?: string;
69
  }
70
 
71
  export interface UploadDocumentResponse {
72
  status: string;
73
  message: string;
74
+ data: {
75
+ id: string;
76
+ user_id: string;
77
+ filename: string;
78
+ blob_name: string;
79
+ file_size: number;
80
+ file_type: string;
81
+ status: DocumentStatus;
82
+ created_at: string;
83
+ };
84
  }
85
 
86
  export interface DocTypeInfo {
87
+ type: string;
88
+ max_size_mb: number;
89
  status: "active" | "inactive";
90
  message: string | null;
91
  }
 
95
  source_type: "schema" | "tabular" | "unstructured";
96
  name: string;
97
  location_ref: string;
98
+ table_count?: number;
99
+ updated_at?: string;
100
  }
101
 
102
  export interface DataCatalog {
 
121
  if (!res.ok) {
122
  const err = await res
123
  .json()
124
+ .catch(() => ({ message: `HTTP ${res.status}` }));
125
+ throw new Error(err.message ?? `HTTP ${res.status}`);
126
  }
127
  return res.json() as Promise<T>;
128
  }
 
138
  // ─── Rooms ────────────────────────────────────────────────────────────────────
139
 
140
  export const getRooms = (userId: string): Promise<Room[]> =>
141
+ request<{ status: string; message: string; data: Room[] | null }>(ORCHESTRATION_BASE_URL, `/api/v1/chat-rooms?user_id=${userId}`)
142
  .then(res => res.data ?? []);
143
 
144
  export const getRoom = (roomId: string): Promise<RoomDetail> =>
145
+ request<{ status: string; message: string; data: RoomDetail }>(ORCHESTRATION_BASE_URL, `/api/v1/chat-rooms/${roomId}`)
146
  .then(res => res.data);
147
 
148
  export const deleteRoom = (roomId: string, userId: string) =>
149
  request<{ status: string; message: string }>(
150
  ORCHESTRATION_BASE_URL,
151
+ `/api/v1/chat-rooms/${roomId}?user_id=${userId}`,
152
  { method: "DELETE" }
153
  );
154
 
155
  export const createRoom = (userId: string, title?: string) =>
156
+ request<CreateRoomResponse>(ORCHESTRATION_BASE_URL, "/api/v1/chat-rooms", {
157
  method: "POST",
158
  body: JSON.stringify({ user_id: userId, title }),
159
  });
 
164
  request<{ status: string; message: string; data: ApiDocument[] | null }>(ORCHESTRATION_BASE_URL, `/api/v1/documents/${userId}`)
165
  .then(res => res.data ?? []);
166
 
167
+ export const uploadDocument = (
168
  userId: string,
169
+ file: File,
170
+ onProgress?: (percent: number) => void
171
+ ): Promise<UploadDocumentResponse> =>
172
+ new Promise((resolve, reject) => {
173
+ const form = new FormData();
174
+ form.append("user_id", userId);
175
+ form.append("file", file);
176
+
177
+ console.log(`[upload] β–Ά XHR start β€” file: "${file.name}", size: ${(file.size / 1024).toFixed(1)} KB`);
178
+ const t0 = performance.now();
179
+
180
+ const xhr = new XMLHttpRequest();
181
+
182
+ xhr.upload.onprogress = (e) => {
183
+ if (e.lengthComputable) {
184
+ const pct = Math.round((e.loaded / e.total) * 100);
185
+ console.log(`[upload] ↑ ${pct}% β€” ${(e.loaded / 1024).toFixed(0)} / ${(e.total / 1024).toFixed(0)} KB`);
186
+ onProgress?.(pct);
187
+ }
188
+ };
189
+
190
+ xhr.onload = () => {
191
+ console.log(`[upload] β—€ HTTP ${xhr.status}, elapsed: ${(performance.now() - t0).toFixed(0)} ms`);
192
+ if (xhr.status >= 200 && xhr.status < 300) {
193
+ try {
194
+ const data = JSON.parse(xhr.responseText) as UploadDocumentResponse;
195
+ console.log(`[upload] βœ“ done β€” total: ${(performance.now() - t0).toFixed(0)} ms`);
196
+ resolve(data);
197
+ } catch {
198
+ reject(new Error("Failed to parse response"));
199
+ }
200
+ } else {
201
+ try {
202
+ const err = JSON.parse(xhr.responseText) as { message?: string };
203
+ reject(new Error(err.message ?? `HTTP ${xhr.status}`));
204
+ } catch {
205
+ reject(new Error(`HTTP ${xhr.status}`));
206
+ }
207
+ }
208
+ };
209
+
210
+ xhr.onerror = () => reject(new Error("Network error during upload"));
211
+ xhr.open("POST", `${ORCHESTRATION_BASE_URL}/api/v1/document/upload`);
212
+ xhr.send(form);
213
+ });
214
 
215
+ export const processDocument = async (userId: string, documentId: string) => {
216
+ console.log(`[process] β–Ά fetch start β€” document_id: ${documentId}`);
217
+ const t0 = performance.now();
218
+ const result = await request<{ status: string; message: string; data: { document_id: string; file_type: string; status: string } }>(
219
  ORCHESTRATION_BASE_URL,
220
  `/api/v1/document/process`,
221
+ { method: "POST", body: JSON.stringify({ user_id: userId, document_id: documentId }) }
222
  );
223
+ console.log(`[process] βœ“ accepted β€” elapsed: ${(performance.now() - t0).toFixed(0)} ms, status: ${result.data?.status}`);
224
+ return result;
225
+ };
226
 
227
+ export const deleteDocument = (userId: string, documentId: string) =>
228
  request<{ status: string; message: string }>(
229
  ORCHESTRATION_BASE_URL,
230
  `/api/v1/document/delete`,
231
+ { method: "DELETE", body: JSON.stringify({ document_id: documentId, user_id: userId }) }
232
  );
233
 
234
  export const getDocumentTypes = (): Promise<DocTypeInfo[]> =>
 
269
  updated_at: string | null;
270
  }
271
 
272
+ export interface IngestColumn {
273
+ name: string;
274
+ data_type: string;
275
+ nullable: boolean;
276
+ }
277
+
278
+ export interface IngestTable {
279
+ name: string;
280
+ row_count: number;
281
+ columns: IngestColumn[];
282
+ fks: Array<{ column_name: string; foreign_table: string; foreign_column_name: string }>;
283
+ }
284
+
285
  export interface IngestResponse {
286
+ tables: IngestTable[];
 
 
287
  }
288
 
289
  export const getDatabaseClientTypes = (): Promise<DbTypeInfo[]> =>
 
312
  { method: "DELETE" }
313
  );
314
 
315
+ export const ingestDatabaseClient = (clientId: string, userId: string): Promise<IngestResponse> =>
316
  request<{ status: string; message: string; data: IngestResponse }>(
317
  ORCHESTRATION_BASE_URL,
318
+ `/api/v1/database-clients/${clientId}/ingest?user_id=${userId}`,
319
  { method: "POST" }
320
  ).then(res => res.data);
321
 
src/services/interviewApi.ts CHANGED
@@ -93,9 +93,9 @@ export const createSession = (
93
  userId: string,
94
  roomId: string,
95
  mode: "text" | "audio" = "text",
96
- language = "id-ID"
97
  ): Promise<CreateSessionResponse> =>
98
- request<CreateSessionResponse>("/sessions", {
99
  method: "POST",
100
  body: JSON.stringify({ framework_id: frameworkId, user_id: userId, room_id: roomId, mode, language }),
101
  });
@@ -104,7 +104,7 @@ export const sendMessage = (
104
  sessionId: string,
105
  message: string
106
  ): Promise<MessageResponse> =>
107
- request<MessageResponse>(`/sessions/${sessionId}/message`, {
108
  method: "POST",
109
  body: JSON.stringify({ message }),
110
  });
@@ -112,19 +112,19 @@ export const sendMessage = (
112
  export const finishSession = (
113
  sessionId: string
114
  ): Promise<FinishSessionResponse> =>
115
- request<FinishSessionResponse>(`/sessions/${sessionId}/finish`, {
116
  method: "POST",
117
  });
118
 
119
  export const getInterviewResult = (roomId: string): Promise<InterviewResult> =>
120
- request<InterviewResult>(`/rooms/${roomId}/result`);
121
 
122
  // SSE streaming β€” returns raw Response so caller can read the stream
123
  export const streamMessage = (
124
  sessionId: string,
125
  message: string
126
  ): Promise<Response> =>
127
- fetch(`${INTERVIEW_BASE_URL}/sessions/${sessionId}/stream-message`, {
128
  method: "POST",
129
  headers: { "Content-Type": "application/json" },
130
  body: JSON.stringify({ message }),
@@ -145,7 +145,7 @@ export function openAudioSession(
145
  onClose: () => void
146
  ): AudioSessionHandle {
147
  const wsBase = INTERVIEW_BASE_URL.replace(/^http/, "ws");
148
- const ws = new WebSocket(`${wsBase}/ws/audio?session_id=${sessionId}`);
149
  ws.binaryType = "arraybuffer";
150
 
151
  ws.onmessage = (e) => {
 
93
  userId: string,
94
  roomId: string,
95
  mode: "text" | "audio" = "text",
96
+ language = "id"
97
  ): Promise<CreateSessionResponse> =>
98
+ request<CreateSessionResponse>("/api/v1/interviews/sessions", {
99
  method: "POST",
100
  body: JSON.stringify({ framework_id: frameworkId, user_id: userId, room_id: roomId, mode, language }),
101
  });
 
104
  sessionId: string,
105
  message: string
106
  ): Promise<MessageResponse> =>
107
+ request<MessageResponse>(`/api/v1/interviews/sessions/${sessionId}/message`, {
108
  method: "POST",
109
  body: JSON.stringify({ message }),
110
  });
 
112
  export const finishSession = (
113
  sessionId: string
114
  ): Promise<FinishSessionResponse> =>
115
+ request<FinishSessionResponse>(`/api/v1/interviews/sessions/${sessionId}/finish`, {
116
  method: "POST",
117
  });
118
 
119
  export const getInterviewResult = (roomId: string): Promise<InterviewResult> =>
120
+ request<InterviewResult>(`/api/v1/interviews/${roomId}/result`);
121
 
122
  // SSE streaming β€” returns raw Response so caller can read the stream
123
  export const streamMessage = (
124
  sessionId: string,
125
  message: string
126
  ): Promise<Response> =>
127
+ fetch(`${INTERVIEW_BASE_URL}/api/v1/interviews/sessions/${sessionId}/stream-message`, {
128
  method: "POST",
129
  headers: { "Content-Type": "application/json" },
130
  body: JSON.stringify({ message }),
 
145
  onClose: () => void
146
  ): AudioSessionHandle {
147
  const wsBase = INTERVIEW_BASE_URL.replace(/^http/, "ws");
148
+ const ws = new WebSocket(`${wsBase}/api/v1/interviews/ws/audio?session_id=${sessionId}`);
149
  ws.binaryType = "arraybuffer";
150
 
151
  ws.onmessage = (e) => {