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/notebooklm.js
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { ChatParser } from './base.js';
|
|
2
|
+
import { convertToMarkdown } from '../utils/html-to-markdown.js';
|
|
3
|
+
|
|
4
|
+
export class NotebookLMParser extends ChatParser {
|
|
5
|
+
name = 'NotebookLM';
|
|
6
|
+
isAvailable(url) {
|
|
7
|
+
return url.includes('notebooklm.google.com') || url.includes('notebook.google.com');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async parse() {
|
|
11
|
+
let title = '';
|
|
12
|
+
|
|
13
|
+
// Title extraction
|
|
14
|
+
const titleEl = document.querySelector(
|
|
15
|
+
'.title-container .title, .notebook-title-input, input[aria-label*="title" i], h1.notebook-title',
|
|
16
|
+
);
|
|
17
|
+
if (titleEl) {
|
|
18
|
+
title = (titleEl.value || titleEl.textContent || '').trim();
|
|
19
|
+
}
|
|
20
|
+
if (!title) {
|
|
21
|
+
title = (document.title || 'Gemini Notebook')
|
|
22
|
+
.replace(/\s*-\s*(Gemini Notebook|NotebookLM)$/i, '')
|
|
23
|
+
.trim();
|
|
24
|
+
}
|
|
25
|
+
title = title || 'NotebookLM Conversation';
|
|
26
|
+
|
|
27
|
+
const messages = [];
|
|
28
|
+
|
|
29
|
+
// Selectors for turn pairs or individual message cards in NotebookLM
|
|
30
|
+
const messagePairs = Array.from(
|
|
31
|
+
document.querySelectorAll('.chat-message-pair, chat-message, .message-pair-container'),
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
// Filter to top-level containers
|
|
35
|
+
const topContainers = messagePairs.filter(
|
|
36
|
+
(el) => !messagePairs.some((parent) => parent !== el && parent.contains(el)),
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
if (topContainers.length > 0) {
|
|
40
|
+
topContainers.forEach((pair) => {
|
|
41
|
+
// User message
|
|
42
|
+
const userContainer = pair.querySelector(
|
|
43
|
+
'.from-user-container, .from-user-message-card-content',
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
// Model / Assistant response
|
|
47
|
+
const aiContainer = pair.querySelector(
|
|
48
|
+
'.to-user-container, .to-user-message-inner-content, labs-tailwind-doc-viewer',
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
if (userContainer) {
|
|
52
|
+
const contentEl =
|
|
53
|
+
userContainer.querySelector(
|
|
54
|
+
'.message-text-content, .md3-body-text, .from-user-message-inner-content',
|
|
55
|
+
) || userContainer;
|
|
56
|
+
|
|
57
|
+
const clone = contentEl.cloneNode(true);
|
|
58
|
+
// Clean up noise buttons & icons
|
|
59
|
+
clone
|
|
60
|
+
.querySelectorAll('button, mat-icon, .mat-mdc-button-touch-target')
|
|
61
|
+
.forEach((el) => el.remove());
|
|
62
|
+
|
|
63
|
+
const userText = convertToMarkdown(clone).trim();
|
|
64
|
+
if (userText) {
|
|
65
|
+
messages.push({ role: 'User', content: userText });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (aiContainer) {
|
|
70
|
+
const contentEl =
|
|
71
|
+
aiContainer.querySelector(
|
|
72
|
+
'labs-tailwind-doc-viewer, .message-text-content, .to-user-message-inner-content',
|
|
73
|
+
) || aiContainer;
|
|
74
|
+
|
|
75
|
+
const clone = contentEl.cloneNode(true);
|
|
76
|
+
// Format citation markers (e.g., button.citation-marker -> <span class="citation-ref">[1]</span>)
|
|
77
|
+
clone.querySelectorAll('button.citation-marker').forEach((btn) => {
|
|
78
|
+
const citeNum = btn.textContent.trim();
|
|
79
|
+
if (citeNum && typeof document !== 'undefined') {
|
|
80
|
+
const spanNode = document.createElement('span');
|
|
81
|
+
spanNode.className = 'citation-ref';
|
|
82
|
+
spanNode.textContent = ` [${citeNum}]`;
|
|
83
|
+
btn.replaceWith(spanNode);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// Clean up other UI noise (thumbs up/down, action buttons)
|
|
88
|
+
clone
|
|
89
|
+
.querySelectorAll('button, mat-icon, .mat-mdc-button-touch-target, .feedback-actions')
|
|
90
|
+
.forEach((el) => el.remove());
|
|
91
|
+
|
|
92
|
+
const aiText = convertToMarkdown(clone).trim();
|
|
93
|
+
if (aiText) {
|
|
94
|
+
messages.push({ role: 'NotebookLM', content: aiText });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Direct fallback if topContainers loop yielded no messages
|
|
101
|
+
if (messages.length === 0) {
|
|
102
|
+
const userContainers = Array.from(document.querySelectorAll('.from-user-container'));
|
|
103
|
+
const aiContainers = Array.from(document.querySelectorAll('.to-user-container'));
|
|
104
|
+
|
|
105
|
+
const maxLen = Math.max(userContainers.length, aiContainers.length);
|
|
106
|
+
for (let i = 0; i < maxLen; i++) {
|
|
107
|
+
if (userContainers[i]) {
|
|
108
|
+
const text = convertToMarkdown(userContainers[i]).trim();
|
|
109
|
+
if (text) messages.push({ role: 'User', content: text });
|
|
110
|
+
}
|
|
111
|
+
if (aiContainers[i]) {
|
|
112
|
+
const clone = aiContainers[i].cloneNode(true);
|
|
113
|
+
clone.querySelectorAll('button.citation-marker').forEach((btn) => {
|
|
114
|
+
const citeNum = btn.textContent.trim();
|
|
115
|
+
if (citeNum && typeof document !== 'undefined') {
|
|
116
|
+
const spanNode = document.createElement('span');
|
|
117
|
+
spanNode.className = 'citation-ref';
|
|
118
|
+
spanNode.textContent = ` [${citeNum}]`;
|
|
119
|
+
btn.replaceWith(spanNode);
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
clone.querySelectorAll('button, mat-icon').forEach((el) => el.remove());
|
|
123
|
+
|
|
124
|
+
const text = convertToMarkdown(clone).trim();
|
|
125
|
+
if (text) messages.push({ role: 'NotebookLM', content: text });
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const currentUrl =
|
|
131
|
+
typeof window !== 'undefined' && window.location ? window.location.href || '' : '';
|
|
132
|
+
const metadata = {
|
|
133
|
+
Source: 'NotebookLM',
|
|
134
|
+
Date: new Date().toLocaleString(),
|
|
135
|
+
Link: currentUrl,
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
return { title, messages, url: currentUrl, metadata };
|
|
139
|
+
}
|
|
140
|
+
}
|
package/ai/perplexity.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { ChatParser } from './base.js';
|
|
2
|
+
import { convertToMarkdown } from '../utils/html-to-markdown.js';
|
|
3
|
+
|
|
4
|
+
export class PerplexityParser extends ChatParser {
|
|
5
|
+
name = 'Perplexity';
|
|
6
|
+
isAvailable(url) {
|
|
7
|
+
return url.includes('perplexity.ai');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async parse() {
|
|
11
|
+
const rawTitle =
|
|
12
|
+
document.querySelector('.share-title-section h1')?.textContent ||
|
|
13
|
+
document.querySelector('h1')?.textContent ||
|
|
14
|
+
document.title ||
|
|
15
|
+
'Perplexity Search';
|
|
16
|
+
const title = rawTitle.trim().replace(/\s+/g, ' ');
|
|
17
|
+
|
|
18
|
+
const messages = [];
|
|
19
|
+
|
|
20
|
+
// Perplexity Container
|
|
21
|
+
const threadContainer = document.querySelector('.max-w-threadContentWidth') || document.body;
|
|
22
|
+
|
|
23
|
+
// Candidates for User Messages
|
|
24
|
+
const userSelectors = [
|
|
25
|
+
'h1.group\\/query',
|
|
26
|
+
'.group\\/query',
|
|
27
|
+
'.whitespace-pre-line.text-pretty',
|
|
28
|
+
'[data-testid="search-bar-input"]', // fallback for input? typically input is not the message display
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
// Candidates for Assistant Messages
|
|
32
|
+
const assistantSelectors = ['div[id^="markdown-content-"]', '.prose'];
|
|
33
|
+
|
|
34
|
+
// Strategy: Iterate children of thread container or find all matches in document
|
|
35
|
+
// Thread container is better to preserve order
|
|
36
|
+
|
|
37
|
+
// Let's select all potential message blocks within the thread container
|
|
38
|
+
const selectorString = [...userSelectors, ...assistantSelectors].join(', ');
|
|
39
|
+
const elements = threadContainer.querySelectorAll(selectorString);
|
|
40
|
+
|
|
41
|
+
// Helper to determine role
|
|
42
|
+
const getRole = (el) => {
|
|
43
|
+
for (const s of userSelectors) {
|
|
44
|
+
if (el.matches(s)) return 'User';
|
|
45
|
+
}
|
|
46
|
+
for (const s of assistantSelectors) {
|
|
47
|
+
if (el.matches(s)) return 'Perplexity';
|
|
48
|
+
}
|
|
49
|
+
return 'Unknown';
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const seenText = new Set();
|
|
53
|
+
|
|
54
|
+
elements.forEach((el) => {
|
|
55
|
+
// Perplexity nests things. Avoid duplicates if we selected a parent and a child.
|
|
56
|
+
// Also avoid "Related" section queries if possible (usually separate container, check parents?)
|
|
57
|
+
|
|
58
|
+
// Check if inside "related" or "sources"
|
|
59
|
+
if (el.closest('[class*="related"], [class*="sources"]')) return;
|
|
60
|
+
|
|
61
|
+
const role = getRole(el);
|
|
62
|
+
let text = convertToMarkdown(el);
|
|
63
|
+
text = text.trim();
|
|
64
|
+
|
|
65
|
+
if (!text || seenText.has(text)) return;
|
|
66
|
+
|
|
67
|
+
// Perplexity specific cleanup
|
|
68
|
+
// Remove "Sources" label text if it gets captured?
|
|
69
|
+
// Usually .prose contains the markdown answer.
|
|
70
|
+
|
|
71
|
+
seenText.add(text);
|
|
72
|
+
messages.push({ role, content: text });
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const currentUrl =
|
|
76
|
+
typeof window !== 'undefined' && window.location ? window.location.href || '' : '';
|
|
77
|
+
const metadata = {
|
|
78
|
+
Source: 'Perplexity',
|
|
79
|
+
Date: new Date().toLocaleString(),
|
|
80
|
+
Link: currentUrl,
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
return { title, messages, url: currentUrl, metadata };
|
|
84
|
+
}
|
|
85
|
+
}
|
package/ai/qwen.js
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { ChatParser } from './base.js';
|
|
2
|
+
import { convertToMarkdown } from '../utils/html-to-markdown.js';
|
|
3
|
+
|
|
4
|
+
export class QwenParser extends ChatParser {
|
|
5
|
+
name = 'Qwen';
|
|
6
|
+
isAvailable(url) {
|
|
7
|
+
return url.includes('qwen.ai');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async parse() {
|
|
11
|
+
// Try to get the actual chat title from multiple possible selectors
|
|
12
|
+
const titleSelectors = [
|
|
13
|
+
'.chat-item-drag-link-content-tip-text',
|
|
14
|
+
'.ant-tooltip-inner',
|
|
15
|
+
'input[placeholder*="title"]',
|
|
16
|
+
'.chat-title',
|
|
17
|
+
'h1',
|
|
18
|
+
'title',
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
let title = 'Qwen Chat';
|
|
22
|
+
for (const selector of titleSelectors) {
|
|
23
|
+
const element = document.querySelector(selector);
|
|
24
|
+
if (element) {
|
|
25
|
+
const text = element.textContent || element.value || element.innerText;
|
|
26
|
+
if (text && text.trim() && text !== document.title) {
|
|
27
|
+
title = text.trim();
|
|
28
|
+
break;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const messages = [];
|
|
34
|
+
|
|
35
|
+
// chat.qwen.ai uses specific class names
|
|
36
|
+
const chatMessages = document.querySelectorAll('.qwen-chat-message');
|
|
37
|
+
|
|
38
|
+
chatMessages.forEach((message) => {
|
|
39
|
+
const isUser = message.classList.contains('qwen-chat-message-user');
|
|
40
|
+
const role = isUser ? 'User' : 'Qwen';
|
|
41
|
+
|
|
42
|
+
let content = '';
|
|
43
|
+
let attachments = [];
|
|
44
|
+
|
|
45
|
+
if (isUser) {
|
|
46
|
+
// Extract attachments first
|
|
47
|
+
const fileItems = message.querySelectorAll('.index-module__file-message-document___OjWnc');
|
|
48
|
+
fileItems.forEach((item) => {
|
|
49
|
+
const fileNameEl = item.querySelector('.fileitem-file-name-text');
|
|
50
|
+
const fileExtEl = item.querySelector('.fileitem-file-name-ext');
|
|
51
|
+
const fileSizeEl = item.querySelector('.fileitem-file-size span');
|
|
52
|
+
|
|
53
|
+
if (fileNameEl && fileExtEl) {
|
|
54
|
+
const fileName = fileNameEl.textContent.trim();
|
|
55
|
+
const fileExt = fileExtEl.textContent.trim();
|
|
56
|
+
const fileSize = fileSizeEl ? fileSizeEl.textContent.trim() : '';
|
|
57
|
+
|
|
58
|
+
attachments.push({
|
|
59
|
+
name: fileName + fileExt,
|
|
60
|
+
size: fileSize,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// User messages are in .user-message-content
|
|
66
|
+
const userContent = message.querySelector('.user-message-content');
|
|
67
|
+
if (userContent) {
|
|
68
|
+
content = convertToMarkdown(userContent);
|
|
69
|
+
}
|
|
70
|
+
} else {
|
|
71
|
+
// Assistant messages are in .qwen-markdown elements
|
|
72
|
+
const markdownContent = message.querySelector('.qwen-markdown');
|
|
73
|
+
if (markdownContent) {
|
|
74
|
+
content = convertToMarkdown(markdownContent);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Add attachments to content if any exist
|
|
79
|
+
if (attachments.length > 0) {
|
|
80
|
+
const attachmentList = attachments
|
|
81
|
+
.map((att) => `- **${att.name}** (${att.size})`)
|
|
82
|
+
.join('\n');
|
|
83
|
+
content = content + '\n\n**Attachments:**\n' + attachmentList;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (content && content.trim()) {
|
|
87
|
+
messages.push({ role, content: content.trim() });
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const currentUrl =
|
|
92
|
+
typeof window !== 'undefined' && window.location ? window.location.href || '' : '';
|
|
93
|
+
const metadata = {
|
|
94
|
+
Source: 'Qwen',
|
|
95
|
+
Date: new Date().toLocaleString(),
|
|
96
|
+
Link: currentUrl,
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
return { title, messages, url: currentUrl, metadata };
|
|
100
|
+
}
|
|
101
|
+
}
|
package/ai/z_ai.js
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { ChatParser } from './base.js';
|
|
2
|
+
import { convertToMarkdown } from '../utils/html-to-markdown.js';
|
|
3
|
+
|
|
4
|
+
export class ZAiParser extends ChatParser {
|
|
5
|
+
name = 'Z.ai';
|
|
6
|
+
isAvailable(url) {
|
|
7
|
+
return url.includes('chat.z.ai');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async parse() {
|
|
11
|
+
let title = '';
|
|
12
|
+
const titleEl = document.querySelector('title');
|
|
13
|
+
if (titleEl) {
|
|
14
|
+
title = titleEl.textContent.trim().replace(/\s+/g, ' ');
|
|
15
|
+
}
|
|
16
|
+
if (!title && document.title) {
|
|
17
|
+
title = document.title.trim().replace(/\s+/g, ' ');
|
|
18
|
+
}
|
|
19
|
+
title = title || 'Z.ai Chat';
|
|
20
|
+
const messages = [];
|
|
21
|
+
|
|
22
|
+
// Selectors for z.ai messages
|
|
23
|
+
const userSelector = '.chat-user';
|
|
24
|
+
const assistantSelector = '.chat-assistant';
|
|
25
|
+
|
|
26
|
+
// We'll traverse the DOM to find these in order
|
|
27
|
+
const allElements = document.querySelectorAll(`${userSelector}, ${assistantSelector}`);
|
|
28
|
+
|
|
29
|
+
allElements.forEach((el) => {
|
|
30
|
+
let role = 'Unknown';
|
|
31
|
+
let contentEl = null;
|
|
32
|
+
|
|
33
|
+
if (el.matches(userSelector)) {
|
|
34
|
+
role = 'User';
|
|
35
|
+
// Select user message text block (excluding edit/copy buttons)
|
|
36
|
+
contentEl = el.querySelector('div.relative.overflow-hidden') || el;
|
|
37
|
+
} else if (el.matches(assistantSelector)) {
|
|
38
|
+
role = 'Z.ai';
|
|
39
|
+
// Select assistant message content wrapper (excluding copy/regenerate buttons)
|
|
40
|
+
const rawContentEl =
|
|
41
|
+
el.querySelector('#response-content-container') ||
|
|
42
|
+
el.querySelector('.markdown-prose') ||
|
|
43
|
+
el;
|
|
44
|
+
const contentElClone = rawContentEl.cloneNode(true);
|
|
45
|
+
|
|
46
|
+
// Preprocess CodeMirror 6 code blocks into standard HTML <pre><code> structures
|
|
47
|
+
contentElClone.querySelectorAll('.cm-editor').forEach((cmEditor) => {
|
|
48
|
+
// Detect language from class names of the parent language container
|
|
49
|
+
const languageWrapper = cmEditor.closest('[class*="language-"]');
|
|
50
|
+
let language = '';
|
|
51
|
+
if (languageWrapper) {
|
|
52
|
+
const classList = Array.from(languageWrapper.classList);
|
|
53
|
+
const langClass = classList.find((cls) => cls.startsWith('language-'));
|
|
54
|
+
if (langClass) {
|
|
55
|
+
language = langClass.replace('language-', '');
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Extract the lines from CodeMirror editor view
|
|
60
|
+
const lines = Array.from(cmEditor.querySelectorAll('.cm-line'));
|
|
61
|
+
const codeText = lines.map((line) => line.textContent).join('\n');
|
|
62
|
+
|
|
63
|
+
// Create new pre and code tags using the clone's owner document context
|
|
64
|
+
const ownerDoc = cmEditor.ownerDocument || document;
|
|
65
|
+
const pre = ownerDoc.createElement('pre');
|
|
66
|
+
const code = ownerDoc.createElement('code');
|
|
67
|
+
if (language) {
|
|
68
|
+
code.className = `language-${language}`;
|
|
69
|
+
}
|
|
70
|
+
code.textContent = codeText;
|
|
71
|
+
pre.appendChild(code);
|
|
72
|
+
|
|
73
|
+
// Replace the enclosing language wrapper or cm-editor with the pre element
|
|
74
|
+
const targetToReplace = languageWrapper || cmEditor;
|
|
75
|
+
if (targetToReplace && targetToReplace.parentNode) {
|
|
76
|
+
targetToReplace.parentNode.replaceChild(pre, targetToReplace);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
contentEl = contentElClone;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (contentEl) {
|
|
84
|
+
const text = convertToMarkdown(contentEl);
|
|
85
|
+
if (text.trim()) {
|
|
86
|
+
messages.push({ role, content: text.trim() });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const currentUrl =
|
|
92
|
+
typeof window !== 'undefined' && window.location ? window.location.href || '' : '';
|
|
93
|
+
const metadata = {
|
|
94
|
+
Source: 'Z.ai',
|
|
95
|
+
Date: new Date().toLocaleString(),
|
|
96
|
+
Link: currentUrl,
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
return { title, messages, url: currentUrl, metadata };
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { AI_CHAT_DOMAINS, URL_PATTERNS } from './domains.js';
|
|
2
|
+
import { ChatGPTParser } from '../ai/chatgpt.js';
|
|
3
|
+
import { ClaudeParser } from '../ai/claude.js';
|
|
4
|
+
import { GeminiParser } from '../ai/gemini.js';
|
|
5
|
+
import { CopilotParser } from '../ai/copilot.js';
|
|
6
|
+
import { DeepSeekParser } from '../ai/deepseek.js';
|
|
7
|
+
import { MetaParser } from '../ai/meta.js';
|
|
8
|
+
import { MistralParser } from '../ai/mistral.js';
|
|
9
|
+
import { PerplexityParser } from '../ai/perplexity.js';
|
|
10
|
+
import { QwenParser } from '../ai/qwen.js';
|
|
11
|
+
import { LumoParser } from '../ai/lumo.js';
|
|
12
|
+
import { ZAiParser } from '../ai/z_ai.js';
|
|
13
|
+
import { GoogleAIStudioParser } from '../ai/google_ai_studio.js';
|
|
14
|
+
import { NotebookLMParser } from '../ai/notebooklm.js';
|
|
15
|
+
import { GoogleSearchAIParser } from '../ai/google_search_ai.js';
|
|
16
|
+
import { GeminiCloudAssistParser } from '../ai/gemini_cloud_assist.js';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Ordered list of parsers. First match wins.
|
|
20
|
+
* The order roughly reflects platform popularity / specificity.
|
|
21
|
+
*/
|
|
22
|
+
export const parsers = [
|
|
23
|
+
new ChatGPTParser(),
|
|
24
|
+
new ClaudeParser(),
|
|
25
|
+
new GeminiParser(),
|
|
26
|
+
new CopilotParser(),
|
|
27
|
+
new PerplexityParser(),
|
|
28
|
+
new DeepSeekParser(),
|
|
29
|
+
new QwenParser(),
|
|
30
|
+
new MetaParser(),
|
|
31
|
+
new MistralParser(),
|
|
32
|
+
new LumoParser(),
|
|
33
|
+
new ZAiParser(),
|
|
34
|
+
new GoogleAIStudioParser(),
|
|
35
|
+
new NotebookLMParser(),
|
|
36
|
+
new GoogleSearchAIParser(),
|
|
37
|
+
new GeminiCloudAssistParser(),
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Detect whether a URL belongs to a known AI chat platform.
|
|
42
|
+
*
|
|
43
|
+
* @param {string} url
|
|
44
|
+
* @returns {{ type: 'ai-chat', platform: string, parser: import('../ai/base.js').ChatParser } | null}
|
|
45
|
+
*/
|
|
46
|
+
export function detectPlatform(url) {
|
|
47
|
+
if (!url) return null;
|
|
48
|
+
|
|
49
|
+
// Try simple domain matching first
|
|
50
|
+
try {
|
|
51
|
+
const parsed = new URL(url);
|
|
52
|
+
const hostname = parsed.hostname.toLowerCase();
|
|
53
|
+
const domainMatch = AI_CHAT_DOMAINS.some(
|
|
54
|
+
(domain) => hostname === domain || hostname.endsWith(`.${domain}`),
|
|
55
|
+
);
|
|
56
|
+
if (domainMatch) {
|
|
57
|
+
const parser = parsers.find((p) => p.isAvailable(url));
|
|
58
|
+
if (parser) {
|
|
59
|
+
return { type: 'ai-chat', platform: parser.getPlatformName(), parser };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
} catch {
|
|
63
|
+
// Invalid URL
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Try regex / complex URL patterns
|
|
67
|
+
for (const pattern of URL_PATTERNS) {
|
|
68
|
+
if (pattern.test(url)) {
|
|
69
|
+
const parser = parsers.find((p) => p.isAvailable(url));
|
|
70
|
+
if (parser) {
|
|
71
|
+
return { type: 'ai-chat', platform: parser.getPlatformName(), parser };
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Quick check: is this URL an AI chat page?
|
|
81
|
+
* Faster than detectPlatform() when you only need a boolean.
|
|
82
|
+
*
|
|
83
|
+
* @param {string} url
|
|
84
|
+
* @returns {boolean}
|
|
85
|
+
*/
|
|
86
|
+
export function isAiChatUrl(url) {
|
|
87
|
+
if (!url) return false;
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
const parsed = new URL(url);
|
|
91
|
+
const hostname = parsed.hostname.toLowerCase();
|
|
92
|
+
if (AI_CHAT_DOMAINS.some((domain) => hostname === domain || hostname.endsWith(`.${domain}`))) {
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
} catch {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return URL_PATTERNS.some((pattern) => pattern.test(url));
|
|
100
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI chat platform domain definitions.
|
|
3
|
+
* Centralised so detection logic and UI (e.g. Decant's tip banner) share the same list.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const AI_CHAT_DOMAINS = [
|
|
7
|
+
'chatgpt.com',
|
|
8
|
+
'claude.ai',
|
|
9
|
+
'gemini.google.com',
|
|
10
|
+
'chat.deepseek.com',
|
|
11
|
+
'perplexity.ai',
|
|
12
|
+
'chat.qwen.ai',
|
|
13
|
+
'qwen.ai',
|
|
14
|
+
'chat.mistral.ai',
|
|
15
|
+
'copilot.microsoft.com',
|
|
16
|
+
'lumo.proton.me',
|
|
17
|
+
'meta.ai',
|
|
18
|
+
'aistudio.google.com',
|
|
19
|
+
'notebooklm.google.com',
|
|
20
|
+
'notebook.google.com',
|
|
21
|
+
'chat.z.ai',
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Additional URL patterns that don't fit simple domain matching.
|
|
26
|
+
* Each entry: { test: (url) => boolean, platform: string }
|
|
27
|
+
*/
|
|
28
|
+
export const URL_PATTERNS = [
|
|
29
|
+
{
|
|
30
|
+
test: (url) => url.includes('copilot.microsoft.com') || url.includes('copilot.com') ||
|
|
31
|
+
url.includes('copilot.cloud.microsoft') || url.includes('m365.cloud.microsoft') ||
|
|
32
|
+
url.includes('m365.microsoft.com') || url.includes('bing.com/chat') ||
|
|
33
|
+
url.includes('bing.com/copilot') || url.includes('bing.com/copilotsearch') ||
|
|
34
|
+
url.includes('edgeservices.bing.com'),
|
|
35
|
+
platform: 'copilot',
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
test: (url) => /google\.[a-z.]+\/search/.test(url),
|
|
39
|
+
platform: 'google-search-ai',
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
test: (url) => url.includes('console.cloud.google.com/gemini'),
|
|
43
|
+
platform: 'gemini-cloud-assist',
|
|
44
|
+
},
|
|
45
|
+
];
|