script.js 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  1. document.addEventListener('DOMContentLoaded', () => {
  2. const state = {
  3. conversationFiles: [],
  4. currentIndex: 0,
  5. identities: {},
  6. totalConversations: 74 // Based on the file list
  7. };
  8. const EPSTEIN_ID = 'e:jeeitunes@gmail.com';
  9. const EPSTEIN_AVATAR_URL = 'https://m1s5.c20.e2-5.dev/files/jeeitunes/pfp/jeffrey-epstein-pfp.png';
  10. const prevBtn = document.getElementById('prev-btn');
  11. const nextBtn = document.getElementById('next-btn');
  12. const exportBtn = document.getElementById('export-btn');
  13. const conversationNumberEl = document.getElementById('conversation-number');
  14. const messageContainer = document.getElementById('message-container');
  15. const participantsHeader = document.querySelector('.imessage-header .participant-names');
  16. const avatarsHeader = document.querySelector('.imessage-header .avatars');
  17. // Modal elements
  18. const modal = document.getElementById('identity-modal');
  19. const modalIdentifierEl = document.getElementById('modal-identifier');
  20. const modalNameInput = document.getElementById('modal-name-input');
  21. const modalAvatarInput = document.getElementById('modal-avatar-input');
  22. const modalSaveBtn = document.getElementById('modal-save-btn');
  23. const closeModalBtn = document.querySelector('.close-btn');
  24. function init() {
  25. for (let i = 1; i <= state.totalConversations; i++) {
  26. state.conversationFiles.push(`data/output_${i}.json`);
  27. }
  28. const savedIdentities = localStorage.getItem('speakerIdentities');
  29. if (savedIdentities) {
  30. state.identities = JSON.parse(savedIdentities);
  31. }
  32. // Hardcode Epstein's identity
  33. state.identities[EPSTEIN_ID] = { name: 'Jeffrey Epstein', avatar: EPSTEIN_AVATAR_URL, initials: 'JE' };
  34. prevBtn.addEventListener('click', () => loadConversation(state.currentIndex - 1));
  35. nextBtn.addEventListener('click', () => loadConversation(state.currentIndex + 1));
  36. exportBtn.addEventListener('click', exportConversation);
  37. messageContainer.addEventListener('click', handleAvatarClick);
  38. closeModalBtn.addEventListener('click', () => modal.style.display = 'none');
  39. modalSaveBtn.addEventListener('click', saveIdentity);
  40. loadConversation(0);
  41. }
  42. async function loadConversation(index) {
  43. if (index < 0 || index >= state.totalConversations) {
  44. return;
  45. }
  46. state.currentIndex = index;
  47. try {
  48. const res = await fetch(`data/epstein_imessage_${index + 1}.json`);
  49. if (!res.ok) {
  50. const fallbackRes = await fetch(`data/output_${index + 1}.json`);
  51. if (!fallbackRes.ok) throw new Error(`Conversation ${index + 1} not found`);
  52. const data = await fallbackRes.json();
  53. renderConversation(data, index + 1);
  54. return;
  55. }
  56. const data = await res.json();
  57. renderConversation(data, index + 1);
  58. } catch (error) {
  59. console.error('Error loading conversation:', error);
  60. messageContainer.innerHTML = `<p style="text-align:center;">Could not load conversation ${index + 1}.</p>`;
  61. }
  62. }
  63. function parseMessages(rawMessages) {
  64. const messages = [];
  65. let tempGroup = [];
  66. for (const item of rawMessages) {
  67. if (item.Message && tempGroup.length > 0) {
  68. const messageObject = tempGroup.reduce((acc, curr) => ({ ...acc, ...curr }), {});
  69. messages.push(messageObject);
  70. tempGroup = [];
  71. }
  72. tempGroup.push(item);
  73. }
  74. if (tempGroup.length > 0) {
  75. const messageObject = tempGroup.reduce((acc, curr) => ({ ...acc, ...curr }), {});
  76. messages.push(messageObject);
  77. }
  78. return messages.map(msg => ({
  79. text: msg.Message || '',
  80. sender: msg.Sender || 'me',
  81. time: msg.Time || ''
  82. })).filter(msg => msg.text);
  83. }
  84. function getIdentity(senderId) {
  85. // Explicitly handle Epstein
  86. if (senderId === EPSTEIN_ID) {
  87. return state.identities[EPSTEIN_ID];
  88. }
  89. // Check for user-defined identities
  90. if (state.identities[senderId]) {
  91. return state.identities[senderId];
  92. }
  93. // Default for everyone else
  94. return {
  95. name: '?',
  96. avatar: null,
  97. initials: '?'
  98. };
  99. }
  100. function generateColor(str) {
  101. // Keep a consistent color for the generic "?" avatar
  102. if (str === '?') return '#8e8e93';
  103. let hash = 0;
  104. for (let i = 0; i < str.length; i++) {
  105. hash = str.charCodeAt(i) + ((hash << 5) - hash);
  106. }
  107. let color = '#';
  108. for (let i = 0; i < 3; i++) {
  109. const value = (hash >> (i * 8)) & 0xFF;
  110. color += ('00' + value.toString(16)).substr(-2);
  111. }
  112. return color;
  113. }
  114. function renderConversation(data, convNumber) {
  115. const messages = parseMessages(data.Messages);
  116. messageContainer.innerHTML = '';
  117. const participants = [...new Set(messages.map(m => m.sender))];
  118. participantsHeader.innerHTML = participants.map(p => getIdentity(p).name).join(', ');
  119. avatarsHeader.innerHTML = '';
  120. participants.forEach(p => {
  121. const identity = getIdentity(p);
  122. const avatarEl = document.createElement('div');
  123. avatarEl.className = 'avatar';
  124. if (identity.avatar) {
  125. avatarEl.style.backgroundImage = `url(${identity.avatar})`;
  126. } else {
  127. avatarEl.textContent = identity.initials;
  128. avatarEl.style.backgroundColor = generateColor(identity.name); // Use name for color generation
  129. }
  130. avatarsHeader.appendChild(avatarEl);
  131. });
  132. messages.forEach(msg => {
  133. const bubble = document.createElement('div');
  134. bubble.className = 'message-bubble';
  135. const identity = getIdentity(msg.sender);
  136. const avatar = document.createElement('div');
  137. avatar.className = 'avatar';
  138. avatar.dataset.senderId = msg.sender;
  139. if (identity.avatar) {
  140. avatar.style.backgroundImage = `url('${identity.avatar}')`;
  141. } else {
  142. avatar.textContent = identity.initials;
  143. avatar.style.backgroundColor = generateColor(identity.name);
  144. }
  145. const content = document.createElement('div');
  146. content.className = 'message-content';
  147. const urlRegex = /(\b(https?|ftp|file):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/ig;
  148. content.innerHTML = msg.text.replace(urlRegex, url => `<a href="${url}" target="_blank">${url}</a>`);
  149. // Set sent/received based on Epstein's ID
  150. bubble.classList.add(msg.sender === EPSTEIN_ID ? 'sent' : 'received');
  151. bubble.appendChild(avatar);
  152. bubble.appendChild(content);
  153. messageContainer.appendChild(bubble);
  154. });
  155. conversationNumberEl.textContent = `Conversation #${convNumber}`;
  156. prevBtn.disabled = state.currentIndex === 0;
  157. nextBtn.disabled = state.currentIndex >= state.totalConversations - 1;
  158. }
  159. function exportConversation() {
  160. const conversationId = state.currentIndex + 1;
  161. html2canvas(messageContainer, {
  162. scrollY: -window.scrollY,
  163. scale: 2
  164. }).then(canvas => {
  165. const link = document.createElement('a');
  166. link.download = `epstein_${conversationId}.png`;
  167. link.href = canvas.toDataURL('image/png');
  168. link.click();
  169. });
  170. }
  171. function handleAvatarClick(event) {
  172. if (event.target.classList.contains('avatar')) {
  173. const senderId = event.target.dataset.senderId;
  174. // Prevent editing Epstein's identity
  175. if (senderId && senderId !== EPSTEIN_ID) {
  176. openIdentityModal(senderId);
  177. }
  178. }
  179. }
  180. function openIdentityModal(senderId) {
  181. const identity = getIdentity(senderId);
  182. modal.style.display = 'block';
  183. modalIdentifierEl.textContent = senderId;
  184. // If the current name is '?', show an empty input
  185. modalNameInput.value = identity.name !== '?' ? identity.name : '';
  186. modalAvatarInput.value = identity.avatar || '';
  187. modalSaveBtn.dataset.senderId = senderId;
  188. }
  189. function saveIdentity() {
  190. const senderId = modalSaveBtn.dataset.senderId;
  191. const name = modalNameInput.value.trim();
  192. const avatar = modalAvatarInput.value.trim();
  193. if (senderId) {
  194. state.identities[senderId] = {
  195. name: name || '?',
  196. avatar: avatar || null,
  197. initials: (name || '?').substring(0, 2).toUpperCase()
  198. };
  199. localStorage.setItem('speakerIdentities', JSON.stringify(state.identities));
  200. modal.style.display = 'none';
  201. loadConversation(state.currentIndex); // Re-render
  202. }
  203. }
  204. init();
  205. });