Spaces:
Sleeping
Sleeping
| // βββ 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<string, string> }).env | |
| .VITE_ORCHESTRATION_API_BASE_URL) ?? "http://localhost:8080"; | |
| async function request<T>(path: string, options?: RequestInit): Promise<T> { | |
| 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<T>; | |
| } | |
| // βββ Endpoints ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| export const getFrameworks = (): Promise<InterviewFramework[]> => | |
| request<InterviewFramework[]>("/frameworks"); | |
| export const createSession = ( | |
| frameworkId: string, | |
| userId: string, | |
| roomId: string, | |
| mode: "text" | "audio" = "text", | |
| language = "id-ID" | |
| ): Promise<CreateSessionResponse> => | |
| request<CreateSessionResponse>("/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<MessageResponse> => | |
| request<MessageResponse>(`/sessions/${sessionId}/message`, { | |
| method: "POST", | |
| body: JSON.stringify({ message }), | |
| }); | |
| export const finishSession = ( | |
| sessionId: string | |
| ): Promise<FinishSessionResponse> => | |
| request<FinishSessionResponse>(`/sessions/${sessionId}/finish`, { | |
| method: "POST", | |
| }); | |
| export const getInterviewResult = (roomId: string): Promise<InterviewResult> => | |
| request<InterviewResult>(`/rooms/${roomId}/result`); | |
| // SSE streaming β returns raw Response so caller can read the stream | |
| export const streamMessage = ( | |
| sessionId: string, | |
| message: string | |
| ): Promise<Response> => | |
| 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(), | |
| }; | |
| } | |