Download Full Chat History as HTML file

Export entire chat history as an HTML file

  1. // ==UserScript==
  2. // @name Download Full Chat History as HTML file
  3. // @namespace http://tampermonkey.net/
  4. // @version 2.5
  5. // @description Export entire chat history as an HTML file
  6. // @author Clawberry+ChatGPT+Moaki
  7. // @match https://story.xoul.ai/*
  8. // @grant GM_xmlhttpRequest
  9. // @connect api.xoul.ai
  10. // @license MIT
  11. // ==/UserScript==
  12.  
  13. (function () {
  14. 'use strict';
  15.  
  16. const button = document.createElement('button');
  17. button.innerText = 'Download full chat';
  18. button.style.position = 'fixed';
  19. button.style.bottom = '60px';
  20. button.style.right = '18px';
  21. button.style.zIndex = '1000';
  22. button.style.padding = '10px';
  23. button.style.backgroundColor = '#404040';
  24. button.style.color = 'white';
  25. button.style.border = 'none';
  26. button.style.borderRadius = '5px';
  27. button.style.cursor = 'pointer';
  28. document.body.appendChild(button);
  29.  
  30. button.addEventListener('click', async () => {
  31. const conversationId = window.location.pathname.split('/').pop();
  32. const detailsUrl = `https://api.xoul.ai/api/v1/conversation/details?conversation_id=${conversationId}`;
  33.  
  34. try {
  35. const details = await fetchJson(detailsUrl);
  36. const assistantName = details.xouls?.[0]?.name || 'assistant';
  37. const userName = details.personas?.[0]?.name || 'user';
  38.  
  39. let allMessages = [];
  40. let cursor = null;
  41.  
  42. // Debug: log initial state
  43. console.log('Initial Cursor:', cursor);
  44.  
  45. // Fetch all messages using cursor pagination
  46. do {
  47. const historyUrl = `https://api.xoul.ai/api/v1/chat/history?conversation_id=${conversationId}` + (cursor ? `&cursor=${cursor}` : '');
  48.  
  49. // Log the current cursor and URL being requested
  50. console.log('Cursor:', cursor);
  51. console.log('Requesting URL:', historyUrl);
  52.  
  53. const history = await fetchJson(historyUrl);
  54.  
  55. // Log the fetched history response
  56. console.log('Fetched history:', history);
  57.  
  58. if (history.length > 0) {
  59. allMessages = allMessages.concat(history);
  60.  
  61. // Debug: log the last message to check the cursor value
  62. console.log('Last message ID:', history[history.length - 1].turn_id);
  63.  
  64. // Debug: Log the structure of the last message
  65. console.log('Last message:', history[history.length - 1]);
  66.  
  67. // Log the properties of the last message to find the ID
  68. console.log('Last message properties:', Object.keys(history[history.length - 1]));
  69.  
  70. cursor = history[history.length - 1].turn_id; // Set cursor to the last message's ID
  71. } else {
  72. cursor = null;
  73. }
  74.  
  75. // Debug: log the updated cursor value after each fetch
  76. console.log('Updated Cursor:', cursor);
  77. } while (cursor);
  78.  
  79. allMessages.reverse(); // Ensure chronological order
  80.  
  81. const firstTimestamp = new Date(allMessages[0].timestamp);
  82. 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')}`;
  83.  
  84. let chatHtml = `
  85. <!DOCTYPE html>
  86. <html lang="en">
  87. <head>
  88. <meta charset="UTF-8">
  89. <title>Chat History</title>
  90. <style>
  91. body { font-family: Roboto, sans-serif; background-color: #1e1e1e; color: #f5f5f5; padding: 20px; }
  92. .chat-container { display: flex; flex-direction: column; gap: 10px; }
  93. .chat-bubble { padding: 10px; border-radius: 10px; max-width: 60%; margin-bottom: 10px; line-height: 1.4; }
  94. .assistant { background-color: #333; color: #fff; align-self: flex-start; }
  95. .user { background-color: #555; color: #fff; align-self: flex-end; }
  96. .timestamp { font-size: 0.8em; color: #aaa; }
  97. </style>
  98. </head>
  99. <body>
  100. <div class="chat-container">
  101. `;
  102.  
  103. allMessages.forEach(entry => {
  104. const role = entry.role === 'assistant' ? assistantName : userName;
  105. const isAssistant = entry.role === 'assistant';
  106. const formattedContent = entry.content.replace(/\*(.*?)\*/g, '<em>$1</em>').replace(/\n/g, '<br><br>');
  107. chatHtml += `
  108. <div class="chat-bubble ${isAssistant ? 'assistant' : 'user'}">
  109. <strong>${role}</strong><br>
  110. <span class="timestamp">${new Date(entry.timestamp).toLocaleString()}</span><br>
  111. ${formattedContent}
  112. </div>
  113. `;
  114. });
  115.  
  116. chatHtml += `
  117. </div>
  118. </body>
  119. </html>
  120. `;
  121.  
  122. const filename = `${assistantName}_${formattedTimestamp}.html`;
  123. const blob = new Blob([chatHtml], { type: 'text/html' });
  124. const link = document.createElement('a');
  125. link.href = URL.createObjectURL(blob);
  126. link.download = filename;
  127. link.click();
  128. } catch (error) {
  129. alert('Error fetching chat history: ' + error.message);
  130. }
  131. });
  132.  
  133. function fetchJson(url) {
  134. return new Promise((resolve, reject) => {
  135. GM_xmlhttpRequest({
  136. method: 'GET',
  137. url,
  138. responseType: 'json',
  139. onload: response => resolve(response.response),
  140. onerror: error => reject(error)
  141. });
  142. });
  143. }
  144. })();