Newer
Older
cortex-hub / ui / client-app / src / services / apiService.js
// This file handles all communication with your API endpoints.
// It is designed to be stateless and does not use any React hooks.

import { convertPcmToFloat32 } from "./audioUtils";

// Please replace with your actual endpoints
const STT_ENDPOINT = "http://localhost:8001/stt/transcribe";
const SESSIONS_CREATE_ENDPOINT = "http://localhost:8001/sessions";
const SESSIONS_CHAT_ENDPOINT = (id) => `http://localhost:8001/sessions/${id}/chat`;
const TTS_ENDPOINT = "http://localhost:8001/speech";
const USERS_LOGIN_ENDPOINT = "http://localhost:8001/users/login";
const USERS_LOGOUT_ENDPOINT = "http://localhost:8001/users/logout";
const USERS_ME_ENDPOINT = "http://localhost:8001/users/me";

/**
 * A central utility function to get the user ID.
 * If not found, it redirects to the login page.
 * @returns {string} The user ID.
 */
const getUserId = () => {
  const userId = localStorage.getItem('userId');
  if (!userId) {
    console.error("User not authenticated. Redirecting to login.");
    // Redirect to the login page
    window.location.href = '/';
  }
  return userId;
};

/**
 * Initiates the OIDC login flow by redirecting the user to the login endpoint.
 * This function now sends the frontend's URI to the backend so the backend
 * knows where to redirect the user after a successful login with the OIDC provider.
 */
export const login = () => {
  // Pass the current frontend origin to the backend's login endpoint.
  // The backend will use this as the `state` parameter for the OIDC provider.
  const frontendCallbackUri = window.location.origin;
  const loginUrl = `${USERS_LOGIN_ENDPOINT}?frontend_callback_uri=${encodeURIComponent(frontendCallbackUri)}`;
  window.location.href = loginUrl;
};

/**
 * Fetches the current user's status from the backend.
 * @param {string} userId - The unique ID of the current user.
 * @returns {Promise<Object>} The user status object from the API response.
 */
export const getUserStatus = async (userId) => {
  // The backend uses the 'X-User-ID' header to identify the user.
  const response = await fetch(USERS_ME_ENDPOINT, {
    method: "GET",
    headers: {
      "X-User-ID": userId,
    },
  });
  if (!response.ok) {
    throw new Error(`Failed to get user status. Status: ${response.status}`);
  }
  return await response.json();
};

/**
 * Logs the current user out.
 * @returns {Promise<Object>} The logout message from the API response.
 */
export const logout = async () => {
  const response = await fetch(USERS_LOGOUT_ENDPOINT, {
    method: "POST",
  });
  if (!response.ok) {
    throw new Error(`Failed to log out. Status: ${response.status}`);
  }
  return await response.json();
};


// --- Updated Existing Functions ---

/**
 * Creates a new chat session.
 * @returns {Promise<Object>} The session object from the API response.
 */
export const createSession = async () => {
  const userId = getUserId();
  const response = await fetch(SESSIONS_CREATE_ENDPOINT, {
    method: "POST",
    headers: { "Content-Type": "application/json", "X-User-ID": userId },
    // Now we pass the userId to the backend.
    body: JSON.stringify({ user_id: userId }),
  });
  if (!response.ok) {
    throw new Error(`Failed to create session. Status: ${response.status}`);
  }
  return await response.json();
};

// --- Unchanged Functions ---

/**
 * Sends an audio blob to the STT endpoint for transcription.
 * @param {Blob} audioBlob - The recorded audio data.
 * @returns {Promise<string>} The transcribed text.
 */
export const transcribeAudio = async (audioBlob) => {
  const userId = getUserId();
  const formData = new FormData();
  formData.append("audio_file", audioBlob, "audio.wav");
  const response = await fetch(STT_ENDPOINT, {
    method: "POST",
    body: formData,
    headers: { "X-User-ID": userId },
  });
  if (!response.ok) {
    throw new Error("STT API failed");
  }
  const result = await response.json();
  return result.transcript;
};

/**
 * Sends a text prompt to the LLM endpoint and gets a text response.
 * @param {string} sessionId - The current chat session ID.
 * @param {string} prompt - The user's text prompt.
 * @returns {Promise<string>} The AI's text response.
 */
export const chatWithAI = async (sessionId, prompt) => {
  const userId = getUserId();
  const response = await fetch(SESSIONS_CHAT_ENDPOINT(sessionId), {
    method: "POST",
    headers: { "Content-Type": "application/json", "X-User-ID": userId },
    body: JSON.stringify({ prompt: prompt, provider_name: "gemini" }),
  });
  if (!response.ok) {
    throw new Error("LLM API failed");
  }
  const result = await response.json();
  return result.answer;
};

/**
 * Streams speech from the TTS endpoint and processes each chunk.
 * It uses a callback to pass the processed audio data back to the caller.
 * @param {string} text - The text to be synthesized.
 * @param {function(Float32Array): void} onData - Callback for each audio chunk.
 * @param {function(): void} onDone - Callback to execute when the stream is finished.
 * @returns {Promise<void>}
 */
export const streamSpeech = async (text, onData, onDone) => {
  const userId = getUserId();
  try {
    const url = `${TTS_ENDPOINT}?stream=true&as_wav=false`;

    const response = await fetch(url, {
      method: "POST",
      headers: { "Content-Type": "application/json", "X-User-ID": userId },
      body: JSON.stringify({ text }),
    });

    if (!response.ok) {
      throw new Error(`HTTP error! Status: ${response.status}`);
    }

    const reader = response.body.getReader();
    let leftover = new Uint8Array(0);

    while (true) {
      const { done, value: chunk } = await reader.read();
      if (done) {
        if (leftover.length > 0) {
          console.warn("Leftover bytes discarded:", leftover.length);
        }
        break;
      }

      let combined = new Uint8Array(leftover.length + chunk.length);
      combined.set(leftover);
      combined.set(chunk, leftover.length);

      let length = combined.length;
      if (length % 2 !== 0) {
        length -= 1;
      }

      const toConvert = combined.slice(0, length);
      leftover = combined.slice(length);
      const float32Raw = convertPcmToFloat32(toConvert);
      
      // Pass the raw float32 data to the caller for resampling
      onData(float32Raw);
    }
  } catch (error) {
    console.error("Failed to stream speech:", error);
    throw error;
  } finally {
    // We call the onDone callback to let the hook know the stream has ended.
    onDone();
  }
};