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
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { ChatParser } from './base.js';
|
|
2
|
+
import { convertToMarkdown } from '../utils/html-to-markdown.js';
|
|
3
|
+
|
|
4
|
+
export class GeminiCloudAssistParser extends ChatParser {
|
|
5
|
+
name = 'Gemini Cloud Assist';
|
|
6
|
+
isAvailable(url) {
|
|
7
|
+
return url.includes('console.cloud.google.com/gemini');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async parse() {
|
|
11
|
+
const activeItem = document.querySelector(
|
|
12
|
+
'mat-list-item.mdc-list-item--activated span.cfc-flex-grow-content',
|
|
13
|
+
);
|
|
14
|
+
let title = activeItem?.textContent?.trim() || '';
|
|
15
|
+
|
|
16
|
+
// Fallback if no active item
|
|
17
|
+
if (!title) {
|
|
18
|
+
const firstUserMsg = document.querySelector('.aic-user-message-text')?.textContent?.trim();
|
|
19
|
+
if (firstUserMsg) {
|
|
20
|
+
title = firstUserMsg.length > 50 ? firstUserMsg.substring(0, 50) + '...' : firstUserMsg;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (!title) {
|
|
25
|
+
title = document.title || 'Gemini Cloud Assist';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Clean up title whitespace
|
|
29
|
+
title = title.replace(/\s+/g, ' ').trim();
|
|
30
|
+
|
|
31
|
+
const messages = [];
|
|
32
|
+
|
|
33
|
+
// Find all turns inside aic-conversation or standard turn containers
|
|
34
|
+
const turns = document.querySelectorAll('aic-conversation .aic-turn, .aic-turn');
|
|
35
|
+
|
|
36
|
+
turns.forEach((turn) => {
|
|
37
|
+
// 1. Process user message
|
|
38
|
+
const userEl = turn.querySelector('.aic-user-message');
|
|
39
|
+
if (userEl) {
|
|
40
|
+
const textEl = userEl.querySelector('.aic-user-message-text') || userEl;
|
|
41
|
+
let text = convertToMarkdown(textEl).trim();
|
|
42
|
+
if (text) {
|
|
43
|
+
messages.push({
|
|
44
|
+
role: 'User',
|
|
45
|
+
content: text,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 2. Process assistant message
|
|
51
|
+
const agentEl = turn.querySelector('aic-agent-entry');
|
|
52
|
+
if (agentEl) {
|
|
53
|
+
// Prefer the actual markdown renderer content if present, to avoid feedback actions/buttons noise
|
|
54
|
+
const markdownEl =
|
|
55
|
+
agentEl.querySelector('.ai-markdown-artifact-renderer') ||
|
|
56
|
+
agentEl.querySelector('.aic-markdown-renderer-container') ||
|
|
57
|
+
agentEl.querySelector('.aic-agent-content') ||
|
|
58
|
+
agentEl;
|
|
59
|
+
|
|
60
|
+
// Clone the element to safely strip unwanted components
|
|
61
|
+
const clone = markdownEl.cloneNode(true);
|
|
62
|
+
// Remove thumb up/down feedback buttons or loader/expander elements
|
|
63
|
+
clone
|
|
64
|
+
.querySelectorAll(
|
|
65
|
+
'button, aic-feedback-actions, .aic-agent-thoughts-container, aic-loading-indicator',
|
|
66
|
+
)
|
|
67
|
+
.forEach((el) => {
|
|
68
|
+
el.remove();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
let text = convertToMarkdown(clone).trim();
|
|
72
|
+
if (text) {
|
|
73
|
+
messages.push({
|
|
74
|
+
role: 'Model',
|
|
75
|
+
content: text,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const currentUrl =
|
|
82
|
+
typeof window !== 'undefined' && window.location ? window.location.href || '' : '';
|
|
83
|
+
const metadata = {
|
|
84
|
+
Source: 'Gemini Cloud Assist',
|
|
85
|
+
Date: new Date().toLocaleString(),
|
|
86
|
+
Link: currentUrl,
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
return { title, messages, url: currentUrl, metadata };
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { ChatParser } from './base.js';
|
|
2
|
+
import { convertToMarkdown } from '../utils/html-to-markdown.js';
|
|
3
|
+
|
|
4
|
+
export class GoogleAIStudioParser extends ChatParser {
|
|
5
|
+
name = 'Google AI Studio';
|
|
6
|
+
isAvailable(url) {
|
|
7
|
+
return url.includes('aistudio.google.com');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async parse() {
|
|
11
|
+
let title = '';
|
|
12
|
+
|
|
13
|
+
// Title extraction strategies
|
|
14
|
+
const titleInput = document.querySelector(
|
|
15
|
+
'input[aria-label*="prompt name" i], input[aria-label*="title" i], .prompt-title',
|
|
16
|
+
);
|
|
17
|
+
if (titleInput && titleInput.value) {
|
|
18
|
+
title = titleInput.value.trim();
|
|
19
|
+
}
|
|
20
|
+
if (!title) {
|
|
21
|
+
const headerTitle = document.querySelector('.title-text, h1, .prompt-name');
|
|
22
|
+
if (headerTitle) {
|
|
23
|
+
title = headerTitle.textContent.trim();
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
if (!title) {
|
|
27
|
+
title = document.title || 'Google AI Studio Chat';
|
|
28
|
+
}
|
|
29
|
+
title = title.replace(/\s+/g, ' ').trim();
|
|
30
|
+
|
|
31
|
+
const messages = [];
|
|
32
|
+
|
|
33
|
+
// Selectors for chat turns
|
|
34
|
+
const allTurnElements = Array.from(
|
|
35
|
+
document.querySelectorAll('ms-chat-turn, .chat-turn-container, ms-prompt-editor'),
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
// Filter to top-level turn elements only (avoiding child elements nested in parent turn containers)
|
|
39
|
+
const turnElements = allTurnElements.filter(
|
|
40
|
+
(el) => !allTurnElements.some((parent) => parent !== el && parent.contains(el)),
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
if (turnElements.length > 0) {
|
|
44
|
+
turnElements.forEach((turn) => {
|
|
45
|
+
// User prompt element
|
|
46
|
+
const userEl =
|
|
47
|
+
turn.querySelector('.user-prompt, .user-prompt-container, .user-message') ||
|
|
48
|
+
(turn.tagName === 'MS-PROMPT-EDITOR' ? turn : null);
|
|
49
|
+
|
|
50
|
+
// Model output element
|
|
51
|
+
const aiEl = turn.querySelector(
|
|
52
|
+
'.model-response-text, .model-output, ms-model-output, .model-prompt-container',
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
if (userEl) {
|
|
56
|
+
const contentEl = userEl.querySelector('.turn-content, .prompt-text, textarea') || userEl;
|
|
57
|
+
let text;
|
|
58
|
+
if (contentEl.tagName === 'TEXTAREA') {
|
|
59
|
+
text = contentEl.value || contentEl.textContent || '';
|
|
60
|
+
} else {
|
|
61
|
+
text = convertToMarkdown(contentEl);
|
|
62
|
+
}
|
|
63
|
+
text = text.trim();
|
|
64
|
+
if (text) {
|
|
65
|
+
messages.push({ role: 'User', content: text });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (aiEl) {
|
|
70
|
+
const clone = aiEl.cloneNode(true);
|
|
71
|
+
// Remove noise elements like copy buttons or action menus
|
|
72
|
+
clone
|
|
73
|
+
.querySelectorAll(
|
|
74
|
+
'button, .mat-expansion-panel-header, .author-label, .timestamp, ms-prompt-options-menu, .navigator-container',
|
|
75
|
+
)
|
|
76
|
+
.forEach((el) => el.remove());
|
|
77
|
+
|
|
78
|
+
let text = convertToMarkdown(clone).trim();
|
|
79
|
+
if (text && text !== 'Thinking...' && !text.includes('model_thought_output')) {
|
|
80
|
+
messages.push({ role: 'Google AI Studio', content: text });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Fallback if turnElements linear loop yielded no messages
|
|
87
|
+
if (messages.length === 0) {
|
|
88
|
+
const userPrompts = document.querySelectorAll('.user-prompt, .user-prompt-container');
|
|
89
|
+
const modelOutputs = document.querySelectorAll(
|
|
90
|
+
'.model-response-text, ms-model-output, .model-output',
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
const maxLen = Math.max(userPrompts.length, modelOutputs.length);
|
|
94
|
+
for (let i = 0; i < maxLen; i++) {
|
|
95
|
+
if (userPrompts[i]) {
|
|
96
|
+
const text = (userPrompts[i].value || convertToMarkdown(userPrompts[i])).trim();
|
|
97
|
+
if (text) messages.push({ role: 'User', content: text });
|
|
98
|
+
}
|
|
99
|
+
if (modelOutputs[i]) {
|
|
100
|
+
const text = convertToMarkdown(modelOutputs[i]).trim();
|
|
101
|
+
if (text) messages.push({ role: 'Google AI Studio', content: text });
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const currentUrl =
|
|
107
|
+
typeof window !== 'undefined' && window.location ? window.location.href || '' : '';
|
|
108
|
+
const metadata = {
|
|
109
|
+
Source: 'Google AI Studio',
|
|
110
|
+
Date: new Date().toLocaleString(),
|
|
111
|
+
Link: currentUrl,
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
return { title, messages, url: currentUrl, metadata };
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { ChatParser } from './base.js';
|
|
2
|
+
import { convertToMarkdown } from '../utils/html-to-markdown.js';
|
|
3
|
+
|
|
4
|
+
export class GoogleSearchAIParser extends ChatParser {
|
|
5
|
+
name = 'Google Search AI';
|
|
6
|
+
isAvailable(url) {
|
|
7
|
+
return /google\.[a-z.]+\/search/.test(url);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async parse() {
|
|
11
|
+
const title = document.title || 'Google Search AI Overview';
|
|
12
|
+
const messages = [];
|
|
13
|
+
|
|
14
|
+
// 1. Extract the user's queries
|
|
15
|
+
const queries = [];
|
|
16
|
+
const queryElements = document.querySelectorAll('.sUKAcb');
|
|
17
|
+
queryElements.forEach((el) => {
|
|
18
|
+
// Clone the element to avoid mutating the live DOM
|
|
19
|
+
const clone = el.cloneNode(true);
|
|
20
|
+
const prefixEl = clone.querySelector('.iMqumd');
|
|
21
|
+
if (prefixEl) {
|
|
22
|
+
prefixEl.remove();
|
|
23
|
+
}
|
|
24
|
+
let text = clone.textContent || '';
|
|
25
|
+
// Fallback regex cleanup if needed
|
|
26
|
+
text = text.replace(/^\s*You\s+said:\s*/i, '').trim();
|
|
27
|
+
if (text) {
|
|
28
|
+
queries.push(text);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// Fallback to single-turn query extraction if no turn query elements are found
|
|
33
|
+
if (queries.length === 0) {
|
|
34
|
+
let userQuery = '';
|
|
35
|
+
const uqEl = document.querySelector('[data-uq]');
|
|
36
|
+
if (uqEl) {
|
|
37
|
+
userQuery = uqEl.getAttribute('data-uq');
|
|
38
|
+
}
|
|
39
|
+
if (!userQuery) {
|
|
40
|
+
const qEl = document.querySelector('[data-q]');
|
|
41
|
+
if (qEl) {
|
|
42
|
+
userQuery = qEl.getAttribute('data-q');
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (!userQuery) {
|
|
46
|
+
try {
|
|
47
|
+
const url = new URL(window.location.href);
|
|
48
|
+
userQuery = url.searchParams.get('q');
|
|
49
|
+
} catch {
|
|
50
|
+
// Ignore invalid URLs
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (!userQuery) {
|
|
54
|
+
const textarea =
|
|
55
|
+
document.querySelector('textarea.gLFyf') ||
|
|
56
|
+
document.querySelector('textarea.ITIRGe') ||
|
|
57
|
+
document.querySelector('input[name="q"]');
|
|
58
|
+
if (textarea) {
|
|
59
|
+
userQuery = textarea.value || textarea.textContent;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (userQuery) {
|
|
63
|
+
queries.push(userQuery.trim());
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// 2. Extract SGE / AI Overview responses
|
|
68
|
+
const responseContainers = [];
|
|
69
|
+
const turnElements = document.querySelectorAll('[data-scope-id="turn"]');
|
|
70
|
+
if (turnElements.length > 0) {
|
|
71
|
+
turnElements.forEach((turnEl) => {
|
|
72
|
+
const resEl =
|
|
73
|
+
turnEl.querySelector('[data-container-id="main-col"]') ||
|
|
74
|
+
turnEl.querySelector('[data-container-id="model-response-placeholder"]');
|
|
75
|
+
if (resEl) {
|
|
76
|
+
responseContainers.push(resEl);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
} else {
|
|
80
|
+
// Fallback for pages without data-scope-id="turn"
|
|
81
|
+
const resEl =
|
|
82
|
+
document.querySelector('[data-container-id="main-col"]') ||
|
|
83
|
+
document.querySelector('[data-container-id="model-response-placeholder"]');
|
|
84
|
+
if (resEl) {
|
|
85
|
+
responseContainers.push(resEl);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Pair queries and responses in order
|
|
90
|
+
const minLength = Math.min(queries.length, responseContainers.length);
|
|
91
|
+
for (let i = 0; i < minLength; i++) {
|
|
92
|
+
messages.push({ role: 'User', content: queries[i].trim() });
|
|
93
|
+
const text = convertToMarkdown(responseContainers[i]);
|
|
94
|
+
if (text.trim()) {
|
|
95
|
+
messages.push({ role: 'Model', content: text.trim() });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// If there is a trailing user query with no response container (yet)
|
|
100
|
+
if (queries.length > responseContainers.length) {
|
|
101
|
+
messages.push({ role: 'User', content: queries[queries.length - 1].trim() });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const currentUrl =
|
|
105
|
+
typeof window !== 'undefined' && window.location ? window.location.href || '' : '';
|
|
106
|
+
const metadata = {
|
|
107
|
+
Source: 'Google Search AI',
|
|
108
|
+
Date: new Date().toLocaleString(),
|
|
109
|
+
Link: currentUrl,
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
return { title, messages, url: currentUrl, metadata };
|
|
113
|
+
}
|
|
114
|
+
}
|
package/ai/index.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @covai/parser-core — barrel export.
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* import { ChatGPTParser, detectPlatform } from '@covai/parser-core';
|
|
6
|
+
* import { isAiChatUrl, AI_CHAT_DOMAINS } from '@covai/parser-core';
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// Base class
|
|
10
|
+
export { ChatParser } from './base.js';
|
|
11
|
+
|
|
12
|
+
// Individual parsers
|
|
13
|
+
export { ChatGPTParser } from './chatgpt.js';
|
|
14
|
+
export { ClaudeParser } from './claude.js';
|
|
15
|
+
export { GeminiParser } from './gemini.js';
|
|
16
|
+
export { CopilotParser } from './copilot.js';
|
|
17
|
+
export { DeepSeekParser } from './deepseek.js';
|
|
18
|
+
export { MetaParser } from './meta.js';
|
|
19
|
+
export { MistralParser } from './mistral.js';
|
|
20
|
+
export { PerplexityParser } from './perplexity.js';
|
|
21
|
+
export { QwenParser } from './qwen.js';
|
|
22
|
+
export { LumoParser } from './lumo.js';
|
|
23
|
+
export { ZAiParser } from './z_ai.js';
|
|
24
|
+
export { GoogleAIStudioParser } from './google_ai_studio.js';
|
|
25
|
+
export { NotebookLMParser } from './notebooklm.js';
|
|
26
|
+
export { GoogleSearchAIParser } from './google_search_ai.js';
|
|
27
|
+
export { GeminiCloudAssistParser } from './gemini_cloud_assist.js';
|
|
28
|
+
|
|
29
|
+
// Utilities
|
|
30
|
+
export { convertToMarkdown, cleanMarkdown } from '../utils/html-to-markdown.js';
|
|
31
|
+
|
|
32
|
+
// Detection
|
|
33
|
+
export { detectPlatform, isAiChatUrl, parsers } from '../detection/detect-platform.js';
|
|
34
|
+
export { AI_CHAT_DOMAINS } from '../detection/domains.js';
|
package/ai/lumo.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 LumoParser extends ChatParser {
|
|
5
|
+
name = 'Lumo';
|
|
6
|
+
isAvailable(url) {
|
|
7
|
+
return url.includes('lumo.proton.me');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async parse() {
|
|
11
|
+
// Extract Title
|
|
12
|
+
let title = '';
|
|
13
|
+
const titleBtn = document.querySelector('.conversation-header-title-view button');
|
|
14
|
+
if (titleBtn && titleBtn.textContent) {
|
|
15
|
+
title = titleBtn.textContent.trim();
|
|
16
|
+
} else if (document.title) {
|
|
17
|
+
title = document.title.replace(/\s*-\s*Lumo.*$/i, '').trim();
|
|
18
|
+
}
|
|
19
|
+
title = title || 'Lumo Conversation';
|
|
20
|
+
|
|
21
|
+
const messages = [];
|
|
22
|
+
const messageElements = document.querySelectorAll('.lumo-chat-item[data-message-role]');
|
|
23
|
+
|
|
24
|
+
for (const el of messageElements) {
|
|
25
|
+
const roleAttr = el.getAttribute('data-message-role');
|
|
26
|
+
|
|
27
|
+
if (roleAttr === 'user') {
|
|
28
|
+
const contentEl =
|
|
29
|
+
el.querySelector('.lumo-markdown') || el.querySelector('.user-msg-container') || el;
|
|
30
|
+
const text = convertToMarkdown(contentEl);
|
|
31
|
+
if (text.trim()) {
|
|
32
|
+
messages.push({
|
|
33
|
+
role: 'User',
|
|
34
|
+
content: text.trim(),
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
} else if (roleAttr === 'assistant') {
|
|
38
|
+
const contentEl =
|
|
39
|
+
el.querySelector('.progressive-markdown-content') ||
|
|
40
|
+
el.querySelector('.assistant-msg-container') ||
|
|
41
|
+
el;
|
|
42
|
+
|
|
43
|
+
const ownerDoc = el.ownerDocument || document;
|
|
44
|
+
const contentElClone = contentEl.cloneNode(true);
|
|
45
|
+
|
|
46
|
+
// Preprocess Lumo code blocks to standard <pre><code class="language-xyz">...</code></pre>
|
|
47
|
+
contentElClone
|
|
48
|
+
.querySelectorAll('.lumo-syntax-highlighter, .lumo-code-block')
|
|
49
|
+
.forEach((codeBlock) => {
|
|
50
|
+
const codeEl = codeBlock.querySelector('code');
|
|
51
|
+
if (codeEl) {
|
|
52
|
+
let language = '';
|
|
53
|
+
const match = (codeEl.className || '').match(/language-([^\s]+)/);
|
|
54
|
+
if (match) {
|
|
55
|
+
language = match[1];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const codeText = codeEl.textContent || '';
|
|
59
|
+
const pre = ownerDoc.createElement('pre');
|
|
60
|
+
const code = ownerDoc.createElement('code');
|
|
61
|
+
if (language) {
|
|
62
|
+
code.className = `language-${language}`;
|
|
63
|
+
}
|
|
64
|
+
code.textContent = codeText;
|
|
65
|
+
pre.appendChild(code);
|
|
66
|
+
|
|
67
|
+
const parentToReplace = codeBlock.closest('.message-container') || codeBlock;
|
|
68
|
+
if (parentToReplace && parentToReplace.parentNode) {
|
|
69
|
+
parentToReplace.parentNode.replaceChild(pre, parentToReplace);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// Clean up UI toolbar and avatar elements from assistant clone
|
|
75
|
+
contentElClone
|
|
76
|
+
.querySelectorAll('.action-toolbar, .lumo-no-copy, .lumo-avatar, button')
|
|
77
|
+
.forEach((noCopy) => {
|
|
78
|
+
noCopy.remove();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const markdown = convertToMarkdown(contentElClone);
|
|
82
|
+
if (markdown.trim()) {
|
|
83
|
+
messages.push({
|
|
84
|
+
role: 'Lumo',
|
|
85
|
+
content: markdown.trim(),
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const currentUrl =
|
|
92
|
+
typeof window !== 'undefined' && window.location ? window.location.href || '' : '';
|
|
93
|
+
const metadata = {
|
|
94
|
+
Source: 'Lumo',
|
|
95
|
+
Date: new Date().toLocaleString(),
|
|
96
|
+
Link: currentUrl,
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
return { title, messages, url: currentUrl, metadata };
|
|
100
|
+
}
|
|
101
|
+
}
|
package/ai/meta.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { ChatParser } from './base.js';
|
|
2
|
+
import { convertToMarkdown } from '../utils/html-to-markdown.js';
|
|
3
|
+
|
|
4
|
+
export class MetaParser extends ChatParser {
|
|
5
|
+
name = 'Meta AI';
|
|
6
|
+
isAvailable(url) {
|
|
7
|
+
return url.includes('meta.ai');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async parse() {
|
|
11
|
+
// Try to get the conversation title from the input field or the header button
|
|
12
|
+
const titleInput = document.querySelector('input[placeholder="Conversation title"]');
|
|
13
|
+
const titleButton = document.querySelector('[data-slot="button"] span.truncate');
|
|
14
|
+
const title =
|
|
15
|
+
titleInput && titleInput.value
|
|
16
|
+
? titleInput.value
|
|
17
|
+
: titleButton
|
|
18
|
+
? titleButton.innerText
|
|
19
|
+
: 'Meta AI Session';
|
|
20
|
+
|
|
21
|
+
const messages = [];
|
|
22
|
+
|
|
23
|
+
// Find all possible message elements directly to avoid missing any that don't have a wrapper
|
|
24
|
+
const messageElements = Array.from(
|
|
25
|
+
document.querySelectorAll(
|
|
26
|
+
'[data-message-type="user"], [data-testid="assistant-message"], [data-message-id$="_user"], [data-message-id$="_assistant"]',
|
|
27
|
+
),
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
// Keep only the outer-most elements if there are nested matches
|
|
31
|
+
const uniqueElements = messageElements.filter((el) => {
|
|
32
|
+
let parent = el.parentElement;
|
|
33
|
+
while (parent) {
|
|
34
|
+
if (messageElements.includes(parent)) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
parent = parent.parentElement;
|
|
38
|
+
}
|
|
39
|
+
return true;
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
uniqueElements.forEach((el) => {
|
|
43
|
+
let role = 'Unknown';
|
|
44
|
+
let content = '';
|
|
45
|
+
|
|
46
|
+
// Check if the element itself identifies as user or assistant
|
|
47
|
+
const isUser =
|
|
48
|
+
el.matches('[data-message-type="user"]') ||
|
|
49
|
+
(el.getAttribute('data-message-id') &&
|
|
50
|
+
el.getAttribute('data-message-id').endsWith('_user'));
|
|
51
|
+
const isAssistant =
|
|
52
|
+
el.matches('[data-testid="assistant-message"]') ||
|
|
53
|
+
(el.getAttribute('data-message-id') &&
|
|
54
|
+
el.getAttribute('data-message-id').endsWith('_assistant'));
|
|
55
|
+
|
|
56
|
+
if (isUser) {
|
|
57
|
+
role = 'User';
|
|
58
|
+
// User text is usually in a span with text-response class or simply pre-wrap
|
|
59
|
+
const textEl =
|
|
60
|
+
el.querySelector('[data-slot="text"].text-response') ||
|
|
61
|
+
el.querySelector('.whitespace-pre-wrap');
|
|
62
|
+
if (textEl) {
|
|
63
|
+
content = convertToMarkdown(textEl);
|
|
64
|
+
} else {
|
|
65
|
+
content = convertToMarkdown(el);
|
|
66
|
+
}
|
|
67
|
+
} else if (isAssistant) {
|
|
68
|
+
role = 'Meta AI';
|
|
69
|
+
// Assistant content is typically styled in markdown-content or prose
|
|
70
|
+
const contentEl =
|
|
71
|
+
el.querySelector('.markdown-content') ||
|
|
72
|
+
el.querySelector('.ur-markdown') ||
|
|
73
|
+
el.querySelector('.prose');
|
|
74
|
+
if (contentEl) {
|
|
75
|
+
const clone = contentEl.cloneNode(true);
|
|
76
|
+
|
|
77
|
+
// Remove noise elements (like citation pills, edit buttons, thinking status, etc)
|
|
78
|
+
const noiseSelectors = [
|
|
79
|
+
'button',
|
|
80
|
+
'.ur-citation-pill',
|
|
81
|
+
'svg',
|
|
82
|
+
'[data-testid="citation-pill"]',
|
|
83
|
+
'[data-testid="thinking-status"]',
|
|
84
|
+
];
|
|
85
|
+
noiseSelectors.forEach((sel) => {
|
|
86
|
+
clone.querySelectorAll(sel).forEach((n) => n.remove());
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
content = convertToMarkdown(clone);
|
|
90
|
+
} else {
|
|
91
|
+
content = convertToMarkdown(el);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (content) {
|
|
96
|
+
messages.push({ role, content });
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const currentUrl =
|
|
101
|
+
typeof window !== 'undefined' && window.location ? window.location.href || '' : '';
|
|
102
|
+
const metadata = {
|
|
103
|
+
Source: 'Meta AI',
|
|
104
|
+
Date: new Date().toLocaleString(),
|
|
105
|
+
Link: currentUrl,
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
return { title, messages, url: currentUrl, metadata };
|
|
109
|
+
}
|
|
110
|
+
}
|
package/ai/mistral.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { ChatParser } from './base.js';
|
|
2
|
+
import { convertToMarkdown } from '../utils/html-to-markdown.js';
|
|
3
|
+
|
|
4
|
+
export class MistralParser extends ChatParser {
|
|
5
|
+
name = 'Mistral';
|
|
6
|
+
isAvailable(url) {
|
|
7
|
+
return url.includes('chat.mistral.ai');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async parse() {
|
|
11
|
+
// Extract Title
|
|
12
|
+
const titleElement = document.querySelector('span.truncate.text-sm');
|
|
13
|
+
let title = 'Mistral Conversation';
|
|
14
|
+
if (titleElement && titleElement.innerText) {
|
|
15
|
+
title = titleElement.innerText.trim();
|
|
16
|
+
} else if (document.title) {
|
|
17
|
+
title = document.title.replace(' - Mistral', '').trim();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const messages = [];
|
|
21
|
+
const messageElements = document.querySelectorAll('[data-message-author-role]');
|
|
22
|
+
|
|
23
|
+
for (const el of messageElements) {
|
|
24
|
+
const role = el.getAttribute('data-message-author-role');
|
|
25
|
+
|
|
26
|
+
if (role === 'user') {
|
|
27
|
+
const contentEl =
|
|
28
|
+
el.querySelector('.select-text') || el.querySelector('.whitespace-pre-wrap');
|
|
29
|
+
if (contentEl) {
|
|
30
|
+
messages.push({
|
|
31
|
+
role: 'User',
|
|
32
|
+
content: contentEl.innerText.trim(),
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
} else if (role === 'assistant') {
|
|
36
|
+
const answerEl = el.querySelector('[data-message-part-type="answer"]');
|
|
37
|
+
if (answerEl) {
|
|
38
|
+
const markdown = convertToMarkdown(answerEl.innerHTML);
|
|
39
|
+
messages.push({
|
|
40
|
+
role: 'Mistral',
|
|
41
|
+
content: markdown,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const currentUrl =
|
|
48
|
+
typeof window !== 'undefined' && window.location ? window.location.href || '' : '';
|
|
49
|
+
const metadata = {
|
|
50
|
+
Source: 'Mistral',
|
|
51
|
+
Date: new Date().toLocaleString(),
|
|
52
|
+
Link: currentUrl,
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
return { title, messages, url: currentUrl, metadata };
|
|
56
|
+
}
|
|
57
|
+
}
|