decant-core 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/ai/chatgpt.js CHANGED
@@ -1,14 +1,16 @@
1
- import { ChatParser } from './base.js';
2
- import { convertToMarkdown } from '../utils/html-to-markdown.js';
1
+ import { ChatParser } from "./base.js";
2
+ import { convertToMarkdown } from "../utils/html-to-markdown.js";
3
3
  import {
4
4
  collectMountedTurnMessages,
5
5
  findChatGPTScrollRoot,
6
6
  getConversationTurns,
7
- } from './chatgpt_scroll_collector.js';
7
+ } from "./chatgpt_scroll_collector.js";
8
8
 
9
9
  function getAccessToken() {
10
10
  try {
11
- const el = typeof document !== 'undefined' && document.getElementById('client-bootstrap');
11
+ const el =
12
+ typeof document !== "undefined" &&
13
+ document.getElementById("client-bootstrap");
12
14
  if (!el) return null;
13
15
  return JSON.parse(el.textContent).session.accessToken;
14
16
  } catch {
@@ -18,8 +20,8 @@ function getAccessToken() {
18
20
 
19
21
  function getConversationId() {
20
22
  try {
21
- if (typeof window === 'undefined' || !window.location) return null;
22
- const path = window.location.pathname || window.location.href || '';
23
+ if (typeof window === "undefined" || !window.location) return null;
24
+ const path = window.location.pathname || window.location.href || "";
23
25
  const match = path.match(/\/(?:c|share|g\/[^/]+\/c)\/([^/?#]+)/);
24
26
  return match ? match[1] : null;
25
27
  } catch {
@@ -30,14 +32,14 @@ function getConversationId() {
30
32
  function fetchConversation(convId, token, includeImages) {
31
33
  return new Promise((resolve, reject) => {
32
34
  const requestId =
33
- typeof crypto !== 'undefined' && crypto.randomUUID
35
+ typeof crypto !== "undefined" && crypto.randomUUID
34
36
  ? crypto.randomUUID()
35
37
  : Math.random().toString(36).substring(2) + Date.now().toString(36);
36
38
 
37
39
  const handler = (event) => {
38
- if (event.data?.source !== 'chatgpt-exporter-page') return;
40
+ if (event.data?.source !== "chatgpt-exporter-page") return;
39
41
  if (event.data?.requestId !== requestId) return;
40
- window.removeEventListener('message', handler);
42
+ window.removeEventListener("message", handler);
41
43
  clearTimeout(timer);
42
44
  if (event.data.error) {
43
45
  reject(new Error(event.data.error));
@@ -47,21 +49,21 @@ function fetchConversation(convId, token, includeImages) {
47
49
  };
48
50
 
49
51
  const timer = setTimeout(() => {
50
- window.removeEventListener('message', handler);
51
- reject(new Error('Request timed out'));
52
+ window.removeEventListener("message", handler);
53
+ reject(new Error("Request timed out"));
52
54
  }, 45000);
53
55
 
54
- window.addEventListener('message', handler);
56
+ window.addEventListener("message", handler);
55
57
  window.postMessage(
56
58
  {
57
- source: 'chatgpt-exporter-ext',
58
- type: 'fetch_conversation',
59
+ source: "chatgpt-exporter-ext",
60
+ type: "fetch_conversation",
59
61
  convId,
60
62
  token,
61
63
  requestId,
62
64
  includeImages,
63
65
  },
64
- 'https://chatgpt.com',
66
+ "https://chatgpt.com",
65
67
  );
66
68
  });
67
69
  }
@@ -84,16 +86,18 @@ function findLeafFromNode(mapping, startNodeId) {
84
86
 
85
87
  function resolveActiveLeafNode(mapping, currentNodeId) {
86
88
  // 1. Try DOM elements first (captures branch if user switched turns in UI)
87
- if (typeof document !== 'undefined' && document.querySelectorAll) {
89
+ if (typeof document !== "undefined" && document.querySelectorAll) {
88
90
  const msgEls = Array.from(
89
91
  document.querySelectorAll(
90
- 'div[data-message-id], [data-message-author-role][data-message-id], section[data-turn-id], article[data-turn-id]',
92
+ "div[data-message-id], [data-message-author-role][data-message-id], section[data-turn-id], article[data-turn-id]",
91
93
  ),
92
94
  );
93
95
  for (let i = msgEls.length - 1; i >= 0; i--) {
94
96
  const el = msgEls[i];
95
97
  const id =
96
- el.dataset?.messageId || el.dataset?.turnId || el.getAttribute?.('data-message-id');
98
+ el.dataset?.messageId ||
99
+ el.dataset?.turnId ||
100
+ el.getAttribute?.("data-message-id");
97
101
  if (id && mapping[id]) {
98
102
  const leaf = findLeafFromNode(mapping, id);
99
103
  if (leaf) return leaf;
@@ -110,21 +114,25 @@ function resolveActiveLeafNode(mapping, currentNodeId) {
110
114
  }
111
115
 
112
116
  export function extractSharedConversationFromDom(
113
- doc = typeof document !== 'undefined' ? document : null,
117
+ doc = typeof document !== "undefined" ? document : null,
114
118
  ) {
115
- if (!doc || typeof doc.querySelectorAll !== 'function') return null;
119
+ if (!doc || typeof doc.querySelectorAll !== "function") return null;
116
120
 
117
121
  function findMappingInObj(obj, seen = new Set()) {
118
- if (!obj || typeof obj !== 'object' || seen.has(obj)) return null;
122
+ if (!obj || typeof obj !== "object" || seen.has(obj)) return null;
119
123
  seen.add(obj);
120
124
 
121
- if (obj.mapping && typeof obj.mapping === 'object' && Object.keys(obj.mapping).length > 0) {
125
+ if (
126
+ obj.mapping &&
127
+ typeof obj.mapping === "object" &&
128
+ Object.keys(obj.mapping).length > 0
129
+ ) {
122
130
  return obj;
123
131
  }
124
132
  if (
125
133
  obj.data &&
126
134
  obj.data.mapping &&
127
- typeof obj.data.mapping === 'object' &&
135
+ typeof obj.data.mapping === "object" &&
128
136
  Object.keys(obj.data.mapping).length > 0
129
137
  ) {
130
138
  return obj.data;
@@ -144,10 +152,14 @@ export function extractSharedConversationFromDom(
144
152
  return null;
145
153
  }
146
154
 
147
- const scripts = Array.from(doc.querySelectorAll('script'));
155
+ const scripts = Array.from(doc.querySelectorAll("script"));
148
156
  for (const script of scripts) {
149
- const text = script.textContent || '';
150
- if (!text || (!text.includes('"mapping"') && !text.includes('current_node'))) continue;
157
+ const text = script.textContent || "";
158
+ if (
159
+ !text ||
160
+ (!text.includes('"mapping"') && !text.includes("current_node"))
161
+ )
162
+ continue;
151
163
 
152
164
  try {
153
165
  const parsed = JSON.parse(text);
@@ -169,7 +181,7 @@ export function extractSharedConversationFromDom(
169
181
  }
170
182
  }
171
183
 
172
- const bootstrapEl = doc.getElementById?.('client-bootstrap');
184
+ const bootstrapEl = doc.getElementById?.("client-bootstrap");
173
185
  if (bootstrapEl) {
174
186
  try {
175
187
  const parsed = JSON.parse(bootstrapEl.textContent);
@@ -197,7 +209,9 @@ export function linearize(mapping, includeImages, currentNodeId) {
197
209
  }
198
210
  path.reverse();
199
211
  } else {
200
- const root = Object.values(mapping).find((n) => !n.parent || !mapping[n.parent]);
212
+ const root = Object.values(mapping).find(
213
+ (n) => !n.parent || !mapping[n.parent],
214
+ );
201
215
  if (!root) return [];
202
216
 
203
217
  const subtreeSize = {};
@@ -206,7 +220,8 @@ export function linearize(mapping, includeImages, currentNodeId) {
206
220
  const node = mapping[id];
207
221
  if (!node) return (subtreeSize[id] = 0);
208
222
  const childSizes = (node.children ?? []).map((cid) => size(cid));
209
- return (subtreeSize[id] = 1 + (childSizes.length ? Math.max(...childSizes) : 0));
223
+ return (subtreeSize[id] =
224
+ 1 + (childSizes.length ? Math.max(...childSizes) : 0));
210
225
  }
211
226
  for (const id of Object.keys(mapping)) size(id);
212
227
 
@@ -215,10 +230,14 @@ export function linearize(mapping, includeImages, currentNodeId) {
215
230
  while (node && !visited.has(node.id)) {
216
231
  visited.add(node.id);
217
232
  path.push(node);
218
- const validChildren = (node.children ?? []).filter((cid) => cid in mapping);
233
+ const validChildren = (node.children ?? []).filter(
234
+ (cid) => cid in mapping,
235
+ );
219
236
  node = validChildren.length
220
237
  ? mapping[
221
- validChildren.reduce((best, cid) => (subtreeSize[cid] > subtreeSize[best] ? cid : best))
238
+ validChildren.reduce((best, cid) =>
239
+ subtreeSize[cid] > subtreeSize[best] ? cid : best,
240
+ )
222
241
  ]
223
242
  : null;
224
243
  }
@@ -234,76 +253,105 @@ export function linearize(mapping, includeImages, currentNodeId) {
234
253
  const role = msg?.author?.role;
235
254
  const authorName = msg?.author?.name;
236
255
  const isThoughtMsg =
237
- authorName === 'thought' ||
238
- msg?.recipient === 'thought' ||
239
- msg?.content?.content_type === 'thought' ||
240
- msg?.content?.content_type === 'thoughts' ||
241
- msg?.metadata?.reasoning_status === 'is_reasoning';
256
+ authorName === "thought" ||
257
+ msg?.recipient === "thought" ||
258
+ msg?.content?.content_type === "thought" ||
259
+ msg?.content?.content_type === "thoughts" ||
260
+ msg?.metadata?.reasoning_status === "is_reasoning";
242
261
 
243
- if (role === 'user' || role === 'assistant' || role === 'tool' || isThoughtMsg) {
262
+ if (
263
+ role === "user" ||
264
+ role === "assistant" ||
265
+ role === "tool" ||
266
+ isThoughtMsg
267
+ ) {
244
268
  const segments = [];
245
269
  const parts = msg?.content?.parts ?? [];
246
270
 
247
271
  for (const part of parts) {
248
- let partText = '';
272
+ let partText = "";
249
273
  let isThoughtPart = isThoughtMsg;
250
274
 
251
- if (typeof part === 'string') {
275
+ if (typeof part === "string") {
252
276
  partText = part;
253
- } else if (part && typeof part === 'object') {
254
- if (part.content_type === 'text' && typeof part.text === 'string') {
277
+ } else if (part && typeof part === "object") {
278
+ if (part.content_type === "text" && typeof part.text === "string") {
255
279
  partText = part.text;
256
- } else if (part.content_type === 'thought' && typeof part.text === 'string') {
280
+ } else if (
281
+ part.content_type === "thought" &&
282
+ typeof part.text === "string"
283
+ ) {
257
284
  partText = part.text;
258
285
  isThoughtPart = true;
259
- } else if (part.content_type === 'audio_transcription' && typeof part.text === 'string') {
286
+ } else if (
287
+ part.content_type === "audio_transcription" &&
288
+ typeof part.text === "string"
289
+ ) {
260
290
  partText = part.text;
261
291
  }
262
292
  }
263
293
 
264
- if (partText && role !== 'tool') {
294
+ if (partText && role !== "tool") {
265
295
  const text = partText
266
- .replace(/\u{E0000}[\u{E0000}-\u{E007F}]*/gu, '')
267
- .replace(/citeturn\d+\w*/g, '')
296
+ .replace(/\u{E0000}[\u{E0000}-\u{E007F}]*/gu, "")
297
+ .replace(/citeturn\d+\w*/g, "")
268
298
  .trim();
269
299
  if (text) {
270
300
  if (isThoughtPart) {
271
- segments.push({ type: 'thought', content: text });
301
+ segments.push({ type: "thought", content: text });
272
302
  } else {
273
- segments.push({ type: 'text', content: text });
303
+ segments.push({ type: "text", content: text });
274
304
  }
275
305
  }
276
306
  } else if (
277
307
  includeImages &&
278
- part?.content_type === 'image_asset_pointer' &&
308
+ part?.content_type === "image_asset_pointer" &&
279
309
  part?.asset_pointer
280
310
  ) {
281
- segments.push({ type: 'image', fileId: part.asset_pointer.split('://')[1] });
311
+ segments.push({
312
+ type: "image",
313
+ fileId: part.asset_pointer.split("://")[1],
314
+ });
282
315
  }
283
316
  }
284
317
 
285
318
  // Handle standalone content.text (e.g. execution_output or plain text)
286
- if (typeof msg.content?.text === 'string' && msg.content.text.trim() && parts.length === 0) {
287
- segments.push({ type: 'text', content: msg.content.text.trim() });
319
+ if (
320
+ typeof msg.content?.text === "string" &&
321
+ msg.content.text.trim() &&
322
+ parts.length === 0
323
+ ) {
324
+ segments.push({ type: "text", content: msg.content.text.trim() });
288
325
  }
289
326
 
290
327
  // Handle o1/o3/o4 reasoning thoughts array: content.thoughts = [{ summary, content }]
291
- if (Array.isArray(msg.content?.thoughts) && msg.content.thoughts.length > 0) {
328
+ if (
329
+ Array.isArray(msg.content?.thoughts) &&
330
+ msg.content.thoughts.length > 0
331
+ ) {
292
332
  const thoughtParts = msg.content.thoughts
293
- .map((t) => (t.summary ? `**${t.summary}**\n${t.content || ''}` : t.content || ''))
333
+ .map((t) =>
334
+ t.summary
335
+ ? `**${t.summary}**\n${t.content || ""}`
336
+ : t.content || "",
337
+ )
294
338
  .filter(Boolean);
295
339
  if (thoughtParts.length > 0) {
296
- segments.push({ type: 'thought', content: thoughtParts.join('\n\n') });
340
+ segments.push({
341
+ type: "thought",
342
+ content: thoughtParts.join("\n\n"),
343
+ });
297
344
  }
298
345
  }
299
346
 
300
347
  // Handle reasoning recap
301
348
  if (
302
- (msg.content?.content_type === 'reasoning_recap' || msg.content?.content) &&
303
- typeof msg.content.content === 'string' &&
349
+ (msg.content?.content_type === "reasoning_recap" ||
350
+ msg.content?.content) &&
351
+ typeof msg.content.content === "string" &&
304
352
  msg.content.content.trim()
305
353
  ) {
306
- segments.push({ type: 'thought', content: msg.content.content.trim() });
354
+ segments.push({ type: "thought", content: msg.content.content.trim() });
307
355
  }
308
356
 
309
357
  // Handle Deep Research reports (widget_state)
@@ -312,14 +360,16 @@ export function linearize(mapping, includeImages, currentNodeId) {
312
360
  msg.metadata?.tool_response_metadata?.venus_widget_state;
313
361
  if (widgetRaw) {
314
362
  try {
315
- const widget = typeof widgetRaw === 'string' ? JSON.parse(widgetRaw) : widgetRaw;
316
- const reportText = widget.report_message?.content?.parts?.[0] || widget.markdown;
363
+ const widget =
364
+ typeof widgetRaw === "string" ? JSON.parse(widgetRaw) : widgetRaw;
365
+ const reportText =
366
+ widget.report_message?.content?.parts?.[0] || widget.markdown;
317
367
  const steering = widget.steering_acknowledgement;
318
- let researchContent = '';
368
+ let researchContent = "";
319
369
  if (steering) researchContent += `${steering}\n\n`;
320
370
  if (reportText) researchContent += reportText;
321
371
  if (researchContent.trim()) {
322
- segments.push({ type: 'text', content: researchContent.trim() });
372
+ segments.push({ type: "text", content: researchContent.trim() });
323
373
  }
324
374
  } catch {
325
375
  // Ignore widget state JSON parse errors
@@ -327,20 +377,33 @@ export function linearize(mapping, includeImages, currentNodeId) {
327
377
  }
328
378
 
329
379
  // Handle attachments
330
- if (Array.isArray(msg.metadata?.attachments) && msg.metadata.attachments.length > 0) {
331
- const fileNames = msg.metadata.attachments.map((att) => att.name).filter(Boolean);
380
+ if (
381
+ Array.isArray(msg.metadata?.attachments) &&
382
+ msg.metadata.attachments.length > 0
383
+ ) {
384
+ const fileNames = msg.metadata.attachments
385
+ .map((att) => att.name)
386
+ .filter(Boolean);
332
387
  if (fileNames.length > 0) {
333
- segments.push({ type: 'text', content: `[Attached: ${fileNames.join(', ')}]` });
388
+ segments.push({
389
+ type: "text",
390
+ content: `[Attached: ${fileNames.join(", ")}]`,
391
+ });
334
392
  }
335
393
  }
336
394
 
337
395
  // Handle Canvas documents
338
396
  if (msg.metadata?.canvas?.title) {
339
- segments.push({ type: 'text', content: `[Canvas: ${msg.metadata.canvas.title}]` });
397
+ segments.push({
398
+ type: "text",
399
+ content: `[Canvas: ${msg.metadata.canvas.title}]`,
400
+ });
340
401
  }
341
402
 
342
- const displayRole = role === 'user' ? 'User' : 'ChatGPT';
343
- const timestamp = msg?.create_time ? new Date(msg.create_time * 1000).toLocaleString() : null;
403
+ const displayRole = role === "user" ? "User" : "ChatGPT";
404
+ const timestamp = msg?.create_time
405
+ ? new Date(msg.create_time * 1000).toLocaleString()
406
+ : null;
344
407
 
345
408
  if (segments.length) {
346
409
  const citeMap = {};
@@ -348,7 +411,10 @@ export function linearize(mapping, includeImages, currentNodeId) {
348
411
  for (const ref of msg?.metadata?.content_references ?? []) {
349
412
  if (ref.matched_text) {
350
413
  if (ref.items?.length) citeMap[ref.matched_text] = ref.items;
351
- if (ref.type === 'image_group' || ref.matched_text.includes('image_group')) {
414
+ if (
415
+ ref.type === "image_group" ||
416
+ ref.matched_text.includes("image_group")
417
+ ) {
352
418
  imageGroupMap[ref.matched_text] = ref;
353
419
  }
354
420
  }
@@ -356,9 +422,9 @@ export function linearize(mapping, includeImages, currentNodeId) {
356
422
 
357
423
  // If previous message is also ChatGPT, merge segments (thoughts in front, content in back)
358
424
  if (
359
- displayRole === 'ChatGPT' &&
425
+ displayRole === "ChatGPT" &&
360
426
  messages.length > 0 &&
361
- messages[messages.length - 1].role === 'ChatGPT'
427
+ messages[messages.length - 1].role === "ChatGPT"
362
428
  ) {
363
429
  const prevMsg = messages[messages.length - 1];
364
430
  if (isThoughtMsg) {
@@ -372,7 +438,13 @@ export function linearize(mapping, includeImages, currentNodeId) {
372
438
  prevMsg.timestamp = timestamp;
373
439
  }
374
440
  } else {
375
- messages.push({ role: displayRole, segments, citeMap, imageGroupMap, timestamp });
441
+ messages.push({
442
+ role: displayRole,
443
+ segments,
444
+ citeMap,
445
+ imageGroupMap,
446
+ timestamp,
447
+ });
376
448
  }
377
449
  }
378
450
  }
@@ -382,12 +454,12 @@ export function linearize(mapping, includeImages, currentNodeId) {
382
454
  }
383
455
 
384
456
  function cleanMarkdownFromApi(text, citeMap, imageGroupMap) {
385
- if (!text) return '';
457
+ if (!text) return "";
386
458
 
387
459
  // 1. Remove specific character ranges (like some PUA ranges)
388
460
  text = text
389
- .replace(/\u{E0000}[\u{E0000}-\u{E007F}]*/gu, '')
390
- .replace(/citeturn\d+\w*/g, '')
461
+ .replace(/\u{E0000}[\u{E0000}-\u{E007F}]*/gu, "")
462
+ .replace(/citeturn\d+\w*/g, "")
391
463
  .trim();
392
464
 
393
465
  // 2. Replace ChatGPT PUA URL annotations: url{label}{href}
@@ -415,87 +487,101 @@ function cleanMarkdownFromApi(text, citeMap, imageGroupMap) {
415
487
  const markdownImgs = ref.images
416
488
  .map((imgObj) => {
417
489
  const res = imgObj.image_result || {};
418
- const title = res.title || imgObj.image_search_query || 'Image';
419
- const src = res.content_url || res.thumbnail_url || res.original_content_url;
490
+ const title = res.title || imgObj.image_search_query || "Image";
491
+ const src =
492
+ res.content_url || res.thumbnail_url || res.original_content_url;
420
493
  if (src) {
421
494
  return `![${title}](${src})`;
422
495
  }
423
- return '';
496
+ return "";
424
497
  })
425
498
  .filter(Boolean);
426
499
  if (markdownImgs.length > 0) {
427
- return '\n\n' + markdownImgs.join('\n\n') + '\n\n';
500
+ return "\n\n" + markdownImgs.join("\n\n") + "\n\n";
428
501
  }
429
502
  }
430
- if (ref.safe_urls && Array.isArray(ref.safe_urls) && ref.safe_urls.length > 0) {
503
+ if (
504
+ ref.safe_urls &&
505
+ Array.isArray(ref.safe_urls) &&
506
+ ref.safe_urls.length > 0
507
+ ) {
431
508
  return (
432
- '\n\n' + ref.safe_urls.map((url, i) => `![Image ${i + 1}](${url})`).join('\n\n') + '\n\n'
509
+ "\n\n" +
510
+ ref.safe_urls
511
+ .map((url, i) => `![Image ${i + 1}](${url})`)
512
+ .join("\n\n") +
513
+ "\n\n"
433
514
  );
434
515
  }
435
516
  if (ref.alt) {
436
- return '\n\n' + ref.alt + '\n\n';
517
+ return "\n\n" + ref.alt + "\n\n";
437
518
  }
438
519
  }
439
- return '';
520
+ return "";
440
521
  });
441
522
 
442
523
  // 4. Replace ChatGPT PUA cite annotations
443
- text = text.replace(/\uE200cite(?:\uE202[^\uE202\uE201]+)+\uE201/g, (match) => {
444
- const items = citeMap?.[match] ?? [];
445
- if (!items.length) return '';
446
- const formatted = items.map((item) => {
447
- const label = item.attribution || item.title || 'Source';
448
- return `[${label}](${item.url})`;
449
- });
450
- return ` (${formatted.join(', ')})`;
451
- });
524
+ text = text.replace(
525
+ /\uE200cite(?:\uE202[^\uE202\uE201]+)+\uE201/g,
526
+ (match) => {
527
+ const items = citeMap?.[match] ?? [];
528
+ if (!items.length) return "";
529
+ const formatted = items.map((item) => {
530
+ const label = item.attribution || item.title || "Source";
531
+ return `[${label}](${item.url})`;
532
+ });
533
+ return ` (${formatted.join(", ")})`;
534
+ },
535
+ );
452
536
 
453
537
  return text;
454
538
  }
455
539
 
456
540
  export class ChatGPTParser extends ChatParser {
457
- name = 'ChatGPT';
541
+ name = "ChatGPT";
458
542
  constructor() {
459
543
  super();
460
544
  this.lastFetch = null;
461
545
  }
462
546
 
463
547
  isAvailable(url) {
464
- return url.includes('chatgpt.com');
548
+ return url.includes("chatgpt.com");
465
549
  }
466
550
 
467
551
  getRoleElement(container) {
468
- if (container.matches?.('[data-message-author-role]')) return container;
469
- return container.querySelector?.('[data-message-author-role]') || null;
552
+ if (container.matches?.("[data-message-author-role]")) return container;
553
+ return container.querySelector?.("[data-message-author-role]") || null;
470
554
  }
471
555
 
472
556
  getRoleElements(container) {
473
- if (container.matches?.('[data-message-author-role]')) return [container];
474
- return Array.from(container.querySelectorAll?.('[data-message-author-role]') || []);
557
+ if (container.matches?.("[data-message-author-role]")) return [container];
558
+ return Array.from(
559
+ container.querySelectorAll?.("[data-message-author-role]") || [],
560
+ );
475
561
  }
476
562
 
477
563
  getMessageRole(container, roleElement) {
478
- const roleAttr = roleElement?.getAttribute('data-message-author-role');
479
- if (roleAttr) return roleAttr === 'user' ? 'User' : 'ChatGPT';
564
+ const roleAttr = roleElement?.getAttribute("data-message-author-role");
565
+ if (roleAttr) return roleAttr === "user" ? "User" : "ChatGPT";
480
566
 
481
- const text = container.innerText || '';
482
- if (text.startsWith('You\n') || text.includes('\nYou\n')) return 'User';
567
+ const text = container.innerText || "";
568
+ if (text.startsWith("You\n") || text.includes("\nYou\n")) return "User";
483
569
 
484
- return 'ChatGPT';
570
+ return "ChatGPT";
485
571
  }
486
572
 
487
573
  getContentElement(container, roleElement) {
488
- if (roleElement?.getAttribute('data-message-author-role') === 'user') {
574
+ if (roleElement?.getAttribute("data-message-author-role") === "user") {
489
575
  return roleElement;
490
576
  }
491
577
 
492
- const selectors = ['.markdown', '.prose', '.whitespace-pre-wrap'];
578
+ const selectors = [".markdown", ".prose", ".whitespace-pre-wrap"];
493
579
  for (const selector of selectors) {
494
580
  const contentElement = container.querySelector?.(selector);
495
581
  if (contentElement) return contentElement;
496
582
  }
497
583
 
498
- return roleElement || (container.matches?.('article') ? container : null);
584
+ return roleElement || (container.matches?.("article") ? container : null);
499
585
  }
500
586
 
501
587
  getContentElements(container, roleElements) {
@@ -514,26 +600,27 @@ export class ChatGPTParser extends ChatParser {
514
600
 
515
601
  cleanContent(content) {
516
602
  return content
517
- .replace(/^Show moreShow less$/gm, '')
518
- .replace(/\n{3,}/g, '\n\n')
603
+ .replace(/^Show moreShow less$/gm, "")
604
+ .replace(/\n{3,}/g, "\n\n")
519
605
  .trim();
520
606
  }
521
607
 
522
608
  getMessageKey(container, roleElement, role, content) {
523
609
  const idElement =
524
- roleElement?.closest?.('[data-message-id]') || container.querySelector?.('[data-message-id]');
525
- const messageId = idElement?.getAttribute('data-message-id');
610
+ roleElement?.closest?.("[data-message-id]") ||
611
+ container.querySelector?.("[data-message-id]");
612
+ const messageId = idElement?.getAttribute("data-message-id");
526
613
  if (messageId) return messageId;
527
614
 
528
- const turnId = container.getAttribute?.('data-testid');
615
+ const turnId = container.getAttribute?.("data-testid");
529
616
  if (turnId) return `${turnId}:${role}`;
530
617
 
531
- return `${role}:${content.replace(/\s+/g, ' ').trim()}`;
618
+ return `${role}:${content.replace(/\s+/g, " ").trim()}`;
532
619
  }
533
620
 
534
621
  extractAttachments(container) {
535
622
  const attachments = [];
536
- const rawContent = container.textContent || container.innerText || '';
623
+ const rawContent = container.textContent || container.innerText || "";
537
624
  const filePatterns = [
538
625
  /([a-zA-Z0-9_-]+\.tex)/g,
539
626
  /([a-zA-Z0-9_-]+\.txt)/g,
@@ -549,16 +636,18 @@ export class ChatGPTParser extends ChatParser {
549
636
  });
550
637
 
551
638
  foundFiles.forEach((fileName) => {
552
- const fileExt = fileName.substring(fileName.lastIndexOf('.') + 1).toLowerCase();
639
+ const fileExt = fileName
640
+ .substring(fileName.lastIndexOf(".") + 1)
641
+ .toLowerCase();
553
642
  const typeMap = {
554
- tex: 'LaTeX',
555
- txt: 'Text',
556
- md: 'Markdown',
557
- pdf: 'PDF',
558
- doc: 'Document',
559
- docx: 'Document',
643
+ tex: "LaTeX",
644
+ txt: "Text",
645
+ md: "Markdown",
646
+ pdf: "PDF",
647
+ doc: "Document",
648
+ docx: "Document",
560
649
  };
561
- attachments.push({ name: fileName, type: typeMap[fileExt] || 'File' });
650
+ attachments.push({ name: fileName, type: typeMap[fileExt] || "File" });
562
651
  });
563
652
 
564
653
  return attachments;
@@ -568,15 +657,15 @@ export class ChatGPTParser extends ChatParser {
568
657
  const seenSrcs = new Set();
569
658
  const capturedImages = [];
570
659
 
571
- container.querySelectorAll?.('img').forEach((img) => {
572
- const src = img.getAttribute('src');
573
- const alt = img.getAttribute('alt') || 'Image';
660
+ container.querySelectorAll?.("img").forEach((img) => {
661
+ const src = img.getAttribute("src");
662
+ const alt = img.getAttribute("alt") || "Image";
574
663
  const isContentImage =
575
- src?.includes('backend-api') ||
576
- src?.includes('files') ||
577
- src?.startsWith('blob:') ||
578
- alt.includes('Uploaded') ||
579
- alt.includes('Generated');
664
+ src?.includes("backend-api") ||
665
+ src?.includes("files") ||
666
+ src?.startsWith("blob:") ||
667
+ alt.includes("Uploaded") ||
668
+ alt.includes("Generated");
580
669
 
581
670
  if (src && !seenSrcs.has(src) && isContentImage) {
582
671
  seenSrcs.add(src);
@@ -599,17 +688,17 @@ export class ChatGPTParser extends ChatParser {
599
688
  Object.entries(groupedAttachments).forEach(([type, files]) => {
600
689
  attachmentLines.push(`**${type} Files:**`);
601
690
  files.forEach((file) => attachmentLines.push(`- ${file}`));
602
- attachmentLines.push('');
691
+ attachmentLines.push("");
603
692
  });
604
693
 
605
694
  if (capturedImages.length > 0) {
606
- if (attachmentLines.length > 0) attachmentLines.push('');
607
- attachmentLines.push('**Images:**');
695
+ if (attachmentLines.length > 0) attachmentLines.push("");
696
+ attachmentLines.push("**Images:**");
608
697
  capturedImages.forEach((image) => attachmentLines.push(`- ${image}`));
609
698
  }
610
699
 
611
700
  if (attachmentLines.length === 0) return content;
612
- return `${content}\n\n**Attachments & Images:**\n${attachmentLines.join('\n')}`;
701
+ return `${content}\n\n**Attachments & Images:**\n${attachmentLines.join("\n")}`;
613
702
  }
614
703
 
615
704
  convertContentElement(contentElement) {
@@ -623,30 +712,41 @@ export class ChatGPTParser extends ChatParser {
623
712
  if (contentElements.length === 0) return null;
624
713
 
625
714
  const role = this.getMessageRole(container, roleElement);
626
- const noiseSelectors = ['.flex.gap-2', 'button', '.sr-only', '[role="button"]'];
715
+ const noiseSelectors = [
716
+ ".flex.gap-2",
717
+ "button",
718
+ ".sr-only",
719
+ '[role="button"]',
720
+ ];
627
721
  const contentParts = contentElements
628
722
  .map((contentElement) => {
629
723
  const clone = contentElement.cloneNode(true);
630
- clone.querySelectorAll('button').forEach((button) => {
631
- const img = button.querySelector('img');
724
+ clone.querySelectorAll("button").forEach((button) => {
725
+ const img = button.querySelector("img");
632
726
  if (!img) return;
633
727
 
634
- let caption = 'Image';
635
- const ariaLabel = button.getAttribute('aria-label') || '';
636
- if (ariaLabel.toLowerCase().includes('open image details for')) {
637
- caption = ariaLabel.replace(/^Open image details for\s*/i, '').trim();
638
- } else if (img.getAttribute('alt') && !img.getAttribute('alt').startsWith('http')) {
639
- caption = img.getAttribute('alt').trim();
728
+ let caption = "Image";
729
+ const ariaLabel = button.getAttribute("aria-label") || "";
730
+ if (ariaLabel.toLowerCase().includes("open image details for")) {
731
+ caption = ariaLabel
732
+ .replace(/^Open image details for\s*/i, "")
733
+ .trim();
734
+ } else if (
735
+ img.getAttribute("alt") &&
736
+ !img.getAttribute("alt").startsWith("http")
737
+ ) {
738
+ caption = img.getAttribute("alt").trim();
640
739
  }
641
740
 
642
- const alt = img.getAttribute('alt') || '';
643
- const src = img.getAttribute('src') || '';
644
- const imageUrl = alt.startsWith('http://') || alt.startsWith('https://') ? alt : src;
741
+ const alt = img.getAttribute("alt") || "";
742
+ const src = img.getAttribute("src") || "";
743
+ const imageUrl =
744
+ alt.startsWith("http://") || alt.startsWith("https://") ? alt : src;
645
745
 
646
746
  if (imageUrl) {
647
- const newImg = clone.ownerDocument.createElement('img');
648
- newImg.setAttribute('src', imageUrl);
649
- newImg.setAttribute('alt', caption);
747
+ const newImg = clone.ownerDocument.createElement("img");
748
+ newImg.setAttribute("src", imageUrl);
749
+ newImg.setAttribute("alt", caption);
650
750
  button.parentNode.replaceChild(newImg, button);
651
751
  }
652
752
  });
@@ -658,7 +758,7 @@ export class ChatGPTParser extends ChatParser {
658
758
  })
659
759
  .filter(Boolean);
660
760
 
661
- let content = contentParts.join('\n\n');
761
+ let content = contentParts.join("\n\n");
662
762
  content = this.appendAttachments(
663
763
  content,
664
764
  this.extractAttachments(container),
@@ -675,11 +775,11 @@ export class ChatGPTParser extends ChatParser {
675
775
  }
676
776
 
677
777
  extractMountedMessages() {
678
- const articles = Array.from(document.querySelectorAll('article'));
778
+ const articles = Array.from(document.querySelectorAll("article"));
679
779
  const containers =
680
780
  articles.length > 0
681
781
  ? articles
682
- : Array.from(document.querySelectorAll('[data-message-author-role]'));
782
+ : Array.from(document.querySelectorAll("[data-message-author-role]"));
683
783
 
684
784
  return containers
685
785
  .map((container) => this.extractMessage(container))
@@ -701,16 +801,22 @@ export class ChatGPTParser extends ChatParser {
701
801
  formatApiResult(convoData, apiMessages, fallbackTitle, images = {}) {
702
802
  const messages = [];
703
803
  for (const msg of apiMessages) {
704
- let content = '';
804
+ let content = "";
705
805
  for (const seg of msg.segments) {
706
- if (seg.type === 'text') {
707
- content += cleanMarkdownFromApi(seg.content, msg.citeMap, msg.imageGroupMap) + '\n\n';
708
- } else if (seg.type === 'thought') {
709
- const thoughtText = cleanMarkdownFromApi(seg.content, msg.citeMap, msg.imageGroupMap);
806
+ if (seg.type === "text") {
807
+ content +=
808
+ cleanMarkdownFromApi(seg.content, msg.citeMap, msg.imageGroupMap) +
809
+ "\n\n";
810
+ } else if (seg.type === "thought") {
811
+ const thoughtText = cleanMarkdownFromApi(
812
+ seg.content,
813
+ msg.citeMap,
814
+ msg.imageGroupMap,
815
+ );
710
816
  if (thoughtText) {
711
817
  content += `<details><summary>Thought Process</summary>\n\n${thoughtText}\n\n</details>\n\n`;
712
818
  }
713
- } else if (seg.type === 'image') {
819
+ } else if (seg.type === "image") {
714
820
  const src = images[seg.fileId];
715
821
  if (src) {
716
822
  content += `![Image](${src})\n\n`;
@@ -731,48 +837,55 @@ export class ChatGPTParser extends ChatParser {
731
837
  }
732
838
 
733
839
  const currentUrl =
734
- typeof window !== 'undefined' && window.location ? window.location.href || '' : '';
840
+ typeof window !== "undefined" && window.location
841
+ ? window.location.href || ""
842
+ : "";
735
843
  const convTitle = convoData?.title || fallbackTitle;
736
844
  const metadata = {
737
- Source: 'ChatGPT',
845
+ Source: "ChatGPT",
738
846
  Date: new Date().toLocaleString(),
739
847
  Link: currentUrl,
740
848
  Model:
741
849
  convoData?.model_slug ||
742
- document.querySelector('[data-testid="model-selector-dropdown"]')?.innerText ||
743
- 'ChatGPT',
850
+ document.querySelector('[data-testid="model-selector-dropdown"]')
851
+ ?.innerText ||
852
+ "ChatGPT",
744
853
  };
745
854
 
746
855
  return { title: convTitle, messages, url: currentUrl, metadata };
747
856
  }
748
857
 
749
858
  async parse(options = {}) {
750
- const title = document.title || 'ChatGPT Session';
859
+ const title = document.title || "ChatGPT Session";
751
860
  const messages = [];
752
861
 
753
862
  const token = getAccessToken();
754
863
  const convId = getConversationId();
755
- const parserMode = options.parserMode || 'auto';
864
+ const parserMode = options.parserMode || "auto";
756
865
  const includeImages = options.includeImages !== false;
757
866
 
758
867
  // 1. If on shared chat URL (/share/...) or SSR conversation data exists in DOM, try SSR data first
759
868
  const isShareUrl =
760
- typeof window !== 'undefined' &&
869
+ typeof window !== "undefined" &&
761
870
  window.location &&
762
- (window.location.pathname || '').startsWith('/share/');
871
+ (window.location.pathname || "").startsWith("/share/");
763
872
  const sharedData = extractSharedConversationFromDom(
764
- typeof document !== 'undefined' ? document : null,
873
+ typeof document !== "undefined" ? document : null,
765
874
  );
766
875
 
767
- if (isShareUrl && sharedData?.mapping && parserMode !== 'prefer_dom') {
768
- const apiMessages = linearize(sharedData.mapping, includeImages, sharedData.current_node);
876
+ if (isShareUrl && sharedData?.mapping && parserMode !== "prefer_dom") {
877
+ const apiMessages = linearize(
878
+ sharedData.mapping,
879
+ includeImages,
880
+ sharedData.current_node,
881
+ );
769
882
  if (apiMessages.length > 0) {
770
883
  return this.formatApiResult(sharedData, apiMessages, title);
771
884
  }
772
885
  }
773
886
 
774
887
  // 2. Try fetching from ChatGPT backend API
775
- if (token && convId && parserMode !== 'prefer_dom') {
888
+ if (token && convId && parserMode !== "prefer_dom") {
776
889
  try {
777
890
  const now = Date.now();
778
891
  let result;
@@ -785,10 +898,10 @@ export class ChatGPTParser extends ChatParser {
785
898
  ) {
786
899
  result = this.lastFetch.result;
787
900
  } else {
788
- if (!document.getElementById('ai-export-chatgpt-helper')) {
789
- const script = document.createElement('script');
790
- script.src = chrome.runtime.getURL('content/chatgpt_helper.js');
791
- script.id = 'ai-export-chatgpt-helper';
901
+ if (!document.getElementById("ai-export-chatgpt-helper")) {
902
+ const script = document.createElement("script");
903
+ script.src = chrome.runtime.getURL("content/chatgpt_helper.js");
904
+ script.id = "ai-export-chatgpt-helper";
792
905
  script.onload = function () {
793
906
  this.remove();
794
907
  };
@@ -805,49 +918,69 @@ export class ChatGPTParser extends ChatParser {
805
918
  };
806
919
  }
807
920
 
808
- const apiMessages = linearize(result.data.mapping, includeImages, result.data.current_node);
921
+ const apiMessages = linearize(
922
+ result.data.mapping,
923
+ includeImages,
924
+ result.data.current_node,
925
+ );
809
926
  if (apiMessages.length > 0) {
810
- return this.formatApiResult(result.data, apiMessages, title, result.images);
927
+ return this.formatApiResult(
928
+ result.data,
929
+ apiMessages,
930
+ title,
931
+ result.images,
932
+ );
811
933
  }
812
934
  } catch (e) {
813
- console.error('[AI Exporter] API parse failed, falling back to SSR/DOM:', e);
935
+ console.error(
936
+ "[AI Exporter] API parse failed, falling back to SSR/DOM:",
937
+ e,
938
+ );
814
939
  }
815
940
  }
816
941
 
817
942
  // 3. If API failed or was not available, check if SSR shared/embedded conversation data exists
818
- if (sharedData?.mapping && parserMode !== 'prefer_dom') {
819
- const apiMessages = linearize(sharedData.mapping, includeImages, sharedData.current_node);
943
+ if (sharedData?.mapping && parserMode !== "prefer_dom") {
944
+ const apiMessages = linearize(
945
+ sharedData.mapping,
946
+ includeImages,
947
+ sharedData.current_node,
948
+ );
820
949
  if (apiMessages.length > 0) {
821
950
  return this.formatApiResult(sharedData, apiMessages, title);
822
951
  }
823
952
  }
824
953
 
825
954
  // Check if we have iframe-based content (deep research feature)
826
- const iframes = document.querySelectorAll('iframe[src*="oaiusercontent.com"]');
955
+ const iframes = document.querySelectorAll(
956
+ 'iframe[src*="oaiusercontent.com"]',
957
+ );
827
958
  if (iframes.length > 0) {
828
- console.log('Detected iframe-based content, attempting extraction...');
959
+ console.log("Detected iframe-based content, attempting extraction...");
829
960
 
830
961
  // Try multiple strategies to extract content
831
- let extractedContent = '';
962
+ let extractedContent = "";
832
963
 
833
964
  // Strategy 1: Look for data in script tags or window objects
834
965
  try {
835
966
  // Check if any conversation data is exposed globally
836
967
  if (window.conversationData || window.chatData) {
837
- extractedContent = JSON.stringify(window.conversationData || window.chatData);
968
+ extractedContent = JSON.stringify(
969
+ window.conversationData || window.chatData,
970
+ );
838
971
  }
839
972
  } catch (e) {
840
- console.log('Global data access failed:', e);
973
+ console.log("Global data access failed:", e);
841
974
  }
842
975
 
843
976
  // Strategy 2: Look for preloaded content in hidden elements
844
977
  if (!extractedContent) {
845
978
  const hiddenSelectors = [
846
- '[data-conversation]',
847
- '[data-messages]',
848
- '.conversation-data',
849
- '.chat-transcript',
850
- 'pre[data-conversation]',
979
+ "[data-conversation]",
980
+ "[data-messages]",
981
+ ".conversation-data",
982
+ ".chat-transcript",
983
+ "pre[data-conversation]",
851
984
  ];
852
985
 
853
986
  for (const selector of hiddenSelectors) {
@@ -862,34 +995,34 @@ export class ChatGPTParser extends ChatParser {
862
995
  // Strategy 3: Enhanced text extraction from main content
863
996
  if (!extractedContent) {
864
997
  const mainContent =
865
- document.querySelector('main') ||
998
+ document.querySelector("main") ||
866
999
  document.querySelector('[role="main"]') ||
867
- document.querySelector('.conversation') ||
1000
+ document.querySelector(".conversation") ||
868
1001
  document.body;
869
1002
 
870
1003
  if (mainContent) {
871
1004
  const textContent = mainContent.textContent || mainContent.innerText;
872
1005
  if (textContent && textContent.trim()) {
873
- const lines = textContent.split('\n').filter((line) => line.trim());
1006
+ const lines = textContent.split("\n").filter((line) => line.trim());
874
1007
 
875
1008
  // Look for conversation patterns
876
1009
  const conversationLines = lines.filter(
877
1010
  (line) =>
878
1011
  line.length > 20 && // Substantial content
879
- !line.includes('ChatGPT') &&
880
- !line.includes('Regenerate') &&
881
- !line.includes('Copy code') &&
882
- !line.includes('Continue') &&
883
- !line.includes('Share') &&
884
- !line.includes('Thumb') &&
885
- !line.includes('New chat') &&
886
- !line.includes('Menu') &&
887
- !line.includes('Settings') &&
888
- !line.includes('History'),
1012
+ !line.includes("ChatGPT") &&
1013
+ !line.includes("Regenerate") &&
1014
+ !line.includes("Copy code") &&
1015
+ !line.includes("Continue") &&
1016
+ !line.includes("Share") &&
1017
+ !line.includes("Thumb") &&
1018
+ !line.includes("New chat") &&
1019
+ !line.includes("Menu") &&
1020
+ !line.includes("Settings") &&
1021
+ !line.includes("History"),
889
1022
  );
890
1023
 
891
1024
  if (conversationLines.length > 0) {
892
- extractedContent = conversationLines.join('\n\n');
1025
+ extractedContent = conversationLines.join("\n\n");
893
1026
  }
894
1027
  }
895
1028
  }
@@ -906,24 +1039,26 @@ export class ChatGPTParser extends ChatParser {
906
1039
  // If we found content, try to structure it
907
1040
  if (extractedContent) {
908
1041
  // Try to identify user vs assistant messages
909
- const lines = extractedContent.split('\n').filter((line) => line.trim());
1042
+ const lines = extractedContent
1043
+ .split("\n")
1044
+ .filter((line) => line.trim());
910
1045
 
911
1046
  lines.forEach((line) => {
912
1047
  if (line.length > 10) {
913
1048
  // Simple heuristic: shorter lines are often user prompts
914
1049
  if (
915
1050
  line.length < 200 ||
916
- line.includes('?') ||
917
- line.includes('write') ||
918
- line.includes('tell')
1051
+ line.includes("?") ||
1052
+ line.includes("write") ||
1053
+ line.includes("tell")
919
1054
  ) {
920
1055
  messages.push({
921
- role: 'User',
1056
+ role: "User",
922
1057
  content: line.trim(),
923
1058
  });
924
1059
  } else {
925
1060
  messages.push({
926
- role: 'ChatGPT',
1061
+ role: "ChatGPT",
927
1062
  content: line.trim(),
928
1063
  });
929
1064
  }
@@ -934,16 +1069,16 @@ export class ChatGPTParser extends ChatParser {
934
1069
  // Add note about extraction method
935
1070
  if (messages.length > 0) {
936
1071
  messages.push({
937
- role: 'ChatGPT',
1072
+ role: "ChatGPT",
938
1073
  content:
939
- '*Note: Content extracted from iframe-based ChatGPT interface. Some formatting may be lost.*',
1074
+ "*Note: Content extracted from iframe-based ChatGPT interface. Some formatting may be lost.*",
940
1075
  });
941
1076
  } else {
942
1077
  // Last resort - add a message explaining the limitation
943
1078
  messages.push({
944
- role: 'ChatGPT',
1079
+ role: "ChatGPT",
945
1080
  content:
946
- '*Note: ChatGPT is using iframe-based content that cannot be accessed by browser extensions. Please try exporting from a standard ChatGPT conversation.*',
1081
+ "*Note: ChatGPT is using iframe-based content that cannot be accessed by browser extensions. Please try exporting from a standard ChatGPT conversation.*",
947
1082
  });
948
1083
  }
949
1084
 
@@ -955,17 +1090,22 @@ export class ChatGPTParser extends ChatParser {
955
1090
  ? await this.extractAllConversationTurns()
956
1091
  : this.extractMountedMessages();
957
1092
  messages.push(
958
- ...(extractedMessages.length > 0 ? extractedMessages : this.extractMountedMessages()),
1093
+ ...(extractedMessages.length > 0
1094
+ ? extractedMessages
1095
+ : this.extractMountedMessages()),
959
1096
  );
960
1097
 
961
1098
  const currentUrl =
962
- typeof window !== 'undefined' && window.location ? window.location.href || '' : '';
1099
+ typeof window !== "undefined" && window.location
1100
+ ? window.location.href || ""
1101
+ : "";
963
1102
  const metadata = {
964
- Source: 'ChatGPT',
1103
+ Source: "ChatGPT",
965
1104
  Date: new Date().toLocaleString(),
966
1105
  Link: currentUrl,
967
1106
  Model:
968
- document.querySelector('[data-testid="model-selector-dropdown"]')?.innerText || 'ChatGPT',
1107
+ document.querySelector('[data-testid="model-selector-dropdown"]')
1108
+ ?.innerText || "ChatGPT",
969
1109
  };
970
1110
 
971
1111
  return { title, messages, url: currentUrl, metadata };