2 directories, 29 files

tinai

Home / testing / ai / tinai
import Api from './api.js';
import { generateUUID } from './util/format-utils.js';

/**
 * Class Storage
 * Manages the application's local storage persistence.
 * Handles configuration settings, the global conversation index,
 * and individual conversation history data.
 */
class Storage {
	/**
	 * Initializes Api instance and default prefix state.
	 */
	constructor() {
		this.api = new Api();
		this.user_id_prefix = '';
		this.KEY_LOCATION_ID = 'LOCATION_ID';
	}

	/**
	 * Initializes location ID, loads models, retrieves user ID, and sets up storage keys.
	 * @returns {Promise<void>}
	 */
	async init() {
		this.init_location_id();
		await this.api.load_models();
		const user_id = await this.api.get_user_id();
		if (user_id) {
			this.user_id_prefix = user_id + '_';
		}
		this.KEY_APP_CONFIG = this.user_id_prefix + 'APP_CONFIG';
		this.KEY_CONFIG_THEME = 'THEME';
		this.KEY_CONFIG_SELECTED_CONVERSATION_GUID = 'SELECTED_CONVERSATION_GUID';
		this.KEY_CONFIG_EXPERIMENTAL_FEATURES = 'EXPERIMENTAL_FEATURES';
		this.KEY_CONFIG_BACKGROUND_KEEP_ALIVE = 'BACKGROUND_KEEP_ALIVE';
		this.KEY_CONFIG_DEFAULTS = this.user_id_prefix + 'APP_CONFIG_DEFAULTS';
		this.KEY_CONFIG_DEFAULT_VERBOSITY = 'VERBOSITY';
		this.KEY_CONFIG_DEFAULT_MODEL = 'MODEL';
		this.KEY_CONFIG_SHOW_SUGGESTED_QUERIES = 'SHOW_SUGGESTED_QUERIES';
		this.KEY_CONFIG_SHOW_RELATED_QUERIES = 'SHOW_RELATED_QUERIES';
		this.KEY_CONFIG_ENFORCE_TOPICS = 'ENFORCE_TOPICS';
		this.KEY_CONFIG_AUTO_RUN_PROPOSED_QUERIES = 'AUTO_RUN_PROPOSED_QUERIES';
		this.KEY_APP_INDEX = this.user_id_prefix + 'APP_INDEX';
		this.KEY_INDEX_DATE_CREATED = 'DATE_CREATED';
		this.KEY_INDEX_DATE_UPDATED = 'DATE_UPDATED';
		this.KEY_INDEX_SYNC = 'SYNC';
		this.KEY_INDEX_GUID = 'GUID';
		this.KEY_SHOW_ARCHIVES = 'SHOW_ARCHIVES';
		this.KEY_APP_CONVERSATION_PREFIX = this.user_id_prefix + 'CONVERSATION_';
		this.KEY_CONVERSATION_TITLE = 'TITLE';
		this.KEY_CONVERSATION_SUMMARY = 'SUMMARY';
		this.KEY_CONVERSATION_TIMESTAMP = 'TIMESTAMP';
		this.KEY_CONVERSATION_HISTORY = 'HISTORY';
		this.KEY_CONVERSATION_ARCHIVED = 'ARCHIVED';
		this.KEY_CONVERSATION_VERBOSITY = 'VERBOSITY';
		this.KEY_CONVERSATION_MODEL = 'MODEL';
		this.KEY_CONVERSATION_TYPE = 'TYPE';
		this.KEY_CONVERSATION_SHOW_SUGGESTED_QUERIES = 'SHOW_SUGGESTED_QUERIES';
		this.KEY_CONVERSATION_SHOW_RELATED_QUERIES = 'SHOW_RELATED_QUERIES';
		this.KEY_CONVERSATION_ENFORCE_TOPICS = 'ENFORCE_TOPICS';
		this.KEY_CONVERSATION_AUTO_RUN_PROPOSED_QUERIES = 'AUTO_RUN_PROPOSED_QUERIES';
		this.KEY_CONVERSATION_TOPICS = 'TOPICS';
		this.KEY_CONVERSATION_CONSIDERATIONS = 'CONSIDERATIONS';
		this.KEY_CONVERSATION_GOOGLE_SEARCH = 'GOOGLE_SEARCH';
		this.KEY_CONVERSATION_DOCUMENT = 'DOCUMENT';
		this.KEY_CONVERSATION_DICTION = 'DICTION';
		this.CONVERSATION_TYPE_CHAT = 'CHAT';
		this.CONVERSATION_TYPE_NOTEBOOK = 'NOTEBOOK';
		this.CONVERSATION_TYPE_SCRATCHPAD = 'SCRATCHPAD';
	}

	/**
	 * Ensures a unique location UUID exists in localStorage.
	 */
	init_location_id() {
		let location_id = localStorage.getItem(this.KEY_LOCATION_ID);
		if (!location_id) {
			location_id = generateUUID();
			localStorage.setItem(this.KEY_LOCATION_ID, location_id);
		}
	}

	/**
	 * Retrieves the location UUID from localStorage.
	 * @returns {string|null}
	 */
	get_location_id() {
		return localStorage.getItem(this.KEY_LOCATION_ID);
	}

	//region App Defaults

	/**
	 * Updates a specific key in the application defaults object and dispatches a storage event.
	 * @param {string} key The key for the item in the APP_CONFIG_DEFAULTS object to be updated.
	 * @param {*} value The new value to be assigned to the key.
	 */
	update_app_defaults(key, value) {
		const defaults = this.get_app_defaults();
		defaults[key] = value;
		localStorage.setItem(this.KEY_CONFIG_DEFAULTS, JSON.stringify(defaults));
		const event = new StorageEvent('storage', {
			key: this.KEY_CONFIG_DEFAULTS
		});
		window.dispatchEvent(event);
	}

	/**
	 * Retrieves the current application defaults object from localStorage.
	 * @returns {Object} The parsed defaults object.
	 */
	get_app_defaults() {
		const defaults = JSON.parse(localStorage.getItem(this.KEY_CONFIG_DEFAULTS) || '{}');
		if (defaults[this.KEY_CONFIG_DEFAULT_VERBOSITY] === undefined) {
			defaults[this.KEY_CONFIG_DEFAULT_VERBOSITY] = 'standard';
		}
		if (defaults[this.KEY_CONFIG_DEFAULT_MODEL] === undefined) {
			defaults[this.KEY_CONFIG_DEFAULT_MODEL] = 'basic';
		}
		if (defaults[this.KEY_CONFIG_SHOW_SUGGESTED_QUERIES] === undefined) {
			defaults[this.KEY_CONFIG_SHOW_SUGGESTED_QUERIES] = false;
		}
		if (defaults[this.KEY_CONFIG_SHOW_RELATED_QUERIES] === undefined) {
			defaults[this.KEY_CONFIG_SHOW_RELATED_QUERIES] = false;
		}
		if (defaults[this.KEY_CONFIG_ENFORCE_TOPICS] === undefined) {
			defaults[this.KEY_CONFIG_ENFORCE_TOPICS] = false;
		}
		if (defaults[this.KEY_CONFIG_AUTO_RUN_PROPOSED_QUERIES] === undefined) {
			defaults[this.KEY_CONFIG_AUTO_RUN_PROPOSED_QUERIES] = false;
		}
		return defaults;
	}

	//endregion

	//region App Configuration

	/**
	 * Updates a specific key in the application configuration object and dispatches a storage event.
	 * @param {string} key The key for the item in the APP_CONFIG object to be updated.
	 * @param {*} value The new value to be assigned to the key.
	 */
	update_app_config(key, value) {
		const config = this.get_app_config();
		config[key] = value;
		localStorage.setItem(this.KEY_APP_CONFIG, JSON.stringify(config));
		const event = new StorageEvent('storage', {
			key: this.KEY_APP_CONFIG
		});
		window.dispatchEvent(event);
	}

	/**
	 * Retrieves the current application configuration object from localStorage.
	 * @returns {Object} The parsed configuration object.
	 */
	get_app_config() {
		return JSON.parse(localStorage.getItem(this.KEY_APP_CONFIG) || '{}');
	}

	//endregion

	//region App Index Management

	/**
	 * Updates or creates metadata for a conversation in the global application index.
	 * @param {string} guid The unique identifier of the conversation. If empty, a new GUID is generated.
	 * @param {boolean} sync Whether the conversation should be marked for synchronization.
	 * @param {string} type The type of conversation, defaults to CHAT.
	 * @returns {string} The GUID of the updated or newly created conversation.
	 */
	update_app_index(guid, sync, type = this.CONVERSATION_TYPE_CHAT) {
		const index = this.get_app_index();
		const date_string = new Date().toISOString();
		if (!guid) {
			guid = generateUUID();
			index.push({
				[this.KEY_INDEX_GUID]: guid,
				[this.KEY_INDEX_DATE_CREATED]: date_string,
				[this.KEY_INDEX_DATE_UPDATED]: date_string,
				[this.KEY_INDEX_SYNC]: sync,
				[this.KEY_CONVERSATION_TYPE]: type
			});
		} else {
			const index_item = index.find(item => item[this.KEY_INDEX_GUID] === guid);
			if (index_item) {
				index_item[this.KEY_INDEX_DATE_UPDATED] = date_string;
				index_item[this.KEY_INDEX_SYNC] = sync;
				if (type && !index_item[this.KEY_CONVERSATION_TYPE]) {
					index_item[this.KEY_CONVERSATION_TYPE] = type;
				}
			} else {
				index.push({
					[this.KEY_INDEX_GUID]: guid,
					[this.KEY_INDEX_DATE_CREATED]: date_string,
					[this.KEY_INDEX_DATE_UPDATED]: date_string,
					[this.KEY_INDEX_SYNC]: sync,
					[this.KEY_CONVERSATION_TYPE]: type
				});
			}
		}
		const conversation = this.get_conversation(guid);
		if (!conversation[this.KEY_CONVERSATION_TYPE]) {
			conversation[this.KEY_CONVERSATION_TYPE] = type;
			if (type === this.CONVERSATION_TYPE_NOTEBOOK) {
				conversation[this.KEY_CONVERSATION_TITLE] = 'New Notebook';
			} else if (type === this.CONVERSATION_TYPE_SCRATCHPAD) {
				conversation[this.KEY_CONVERSATION_TITLE] = 'New Scratchpad';
			}
			this.save_conversation(guid, conversation);
		}

		this.save_app_index(index);
		const config = this.get_app_config();
		config[this.KEY_CONFIG_SELECTED_CONVERSATION_GUID] = guid;
		this.update_app_config(this.KEY_CONFIG_SELECTED_CONVERSATION_GUID, guid);

		return guid;
	}

	/**
	 * Deletes a conversation by its GUID from the application index and localStorage.
	 * @param {string} guid The unique identifier of the conversation to delete.
	 */
	index_delete(guid) {
		const config = this.get_app_config();
		if (config[this.KEY_CONFIG_SELECTED_CONVERSATION_GUID] === guid) {
			this.update_app_config(this.KEY_CONFIG_SELECTED_CONVERSATION_GUID, '');
		}
		let index = this.get_app_index();
		index = index.filter(item => item[this.KEY_INDEX_GUID] !== guid);
		localStorage.setItem(this.KEY_APP_INDEX, JSON.stringify(index));
		localStorage.removeItem(this.KEY_APP_CONVERSATION_PREFIX + guid);
		const event = new StorageEvent('storage', {
			key: this.KEY_APP_INDEX
		});
		window.dispatchEvent(event);
	}

	/**
	 * Saves the global application conversation index array to localStorage.
	 * @param {Array<Object>} index The conversation index array to save.
	 */
	save_app_index(index) {
		localStorage.setItem(this.KEY_APP_INDEX, JSON.stringify(index));
		const event = new StorageEvent('storage', {
			key: this.KEY_APP_INDEX
		});
		window.dispatchEvent(event);
	}

	/**
	 * Retrieves the global application conversation index from localStorage.
	 * @returns {Array<Object>} The conversation index array.
	 */
	get_app_index() {
		return JSON.parse(localStorage.getItem(this.KEY_APP_INDEX) || '[]');
	}

	//endregion

	//region Conversation Data Management

	/**
	 * Retrieves the full conversation object for the given GUID from localStorage.
	 * @param {string} guid The unique identifier of the conversation.
	 * @returns {Object} The conversation object or an empty object if not found.
	 */
	get_conversation(guid) {
		if (!guid) return {};
		const conversation = JSON.parse(localStorage.getItem(this.KEY_APP_CONVERSATION_PREFIX + guid) || '{}');
		if (conversation[this.KEY_CONVERSATION_SHOW_SUGGESTED_QUERIES] === undefined) {
			conversation[this.KEY_CONVERSATION_SHOW_SUGGESTED_QUERIES] = false;
		}
		if (conversation[this.KEY_CONVERSATION_SHOW_RELATED_QUERIES] === undefined) {
			conversation[this.KEY_CONVERSATION_SHOW_RELATED_QUERIES] = false;
		}
		if (conversation[this.KEY_CONVERSATION_ENFORCE_TOPICS] === undefined) {
			conversation[this.KEY_CONVERSATION_ENFORCE_TOPICS] = false;
		}
		if (conversation[this.KEY_CONVERSATION_AUTO_RUN_PROPOSED_QUERIES] === undefined) {
			conversation[this.KEY_CONVERSATION_AUTO_RUN_PROPOSED_QUERIES] = false;
		}
		if (conversation[this.KEY_CONVERSATION_TOPICS] === undefined) {
			conversation[this.KEY_CONVERSATION_TOPICS] = [];
		}
		if (conversation[this.KEY_CONVERSATION_CONSIDERATIONS] === undefined) {
			conversation[this.KEY_CONVERSATION_CONSIDERATIONS] = [];
		}
		if (conversation[this.KEY_CONVERSATION_GOOGLE_SEARCH] === undefined) {
			conversation[this.KEY_CONVERSATION_GOOGLE_SEARCH] = false;
		}
		return conversation;
	}

	/**
	 * Retrieves the full conversation object for the currently selected GUID from localStorage.
	 * @returns {Object|null} The conversation object or null if none selected.
	 */
	get_selected_conversation() {
		const config = this.get_app_config();
		const guid = config[this.KEY_CONFIG_SELECTED_CONVERSATION_GUID];
		if (!guid) return null;
		return this.get_conversation(guid);
	}

	/**
	 * Saves the full conversation object for the given GUID to localStorage.
	 * @param {string} guid The unique identifier of the conversation.
	 * @param {Object} conversation The conversation object to save.
	 */
	save_conversation(guid, conversation) {
		if (!guid) return;
		localStorage.setItem(this.KEY_APP_CONVERSATION_PREFIX + guid, JSON.stringify(conversation));
		window.dispatchEvent(new StorageEvent('storage', {
			key: this.KEY_APP_CONVERSATION_PREFIX + guid
		}));
	}

	/**
	 * Updates a single field in a conversation and dispatches a storage event.
	 * @param {string} guid The unique identifier of the conversation.
	 * @param {string} field The field name to update.
	 * @param {*} value The value to assign to the field.
	 */
	update_conversation_field(guid, field, value) {
		if (!guid) return;
		const conversation = this.get_conversation(guid);
		conversation[field] = value;
		this.save_conversation(guid, conversation);
	}

	/**
	 * Adds a new topic to a conversation if it is not already present.
	 * @param {string} guid The unique identifier of the conversation.
	 * @param {string} topic The topic to add.
	 */
	add_conversation_topic(guid, topic) {
		if (!guid || !topic) return;
		const conversation = this.get_conversation(guid);
		const topics = conversation[this.KEY_CONVERSATION_TOPICS] || [];
		if (!topics.includes(topic)) {
			topics.push(topic);
			this.update_conversation_field(guid, this.KEY_CONVERSATION_TOPICS, topics);
		}
	}

	/**
	 * Alias for add_conversation_topic.
	 * @param {string} guid - The unique identifier of the conversation.
	 * @param {string} topic - The topic to add.
	 */
	addTopicToConversation(guid, topic) {
		this.add_conversation_topic(guid, topic);
	}

	/**
	 * Removes a topic from a conversation.
	 * @param {string} guid The unique identifier of the conversation.
	 * @param {string} topic The topic to remove.
	 */
	remove_conversation_topic(guid, topic) {
		if (!guid || !topic) return;
		const conversation = this.get_conversation(guid);
		let topics = conversation[this.KEY_CONVERSATION_TOPICS] || [];
		if (topics.includes(topic)) {
			topics = topics.filter(t => t !== topic);
			this.update_conversation_field(guid, this.KEY_CONVERSATION_TOPICS, topics);
		}
	}

	/**
	 * Alias for remove_conversation_topic.
	 * @param {string} guid - The unique identifier of the conversation.
	 * @param {string} topic - The topic to remove.
	 */
	deleteTopicFromConversation(guid, topic) {
		this.remove_conversation_topic(guid, topic);
	}

	/**
	 * Adds a new consideration to a conversation if it is not already present.
	 * @param {string} guid The unique identifier of the conversation.
	 * @param {string} consideration The consideration to add.
	 */
	add_conversation_consideration(guid, consideration) {
		if (!guid || !consideration) return;
		const conversation = this.get_conversation(guid);
		const considerations = conversation[this.KEY_CONVERSATION_CONSIDERATIONS] || [];
		if (!considerations.includes(consideration)) {
			considerations.push(consideration);
			this.update_conversation_field(guid, this.KEY_CONVERSATION_CONSIDERATIONS, considerations);
		}
	}

	/**
	 * Alias for add_conversation_consideration.
	 * @param {string} guid - The unique identifier of the conversation.
	 * @param {string} consideration - The consideration to add.
	 */
	addConsiderationToConversation(guid, consideration) {
		this.add_conversation_consideration(guid, consideration);
	}

	/**
	 * Removes a consideration from a conversation.
	 * @param {string} guid The unique identifier of the conversation.
	 * @param {string} consideration The consideration to remove.
	 */
	remove_conversation_consideration(guid, consideration) {
		if (!guid || !consideration) return;
		const conversation = this.get_conversation(guid);
		let considerations = conversation[this.KEY_CONVERSATION_CONSIDERATIONS] || [];
		if (considerations.includes(consideration)) {
			considerations = considerations.filter(c => c !== consideration);
			this.update_conversation_field(guid, this.KEY_CONVERSATION_CONSIDERATIONS, considerations);
		}
	}

	/**
	 * Alias for remove_conversation_consideration.
	 * @param {string} guid - The unique identifier of the conversation.
	 * @param {string} consideration - The consideration to remove.
	 */
	deleteConsiderationFromConversation(guid, consideration) {
		this.remove_conversation_consideration(guid, consideration);
	}

	//endregion
}

export default Storage;
🌐
storage.js ×
Type: Web, text/plain
15.25 Kilobytes
Last Modified 2026-09-04 14:24:12
⬇ Download File