// ─── Types ──────────────────────────────────────────────────────────────────── export interface InterviewFramework { id: string; name: string; } export interface CreateSessionResponse { session_id: string; framework_id: string; room_id: string; status: string; opening_message: string; first_question: string; } export interface MessageResponse { reply: string; stage: "in_progress" | "next_question" | "follow_up" | "closing"; finished: boolean; } export interface QAPair { question_text: string; answer_cleaned: string; follow_ups?: QAPair[]; } export interface SectionResult { section_title: string; objective: string[]; qa_pairs: QAPair[]; section_summary: string; } export interface InterviewResult { framework_name: string; mode: string; language: string; started_at: string; ended_at: string; summary: string; goals: string[]; section_results: SectionResult[]; key_insights: string[]; unresolved_items: string[]; } export interface FinishSessionResponse { session_id: string; room_id: string; status: string; result: InterviewResult; } export interface StreamMetadata { finished: boolean; stage: "next_question" | "follow_up" | "closing"; } export type AudioServerEvent = | { type: "token_chunk"; payload: string } | { type: "assistant_reply"; payload: string } | { type: "session_done" } | { type: "error"; payload: string }; // ─── Base Client ────────────────────────────────────────────────────────────── const INTERVIEW_BASE_URL = ((import.meta as unknown as { env: Record }).env .VITE_ORCHESTRATION_API_BASE_URL) ?? "http://localhost:8080"; async function request(path: string, options?: RequestInit): Promise { const res = await fetch(`${INTERVIEW_BASE_URL}${path}`, { headers: { "Content-Type": "application/json", ...options?.headers }, ...options, }); if (!res.ok) { const err = await res.json().catch(() => ({ error: `HTTP ${res.status}` })); throw new Error(err.error ?? `HTTP ${res.status}`); } return res.json() as Promise; } // ─── Endpoints ──────────────────────────────────────────────────────────────── export const getFrameworks = (): Promise => request("/frameworks"); export const createSession = ( frameworkId: string, userId: string, roomId: string, mode: "text" | "audio" = "text", language = "id-ID" ): Promise => request("/sessions", { method: "POST", body: JSON.stringify({ framework_id: frameworkId, user_id: userId, room_id: roomId, mode, language }), }); export const sendMessage = ( sessionId: string, message: string ): Promise => request(`/sessions/${sessionId}/message`, { method: "POST", body: JSON.stringify({ message }), }); export const finishSession = ( sessionId: string ): Promise => request(`/sessions/${sessionId}/finish`, { method: "POST", }); export const getInterviewResult = (roomId: string): Promise => request(`/rooms/${roomId}/result`); // SSE streaming — returns raw Response so caller can read the stream export const streamMessage = ( sessionId: string, message: string ): Promise => fetch(`${INTERVIEW_BASE_URL}/sessions/${sessionId}/stream-message`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ message }), }); // ─── Audio WebSocket ────────────────────────────────────────────────────────── export interface AudioSessionHandle { sendAudioChunk: (chunk: ArrayBuffer) => void; sendEndUtterance: () => void; close: () => void; } export function openAudioSession( sessionId: string, onEvent: (event: AudioServerEvent) => void, onAudio: (audioBuffer: ArrayBuffer) => void, onClose: () => void ): AudioSessionHandle { const wsBase = INTERVIEW_BASE_URL.replace(/^http/, "ws"); const ws = new WebSocket(`${wsBase}/ws/audio?session_id=${sessionId}`); ws.binaryType = "arraybuffer"; ws.onmessage = (e) => { if (e.data instanceof ArrayBuffer) { onAudio(e.data); } else { try { const parsed = JSON.parse(e.data) as AudioServerEvent; onEvent(parsed); } catch { // ignore malformed messages } } }; ws.onclose = onClose; return { sendAudioChunk: (chunk) => { if (ws.readyState === WebSocket.OPEN) ws.send(chunk); }, sendEndUtterance: () => { if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ type: "end_utterance" })); }, close: () => ws.close(), }; }