-
❓ .gitignoreUnknown, 37 B⬇
-
❓ README.mdUnknown, 2.94 kB⬇
-
🌐 admin_panel.phpWeb, 1.54 kB⬇
-
🌐 api.jsWeb, 22.21 kB⬇
-
🌐 api.phpWeb, 9.62 kB⬇
-
🌐 app.jsWeb, 61.42 kB⬇
-
🌐 attachments.jsWeb, 7.30 kB⬇
-
🌐 base.cssWeb, 2.23 kB⬇
-
🌐 choices-editor.jsWeb, 28.11 kB⬇
-
🌐 choices.cssWeb, 1.14 kB⬇
-
🌐 content.jsWeb, 31.08 kB⬇
-
🌐 context.jsWeb, 9.26 kB⬇
-
🌐 conversation-index.jsWeb, 10.74 kB⬇
-
🌐 conversation.cssWeb, 16.33 kB⬇
-
🌐 conversation.jsWeb, 1.11 kB⬇
-
🌐 dialogs.cssWeb, 11.43 kB⬇
-
🌐 diction.jsWeb, 948 B⬇
-
🌐 document-index.jsWeb, 1.04 kB⬇
-
🌐 document-model.jsWeb, 12.88 kB⬇
-
🌐 document.cssWeb, 6.74 kB⬇
-
🌐 document.jsWeb, 44.96 kB⬇
-
🌐 dynamic.phpWeb, 533 B⬇
-
📷 favicon-lg.pngImage, 9.11 kB⬇
-
📷 favicon.pngImage, 3.12 kB⬇
-
🌐 index-lists.cssWeb, 4.77 kB⬇
-
🌐 index.phpWeb, 49.72 kB⬇
-
🌐 indexjs.jsWeb, 377 B⬇
-
🌐 instant-answer.cssWeb, 5.17 kB⬇
-
🌐 layout.cssWeb, 11.37 kB⬇
-
🌐 login.phpWeb, 997 B⬇
-
❓ manifest.webmanifestUnknown, 262 B⬇
-
🌐 memory.jsWeb, 407 B⬇
-
🌐 mermaid.cssWeb, 6.16 kB⬇
-
🌐 model-presets.jsonWeb, 5.51 kB⬇
-
🌐 models.jsonWeb, 8.05 kB⬇
-
🌐 notes.cssWeb, 11.97 kB⬇
-
🌐 overlays.cssWeb, 13.97 kB⬇
-
⚠️ php.iniOS File, 424 B⬇
-
🌐 rankings-view.jsWeb, 5.67 kB⬇
-
🌐 rankings.cssWeb, 3.35 kB⬇
-
🌐 scratchpad-conversation.jsWeb, 4.87 kB⬇
-
🌐 scratchpad-index.jsWeb, 15.55 kB⬇
-
🌐 scratchpad-model.jsWeb, 13.29 kB⬇
-
🌐 scratchpad.cssWeb, 3.12 kB⬇
-
🌐 settings.cssWeb, 4.65 kB⬇
-
🌐 shared-view.cssWeb, 3.48 kB⬇
-
🌐 shared.cssWeb, 16.26 kB⬇
-
🌐 shared.jsWeb, 14.63 kB⬇
-
🌐 shared.phpWeb, 3.28 kB⬇
-
🌐 sticky-notes-editor.jsWeb, 64.84 kB⬇
-
🌐 storage.jsWeb, 36.73 kB⬇
-
🌐 style.cssWeb, 12.06 kB⬇
-
🌐 sw.jsWeb, 1.69 kB⬇
-
🌐 sync.jsWeb, 19.02 kB⬇
-
🌐 tools-index.jsWeb, 15.00 kB⬇
-
🌐 users.jsWeb, 49.86 kB⬇
-
🌐 utility-models.jsonWeb, 1.04 kB⬇
-
🌐 voice-models.jsonWeb, 4.81 kB⬇
import { createElementFromHTML } from './util/dom-utils.js';
import { escapeHtml } from './util/format-utils.js';
import { SelectionManager } from './util/selection-manager.js';
import { customConfirm, shared_copies_note } from './util/confirm-dialog.js';
/**
* Class ToolsIndex
* Manages the secondary navigation index for tools.
* Renders the list of tool entries and the fixed "New ..." trigger buttons at
* the top. Supports the scratchpad-style selection mode: Select toggles
* checkboxes on the entries, Bookmark toggles their bookmark state, and
* Delete removes the selected tool conversations after confirmation.
*/
class ToolsIndex {
#storage;
#app_callbacks;
#selection;
div_index_list;
btn_tools_index_select;
btn_tools_index_favorite;
btn_tools_index_delete;
BREAKPOINT_MOBILE;
/**
* Initializes the ToolsIndex 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_tools_index_select = elements.btn_tools_index_select || elements.btn_conversation_index_select;
this.btn_tools_index_favorite = elements.btn_tools_index_favorite || elements.btn_conversation_index_favorite;
this.btn_tools_index_delete = elements.btn_tools_index_delete || elements.btn_conversation_index_delete;
this.BREAKPOINT_MOBILE = breakpoints.BREAKPOINT_MOBILE;
this.#selection = new SelectionManager(() => this._update_selection_ui());
}
//region Tool Actions
/**
* Creates a new Ranking tool entry.
*/
create_new_ranking() {
this.#app_callbacks.create_new_ranking?.();
}
/**
* Creates a new Colors Palettes tool entry.
*/
create_new_palettes() {
this.#app_callbacks.create_new_palettes?.();
}
/**
* Creates a new Sticky Notes tool entry.
*/
create_new_sticky_notes() {
this.#app_callbacks.create_new_sticky_notes?.();
}
/**
* Toggles the bookmark status for all currently selected tools, or
* toggles the bookmark status of the active tool 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 && this.#is_tool_type(type)) {
conversation.BOOKMARKED = !(conversation.BOOKMARKED || false);
this.#storage.save_conversation(selected_guid, conversation);
this.on_tools_index_updated();
this.#app_callbacks.on_conversation_updated_main_panel?.();
}
}
return;
}
selectedGuids.forEach(guid => {
const conversation = this.#storage.get_conversation(guid);
if (conversation) {
conversation.BOOKMARKED = !(conversation.BOOKMARKED || false);
this.#storage.save_conversation(guid, conversation);
}
});
this.#selection.clear();
this._update_selection_ui();
this.on_tools_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 tool conversations after user confirmation.
* The dialog notes when selected tools have shared copies, since deleting
* them also deletes those copies and their links.
*/
async delete_selected_tools() {
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 tool?'
: `Are you sure you want to delete ${count} selected tools?`;
const note = await shared_copies_note(this.#app_callbacks, selectedGuids, 'tool');
if (await customConfirm('Delete Tools', message, 'Yes', 'No', note)) {
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);
}
const synced = !!this.#storage.get_conversation(guid)?.[this.#storage.KEY_CONVERSATION_SYNCED];
this.#storage.index_delete(guid);
this.#app_callbacks.conversation_deleted?.(guid, synced);
});
this.#selection.clear();
this._update_selection_ui();
this.on_tools_index_updated();
this.#app_callbacks.on_conversation_updated_main_panel?.();
}
}
/**
* Alias for delete_selected_tools to conform to standard index interface.
*/
async delete_selected_responses() {
await this.delete_selected_tools();
}
/**
* Toggles the selection mode for tools.
*/
toggle_tool_selection_mode() {
this.#selection.toggleMode();
this._update_selection_ui();
this.on_tools_index_updated();
}
/**
* Alias for toggle_tool_selection_mode to conform to standard index interface.
*/
toggle_response_selection_mode() {
this.toggle_tool_selection_mode();
}
/**
* Returns whether a conversation type is a tool type.
* @param {string} type - The conversation type.
* @returns {boolean} True for ranking and colors conversations.
*/
#is_tool_type(type) {
return type === this.#storage.CONVERSATION_TYPE_RANKING
|| type === this.#storage.CONVERSATION_TYPE_COLORS
|| type === this.#storage.CONVERSATION_TYPE_STICKY_NOTES;
}
//endregion
//region HTML Generation
/**
* Returns all tool conversation entries from the index.
* @returns {Array<Object>}
*/
get_tool_conversations() {
const index = this.#storage.get_app_index() || [];
return index
.map(item => ({ guid: item[this.#storage.KEY_INDEX_GUID], conversation: this.#storage.get_conversation(item[this.#storage.KEY_INDEX_GUID]) }))
.filter(({ conversation }) => this.#is_tool_type(conversation?.[this.#storage.KEY_CONVERSATION_TYPE]));
}
/**
* Sorts tool entries: bookmarked first, then the rest.
* @param {Array<Object>} tools - The tool entries.
* @returns {Array<Object>} The sorted entries.
*/
#sorted_tools(tools) {
const bookmarked = tools.filter(item => item.conversation.BOOKMARKED === true);
const others = tools.filter(item => item.conversation.BOOKMARKED !== true);
return [...bookmarked, ...others];
}
/**
* Determines the display title for a tool conversation.
* @param {Object} conversation - The conversation object.
* @returns {string} The display title.
*/
get_tool_title(conversation) {
if (!conversation) return 'New Tool';
if (conversation[this.#storage.KEY_CONVERSATION_CUSTOM_TITLE]) {
return conversation[this.#storage.KEY_CONVERSATION_TITLE] || 'New Tool';
}
const history = conversation[this.#storage.KEY_CONVERSATION_HISTORY] || [];
for (let i = history.length - 1; i >= 0; i--) {
const item = history[i];
if (item?.conversationTitle) {
return item.conversationTitle;
}
}
return conversation[this.#storage.KEY_CONVERSATION_TITLE] || 'New Tool';
}
/**
* Creates and returns a DOM element for a single tool entry in the index.
* @param {Object} conversation - The conversation object.
* @param {string} guid - The unique identifier of the tool conversation.
* @returns {HTMLElement} The created index item element.
*/
create_tool_index_item(conversation, guid) {
const type = conversation[this.#storage.KEY_CONVERSATION_TYPE];
const typeIcon = type === this.#storage.CONVERSATION_TYPE_RANKING
? '🏆'
: type === this.#storage.CONVERSATION_TYPE_COLORS
? '🎨'
: '📋';
const isBookmarked = conversation.BOOKMARKED === true;
const iconClass = isBookmarked ? 'bookmark-icon-active' : 'bookmark-icon-inactive';
const escapedTitle = escapeHtml(this.get_tool_title(conversation));
const config = this.#storage.get_app_config();
const is_selected = guid === config[this.#storage.KEY_CONFIG_SELECTED_CONVERSATION_GUID];
const is_loading = this.#app_callbacks.is_request_active ? this.#app_callbacks.is_request_active(guid) : false;
const isChecked = this.#selection.isSelected(guid);
const html = `
<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="tool-item-${guid}"
data-guid="${guid}">
${this.#selection.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-icon as-icon">${typeIcon}</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>
`;
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;
}
/**
* Creates and returns the DOM element for the "New Ranking" trigger button.
* @returns {HTMLElement}
*/
create_new_ranking_trigger_item() {
const html = `
<button class="btn-new-scratchpad">
<span class="index-item-icon as-icon" style="margin-right: 0.5em;">➕</span>
<span>New Ranking</span>
</button>
`;
const div = createElementFromHTML(html);
if (!div) return document.createElement('button');
div.onclick = (e) => {
e.stopPropagation();
this.create_new_ranking();
};
return div;
}
/**
* Creates and returns the DOM element for the "New Colors Palettes" trigger button.
* @returns {HTMLElement}
*/
create_new_palettes_trigger_item() {
const html = `
<button class="btn-new-scratchpad">
<span class="index-item-icon as-icon" style="margin-right: 0.5em;">➕</span>
<span>New Colors Palettes</span>
</button>
`;
const div = createElementFromHTML(html);
if (!div) return document.createElement('button');
div.onclick = (e) => {
e.stopPropagation();
this.create_new_palettes();
};
return div;
}
/**
* Creates and returns the DOM element for the "New Sticky Notes" trigger button.
* @returns {HTMLElement}
*/
create_new_sticky_notes_trigger_item() {
const html = `
<button class="btn-new-scratchpad">
<span class="index-item-icon as-icon" style="margin-right: 0.5em;">➕</span>
<span>New Sticky Notes</span>
</button>
`;
const div = createElementFromHTML(html);
if (!div) return document.createElement('button');
div.onclick = (e) => {
e.stopPropagation();
this.create_new_sticky_notes();
};
return div;
}
//endregion
//region UI Updates
/**
* Updates the loading indicator for a specific tool item in the index.
* @param {string} guid - The tool conversation guid.
* @param {boolean} isLoading - Whether the request is active.
*/
set_tool_loading(guid, isLoading) {
if (!this.div_index_list) return;
const itemEl = document.getElementById(`tool-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');
if (indicator) {
indicator.style.display = isLoading ? 'inline-block' : 'none';
}
}
}
/**
* Updates the tools 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 isToolActive = !!(selectedConv && this.#is_tool_type(selectedConv[this.#storage.KEY_CONVERSATION_TYPE]));
const hasTools = this.get_tool_conversations().length > 0;
if (this.btn_tools_index_select) {
this.btn_tools_index_select.disabled = !hasTools;
}
if (this.#selection.isSelectionMode) {
this.#selection.updateControls(
this.btn_tools_index_select,
[this.btn_tools_index_favorite, this.btn_tools_index_delete],
'Select'
);
} else {
if (this.btn_tools_index_select) {
this.btn_tools_index_select.textContent = 'Select';
}
if (this.btn_tools_index_favorite) {
this.btn_tools_index_favorite.disabled = !isToolActive;
}
if (this.btn_tools_index_delete) {
this.btn_tools_index_delete.disabled = true;
}
}
}
/**
* Highlights the active tool 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();
}
/**
* Re-renders the tools index view in the DOM.
*/
on_tools_index_updated() {
(window.__trace ||= []).push(["tools", Math.round(performance.now())]);
if (this.div_index_list) {
this.div_index_list.innerHTML = '';
this.div_index_list.appendChild(this.create_new_ranking_trigger_item());
this.div_index_list.appendChild(this.create_new_sticky_notes_trigger_item());
// colors palettes are experimental: the creation button only
// shows while the flag is on
if (this.#app_callbacks.experimental_enabled?.()) {
this.div_index_list.appendChild(this.create_new_palettes_trigger_item());
}
this.get_tool_conversations().forEach(({ guid, conversation }) => {
this.div_index_list.appendChild(this.create_tool_index_item(conversation, guid));
});
}
this.highlight_active_index_item();
}
//endregion
}
export default ToolsIndex;
tools-index.js
×
Type: Web, text/plain
15.00 Kilobytes
Last Modified 2026-11-04 09:48:28
⬇ Download File
Type: Web, text/plain
15.00 Kilobytes
Last Modified 2026-11-04 09:48:28
⬇ Download File