-
❓ .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 { getEl, addSafeEventListener } from './util/dom-utils.js';
import { escapeHtml, generateUUID } from './util/format-utils.js';
import { customConfirm } from './util/confirm-dialog.js';
/**
* class Attachments
* Handles the attachments tab: conversation attachments on top and global
* document management below. Documents are added via file pickers, stored as
* blobs in IndexedDB, and listed in the global document list.
*/
class Attachments {
/**
* Initializes a new instance of the Attachments class.
* @param {Object} storage - Storage instance.
* @param {Object} app_callbacks - Application callbacks (renderAttachments).
*/
constructor(storage, app_callbacks) {
this.storage = storage;
this.app_callbacks = app_callbacks;
}
/**
* Renders the attachments view.
* @returns {string} HTML string for the attachments view.
*/
render() {
const config = this.storage.get_app_config();
const guid = config[this.storage.KEY_CONFIG_SELECTED_CONVERSATION_GUID];
const conversation = guid ? this.storage.get_conversation(guid) : null;
const attached_ids = conversation ? (conversation[this.storage.KEY_CONVERSATION_ATTACHMENTS] || []) : [];
const documents = this.storage.get_app_documents();
let html = '<div class="div-attachments-split">';
html += '<div class="div-attachments-conversation">';
html += '<h4>Conversation Attachments</h4><hr>';
const attached = documents.filter(doc => attached_ids.includes(doc.id));
if (attached.length > 0) {
attached.forEach(doc => {
html += this._render_document_row(doc, [
{ class: 'btn-remove-attachment', label: '-', title: 'Remove from conversation' }
]);
});
} else {
html += '<p class="p-attachments-empty">No Attachments</p>';
}
html += '</div>';
html += '<div class="div-attachments-global" data-simplebar data-simplebar-auto-hide="false">';
html += '<h4>Global Documents</h4><hr>';
if (documents.length > 0) {
documents.forEach(doc => {
const buttons = [];
if (!attached_ids.includes(doc.id)) {
buttons.push({ class: 'btn-add-attachment', label: '+', title: 'Add to conversation' });
}
buttons.push({ class: 'btn-delete-document', label: 'X', title: 'Delete from storage' });
html += this._render_document_row(doc, buttons);
});
} else {
html += '<p class="p-attachments-empty">No Documents</p>';
}
html += '</div>';
html += `
<div class="div-attachments-actions">
<button id="btn-add-text-document">Add Text Document</button>
<button id="btn-add-image">Add Image</button>
</div>
`;
html += '</div>';
return html;
}
/**
* Renders a single document row with action buttons.
* @param {Object} doc - The document metadata.
* @param {Array<Object>} buttons - Buttons to render ({class, label, title}).
* @returns {string} HTML string for the row.
* @private
*/
_render_document_row(doc, buttons) {
const name = escapeHtml(doc.name || 'Untitled');
const size = this._format_size(doc.size);
let buttons_html = '';
buttons.forEach(button => {
buttons_html += `<button class="${button.class}" data-doc-id="${escapeHtml(doc.id)}" title="${button.title}">${button.label}</button>`;
});
return `
<div class="div-attachment-row">
<span class="span-attachment-name" title="${name}">${name}</span>
<span class="span-attachment-size">${size}</span>
${buttons_html}
</div>
`;
}
/**
* Formats a byte size for display.
* @param {number} bytes - The size in bytes.
* @returns {string} Formatted size string.
* @private
*/
_format_size(bytes) {
if (!bytes && bytes !== 0) return '';
if (bytes < 1024) return bytes + ' B';
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
}
/**
* Attaches event listeners for the attachments view.
*/
attachEventListeners() {
const container = getEl('div-chat-attachments-scroll');
addSafeEventListener(container, 'click', (event) => this._handle_click(event));
const text_input = getEl('input-attachment-text');
addSafeEventListener(text_input, 'change', (event) => {
void this._import_files(event.target.files);
text_input.value = '';
});
const image_input = getEl('input-attachment-image');
addSafeEventListener(image_input, 'change', (event) => {
void this._import_files(event.target.files);
image_input.value = '';
});
}
/**
* Handles clicks within the attachments view via event delegation.
* @param {Event} event - The click event.
* @private
*/
_handle_click(event) {
const target = event.target.closest('button');
if (!target) return;
const doc_id = target.dataset.docId;
if (target.id === 'btn-add-text-document') {
getEl('input-attachment-text').click();
} else if (target.id === 'btn-add-image') {
getEl('input-attachment-image').click();
} else if (target.classList.contains('btn-add-attachment') && doc_id) {
this._add_to_conversation(doc_id);
} else if (target.classList.contains('btn-remove-attachment') && doc_id) {
this._remove_from_conversation(doc_id);
} else if (target.classList.contains('btn-delete-document') && doc_id) {
void this._delete_document(doc_id);
}
}
/**
* Adds a global document to the current conversation's attachments.
* @param {string} doc_id - The document id.
* @private
*/
_add_to_conversation(doc_id) {
const config = this.storage.get_app_config();
const guid = config[this.storage.KEY_CONFIG_SELECTED_CONVERSATION_GUID];
if (guid) {
this.storage.add_conversation_attachment(guid, doc_id);
this._refresh();
}
}
/**
* Removes a document from the current conversation's attachments.
* @param {string} doc_id - The document id.
* @private
*/
_remove_from_conversation(doc_id) {
const config = this.storage.get_app_config();
const guid = config[this.storage.KEY_CONFIG_SELECTED_CONVERSATION_GUID];
if (guid) {
this.storage.remove_conversation_attachment(guid, doc_id);
this._refresh();
}
}
/**
* Deletes a document from storage (and all conversation references) after confirmation.
* @param {string} doc_id - The document id.
* @private
*/
async _delete_document(doc_id) {
const documents = this.storage.get_app_documents();
const doc = documents.find(d => d.id === doc_id);
const name = doc ? doc.name : 'this document';
if (await customConfirm('Delete Document', `Do you want to delete "${name}" from storage?`)) {
await this.storage.remove_app_document(doc_id);
this._refresh();
}
}
/**
* Imports selected files into global storage and attaches them to the current conversation.
* @param {FileList} files - The selected files.
* @private
*/
async _import_files(files) {
const config = this.storage.get_app_config();
const guid = config[this.storage.KEY_CONFIG_SELECTED_CONVERSATION_GUID];
for (const file of Array.from(files || [])) {
const id = generateUUID();
const meta = {
id: id,
name: file.name,
mime: file.type,
size: file.size,
date: new Date().toISOString()
};
await this.storage.save_document_blob(id, file);
this.storage.add_app_document(meta);
if (guid) {
this.storage.add_conversation_attachment(guid, id);
}
}
this._refresh();
}
/**
* Re-renders the attachments view.
* @private
*/
_refresh() {
if (this.app_callbacks.renderAttachments) {
this.app_callbacks.renderAttachments();
}
}
}
export default Attachments;
attachments.js
×
Type: Web, text/plain
7.30 Kilobytes
Last Modified 2026-11-04 09:48:28
⬇ Download File
Type: Web, text/plain
7.30 Kilobytes
Last Modified 2026-11-04 09:48:28
⬇ Download File