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/ai/copilot.js ADDED
@@ -0,0 +1,413 @@
1
+ import { ChatParser } from './base.js';
2
+ import { convertToMarkdown } from '../utils/html-to-markdown.js';
3
+
4
+ export class CopilotParser extends ChatParser {
5
+ name = 'Copilot';
6
+ isAvailable(url) {
7
+ return (
8
+ url.includes('copilot.microsoft.com') ||
9
+ url.includes('copilot.com') ||
10
+ url.includes('copilot.cloud.microsoft') ||
11
+ url.includes('m365.cloud.microsoft') ||
12
+ url.includes('m365.microsoft.com') ||
13
+ url.includes('bing.com/chat') ||
14
+ url.includes('bing.com/copilot') ||
15
+ url.includes('bing.com/copilotsearch') ||
16
+ url.includes('edgeservices.bing.com')
17
+ );
18
+ }
19
+
20
+ async parse() {
21
+ let title = 'Copilot Conversation';
22
+ if (document.title) {
23
+ const cleanTitle = document.title
24
+ .replace(/^Microsoft Copilot:\s*/i, '')
25
+ .replace(/\s*-\s*Microsoft Copilot$/i, '')
26
+ .replace(/^Copilot:\s*/i, '')
27
+ .replace(/\s*-\s*Copilot$/i, '')
28
+ .replace(/Your AI companion/i, '')
29
+ .trim();
30
+ if (
31
+ cleanTitle &&
32
+ cleanTitle.toLowerCase() !== 'microsoft copilot' &&
33
+ cleanTitle.toLowerCase() !== 'copilot'
34
+ ) {
35
+ title = cleanTitle;
36
+ }
37
+ }
38
+
39
+ const messages = [];
40
+
41
+ // Helper to process code blocks and links before HTML-to-markdown conversion
42
+ const processAiElement = (element) => {
43
+ const clone = element.cloneNode(true);
44
+
45
+ // Remove UI noise elements
46
+ const noiseSelectors = [
47
+ '[data-testid="message-item-reactions"]',
48
+ '[data-testid="user-message-reactions"]',
49
+ '[data-testid="copy-ai-message-button"]',
50
+ '[data-testid="copy-user-message-button"]',
51
+ '[data-testid="CopyButtonContainerTestId"]',
52
+ '[data-testid="CopyButtonTestId"]',
53
+ '[data-testid="FeedbackContainerTestId"]',
54
+ '[data-testid="feedback-button-testid"]',
55
+ '[data-testid="overflow-menu-button"]',
56
+ '[data-testid="share-message-button"]',
57
+ '[data-testid="message-thumbs-up-button"]',
58
+ '[data-testid="message-thumbs-down-button"]',
59
+ '[data-testid="message-read-aloud-button"]',
60
+ '[data-testid="regenerate-message-button-popover"]',
61
+ '[data-testid="chat-suggestion"]',
62
+ '[data-testid="loading-message"]',
63
+ '.fai-CopilotMessage__actions',
64
+ '.fai-SuggestionList',
65
+ '.fai-UserMessage__accessibleHeading',
66
+ '.fai-CopilotMessage__accessibleHeading',
67
+ '[class*="suggestedReplies"]',
68
+ '[class*="workingCard"]',
69
+ '[class*="WorkingCard"]',
70
+ '[class*="disclaimerText"]',
71
+ 'cib-action-bar',
72
+ 'cib-feedback-buttons',
73
+ 'cib-message-actions',
74
+ '.sr-only',
75
+ ];
76
+ noiseSelectors.forEach((sel) => {
77
+ clone.querySelectorAll(sel).forEach((el) => el.remove());
78
+ });
79
+
80
+ // Transform data-url span buttons into standard anchor tags
81
+ clone.querySelectorAll('span[data-url]').forEach((span) => {
82
+ const url = span.getAttribute('data-url');
83
+ if (url && !url.startsWith('ca://')) {
84
+ const a = document.createElement('a');
85
+ a.href = url;
86
+ a.textContent = span.textContent;
87
+ span.replaceWith(a);
88
+ }
89
+ });
90
+
91
+ // Standardize code blocks with language labels
92
+ clone.querySelectorAll('div.rounded-xl, div[class*="code-block"]').forEach((block) => {
93
+ const langEl = block.querySelector('span.capitalize, [class*="language-"]');
94
+ const codeEl = block.querySelector('code, pre');
95
+ if (codeEl) {
96
+ const lang = langEl ? langEl.innerText.trim().toLowerCase() : '';
97
+ const codeText = codeEl.innerText || codeEl.textContent;
98
+ const newPre = document.createElement('pre');
99
+ const newCode = document.createElement('code');
100
+ if (lang) {
101
+ newCode.className = `language-${lang}`;
102
+ }
103
+ newCode.textContent = codeText;
104
+ newPre.appendChild(newCode);
105
+ block.replaceWith(newPre);
106
+ }
107
+ });
108
+
109
+ return clone.innerHTML;
110
+ };
111
+
112
+ // Multi-tier DOM extraction strategy
113
+
114
+ // Tier 1: Modern M365 Copilot / Bebop layout & data-content markers
115
+ let turnElements = Array.from(
116
+ document.querySelectorAll(
117
+ '[data-testid="chatQuestion"], [data-testid="copilot-message-div"], [data-content="user-message"], [data-content="ai-message"], [data-content="assistant-message"], [data-message-author-role="user"], [data-message-author-role="assistant"], [data-testid="chat-turn-user"], [data-testid="chat-turn-bot"], [data-testid="chat-turn-assistant"], [data-testid="user-turn"], [data-testid="copilot-turn"], .fai-UserMessage, .fai-CopilotMessage',
118
+ ),
119
+ );
120
+
121
+ // Filter out nested matches
122
+ if (turnElements.length) {
123
+ turnElements = turnElements.filter(
124
+ (el) => !turnElements.some((other) => other !== el && other.contains(el)),
125
+ );
126
+ }
127
+
128
+ // Tier 2: Tailwind group classes if direct markers are absent
129
+ if (!turnElements.length) {
130
+ turnElements = Array.from(
131
+ document.querySelectorAll(
132
+ '[class*="group/user-message"], [class*="group/ai-message"], [class*="group/assistant-message"]',
133
+ ),
134
+ );
135
+ }
136
+
137
+ // Tier 3: testid attributes
138
+ if (!turnElements.length) {
139
+ turnElements = Array.from(
140
+ document.querySelectorAll(
141
+ '[data-testid="user-message"], [data-testid="ai-message"], [data-testid="assistant-message"]',
142
+ ),
143
+ );
144
+ }
145
+
146
+ if (turnElements.length) {
147
+ turnElements.forEach((node) => {
148
+ const dataContent = node.getAttribute('data-content') || '';
149
+ const dataAuthor = (node.getAttribute('data-message-author-role') || '').toLowerCase();
150
+ const className = typeof node.className === 'string' ? node.className : '';
151
+ const testId = node.getAttribute('data-testid') || '';
152
+
153
+ const isUser =
154
+ dataAuthor === 'user' ||
155
+ testId === 'chatQuestion' ||
156
+ testId === 'chat-turn-user' ||
157
+ testId === 'user-turn' ||
158
+ className.includes('UserMessage') ||
159
+ dataContent === 'user-message' ||
160
+ className.includes('user-message') ||
161
+ testId === 'user-message';
162
+
163
+ if (isUser) {
164
+ const targetNode =
165
+ node.querySelector(
166
+ '[data-testid="chatOutput"], [data-testid="user-message-content"], .fai-UserMessage__message, [data-content="user-message"]',
167
+ ) || node;
168
+ const clone = targetNode.cloneNode(true);
169
+ clone
170
+ .querySelectorAll('.fai-UserMessage__accessibleHeading, .sr-only, button')
171
+ .forEach((el) => el.remove());
172
+ const text = clone.innerText || clone.textContent;
173
+ if (text && text.trim()) {
174
+ messages.push({ role: 'User', content: text.trim() });
175
+ }
176
+ } else {
177
+ const targetNode =
178
+ node.querySelector('[data-testid="markdown-reply"]') ||
179
+ node.querySelector('[data-testid="ai-message-body"]') ||
180
+ node.querySelector('[data-testid="copilot-message-content"]') ||
181
+ node.querySelector('.fai-CopilotMessage__content') ||
182
+ node.querySelector('[class*="group/ai-message-item"]') ||
183
+ node;
184
+ const html = processAiElement(targetNode);
185
+ const markdown = convertToMarkdown(html);
186
+ if (markdown && markdown.trim()) {
187
+ messages.push({ role: 'Copilot', content: markdown });
188
+ }
189
+ }
190
+ });
191
+ }
192
+
193
+ // Helper to extract messages from Shadow DOM cib-serp components (Classic Bing Chat / Copilot)
194
+ const extractFromCibSerp = (rootDoc = document) => {
195
+ try {
196
+ const cibSerp = rootDoc.querySelector('cib-serp');
197
+ if (!cibSerp || !cibSerp.shadowRoot) return [];
198
+ const cibConversation = cibSerp.shadowRoot.querySelector('cib-conversation');
199
+ if (!cibConversation || !cibConversation.shadowRoot) return [];
200
+ const cibTurns = cibConversation.shadowRoot.querySelectorAll('cib-chat-turn');
201
+ const results = [];
202
+
203
+ cibTurns.forEach((turn) => {
204
+ const turnRoot = turn.shadowRoot || turn;
205
+ const msgGroups = turnRoot.querySelectorAll('cib-message-group');
206
+ msgGroups.forEach((group) => {
207
+ const source = (group.getAttribute('source') || '').toLowerCase();
208
+ const role = source === 'user' ? 'User' : 'Copilot';
209
+ const groupRoot = group.shadowRoot || group;
210
+ const cibMessages = groupRoot.querySelectorAll('cib-message');
211
+
212
+ cibMessages.forEach((msg) => {
213
+ const msgRoot = msg.shadowRoot || msg;
214
+ if (role === 'User') {
215
+ const text = msgRoot.textContent?.trim();
216
+ if (text) results.push({ role: 'User', content: text });
217
+ } else {
218
+ const shared = msgRoot.querySelector('cib-shared') || msgRoot;
219
+ const html = processAiElement(shared);
220
+ const md = convertToMarkdown(html);
221
+ if (md && md.trim()) results.push({ role: 'Copilot', content: md.trim() });
222
+ }
223
+ });
224
+ });
225
+ });
226
+ return results;
227
+ } catch (err) {
228
+ console.warn('[AI Exporter] Shadow DOM cib-serp extraction error:', err);
229
+ return [];
230
+ }
231
+ };
232
+
233
+ // Tier 4: Shadow DOM cib-serp components
234
+ if (!messages.length) {
235
+ const cibMessages = extractFromCibSerp(document);
236
+ if (cibMessages.length) {
237
+ messages.push(...cibMessages);
238
+ }
239
+ }
240
+
241
+ // Tier 5: Web components in light DOM (cib-chat-turn / cib-message-group)
242
+ if (!messages.length) {
243
+ const cibTurns = document.querySelectorAll('cib-chat-turn');
244
+ if (cibTurns.length) {
245
+ cibTurns.forEach((turn) => {
246
+ const source = (turn.getAttribute('source') || '').toLowerCase();
247
+ const role = source === 'user' ? 'User' : 'Copilot';
248
+ if (role === 'User') {
249
+ const text = turn.innerText || turn.textContent;
250
+ if (text && text.trim()) {
251
+ messages.push({ role: 'User', content: text.trim() });
252
+ }
253
+ } else {
254
+ const html = processAiElement(turn);
255
+ const markdown = convertToMarkdown(html);
256
+ if (markdown && markdown.trim()) {
257
+ messages.push({ role: 'Copilot', content: markdown });
258
+ }
259
+ }
260
+ });
261
+ }
262
+ }
263
+
264
+ // Tier 6: React [data-turn-id] layout
265
+ if (!messages.length) {
266
+ const turnNodes = document.querySelectorAll('[data-turn-id]');
267
+ turnNodes.forEach((node) => {
268
+ const roleAttr = (
269
+ node.getAttribute('data-turn-role') ||
270
+ node.getAttribute('data-author') ||
271
+ ''
272
+ ).toLowerCase();
273
+ const role = roleAttr.includes('user') ? 'User' : 'Copilot';
274
+
275
+ if (role === 'User') {
276
+ const text = node.innerText || node.textContent;
277
+ if (text && text.trim()) {
278
+ messages.push({ role: 'User', content: text.trim() });
279
+ }
280
+ } else {
281
+ const html = processAiElement(node);
282
+ const markdown = convertToMarkdown(html);
283
+ if (markdown && markdown.trim()) {
284
+ messages.push({ role: 'Copilot', content: markdown });
285
+ }
286
+ }
287
+ });
288
+ }
289
+
290
+ // Tier 7: Universal role="article" scanner for Copilot
291
+ if (!messages.length) {
292
+ const articles = Array.from(document.querySelectorAll('[role="article"]'));
293
+ articles.forEach((art) => {
294
+ const headingText = (
295
+ art.querySelector('h1, h2, h3, h4, h5, h6')?.textContent || ''
296
+ ).toLowerCase();
297
+ const ariaLabel = (
298
+ art.getAttribute('aria-label') ||
299
+ art.getAttribute('aria-labelledby') ||
300
+ ''
301
+ ).toLowerCase();
302
+ const className = typeof art.className === 'string' ? art.className : '';
303
+
304
+ const isUser =
305
+ headingText.includes('you said') ||
306
+ ariaLabel.includes('user-message') ||
307
+ ariaLabel.includes('you said') ||
308
+ className.includes('UserMessage') ||
309
+ art.matches('[data-testid="chatQuestion"], [data-testid="user-message"]');
310
+
311
+ const isCopilot =
312
+ headingText.includes('copilot said') ||
313
+ ariaLabel.includes('copilot-message') ||
314
+ ariaLabel.includes('copilot said') ||
315
+ className.includes('CopilotMessage') ||
316
+ art.matches('[data-testid="copilot-message-div"], [data-testid="ai-message"]');
317
+
318
+ if (isUser) {
319
+ const clone = art.cloneNode(true);
320
+ clone
321
+ .querySelectorAll('h1, h2, h3, h4, h5, h6, .sr-only, button')
322
+ .forEach((el) => el.remove());
323
+ const text = clone.innerText || clone.textContent;
324
+ if (text && text.trim()) {
325
+ messages.push({ role: 'User', content: text.trim() });
326
+ }
327
+ } else if (isCopilot) {
328
+ const targetNode =
329
+ art.querySelector('[data-testid="markdown-reply"]') ||
330
+ art.querySelector('.fai-CopilotMessage__content') ||
331
+ art.querySelector('[data-testid="ai-message-body"]') ||
332
+ art;
333
+ const html = processAiElement(targetNode);
334
+ const markdown = convertToMarkdown(html);
335
+ if (markdown && markdown.trim()) {
336
+ messages.push({ role: 'Copilot', content: markdown });
337
+ }
338
+ }
339
+ });
340
+ }
341
+
342
+ // Tier 8: Check embedded iframe documents (e.g. Edge Sidebar panel iframe)
343
+ if (!messages.length) {
344
+ const iframes = Array.from(document.querySelectorAll('iframe'));
345
+ for (const iframe of iframes) {
346
+ try {
347
+ const iframeDoc = iframe.contentDocument || iframe.contentWindow?.document;
348
+ if (iframeDoc) {
349
+ const iframeCib = extractFromCibSerp(iframeDoc);
350
+ if (iframeCib.length) {
351
+ messages.push(...iframeCib);
352
+ break;
353
+ }
354
+ const iframeTurns = Array.from(
355
+ iframeDoc.querySelectorAll(
356
+ '[data-testid="chatQuestion"], [data-testid="copilot-message-div"], [data-content="user-message"], [data-content="ai-message"]',
357
+ ),
358
+ );
359
+ if (iframeTurns.length) {
360
+ iframeTurns.forEach((node) => {
361
+ const dataContent = node.getAttribute('data-content') || '';
362
+ const testId = node.getAttribute('data-testid') || '';
363
+ const isUser =
364
+ testId === 'chatQuestion' ||
365
+ dataContent === 'user-message' ||
366
+ testId === 'user-message';
367
+ if (isUser) {
368
+ const targetNode =
369
+ node.querySelector(
370
+ '[data-testid="chatOutput"], .fai-UserMessage__message, [data-content="user-message"]',
371
+ ) || node;
372
+ const text = targetNode.innerText || targetNode.textContent;
373
+ if (text && text.trim()) messages.push({ role: 'User', content: text.trim() });
374
+ } else {
375
+ const targetNode =
376
+ node.querySelector('[data-testid="markdown-reply"]') ||
377
+ node.querySelector('[data-testid="ai-message-body"]') ||
378
+ node;
379
+ const html = processAiElement(targetNode);
380
+ const markdown = convertToMarkdown(html);
381
+ if (markdown && markdown.trim()) {
382
+ messages.push({ role: 'Copilot', content: markdown });
383
+ }
384
+ }
385
+ });
386
+ if (messages.length) break;
387
+ }
388
+ }
389
+ } catch {
390
+ // Ignore cross-origin iframe security restrictions
391
+ }
392
+ }
393
+ }
394
+
395
+ // Fallback title generation if default title is generic
396
+ if (title === 'Copilot Conversation' && messages.length > 0 && messages[0].role === 'User') {
397
+ const firstPrompt = messages[0].content.split('\n')[0].trim();
398
+ if (firstPrompt) {
399
+ title = firstPrompt.length > 40 ? `${firstPrompt.slice(0, 40)}...` : firstPrompt;
400
+ }
401
+ }
402
+
403
+ const currentUrl =
404
+ typeof window !== 'undefined' && window.location ? window.location.href || '' : '';
405
+ const metadata = {
406
+ Source: 'Copilot',
407
+ Date: new Date().toLocaleString(),
408
+ Link: currentUrl,
409
+ };
410
+
411
+ return { title, messages, url: currentUrl, metadata };
412
+ }
413
+ }
package/ai/deepseek.js ADDED
@@ -0,0 +1,155 @@
1
+ import { ChatParser } from './base.js';
2
+ import { convertToMarkdown } from '../utils/html-to-markdown.js';
3
+
4
+ function getUserToken() {
5
+ try {
6
+ if (typeof localStorage === 'undefined') return null;
7
+ const raw = localStorage.getItem('userToken');
8
+ if (!raw) return null;
9
+ try {
10
+ const parsed = JSON.parse(raw);
11
+ return parsed?.value || parsed || null;
12
+ } catch {
13
+ return raw;
14
+ }
15
+ } catch {
16
+ return null;
17
+ }
18
+ }
19
+
20
+ function getConversationId() {
21
+ try {
22
+ if (typeof window === 'undefined' || !window.location) return null;
23
+ const path = window.location.pathname || window.location.href || '';
24
+ return (
25
+ path.match(/\/chat\/s\/([a-f0-9-]+)/)?.[1] ??
26
+ path.match(/\/a\/chat\/s\/([a-f0-9-]+)/)?.[1] ??
27
+ null
28
+ );
29
+ } catch {
30
+ return null;
31
+ }
32
+ }
33
+
34
+ async function fetchDeepSeekConversation(sessionId, token) {
35
+ const url = `https://chat.deepseek.com/api/v0/chat/history_messages?chat_session_id=${sessionId}&cache_version=0`;
36
+ const response = await fetch(url, {
37
+ method: 'GET',
38
+ credentials: 'include',
39
+ headers: {
40
+ 'Content-Type': 'application/json',
41
+ Authorization: `Bearer ${token}`,
42
+ },
43
+ });
44
+
45
+ if (!response.ok) {
46
+ throw new Error(`DeepSeek API request failed: ${response.status}`);
47
+ }
48
+
49
+ const json = await response.json();
50
+ const bizData = json?.data?.biz_data;
51
+ const chatMessages = bizData?.chat_messages || [];
52
+ const currentMsgId = bizData?.chat_session?.current_message_id;
53
+
54
+ if (!chatMessages.length || currentMsgId == null) {
55
+ return [];
56
+ }
57
+
58
+ const messageMap = new Map();
59
+ chatMessages.forEach((msg) => {
60
+ if (msg && msg.message_id != null) {
61
+ messageMap.set(msg.message_id, msg);
62
+ }
63
+ });
64
+
65
+ const branch = [];
66
+ let currentId = currentMsgId;
67
+ while (currentId != null && messageMap.has(currentId)) {
68
+ const msgNode = messageMap.get(currentId);
69
+ branch.push(msgNode);
70
+ currentId = msgNode.parent_id ?? null;
71
+ }
72
+
73
+ branch.reverse();
74
+
75
+ return branch
76
+ .map((msgNode) => {
77
+ const isUser = msgNode.role === 'USER' || msgNode.role === 'user';
78
+ const role = isUser ? 'User' : 'DeepSeek';
79
+ const content = msgNode.content || msgNode.text || '';
80
+ return { role, content: content.trim() };
81
+ })
82
+ .filter((msg) => msg.content.length > 0);
83
+ }
84
+
85
+ export class DeepSeekParser extends ChatParser {
86
+ name = 'DeepSeek';
87
+ isAvailable(url) {
88
+ return url.includes('chat.deepseek.com');
89
+ }
90
+
91
+ async parse() {
92
+ const title = document.title || 'DeepSeek Chat';
93
+
94
+ const currentUrl =
95
+ typeof window !== 'undefined' && window.location ? window.location.href || '' : '';
96
+ const metadata = {
97
+ Source: 'DeepSeek',
98
+ Date: new Date().toLocaleString(),
99
+ Link: currentUrl,
100
+ };
101
+
102
+ // Primary: API Extraction
103
+ try {
104
+ const token = getUserToken();
105
+ const sessionId = getConversationId();
106
+ if (token && sessionId) {
107
+ const apiMessages = await fetchDeepSeekConversation(sessionId, token);
108
+ if (apiMessages && apiMessages.length > 0) {
109
+ return { title, messages: apiMessages, url: currentUrl, metadata };
110
+ }
111
+ }
112
+ } catch (e) {
113
+ console.warn('[AI Exporter] DeepSeek API fetch failed, falling back to DOM:', e);
114
+ }
115
+
116
+ // Secondary: DOM Fallback
117
+ const messages = [];
118
+
119
+ // Selectors from research
120
+ const userSelector = '.fbb737a4';
121
+ const assistantSelector = '.ds-markdown';
122
+
123
+ // We'll traverse the DOM to find these in order
124
+ const allElements = document.querySelectorAll(`${userSelector}, ${assistantSelector}`);
125
+
126
+ allElements.forEach((el) => {
127
+ let role = 'Unknown';
128
+ if (el.matches(userSelector)) {
129
+ role = 'User';
130
+ } else if (el.matches(assistantSelector)) {
131
+ role = 'DeepSeek';
132
+ }
133
+
134
+ const text = convertToMarkdown(el);
135
+ if (text.trim()) {
136
+ messages.push({ role, content: text.trim() });
137
+ }
138
+ });
139
+
140
+ // Fallback if the specific classes fail (e.g. class name rotation)
141
+ if (messages.length === 0) {
142
+ const messageRows = document.querySelectorAll('.ds-message-row, .message-row');
143
+ messageRows.forEach((row) => {
144
+ const isUser = row.classList.contains('ds-user-message');
145
+ const role = isUser ? 'User' : 'DeepSeek';
146
+ const text = convertToMarkdown(row);
147
+ if (text.trim()) {
148
+ messages.push({ role, content: text.trim() });
149
+ }
150
+ });
151
+ }
152
+
153
+ return { title, messages, url: currentUrl, metadata };
154
+ }
155
+ }