| |
|
|
| 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 }; |
|
|
| |
|
|
| import { getEnv } from "@/env"; |
|
|
| const INTERVIEW_BASE_URL = |
| getEnv("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>; |
| } |
|
|
| |
|
|
| export const getFrameworks = (): Promise<InterviewFramework[]> => |
| request<InterviewFramework[]>("/frameworks"); |
|
|
| export const createSession = ( |
| frameworkId: string, |
| userId: string, |
| roomId: string, |
| mode: "text" | "audio" = "text", |
| language = "id" |
| ): Promise<CreateSessionResponse> => |
| request<CreateSessionResponse>("/api/v1/interviews/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>(`/api/v1/interviews/sessions/${sessionId}/message`, { |
| method: "POST", |
| body: JSON.stringify({ message }), |
| }); |
|
|
| export const finishSession = ( |
| sessionId: string |
| ): Promise<FinishSessionResponse> => |
| request<FinishSessionResponse>(`/api/v1/interviews/sessions/${sessionId}/finish`, { |
| method: "POST", |
| }); |
|
|
| export const getInterviewResult = (roomId: string): Promise<InterviewResult> => |
| request<InterviewResult>(`/api/v1/interviews/${roomId}/result`); |
|
|
| |
| export const streamMessage = ( |
| sessionId: string, |
| message: string |
| ): Promise<Response> => |
| fetch(`${INTERVIEW_BASE_URL}/api/v1/interviews/sessions/${sessionId}/stream-message`, { |
| method: "POST", |
| headers: { "Content-Type": "application/json" }, |
| body: JSON.stringify({ message }), |
| }); |
|
|
| |
|
|
| 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}/api/v1/interviews/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 { |
| |
| } |
| } |
| }; |
|
|
| 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(), |
| }; |
| } |
|
|