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">×</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">×</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');
})();