-
🌐 admin_panel.phpWeb, 1.63 kB⬇
-
🌐 api.jsWeb, 3.06 kB⬇
-
🌐 api.phpWeb, 6.18 kB⬇
-
🌐 app.jsWeb, 58.50 kB⬇
-
🌐 base.cssWeb, 2.23 kB⬇
-
🌐 context.jsWeb, 8.71 kB⬇
-
🌐 conversation-index.jsWeb, 11.46 kB⬇
-
🌐 conversation.jsWeb, 9.87 kB⬇
-
🌐 conversations.jsWeb, 14.49 kB⬇
-
🌐 diction.jsWeb, 384 B⬇
-
🌐 document.jsWeb, 391 B⬇
-
🌐 dynamic.phpWeb, 430 B⬇
-
📷 favicon-lg.pngImage, 9.11 kB⬇
-
📷 favicon.pngImage, 3.12 kB⬇
-
🌐 index.phpWeb, 11.54 kB⬇
-
🌐 indexjs.jsWeb, 544 B⬇
-
🌐 login.phpWeb, 619 B⬇
-
🌐 manifest.jsonWeb, 256 B⬇
-
❓ manifest.webmanifestUnknown, 262 B⬇
-
🌐 memory.jsWeb, 407 B⬇
-
🌐 models.jsonWeb, 1.70 kB⬇
-
🌐 notebook-index.jsWeb, 527 B⬇
-
⚠️ php.iniOS File, 356 B⬇
-
🌐 storage.jsWeb, 12.57 kB⬇
-
🌐 style.cssWeb, 16.16 kB⬇
-
🌐 theme_dark.jsonWeb, 266 B⬇
-
🌐 theme_light.jsonWeb, 267 B⬇
-
🌐 users.jsWeb, 12.40 kB⬇
/**
* Class ConversationsList
* Manages the primary navigation list of conversations.
* Handles rendering the list, filtering active/archived conversations,
* and managing multi-selection for bulk actions.
*/
class ConversationsList {
#storage;
#app_callbacks; // { update_app_config, apply_panels_layout, render_conversation_header, close_conversation, set_v_open, set_i_open, on_conversation_updated, set_active_tab }
div_list;
div_title;
btn_select_conversations;
btn_archive_conversations;
btn_delete_conversations;
btn_archives_toggle;
btn_conversations;
btn_show_conversations_new;
state_conversation_select = false;
state_conversations_selected = [];
BREAKPOINT_MOBILE;
constructor(storage_instance, app_callbacks, elements, breakpoints) {
this.#storage = storage_instance;
this.#app_callbacks = app_callbacks;
this.div_list = elements.div_list;
this.div_title = elements.div_title;
this.btn_select_conversations = elements.btn_select_conversations;
this.btn_archive_conversations = elements.btn_archive_conversations;
this.btn_delete_conversations = elements.btn_delete_conversations;
this.btn_archives_toggle = elements.btn_archives_toggle;
this.btn_conversations = elements.btn_conversations;
this.btn_show_conversations_new = elements.btn_show_conversations_new;
this.BREAKPOINT_MOBILE = breakpoints.BREAKPOINT_MOBILE;
this.btn_show_conversations_new.onclick = () => this.#app_callbacks.close_conversation();
}
//region Conversation Management
/**
* Creates a new conversation, updates the index, and sets it as the currently selected conversation.
*/
index_new(type) {
const guid = this.#storage.update_app_index('', false, type);
if (window.innerWidth <= this.BREAKPOINT_MOBILE) {
this.#app_callbacks.set_v_open(false);
this.#app_callbacks.set_i_open(false);
}
this.#app_callbacks.update_app_config(this.#storage.KEY_CONFIG_SELECTED_CONVERSATION_GUID, guid);
if (type === this.#storage.CONVERSATION_TYPE_CHAT) {
this.#app_callbacks.set_active_tab('conversation');
} else if (type === this.#storage.CONVERSATION_TYPE_NOTEBOOK) {
this.#app_callbacks.set_active_tab('document');
}
this.#app_callbacks.on_conversation_updated();
}
/**
* 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) {
this.#storage.index_delete(guid);
this.#app_callbacks.apply_panels_layout();
}
//endregion
//region Selection and Actions
/**
* Updates the UI elements related to conversation selection (button text and disabled states).
* @private
*/
_update_selection_ui() {
const count = this.state_conversations_selected.length;
this.btn_select_conversations.textContent = count > 0 ? count : 'Select';
this.btn_archive_conversations.disabled = count === 0;
this.btn_delete_conversations.disabled = count === 0;
}
/**
* Toggles the multi-selection mode for managing the list of conversations.
*/
toggle_conversation_selection_mode() {
this.state_conversation_select = !this.state_conversation_select;
if (!this.state_conversation_select) {
this.state_conversations_selected = [];
}
this._update_selection_ui();
this.on_app_index_updated();
}
/**
* Archives or unarchives all conversations currently selected in the list.
*/
archive_selected_conversations() {
const count = this.state_conversations_selected.length;
if (count === 0) return;
this.state_conversations_selected.forEach(guid => {
const conversation = this.#storage.get_conversation(guid);
const isArchived = conversation[this.#storage.KEY_CONVERSATION_ARCHIVED] || false;
conversation[this.#storage.KEY_CONVERSATION_ARCHIVED] = !isArchived;
this.#storage.save_conversation(guid, conversation);
});
this.state_conversations_selected = [];
this.state_conversation_select = false;
this._update_selection_ui();
this.on_app_index_updated();
}
/**
* Deletes all conversations currently selected in the list after user confirmation.
*/
delete_selected_conversations() {
const count = this.state_conversations_selected.length;
if (count === 0) return;
const message = count === 1
? "Are you sure you want to delete this conversation?"
: `Are you sure you want to delete ${count} selected conversations?`;
if (confirm(message)) {
const config = this.#storage.get_app_config();
const selected_guid = config[this.#storage.KEY_CONFIG_SELECTED_CONVERSATION_GUID];
// Copy the array to avoid mutation issues during iteration
const toDelete = [...this.state_conversations_selected];
toDelete.forEach(guid => {
if (selected_guid === guid) {
this.#app_callbacks.close_conversation();
}
this.index_delete(guid);
});
this.state_conversations_selected = [];
this.state_conversation_select = false;
this._update_selection_ui();
this.on_app_index_updated();
}
}
/**
* Toggles whether archived conversations are displayed in the main conversations list.
*/
toggle_archives() {
const config = this.#storage.get_app_config();
const current = config[this.#storage.KEY_SHOW_ARCHIVES] || false;
const next = !current;
this.#app_callbacks.update_app_config(this.#storage.KEY_SHOW_ARCHIVES, next);
if (!next) {
const selected_guid = config[this.#storage.KEY_CONFIG_SELECTED_CONVERSATION_GUID];
if (selected_guid) {
const conversation = this.#storage.get_conversation(selected_guid);
if (conversation[this.#storage.KEY_CONVERSATION_ARCHIVED]) {
this.#app_callbacks.close_conversation();
}
}
}
this.on_app_index_updated();
}
//endregion
//region HTML Generation
/**
* Creates and returns a DOM element for a single conversation entry in the navigation list.
* @param {Object} data The metadata from the application index.
* @param {string} guid The GUID of the conversation.
* @param {string} title The display title of the conversation.
* @param {boolean} is_archived Whether the conversation is currently archived.
* @param {string} type The type of conversation.
* @returns {HTMLElement} The created list item element.
*/
create_index_item_element(data, guid, title, is_archived, type) {
const isChecked = this.state_conversations_selected.includes(guid);
const html = this._get_index_item_html(guid, title, is_archived, this.state_conversation_select, isChecked, type);
const temp = document.createElement('div');
temp.innerHTML = html.trim();
const div = temp.firstElementChild;
div.onclick = () => {
if (window.innerWidth <= this.BREAKPOINT_MOBILE) {
this.#app_callbacks.set_v_open(false);
this.#app_callbacks.set_i_open(false);
}
this.#app_callbacks.update_app_config(this.#storage.KEY_CONFIG_SELECTED_CONVERSATION_GUID, guid);
if (type === this.#storage.CONVERSATION_TYPE_CHAT) {
this.#app_callbacks.set_active_tab('conversation');
} else if (type === this.#storage.CONVERSATION_TYPE_NOTEBOOK) {
this.#app_callbacks.set_active_tab('document');
}
this.#app_callbacks.on_conversation_updated();
};
const chk = div.querySelector('.index-item-checkbox');
if (chk) {
chk.onclick = (e) => {
e.stopPropagation();
if (chk.checked) {
this.state_conversations_selected.push(guid);
} else {
this.state_conversations_selected = this.state_conversations_selected.filter(id => id !== guid);
}
this._update_selection_ui();
};
}
return div;
}
/**
* Generates the HTML string for a single conversation index item.
* @param {string} guid The GUID of the conversation.
* @param {string} title The display title of the conversation.
* @param {boolean} is_archived Whether the conversation is currently archived.
* @param {boolean} state_conversation_select Whether conversation selection mode is active.
* @param {boolean} isChecked Whether the checkbox for this item should be checked.
* @param {string} type The type of conversation.
* @returns {string} The HTML string for the conversation index item.
*/
_get_index_item_html(guid, title, is_archived, state_conversation_select, isChecked, type) {
const icon = type === this.#storage.CONVERSATION_TYPE_NOTEBOOK ? '📝' : '💬';
return `
<div class="div-index-item div-index-item-flex ${is_archived ? 'div-conversation-item-archived' : ''}"
id="conversation-${guid}">
${state_conversation_select ? `<input type="checkbox" ${isChecked ? 'checked' : ''} class="index-item-checkbox index-item-checkbox-margin">` : ''}
<span class="index-item-icon as-icon">${icon}</span>
<span class="index-item-text-grow">${title}</span>
</div>
`;
}
/**
* Generates the HTML string for the "Archives" header.
* @returns {string} The HTML string for the archives header.
*/
_get_archive_header_html() {
return '<h5 class="div-index-archive-header">Archives</h5>' +
'<hr/>';
}
/**
* Generates the HTML string for the "No archived conversations" message.
* @returns {string} The HTML string for the no archived conversations message.
*/
_get_no_archive_message_html() {
return '<div class="div-index-archive-empty">No archived conversations</div>';
}
//endregion
//region UI Updates
/**
* Renders the entire conversations navigation list, categorized by active and archived status.
*/
on_app_index_updated(){
const index = this.#storage.get_app_index();
this.div_list.innerHTML = '';
if (index.length === 0) {
this.#app_callbacks.set_v_open(false);
this.btn_conversations.disabled = true;
this.btn_select_conversations.disabled = true;
this.btn_show_conversations_new.style.display = 'none';
this.#app_callbacks.apply_panels_layout();
} else {
this.btn_conversations.disabled = false;
this.btn_select_conversations.disabled = false;
this.btn_show_conversations_new.style.display = '';
}
this._update_selection_ui();
this.btn_archives_toggle.disabled = false;
const config = this.#storage.get_app_config();
const showArchives = config[this.#storage.KEY_SHOW_ARCHIVES] || false;
index.sort((a, b) => b[this.#storage.KEY_INDEX_DATE_UPDATED] - a[this.#storage.KEY_INDEX_DATE_UPDATED]);
const activeItems = [];
const archivedItems = [];
index.forEach(item => {
const guid = item[this.#storage.KEY_INDEX_GUID];
const conversation = this.#storage.get_conversation(guid);
if (conversation[this.#storage.KEY_CONVERSATION_ARCHIVED]) {
archivedItems.push({ item, conversation });
} else {
activeItems.push({ item, conversation });
}
});
activeItems.forEach(data => {
const guid = data.item[this.#storage.KEY_INDEX_GUID];
const title = data.conversation[this.#storage.KEY_CONVERSATION_TITLE] || 'New Conversation';
const type = data.conversation[this.#storage.KEY_CONVERSATION_TYPE] || this.#storage.CONVERSATION_TYPE_CHAT;
this.div_list.appendChild(this.create_index_item_element(data, guid, title, false, type));
});
if (showArchives) {
const tempHeader = document.createElement('div');
tempHeader.innerHTML = this._get_archive_header_html().trim();
while (tempHeader.firstChild) {
this.div_list.appendChild(tempHeader.firstChild);
}
if (archivedItems.length > 0) {
archivedItems.forEach(data => {
const guid = data.item[this.#storage.KEY_INDEX_GUID];
const title = data.conversation[this.#storage.KEY_CONVERSATION_TITLE] || 'New Conversation';
const type = data.conversation[this.#storage.KEY_CONVERSATION_TYPE] || this.#storage.CONVERSATION_TYPE_CHAT;
this.div_list.appendChild(this.create_index_item_element(data, guid, title, true, type));
});
} else {
const tempEmptyMsg = document.createElement('div');
tempEmptyMsg.innerHTML = this._get_no_archive_message_html().trim();
this.div_list.appendChild(tempEmptyMsg.firstElementChild);
}
}
const selected_guid = config[this.#storage.KEY_CONFIG_SELECTED_CONVERSATION_GUID];
if (!selected_guid) {
this.div_title.innerHTML = '<h4>Welcome</h4>';
} else {
this.#app_callbacks.render_conversation_header();
this.#app_callbacks.apply_panels_layout()
}
this.apply_selected_index_class();
}
/**
* Updates the styling of conversation list items to highlight the currently selected conversation.
*/
apply_selected_index_class() {
const config = this.#storage.get_app_config();
const selected_guid = config[this.#storage.KEY_CONFIG_SELECTED_CONVERSATION_GUID];
this.div_list.querySelectorAll('.div-index-item.div-index-item-selected').forEach(item => {
item.classList.remove('div-index-item-selected');
});
const selected_item = document.getElementById('conversation-' + selected_guid);
if (selected_item) {
selected_item.classList.add('div-index-item-selected');
this.#app_callbacks.apply_panels_layout()
}
}
}
export default ConversationsList;
conversations.js
×
Type: Web, text/plain
14.49 Kilobytes
Last Modified 2026-04-25 02:55:58
⬇ Download File
Type: Web, text/plain
14.49 Kilobytes
Last Modified 2026-04-25 02:55:58
⬇ Download File