E2E-Frontend-Data-Eyond / src /services /interviewApi.ts
ishaq101's picture
[KM-475] [DED][FE] Align API service layer with backend contracts
5eefa6a
raw
history blame
5.27 kB
// ─── 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(),
};
}