-
🌐 admin_panel.phpWeb, 1.54 kB⬇
-
🌐 api.jsWeb, 6.70 kB⬇
-
🌐 api.phpWeb, 6.80 kB⬇
-
🌐 app.jsWeb, 105.13 kB⬇
-
🌐 base.cssWeb, 2.23 kB⬇
-
🌐 context.jsWeb, 9.37 kB⬇
-
🌐 conversation-index.jsWeb, 10.19 kB⬇
-
🌐 conversation.jsWeb, 18.63 kB⬇
-
🌐 conversations.jsWeb, 16.57 kB⬇
-
🌐 diction.jsWeb, 935 B⬇
-
🌐 document.jsWeb, 947 B⬇
-
🌐 dynamic.phpWeb, 430 B⬇
-
📷 favicon-lg.pngImage, 9.11 kB⬇
-
📷 favicon.pngImage, 3.12 kB⬇
-
🌐 index.phpWeb, 20.30 kB⬇
-
🌐 indexjs.jsWeb, 572 B⬇
-
🌐 login.phpWeb, 787 B⬇
-
❓ manifest.webmanifestUnknown, 262 B⬇
-
🌐 memory.jsWeb, 407 B⬇
-
🌐 model-presets.jsonWeb, 4.27 kB⬇
-
🌐 models.jsonWeb, 3.42 kB⬇
-
🌐 notebook-index.jsWeb, 721 B⬇
-
⚠️ php.iniOS File, 356 B⬇
-
🌐 scratchpad-conversation.jsWeb, 2.89 kB⬇
-
🌐 scratchpad-index.jsWeb, 10.25 kB⬇
-
🌐 storage.jsWeb, 18.96 kB⬇
-
🌐 style.cssWeb, 23.14 kB⬇
-
🌐 theme_dark.jsonWeb, 360 B⬇
-
🌐 theme_light.jsonWeb, 361 B⬇
-
🌐 users.jsWeb, 23.71 kB⬇
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_DEFAULT_MODEL_FAMILY = 'MODEL_FAMILY';
this.KEY_CONFIG_DEFAULT_MODEL_VERSION = 'MODEL_VERSION';
this.KEY_CONFIG_DEFAULT_THINKING_LEVEL = 'THINKING_LEVEL';
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_CONFIG_PLAY_CHIME = 'PLAY_CHIME';
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_MODEL_FAMILY = 'MODEL_FAMILY';
this.KEY_CONVERSATION_MODEL_VERSION = 'MODEL_VERSION';
this.KEY_CONVERSATION_THINKING_LEVEL = 'THINKING_LEVEL';
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_DEFAULT_MODEL_VERSION] === undefined || defaults[this.KEY_CONFIG_DEFAULT_MODEL_FAMILY] === undefined) {
const presetKey = defaults[this.KEY_CONFIG_DEFAULT_MODEL] || 'basic';
const preset = Api.PRESETS[presetKey] || Api.PRESETS['basic'];
if (preset) {
defaults[this.KEY_CONFIG_DEFAULT_MODEL_FAMILY] = preset.provider || 'gemini';
defaults[this.KEY_CONFIG_DEFAULT_MODEL_VERSION] = preset.model || 'gemini-3.5-flash-lite';
defaults[this.KEY_CONFIG_DEFAULT_THINKING_LEVEL] = preset.thinking !== undefined ? preset.thinking : 'MINIMAL';
} else {
defaults[this.KEY_CONFIG_DEFAULT_MODEL_FAMILY] = 'gemini';
defaults[this.KEY_CONFIG_DEFAULT_MODEL_VERSION] = 'gemini-3.5-flash-lite';
defaults[this.KEY_CONFIG_DEFAULT_THINKING_LEVEL] = 'MINIMAL';
}
}
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;
}
if (defaults[this.KEY_CONFIG_PLAY_CHIME] === undefined) {
defaults[this.KEY_CONFIG_PLAY_CHIME] = 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 = null) {
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 || false,
[this.KEY_CONVERSATION_TYPE]: type || this.CONVERSATION_TYPE_CHAT
});
} else {
const conversation_meta = index.find(item => item[this.KEY_INDEX_GUID] === guid);
if (conversation_meta) {
conversation_meta[this.KEY_INDEX_DATE_UPDATED] = date_string;
conversation_meta[this.KEY_INDEX_SYNC] = sync || false;
if (type) {
conversation_meta[this.KEY_CONVERSATION_TYPE] = type;
}
}
}
localStorage.setItem(this.KEY_APP_INDEX, JSON.stringify(index));
const event = new StorageEvent('storage', {
key: this.KEY_APP_INDEX
});
window.dispatchEvent(event);
return guid;
}
/**
* Retrieves the global application index array from localStorage.
* @returns {Array<Object>} The array of conversation metadata objects.
*/
get_app_index() {
return JSON.parse(localStorage.getItem(this.KEY_APP_INDEX) || '[]');
}
/**
* Removes a conversation's metadata from the application index.
* @param {string} guid The unique identifier of the conversation to delete.
*/
index_delete(guid) {
const index = this.get_app_index();
const new_index = index.filter(item => item[this.KEY_INDEX_GUID] !== guid);
localStorage.setItem(this.KEY_APP_INDEX, JSON.stringify(new_index));
localStorage.removeItem(this.KEY_APP_CONVERSATION_PREFIX + guid);
const event = new StorageEvent('storage', {
key: this.KEY_APP_INDEX
});
window.dispatchEvent(event);
}
delete_from_app_index(guid) {
this.index_delete(guid);
}
/**
* Deletes all conversation records and index entries from localStorage.
*/
delete_all_conversations() {
const index = this.get_app_index();
index.forEach(item => {
const guid = item[this.KEY_INDEX_GUID];
localStorage.removeItem(this.KEY_APP_CONVERSATION_PREFIX + guid);
});
localStorage.removeItem(this.KEY_APP_INDEX);
const event = new StorageEvent('storage', {
key: this.KEY_APP_INDEX
});
window.dispatchEvent(event);
}
//endregion
//region Conversation Management
/**
* Retrieves a full conversation object by its GUID from localStorage.
* Initializes default fields if they do not exist.
* @param {string} guid The unique identifier of the conversation.
* @returns {Object} The complete conversation object with defaults.
*/
get_conversation(guid) {
let conversation = JSON.parse(localStorage.getItem(this.KEY_APP_CONVERSATION_PREFIX + guid) || '{}');
const app_defaults = this.get_app_defaults();
const index = this.get_app_index();
const index_item = index.find(item => item[this.KEY_INDEX_GUID] === guid);
const index_type = index_item?.[this.KEY_CONVERSATION_TYPE];
if (conversation[this.KEY_CONVERSATION_TYPE] === undefined) {
conversation[this.KEY_CONVERSATION_TYPE] = index_type || this.CONVERSATION_TYPE_CHAT;
}
if (!conversation[this.KEY_CONVERSATION_TITLE]) {
if (conversation[this.KEY_CONVERSATION_TYPE] === this.CONVERSATION_TYPE_SCRATCHPAD) {
conversation[this.KEY_CONVERSATION_TITLE] = 'New Scratchpad';
} else if (conversation[this.KEY_CONVERSATION_TYPE] === this.CONVERSATION_TYPE_NOTEBOOK) {
conversation[this.KEY_CONVERSATION_TITLE] = 'New Notebook';
} else {
conversation[this.KEY_CONVERSATION_TITLE] = 'New Conversation';
}
}
if (!conversation[this.KEY_CONVERSATION_SUMMARY]) {
conversation[this.KEY_CONVERSATION_SUMMARY] = '';
}
if (!conversation[this.KEY_CONVERSATION_TIMESTAMP]) {
conversation[this.KEY_CONVERSATION_TIMESTAMP] = new Date().toISOString();
}
if (!conversation[this.KEY_CONVERSATION_HISTORY]) {
conversation[this.KEY_CONVERSATION_HISTORY] = [];
}
if (conversation[this.KEY_CONVERSATION_ARCHIVED] === undefined) {
conversation[this.KEY_CONVERSATION_ARCHIVED] = false;
}
if (conversation[this.KEY_CONVERSATION_VERBOSITY] === undefined) {
conversation[this.KEY_CONVERSATION_VERBOSITY] = app_defaults[this.KEY_CONFIG_DEFAULT_VERBOSITY];
}
if (conversation[this.KEY_CONVERSATION_MODEL] === undefined) {
conversation[this.KEY_CONVERSATION_MODEL] = app_defaults[this.KEY_CONFIG_DEFAULT_MODEL];
}
if (conversation[this.KEY_CONVERSATION_MODEL_FAMILY] === undefined) {
conversation[this.KEY_CONVERSATION_MODEL_FAMILY] = app_defaults[this.KEY_CONFIG_DEFAULT_MODEL_FAMILY] || 'gemini';
}
if (conversation[this.KEY_CONVERSATION_MODEL_VERSION] === undefined) {
conversation[this.KEY_CONVERSATION_MODEL_VERSION] = app_defaults[this.KEY_CONFIG_DEFAULT_MODEL_VERSION] || 'gemini-3.5-flash-lite';
}
if (conversation[this.KEY_CONVERSATION_THINKING_LEVEL] === undefined) {
conversation[this.KEY_CONVERSATION_THINKING_LEVEL] = app_defaults[this.KEY_CONFIG_DEFAULT_THINKING_LEVEL] !== undefined ? app_defaults[this.KEY_CONFIG_DEFAULT_THINKING_LEVEL] : 'MINIMAL';
}
if (conversation[this.KEY_CONVERSATION_SHOW_SUGGESTED_QUERIES] === undefined) {
conversation[this.KEY_CONVERSATION_SHOW_SUGGESTED_QUERIES] = app_defaults[this.KEY_CONFIG_SHOW_SUGGESTED_QUERIES];
}
if (conversation[this.KEY_CONVERSATION_SHOW_RELATED_QUERIES] === undefined) {
conversation[this.KEY_CONVERSATION_SHOW_RELATED_QUERIES] = app_defaults[this.KEY_CONFIG_SHOW_RELATED_QUERIES];
}
if (conversation[this.KEY_CONVERSATION_ENFORCE_TOPICS] === undefined) {
conversation[this.KEY_CONVERSATION_ENFORCE_TOPICS] = app_defaults[this.KEY_CONFIG_ENFORCE_TOPICS];
}
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;
}
// Backward compatibility: If fine-grained keys are not present, look up from old MODEL preset key
if (conversation[this.KEY_CONVERSATION_MODEL_VERSION] === undefined && conversation[this.KEY_CONVERSATION_MODEL]) {
const presetKey = conversation[this.KEY_CONVERSATION_MODEL];
const preset = Api.PRESETS[presetKey];
if (preset) {
conversation[this.KEY_CONVERSATION_MODEL_FAMILY] = preset.provider || 'gemini';
conversation[this.KEY_CONVERSATION_MODEL_VERSION] = preset.model || 'gemini-3.5-flash-lite';
conversation[this.KEY_CONVERSATION_THINKING_LEVEL] = preset.thinking !== undefined ? preset.thinking : null;
localStorage.setItem(this.KEY_APP_CONVERSATION_PREFIX + guid, JSON.stringify(conversation));
}
}
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
18.96 Kilobytes
Last Modified 2026-09-05 04:03:10
⬇ Download File
Type: Web, text/plain
18.96 Kilobytes
Last Modified 2026-09-05 04:03:10
⬇ Download File