-
🌐 admin_panel.phpWeb, 1.54 kB⬇
-
🌐 api.jsWeb, 12.79 kB⬇
-
🌐 api.phpWeb, 7.79 kB⬇
-
🌐 app.jsWeb, 150.45 kB⬇
-
🌐 base.cssWeb, 2.23 kB⬇
-
🌐 context.jsWeb, 9.26 kB⬇
-
🌐 conversation-index.jsWeb, 10.34 kB⬇
-
🌐 conversation.jsWeb, 1.11 kB⬇
-
🌐 conversations.jsWeb, 23.87 kB⬇
-
🌐 diction.jsWeb, 935 B⬇
-
🌐 document.jsWeb, 947 B⬇
-
🌐 dynamic.phpWeb, 533 B⬇
-
📷 favicon-lg.pngImage, 9.11 kB⬇
-
📷 favicon.pngImage, 3.12 kB⬇
-
🌐 index.phpWeb, 30.31 kB⬇
-
🌐 indexjs.jsWeb, 580 B⬇
-
🌐 login.phpWeb, 876 B⬇
-
❓ manifest.webmanifestUnknown, 262 B⬇
-
🌐 memory.jsWeb, 407 B⬇
-
🌐 model-presets.jsonWeb, 7.89 kB⬇
-
🌐 models.jsonWeb, 12.83 kB⬇
-
🌐 notebook-index.jsWeb, 721 B⬇
-
⚠️ php.iniOS File, 356 B⬇
-
🌐 scratchpad-conversation.jsWeb, 2.70 kB⬇
-
🌐 scratchpad-index.jsWeb, 14.47 kB⬇
-
🌐 storage.jsWeb, 20.49 kB⬇
-
🌐 style.cssWeb, 49.16 kB⬇
-
🌐 users.jsWeb, 45.79 kB⬇
-
🌐 utility-models.jsonWeb, 1.46 kB⬇
import { createElementFromHTML } from './util/dom-utils.js';
import { SelectionManager } from './util/selection-manager.js';
import { customConfirm } from './util/confirm-dialog.js';
import { escapeHtml } from './util/format-utils.js';
/**
* Class ScratchpadIndex
* Manages the secondary navigation index for scratchpads.
* Handles rendering the list of scratchpads (bookmarked and others),
* managing selections, bookmarking, and deletion.
*/
class ScratchpadIndex {
#storage;
#app_callbacks;
#selection;
div_index_list;
btn_scratchpad_index_select;
btn_scratchpad_index_favorite;
btn_scratchpad_index_delete;
btn_show_scratchpads_new;
BREAKPOINT_MOBILE;
/**
* Initializes the ScratchpadIndex 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.
*/
constructor(storage_instance, app_callbacks, elements, breakpoints) {
this.#storage = storage_instance;
this.#app_callbacks = app_callbacks;
this.div_index_list = elements.div_index_list;
this.btn_scratchpad_index_select = elements.btn_scratchpad_index_select || elements.btn_conversation_index_select;
this.btn_scratchpad_index_favorite = elements.btn_scratchpad_index_favorite || elements.btn_conversation_index_favorite;
this.btn_scratchpad_index_delete = elements.btn_scratchpad_index_delete || elements.btn_conversation_index_delete;
this.btn_show_scratchpads_new = elements.btn_show_scratchpads_new;
this.BREAKPOINT_MOBILE = breakpoints.BREAKPOINT_MOBILE;
this.#selection = new SelectionManager(() => this._update_selection_ui());
if (this.btn_show_scratchpads_new) {
this.btn_show_scratchpads_new.onclick = () => {
this.#app_callbacks.create_new_scratchpad();
};
}
}
//region Scratchpad Actions
/**
* Toggles the bookmark status for all currently selected scratchpads,
* or toggles the bookmark status of the active scratchpad if no selection.
*/
toggle_selected_bookmarks() {
const selectedGuids = this.#selection.selectedItems;
if (selectedGuids.length === 0) {
const config = this.#storage.get_app_config();
const selected_guid = config[this.#storage.KEY_CONFIG_SELECTED_CONVERSATION_GUID];
if (selected_guid) {
const conversation = this.#storage.get_conversation(selected_guid);
const type = conversation?.[this.#storage.KEY_CONVERSATION_TYPE];
if (conversation && type === this.#storage.CONVERSATION_TYPE_SCRATCHPAD) {
const isBookmarked = conversation.BOOKMARKED || false;
conversation.BOOKMARKED = !isBookmarked;
this.#storage.save_conversation(selected_guid, conversation);
this.on_conversation_index_updated();
this.#app_callbacks.on_conversation_updated_main_panel?.();
}
}
return;
}
selectedGuids.forEach(guid => {
const conversation = this.#storage.get_conversation(guid);
if (conversation) {
const isBookmarked = conversation.BOOKMARKED || false;
conversation.BOOKMARKED = !isBookmarked;
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?.();
}
/**
* Alias for toggle_selected_bookmarks to conform to standard index interface.
*/
bookmark_selected_responses() {
this.toggle_selected_bookmarks();
}
/**
* Deletes all currently selected scratchpads after user confirmation.
*/
async delete_selected_scratchpads() {
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 scratchpad?'
: `Are you sure you want to delete ${count} selected scratchpads?`;
if (await customConfirm('Delete Scratchpads', message)) {
const config = this.#storage.get_app_config();
const selected_guid = config[this.#storage.KEY_CONFIG_SELECTED_CONVERSATION_GUID];
if (selectedGuids.includes(selected_guid)) {
this.#app_callbacks.close_conversation();
}
const toDelete = [...selectedGuids];
toDelete.forEach(guid => {
if (this.#app_callbacks.abort_request) {
this.#app_callbacks.abort_request(guid);
}
this.#storage.index_delete(guid);
});
this.#selection.clear();
this._update_selection_ui();
this.on_conversation_index_updated();
this.#app_callbacks.on_conversation_updated_main_panel?.();
}
}
/**
* Alias for delete_selected_scratchpads to conform to standard index interface.
*/
async delete_selected_responses() {
await this.delete_selected_scratchpads();
}
/**
* Toggles the selection mode for scratchpads.
*/
toggle_scratchpad_selection_mode() {
this.#selection.toggleMode();
this._update_selection_ui();
this.on_conversation_index_updated();
}
/**
* Alias for toggle_scratchpad_selection_mode to conform to standard index interface.
*/
toggle_response_selection_mode() {
this.toggle_scratchpad_selection_mode();
}
/**
* Returns all scratchpad conversation entries from the index.
* @returns {Array<Object>}
*/
get_scratchpad_conversations() {
const index = this.#storage.get_app_index() || [];
return index.filter(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];
return type === this.#storage.CONVERSATION_TYPE_SCRATCHPAD && conversation;
});
}
//endregion
//region HTML Generation
/**
* Creates and returns a DOM element for a single scratchpad entry in the index.
* @param {Object} conversation - The conversation object.
* @param {string} guid - The unique identifier of the scratchpad.
* @param {boolean} [is_loading=false] - Whether an API request is running.
* @returns {HTMLElement} The created index item element.
*/
create_scratchpad_index_item(conversation, guid, is_loading = false) {
const isChecked = this.#selection.isSelected(guid);
const title = this.get_scratchpad_title(conversation);
const isBookmarked = conversation.BOOKMARKED || false;
const config = this.#storage.get_app_config();
const selected_guid = config[this.#storage.KEY_CONFIG_SELECTED_CONVERSATION_GUID];
const is_selected = (guid === selected_guid);
const html = this._create_scratchpad_item_html(title, guid, isBookmarked, this.#selection.isSelectionMode, isChecked, is_loading, is_selected);
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(guid, chk.checked);
this._update_selection_ui();
};
}
index_item.onclick = () => {
if (window.innerWidth <= this.BREAKPOINT_MOBILE) {
this.#app_callbacks.set_i_open(false);
this.#app_callbacks.apply_panels_layout();
}
this.#app_callbacks.update_app_config(this.#storage.KEY_CONFIG_SELECTED_CONVERSATION_GUID, guid);
this.#app_callbacks.on_conversation_updated_main_panel();
this.highlight_active_index_item();
};
return index_item;
}
/**
* Generates the HTML string for a single scratchpad index item.
* @param {string} title - The title of the scratchpad.
* @param {string} guid - The GUID of the scratchpad.
* @param {boolean} isBookmarked - Whether it is bookmarked.
* @param {boolean} isSelectionMode - Whether selection mode is active.
* @param {boolean} isChecked - Whether checked.
* @param {boolean} [is_loading=false] - Whether an API request is active.
* @param {boolean} [is_selected=false] - Whether this scratchpad is selected.
* @returns {string} The HTML string.
*/
_create_scratchpad_item_html(title, guid, isBookmarked, isSelectionMode, isChecked, is_loading = false, is_selected = false) {
const iconClass = isBookmarked ? 'bookmark-icon-active' : 'bookmark-icon-inactive';
const escapedTitle = escapeHtml(title);
return `
<div class="div-index-item div-index-item-flex ${isBookmarked ? 'div-index-item-bookmarked' : ''} ${is_loading ? 'div-index-item-loading' : ''} ${is_selected ? 'div-index-item-selected' : ''}"
id="scratchpad-item-${guid}"
data-guid="${guid}">
${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" title="${escapedTitle}">${escapedTitle}</span>
<span class="index-item-indicator as-icon" style="display: ${is_loading ? 'inline-block' : 'none'};" title="Processing...">●</span>
</div>
`;
}
/**
* Generates the HTML string for a horizontal divider line.
* @returns {string}
*/
_create_divider_html() {
return '<hr class="hr-chat-response-divider" />';
}
/**
* Creates and returns the DOM element for the "New Scratchpad" trigger button.
* @returns {HTMLElement}
*/
create_new_scratchpad_trigger_item() {
const html = `
<button class="btn-new-scratchpad">
<span class="index-item-icon as-icon" style="margin-right: 0.5em;">➕</span>
<span>New Scratchpad</span>
</button>
`;
const div = createElementFromHTML(html);
if (!div) return document.createElement('button');
div.onclick = (e) => {
e.stopPropagation();
this.#app_callbacks.create_new_scratchpad();
};
return div;
}
//endregion
//region UI Updates
/**
* Updates the loading indicator for a specific scratchpad item in the index.
* @param {string} guid - The scratchpad GUID.
* @param {boolean} isLoading - Whether the request is active.
*/
set_scratchpad_loading(guid, isLoading) {
if (!this.div_index_list) return;
const itemEl = document.getElementById(`scratchpad-item-${guid}`) || this.div_index_list.querySelector(`[data-guid="${guid}"]`);
if (itemEl && this.div_index_list.contains(itemEl)) {
if (isLoading) {
itemEl.classList.add('div-index-item-loading');
} else {
itemEl.classList.remove('div-index-item-loading');
}
const indicator = itemEl.querySelector('.index-item-indicator, .index-item-spinner');
if (indicator) {
indicator.style.display = isLoading ? 'inline-block' : 'none';
}
}
}
/**
* Highlights the active scratchpad conversation in the index.
*/
highlight_active_index_item() {
if (!this.div_index_list) return;
const config = this.#storage.get_app_config();
const selected_guid = config[this.#storage.KEY_CONFIG_SELECTED_CONVERSATION_GUID];
const indexItems = this.div_index_list.querySelectorAll('.div-index-item');
indexItems.forEach((item) => {
if (item.dataset.guid && item.dataset.guid === selected_guid) {
item.classList.add('div-index-item-selected');
} else {
item.classList.remove('div-index-item-selected');
}
});
this._update_selection_ui();
}
/**
* Updates the scratchpad index selection controls (buttons, active states).
* @private
*/
_update_selection_ui() {
const config = this.#storage.get_app_config();
const selected_guid = config[this.#storage.KEY_CONFIG_SELECTED_CONVERSATION_GUID];
const selectedConv = selected_guid ? this.#storage.get_conversation(selected_guid) : null;
const isScratchpadActive = !!(selectedConv && selectedConv[this.#storage.KEY_CONVERSATION_TYPE] === this.#storage.CONVERSATION_TYPE_SCRATCHPAD);
if (this.#selection.isSelectionMode) {
this.#selection.updateControls(
this.btn_scratchpad_index_select,
[this.btn_scratchpad_index_favorite, this.btn_scratchpad_index_delete],
'Select'
);
} else {
if (this.btn_scratchpad_index_select) {
this.btn_scratchpad_index_select.textContent = 'Select';
}
if (this.btn_scratchpad_index_favorite) {
this.btn_scratchpad_index_favorite.disabled = !isScratchpadActive;
}
if (this.btn_scratchpad_index_delete) {
this.btn_scratchpad_index_delete.disabled = true;
}
}
}
/**
* Determines the display title for a scratchpad conversation.
* @param {Object} conversation - The conversation object.
* @returns {string} The display title.
*/
get_scratchpad_title(conversation) {
if (!conversation) return 'New Scratchpad';
const history = conversation[this.#storage.KEY_CONVERSATION_HISTORY] || [];
if (history.length > 0) {
for (let i = history.length - 1; i >= 0; i--) {
const item = history[i];
if (item && (item.title || item.conversationTitle)) {
return item.title || item.conversationTitle;
}
}
}
return conversation[this.#storage.KEY_CONVERSATION_TITLE] || 'New Scratchpad';
}
/**
* Re-renders the scratchpad index view in the DOM.
*/
on_conversation_index_updated() {
if (this.div_index_list) {
this.div_index_list.innerHTML = '';
this.div_index_list.appendChild(this.create_new_scratchpad_trigger_item());
const index = this.#storage.get_app_index() || [];
const scratchpads = [];
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 && conversation) {
scratchpads.push({ guid, conversation });
}
});
if (this.btn_scratchpad_index_select) {
this.btn_scratchpad_index_select.disabled = (scratchpads.length === 0);
}
const bookmarked = scratchpads.filter(item => item.conversation.BOOKMARKED === true);
const others = scratchpads.filter(item => item.conversation.BOOKMARKED !== true);
bookmarked.forEach(item => {
const is_loading = this.#app_callbacks.is_request_active ? this.#app_callbacks.is_request_active(item.guid) : false;
this.div_index_list.appendChild(this.create_scratchpad_index_item(item.conversation, item.guid, is_loading));
});
if (bookmarked.length > 0 && others.length > 0) {
const divider = createElementFromHTML(this._create_divider_html());
if (divider) this.div_index_list.appendChild(divider);
}
others.forEach(item => {
const is_loading = this.#app_callbacks.is_request_active ? this.#app_callbacks.is_request_active(item.guid) : false;
this.div_index_list.appendChild(this.create_scratchpad_index_item(item.conversation, item.guid, is_loading));
});
}
this.highlight_active_index_item();
this._update_selection_ui();
}
//endregion
}
export default ScratchpadIndex;
scratchpad-index.js
×
Type: Web, text/plain
14.47 Kilobytes
Last Modified 2026-09-23 02:25:01
⬇ Download File
Type: Web, text/plain
14.47 Kilobytes
Last Modified 2026-09-23 02:25:01
⬇ Download File