decant-core 1.3.1 → 1.5.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/chatgpt.js +19 -4
- package/ai/claude.js +318 -50
- package/ai/perplexity.js +275 -23
- package/package.json +4 -1
package/ai/chatgpt.js
CHANGED
|
@@ -759,6 +759,19 @@ export class ChatGPTParser extends ChatParser {
|
|
|
759
759
|
}
|
|
760
760
|
});
|
|
761
761
|
|
|
762
|
+
clone
|
|
763
|
+
.querySelectorAll(
|
|
764
|
+
'[data-testid="webpage-citation-pill"], [data-testid*="citation-pill"]',
|
|
765
|
+
)
|
|
766
|
+
.forEach((pill) => {
|
|
767
|
+
pill.querySelectorAll("img").forEach((img) => img.remove());
|
|
768
|
+
pill.querySelectorAll("span").forEach((span) => {
|
|
769
|
+
if (/^\+\d+$/.test(span.textContent.trim())) {
|
|
770
|
+
span.remove();
|
|
771
|
+
}
|
|
772
|
+
});
|
|
773
|
+
});
|
|
774
|
+
|
|
762
775
|
noiseSelectors.forEach((selector) => {
|
|
763
776
|
clone.querySelectorAll(selector).forEach((node) => node.remove());
|
|
764
777
|
});
|
|
@@ -783,11 +796,13 @@ export class ChatGPTParser extends ChatParser {
|
|
|
783
796
|
}
|
|
784
797
|
|
|
785
798
|
extractMountedMessages() {
|
|
786
|
-
const
|
|
799
|
+
const turns = getConversationTurns(document);
|
|
787
800
|
const containers =
|
|
788
|
-
|
|
789
|
-
?
|
|
790
|
-
: Array.from(
|
|
801
|
+
turns.length > 0
|
|
802
|
+
? turns
|
|
803
|
+
: Array.from(
|
|
804
|
+
document.querySelectorAll("article, [data-message-author-role]"),
|
|
805
|
+
);
|
|
791
806
|
|
|
792
807
|
return containers
|
|
793
808
|
.map((container) => this.extractMessage(container))
|
package/ai/claude.js
CHANGED
|
@@ -70,6 +70,39 @@ function getCurrentBranch(data) {
|
|
|
70
70
|
return branch;
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
const EXT_TO_LANG = {
|
|
74
|
+
py: "python",
|
|
75
|
+
js: "javascript",
|
|
76
|
+
jsx: "jsx",
|
|
77
|
+
ts: "typescript",
|
|
78
|
+
tsx: "tsx",
|
|
79
|
+
md: "markdown",
|
|
80
|
+
html: "html",
|
|
81
|
+
css: "css",
|
|
82
|
+
json: "json",
|
|
83
|
+
sh: "bash",
|
|
84
|
+
bash: "bash",
|
|
85
|
+
yml: "yaml",
|
|
86
|
+
yaml: "yaml",
|
|
87
|
+
sql: "sql",
|
|
88
|
+
java: "java",
|
|
89
|
+
rb: "ruby",
|
|
90
|
+
go: "go",
|
|
91
|
+
rs: "rust",
|
|
92
|
+
c: "c",
|
|
93
|
+
cpp: "cpp",
|
|
94
|
+
txt: "text",
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const MIME_TO_LANG = {
|
|
98
|
+
"application/vnd.ant.react": "jsx",
|
|
99
|
+
"text/html": "html",
|
|
100
|
+
"image/svg+xml": "svg",
|
|
101
|
+
"application/vnd.ant.mermaid": "mermaid",
|
|
102
|
+
"text/markdown": "markdown",
|
|
103
|
+
"application/vnd.ant.code": "text",
|
|
104
|
+
};
|
|
105
|
+
|
|
73
106
|
function extractArtifactsFromText(text) {
|
|
74
107
|
const artifactRegex = /<antArtifact[^>]*>([\s\S]*?)<\/antArtifact>/g;
|
|
75
108
|
const artifacts = [];
|
|
@@ -90,53 +123,139 @@ function extractArtifactsFromText(text) {
|
|
|
90
123
|
return artifacts;
|
|
91
124
|
}
|
|
92
125
|
|
|
93
|
-
function
|
|
126
|
+
function collectArtifacts(messages) {
|
|
127
|
+
const artifacts = new Map();
|
|
128
|
+
for (const m of messages) {
|
|
129
|
+
if (!Array.isArray(m?.content)) continue;
|
|
130
|
+
for (const block of m.content) {
|
|
131
|
+
if (block.type !== "tool_use" || block.name !== "artifacts") continue;
|
|
132
|
+
const input = block.input || {};
|
|
133
|
+
const id = input.id || "__artifact__";
|
|
134
|
+
let a = artifacts.get(id);
|
|
135
|
+
if (!a) {
|
|
136
|
+
a = { content: "" };
|
|
137
|
+
artifacts.set(id, a);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (input.command === "update") {
|
|
141
|
+
if (
|
|
142
|
+
typeof input.old_str === "string" &&
|
|
143
|
+
typeof input.new_str === "string"
|
|
144
|
+
) {
|
|
145
|
+
if (a.content.includes(input.old_str)) {
|
|
146
|
+
a.content = a.content.replace(input.old_str, () => input.new_str);
|
|
147
|
+
} else {
|
|
148
|
+
console.warn(
|
|
149
|
+
`[AI Exporter] Artifact "${a.title || id}": update could not be applied (source text not found).`,
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
} else if (typeof input.content === "string") {
|
|
154
|
+
a.content = input.content;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (input.title) a.title = input.title;
|
|
158
|
+
if (input.type) a.type = input.type;
|
|
159
|
+
if (input.language) a.language = input.language;
|
|
160
|
+
a.lastBlock = block;
|
|
161
|
+
if (input.version_uuid) a.lastVersionUuid = input.version_uuid;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return artifacts;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function extractArtifacts(message, foldedArtifacts = new Map()) {
|
|
94
168
|
const artifacts = [];
|
|
95
169
|
if (message.content && Array.isArray(message.content)) {
|
|
96
170
|
for (const content of message.content) {
|
|
97
|
-
if (
|
|
98
|
-
content.
|
|
99
|
-
(content.name === "artifacts"
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
171
|
+
if (content.type === "tool_use") {
|
|
172
|
+
const input = content.input || {};
|
|
173
|
+
if (content.name === "artifacts") {
|
|
174
|
+
const id = input.id || "__artifact__";
|
|
175
|
+
const folded = foldedArtifacts.get(id);
|
|
176
|
+
|
|
177
|
+
// Only emit at the final edit block for this artifact
|
|
178
|
+
if (folded && folded.lastBlock && folded.lastBlock !== content) {
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
if (
|
|
182
|
+
folded &&
|
|
183
|
+
folded.lastVersionUuid &&
|
|
184
|
+
input.version_uuid &&
|
|
185
|
+
input.version_uuid !== folded.lastVersionUuid
|
|
186
|
+
) {
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const title = input.title || (folded && folded.title) || "Artifact";
|
|
191
|
+
const lang =
|
|
192
|
+
input.language ||
|
|
193
|
+
(folded && folded.language) ||
|
|
194
|
+
MIME_TO_LANG[input.type || (folded && folded.type)] ||
|
|
195
|
+
"text";
|
|
196
|
+
const code =
|
|
197
|
+
(folded && folded.content) || input.content || input.new_str || "";
|
|
198
|
+
if (code) {
|
|
199
|
+
artifacts.push({
|
|
200
|
+
title,
|
|
201
|
+
language: lang,
|
|
202
|
+
content: code.trim(),
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
} else if (content.name === "create_file") {
|
|
206
|
+
let code = "";
|
|
207
|
+
let filename = "file";
|
|
208
|
+
let lang = "";
|
|
209
|
+
|
|
210
|
+
if (typeof input.file_text === "string" && input.file_text) {
|
|
211
|
+
code = input.file_text.trim();
|
|
212
|
+
filename = String(input.path || "file")
|
|
213
|
+
.split("/")
|
|
214
|
+
.pop();
|
|
215
|
+
const ext = filename.includes(".")
|
|
216
|
+
? filename.split(".").pop().toLowerCase()
|
|
217
|
+
: "";
|
|
218
|
+
lang = EXT_TO_LANG[ext] || ext || "text";
|
|
219
|
+
} else if (content.display_content) {
|
|
220
|
+
const displayContent = content.display_content;
|
|
221
|
+
if (displayContent.type === "code_block" && displayContent.code) {
|
|
222
|
+
filename = displayContent.filename || "artifact";
|
|
223
|
+
code = displayContent.code.trim();
|
|
224
|
+
lang = displayContent.language || "text";
|
|
225
|
+
} else if (
|
|
226
|
+
displayContent.type === "json_block" &&
|
|
227
|
+
displayContent.json_block
|
|
228
|
+
) {
|
|
229
|
+
try {
|
|
230
|
+
const data = JSON.parse(displayContent.json_block);
|
|
231
|
+
if (data.filename) {
|
|
232
|
+
filename = data.filename;
|
|
233
|
+
code = (data.code || "").trim();
|
|
234
|
+
lang = data.language || "text";
|
|
235
|
+
}
|
|
236
|
+
} catch (e) {
|
|
237
|
+
console.warn(
|
|
238
|
+
"[AI Exporter] Failed to parse tool use artifact json:",
|
|
239
|
+
e,
|
|
240
|
+
);
|
|
241
|
+
}
|
|
131
242
|
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (code) {
|
|
246
|
+
const title = filename
|
|
247
|
+
.split("/")
|
|
248
|
+
.pop()
|
|
249
|
+
.replace(/\.[^.]+$/, "");
|
|
250
|
+
artifacts.push({
|
|
251
|
+
title: title || "Artifact",
|
|
252
|
+
language: lang,
|
|
253
|
+
content: code,
|
|
254
|
+
});
|
|
137
255
|
}
|
|
138
256
|
}
|
|
139
257
|
}
|
|
258
|
+
|
|
140
259
|
if (content.text) {
|
|
141
260
|
artifacts.push(...extractArtifactsFromText(content.text));
|
|
142
261
|
}
|
|
@@ -148,6 +267,68 @@ function extractArtifacts(message) {
|
|
|
148
267
|
return artifacts;
|
|
149
268
|
}
|
|
150
269
|
|
|
270
|
+
function unrollInteractiveElements(root, doc) {
|
|
271
|
+
if (!root || !doc) return;
|
|
272
|
+
|
|
273
|
+
// Group slide titles by their common card container
|
|
274
|
+
const titleEls = Array.from(root.querySelectorAll(".text-title"));
|
|
275
|
+
if (titleEls.length > 0) {
|
|
276
|
+
const containerMap = new Map();
|
|
277
|
+
for (const titleEl of titleEls) {
|
|
278
|
+
let container = titleEl.parentElement;
|
|
279
|
+
while (container && container !== root) {
|
|
280
|
+
if (
|
|
281
|
+
container.classList.contains("@container") ||
|
|
282
|
+
container.classList.contains("bg-surface-2") ||
|
|
283
|
+
container.querySelector(
|
|
284
|
+
'[aria-label*="Go to step"], [aria-current="step"]',
|
|
285
|
+
)
|
|
286
|
+
) {
|
|
287
|
+
break;
|
|
288
|
+
}
|
|
289
|
+
container = container.parentElement;
|
|
290
|
+
}
|
|
291
|
+
if (container && container !== root) {
|
|
292
|
+
if (!containerMap.has(container)) {
|
|
293
|
+
containerMap.set(container, []);
|
|
294
|
+
}
|
|
295
|
+
containerMap.get(container).push(titleEl);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
for (const [container, titles] of containerMap.entries()) {
|
|
300
|
+
const slides = [];
|
|
301
|
+
titles.forEach((titleEl, idx) => {
|
|
302
|
+
const title = titleEl.textContent.trim();
|
|
303
|
+
const bodyEl =
|
|
304
|
+
titleEl.nextElementSibling ||
|
|
305
|
+
titleEl.parentElement.querySelector(".text-body");
|
|
306
|
+
const body = bodyEl ? bodyEl.textContent.trim() : "";
|
|
307
|
+
if (title) slides.push({ index: idx + 1, title, body });
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
if (slides.length > 0) {
|
|
311
|
+
const replacement = doc.createElement("div");
|
|
312
|
+
replacement.className = "unrolled-interactive-steps";
|
|
313
|
+
slides.forEach((slide) => {
|
|
314
|
+
const h3 = doc.createElement("h3");
|
|
315
|
+
h3.textContent = `${slide.index}. ${slide.title}`;
|
|
316
|
+
replacement.appendChild(h3);
|
|
317
|
+
if (slide.body) {
|
|
318
|
+
const p = doc.createElement("p");
|
|
319
|
+
p.textContent = slide.body;
|
|
320
|
+
replacement.appendChild(p);
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
container.replaceWith(replacement);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Remove any remaining buttons
|
|
329
|
+
root.querySelectorAll("button").forEach((btn) => btn.remove());
|
|
330
|
+
}
|
|
331
|
+
|
|
151
332
|
export class ClaudeParser extends ChatParser {
|
|
152
333
|
name = "Claude";
|
|
153
334
|
constructor() {
|
|
@@ -189,6 +370,31 @@ export class ClaudeParser extends ChatParser {
|
|
|
189
370
|
}
|
|
190
371
|
|
|
191
372
|
const branch = getCurrentBranch(data);
|
|
373
|
+
const foldedArtifacts = collectArtifacts(branch);
|
|
374
|
+
|
|
375
|
+
const toolResultMap = new Map();
|
|
376
|
+
for (const msg of branch) {
|
|
377
|
+
if (Array.isArray(msg?.content)) {
|
|
378
|
+
for (const block of msg.content) {
|
|
379
|
+
if (block.type === "tool_result") {
|
|
380
|
+
const toolUseId = block.tool_use_id || block.id;
|
|
381
|
+
let answers = block.toolUseResult?.answers || {};
|
|
382
|
+
if (
|
|
383
|
+
(!answers || Object.keys(answers).length === 0) &&
|
|
384
|
+
typeof block.content === "string"
|
|
385
|
+
) {
|
|
386
|
+
try {
|
|
387
|
+
const parsed = JSON.parse(block.content);
|
|
388
|
+
answers = parsed.answers || parsed;
|
|
389
|
+
} catch {
|
|
390
|
+
answers = { result: block.content };
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
toolResultMap.set(toolUseId, answers);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
192
398
|
|
|
193
399
|
const convTitle = data.name || title;
|
|
194
400
|
|
|
@@ -209,6 +415,47 @@ export class ClaudeParser extends ChatParser {
|
|
|
209
415
|
if (cleanText) {
|
|
210
416
|
contentStr += `${cleanText}\n\n`;
|
|
211
417
|
}
|
|
418
|
+
} else if (block.type === "tool_use") {
|
|
419
|
+
const input = block.input || {};
|
|
420
|
+
if (
|
|
421
|
+
block.name === "visualize:show_widget" &&
|
|
422
|
+
input.widget_code
|
|
423
|
+
) {
|
|
424
|
+
const widgetTitle = input.title || "Interactive Widget";
|
|
425
|
+
contentStr += `> **Interactive Widget: ${widgetTitle}**\n\n\`\`\`jsx\n${input.widget_code.trim()}\n\`\`\`\n\n`;
|
|
426
|
+
} else if (block.name === "repl" && input.code) {
|
|
427
|
+
contentStr += `**Analyzed data**\n\n\`\`\`javascript\n${input.code.trim()}\n\`\`\`\n\n`;
|
|
428
|
+
} else if (
|
|
429
|
+
block.name === "AskUserQuestion" &&
|
|
430
|
+
Array.isArray(input.questions) &&
|
|
431
|
+
input.questions.length > 0
|
|
432
|
+
) {
|
|
433
|
+
const qCount = input.questions.length;
|
|
434
|
+
const countLabel =
|
|
435
|
+
qCount === 1
|
|
436
|
+
? "Asked 1 question"
|
|
437
|
+
: `Asked ${qCount} questions`;
|
|
438
|
+
let qStr = `> **${countLabel}:**\n`;
|
|
439
|
+
const answers = toolResultMap.get(block.id) || {};
|
|
440
|
+
for (const q of input.questions) {
|
|
441
|
+
const qText = q.question || q.text || "";
|
|
442
|
+
const ans = answers[qText];
|
|
443
|
+
let ansStr = "";
|
|
444
|
+
if (typeof ans === "string") {
|
|
445
|
+
ansStr = ans;
|
|
446
|
+
} else if (Array.isArray(ans)) {
|
|
447
|
+
ansStr = ans.join(", ");
|
|
448
|
+
} else if (ans && typeof ans === "object") {
|
|
449
|
+
ansStr = JSON.stringify(ans);
|
|
450
|
+
} else if (ans != null) {
|
|
451
|
+
ansStr = String(ans);
|
|
452
|
+
}
|
|
453
|
+
qStr += ansStr
|
|
454
|
+
? `> - **${qText}** — ${ansStr}\n`
|
|
455
|
+
: `> - ${qText}\n`;
|
|
456
|
+
}
|
|
457
|
+
contentStr += `${qStr}\n`;
|
|
458
|
+
}
|
|
212
459
|
}
|
|
213
460
|
}
|
|
214
461
|
} else if (message.text) {
|
|
@@ -245,13 +492,37 @@ export class ClaudeParser extends ChatParser {
|
|
|
245
492
|
}
|
|
246
493
|
}
|
|
247
494
|
|
|
495
|
+
// Append files (images/documents)
|
|
496
|
+
if (message.files && Array.isArray(message.files)) {
|
|
497
|
+
for (const file of message.files) {
|
|
498
|
+
const name = file.file_name || "file";
|
|
499
|
+
if (
|
|
500
|
+
file.file_kind === "image" &&
|
|
501
|
+
(file.preview_url || file.preview_asset?.url)
|
|
502
|
+
) {
|
|
503
|
+
const url = file.preview_url || file.preview_asset.url;
|
|
504
|
+
contentStr += `\n\n**Attachment: ${name}**\n\n\n\n`;
|
|
505
|
+
} else if (
|
|
506
|
+
file.file_kind === "document" &&
|
|
507
|
+
file.document_asset?.url
|
|
508
|
+
) {
|
|
509
|
+
const url = file.document_asset.url;
|
|
510
|
+
const pages = file.document_asset.page_count;
|
|
511
|
+
const pageInfo = pages
|
|
512
|
+
? ` · ${pages} page${pages === 1 ? "" : "s"}`
|
|
513
|
+
: "";
|
|
514
|
+
contentStr += `\n\n**Attachment: [${name}](${url})** _(document${pageInfo})_\n\n`;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
248
519
|
contentStr = contentStr.trim();
|
|
249
520
|
if (contentStr) {
|
|
250
521
|
messages.push({ role, content: contentStr });
|
|
251
522
|
}
|
|
252
523
|
|
|
253
524
|
// Extract and push artifacts
|
|
254
|
-
const artifacts = extractArtifacts(message);
|
|
525
|
+
const artifacts = extractArtifacts(message, foldedArtifacts);
|
|
255
526
|
for (const artifact of artifacts) {
|
|
256
527
|
let artContent = "";
|
|
257
528
|
const artTitle = artifact.title || "Artifact";
|
|
@@ -356,13 +627,10 @@ export class ClaudeParser extends ChatParser {
|
|
|
356
627
|
return !overlapsWithError;
|
|
357
628
|
});
|
|
358
629
|
|
|
359
|
-
const
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
? -1
|
|
364
|
-
: 1;
|
|
365
|
-
});
|
|
630
|
+
const combinedSet = new Set([...strictCandidates, ...validFallbacks]);
|
|
631
|
+
const allElements = Array.from(
|
|
632
|
+
document.querySelectorAll(`${strictSelectors}, ${fallbackSelectors}`),
|
|
633
|
+
).filter((el) => combinedSet.has(el));
|
|
366
634
|
|
|
367
635
|
const artifactElements = document.querySelectorAll(".artifact-block-cell");
|
|
368
636
|
const artifactMap = new Map();
|
|
@@ -375,7 +643,7 @@ export class ClaudeParser extends ChatParser {
|
|
|
375
643
|
if (el.matches('[data-testid="user-message"]')) {
|
|
376
644
|
role = "User";
|
|
377
645
|
const clone = el.cloneNode(true);
|
|
378
|
-
clone.
|
|
646
|
+
unrollInteractiveElements(clone, el.ownerDocument || document);
|
|
379
647
|
content = convertToMarkdown(clone);
|
|
380
648
|
} else if (
|
|
381
649
|
el.matches(".font-claude-message") ||
|
|
@@ -384,7 +652,7 @@ export class ClaudeParser extends ChatParser {
|
|
|
384
652
|
) {
|
|
385
653
|
role = "Claude";
|
|
386
654
|
const clone = el.cloneNode(true);
|
|
387
|
-
clone.
|
|
655
|
+
unrollInteractiveElements(clone, el.ownerDocument || document);
|
|
388
656
|
content = convertToMarkdown(clone);
|
|
389
657
|
} else if (el.matches(".artifact-block-cell")) {
|
|
390
658
|
role = "Claude Artifact";
|
package/ai/perplexity.js
CHANGED
|
@@ -1,5 +1,191 @@
|
|
|
1
1
|
import { ChatParser } from "./base.js";
|
|
2
2
|
import { convertToMarkdown } from "../utils/html-to-markdown.js";
|
|
3
|
+
import { normalizeLatexMath } from "../utils/latex-math.js";
|
|
4
|
+
|
|
5
|
+
export function getThreadSlug(url) {
|
|
6
|
+
if (!url || typeof url !== "string") return null;
|
|
7
|
+
const match = url.match(/\/search\/([a-zA-Z0-9_-]+)/);
|
|
8
|
+
if (match) return match[1];
|
|
9
|
+
const matchPage = url.match(/\/page\/([a-zA-Z0-9_-]+)/);
|
|
10
|
+
if (matchPage) return matchPage[1];
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function extractAssistantAnswer(entry) {
|
|
15
|
+
if (!entry) return "";
|
|
16
|
+
// 1. Check blocks -> plan_block.goals for markdown answer
|
|
17
|
+
if (Array.isArray(entry.blocks)) {
|
|
18
|
+
for (const b of entry.blocks) {
|
|
19
|
+
if (b.intended_usage === "plan" && b.plan_block?.goals) {
|
|
20
|
+
for (const g of b.plan_block.goals) {
|
|
21
|
+
if (g.description && g.description.length > 100) {
|
|
22
|
+
return g.description.trim();
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
if (b.answer_block?.answer) {
|
|
27
|
+
return b.answer_block.answer.trim();
|
|
28
|
+
}
|
|
29
|
+
if (b.markdown_block?.markdown) {
|
|
30
|
+
return b.markdown_block.markdown.trim();
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
// 2. Check direct answer or text fields
|
|
35
|
+
if (entry.answer && typeof entry.answer === "string") {
|
|
36
|
+
return entry.answer.trim();
|
|
37
|
+
}
|
|
38
|
+
if (entry.text && typeof entry.text === "string") {
|
|
39
|
+
try {
|
|
40
|
+
const parsed = JSON.parse(entry.text);
|
|
41
|
+
if (parsed.answer) return parsed.answer.trim();
|
|
42
|
+
} catch {
|
|
43
|
+
return entry.text.trim();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return "";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function formatApiResult(threadData, currentUrl, fallbackTitle) {
|
|
50
|
+
const title =
|
|
51
|
+
threadData.thread_title ||
|
|
52
|
+
threadData.first_entry?.query_str ||
|
|
53
|
+
fallbackTitle ||
|
|
54
|
+
"Perplexity Search";
|
|
55
|
+
|
|
56
|
+
const allEntries = [];
|
|
57
|
+
if (threadData.first_entry) {
|
|
58
|
+
allEntries.push(threadData.first_entry);
|
|
59
|
+
}
|
|
60
|
+
if (Array.isArray(threadData.entries)) {
|
|
61
|
+
allEntries.push(...threadData.entries);
|
|
62
|
+
}
|
|
63
|
+
if (threadData.latest_entry) {
|
|
64
|
+
allEntries.push(threadData.latest_entry);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Deduplicate entries by uuid / backend_uuid
|
|
68
|
+
const seenUuids = new Set();
|
|
69
|
+
const uniqueEntries = [];
|
|
70
|
+
for (const entry of allEntries) {
|
|
71
|
+
const id = entry.uuid || entry.backend_uuid || entry.query_str;
|
|
72
|
+
if (id && !seenUuids.has(id)) {
|
|
73
|
+
seenUuids.add(id);
|
|
74
|
+
uniqueEntries.push(entry);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const messages = [];
|
|
79
|
+
for (const entry of uniqueEntries) {
|
|
80
|
+
const prompt = (entry.query_str || "").trim();
|
|
81
|
+
const timestamp =
|
|
82
|
+
entry.entry_created_datetime || entry.updated_datetime || undefined;
|
|
83
|
+
|
|
84
|
+
if (prompt) {
|
|
85
|
+
const userMsg = { role: "User", content: prompt };
|
|
86
|
+
if (timestamp) userMsg.timestamp = timestamp;
|
|
87
|
+
messages.push(userMsg);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const rawAnswer = extractAssistantAnswer(entry);
|
|
91
|
+
if (rawAnswer) {
|
|
92
|
+
const normalizedAnswer = normalizeLatexMath(rawAnswer);
|
|
93
|
+
const assistantMsg = { role: "Perplexity", content: normalizedAnswer };
|
|
94
|
+
if (timestamp) assistantMsg.timestamp = timestamp;
|
|
95
|
+
messages.push(assistantMsg);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const effectiveUrl =
|
|
100
|
+
currentUrl ||
|
|
101
|
+
(threadData.first_entry?.thread_url_slug
|
|
102
|
+
? `https://www.perplexity.ai/search/${threadData.first_entry.thread_url_slug}`
|
|
103
|
+
: "");
|
|
104
|
+
|
|
105
|
+
const metadata = {
|
|
106
|
+
Source: "Perplexity",
|
|
107
|
+
Date: new Date().toLocaleString(),
|
|
108
|
+
Link: effectiveUrl,
|
|
109
|
+
Method: "API",
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
return { title, messages, url: effectiveUrl, metadata };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export async function getPerplexityAccount() {
|
|
116
|
+
if (typeof window === "undefined") return null;
|
|
117
|
+
|
|
118
|
+
const isUuid = (str) =>
|
|
119
|
+
typeof str === "string" &&
|
|
120
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(
|
|
121
|
+
str.trim(),
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
// 1. Check sessionStorage ('pplx-active-account')
|
|
125
|
+
try {
|
|
126
|
+
const sessionAcc = window.sessionStorage?.getItem("pplx-active-account");
|
|
127
|
+
if (sessionAcc && isUuid(sessionAcc)) return sessionAcc.trim();
|
|
128
|
+
} catch {
|
|
129
|
+
// Ignore storage errors
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// 2. Check localStorage ('pplx-last-active-account')
|
|
133
|
+
try {
|
|
134
|
+
const localAcc = window.localStorage?.getItem("pplx-last-active-account");
|
|
135
|
+
if (localAcc && isUuid(localAcc)) return localAcc.trim();
|
|
136
|
+
} catch {
|
|
137
|
+
// Ignore storage errors
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// 3. Scan keys in sessionStorage and localStorage for pplx account UUIDs
|
|
141
|
+
try {
|
|
142
|
+
for (const storage of [window.sessionStorage, window.localStorage]) {
|
|
143
|
+
if (!storage) continue;
|
|
144
|
+
for (let i = 0; i < storage.length; i++) {
|
|
145
|
+
const key = storage.key(i);
|
|
146
|
+
if (key && (key.includes("pplx") || key.includes("account"))) {
|
|
147
|
+
const val = storage.getItem(key);
|
|
148
|
+
if (val && isUuid(val)) return val.trim();
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
} catch {
|
|
153
|
+
// Ignore storage scanning errors
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// 4. Check document.cookie if available
|
|
157
|
+
try {
|
|
158
|
+
const match = document.cookie?.match(
|
|
159
|
+
/(?:pplx-last-active-account|pplx-active-account)=([0-9a-f-]{36})/i,
|
|
160
|
+
);
|
|
161
|
+
if (match && isUuid(match[1])) return match[1].trim();
|
|
162
|
+
} catch {
|
|
163
|
+
// Ignore cookie errors
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// 5. Fallback: query https://www.perplexity.ai/api/auth/linked-accounts using session cookies
|
|
167
|
+
try {
|
|
168
|
+
if (typeof fetch === "function") {
|
|
169
|
+
const res = await fetch(
|
|
170
|
+
"https://www.perplexity.ai/api/auth/linked-accounts",
|
|
171
|
+
{
|
|
172
|
+
method: "GET",
|
|
173
|
+
credentials: "include",
|
|
174
|
+
headers: { Accept: "application/json" },
|
|
175
|
+
},
|
|
176
|
+
);
|
|
177
|
+
if (res.ok) {
|
|
178
|
+
const data = await res.json();
|
|
179
|
+
const accountId = data?.accounts?.[0]?.user_id;
|
|
180
|
+
if (accountId && isUuid(accountId)) return accountId.trim();
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
} catch {
|
|
184
|
+
// Ignore linked-accounts network error
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
3
189
|
|
|
4
190
|
export class PerplexityParser extends ChatParser {
|
|
5
191
|
name = "Perplexity";
|
|
@@ -7,7 +193,38 @@ export class PerplexityParser extends ChatParser {
|
|
|
7
193
|
return url.includes("perplexity.ai");
|
|
8
194
|
}
|
|
9
195
|
|
|
10
|
-
async
|
|
196
|
+
async fetchThread(slug) {
|
|
197
|
+
const accountId = await getPerplexityAccount();
|
|
198
|
+
const url = `https://www.perplexity.ai/rest/thread/${slug}?with_parent_info=true&with_schematized_response=true&version=2.18&source=default&limit=100&offset=0&from_first=true`;
|
|
199
|
+
const headers = {
|
|
200
|
+
Accept: "application/json",
|
|
201
|
+
"x-app-apiclient": "default",
|
|
202
|
+
"x-app-apiversion": "2.18",
|
|
203
|
+
};
|
|
204
|
+
if (accountId) {
|
|
205
|
+
headers["x-pplx-account"] = accountId;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const response = await fetch(url, {
|
|
209
|
+
method: "GET",
|
|
210
|
+
credentials: "include",
|
|
211
|
+
headers,
|
|
212
|
+
});
|
|
213
|
+
if (!response.ok) {
|
|
214
|
+
throw new Error(
|
|
215
|
+
`Perplexity API request failed with status ${response.status}`,
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
return response.json();
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async parse(options = {}) {
|
|
222
|
+
const parserMode = options.parserMode || "prefer_api";
|
|
223
|
+
const currentUrl =
|
|
224
|
+
typeof window !== "undefined" && window.location
|
|
225
|
+
? window.location.href || ""
|
|
226
|
+
: "";
|
|
227
|
+
|
|
11
228
|
const rawTitle =
|
|
12
229
|
document.querySelector(".share-title-section h1")?.textContent ||
|
|
13
230
|
document.querySelector("h1")?.textContent ||
|
|
@@ -15,6 +232,25 @@ export class PerplexityParser extends ChatParser {
|
|
|
15
232
|
"Perplexity Search";
|
|
16
233
|
const title = rawTitle.trim().replace(/\s+/g, " ");
|
|
17
234
|
|
|
235
|
+
// 1. Attempt API-first extraction when in prefer_api mode
|
|
236
|
+
if (parserMode !== "prefer_dom") {
|
|
237
|
+
const slug = getThreadSlug(currentUrl);
|
|
238
|
+
if (slug) {
|
|
239
|
+
try {
|
|
240
|
+
const apiData = await this.fetchThread(slug);
|
|
241
|
+
if (apiData && (apiData.entries?.length || apiData.first_entry)) {
|
|
242
|
+
return formatApiResult(apiData, currentUrl, title);
|
|
243
|
+
}
|
|
244
|
+
} catch (err) {
|
|
245
|
+
console.warn(
|
|
246
|
+
"[AI Exporter] Perplexity API fetch failed, falling back to DOM:",
|
|
247
|
+
err,
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// 2. DOM extraction fallback
|
|
18
254
|
const messages = [];
|
|
19
255
|
|
|
20
256
|
// Perplexity Container
|
|
@@ -23,6 +259,7 @@ export class PerplexityParser extends ChatParser {
|
|
|
23
259
|
|
|
24
260
|
// Candidates for User Messages
|
|
25
261
|
const userSelectors = [
|
|
262
|
+
".group\\/user-bubble",
|
|
26
263
|
"h1.group\\/query",
|
|
27
264
|
".group\\/query",
|
|
28
265
|
".whitespace-pre-line.text-pretty",
|
|
@@ -40,43 +277,58 @@ export class PerplexityParser extends ChatParser {
|
|
|
40
277
|
const elements = threadContainer.querySelectorAll(selectorString);
|
|
41
278
|
|
|
42
279
|
// Helper to determine role
|
|
43
|
-
const
|
|
280
|
+
const isUser = (el) => {
|
|
44
281
|
for (const s of userSelectors) {
|
|
45
|
-
if (el.matches(s)) return
|
|
282
|
+
if (el.matches(s)) return true;
|
|
46
283
|
}
|
|
284
|
+
return false;
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
const isAssistant = (el) => {
|
|
47
288
|
for (const s of assistantSelectors) {
|
|
48
|
-
if (el.matches(s)) return
|
|
289
|
+
if (el.matches(s)) return true;
|
|
49
290
|
}
|
|
50
|
-
return
|
|
291
|
+
return false;
|
|
51
292
|
};
|
|
52
293
|
|
|
53
|
-
const seenText = new Set();
|
|
54
|
-
|
|
55
294
|
elements.forEach((el) => {
|
|
56
|
-
// Perplexity nests things. Avoid duplicates if we selected a parent and a child.
|
|
57
|
-
// Also avoid "Related" section queries if possible (usually separate container, check parents?)
|
|
58
|
-
|
|
59
295
|
// Check if inside "related" or "sources"
|
|
60
296
|
if (el.closest('[class*="related"], [class*="sources"]')) return;
|
|
61
297
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
298
|
+
if (isUser(el)) {
|
|
299
|
+
if (el.parentElement?.closest(".group\\/user-bubble, .group\\/query")) {
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Extract timestamp if present in bubble
|
|
304
|
+
const timeEl = el.querySelector(
|
|
305
|
+
".text-tertiary, [class*='text-tertiary']",
|
|
306
|
+
);
|
|
307
|
+
const timestamp = timeEl ? timeEl.textContent.trim() : undefined;
|
|
65
308
|
|
|
66
|
-
|
|
309
|
+
// Clean user text container
|
|
310
|
+
const textEl = el.querySelector(".whitespace-pre-line") || el;
|
|
311
|
+
const text = convertToMarkdown(textEl).trim();
|
|
312
|
+
if (!text) return;
|
|
67
313
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
314
|
+
const msgObj = { role: "User", content: text };
|
|
315
|
+
if (timestamp) {
|
|
316
|
+
msgObj.timestamp = timestamp;
|
|
317
|
+
}
|
|
318
|
+
messages.push(msgObj);
|
|
319
|
+
} else if (isAssistant(el)) {
|
|
320
|
+
// Avoid duplicate nested assistant containers (e.g. .prose inside div[id^="markdown-content-"])
|
|
321
|
+
if (el.parentElement?.closest('div[id^="markdown-content-"], .prose')) {
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
71
324
|
|
|
72
|
-
|
|
73
|
-
|
|
325
|
+
const text = convertToMarkdown(el).trim();
|
|
326
|
+
if (!text) return;
|
|
327
|
+
|
|
328
|
+
messages.push({ role: "Perplexity", content: text });
|
|
329
|
+
}
|
|
74
330
|
});
|
|
75
331
|
|
|
76
|
-
const currentUrl =
|
|
77
|
-
typeof window !== "undefined" && window.location
|
|
78
|
-
? window.location.href || ""
|
|
79
|
-
: "";
|
|
80
332
|
const metadata = {
|
|
81
333
|
Source: "Perplexity",
|
|
82
334
|
Date: new Date().toLocaleString(),
|
package/package.json
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "decant-core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "Shared AI chat platform parsers and detection for Covai browser extensions.",
|
|
5
5
|
"type": "module",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=26.0.0"
|
|
8
|
+
},
|
|
6
9
|
"main": "ai/index.js",
|
|
7
10
|
"exports": {
|
|
8
11
|
".": "./ai/index.js",
|