// 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";
// Read base API URL from environment variables, defaulting to localhost
const API_BASE_URL = process.env.REACT_APP_API_BASE_URL || "http://localhost:8001";
export { API_BASE_URL };
const CHAT_ENDPOINT = `${API_BASE_URL}/chat`;
const USER_CONFIG_ENDPOINT = `${API_BASE_URL}/users/me/config`;
const STT_ENDPOINT = `${API_BASE_URL}/stt/transcribe`;
const SESSIONS_CREATE_ENDPOINT = `${API_BASE_URL}/sessions/`;
const SESSIONS_CHAT_ENDPOINT = (id) => `${API_BASE_URL}/sessions/${id}/chat`;
const SESSIONS_TOKEN_ENDPOINT = (id) => `${API_BASE_URL}/sessions/${id}/tokens`;
const SESSIONS_MESSAGES_ENDPOINT = (id) => `${API_BASE_URL}/sessions/${id}/messages`;
const TTS_ENDPOINT = `${API_BASE_URL}/speech`;
const USERS_LOGIN_ENDPOINT = `${API_BASE_URL}/users/login`;
const USERS_LOGOUT_ENDPOINT = `${API_BASE_URL}/users/logout`;
const USERS_ME_ENDPOINT = `${API_BASE_URL}/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 ---
const SESSIONS_GET_ALL_ENDPOINT = `${API_BASE_URL}/sessions/`;
const SESSIONS_DELETE_ENDPOINT = (id) => `${API_BASE_URL}/sessions/${id}`;
/**
* Creates a new chat session.
* @param {string} featureName - The namespace for isolated feature tracking.
* @param {string} providerName - The initial LLM provider for the session.
* @returns {Promise<Object>} The session object from the API response.
*/
export const createSession = async (featureName = "default", providerName = "deepseek") => {
const userId = getUserId();
const response = await fetch(SESSIONS_CREATE_ENDPOINT, {
method: "POST",
headers: { "Content-Type": "application/json", "X-User-ID": userId },
body: JSON.stringify({ user_id: userId, feature_name: featureName, provider_name: providerName }),
});
if (!response.ok) {
throw new Error(`Failed to create session. Status: ${response.status}`);
}
return await response.json();
};
/**
* Fetches all sessions for a specific feature for the current user.
* @param {string} featureName - The namespace for isolated feature tracking.
* @returns {Promise<Array>} The sessions list from the API response.
*/
export const getUserSessions = async (featureName = "default") => {
const userId = getUserId();
const params = new URLSearchParams({ user_id: userId, feature_name: featureName });
const response = await fetch(`${SESSIONS_GET_ALL_ENDPOINT}?${params.toString()}`, {
method: "GET",
headers: { "X-User-ID": userId },
});
if (!response.ok) {
throw new Error(`Failed to fetch sessions. Status: ${response.status}`);
}
return await response.json();
};
/**
* Deletes a single chat session by ID.
* @param {string} sessionId - The session ID to delete.
*/
export const deleteSession = async (sessionId) => {
const userId = getUserId();
const response = await fetch(SESSIONS_DELETE_ENDPOINT(sessionId), {
method: "DELETE",
headers: { "X-User-ID": userId },
});
if (!response.ok) {
throw new Error(`Failed to delete session. Status: ${response.status}`);
}
return await response.json();
};
/**
* Gets a single chat session by ID.
* @param {string} sessionId - The session ID to fetch.
*/
export const getSession = async (sessionId) => {
const userId = getUserId();
// We can reuse SESSIONS_DELETE_ENDPOINT generator since it creates the URL with the ID
const response = await fetch(SESSIONS_DELETE_ENDPOINT(sessionId), {
method: "GET",
headers: { "X-User-ID": userId },
});
if (!response.ok) {
throw new Error(`Failed to fetch session. Status: ${response.status}`);
}
return await response.json();
};
/**
* Deletes all chat sessions for a given feature.
* @param {string} featureName - The feature namespace to clear.
*/
export const deleteAllSessions = async (featureName = "default") => {
const userId = getUserId();
const params = new URLSearchParams({ user_id: userId, feature_name: featureName });
const response = await fetch(`${SESSIONS_GET_ALL_ENDPOINT}?${params.toString()}`, {
method: "DELETE",
headers: { "X-User-ID": userId },
});
if (!response.ok) {
throw new Error(`Failed to delete all sessions. Status: ${response.status}`);
}
return await response.json();
};
/**
* Fetches the token usage status for a session.
* @param {string} sessionId - The session ID to fetch tokens for.
* @returns {Promise<Object>} The token usage object.
*/
export const getSessionTokenStatus = async (sessionId) => {
const userId = getUserId();
const response = await fetch(SESSIONS_TOKEN_ENDPOINT(sessionId), {
method: "GET",
headers: { "X-User-ID": userId },
});
if (!response.ok) {
throw new Error(`Failed to fetch token status. Status: ${response.status}`);
}
return await response.json();
};
/**
* Fetches the chat history for a session.
* @param {string} sessionId - The session ID to fetch messages for.
* @returns {Promise<Object>} The message history object.
*/
export const getSessionMessages = async (sessionId) => {
const userId = getUserId();
const response = await fetch(SESSIONS_MESSAGES_ENDPOINT(sessionId), {
method: "GET",
headers: { "X-User-ID": userId },
});
if (!response.ok) {
throw new Error(`Failed to fetch session messages. 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, providerName = null) => {
const userId = getUserId();
const formData = new FormData();
formData.append("audio_file", audioBlob, "audio.wav");
let url = STT_ENDPOINT;
if (providerName) {
url += `?provider_name=${encodeURIComponent(providerName)}`;
}
const response = await fetch(url, {
method: "POST",
body: formData,
headers: { "X-User-ID": userId },
});
if (!response.ok) {
let detail = `STT API failed (${response.status})`;
try {
const errBody = await response.json();
detail = errBody.detail || detail;
} catch { }
throw new Error(detail);
}
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, providerName = "gemini") => {
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: providerName }),
});
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, providerName = null) => {
const userId = getUserId();
try {
let url = `${TTS_ENDPOINT}?stream=true&as_wav=false`;
if (providerName) {
url += `&provider_name=${encodeURIComponent(providerName)}`;
}
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();
}
};
/**
* Fetches the current user's preferences.
* @returns {Promise<Object>} The user preferences object.
*/
export const getUserConfig = async () => {
const userId = getUserId();
const response = await fetch(USER_CONFIG_ENDPOINT, {
headers: { "X-User-ID": userId },
});
if (!response.ok) {
throw new Error(`Failed to fetch user config. Status: ${response.status}`);
}
return await response.json();
};
/**
* Updates the current user's preferences.
* @param {Object} config The new configuration object.
* @returns {Promise<Object>} The updated user preferences object.
*/
export const updateUserConfig = async (config) => {
const userId = getUserId();
const response = await fetch(USER_CONFIG_ENDPOINT, {
method: "PUT",
headers: {
"X-User-ID": userId,
"Content-Type": "application/json",
},
body: JSON.stringify(config),
});
if (!response.ok) {
throw new Error(`Failed to update user config. Status: ${response.status}`);
}
return await response.json();
};
/**
* Download the effective user configurations as YAML.
* @returns {Promise<Response>} The raw API response to extract the file.
*/
export const exportUserConfig = async () => {
const userId = getUserId();
const exportUrl = `${USER_CONFIG_ENDPOINT}/export`;
const response = await fetch(exportUrl, {
method: "GET",
headers: { "X-User-ID": userId },
});
if (!response.ok) {
throw new Error(`Failed to export config. Status: ${response.status}`);
}
return response;
};
/**
* Download the effective user configurations as YAML.
* @returns {Promise<Response>} The raw API response to extract the file.
*/
export const uploadDocument = async (file) => {
const userId = getUserId();
const formData = new FormData();
formData.append("file", file);
const response = await fetch(`${API_BASE_URL}/rag/documents`, {
method: "POST",
headers: {
"X-User-ID": userId,
},
body: formData,
});
if (!response.ok) {
throw new Error(`Failed to upload document. Status: ${response.status}`);
}
return await response.json();
};
/**
* Verify a provider configuration
* @param {string} section - 'llm', 'tts', or 'stt'
* @param {Object} payload - { provider_name, api_key, model, voice }
*/
export const verifyProvider = async (section, payload) => {
const userId = getUserId();
const response = await fetch(`${API_BASE_URL}/users/me/config/verify_${section}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-User-ID": userId,
},
body: JSON.stringify(payload),
});
if (!response.ok) {
throw new Error(`Failed to verify provider. Status: ${response.status}`);
}
return await response.json();
};
export const getProviderModels = async (providerName, section = "llm") => {
const userId = getUserId();
const params = new URLSearchParams({ provider_name: providerName, section: section });
const response = await fetch(`${API_BASE_URL}/users/me/config/models?${params.toString()}`, {
method: "GET",
headers: {
"X-User-ID": userId,
},
});
if (!response.ok) {
throw new Error(`Failed to fetch models for ${providerName}`);
}
return await response.json();
};
export const getAllProviders = async (section = "llm") => {
const userId = getUserId();
const params = new URLSearchParams({ section: section });
const response = await fetch(`${API_BASE_URL}/users/me/config/providers?${params.toString()}`, {
method: "GET",
headers: {
"X-User-ID": userId,
},
});
if (!response.ok) {
throw new Error(`Failed to fetch underlying providers.`);
}
return await response.json();
};
/**
* Fetches the user profile info.
*/
export const getUserProfile = async () => {
const userId = getUserId();
const response = await fetch(`${API_BASE_URL}/users/me/profile`, {
method: "GET",
headers: { "X-User-ID": userId },
});
if (!response.ok) throw new Error("Failed to fetch user profile");
return await response.json();
};
/**
* Updates the user profile info.
*/
export const updateUserProfile = async (profileData) => {
const userId = getUserId();
const response = await fetch(`${API_BASE_URL}/users/me/profile`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
"X-User-ID": userId,
},
body: JSON.stringify(profileData),
});
if (!response.ok) throw new Error("Failed to update user profile");
return await response.json();
};
/**
* Fetches available TTS voice names.
*/
export const getVoices = async (provider = null, apiKey = null) => {
try {
const userId = getUserId();
const urlParams = new URLSearchParams();
if (provider) urlParams.append('provider', provider);
if (apiKey && apiKey !== 'null') urlParams.append('api_key', apiKey);
const url = `${API_BASE_URL}/speech/voices?${urlParams.toString()}`;
const response = await fetch(url, {
method: 'GET',
headers: { 'X-User-ID': userId }
});
if (!response.ok) return [];
return await response.json();
} catch (e) {
console.error("Failed to fetch voices", e);
return [];
}
};
export const importUserConfig = async (formData) => {
const userId = getUserId();
const response = await fetch(`${API_BASE_URL}/users/me/config/import`, {
method: "POST",
headers: {
"X-User-ID": userId,
},
body: formData,
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.detail || `Failed to import configuration. Status: ${response.status}`);
}
return await response.json();
};
/**
* [ADMIN ONLY] Fetches all registered users.
*/
export const getAdminUsers = async () => {
const userId = getUserId();
const response = await fetch(`${API_BASE_URL}/users/admin/users`, {
method: "GET",
headers: { "X-User-ID": userId },
});
if (!response.ok) throw new Error("Failed to fetch admin user list");
return await response.json();
};
/**
* [ADMIN ONLY] Updates a user's role.
*/
export const updateUserRole = async (targetUserId, role) => {
const userId = getUserId();
const response = await fetch(`${API_BASE_URL}/users/admin/users/${targetUserId}/role`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
"X-User-ID": userId,
},
body: JSON.stringify({ role }),
});
if (!response.ok) throw new Error("Failed to update user role");
return await response.json();
};
/**
* [ADMIN ONLY] Assigns a user to a group.
*/
export const updateUserGroup = async (targetUserId, groupId) => {
const userId = getUserId();
const response = await fetch(`${API_BASE_URL}/users/admin/users/${targetUserId}/group`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
"X-User-ID": userId,
},
body: JSON.stringify({ group_id: groupId }),
});
if (!response.ok) throw new Error("Failed to update user group");
return await response.json();
};
/**
* [ADMIN ONLY] Fetches all groups.
*/
export const getAdminGroups = async () => {
const userId = getUserId();
const response = await fetch(`${API_BASE_URL}/users/admin/groups`, {
method: "GET",
headers: { "X-User-ID": userId },
});
if (!response.ok) throw new Error("Failed to fetch group list");
return await response.json();
};
/**
* [ADMIN ONLY] Creates a new group.
*/
export const createAdminGroup = async (groupData) => {
const userId = getUserId();
const response = await fetch(`${API_BASE_URL}/users/admin/groups`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-User-ID": userId,
},
body: JSON.stringify(groupData),
});
if (!response.ok) {
const errData = await response.json().catch(() => ({}));
throw new Error(errData.detail || "Failed to create group");
}
return await response.json();
};
/**
* [ADMIN ONLY] Updates a group.
*/
export const updateAdminGroup = async (groupId, groupData) => {
const userId = getUserId();
const response = await fetch(`${API_BASE_URL}/users/admin/groups/${groupId}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
"X-User-ID": userId,
},
body: JSON.stringify(groupData),
});
if (!response.ok) {
const errData = await response.json().catch(() => ({}));
throw new Error(errData.detail || "Failed to update group");
}
return await response.json();
};
/**
* [ADMIN ONLY] Deletes a group.
*/
export const deleteAdminGroup = async (groupId) => {
const userId = getUserId();
const response = await fetch(`${API_BASE_URL}/users/admin/groups/${groupId}`, {
method: "DELETE",
headers: { "X-User-ID": userId },
});
if (!response.ok) throw new Error("Failed to delete group");
return await response.json();
};