2 directories, 29 files

tinai

Home / testing / ai / tinai
/**
 * Service class to handle communication with the TinAI backend API.
 */
class Api {

	static MODELS = {};

	#endpoint = 'api.php';
	static #modelsPromise = null;

	/**
	 * Initializes the Api service and triggers loading of model definitions.
	 */
	constructor() {
		void this.load_models();
	}

	/**
	 * Loads available model definitions from models.json.
	 * @returns {Promise<Object>}
	 */
	async load_models() {
		if (Object.keys(Api.MODELS).length > 0) {
			return Api.MODELS;
		}
		if (Api.#modelsPromise) {
			return Api.#modelsPromise;
		}
		Api.#modelsPromise = (async () => {
			try {
				const response = await fetch('models.json');
				Api.MODELS = await response.json();
				return Api.MODELS;
			} catch (error) {
				console.error('Failed to load models.json:', error);
				return {};
			} finally {
				Api.#modelsPromise = null;
			}
		})();
		return Api.#modelsPromise;
	}

	/**
	 * Formats the query and context into a data object for the API request.
	 * @param {string} [query=''] - The user's input string.
	 * @param {Array} [context=[]] - The conversational history/context.
	 * @param {string} [verbosity='standard'] - The level of verbosity requested.
	 * @param {string} [model_key='basic'] - The key for the selected model.
	 * @param {object} [meta_context={}] - Meta context object.
	 * @param {boolean} [debug=false] - Whether to enable debug mode.
	 * @returns {Object} The formatted data object.
	 */
	format_data(query = '', context = [], verbosity = 'standard', model_key = 'basic', meta_context = {}, debug = false) {
		return {
			context: Array.isArray(context) ? context : [],
			debug: !!debug,
			query: query || '',
			verbosity: verbosity || 'standard',
			model_key: model_key || 'basic',
			meta_context: meta_context || {}
		};
	}

	/**
	 * Sends a POST request to the API endpoint with optional NDJSON streaming and abort support.
	 * @param {Object} data - The payload to send.
	 * @param {Function} on_success - Callback executed on successful response.
	 * @param {Function} on_failure - Callback executed on request or parsing error.
	 * @param {string} [function_name='chat'] - The API function to call.
	 * @param {Function|null} [on_thinking=null] - Callback executed on each thinking chunk.
	 * @param {Function|null} [on_progress=null] - Callback executed with bytes received.
	 * @param {AbortSignal|null} [signal=null] - Abort signal to cancel the request.
	 * @returns {Promise<void>}
	 */
	async post(data, on_success, on_failure, function_name = 'chat', on_thinking = null, on_progress = null, signal = null) {
		const safeSuccess = typeof on_success === 'function' ? on_success : () => {};
		const safeFailure = typeof on_failure === 'function' ? on_failure : (err) => console.error(err);

		try {
			const url = `${this.#endpoint}?function=${function_name}`;
			const response = await fetch(url, {
				method: 'POST',
				headers: {
					'Content-Type': 'application/json'
				},
				body: JSON.stringify(data),
				signal: signal
			});

			if (response.status === 401) {
				console.warn('Session expired. Redirecting to login page.');
				window.location.reload();
				return;
			}

			const contentType = response.headers.get('content-type') || '';
			if (contentType.includes('application/x-ndjson')) {
				if (!response.body || typeof response.body.getReader !== 'function') {
					safeFailure(new Error('ReadableStream not supported by browser environment.'));
					return;
				}

				const reader = response.body.getReader();
				const decoder = new TextDecoder();
				let buffer = '';
				let lastResponseObject = null;
				let bytesReceived = 0;

				while (true) {
					const { done, value } = await reader.read();
					if (done) break;

					bytesReceived += value.length;
					if (typeof on_progress === 'function') {
						on_progress(bytesReceived);
					}

					buffer += decoder.decode(value, { stream: true });
					const lines = buffer.split('\n');
					buffer = lines.pop(); // Keep the last incomplete line

					for (const line of lines) {
						if (line.trim()) {
							try {
								const parsed = JSON.parse(line);
								if (parsed && parsed.type === 'thinking') {
									if (typeof on_thinking === 'function') {
										on_thinking(parsed.text);
									}
								} else {
									lastResponseObject = parsed;
								}
							} catch (e) {
								console.error('Failed to parse stream line:', line, e);
							}
						}
					}
				}

				if (buffer.trim()) {
					try {
						const parsed = JSON.parse(buffer);
						if (parsed && parsed.type === 'thinking') {
							if (typeof on_thinking === 'function') {
								on_thinking(parsed.text);
							}
						} else {
							lastResponseObject = parsed;
						}
					} catch (e) {
						console.error('Failed to parse final stream buffer:', buffer, e);
					}
				}

				if (lastResponseObject) {
					if (lastResponseObject.error) {
						safeFailure(new Error(lastResponseObject.error));
					} else {
						safeSuccess(lastResponseObject);
					}
				} else {
					safeFailure(new Error('No final response received from stream'));
				}
			} else {
				const response_object = await response.json();
				if (response_object && response_object.error) {
					safeFailure(new Error(response_object.error));
				} else {
					safeSuccess(response_object);
				}
			}
		} catch (error) {
			if (error.name === 'AbortError') {
				console.log('Request aborted.');
			} else {
				console.error('API Error:', error);
				safeFailure(error);
			}
		}
	}

	/**
	 * Retrieves the current user's ID from the backend.
	 * @returns {Promise<string|null>} The user ID or null on failure.
	 */
	async get_user_id() {
		return new Promise((resolve) => {
			this.post({}, (response) => {
				if (response && response.user_id) {
					resolve(response.user_id);
				} else {
					console.error('User ID not found in response');
					resolve(null);
				}
			}, (error) => {
				console.error('Failed to get user ID:', error);
				resolve(null);
			}, 'get_user_id');
		});
	}
}

export default Api;
🌐
api.js ×
Type: Web, text/plain
5.89 Kilobytes
Last Modified 2026-09-04 14:23:33
⬇ Download File