decant-core 1.0.0
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/LICENSE +661 -0
- package/README.md +116 -0
- package/ai/base.js +26 -0
- package/ai/chatgpt.js +973 -0
- package/ai/chatgpt_helper.js +137 -0
- package/ai/chatgpt_scroll_collector.js +193 -0
- package/ai/claude.js +410 -0
- package/ai/claude_react_reader.js +59 -0
- package/ai/copilot.js +413 -0
- package/ai/deepseek.js +155 -0
- package/ai/gemini.js +856 -0
- package/ai/gemini_cloud_assist.js +91 -0
- package/ai/google_ai_studio.js +116 -0
- package/ai/google_search_ai.js +114 -0
- package/ai/index.js +34 -0
- package/ai/lumo.js +101 -0
- package/ai/meta.js +110 -0
- package/ai/mistral.js +57 -0
- package/ai/notebooklm.js +140 -0
- package/ai/perplexity.js +85 -0
- package/ai/qwen.js +101 -0
- package/ai/z_ai.js +101 -0
- package/detection/detect-platform.js +100 -0
- package/detection/domains.js +45 -0
- package/lib/turndown.js +803 -0
- package/package.json +56 -0
- package/utils/html-to-markdown.js +374 -0
package/ai/chatgpt.js
ADDED
|
@@ -0,0 +1,973 @@
|
|
|
1
|
+
import { ChatParser } from './base.js';
|
|
2
|
+
import { convertToMarkdown } from '../utils/html-to-markdown.js';
|
|
3
|
+
import {
|
|
4
|
+
collectMountedTurnMessages,
|
|
5
|
+
findChatGPTScrollRoot,
|
|
6
|
+
getConversationTurns,
|
|
7
|
+
} from './chatgpt_scroll_collector.js';
|
|
8
|
+
|
|
9
|
+
function getAccessToken() {
|
|
10
|
+
try {
|
|
11
|
+
const el = typeof document !== 'undefined' && document.getElementById('client-bootstrap');
|
|
12
|
+
if (!el) return null;
|
|
13
|
+
return JSON.parse(el.textContent).session.accessToken;
|
|
14
|
+
} catch {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function getConversationId() {
|
|
20
|
+
try {
|
|
21
|
+
if (typeof window === 'undefined' || !window.location) return null;
|
|
22
|
+
const path = window.location.pathname || window.location.href || '';
|
|
23
|
+
const match = path.match(/\/(?:c|share|g\/[^/]+\/c)\/([^/?#]+)/);
|
|
24
|
+
return match ? match[1] : null;
|
|
25
|
+
} catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function fetchConversation(convId, token, includeImages) {
|
|
31
|
+
return new Promise((resolve, reject) => {
|
|
32
|
+
const requestId =
|
|
33
|
+
typeof crypto !== 'undefined' && crypto.randomUUID
|
|
34
|
+
? crypto.randomUUID()
|
|
35
|
+
: Math.random().toString(36).substring(2) + Date.now().toString(36);
|
|
36
|
+
|
|
37
|
+
const handler = (event) => {
|
|
38
|
+
if (event.data?.source !== 'chatgpt-exporter-page') return;
|
|
39
|
+
if (event.data?.requestId !== requestId) return;
|
|
40
|
+
window.removeEventListener('message', handler);
|
|
41
|
+
clearTimeout(timer);
|
|
42
|
+
if (event.data.error) {
|
|
43
|
+
reject(new Error(event.data.error));
|
|
44
|
+
} else {
|
|
45
|
+
resolve({ data: event.data.data, images: event.data.images ?? {} });
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const timer = setTimeout(() => {
|
|
50
|
+
window.removeEventListener('message', handler);
|
|
51
|
+
reject(new Error('Request timed out'));
|
|
52
|
+
}, 45000);
|
|
53
|
+
|
|
54
|
+
window.addEventListener('message', handler);
|
|
55
|
+
window.postMessage(
|
|
56
|
+
{
|
|
57
|
+
source: 'chatgpt-exporter-ext',
|
|
58
|
+
type: 'fetch_conversation',
|
|
59
|
+
convId,
|
|
60
|
+
token,
|
|
61
|
+
requestId,
|
|
62
|
+
includeImages,
|
|
63
|
+
},
|
|
64
|
+
'https://chatgpt.com',
|
|
65
|
+
);
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function findLeafFromNode(mapping, startNodeId) {
|
|
70
|
+
let nodeId = startNodeId;
|
|
71
|
+
if (!nodeId || !mapping[nodeId]) return null;
|
|
72
|
+
let node = mapping[nodeId];
|
|
73
|
+
const visited = new Set();
|
|
74
|
+
while (node?.children?.length) {
|
|
75
|
+
if (visited.has(nodeId)) break;
|
|
76
|
+
visited.add(nodeId);
|
|
77
|
+
const lastChildId = node.children[node.children.length - 1];
|
|
78
|
+
if (!mapping[lastChildId]) break;
|
|
79
|
+
node = mapping[lastChildId];
|
|
80
|
+
nodeId = lastChildId;
|
|
81
|
+
}
|
|
82
|
+
return nodeId;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function resolveActiveLeafNode(mapping, currentNodeId) {
|
|
86
|
+
// 1. Try DOM elements first (captures branch if user switched turns in UI)
|
|
87
|
+
if (typeof document !== 'undefined' && document.querySelectorAll) {
|
|
88
|
+
const msgEls = Array.from(
|
|
89
|
+
document.querySelectorAll(
|
|
90
|
+
'div[data-message-id], [data-message-author-role][data-message-id], section[data-turn-id], article[data-turn-id]',
|
|
91
|
+
),
|
|
92
|
+
);
|
|
93
|
+
for (let i = msgEls.length - 1; i >= 0; i--) {
|
|
94
|
+
const el = msgEls[i];
|
|
95
|
+
const id =
|
|
96
|
+
el.dataset?.messageId || el.dataset?.turnId || el.getAttribute?.('data-message-id');
|
|
97
|
+
if (id && mapping[id]) {
|
|
98
|
+
const leaf = findLeafFromNode(mapping, id);
|
|
99
|
+
if (leaf) return leaf;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// 2. Fall back to currentNodeId from API
|
|
105
|
+
if (currentNodeId && mapping[currentNodeId]) {
|
|
106
|
+
return findLeafFromNode(mapping, currentNodeId);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function extractSharedConversationFromDom(
|
|
113
|
+
doc = typeof document !== 'undefined' ? document : null,
|
|
114
|
+
) {
|
|
115
|
+
if (!doc || typeof doc.querySelectorAll !== 'function') return null;
|
|
116
|
+
|
|
117
|
+
function findMappingInObj(obj, seen = new Set()) {
|
|
118
|
+
if (!obj || typeof obj !== 'object' || seen.has(obj)) return null;
|
|
119
|
+
seen.add(obj);
|
|
120
|
+
|
|
121
|
+
if (obj.mapping && typeof obj.mapping === 'object' && Object.keys(obj.mapping).length > 0) {
|
|
122
|
+
return obj;
|
|
123
|
+
}
|
|
124
|
+
if (
|
|
125
|
+
obj.data &&
|
|
126
|
+
obj.data.mapping &&
|
|
127
|
+
typeof obj.data.mapping === 'object' &&
|
|
128
|
+
Object.keys(obj.data.mapping).length > 0
|
|
129
|
+
) {
|
|
130
|
+
return obj.data;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (Array.isArray(obj)) {
|
|
134
|
+
for (const item of obj) {
|
|
135
|
+
const found = findMappingInObj(item, seen);
|
|
136
|
+
if (found) return found;
|
|
137
|
+
}
|
|
138
|
+
} else {
|
|
139
|
+
for (const val of Object.values(obj)) {
|
|
140
|
+
const found = findMappingInObj(val, seen);
|
|
141
|
+
if (found) return found;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const scripts = Array.from(doc.querySelectorAll('script'));
|
|
148
|
+
for (const script of scripts) {
|
|
149
|
+
const text = script.textContent || '';
|
|
150
|
+
if (!text || (!text.includes('"mapping"') && !text.includes('current_node'))) continue;
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
const parsed = JSON.parse(text);
|
|
154
|
+
const convo = findMappingInObj(parsed);
|
|
155
|
+
if (convo) return convo;
|
|
156
|
+
} catch {
|
|
157
|
+
const matches = text.match(/\{[\s\S]*"mapping"[\s\S]*\}/g);
|
|
158
|
+
if (matches) {
|
|
159
|
+
for (const match of matches) {
|
|
160
|
+
try {
|
|
161
|
+
const parsed = JSON.parse(match);
|
|
162
|
+
const convo = findMappingInObj(parsed);
|
|
163
|
+
if (convo) return convo;
|
|
164
|
+
} catch {
|
|
165
|
+
// Ignore JSON parse errors in script segments
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const bootstrapEl = doc.getElementById?.('client-bootstrap');
|
|
173
|
+
if (bootstrapEl) {
|
|
174
|
+
try {
|
|
175
|
+
const parsed = JSON.parse(bootstrapEl.textContent);
|
|
176
|
+
const convo = findMappingInObj(parsed);
|
|
177
|
+
if (convo) return convo;
|
|
178
|
+
} catch {
|
|
179
|
+
// Ignore client-bootstrap JSON parse errors
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function linearize(mapping, includeImages, currentNodeId) {
|
|
187
|
+
let path = [];
|
|
188
|
+
const leafId = resolveActiveLeafNode(mapping, currentNodeId);
|
|
189
|
+
|
|
190
|
+
if (leafId) {
|
|
191
|
+
let id = leafId;
|
|
192
|
+
const visited = new Set();
|
|
193
|
+
while (id && mapping[id] && !visited.has(id)) {
|
|
194
|
+
visited.add(id);
|
|
195
|
+
path.push(mapping[id]);
|
|
196
|
+
id = mapping[id].parent;
|
|
197
|
+
}
|
|
198
|
+
path.reverse();
|
|
199
|
+
} else {
|
|
200
|
+
const root = Object.values(mapping).find((n) => !n.parent || !mapping[n.parent]);
|
|
201
|
+
if (!root) return [];
|
|
202
|
+
|
|
203
|
+
const subtreeSize = {};
|
|
204
|
+
function size(id) {
|
|
205
|
+
if (id in subtreeSize) return subtreeSize[id];
|
|
206
|
+
const node = mapping[id];
|
|
207
|
+
if (!node) return (subtreeSize[id] = 0);
|
|
208
|
+
const childSizes = (node.children ?? []).map((cid) => size(cid));
|
|
209
|
+
return (subtreeSize[id] = 1 + (childSizes.length ? Math.max(...childSizes) : 0));
|
|
210
|
+
}
|
|
211
|
+
for (const id of Object.keys(mapping)) size(id);
|
|
212
|
+
|
|
213
|
+
let node = root;
|
|
214
|
+
const visited = new Set();
|
|
215
|
+
while (node && !visited.has(node.id)) {
|
|
216
|
+
visited.add(node.id);
|
|
217
|
+
path.push(node);
|
|
218
|
+
const validChildren = (node.children ?? []).filter((cid) => cid in mapping);
|
|
219
|
+
node = validChildren.length
|
|
220
|
+
? mapping[
|
|
221
|
+
validChildren.reduce((best, cid) => (subtreeSize[cid] > subtreeSize[best] ? cid : best))
|
|
222
|
+
]
|
|
223
|
+
: null;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const messages = [];
|
|
228
|
+
|
|
229
|
+
for (const node of path) {
|
|
230
|
+
const msg = node.message;
|
|
231
|
+
if (!msg) continue;
|
|
232
|
+
if (msg.metadata?.is_visually_hidden_from_conversation === true) continue;
|
|
233
|
+
|
|
234
|
+
const role = msg?.author?.role;
|
|
235
|
+
const authorName = msg?.author?.name;
|
|
236
|
+
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';
|
|
242
|
+
|
|
243
|
+
if (role === 'user' || role === 'assistant' || role === 'tool' || isThoughtMsg) {
|
|
244
|
+
const segments = [];
|
|
245
|
+
const parts = msg?.content?.parts ?? [];
|
|
246
|
+
|
|
247
|
+
for (const part of parts) {
|
|
248
|
+
let partText = '';
|
|
249
|
+
let isThoughtPart = isThoughtMsg;
|
|
250
|
+
|
|
251
|
+
if (typeof part === 'string') {
|
|
252
|
+
partText = part;
|
|
253
|
+
} else if (part && typeof part === 'object') {
|
|
254
|
+
if (part.content_type === 'text' && typeof part.text === 'string') {
|
|
255
|
+
partText = part.text;
|
|
256
|
+
} else if (part.content_type === 'thought' && typeof part.text === 'string') {
|
|
257
|
+
partText = part.text;
|
|
258
|
+
isThoughtPart = true;
|
|
259
|
+
} else if (part.content_type === 'audio_transcription' && typeof part.text === 'string') {
|
|
260
|
+
partText = part.text;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (partText && role !== 'tool') {
|
|
265
|
+
const text = partText
|
|
266
|
+
.replace(/\u{E0000}[\u{E0000}-\u{E007F}]*/gu, '')
|
|
267
|
+
.replace(/citeturn\d+\w*/g, '')
|
|
268
|
+
.trim();
|
|
269
|
+
if (text) {
|
|
270
|
+
if (isThoughtPart) {
|
|
271
|
+
segments.push({ type: 'thought', content: text });
|
|
272
|
+
} else {
|
|
273
|
+
segments.push({ type: 'text', content: text });
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
} else if (
|
|
277
|
+
includeImages &&
|
|
278
|
+
part?.content_type === 'image_asset_pointer' &&
|
|
279
|
+
part?.asset_pointer
|
|
280
|
+
) {
|
|
281
|
+
segments.push({ type: 'image', fileId: part.asset_pointer.split('://')[1] });
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// 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() });
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Handle o1/o3/o4 reasoning thoughts array: content.thoughts = [{ summary, content }]
|
|
291
|
+
if (Array.isArray(msg.content?.thoughts) && msg.content.thoughts.length > 0) {
|
|
292
|
+
const thoughtParts = msg.content.thoughts
|
|
293
|
+
.map((t) => (t.summary ? `**${t.summary}**\n${t.content || ''}` : t.content || ''))
|
|
294
|
+
.filter(Boolean);
|
|
295
|
+
if (thoughtParts.length > 0) {
|
|
296
|
+
segments.push({ type: 'thought', content: thoughtParts.join('\n\n') });
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Handle reasoning recap
|
|
301
|
+
if (
|
|
302
|
+
(msg.content?.content_type === 'reasoning_recap' || msg.content?.content) &&
|
|
303
|
+
typeof msg.content.content === 'string' &&
|
|
304
|
+
msg.content.content.trim()
|
|
305
|
+
) {
|
|
306
|
+
segments.push({ type: 'thought', content: msg.content.content.trim() });
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Handle Deep Research reports (widget_state)
|
|
310
|
+
const widgetRaw =
|
|
311
|
+
msg.metadata?.chatgpt_sdk?.widget_state ||
|
|
312
|
+
msg.metadata?.tool_response_metadata?.venus_widget_state;
|
|
313
|
+
if (widgetRaw) {
|
|
314
|
+
try {
|
|
315
|
+
const widget = typeof widgetRaw === 'string' ? JSON.parse(widgetRaw) : widgetRaw;
|
|
316
|
+
const reportText = widget.report_message?.content?.parts?.[0] || widget.markdown;
|
|
317
|
+
const steering = widget.steering_acknowledgement;
|
|
318
|
+
let researchContent = '';
|
|
319
|
+
if (steering) researchContent += `${steering}\n\n`;
|
|
320
|
+
if (reportText) researchContent += reportText;
|
|
321
|
+
if (researchContent.trim()) {
|
|
322
|
+
segments.push({ type: 'text', content: researchContent.trim() });
|
|
323
|
+
}
|
|
324
|
+
} catch {
|
|
325
|
+
// Ignore widget state JSON parse errors
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// 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);
|
|
332
|
+
if (fileNames.length > 0) {
|
|
333
|
+
segments.push({ type: 'text', content: `[Attached: ${fileNames.join(', ')}]` });
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Handle Canvas documents
|
|
338
|
+
if (msg.metadata?.canvas?.title) {
|
|
339
|
+
segments.push({ type: 'text', content: `[Canvas: ${msg.metadata.canvas.title}]` });
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const displayRole = role === 'user' ? 'User' : 'ChatGPT';
|
|
343
|
+
const timestamp = msg?.create_time ? new Date(msg.create_time * 1000).toLocaleString() : null;
|
|
344
|
+
|
|
345
|
+
if (segments.length) {
|
|
346
|
+
const citeMap = {};
|
|
347
|
+
const imageGroupMap = {};
|
|
348
|
+
for (const ref of msg?.metadata?.content_references ?? []) {
|
|
349
|
+
if (ref.matched_text) {
|
|
350
|
+
if (ref.items?.length) citeMap[ref.matched_text] = ref.items;
|
|
351
|
+
if (ref.type === 'image_group' || ref.matched_text.includes('image_group')) {
|
|
352
|
+
imageGroupMap[ref.matched_text] = ref;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// If previous message is also ChatGPT, merge segments (thoughts in front, content in back)
|
|
358
|
+
if (
|
|
359
|
+
displayRole === 'ChatGPT' &&
|
|
360
|
+
messages.length > 0 &&
|
|
361
|
+
messages[messages.length - 1].role === 'ChatGPT'
|
|
362
|
+
) {
|
|
363
|
+
const prevMsg = messages[messages.length - 1];
|
|
364
|
+
if (isThoughtMsg) {
|
|
365
|
+
prevMsg.segments.unshift(...segments);
|
|
366
|
+
} else {
|
|
367
|
+
prevMsg.segments.push(...segments);
|
|
368
|
+
}
|
|
369
|
+
Object.assign(prevMsg.citeMap, citeMap);
|
|
370
|
+
Object.assign(prevMsg.imageGroupMap, imageGroupMap);
|
|
371
|
+
if (timestamp && !prevMsg.timestamp) {
|
|
372
|
+
prevMsg.timestamp = timestamp;
|
|
373
|
+
}
|
|
374
|
+
} else {
|
|
375
|
+
messages.push({ role: displayRole, segments, citeMap, imageGroupMap, timestamp });
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
return messages;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function cleanMarkdownFromApi(text, citeMap, imageGroupMap) {
|
|
385
|
+
if (!text) return '';
|
|
386
|
+
|
|
387
|
+
// 1. Remove specific character ranges (like some PUA ranges)
|
|
388
|
+
text = text
|
|
389
|
+
.replace(/\u{E0000}[\u{E0000}-\u{E007F}]*/gu, '')
|
|
390
|
+
.replace(/citeturn\d+\w*/g, '')
|
|
391
|
+
.trim();
|
|
392
|
+
|
|
393
|
+
// 2. Replace ChatGPT PUA URL annotations: url{label}{href}
|
|
394
|
+
text = text.replace(
|
|
395
|
+
/\uE200url\uE202([^\uE202\uE201]+)\uE202([^\uE201]+)\uE201/g,
|
|
396
|
+
(_, label, href) => `[${label.trim()}](${href.trim()})`,
|
|
397
|
+
);
|
|
398
|
+
|
|
399
|
+
// 3. Replace ChatGPT PUA entity annotations: entity[\"type\",\"name\",...]
|
|
400
|
+
text = text.replace(/\uE200entity\uE202([^\uE201]+)\uE201/g, (_, json) => {
|
|
401
|
+
try {
|
|
402
|
+
const arr = JSON.parse(json.replace(/\\"/g, '"'));
|
|
403
|
+
const name = Array.isArray(arr) && arr[1] ? arr[1] : json;
|
|
404
|
+
return name;
|
|
405
|
+
} catch {
|
|
406
|
+
return json;
|
|
407
|
+
}
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
// 3.5. Clean/replace ChatGPT PUA image_group annotations: image_group{json}
|
|
411
|
+
text = text.replace(/\uE200image_group\uE202[^\uE201]+\uE201/g, (match) => {
|
|
412
|
+
const ref = imageGroupMap?.[match];
|
|
413
|
+
if (ref) {
|
|
414
|
+
if (Array.isArray(ref.images) && ref.images.length > 0) {
|
|
415
|
+
const markdownImgs = ref.images
|
|
416
|
+
.map((imgObj) => {
|
|
417
|
+
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;
|
|
420
|
+
if (src) {
|
|
421
|
+
return ``;
|
|
422
|
+
}
|
|
423
|
+
return '';
|
|
424
|
+
})
|
|
425
|
+
.filter(Boolean);
|
|
426
|
+
if (markdownImgs.length > 0) {
|
|
427
|
+
return '\n\n' + markdownImgs.join('\n\n') + '\n\n';
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
if (ref.safe_urls && Array.isArray(ref.safe_urls) && ref.safe_urls.length > 0) {
|
|
431
|
+
return (
|
|
432
|
+
'\n\n' + ref.safe_urls.map((url, i) => ``).join('\n\n') + '\n\n'
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
if (ref.alt) {
|
|
436
|
+
return '\n\n' + ref.alt + '\n\n';
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
return '';
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
// 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
|
+
});
|
|
452
|
+
|
|
453
|
+
return text;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
export class ChatGPTParser extends ChatParser {
|
|
457
|
+
name = 'ChatGPT';
|
|
458
|
+
constructor() {
|
|
459
|
+
super();
|
|
460
|
+
this.lastFetch = null;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
isAvailable(url) {
|
|
464
|
+
return url.includes('chatgpt.com');
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
getRoleElement(container) {
|
|
468
|
+
if (container.matches?.('[data-message-author-role]')) return container;
|
|
469
|
+
return container.querySelector?.('[data-message-author-role]') || null;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
getRoleElements(container) {
|
|
473
|
+
if (container.matches?.('[data-message-author-role]')) return [container];
|
|
474
|
+
return Array.from(container.querySelectorAll?.('[data-message-author-role]') || []);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
getMessageRole(container, roleElement) {
|
|
478
|
+
const roleAttr = roleElement?.getAttribute('data-message-author-role');
|
|
479
|
+
if (roleAttr) return roleAttr === 'user' ? 'User' : 'ChatGPT';
|
|
480
|
+
|
|
481
|
+
const text = container.innerText || '';
|
|
482
|
+
if (text.startsWith('You\n') || text.includes('\nYou\n')) return 'User';
|
|
483
|
+
|
|
484
|
+
return 'ChatGPT';
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
getContentElement(container, roleElement) {
|
|
488
|
+
if (roleElement?.getAttribute('data-message-author-role') === 'user') {
|
|
489
|
+
return roleElement;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
const selectors = ['.markdown', '.prose', '.whitespace-pre-wrap'];
|
|
493
|
+
for (const selector of selectors) {
|
|
494
|
+
const contentElement = container.querySelector?.(selector);
|
|
495
|
+
if (contentElement) return contentElement;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
return roleElement || (container.matches?.('article') ? container : null);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
getContentElements(container, roleElements) {
|
|
502
|
+
const contentElements = [];
|
|
503
|
+
|
|
504
|
+
roleElements.forEach((roleElement) => {
|
|
505
|
+
const contentElement = this.getContentElement(roleElement, roleElement);
|
|
506
|
+
if (contentElement) contentElements.push(contentElement);
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
if (contentElements.length > 0) return contentElements;
|
|
510
|
+
|
|
511
|
+
const fallback = this.getContentElement(container, roleElements[0]);
|
|
512
|
+
return fallback ? [fallback] : [];
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
cleanContent(content) {
|
|
516
|
+
return content
|
|
517
|
+
.replace(/^Show moreShow less$/gm, '')
|
|
518
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
519
|
+
.trim();
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
getMessageKey(container, roleElement, role, content) {
|
|
523
|
+
const idElement =
|
|
524
|
+
roleElement?.closest?.('[data-message-id]') || container.querySelector?.('[data-message-id]');
|
|
525
|
+
const messageId = idElement?.getAttribute('data-message-id');
|
|
526
|
+
if (messageId) return messageId;
|
|
527
|
+
|
|
528
|
+
const turnId = container.getAttribute?.('data-testid');
|
|
529
|
+
if (turnId) return `${turnId}:${role}`;
|
|
530
|
+
|
|
531
|
+
return `${role}:${content.replace(/\s+/g, ' ').trim()}`;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
extractAttachments(container) {
|
|
535
|
+
const attachments = [];
|
|
536
|
+
const rawContent = container.textContent || container.innerText || '';
|
|
537
|
+
const filePatterns = [
|
|
538
|
+
/([a-zA-Z0-9_-]+\.tex)/g,
|
|
539
|
+
/([a-zA-Z0-9_-]+\.txt)/g,
|
|
540
|
+
/([a-zA-Z0-9_-]+\.md)/g,
|
|
541
|
+
/([a-zA-Z0-9_-]+\.pdf)/g,
|
|
542
|
+
/([a-zA-Z0-9_-]+\.doc)/g,
|
|
543
|
+
];
|
|
544
|
+
const foundFiles = new Set();
|
|
545
|
+
|
|
546
|
+
filePatterns.forEach((pattern) => {
|
|
547
|
+
const matches = rawContent.match(pattern);
|
|
548
|
+
if (matches) matches.forEach((match) => foundFiles.add(match));
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
foundFiles.forEach((fileName) => {
|
|
552
|
+
const fileExt = fileName.substring(fileName.lastIndexOf('.') + 1).toLowerCase();
|
|
553
|
+
const typeMap = {
|
|
554
|
+
tex: 'LaTeX',
|
|
555
|
+
txt: 'Text',
|
|
556
|
+
md: 'Markdown',
|
|
557
|
+
pdf: 'PDF',
|
|
558
|
+
doc: 'Document',
|
|
559
|
+
docx: 'Document',
|
|
560
|
+
};
|
|
561
|
+
attachments.push({ name: fileName, type: typeMap[fileExt] || 'File' });
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
return attachments;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
extractImages(container) {
|
|
568
|
+
const seenSrcs = new Set();
|
|
569
|
+
const capturedImages = [];
|
|
570
|
+
|
|
571
|
+
container.querySelectorAll?.('img').forEach((img) => {
|
|
572
|
+
const src = img.getAttribute('src');
|
|
573
|
+
const alt = img.getAttribute('alt') || 'Image';
|
|
574
|
+
const isContentImage =
|
|
575
|
+
src?.includes('backend-api') ||
|
|
576
|
+
src?.includes('files') ||
|
|
577
|
+
src?.startsWith('blob:') ||
|
|
578
|
+
alt.includes('Uploaded') ||
|
|
579
|
+
alt.includes('Generated');
|
|
580
|
+
|
|
581
|
+
if (src && !seenSrcs.has(src) && isContentImage) {
|
|
582
|
+
seenSrcs.add(src);
|
|
583
|
+
capturedImages.push(``);
|
|
584
|
+
}
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
return capturedImages;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
appendAttachments(content, attachments, capturedImages) {
|
|
591
|
+
const attachmentLines = [];
|
|
592
|
+
const groupedAttachments = {};
|
|
593
|
+
|
|
594
|
+
attachments.forEach((attachment) => {
|
|
595
|
+
groupedAttachments[attachment.type] ||= [];
|
|
596
|
+
groupedAttachments[attachment.type].push(attachment.name);
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
Object.entries(groupedAttachments).forEach(([type, files]) => {
|
|
600
|
+
attachmentLines.push(`**${type} Files:**`);
|
|
601
|
+
files.forEach((file) => attachmentLines.push(`- ${file}`));
|
|
602
|
+
attachmentLines.push('');
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
if (capturedImages.length > 0) {
|
|
606
|
+
if (attachmentLines.length > 0) attachmentLines.push('');
|
|
607
|
+
attachmentLines.push('**Images:**');
|
|
608
|
+
capturedImages.forEach((image) => attachmentLines.push(`- ${image}`));
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
if (attachmentLines.length === 0) return content;
|
|
612
|
+
return `${content}\n\n**Attachments & Images:**\n${attachmentLines.join('\n')}`;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
convertContentElement(contentElement) {
|
|
616
|
+
return convertToMarkdown(contentElement);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
extractMessage(container) {
|
|
620
|
+
const roleElements = this.getRoleElements(container);
|
|
621
|
+
const roleElement = roleElements[0] || this.getRoleElement(container);
|
|
622
|
+
const contentElements = this.getContentElements(container, roleElements);
|
|
623
|
+
if (contentElements.length === 0) return null;
|
|
624
|
+
|
|
625
|
+
const role = this.getMessageRole(container, roleElement);
|
|
626
|
+
const noiseSelectors = ['.flex.gap-2', 'button', '.sr-only', '[role="button"]'];
|
|
627
|
+
const contentParts = contentElements
|
|
628
|
+
.map((contentElement) => {
|
|
629
|
+
const clone = contentElement.cloneNode(true);
|
|
630
|
+
clone.querySelectorAll('button').forEach((button) => {
|
|
631
|
+
const img = button.querySelector('img');
|
|
632
|
+
if (!img) return;
|
|
633
|
+
|
|
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();
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
const alt = img.getAttribute('alt') || '';
|
|
643
|
+
const src = img.getAttribute('src') || '';
|
|
644
|
+
const imageUrl = alt.startsWith('http://') || alt.startsWith('https://') ? alt : src;
|
|
645
|
+
|
|
646
|
+
if (imageUrl) {
|
|
647
|
+
const newImg = clone.ownerDocument.createElement('img');
|
|
648
|
+
newImg.setAttribute('src', imageUrl);
|
|
649
|
+
newImg.setAttribute('alt', caption);
|
|
650
|
+
button.parentNode.replaceChild(newImg, button);
|
|
651
|
+
}
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
noiseSelectors.forEach((selector) => {
|
|
655
|
+
clone.querySelectorAll(selector).forEach((node) => node.remove());
|
|
656
|
+
});
|
|
657
|
+
return this.cleanContent(this.convertContentElement(clone));
|
|
658
|
+
})
|
|
659
|
+
.filter(Boolean);
|
|
660
|
+
|
|
661
|
+
let content = contentParts.join('\n\n');
|
|
662
|
+
content = this.appendAttachments(
|
|
663
|
+
content,
|
|
664
|
+
this.extractAttachments(container),
|
|
665
|
+
this.extractImages(container),
|
|
666
|
+
);
|
|
667
|
+
|
|
668
|
+
if (!content) return null;
|
|
669
|
+
|
|
670
|
+
return {
|
|
671
|
+
role,
|
|
672
|
+
content,
|
|
673
|
+
key: this.getMessageKey(container, roleElement, role, content),
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
extractMountedMessages() {
|
|
678
|
+
const articles = Array.from(document.querySelectorAll('article'));
|
|
679
|
+
const containers =
|
|
680
|
+
articles.length > 0
|
|
681
|
+
? articles
|
|
682
|
+
: Array.from(document.querySelectorAll('[data-message-author-role]'));
|
|
683
|
+
|
|
684
|
+
return containers
|
|
685
|
+
.map((container) => this.extractMessage(container))
|
|
686
|
+
.filter(Boolean)
|
|
687
|
+
.map(({ role, content }) => ({ role, content }));
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
async extractAllConversationTurns() {
|
|
691
|
+
const turns = getConversationTurns(document);
|
|
692
|
+
if (turns.length === 0) return [];
|
|
693
|
+
|
|
694
|
+
return collectMountedTurnMessages({
|
|
695
|
+
turns,
|
|
696
|
+
scrollRoot: findChatGPTScrollRoot(turns, document),
|
|
697
|
+
extractMessage: (turn) => this.extractMessage(turn),
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
formatApiResult(convoData, apiMessages, fallbackTitle, images = {}) {
|
|
702
|
+
const messages = [];
|
|
703
|
+
for (const msg of apiMessages) {
|
|
704
|
+
let content = '';
|
|
705
|
+
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);
|
|
710
|
+
if (thoughtText) {
|
|
711
|
+
content += `<details><summary>Thought Process</summary>\n\n${thoughtText}\n\n</details>\n\n`;
|
|
712
|
+
}
|
|
713
|
+
} else if (seg.type === 'image') {
|
|
714
|
+
const src = images[seg.fileId];
|
|
715
|
+
if (src) {
|
|
716
|
+
content += `\n\n`;
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
content = content.trim();
|
|
721
|
+
if (content) {
|
|
722
|
+
const msgObj = {
|
|
723
|
+
role: msg.role,
|
|
724
|
+
content: content,
|
|
725
|
+
};
|
|
726
|
+
if (msg.timestamp) {
|
|
727
|
+
msgObj.timestamp = msg.timestamp;
|
|
728
|
+
}
|
|
729
|
+
messages.push(msgObj);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
const currentUrl =
|
|
734
|
+
typeof window !== 'undefined' && window.location ? window.location.href || '' : '';
|
|
735
|
+
const convTitle = convoData?.title || fallbackTitle;
|
|
736
|
+
const metadata = {
|
|
737
|
+
Source: 'ChatGPT',
|
|
738
|
+
Date: new Date().toLocaleString(),
|
|
739
|
+
Link: currentUrl,
|
|
740
|
+
Model:
|
|
741
|
+
convoData?.model_slug ||
|
|
742
|
+
document.querySelector('[data-testid="model-selector-dropdown"]')?.innerText ||
|
|
743
|
+
'ChatGPT',
|
|
744
|
+
};
|
|
745
|
+
|
|
746
|
+
return { title: convTitle, messages, url: currentUrl, metadata };
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
async parse(options = {}) {
|
|
750
|
+
const title = document.title || 'ChatGPT Session';
|
|
751
|
+
const messages = [];
|
|
752
|
+
|
|
753
|
+
const token = getAccessToken();
|
|
754
|
+
const convId = getConversationId();
|
|
755
|
+
const parserMode = options.parserMode || 'auto';
|
|
756
|
+
const includeImages = options.includeImages !== false;
|
|
757
|
+
|
|
758
|
+
// 1. If on shared chat URL (/share/...) or SSR conversation data exists in DOM, try SSR data first
|
|
759
|
+
const isShareUrl =
|
|
760
|
+
typeof window !== 'undefined' &&
|
|
761
|
+
window.location &&
|
|
762
|
+
(window.location.pathname || '').startsWith('/share/');
|
|
763
|
+
const sharedData = extractSharedConversationFromDom(
|
|
764
|
+
typeof document !== 'undefined' ? document : null,
|
|
765
|
+
);
|
|
766
|
+
|
|
767
|
+
if (isShareUrl && sharedData?.mapping && parserMode !== 'prefer_dom') {
|
|
768
|
+
const apiMessages = linearize(sharedData.mapping, includeImages, sharedData.current_node);
|
|
769
|
+
if (apiMessages.length > 0) {
|
|
770
|
+
return this.formatApiResult(sharedData, apiMessages, title);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// 2. Try fetching from ChatGPT backend API
|
|
775
|
+
if (token && convId && parserMode !== 'prefer_dom') {
|
|
776
|
+
try {
|
|
777
|
+
const now = Date.now();
|
|
778
|
+
let result;
|
|
779
|
+
|
|
780
|
+
if (
|
|
781
|
+
this.lastFetch &&
|
|
782
|
+
this.lastFetch.convId === convId &&
|
|
783
|
+
this.lastFetch.includeImages === includeImages &&
|
|
784
|
+
now - this.lastFetch.timestamp < 20000
|
|
785
|
+
) {
|
|
786
|
+
result = this.lastFetch.result;
|
|
787
|
+
} 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';
|
|
792
|
+
script.onload = function () {
|
|
793
|
+
this.remove();
|
|
794
|
+
};
|
|
795
|
+
(document.head || document.documentElement).appendChild(script);
|
|
796
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
result = await fetchConversation(convId, token, includeImages);
|
|
800
|
+
this.lastFetch = {
|
|
801
|
+
convId,
|
|
802
|
+
includeImages,
|
|
803
|
+
timestamp: now,
|
|
804
|
+
result,
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
const apiMessages = linearize(result.data.mapping, includeImages, result.data.current_node);
|
|
809
|
+
if (apiMessages.length > 0) {
|
|
810
|
+
return this.formatApiResult(result.data, apiMessages, title, result.images);
|
|
811
|
+
}
|
|
812
|
+
} catch (e) {
|
|
813
|
+
console.error('[AI Exporter] API parse failed, falling back to SSR/DOM:', e);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// 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);
|
|
820
|
+
if (apiMessages.length > 0) {
|
|
821
|
+
return this.formatApiResult(sharedData, apiMessages, title);
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
// Check if we have iframe-based content (deep research feature)
|
|
826
|
+
const iframes = document.querySelectorAll('iframe[src*="oaiusercontent.com"]');
|
|
827
|
+
if (iframes.length > 0) {
|
|
828
|
+
console.log('Detected iframe-based content, attempting extraction...');
|
|
829
|
+
|
|
830
|
+
// Try multiple strategies to extract content
|
|
831
|
+
let extractedContent = '';
|
|
832
|
+
|
|
833
|
+
// Strategy 1: Look for data in script tags or window objects
|
|
834
|
+
try {
|
|
835
|
+
// Check if any conversation data is exposed globally
|
|
836
|
+
if (window.conversationData || window.chatData) {
|
|
837
|
+
extractedContent = JSON.stringify(window.conversationData || window.chatData);
|
|
838
|
+
}
|
|
839
|
+
} catch (e) {
|
|
840
|
+
console.log('Global data access failed:', e);
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
// Strategy 2: Look for preloaded content in hidden elements
|
|
844
|
+
if (!extractedContent) {
|
|
845
|
+
const hiddenSelectors = [
|
|
846
|
+
'[data-conversation]',
|
|
847
|
+
'[data-messages]',
|
|
848
|
+
'.conversation-data',
|
|
849
|
+
'.chat-transcript',
|
|
850
|
+
'pre[data-conversation]',
|
|
851
|
+
];
|
|
852
|
+
|
|
853
|
+
for (const selector of hiddenSelectors) {
|
|
854
|
+
const element = document.querySelector(selector);
|
|
855
|
+
if (element && element.textContent) {
|
|
856
|
+
extractedContent = element.textContent;
|
|
857
|
+
break;
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
// Strategy 3: Enhanced text extraction from main content
|
|
863
|
+
if (!extractedContent) {
|
|
864
|
+
const mainContent =
|
|
865
|
+
document.querySelector('main') ||
|
|
866
|
+
document.querySelector('[role="main"]') ||
|
|
867
|
+
document.querySelector('.conversation') ||
|
|
868
|
+
document.body;
|
|
869
|
+
|
|
870
|
+
if (mainContent) {
|
|
871
|
+
const textContent = mainContent.textContent || mainContent.innerText;
|
|
872
|
+
if (textContent && textContent.trim()) {
|
|
873
|
+
const lines = textContent.split('\n').filter((line) => line.trim());
|
|
874
|
+
|
|
875
|
+
// Look for conversation patterns
|
|
876
|
+
const conversationLines = lines.filter(
|
|
877
|
+
(line) =>
|
|
878
|
+
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'),
|
|
889
|
+
);
|
|
890
|
+
|
|
891
|
+
if (conversationLines.length > 0) {
|
|
892
|
+
extractedContent = conversationLines.join('\n\n');
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
// Strategy 4: Last resort - check for any meaningful content
|
|
899
|
+
if (!extractedContent) {
|
|
900
|
+
const allText = document.body.textContent || document.body.innerText;
|
|
901
|
+
if (allText && allText.trim().length > 100) {
|
|
902
|
+
extractedContent = allText.trim();
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
// If we found content, try to structure it
|
|
907
|
+
if (extractedContent) {
|
|
908
|
+
// Try to identify user vs assistant messages
|
|
909
|
+
const lines = extractedContent.split('\n').filter((line) => line.trim());
|
|
910
|
+
|
|
911
|
+
lines.forEach((line) => {
|
|
912
|
+
if (line.length > 10) {
|
|
913
|
+
// Simple heuristic: shorter lines are often user prompts
|
|
914
|
+
if (
|
|
915
|
+
line.length < 200 ||
|
|
916
|
+
line.includes('?') ||
|
|
917
|
+
line.includes('write') ||
|
|
918
|
+
line.includes('tell')
|
|
919
|
+
) {
|
|
920
|
+
messages.push({
|
|
921
|
+
role: 'User',
|
|
922
|
+
content: line.trim(),
|
|
923
|
+
});
|
|
924
|
+
} else {
|
|
925
|
+
messages.push({
|
|
926
|
+
role: 'ChatGPT',
|
|
927
|
+
content: line.trim(),
|
|
928
|
+
});
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
});
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
// Add note about extraction method
|
|
935
|
+
if (messages.length > 0) {
|
|
936
|
+
messages.push({
|
|
937
|
+
role: 'ChatGPT',
|
|
938
|
+
content:
|
|
939
|
+
'*Note: Content extracted from iframe-based ChatGPT interface. Some formatting may be lost.*',
|
|
940
|
+
});
|
|
941
|
+
} else {
|
|
942
|
+
// Last resort - add a message explaining the limitation
|
|
943
|
+
messages.push({
|
|
944
|
+
role: 'ChatGPT',
|
|
945
|
+
content:
|
|
946
|
+
'*Note: ChatGPT is using iframe-based content that cannot be accessed by browser extensions. Please try exporting from a standard ChatGPT conversation.*',
|
|
947
|
+
});
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
return { title, messages };
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
const fullExport = options.full !== false;
|
|
954
|
+
const extractedMessages = fullExport
|
|
955
|
+
? await this.extractAllConversationTurns()
|
|
956
|
+
: this.extractMountedMessages();
|
|
957
|
+
messages.push(
|
|
958
|
+
...(extractedMessages.length > 0 ? extractedMessages : this.extractMountedMessages()),
|
|
959
|
+
);
|
|
960
|
+
|
|
961
|
+
const currentUrl =
|
|
962
|
+
typeof window !== 'undefined' && window.location ? window.location.href || '' : '';
|
|
963
|
+
const metadata = {
|
|
964
|
+
Source: 'ChatGPT',
|
|
965
|
+
Date: new Date().toLocaleString(),
|
|
966
|
+
Link: currentUrl,
|
|
967
|
+
Model:
|
|
968
|
+
document.querySelector('[data-testid="model-selector-dropdown"]')?.innerText || 'ChatGPT',
|
|
969
|
+
};
|
|
970
|
+
|
|
971
|
+
return { title, messages, url: currentUrl, metadata };
|
|
972
|
+
}
|
|
973
|
+
}
|