Download Full Chat History as HTML file

Export entire chat history as an HTML file

// ==UserScript==
// @name         Download Full Chat History as HTML file
// @namespace    http://tampermonkey.net/
// @version      2.5
// @description  Export entire chat history as an HTML file
// @author       Clawberry+ChatGPT+Moaki
// @match        https://story.xoul.ai/*
// @grant        GM_xmlhttpRequest
// @connect      api.xoul.ai
// @license MIT
// ==/UserScript==

(function () {
    'use strict';

    const button = document.createElement('button');
    button.innerText = 'Download full chat';
    button.style.position = 'fixed';
    button.style.bottom = '60px';
    button.style.right = '18px';
    button.style.zIndex = '1000';
    button.style.padding = '10px';
    button.style.backgroundColor = '#404040';
    button.style.color = 'white';
    button.style.border = 'none';
    button.style.borderRadius = '5px';
    button.style.cursor = 'pointer';
    document.body.appendChild(button);

    button.addEventListener('click', async () => {
        const conversationId = window.location.pathname.split('/').pop();
        const detailsUrl = `https://api.xoul.ai/api/v1/conversation/details?conversation_id=${conversationId}`;

        try {
            const details = await fetchJson(detailsUrl);
            const assistantName = details.xouls?.[0]?.name || 'assistant';
            const userName = details.personas?.[0]?.name || 'user';

            let allMessages = [];
            let cursor = null;

            // Debug: log initial state
            console.log('Initial Cursor:', cursor);

            // Fetch all messages using cursor pagination
            do {
                const historyUrl = `https://api.xoul.ai/api/v1/chat/history?conversation_id=${conversationId}` + (cursor ? `&cursor=${cursor}` : '');

                 // Log the current cursor and URL being requested
                console.log('Cursor:', cursor);
                console.log('Requesting URL:', historyUrl);

                const history = await fetchJson(historyUrl);

                // Log the fetched history response
                console.log('Fetched history:', history);

                if (history.length > 0) {
                    allMessages = allMessages.concat(history);

                    // Debug: log the last message to check the cursor value
                    console.log('Last message ID:', history[history.length - 1].turn_id);

                    // Debug: Log the structure of the last message
                    console.log('Last message:', history[history.length - 1]);

                    // Log the properties of the last message to find the ID
                    console.log('Last message properties:', Object.keys(history[history.length - 1]));

                    cursor = history[history.length - 1].turn_id; // Set cursor to the last message's ID
                } else {
                    cursor = null;
                }

                // Debug: log the updated cursor value after each fetch
                console.log('Updated Cursor:', cursor);
            } while (cursor);

            allMessages.reverse(); // Ensure chronological order

            const firstTimestamp = new Date(allMessages[0].timestamp);
            const formattedTimestamp = `${firstTimestamp.getFullYear()}-${String(firstTimestamp.getMonth() + 1).padStart(2, '0')}-${String(firstTimestamp.getDate()).padStart(2, '0')}_${String(firstTimestamp.getHours()).padStart(2, '0')}-${String(firstTimestamp.getMinutes()).padStart(2, '0')}-${String(firstTimestamp.getSeconds()).padStart(2, '0')}`;

            let chatHtml = `
                <!DOCTYPE html>
                <html lang="en">
                <head>
                    <meta charset="UTF-8">
                    <title>Chat History</title>
                    <style>
                        body { font-family: Roboto, sans-serif; background-color: #1e1e1e; color: #f5f5f5; padding: 20px; }
                        .chat-container { display: flex; flex-direction: column; gap: 10px; }
                        .chat-bubble { padding: 10px; border-radius: 10px; max-width: 60%; margin-bottom: 10px; line-height: 1.4; }
                        .assistant { background-color: #333; color: #fff; align-self: flex-start; }
                        .user { background-color: #555; color: #fff; align-self: flex-end; }
                        .timestamp { font-size: 0.8em; color: #aaa; }
                    </style>
                </head>
                <body>
                    <div class="chat-container">
                `;

            allMessages.forEach(entry => {
                const role = entry.role === 'assistant' ? assistantName : userName;
                const isAssistant = entry.role === 'assistant';
                const formattedContent = entry.content.replace(/\*(.*?)\*/g, '<em>$1</em>').replace(/\n/g, '<br><br>');
                chatHtml += `
                    <div class="chat-bubble ${isAssistant ? 'assistant' : 'user'}">
                        <strong>${role}</strong><br>
                        <span class="timestamp">${new Date(entry.timestamp).toLocaleString()}</span><br>
                        ${formattedContent}
                    </div>
                    `;
            });

            chatHtml += `
                    </div>
                </body>
                </html>
                `;

            const filename = `${assistantName}_${formattedTimestamp}.html`;
            const blob = new Blob([chatHtml], { type: 'text/html' });
            const link = document.createElement('a');
            link.href = URL.createObjectURL(blob);
            link.download = filename;
            link.click();
        } catch (error) {
            alert('Error fetching chat history: ' + error.message);
        }
    });

    function fetchJson(url) {
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: 'GET',
                url,
                responseType: 'json',
                onload: response => resolve(response.response),
                onerror: error => reject(error)
            });
        });
    }
})();