Download Chat History as HTML file

Export chat history as an HTML file

当前为 2025-01-10 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

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

(function() {
    'use strict';

    // Create and style the button
    const button = document.createElement('button');
    button.innerText = 'Download chat';
    button.style.position = 'fixed';
    button.style.top = '3px';
    button.style.right = '93px';
    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', () => {
        const conversationId = window.location.pathname.split('/').pop();

        // Fetch chat history
        const historyUrl = `https://api.xoul.ai/api/v1/chat/history?conversation_id=${conversationId}`;
        const detailsUrl = `https://api.xoul.ai/api/v1/conversation/details?conversation_id=${conversationId}`;

        Promise.all([
            fetchJson(historyUrl),
            fetchJson(detailsUrl)
        ]).then(([history, details]) => {
            const assistantName = details.xouls?.[0]?.name || 'assistant';
            const userName = details.personas?.[0]?.name || 'user';

            // Get the first message timestamp and format it
            const firstTimestamp = new Date(history[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')}`;

            // Create HTML structure for chat
            let chatHtml = `
            <!DOCTYPE html>
            <html lang="en">
            <head>
                <meta charset="UTF-8">
                <meta name="viewport" content="width=device-width, initial-scale=1.0">
                <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; }
                    strong { font-weight: bold; }
                    .timestamp { font-size: 0.8em; color: #aaa; }
                </style>
            </head>
            <body>
                <div class="chat-container">
            `;

            // Format each message as a bubble
            history.reverse().forEach(entry => {
                const role = entry.role === 'assistant' ? assistantName : userName;
                const isAssistant = entry.role === 'assistant';
                // Replace *italic* syntax and newlines with <br>
                const formattedContent = entry.content
                    .replace(/\*(.*?)\*/g, '<em>$1</em>') // Markdown: *italic* => <em>
                    .replace(/\n/g, '<br><br>'); // Replace newlines with <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>
            `;

            // Create and download HTML file with custom filename (assistantName and timestamp)
            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)
            });
        });
    }
})();