Gemini JSON exporter

Export messages from Gemini share links into tavern/ooba format

// ==UserScript==
// @name         Gemini JSON exporter
// @namespace    lugia19.com
// @version      0.4
// @description  Export messages from Gemini share links into tavern/ooba format
// @author       lugia19
// @match        https://gemini.google.com/share/*
// @grant        none
// @license      MIT
// ==/UserScript==

//Data conversion stuff
function extractConversationGemini() {
    const messages = [];
    // Collect all user and assistant messages
    const userQueries = document.querySelectorAll('user-query .query-text');
    const assistantResponses = document.querySelectorAll('response-container .message-content');

    for (let i = 0; i < userQueries.length || i < assistantResponses.length; i++) {
        if (userQueries[i]) {
            messages.push({
                parent: "",
                message: {
                    author: {
                        role: "user"
                    },
                    create_time: getCurrentUnixTimestamp(),
                    content: {
                        parts: [userQueries[i].textContent.trim()]
                    }
                }
            });
        }
        if (assistantResponses[i]) {
            messages.push({
                parent: "",
                message: {
                    author: {
                        role: "assistant"
                    },
                    create_time: getCurrentUnixTimestamp(),
                    content: {
                        parts: [assistantResponses[i].textContent.trim()]
                    }
                }
            });
        }
    }

    return messages;
}

function convertMessageToTavern(messageData) {
    if (!messageData.message) {
        return null;
    }

    const senderRole = messageData.message.author.role;
    if (senderRole === 'system') {
        return null;
    }

    const isAssistant = senderRole === 'assistant';
    const createTime = messageData.message.create_time;
    const text = messageData.message.content.parts[0];

    return {
        name: isAssistant ? 'Assistant' : 'You',
        is_user: !isAssistant,
        is_name: isAssistant,
        send_date: createTime,
        mes: text,
        swipes: [text],
        swipe_id: 0,
    };
}

function jsonlStringify(messageArray) {
    return messageArray.map(msg => JSON.stringify(msg)).join('\n');
}

function getTavernString() {
    const conversation = extractConversationGemini();

    const convertedConvo = [{
        user_name: 'You',
        character_name: 'Assistant',
    }];

    conversation.forEach((message) => {
        const convertedMsg = convertMessageToTavern(message);
        if (convertedMsg !== null) {
            convertedConvo.push(convertedMsg);
        }
    });

    return jsonlStringify(convertedConvo);
}

function getOobaString() {
    const messages = extractConversationGemini();
    const pairs = [];
    let idx = 0;

    while (idx < messages.length - 1) {
        const message = messages[idx];
        const nextMessage = messages[idx + 1];
        let role, text, nextRole, nextText;

        if (!message.message || !nextMessage.message) {
            idx += 1;
            continue;
        }

        try {
            role = message.message.author.role;
            text = message.message.content.parts[0];
            nextRole = nextMessage.message.author.role;
            nextText = nextMessage.message.content.parts[0];
        } catch (error) {
            idx += 1;
            continue;
        }

        if (role === 'system') {
            if (text !== '') {
                pairs.push(['<|BEGIN-VISIBLE-CHAT|>', text]);
            }
            idx += 1;
            continue;
        }

        if (role === 'user') {
            if (nextRole === 'assistant') {
                pairs.push([text, nextText]);
                idx += 2;
                continue;
            } else if (nextRole === 'user') {
                pairs.push([text, '']);
                idx += 1;
                continue;
            }
        }

        if (role === 'assistant') {
            pairs.push(['', text]);
            idx += 1;
        }
    }
    const oobaData = {
        internal: pairs,
        visible: JSON.parse(JSON.stringify(pairs)),
    };

    if (oobaData.visible[0] && oobaData.visible[0][0] === '<|BEGIN-VISIBLE-CHAT|>') {
        oobaData.visible[0][0] = '';
    }

    return JSON.stringify(oobaData, null, 2);
}


// Function to get current Unix timestamp in milliseconds
function getCurrentUnixTimestamp() {
    return Math.floor(Date.now());
}


// Function to download data as JSON (Shamelessly copied from ChatGPT-Exporter)
function downloadFile(filename, type, content) {
    const blob = content instanceof Blob ? content : new Blob([content], { type })
    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)
}

// Create and style button
const menuButton = document.createElement('button');
menuButton.textContent = 'Export Chatlog';
Object.assign(menuButton.style, {
    position: 'fixed',
    bottom: '20px',
    right: '20px',
    zIndex: 1000,
    backgroundColor: '#131314',
    color: 'white',
    border: '1px solid white',
    padding: '10px 20px',
    cursor: 'pointer',
    borderRadius: '5px',
    boxSizing: 'border-box', // Include padding and border in the width
    textAlign: 'center', // Center the text
});

// Create menu container
const menuContainer = document.createElement('div');
Object.assign(menuContainer.style, {
    position: 'fixed',
    bottom: '70px', // Adjusted to not overlap the main button
    right: '20px',
    zIndex: 1001,
    display: 'none', // Initially hidden
    backgroundColor: '#131314',
    borderRadius: '0px',
    boxShadow: '0 4px 8px rgba(0,0,0,0.1)',
});

// Function to toggle menu display
function toggleMenu() {
    menuContainer.style.display = menuContainer.style.display === 'none' ? 'block' : 'none';
}

// Add click event listener to menu button
menuButton.addEventListener('click', toggleMenu);

// Add buttons to the menu
['Tavern', 'Ooba'].forEach(type => {
    const button = document.createElement('button');
    button.textContent = `${type} format`;
    Object.assign(button.style, {
        backgroundColor: '#131314',
        color: 'white',
        border: '1px solid white',
        padding: '10px 20px',
        cursor: 'pointer',
        borderRadius: '5px',
        display: 'block', // Ensure each button is on its own line
        marginTop: '10px', // Space out the buttons
        width: '100%', // Ensure consistent width
        boxSizing: 'border-box', // Include padding and border in the width
        textAlign: 'center', // Center the text
    });
    button.onclick = function () {
        let title = document.title.replace(/^\W+/, '').trim().replace(/\s+/g, '_');
        if (type === 'Tavern') {
            const resultString = getTavernString();
            downloadFile(`${title}_tavern.jsonl`, 'application/json', resultString);
        } else if (type === 'Ooba') {
            const resultString = getOobaString();
            downloadFile(`${title}_ooba.json`, 'application/json', resultString);
        } else { // JSON
            const resultString = JSON.stringify(extractConversationGemini(), null, 2);
            downloadFile(`${title}.json`, 'application/json', resultString);
        }
        // Optionally close the menu after download
        toggleMenu();
    };
    menuContainer.appendChild(button);
});

// Add menu and menu button to the page
document.body.appendChild(menuButton);
document.body.appendChild(menuContainer);