Save Button is gone and the Automatic Saving of prompts is not working

The Save Button for Google AI Studio is gone and has been replaced by some sort of “automatic” save function, but that starts OK but locks up after about 15 minutes. The result is that if I quit all the additional prompting and chat that I have done since is lost. OK have a “automatic” save, but don’t take away the Manual Save. With that I can at least have a local copy, and can retrieve my chat threads again. BTW I am using Firefox.

7 Likes

Is this for the data set thing? If so, I have this as well.

2 Likes

I had the same issue already at least three times. The first two times that I remember I was thinking that the reason might have been me opening the same chat from desktop and mobile browser. Therefore I thought I could prevent the situation from happening again by only having a single access to the site.

It latest a little until I realised that it was an issue with auto-save not working as it should. As you I was observing that at a time auto-save started without ever coming to an end. And when I checked the history at a separate tab I could verify that the last save was hours ago despite of me working with the chat. Unfortunately, there isn’t a separate save button and also disabling auto-save doesn’t give us that button back without reloading the chat what of course I didn’t want to do. Therefore I kept my chat open and used hibernation to not lose my messages for as long as possible. But because the next browser or system update was lurking at the corner, I was in a rush to find a way to not lose all of my data.

I finally came up with a page scraper script with the help of AI, that I added to the snippets in the tab sources in the browser’s developer tools and started it. The script slowly scrolls the page to get the current content loaded and then collects the text of the different messages and logs it to the developer console log. It also tries to include the images as standalone base64 but doesn’t seem to be as successful with that currently. Nevertheless I was able to extract my whole chat and was able to continue it in another chat without losing too much of my previous research. The old chat has been reset to its old state by a browser update in the meanwhile. But at least, the new chat has my old chat’s data now.

Now I have also turned of auto-save and instead click on save on my own regularly - hopefully I will always remember to do it and it doesn’t also get stuck then though. And if it should get stuck again, at least I have a script to rescue my unsaved messages if I notice it in time.

For those who are interested here is the code snippet that I used. Use it with care and at your own risk. Make sure that the result fulfills your expectations before closing your old chat or else you won’t be able to get the unsaved messages back then. I have continuously developed this script to this point with AI but didn’t spend any further time to double-check or optimize its code even though I have could. The results were good enough for me to leave it like that.

// Extended version with Base64 images and correct escaping
(async () => {
    const container = document.querySelector('ms-autoscroll-container');
    if (!container) {
        console.error('❌ ms-autoscroll-container not found!');
        return;
    }

    console.log('Starting extended extraction with Base64 images...');

    // Function to convert an image to a Base64 string
    async function imageToBase64(imgElement) {
        return new Promise((resolve, reject) => {
            try {
                const canvas = document.createElement('canvas');
                const ctx = canvas.getContext('2d');

                // Set crossOrigin for external images
                const img = new Image();
                img.crossOrigin = 'anonymous';

                img.onload = function () {
                    canvas.width = img.naturalWidth;
                    canvas.height = img.naturalHeight;
                    ctx.drawImage(img, 0, 0);

                    try {
                        const dataURL = canvas.toDataURL('image/jpeg', 0.85);
                        resolve(dataURL);
                    } catch (e) {
                        console.warn('⚠️ Could not convert image to Data URL:', e);
                        resolve(imgElement.src); // Fallback to the original URL
                    }
                };

                img.onerror = function () {
                    console.warn('⚠️ Could not load image:', imgElement.src);
                    resolve(imgElement.src); // Fallback to the original URL
                };

                img.src = imgElement.src;
            } catch (e) {
                console.warn('⚠️ Image conversion failed:', e);
                resolve(imgElement.src); // Fallback to the original URL
            }
        });
    }

    // Function to escape HTML special characters
    function escapeHtml(text) {
        const div = document.createElement('div');
        div.textContent = text;
        return div.innerHTML;
    }

    // Function to get all chunks in their actual DOM order
    function getAllChunksInOrder() {
        const chunks = [];
        const walker = document.createTreeWalker(
            container,
            NodeFilter.SHOW_ELEMENT, {
                acceptNode: function (node) {
                    return node.tagName === 'MS-PROMPT-CHUNK' ?
                        NodeFilter.FILTER_ACCEPT :
                        NodeFilter.FILTER_SKIP;
                }
            }
        );

        let node;
        while (node = walker.nextNode()) {
            chunks.push(node);
        }

        return chunks;
    }

    const seenTexts = new Set();
    const result = [];

    async function collectAllChunks() {
        const allChunks = getAllChunksInOrder();
        console.log(` Found: ${allChunks.length} ms-prompt-chunk elements`);

        for (const [index, chunk] of allChunks.entries()) {
            const text = chunk.innerText ? .trim();
            if (!text || text.length < 5) continue;

            const textHash = text.substring(0, 150).replace(/\s+/g, ' ').trim();
            if (seenTexts.has(textHash)) continue;

            seenTexts.add(textHash);

            // Determine the role
            const role = chunk.closest('[class*="user"]') ? 'USER' :
                chunk.closest('[class*="assistant"], [class*="ai"], [class*="gemini"]') ? 'GEMINI' : '❓ UNKNOWN';

            // Collect media and convert images to Base64
            const media = [];
            const images = chunk.querySelectorAll('img');
            for (const img of images) {
                if (img.src) {
                    console.log(` Converting image to Base64...`);
                    const base64 = await imageToBase64(img);
                    media.push({
                        type: 'img',
                        data: base64,
                        alt: img.alt || '',
                        width: img.naturalWidth || img.width,
                        height: img.naturalHeight || img.height
                    });
                }
            }

            // Collect SVGs
            chunk.querySelectorAll('svg').forEach(svg => {
                const svgData = new XMLSerializer().serializeToString(svg);
                media.push({
                    type: 'svg',
                    data: svgData,
                    alt: 'SVG Graphic'
                });
            });

            result.push({
                text,
                role,
                media,
                domIndex: index
            });
        }
        console.log(`✅ ${result.length} unique chunks collected`);
    }

    // Collection process in multiple steps
    console.log('Collection starting...');
    container.scrollTop = 0;
    await new Promise(r => setTimeout(r, 1000));
    await collectAllChunks();

    const step = Math.floor(container.clientHeight * 0.3);
    for (let pos = 0; pos <= container.scrollHeight; pos += step) {
        container.scrollTop = pos;
        await new Promise(r => setTimeout(r, 300));
        await collectAllChunks();
    }

    container.scrollTop = container.scrollHeight;
    await new Promise(r => setTimeout(r, 1000));
    await collectAllChunks();

    // Final collection
    await collectAllChunks();

    // Sort by DOM index
    result.sort((a, b) => a.domIndex - b.domIndex);

    const totalMedia = result.reduce((sum, c) => sum + c.media.length, 0);
    const totalImages = result.reduce((sum, c) => sum + c.media.filter(m => m.type === 'img').length, 0);
    console.log(`✅ Final result: ${result.length} chunks, ${totalMedia} media items (${totalImages} images as Base64)`);

    // Generate text output
    let textOutput = `CHAT EXPORT - ${new Date().toLocaleString('en-US')}\n`;
    textOutput += `${'='.repeat(60)}\n`;
    textOutput += ` ${result.length} Messages • ${totalMedia} Media Items • ${totalImages} Images (Base64)\n`;
    textOutput += `${'='.repeat(60)}\n\n`;

    let currentRole = null;
    let turnNum = 0;

    result.forEach((chunk, i) => {
        if (chunk.role !== currentRole) {
            currentRole = chunk.role;
            turnNum++;
            textOutput += `\n${chunk.role} (Turn ${turnNum})\n${'='.repeat(60)}\n`;
        }

        textOutput += `[${i + 1}] ${chunk.text}\n`;

        if (chunk.media.length > 0) {
            textOutput += ` ${chunk.media.length} media item(s):\n`;
            chunk.media.forEach((item, j) => {
                if (item.type === 'img') {
                    textOutput += `   ${j + 1}. 🖼️ Image: ${item.width}x${item.height}px (Base64: ${Math.round(item.data.length/1024)}KB)\n`;
                } else if (item.type === 'svg') {
                    textOutput += `   ${j + 1}. 🎨 SVG: ${Math.round(item.data.length/1024)}KB\n`;
                }
            });
        }
        textOutput += `\n`;
    });

    // Generate HTML output with Base64 images and correct escaping
    const htmlOutput = `
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Chat Export with Base64 Images</title>
        <style>
            body { 
                font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; 
                max-width: 1200px; margin: 0 auto; padding: 20px; 
                background: #f8f9fa; line-height: 1.6; 
            }
            .header { 
                background: white; padding: 20px; border-radius: 10px; 
                box-shadow: 0 2px 10px rgba(0,0,0,0.1); margin-bottom: 20px; 
                text-align: center; 
            }
            .controls { 
                text-align: center; margin-bottom: 20px; 
                display: flex; gap: 10px; justify-content: center; flex-wrap: wrap;
            }
            .btn { 
                padding: 10px 20px; border: none; border-radius: 6px; 
                cursor: pointer; font-weight: 600; background: #007bff; 
                color: white; transition: all 0.2s;
            }
            .btn:hover { background: #0056b3; }
            .btn-secondary { background: #6c757d; }
            .btn-secondary:hover { background: #545b62; }
            .btn-success { background: #28a745; }
            .btn-success:hover { background: #1e7e34; }
            
            .chunk { 
                background: white; margin: 15px 0; border-radius: 10px; 
                box-shadow: 0 2px 10px rgba(0,0,0,0.1); overflow: hidden; 
            }
            .chunk-header { 
                padding: 12px 20px; color: white; font-weight: 600; 
                display: flex; justify-content: space-between; align-items: center;
            }
            .user-header { background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); }
            .gemini-header { background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%); }
            .unknown-header { background: linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%); color: #333; }
            
            .chunk-content { 
                padding: 20px; 
                white-space: pre-wrap; 
                word-wrap: break-word; 
                overflow-wrap: break-word;
            }
            
            .chunk-content code {
                background: #f8f9fa;
                padding: 2px 4px;
                border-radius: 3px;
                font-family: 'Courier New', monospace;
                font-size: 0.9em;
            }
            
            .chunk-media { 
                display: flex; flex-wrap: wrap; gap: 15px; 
                margin-top: 15px; padding: 15px 20px; 
                border-top: 1px solid #e9ecef;
            }
            .media-item { 
                max-width: 100%; 
                border-radius: 8px; 
                overflow: hidden;
                box-shadow: 0 2px 8px rgba(0,0,0,0.1);
            }
            .media-item img { 
                max-width: 100%; 
                height: auto; 
                display: block;
                border-radius: 8px;
            }
            .media-item svg { 
                max-width: 400px; 
                height: auto; 
                border-radius: 8px; 
                border: 1px solid #dee2e6; 
            }
            .media-caption {
                font-size: 0.8em;
                color: #6c757d;
                margin-top: 5px;
                text-align: center;
            }
            
            .success-msg { 
                position: fixed; top: 20px; right: 20px; 
                background: #28a745; color: white; padding: 10px 20px; 
                border-radius: 6px; z-index: 1000; 
                animation: fadeInOut 3s ease-in-out; 
            }
            @keyframes fadeInOut { 
                0%, 100% { opacity: 0; transform: translateY(-20px); } 
                15%, 85% { opacity: 1; transform: translateY(0); } 
            }
            
            .stats {
                background: #e9ecef;
                padding: 10px;
                border-radius: 5px;
                margin-bottom: 10px;
                font-size: 0.9em;
            }
        </style>
    </head>
    <body>
        <div class="header">
            <h1>Chat Export with Base64 Images</h1>
            <div class="stats">
                 ${result.length} Messages • 🖼️ ${totalImages} Images (Base64) • 📂 ${totalMedia} Total Media
            </div>
            <p>Exported on ${new Date().toLocaleString('en-US')}</p>
        </div>
        
        <div class="controls">
            <button class="btn" onclick="copyAllAsText()">Copy All as Text</button>
            <button class="btn btn-secondary" onclick="saveAsHTML()">Save as HTML</button>
            <button class="btn btn-success" onclick="printPage()">🖨️ Print</button>
        </div>
        
        <div id="chunks">
            ${result.map((chunk, i) => {
                const isNewRole = i === 0 || chunk.role !== result[i - 1].role;
                const roleClass = chunk.role.includes('USER') ? 'user-header' : 
                                 chunk.role.includes('GEMINI') ? 'gemini-header' : 'unknown-header';
                
                const escapedText = escapeHtml(chunk.text);
                
                return \`
                <div class="chunk">
                    \${isNewRole ? \`<div class="chunk-header \${roleClass}">
                        <span>\${chunk.role}</span>
                        <span>Message \${i + 1}</span>
                    </div>\` : ''}
                    
                    <div class="chunk-content">\${escapedText}</div>
                    
                    \${chunk.media.length > 0 ? \`
                        <div class="chunk-media">
                            \${chunk.media.map((item, j) => {
                                if (item.type === 'img') {
                                    return \`
                                        <div class="media-item">
                                            <img src="\${item.data}" alt="\${escapeHtml(item.alt)}" loading="lazy">
                                            <div class="media-caption">
                                                🖼️ Image \${j + 1}: \${item.width}×\${item.height}px 
                                                (\${Math.round(item.data.length/1024)}KB)
                                            </div>
                                        </div>
                                    \`;
                                } else if (item.type === 'svg') {
                                    return \`
                                        <div class="media-item">
                                            \${item.data}
                                            <div class="media-caption">
                                                🎨 SVG \${j + 1} (\${Math.round(item.data.length/1024)}KB)
                                            </div>
                                        </div>
                                    \`;
                                }
                                return '';
                            }).join('')}
                        </div>
                    \` : ''}
                </div>
                \`;
            }).join('')}
        </div>
        
        <script>
            const textOutput = \${JSON.stringify(textOutput)};
            
            function showSuccessMsg(msg) {
                const div = document.createElement('div');
                div.className = 'success-msg';
                div.textContent = msg;
                document.body.appendChild(div);
                setTimeout(() => div.remove(), 3000);
            }
            
            function copyAllAsText() {
                navigator.clipboard.writeText(textOutput).then(() => {
                    showSuccessMsg('✅ Full text copied to clipboard!');
                }).catch(err => {
                    showSuccessMsg('❌ Copy failed: ' + err.message);
                });
            }
            
            function saveAsHTML() {
                const blob = new Blob([document.documentElement.outerHTML], {
                    type: 'text/html;charset=utf-8'
                });
                const url = URL.createObjectURL(blob);
                const a = document.createElement('a');
                a.href = url;
                a.download = 'chat-export-' + new Date().toISOString().slice(0,10) + '.html';
                document.body.appendChild(a);
                a.click();
                document.body.removeChild(a);
                URL.revokeObjectURL(url);
                showSuccessMsg('✅ HTML file saved!');
            }
            
            function printPage() {
                window.print();
            }
        </script>
    </body>
    </html>
    `;

    // Log the final output to the console
    console.log("---BEGIN textOutput---");
    console.log(textOutput);
    console.log("---END textOutput---");

    console.log("---BEGIN htmlOutput---");
    console.log(htmlOutput);
    console.log("---END htmlOutput---");
    
})();
1 Like