// utils.js - Shared utility functions /** * Escape HTML to prevent XSS attacks * @param {string} text - The text to escape * @returns {string} - HTML-escaped text */ function escapeHtml(text) { const div = document.createElement("div"); div.textContent = text; return div.innerHTML; } /** * Wrap tables in a scrollable container for mobile responsiveness * @param {HTMLElement} element - The element containing tables to wrap */ function wrapTablesForScroll(element) { const tables = element.querySelectorAll("table"); tables.forEach(table => { if (!table.parentElement.classList.contains("table-wrapper")) { const wrapper = document.createElement("div"); wrapper.className = "table-wrapper"; table.parentNode.insertBefore(wrapper, table); wrapper.appendChild(table); } }); } /** * Download a file with the given content * @param {string} content - The file content * @param {string} filename - The filename to use * @param {string} mimeType - The MIME type of the file */ function downloadFile(content, filename, mimeType) { const blob = new Blob([content], { type: mimeType }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } /** * Sanitize a filename by removing invalid characters * @param {string} name - The filename to sanitize * @returns {string} - Sanitized filename */ function sanitizeFilename(name) { return name.replace(/[^a-z0-9]/gi, "_").substring(0, 50); } /** * Show export format selection modal and return chosen format * @param {string} conversationTitle - Title of the conversation for display * @returns {Promise} - 'json', 'markdown', or null if cancelled */ function showExportFormatModal(conversationTitle) { return new Promise(resolve => { // Create modal const modal = document.createElement("div"); modal.className = "modal"; modal.id = "exportFormatModal"; modal.innerHTML = ` `; document.body.appendChild(modal); // Event handlers const closeModal = () => { modal.remove(); resolve(null); }; modal.querySelector(".close-btn").addEventListener("click", closeModal); modal.addEventListener("click", e => { if (e.target === modal) closeModal(); }); modal.querySelectorAll("[data-format]").forEach(btn => { btn.addEventListener("click", () => { const format = btn.dataset.format; modal.remove(); resolve(format); }); }); // ESC to close const handleEsc = e => { if (e.key === "Escape") { document.removeEventListener("keydown", handleEsc); closeModal(); } }; document.addEventListener("keydown", handleEsc); }); } /** * Show import file picker and handle the import * @returns {Promise<{conversation: Object, messages: Array}|null>} - Imported data or null */ function showImportFilePicker() { return new Promise(resolve => { const input = document.createElement("input"); input.type = "file"; input.accept = ".json"; input.addEventListener("change", async e => { const file = e.target.files[0]; if (!file) { resolve(null); return; } try { const text = await file.text(); const data = JSON.parse(text); // Validate basic structure if (!data.conversation || !data.messages || !Array.isArray(data.messages)) { throw new Error("Invalid file format: missing conversation or messages"); } // Validate conversation object if (!data.conversation.title || typeof data.conversation.title !== "string") { throw new Error("Invalid file format: missing or invalid conversation title"); } // Validate messages structure for (let i = 0; i < data.messages.length; i++) { const msg = data.messages[i]; if (!msg.role || !msg.content) { throw new Error(`Invalid message at index ${i}: missing role or content`); } if (msg.role !== "user" && msg.role !== "assistant") { throw new Error(`Invalid message role at index ${i}: must be 'user' or 'assistant'`); } } resolve(data); } catch (error) { showNotification("Invalid JSON file: " + error.message, "error"); resolve(null); } }); input.click(); }); } // Export for use in other scripts if (typeof window !== "undefined") { window.escapeHtml = escapeHtml; window.wrapTablesForScroll = wrapTablesForScroll; window.downloadFile = downloadFile; window.sanitizeFilename = sanitizeFilename; window.showExportFormatModal = showExportFormatModal; window.showImportFilePicker = showImportFilePicker; }