decant-core 1.3.0 → 1.4.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/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 extractArtifacts(message) {
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.type === "tool_use" &&
99
- (content.name === "artifacts" || content.name === "create_file") &&
100
- content.display_content
101
- ) {
102
- const displayContent = content.display_content;
103
- if (displayContent.type === "code_block" && displayContent.code) {
104
- const filename = displayContent.filename || "artifact";
105
- const title = filename
106
- .split("/")
107
- .pop()
108
- .replace(/\.[^.]+$/, "");
109
- artifacts.push({
110
- title: title || "Artifact",
111
- language: displayContent.language || "text",
112
- content: displayContent.code.trim(),
113
- });
114
- } else if (
115
- displayContent.type === "json_block" &&
116
- displayContent.json_block
117
- ) {
118
- try {
119
- const data = JSON.parse(displayContent.json_block);
120
- if (data.filename) {
121
- const filename = data.filename;
122
- const title = filename
123
- .split("/")
124
- .pop()
125
- .replace(/\.[^.]+$/, "");
126
- artifacts.push({
127
- title: title || "Artifact",
128
- language: data.language || "text",
129
- content: (data.code || "").trim(),
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
- } catch (e) {
133
- console.warn(
134
- "[AI Exporter] Failed to parse tool use artifact json:",
135
- e,
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![${name}](${url})\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 combined = [...new Set([...strictCandidates, ...validFallbacks])];
360
-
361
- const allElements = combined.sort((a, b) => {
362
- return a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING
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.querySelectorAll("button").forEach((btn) => btn.remove());
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.querySelectorAll("button").forEach((btn) => btn.remove());
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";
@@ -61,6 +61,14 @@ export function sanitizeResponseContainer(container) {
61
61
  export function cleanMarkdownSpacing(markdown) {
62
62
  if (!markdown) return "";
63
63
  return markdown
64
+ .replace(
65
+ /(?:google|sn)\.\\?_setImageSrc\s*\(\s*['"][^'"]*['"]\s*,\s*['"][^'"]*['"]\s*\);?/gi,
66
+ "",
67
+ )
68
+ .replace(
69
+ /data:image(?:\\)?\/[a-zA-Z0-9+.-]+;base64,[A-Za-z0-9+/=\\]+/gi,
70
+ "",
71
+ )
64
72
  .replace(/[ \t]+$/gm, "")
65
73
  .replace(/\n{3,}/g, "\n\n")
66
74
  .trim();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "decant-core",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "Shared AI chat platform parsers and detection for Covai browser extensions.",
5
5
  "type": "module",
6
6
  "main": "ai/index.js",
@@ -52,9 +52,9 @@
52
52
  "access": "public"
53
53
  },
54
54
  "dependencies": {
55
- "@extractus/article-extractor": "^9.0.0",
55
+ "@extractus/article-extractor": "^9.0.1",
56
56
  "@mozilla/readability": "^0.6.0",
57
- "defuddle": "^0.19.2",
57
+ "defuddle": "^0.19.3",
58
58
  "turndown": "^7.2.4",
59
59
  "turndown-plugin-gfm": "^1.0.2"
60
60
  },