-
🌐 admin_panel.phpWeb, 1.54 kB⬇
-
🌐 api.jsWeb, 5.89 kB⬇
-
🌐 api.phpWeb, 6.54 kB⬇
-
🌐 app.jsWeb, 84.72 kB⬇
-
🌐 base.cssWeb, 2.23 kB⬇
-
🌐 context.jsWeb, 9.37 kB⬇
-
🌐 conversation-index.jsWeb, 10.19 kB⬇
-
🌐 conversation.jsWeb, 16.61 kB⬇
-
🌐 conversations.jsWeb, 15.16 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, 17.38 kB⬇
-
🌐 indexjs.jsWeb, 572 B⬇
-
🌐 login.phpWeb, 767 B⬇
-
❓ manifest.webmanifestUnknown, 262 B⬇
-
🌐 memory.jsWeb, 407 B⬇
-
🌐 models.jsonWeb, 4.27 kB⬇
-
🌐 notebook-index.jsWeb, 721 B⬇
-
⚠️ php.iniOS File, 356 B⬇
-
🌐 scratchpad-conversation.jsWeb, 2.09 kB⬇
-
🌐 scratchpad-index.jsWeb, 10.19 kB⬇
-
🌐 storage.jsWeb, 15.25 kB⬇
-
🌐 style.cssWeb, 20.81 kB⬇
-
🌐 theme_dark.jsonWeb, 266 B⬇
-
🌐 theme_light.jsonWeb, 267 B⬇
-
🌐 users.jsWeb, 18.18 kB⬇
import { createElementFromHTML } from './util/dom-utils.js';
import { SelectionManager } from './util/selection-manager.js';
import { customConfirm } from './util/confirm-dialog.js';
/**
* 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;
#selection;
div_list;
div_title;
btn_select_conversations;
btn_archive_conversations;
btn_delete_conversations;
btn_archives_toggle;
btn_conversations;
btn_show_conversations_new;
BREAKPOINT_MOBILE;
BREAKPOINT_TABLET;
/**
* Initializes the ConversationsList instance with storage, callbacks, element references, and breakpoints.
* @param {Storage} storage_instance - Storage manager instance.
* @param {object} app_callbacks - Application callback methods.
* @param {object} elements - DOM element references.
* @param {object} breakpoints - Responsive breakpoint constants.
*/
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.BREAKPOINT_TABLET = breakpoints.BREAKPOINT_TABLET;
this.#selection = new SelectionManager(() => this._update_selection_ui());
if (this.btn_show_conversations_new) {
this.btn_show_conversations_new.onclick = () => this.#app_callbacks.close_conversation();
}
}
//region Conversation Management
/**
* Selects and activates a conversation, handling responsive panel transitions.
* @param {string} guid - The conversation GUID.
* @param {string} type - The conversation type.
* @private
*/
_select_and_open_conversation(guid, 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();
}
/**
* Creates a new conversation, updates the index, and sets it as the currently selected conversation.
* @param {string} type - The conversation type.
*/
index_new(type) {
const guid = this.#storage.update_app_index('', false, type);
this._select_and_open_conversation(guid, type);
}
/**
* 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() {
this.#selection.updateControls(
this.btn_select_conversations,
[this.btn_archive_conversations, this.btn_delete_conversations],
'Select'
);
}
/**
* Toggles the multi-selection mode for managing the list of conversations.
*/
toggle_conversation_selection_mode() {
this.#selection.toggleMode();
this._update_selection_ui();
this.on_app_index_updated();
}
/**
* Archives or unarchives all conversations currently selected in the list.
*/
archive_selected_conversations() {
const selectedGuids = this.#selection.selectedItems;
if (selectedGuids.length === 0) return;
selectedGuids.forEach(guid => {
const conversation = this.#storage.get_conversation(guid);
if (conversation) {
const isArchived = conversation[this.#storage.KEY_CONVERSATION_ARCHIVED] || false;
conversation[this.#storage.KEY_CONVERSATION_ARCHIVED] = !isArchived;
this.#storage.save_conversation(guid, conversation);
}
});
this.#selection.clear();
this._update_selection_ui();
this.on_app_index_updated();
}
/**
* Deletes all conversations currently selected in the list after user confirmation.
*/
async delete_selected_conversations() {
const selectedGuids = this.#selection.selectedItems;
const count = selectedGuids.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 (await customConfirm('Delete Conversations', message)) {
const config = this.#storage.get_app_config();
const selected_guid = config[this.#storage.KEY_CONFIG_SELECTED_CONVERSATION_GUID];
const toDelete = [...selectedGuids];
toDelete.forEach(guid => {
if (selected_guid === guid) {
this.#app_callbacks.close_conversation();
}
this.index_delete(guid);
});
this.#selection.clear();
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 && 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.#selection.isSelected(guid);
const html = this._get_index_item_html(guid, title, is_archived, this.#selection.isSelectionMode, isChecked, type);
const div = createElementFromHTML(html);
if (!div) return document.createElement('div');
div.onclick = () => {
this._select_and_open_conversation(guid, type);
};
const chk = div.querySelector('.index-item-checkbox');
if (chk) {
chk.onclick = (e) => {
e.stopPropagation();
this.#selection.toggleItem(guid, chk.checked);
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} isSelectionMode - 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, isSelectionMode, 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}">
${isSelectionMode ? `<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>';
}
/**
* Creates and returns the DOM element for the "New Conversation" trigger button.
* @returns {HTMLElement}
*/
create_new_conversation_trigger_item() {
const html = `
<button class="btn-new-scratchpad btn-new-conversation">
<span class="index-item-icon as-icon" style="margin-right: 0.5em;">➕</span>
<span>New Conversation</span>
</button>
`;
const div = createElementFromHTML(html);
if (!div) return document.createElement('button');
div.onclick = (e) => {
e.stopPropagation();
this.index_new(this.#storage.CONVERSATION_TYPE_CHAT);
};
return div;
}
/**
* Returns the total number of scratchpads in the index.
* @returns {number}
* @private
*/
_get_scratchpad_count() {
const index = this.#storage.get_app_index() || [];
let count = 0;
index.forEach(item => {
const guid = item[this.#storage.KEY_INDEX_GUID];
const conversation = this.#storage.get_conversation(guid);
const type = conversation?.[this.#storage.KEY_CONVERSATION_TYPE] || item?.[this.#storage.KEY_CONVERSATION_TYPE];
if (type === this.#storage.CONVERSATION_TYPE_SCRATCHPAD) {
count++;
}
});
return count;
}
/**
* Creates and returns the DOM element for the Scratchpad list entry.
* @returns {HTMLElement}
*/
create_scratchpad_item_element() {
const config = this.#storage.get_app_config();
const selected_guid = config[this.#storage.KEY_CONFIG_SELECTED_CONVERSATION_GUID];
const conversation = this.#storage.get_conversation(selected_guid);
const isSelected = selected_guid === 'scratchpad' || (conversation && conversation[this.#storage.KEY_CONVERSATION_TYPE] === this.#storage.CONVERSATION_TYPE_SCRATCHPAD);
const count = this._get_scratchpad_count();
const html = `
<div class="div-index-item div-index-item-flex ${isSelected ? 'div-index-item-selected' : ''}"
id="conversation-scratchpad">
<span class="index-item-icon as-icon">📄</span>
<span class="index-item-text-grow">Scratchpads</span>
<span class="index-item-count">${count}</span>
</div>
`;
const div = createElementFromHTML(html);
if (!div) return document.createElement('div');
div.onclick = () => {
if (window.innerWidth <= (this.BREAKPOINT_TABLET || 1560)) {
this.#app_callbacks.set_v_open(false);
this.#app_callbacks.set_i_open(true);
} else {
this.#app_callbacks.set_i_open(true);
}
this.#app_callbacks.update_app_config(this.#storage.KEY_CONFIG_SELECTED_CONVERSATION_GUID, 'scratchpad');
this.#app_callbacks.on_conversation_updated();
};
return div;
}
//endregion
//region Rendering
/**
* Updates the active/selected CSS class on conversation list items without re-rendering the whole list.
*/
apply_selected_index_class() {
if (!this.div_list) return;
const config = this.#storage.get_app_config();
const selected_guid = config[this.#storage.KEY_CONFIG_SELECTED_CONVERSATION_GUID];
const conversation = this.#storage.get_conversation(selected_guid);
const isScratchpad = selected_guid === 'scratchpad' || (conversation && conversation[this.#storage.KEY_CONVERSATION_TYPE] === this.#storage.CONVERSATION_TYPE_SCRATCHPAD);
const items = this.div_list.querySelectorAll('.div-index-item');
items.forEach(el => {
el.classList.remove('div-index-item-selected');
});
if (isScratchpad) {
const scratchpadEl = this.div_list.querySelector('#conversation-scratchpad');
if (scratchpadEl) {
scratchpadEl.classList.add('div-index-item-selected');
}
} else if (selected_guid) {
const activeEl = this.div_list.querySelector(`#conversation-${selected_guid}`);
if (activeEl) {
activeEl.classList.add('div-index-item-selected');
}
}
}
/**
* Re-renders the full conversations list in the DOM based on current storage and filter state.
*/
on_app_index_updated() {
const rawIndex = this.#storage.get_app_index() || [];
const index = [...rawIndex];
index.sort((a, b) => (b[this.#storage.KEY_INDEX_DATE_UPDATED] || 0) - (a[this.#storage.KEY_INDEX_DATE_UPDATED] || 0));
const config = this.#storage.get_app_config();
const show_archives = config[this.#storage.KEY_SHOW_ARCHIVES] || false;
const selected_guid = config[this.#storage.KEY_CONFIG_SELECTED_CONVERSATION_GUID];
let active_count = 0;
let archived_count = 0;
if (this.div_list) {
this.div_list.innerHTML = '';
const fragment = document.createDocumentFragment();
fragment.appendChild(this.create_scratchpad_item_element());
fragment.appendChild(this.create_new_conversation_trigger_item());
index.forEach(item => {
const guid = item[this.#storage.KEY_INDEX_GUID];
const conversation = this.#storage.get_conversation(guid);
if (!conversation) return;
const is_archived = conversation[this.#storage.KEY_CONVERSATION_ARCHIVED] || false;
const type = conversation[this.#storage.KEY_CONVERSATION_TYPE] || this.#storage.CONVERSATION_TYPE_CHAT;
if (type === this.#storage.CONVERSATION_TYPE_SCRATCHPAD) {
return;
}
if (is_archived) {
archived_count++;
if (!show_archives) {
return;
}
} else {
active_count++;
}
const title = conversation[this.#storage.KEY_CONVERSATION_TITLE] || 'Untitled';
const element = this.create_index_item_element(item, guid, title, is_archived, type);
if (guid === selected_guid) {
element.classList.add('div-index-item-selected');
}
fragment.appendChild(element);
});
if (show_archives) {
const archive_header = createElementFromHTML(this._get_archive_header_html());
if (archive_header) fragment.appendChild(archive_header);
if (archived_count === 0) {
const no_archives = createElementFromHTML(this._get_no_archive_message_html());
if (no_archives) fragment.appendChild(no_archives);
}
}
this.div_list.appendChild(fragment);
}
const total_selectable = active_count + (show_archives ? archived_count : 0);
if (this.btn_select_conversations) {
this.btn_select_conversations.disabled = (total_selectable === 0);
}
if (total_selectable === 0 && this.#selection.isSelectionMode) {
this.#selection.clear();
}
this._update_selection_ui();
if (this.btn_archives_toggle) {
this.btn_archives_toggle.textContent = show_archives ? 'Hide Archives' : 'Show Archives';
}
}
//endregion
}
export default ConversationsList;
conversations.js
×
Type: Web, text/plain
15.16 Kilobytes
Last Modified 2026-09-04 14:23:40
⬇ Download File
Type: Web, text/plain
15.16 Kilobytes
Last Modified 2026-09-04 14:23:40
⬇ Download File