📜
saveload.js
Back
📝 Javascript ⚡ Executable Ctrl+S: Save • Ctrl+R: Run • Ctrl+F: Find
window.App = window.App || {}; (() => { const els = { saveConversation: document.getElementById('saveConversation'), loadConversation: document.getElementById('loadConversation'), loadConversationFile: document.getElementById('loadConversationFile') }; function generateFilename() { const now = new Date(); const year = now.getFullYear(); const month = String(now.getMonth() + 1).padStart(2, '0'); const day = String(now.getDate()).padStart(2, '0'); const hours = String(now.getHours()).padStart(2, '0'); const minutes = String(now.getMinutes()).padStart(2, '0'); return `chat-conversation-${year}${month}${day}-${hours}${minutes}.json`; } function isMobileDevice() { return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) || ('ontouchstart' in window) || (navigator.maxTouchPoints > 0); } function showSaveModal(jsonString, filename) { // Create modal overlay const overlay = document.createElement('div'); overlay.className = 'fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm'; overlay.style.animation = 'fadeIn 0.2s ease-out'; const modal = document.createElement('div'); modal.className = 'bg-white dark:bg-zinc-900 rounded-2xl shadow-2xl w-full max-w-2xl max-h-[90vh] flex flex-col border border-zinc-200 dark:border-zinc-800'; // Header const header = document.createElement('div'); header.className = 'p-4 border-b border-zinc-200 dark:border-zinc-800 flex items-center justify-between'; header.innerHTML = ` <h3 class="font-semibold text-lg">Save Conversation</h3> <button id="closeModal" class="w-8 h-8 flex items-center justify-center rounded-full hover:bg-zinc-100 dark:hover:bg-zinc-800 text-zinc-500 hover:text-zinc-700 dark:hover:text-zinc-300 text-2xl leading-none">&times;</button> `; // Body const body = document.createElement('div'); body.className = 'p-4 overflow-y-auto flex-1'; const instructions = document.createElement('div'); instructions.className = 'mb-4 p-3 bg-blue-50 dark:bg-blue-900/20 rounded-lg text-sm'; instructions.innerHTML = ` <p class="font-medium mb-2">Choose how to save:</p> <ul class="space-y-1 text-zinc-700 dark:text-zinc-300"> <li><strong>1. Copy & Save:</strong> Copy the text below and paste into a notes app or text editor, then save as <code class="px-1 bg-white dark:bg-zinc-800 rounded">${filename}</code></li> <li><strong>2. Download:</strong> Try the download button (may not work on all mobile browsers)</li> ${navigator.share ? '<li><strong>3. Share:</strong> Use the share button to send to another app</li>' : ''} </ul> `; const textarea = document.createElement('textarea'); textarea.className = 'w-full h-64 p-3 rounded-lg border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 font-mono text-xs'; textarea.value = jsonString; textarea.readOnly = true; body.append(instructions, textarea); // Footer with buttons const footer = document.createElement('div'); footer.className = 'p-4 border-t border-zinc-200 dark:border-zinc-800 flex gap-2 flex-wrap'; const copyBtn = document.createElement('button'); copyBtn.className = 'flex-1 px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 text-sm font-medium'; copyBtn.textContent = 'Copy to Clipboard'; const downloadBtn = document.createElement('button'); downloadBtn.className = 'flex-1 px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 text-sm font-medium'; downloadBtn.textContent = 'Try Download'; footer.append(copyBtn, downloadBtn); // Add share button if supported if (navigator.share) { const shareBtn = document.createElement('button'); shareBtn.className = 'flex-1 px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 text-sm font-medium'; shareBtn.textContent = 'Share'; footer.appendChild(shareBtn); shareBtn.onclick = async () => { try { const blob = new Blob([jsonString], { type: 'application/json' }); const file = new File([blob], filename, { type: 'application/json' }); await navigator.share({ files: [file], title: 'Chat Conversation', text: 'Saved conversation from Unified Chat' }); } catch (err) { if (err.name !== 'AbortError') { alert('Share failed. Try copying the text instead.'); } } }; } modal.append(header, body, footer); overlay.appendChild(modal); document.body.appendChild(overlay); // Event handlers const close = () => { overlay.style.animation = 'fadeOut 0.2s ease-out'; setTimeout(() => document.body.removeChild(overlay), 200); }; header.querySelector('#closeModal').onclick = close; overlay.onclick = (e) => { if (e.target === overlay) close(); }; copyBtn.onclick = async () => { try { await navigator.clipboard.writeText(jsonString); copyBtn.textContent = '✓ Copied!'; copyBtn.classList.add('bg-green-600', 'hover:bg-green-700'); copyBtn.classList.remove('bg-indigo-600', 'hover:bg-indigo-700'); setTimeout(() => { copyBtn.textContent = 'Copy to Clipboard'; copyBtn.classList.remove('bg-green-600', 'hover:bg-green-700'); copyBtn.classList.add('bg-indigo-600', 'hover:bg-indigo-700'); }, 2000); } catch (err) { textarea.select(); textarea.setSelectionRange(0, 99999); alert('Text selected - now use your device\'s copy function (usually long-press and select "Copy")'); } }; downloadBtn.onclick = () => { try { const blob = new Blob([jsonString], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; a.click(); URL.revokeObjectURL(url); downloadBtn.textContent = '✓ Download Started'; setTimeout(() => { downloadBtn.textContent = 'Try Download'; }, 2000); } catch (err) { alert('Download failed. Please use the copy method instead.'); } }; setTimeout(() => textarea.select(), 100); } function showLoadModal() { const overlay = document.createElement('div'); overlay.className = 'fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm'; overlay.style.animation = 'fadeIn 0.2s ease-out'; const modal = document.createElement('div'); modal.className = 'bg-white dark:bg-zinc-900 rounded-2xl shadow-2xl w-full max-w-2xl max-h-[90vh] flex flex-col border border-zinc-200 dark:border-zinc-800'; const header = document.createElement('div'); header.className = 'p-4 border-b border-zinc-200 dark:border-zinc-800 flex items-center justify-between'; header.innerHTML = ` <h3 class="font-semibold text-lg">Load Conversation</h3> <button id="closeLoadModal" class="w-8 h-8 flex items-center justify-center rounded-full hover:bg-zinc-100 dark:hover:bg-zinc-800 text-zinc-500 hover:text-zinc-700 dark:hover:text-zinc-300 text-2xl leading-none">&times;</button> `; const body = document.createElement('div'); body.className = 'p-4 overflow-y-auto flex-1'; const instructions = document.createElement('div'); instructions.className = 'mb-4 p-3 bg-blue-50 dark:bg-blue-900/20 rounded-lg text-sm'; instructions.innerHTML = ` <p class="font-medium mb-2">Choose how to load:</p> <ul class="space-y-1 text-zinc-700 dark:text-zinc-300"> <li><strong>1. Paste JSON:</strong> Copy the saved conversation JSON and paste it in the box below</li> <li><strong>2. Select File:</strong> Click "Browse File" to select a saved .json file</li> </ul> `; const textarea = document.createElement('textarea'); textarea.id = 'pasteJsonArea'; textarea.className = 'w-full h-64 p-3 rounded-lg border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 font-mono text-xs'; textarea.placeholder = 'Paste your conversation JSON here...'; body.append(instructions, textarea); const footer = document.createElement('div'); footer.className = 'p-4 border-t border-zinc-200 dark:border-zinc-800 flex gap-2 flex-wrap'; const pasteBtn = document.createElement('button'); pasteBtn.className = 'flex-1 px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 text-sm font-medium'; pasteBtn.textContent = 'Load from Paste'; const fileBtn = document.createElement('button'); fileBtn.className = 'flex-1 px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 text-sm font-medium'; fileBtn.textContent = 'Browse File'; footer.append(pasteBtn, fileBtn); modal.append(header, body, footer); overlay.appendChild(modal); document.body.appendChild(overlay); const close = () => { overlay.style.animation = 'fadeOut 0.2s ease-out'; setTimeout(() => document.body.removeChild(overlay), 200); }; header.querySelector('#closeLoadModal').onclick = close; overlay.onclick = (e) => { if (e.target === overlay) close(); }; pasteBtn.onclick = () => { const jsonText = textarea.value.trim(); if (!jsonText) { alert('Please paste JSON data first'); return; } try { const conversationData = JSON.parse(jsonText); if (!conversationData.messages || !Array.isArray(conversationData.messages)) { throw new Error('Invalid conversation data: missing or invalid messages array'); } const messageCount = conversationData.messages.length; const artifactCount = (conversationData.artifacts || []).length; const exportDate = conversationData.exportedAt ? new Date(conversationData.exportedAt).toLocaleString() : 'Unknown'; const confirmMsg = `Load this conversation?\n\n` + `• Messages: ${messageCount}\n` + `• Artifacts: ${artifactCount}\n` + `• Exported: ${exportDate}\n\n` + `This will replace your current conversation.`; if (!confirm(confirmMsg)) return; loadConversationData(conversationData); close(); alert(`Conversation loaded!\n\n${messageCount} messages and ${artifactCount} artifacts restored.`); } catch (error) { console.error('Error loading conversation:', error); alert(`Failed to load conversation:\n\n${error.message}\n\nMake sure you pasted valid JSON data.`); } }; fileBtn.onclick = () => { close(); els.loadConversationFile.click(); }; } function loadConversationData(conversationData) { App.state.messages = conversationData.messages || []; App.state.artifacts = conversationData.artifacts || []; if (conversationData.settings) { const safeSettings = ['codeStyle', 'fullCodePrompt', 'snippetsPrompt', 'chunkedPrompt', 'system']; safeSettings.forEach(key => { if (conversationData.settings[key] !== undefined) { App.state.settings[key] = conversationData.settings[key]; } }); } App.saveState(App.state); if (App.renderTranscript) App.renderTranscript(); if (App.renderSettings) App.renderSettings(); const originalText = els.loadConversation.textContent; els.loadConversation.textContent = '✓ Loaded!'; els.loadConversation.classList.add('bg-green-600'); els.loadConversation.classList.remove('bg-purple-600'); setTimeout(() => { els.loadConversation.textContent = originalText; els.loadConversation.classList.remove('bg-green-600'); els.loadConversation.classList.add('bg-purple-600'); }, 2000); } function saveConversation() { try { const conversationData = { version: '1.0', exportedAt: new Date().toISOString(), messages: App.state.messages || [], artifacts: App.state.artifacts || [], settings: App.state.settings || {}, messageCount: (App.state.messages || []).length }; const jsonString = JSON.stringify(conversationData, null, 2); const filename = generateFilename(); showSaveModal(jsonString, filename); const originalText = els.saveConversation.textContent; els.saveConversation.textContent = '✓ Ready'; setTimeout(() => { els.saveConversation.textContent = originalText; }, 2000); console.log(`Conversation ready to save: ${conversationData.messageCount} messages`); } catch (error) { console.error('Error preparing conversation:', error); alert(`Failed to prepare conversation: ${error.message}`); } } function loadConversation() { showLoadModal(); } function handleFileLoad(event) { const file = event.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = (e) => { try { const conversationData = JSON.parse(e.target.result); if (!conversationData.messages || !Array.isArray(conversationData.messages)) { throw new Error('Invalid conversation file: missing or invalid messages array'); } const messageCount = conversationData.messages.length; const artifactCount = (conversationData.artifacts || []).length; const exportDate = conversationData.exportedAt ? new Date(conversationData.exportedAt).toLocaleString() : 'Unknown'; const confirmMsg = `Load this conversation?\n\n` + `• File: ${file.name}\n` + `• Messages: ${messageCount}\n` + `• Artifacts: ${artifactCount}\n` + `• Exported: ${exportDate}\n\n` + `This will replace your current conversation.`; if (!confirm(confirmMsg)) { els.loadConversationFile.value = ''; return; } loadConversationData(conversationData); alert(`Conversation loaded!\n\n${messageCount} messages and ${artifactCount} artifacts restored.`); els.loadConversationFile.value = ''; } catch (error) { console.error('Error loading conversation:', error); alert(`Failed to load:\n\n${error.message}`); els.loadConversationFile.value = ''; } }; reader.onerror = () => { alert('Failed to read file.'); els.loadConversationFile.value = ''; }; reader.readAsText(file); } if (els.saveConversation) { els.saveConversation.addEventListener('click', saveConversation); } if (els.loadConversation) { els.loadConversation.addEventListener('click', loadConversation); } if (els.loadConversationFile) { els.loadConversationFile.addEventListener('change', handleFileLoad); } App.saveConversation = saveConversation; App.loadConversation = loadConversation; console.log('Save/Load conversation module loaded'); })();