-
🌐 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, getEl } from './util/dom-utils.js';
import { SelectionManager } from './util/selection-manager.js';
import { customConfirm } from './util/confirm-dialog.js';
/**
* class ConversationIndex
* Manages the secondary navigation index for a single conversation.
* Handles the display, selection, bookmarking, and deletion of individual responses
* within the active conversation's history.
*/
class ConversationIndex {
#storage;
#app_callbacks;
#selection;
#scrollTimeout = null;
div_index_list;
btn_conversation_index_select;
btn_conversation_index_favorite;
btn_conversation_index_delete;
SCROLL_DELAY;
BREAKPOINT_MOBILE;
/**
* Initializes the ConversationIndex instance with storage, callbacks, and element references.
* @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.
* @param {number} scroll_delay - Scroll animation delay in milliseconds.
*/
constructor(storage_instance, app_callbacks, elements, breakpoints, scroll_delay) {
this.#storage = storage_instance;
this.#app_callbacks = app_callbacks;
this.div_index_list = elements.div_index_list;
this.btn_conversation_index_select = elements.btn_conversation_index_select;
this.btn_conversation_index_favorite = elements.btn_conversation_index_favorite;
this.btn_conversation_index_delete = elements.btn_conversation_index_delete;
this.SCROLL_DELAY = scroll_delay;
this.BREAKPOINT_MOBILE = breakpoints.BREAKPOINT_MOBILE;
this.#selection = new SelectionManager(() => this._update_selection_ui());
}
/**
* Cancels any pending scheduled scroll operation.
*/
cancel_scroll() {
if (this.#scrollTimeout) {
clearTimeout(this.#scrollTimeout);
this.#scrollTimeout = null;
}
}
//region Response Selection and Actions
/**
* Updates the selection controls UI state.
* @private
*/
_update_selection_ui() {
this.#selection.updateControls(
this.btn_conversation_index_select,
[this.btn_conversation_index_favorite, this.btn_conversation_index_delete],
'Select'
);
}
/**
* Toggles multi-selection mode for individual responses within the current conversation.
*/
toggle_response_selection_mode() {
this.#selection.toggleMode();
this._update_selection_ui();
this.on_conversation_index_updated();
}
/**
* Toggles the bookmarked status of all responses currently selected in the response index.
*/
bookmark_selected_responses() {
const selectedIndices = this.#selection.selectedItems;
if (selectedIndices.length === 0) return;
const config = this.#storage.get_app_config();
const guid = config[this.#storage.KEY_CONFIG_SELECTED_CONVERSATION_GUID];
if (!guid) return;
const conversation = this.#storage.get_conversation(guid);
let history = conversation[this.#storage.KEY_CONVERSATION_HISTORY] || [];
selectedIndices.forEach(index => {
if (history[index]) {
const isBookmarked = history[index].BOOKMARKED || false;
history[index].BOOKMARKED = !isBookmarked;
}
});
conversation[this.#storage.KEY_CONVERSATION_HISTORY] = history;
this.#storage.save_conversation(guid, conversation);
this.#selection.clear();
this._update_selection_ui();
this.on_conversation_index_updated();
this.#app_callbacks.on_conversation_updated_main_panel(false);
}
/**
* Deletes all responses currently selected in the response index from the history after user confirmation.
*/
async delete_selected_responses() {
const selectedIndices = this.#selection.selectedItems;
const count = selectedIndices.length;
if (count === 0) return;
const message = count === 1
? 'Are you sure you want to delete this response from the conversation?'
: `Are you sure you want to delete ${count} selected responses from the conversation?`;
if (await customConfirm('Delete Responses', message)) {
const config = this.#storage.get_app_config();
const guid = config[this.#storage.KEY_CONFIG_SELECTED_CONVERSATION_GUID];
if (!guid) return;
const conversation = this.#storage.get_conversation(guid);
let history = conversation[this.#storage.KEY_CONVERSATION_HISTORY] || [];
// Filter out items whose index is in the selection array
history = history.filter((_, index) => !selectedIndices.includes(index));
conversation[this.#storage.KEY_CONVERSATION_HISTORY] = history;
this.#storage.save_conversation(guid, conversation);
this.#selection.clear();
this._update_selection_ui();
this.on_conversation_index_updated();
this.#app_callbacks.on_conversation_updated_main_panel(false);
}
}
//endregion
//region HTML Generation
/**
* Creates and returns a DOM element for a single response entry in the conversation index.
* @param {Object} data - The response data object.
* @param {number} index - The numerical index of the response in history.
* @returns {HTMLElement} The created index item element.
*/
create_response_index_item(data, index) {
const isChecked = this.#selection.isSelected(index);
const html = this._create_response_item_html(data, index, this.#selection.isSelectionMode, isChecked);
const index_item = createElementFromHTML(html);
if (!index_item) return document.createElement('div');
const chk = index_item.querySelector('.response-item-checkbox');
if (chk) {
chk.onclick = (e) => {
e.stopPropagation();
this.#selection.toggleItem(index, chk.checked);
this._update_selection_ui();
};
}
index_item.onclick = () => {
const el = getEl('chat-item-' + index);
if (el) {
this.cancel_scroll();
if (window.innerWidth <= this.BREAKPOINT_MOBILE) {
this.#scrollTimeout = setTimeout(() => {
this.#scrollTimeout = null;
el.scrollIntoView({ behavior: 'smooth' });
}, this.SCROLL_DELAY);
} else {
el.scrollIntoView({ behavior: 'smooth' });
}
}
if (window.innerWidth <= this.BREAKPOINT_MOBILE) {
this.#app_callbacks.set_i_open(false);
this.#app_callbacks.apply_panels_layout();
}
};
return index_item;
}
/**
* Generates the HTML string for a single response index item.
* @param {Object} data - The response data object.
* @param {number} index - The numerical index of the response in history.
* @param {boolean} isSelectionMode - Whether response selection mode is active.
* @param {boolean} isChecked - Whether the checkbox for this item should be checked.
* @returns {string} The HTML string for the response index item.
*/
_create_response_item_html(data, index, isSelectionMode, isChecked) {
const iconClass = data.BOOKMARKED ? 'bookmark-icon-active' : 'bookmark-icon-inactive';
return `
<div class="div-index-item div-index-item-flex ${data.BOOKMARKED ? 'div-index-item-bookmarked' : ''}"
data-index="${index}">
${isSelectionMode ? `<input type="checkbox" ${isChecked ? 'checked' : ''} class="response-item-checkbox index-item-checkbox-margin">` : ''}
<span class="index-item-icon as-icon ${iconClass}">🔖</span>
<span class="index-item-text-grow">${data.title || ''}</span>
</div>
`;
}
//endregion
//region UI Updates
/**
* Highlights the item in the response index that corresponds to the chat response currently in the user's viewport.
*/
highlight_active_index_item() {
const anchors = document.querySelectorAll('[id^="chat-item-"]');
const scrollContainer = document.querySelector('.div-chat-container-scroll');
if (!scrollContainer || anchors.length === 0 || !this.div_index_list) return;
const containerRect = scrollContainer.getBoundingClientRect();
let activeIndex = 0;
// Check if the last anchor is visible
const lastAnchor = anchors[anchors.length - 1];
const lastRect = lastAnchor.getBoundingClientRect();
const isLastVisible = (lastRect.top < containerRect.bottom && lastRect.bottom > containerRect.top);
if (isLastVisible) {
activeIndex = anchors.length - 1;
} else {
// Find anchor closest to the top of the screen/container
let minDistance = Infinity;
anchors.forEach((anchor, i) => {
const rect = anchor.getBoundingClientRect();
const distance = Math.abs(rect.top - containerRect.top);
if (distance < minDistance) {
minDistance = distance;
activeIndex = i;
}
});
}
const indexItems = this.div_index_list.querySelectorAll('.div-index-item');
indexItems.forEach((item) => {
if (parseInt(item.dataset.index, 10) === activeIndex) {
item.classList.add('div-index-item-selected');
} else {
item.classList.remove('div-index-item-selected');
}
});
}
/**
* Renders the conversation index panel based on the current conversation history.
*/
on_conversation_index_updated() {
const conversation = this.#app_callbacks.get_selected_conversation();
const history = conversation ? conversation[this.#storage.KEY_CONVERSATION_HISTORY] : null;
if (!this.div_index_list) return;
this.div_index_list.innerHTML = '';
if (history && history.length > 0) {
if (this.btn_conversation_index_select) this.btn_conversation_index_select.disabled = false;
} else {
if (this.btn_conversation_index_select) this.btn_conversation_index_select.disabled = true;
}
if (!history || history.length === 0) {
if (this.btn_conversation_index_favorite) this.btn_conversation_index_favorite.disabled = true;
if (this.btn_conversation_index_delete) this.btn_conversation_index_delete.disabled = true;
return;
}
this._update_selection_ui();
const indexedHistory = history.map((data, index) => ({ data, index }));
const bookmarked = indexedHistory.filter(item => item.data.BOOKMARKED === true);
const others = indexedHistory.filter(item => item.data.BOOKMARKED !== true);
bookmarked.forEach(item => {
if (item.data.title) {
this.div_index_list.appendChild(this.create_response_index_item(item.data, item.index));
}
});
if (bookmarked.length > 0 && others.length > 0) {
const hr = document.createElement('hr');
hr.className = 'hr-chat-response-divider';
this.div_index_list.appendChild(hr);
}
others.forEach(item => {
if (item.data.title) {
this.div_index_list.appendChild(this.create_response_index_item(item.data, item.index));
}
});
this.highlight_active_index_item();
}
//endregion
}
export default ConversationIndex;
conversation-index.js
×
Type: Web, text/plain
10.19 Kilobytes
Last Modified 2026-09-04 18:10:30
⬇ Download File
Type: Web, text/plain
10.19 Kilobytes
Last Modified 2026-09-04 18:10:30
⬇ Download File