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.
@@ -0,0 +1,137 @@
1
+ if (!window.__chatgptHelperInjected) {
2
+ window.__chatgptHelperInjected = true;
3
+
4
+ window.capturedAuthStore = window.capturedAuthStore || {
5
+ authorization: null,
6
+ extraHeaders: {},
7
+ };
8
+
9
+ function getCookieDeviceId() {
10
+ try {
11
+ const match = typeof document !== 'undefined' && document.cookie.match(/oai-did=([^;]+)/);
12
+ return match ? match[1] : null;
13
+ } catch {
14
+ return null;
15
+ }
16
+ }
17
+
18
+ // Hook fetch to store Authorization and custom headers when page performs network calls
19
+ if (typeof window !== 'undefined' && window.fetch) {
20
+ const originalFetch = window.fetch;
21
+ window.fetch = function (...args) {
22
+ try {
23
+ const url = typeof args[0] === 'string' ? args[0] : args[0]?.url;
24
+ if (url && (url.includes('/backend-api/') || url.includes('/api/auth/'))) {
25
+ const options = args[1];
26
+ if (options?.headers) {
27
+ let auth = null;
28
+ if (options.headers instanceof Headers) {
29
+ auth = options.headers.get('Authorization');
30
+ } else if (typeof options.headers === 'object') {
31
+ auth = options.headers.Authorization || options.headers.authorization;
32
+ }
33
+ if (auth) {
34
+ window.capturedAuthStore.authorization = auth;
35
+ }
36
+ }
37
+ }
38
+ } catch {
39
+ // Ignore interception errors
40
+ }
41
+ return originalFetch.apply(this, args);
42
+ };
43
+ }
44
+
45
+ window.addEventListener('message', async (event) => {
46
+ if (event.origin !== 'https://chatgpt.com') return;
47
+ if (event.data?.source !== 'chatgpt-exporter-ext') return;
48
+ if (event.data?.type !== 'fetch_conversation') return;
49
+
50
+ const { convId, token, requestId, includeImages } = event.data;
51
+
52
+ try {
53
+ const headers = {
54
+ Accept: 'application/json',
55
+ };
56
+
57
+ const authToken = token ? `Bearer ${token}` : window.capturedAuthStore?.authorization;
58
+ if (authToken) {
59
+ headers.Authorization = authToken;
60
+ }
61
+
62
+ const deviceId = getCookieDeviceId();
63
+ if (deviceId) {
64
+ headers['oai-device-id'] = deviceId;
65
+ }
66
+
67
+ const res = await fetch(`https://chatgpt.com/backend-api/conversation/${convId}`, {
68
+ headers,
69
+ });
70
+ if (!res.ok) throw new Error(`API returned ${res.status}`);
71
+ const data = await res.json();
72
+
73
+ let images = {};
74
+ if (includeImages) {
75
+ const fileIds = new Set();
76
+ for (const node of Object.values(data.mapping)) {
77
+ const msg = node.message;
78
+ if (msg && msg.content && Array.isArray(msg.content.parts)) {
79
+ for (const part of msg.content.parts) {
80
+ if (part && part.content_type === 'image_asset_pointer' && part.asset_pointer) {
81
+ fileIds.add(part.asset_pointer.split('://')[1]);
82
+ }
83
+ }
84
+ }
85
+ }
86
+
87
+ const entries = await Promise.all(
88
+ [...fileIds].map(async (id) => {
89
+ try {
90
+ const b64 = await fetchImageAsBase64(id, token);
91
+ return [id, b64];
92
+ } catch (e) {
93
+ console.error('[AI Exporter] Error fetching image:', id, e);
94
+ return [id, null];
95
+ }
96
+ }),
97
+ );
98
+ images = Object.fromEntries(entries.filter(([, b64]) => b64 !== null));
99
+ }
100
+
101
+ window.postMessage(
102
+ { source: 'chatgpt-exporter-page', requestId, data, images },
103
+ 'https://chatgpt.com',
104
+ );
105
+ } catch (err) {
106
+ window.postMessage(
107
+ { source: 'chatgpt-exporter-page', requestId, error: err.message },
108
+ 'https://chatgpt.com',
109
+ );
110
+ }
111
+ });
112
+
113
+ async function fetchImageAsBase64(fileId, token) {
114
+ try {
115
+ const dlRes = await fetch(`https://chatgpt.com/backend-api/files/download/${fileId}`, {
116
+ headers: { Authorization: `Bearer ${token}` },
117
+ });
118
+ if (!dlRes.ok) return null;
119
+ const { download_url } = await dlRes.json();
120
+ if (!download_url) return null;
121
+
122
+ const imgRes = await fetch(download_url);
123
+ if (!imgRes.ok) return null;
124
+
125
+ const blob = await imgRes.blob();
126
+ return await new Promise((resolve) => {
127
+ const reader = new FileReader();
128
+ reader.onload = () => resolve(reader.result);
129
+ reader.onerror = () => resolve(null);
130
+ reader.readAsDataURL(blob);
131
+ });
132
+ } catch (e) {
133
+ console.error('[AI Exporter] fetchImageAsBase64 error:', e);
134
+ return null;
135
+ }
136
+ }
137
+ }
@@ -0,0 +1,193 @@
1
+ const TURN_SELECTOR =
2
+ 'article, [data-testid^="conversation-turn-"], section[data-turn-id], [data-message-author-role]';
3
+ const DEFAULT_RENDER_WAIT_MS = 160;
4
+
5
+ function delay(ms) {
6
+ return new Promise((resolve) => setTimeout(resolve, ms));
7
+ }
8
+
9
+ function isScrollable(element) {
10
+ return element && element.scrollHeight > element.clientHeight + 80;
11
+ }
12
+
13
+ function messageKey(message) {
14
+ if (message.key) return message.key;
15
+ return `${message.role}:${message.content.replace(/\s+/g, ' ').trim()}`;
16
+ }
17
+
18
+ function publicMessage(message) {
19
+ return {
20
+ role: message.role,
21
+ content: message.content,
22
+ };
23
+ }
24
+
25
+ export function getConversationTurnIndex(turn) {
26
+ if (!turn || typeof turn.getAttribute !== 'function') return Number.POSITIVE_INFINITY;
27
+ const testId = turn.getAttribute('data-testid') || '';
28
+ const match = testId.match(/^conversation-turn-(\d+)$/);
29
+ if (match) return Number(match[1]);
30
+ const turnId = turn.getAttribute('data-turn-id');
31
+ if (turnId && /^\d+$/.test(turnId)) return Number(turnId);
32
+ return Number.POSITIVE_INFINITY;
33
+ }
34
+
35
+ export function getConversationTurns(doc = document) {
36
+ if (!doc || typeof doc.querySelectorAll !== 'function') return [];
37
+ const turns = Array.from(doc.querySelectorAll(TURN_SELECTOR));
38
+ // Filter out nested duplicates (e.g. [data-message-author-role] inside an article)
39
+ const filtered = turns.filter((el) => {
40
+ return !turns.some((parent) => parent !== el && parent.contains(el));
41
+ });
42
+
43
+ return filtered.sort((a, b) => {
44
+ const idxA = getConversationTurnIndex(a);
45
+ const idxB = getConversationTurnIndex(b);
46
+ if (idxA !== idxB) return idxA - idxB;
47
+ if (typeof a.compareDocumentPosition === 'function') {
48
+ return a.compareDocumentPosition(b) &
49
+ (doc.defaultView?.Node?.DOCUMENT_POSITION_FOLLOWING || 4)
50
+ ? -1
51
+ : 1;
52
+ }
53
+ return 0;
54
+ });
55
+ }
56
+
57
+ export function findChatGPTScrollRoot(turns, doc = document) {
58
+ const firstTurn = turns.find(Boolean);
59
+ let current = firstTurn?.parentElement || null;
60
+
61
+ while (current) {
62
+ if (isScrollable(current)) return current;
63
+ current = current.parentElement;
64
+ }
65
+
66
+ const main = doc.querySelector('main');
67
+ if (isScrollable(main)) return main;
68
+
69
+ return doc.scrollingElement || doc.documentElement || doc.body;
70
+ }
71
+
72
+ function createProgressOverlay(doc) {
73
+ if (!doc || typeof doc.createElement !== 'function' || !doc.body) return null;
74
+ try {
75
+ const overlay = doc.createElement('div');
76
+ overlay.id = 'ai-exporter-progress-overlay';
77
+ Object.assign(overlay.style, {
78
+ position: 'fixed',
79
+ top: '16px',
80
+ left: '50%',
81
+ transform: 'translateX(-50%)',
82
+ zIndex: '999999',
83
+ padding: '10px 20px',
84
+ background: 'rgba(15, 23, 42, 0.92)',
85
+ color: '#f8fafc',
86
+ borderRadius: '8px',
87
+ fontFamily: 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
88
+ fontSize: '14px',
89
+ fontWeight: '500',
90
+ boxShadow: '0 10px 25px -5px rgba(0, 0, 0, 0.3), 0 8px 10px -6px rgba(0, 0, 0, 0.3)',
91
+ pointerEvents: 'none',
92
+ transition: 'opacity 0.2s ease',
93
+ });
94
+ overlay.textContent = 'Preparing conversation export...';
95
+ doc.body.appendChild(overlay);
96
+ return overlay;
97
+ } catch {
98
+ return null;
99
+ }
100
+ }
101
+
102
+ function updateProgressOverlay(overlay, current, total) {
103
+ if (!overlay) return;
104
+ const percentage = total > 0 ? Math.round((current / total) * 100) : 0;
105
+ overlay.textContent = `Processing messages (${current}/${total} - ${percentage}%)...`;
106
+ }
107
+
108
+ function removeProgressOverlay(overlay) {
109
+ if (!overlay) return;
110
+ try {
111
+ overlay.style.opacity = '0';
112
+ setTimeout(() => overlay.remove(), 200);
113
+ } catch {
114
+ // Ignore cleanup errors
115
+ }
116
+ }
117
+
118
+ export async function collectMountedTurnMessages({
119
+ turns,
120
+ scrollRoot,
121
+ extractMessage,
122
+ waitForRender = delay,
123
+ renderWaitMs = DEFAULT_RENDER_WAIT_MS,
124
+ renderAttempts = 4,
125
+ doc = typeof document !== 'undefined' ? document : null,
126
+ }) {
127
+ const originalTop = scrollRoot?.scrollTop;
128
+ const originalBehavior = scrollRoot?.style?.scrollBehavior;
129
+ const overlay = createProgressOverlay(doc);
130
+
131
+ if (scrollRoot && scrollRoot.style) {
132
+ scrollRoot.style.scrollBehavior = 'auto';
133
+ }
134
+
135
+ const seen = new Set();
136
+ const messages = [];
137
+
138
+ try {
139
+ // Scroll to top first to trigger un-virtualization of earlier turns
140
+ if (scrollRoot && typeof scrollRoot.scrollTop === 'number') {
141
+ scrollRoot.scrollTop = 0;
142
+ await waitForRender(renderWaitMs);
143
+ }
144
+
145
+ // Accumulate all turns dynamically across scroll passes
146
+ const allTurnElements = new Set(turns || []);
147
+ if (doc) {
148
+ getConversationTurns(doc).forEach((t) => allTurnElements.add(t));
149
+ }
150
+
151
+ const orderedTurns = Array.from(allTurnElements).sort((a, b) => {
152
+ return getConversationTurnIndex(a) - getConversationTurnIndex(b);
153
+ });
154
+
155
+ const totalTurns = orderedTurns.length;
156
+
157
+ for (let idx = 0; idx < totalTurns; idx += 1) {
158
+ const turn = orderedTurns[idx];
159
+ updateProgressOverlay(overlay, idx + 1, totalTurns);
160
+
161
+ if (typeof turn.scrollIntoView === 'function') {
162
+ turn.scrollIntoView({ block: 'center' });
163
+ }
164
+
165
+ let message = null;
166
+ for (let attempt = 0; attempt < renderAttempts; attempt += 1) {
167
+ await waitForRender(renderWaitMs);
168
+ message = extractMessage(turn);
169
+ if (message?.content) break;
170
+ }
171
+
172
+ if (!message?.content) continue;
173
+
174
+ const key = messageKey(message);
175
+ if (seen.has(key)) continue;
176
+
177
+ seen.add(key);
178
+ messages.push(publicMessage(message));
179
+ }
180
+ } finally {
181
+ removeProgressOverlay(overlay);
182
+
183
+ if (scrollRoot && scrollRoot.style) {
184
+ scrollRoot.style.scrollBehavior = originalBehavior || '';
185
+ }
186
+
187
+ if (scrollRoot && Number.isFinite(originalTop)) {
188
+ scrollRoot.scrollTop = originalTop;
189
+ }
190
+ }
191
+
192
+ return messages;
193
+ }