Refactored to use a build script
This commit is contained in:
1 parent
fe4b3c3e1b
commit
eeb611b7af
7 files changed
+383
-330
No files matched your search
@@ -0,0 +1 @@
|
|||||||
|
content.js
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set +e
|
||||||
|
|
||||||
|
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)"
|
||||||
|
cd "$DIR/.."
|
||||||
|
|
||||||
|
# Build script to combine all source files into a single content.js
|
||||||
|
|
||||||
|
# Create the output file and start the IIFE
|
||||||
|
echo '(function() {' >content.js
|
||||||
|
echo '' >>content.js
|
||||||
|
|
||||||
|
# Add each external library first
|
||||||
|
for file in lib/*; do
|
||||||
|
cat "$file" >>content.js
|
||||||
|
echo "// === $file ===" >>content.js
|
||||||
|
echo '' >>content.js
|
||||||
|
echo '' >>content.js
|
||||||
|
done
|
||||||
|
|
||||||
|
# Add each source file in order
|
||||||
|
for file in src/*; do
|
||||||
|
echo "// === $file ===" >>content.js
|
||||||
|
cat "$file" >>content.js
|
||||||
|
echo '' >>content.js
|
||||||
|
echo '' >>content.js
|
||||||
|
done
|
||||||
|
|
||||||
|
echo 'main();' >>content.js
|
||||||
|
|
||||||
|
# Close the IIFE
|
||||||
|
echo '})();' >>content.js
|
||||||
|
|
||||||
|
echo "Built content.js from source files"
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
(function() {
|
|
||||||
/*!
|
/*!
|
||||||
|
|
||||||
JSZip v3.10.1 - A JavaScript class for generating and reading zip files
|
JSZip v3.10.1 - A JavaScript class for generating and reading zip files
|
||||||
@@ -12,332 +11,3 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
!function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{("undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this).JSZip=e()}}(function(){return function s(a,o,h){function u(r,e){if(!o[r]){if(!a[r]){var t="function"==typeof require&&require;if(!e&&t)return t(r,!0);if(l)return l(r,!0);var n=new Error("Cannot find module '"+r+"'");throw n.code="MODULE_NOT_FOUND",n}var i=o[r]={exports:{}};a[r][0].call(i.exports,function(e){var t=a[r][1][e];return u(t||e)},i,i.exports,s,a,o,h)}return o[r].exports}for(var l="function"==typeof require&&require,e=0;e<h.length;e++)u(h[e]);return u}({1:[function(e,t,r){"use strict";var d=e("./utils"),c=e("./support"),p="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";r.encode=function(e){for(var t,r,n,i,s,a,o,h=[],u=0,l=e.length,f=l,c="string"!==d.getTypeOf(e);u<e.length;)f=l-u,n=c?(t=e[u++],r=u<l?e[u++]:0,u<l?e[u++]:0):(t=e.charCodeAt(u++),r=u<l?e.charCodeAt(u++):0,u<l?e.charCodeAt(u++):0),i=t>>2,s=(3&t)<<4|r>>4,a=1<f?(15&r)<<2|n>>6:64,o=2<f?63&n:64,h.push(p.charAt(i)+p.charAt(s)+p.charAt(a)+p.charAt(o));return h.join("")},r.decode=function(e){var t,r,n,i,s,a,o=0,h=0,u="data:";if(e.substr(0,u.length)===u)throw new Error("Invalid base64 input, it looks like a data url.");var l,f=3*(e=e.replace(/[^A-Za-z0-9+/=]/g,"")).length/4;if(e.charAt(e.length-1)===p.charAt(64)&&f--,e.charAt(e.length-2)===p.charAt(64)&&f--,f%1!=0)throw new Error("Invalid base64 input, bad content length.");for(l=c.uint8array?new Uint8Array(0|f):new Array(0|f);o<e.length;)t=p.indexOf(e.charAt(o++))<<2|(i=p.indexOf(e.charAt(o++)))>>4,r=(15&i)<<4|(s=p.indexOf(e.charAt(o++)))>>2,n=(3&s)<<6|(a=p.indexOf(e.charAt(o++))),l[h++]=t,64!==s&&(l[h++]=r),64!==a&&(l[h++]=n);return l}},{"./support":30,"./utils":32}],2:[function(e,t,r){"use strict";var n=e("./external"),i=e("./stream/DataWorker"),s=e("./stream/Crc32Probe"),a=e("./stream/DataLengthProbe");function o(e,t,r,n,i){this.compressedSize=e,this.uncompressedSize=t,this.crc32=r,this.compression=n,this.compressedContent=i}o.prototype={getContentWorker:function(){var e=new i(n.Promise.resolve(this.compressedContent)).pipe(this.compression.uncompressWorker()).pipe(new a("data_length")),t=this;return e.on("end",function(){if(this.streamInfo.data_length!==t.uncompressedSize)throw new Error("Bug : uncompressed data size mismatch")}),e},getCompressedWorker:function(){return new i(n.Promise.resolve(this.compressedContent)).withStreamInfo("compressedSize",this.compressedSize).withStreamInfo("uncompressedSize",this.uncompressedSize).withStreamInfo("crc32",this.crc32).withStreamInfo("compression",this.compression)}},o.createWorkerFrom=function(e,t,r){return e.pipe(new s).pipe(new a("uncompressedSize")).pipe(t.compressWorker(r)).pipe(new a("compressedSize")).withStreamInfo("compression",t)},t.exports=o},{"./external":6,"./stream/Crc32Probe":25,"./stream/DataLengthProbe":26,"./stream/DataWorker":27}],3:[function(e,t,r){"use strict";var n=e("./stream/GenericWorker");r.STORE={magic:"\0\0",compressWorker:function(){return new n("STORE compression")},uncompressWorker:function(){return new n("STORE decompression")}},r.DEFLATE=e("./flate")},{"./flate":7,"./stream/GenericWorker":28}],4:[function(e,t,r){"use strict";var n=e("./utils");var o=function(){for(var e,t=[],r=0;r<256;r++){e=r;for(var n=0;n<8;n++)e=1&e?3988292384^e>>>1:e>>>1;t[r]=e}return t}();t.exports=function(e,t){return void 0!==e&&e.length?"string"!==n.getTypeOf(e)?function(e,t,r,n){var i=o,s=n+r;e^=-1;for(var a=n;a<s;a++)e=e>>>8^i[255&(e^t[a])];return-1^e}(0|t,e,e.length,0):function(e,t,r,n){var i=o,s=n+r;e^=-1;for(var a=n;a<s;a++)e=e>>>8^i[255&(e^t.charCodeAt(a))];return-1^e}(0|t,e,e.length,0):0}},{"./utils":32}],5:[function(e,t,r){"use strict";r.base64=!1,r.binary=!1,r.dir=!1,r.createFolders=!0,r.date=null,r.compression=null,r.compressionOptions=null,r.comment=null,r.unixPermissions=null,r.dosPermissions=null},{}],6:[function(e,t,r){"use strict";var n=null;n="undefined"!=typeof Promise?Promise:e("lie"),t.exports={Promise:n}},{lie:37}],7:[function(e,t,r){"use strict";var n="undefined"!=typeof Uint8Array&&"undefined"!=typeof Uint16Array&&"undefined"!=typeof Uint32Array,i=e("pako"),s=e("./utils"),a=e("./stream/GenericWorker"),o=n?"uint8array":"array";function h(e,t){a.call(this,"FlateWorker/"+e),this._pako=null,this._pakoAction=e,this._pakoOptions=t,this.meta={}}r.magic="\b\0",s.inherits(h,a),h.prototype.processChunk=function(e){this.meta=e.meta,null===this._pako&&this._createPako(),this._pako.push(s.transformTo(o,e.data),!1)},h.prototype.flush=function(){a.prototype.flush.call(this),null===this._pako&&this._createPako(),this._pako.push([],!0)},h.prototype.cleanUp=function(){a.prototype.cleanUp.call(this),this._pako=null},h.prototype._createPako=function(){this._pako=new i[this._pakoAction]({raw:!0,level:this._pakoOptions.level||-1});var t=this;this._pako.onData=functioLine truncated
|
!function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{("undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this).JSZip=e()}}(function(){return function s(a,o,h){function u(r,e){if(!o[r]){if(!a[r]){var t="function"==typeof require&&require;if(!e&&t)return t(r,!0);if(l)return l(r,!0);var n=new Error("Cannot find module '"+r+"'");throw n.code="MODULE_NOT_FOUND",n}var i=o[r]={exports:{}};a[r][0].call(i.exports,function(e){var t=a[r][1][e];return u(t||e)},i,i.exports,s,a,o,h)}return o[r].exports}for(var l="function"==typeof require&&require,e=0;e<h.length;e++)u(h[e]);return u}({1:[function(e,t,r){"use strict";var d=e("./utils"),c=e("./support"),p="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";r.encode=function(e){for(var t,r,n,i,s,a,o,h=[],u=0,l=e.length,f=l,c="string"!==d.getTypeOf(e);u<e.length;)f=l-u,n=c?(t=e[u++],r=u<l?e[u++]:0,u<l?e[u++]:0):(t=e.charCodeAt(u++),r=u<l?e.charCodeAt(u++):0,u<l?e.charCodeAt(u++):0),i=t>>2,s=(3&t)<<4|r>>4,a=1<f?(15&r)<<2|n>>6:64,o=2<f?63&n:64,h.push(p.charAt(i)+p.charAt(s)+p.charAt(a)+p.charAt(o));return h.join("")},r.decode=function(e){var t,r,n,i,s,a,o=0,h=0,u="data:";if(e.substr(0,u.length)===u)throw new Error("Invalid base64 input, it looks like a data url.");var l,f=3*(e=e.replace(/[^A-Za-z0-9+/=]/g,"")).length/4;if(e.charAt(e.length-1)===p.charAt(64)&&f--,e.charAt(e.length-2)===p.charAt(64)&&f--,f%1!=0)throw new Error("Invalid base64 input, bad content length.");for(l=c.uint8array?new Uint8Array(0|f):new Array(0|f);o<e.length;)t=p.indexOf(e.charAt(o++))<<2|(i=p.indexOf(e.charAt(o++)))>>4,r=(15&i)<<4|(s=p.indexOf(e.charAt(o++)))>>2,n=(3&s)<<6|(a=p.indexOf(e.charAt(o++))),l[h++]=t,64!==s&&(l[h++]=r),64!==a&&(l[h++]=n);return l}},{"./support":30,"./utils":32}],2:[function(e,t,r){"use strict";var n=e("./external"),i=e("./stream/DataWorker"),s=e("./stream/Crc32Probe"),a=e("./stream/DataLengthProbe");function o(e,t,r,n,i){this.compressedSize=e,this.uncompressedSize=t,this.crc32=r,this.compression=n,this.compressedContent=i}o.prototype={getContentWorker:function(){var e=new i(n.Promise.resolve(this.compressedContent)).pipe(this.compression.uncompressWorker()).pipe(new a("data_length")),t=this;return e.on("end",function(){if(this.streamInfo.data_length!==t.uncompressedSize)throw new Error("Bug : uncompressed data size mismatch")}),e},getCompressedWorker:function(){return new i(n.Promise.resolve(this.compressedContent)).withStreamInfo("compressedSize",this.compressedSize).withStreamInfo("uncompressedSize",this.uncompressedSize).withStreamInfo("crc32",this.crc32).withStreamInfo("compression",this.compression)}},o.createWorkerFrom=function(e,t,r){return e.pipe(new s).pipe(new a("uncompressedSize")).pipe(t.compressWorker(r)).pipe(new a("compressedSize")).withStreamInfo("compression",t)},t.exports=o},{"./external":6,"./stream/Crc32Probe":25,"./stream/DataLengthProbe":26,"./stream/DataWorker":27}],3:[function(e,t,r){"use strict";var n=e("./stream/GenericWorker");r.STORE={magic:"\0\0",compressWorker:function(){return new n("STORE compression")},uncompressWorker:function(){return new n("STORE decompression")}},r.DEFLATE=e("./flate")},{"./flate":7,"./stream/GenericWorker":28}],4:[function(e,t,r){"use strict";var n=e("./utils");var o=function(){for(var e,t=[],r=0;r<256;r++){e=r;for(var n=0;n<8;n++)e=1&e?3988292384^e>>>1:e>>>1;t[r]=e}return t}();t.exports=function(e,t){return void 0!==e&&e.length?"string"!==n.getTypeOf(e)?function(e,t,r,n){var i=o,s=n+r;e^=-1;for(var a=n;a<s;a++)e=e>>>8^i[255&(e^t[a])];return-1^e}(0|t,e,e.length,0):function(e,t,r,n){var i=o,s=n+r;e^=-1;for(var a=n;a<s;a++)e=e>>>8^i[255&(e^t.charCodeAt(a))];return-1^e}(0|t,e,e.length,0):0}},{"./utils":32}],5:[function(e,t,r){"use strict";r.base64=!1,r.binary=!1,r.dir=!1,r.createFolders=!0,r.date=null,r.compression=null,r.compressionOptions=null,r.comment=null,r.unixPermissions=null,r.dosPermissions=null},{}],6:[function(e,t,r){"use strict";var n=null;n="undefined"!=typeof Promise?Promise:e("lie"),t.exports={Promise:n}},{lie:37}],7:[function(e,t,r){"use strict";var n="undefined"!=typeof Uint8Array&&"undefined"!=typeof Uint16Array&&"undefined"!=typeof Uint32Array,i=e("pako"),s=e("./utils"),a=e("./stream/GenericWorker"),o=n?"uint8array":"array";function h(e,t){a.call(this,"FlateWorker/"+e),this._pako=null,this._pakoAction=e,this._pakoOptions=t,this.meta={}}r.magic="\b\0",s.inherits(h,a),h.prototype.processChunk=function(e){this.meta=e.meta,null===this._pako&&this._createPako(),this._pako.push(s.transformTo(o,e.data),!1)},h.prototype.flush=function(){a.prototype.flush.call(this),null===this._pako&&this._createPako(),this._pako.push([],!0)},h.prototype.cleanUp=function(){a.prototype.cleanUp.call(this),this._pako=null},h.prototype._createPako=function(){this._pako=new i[this._pakoAction]({raw:!0,level:this._pakoOptions.level||-1});var t=this;this._pako.onData=functioLine truncated
|
||||||
|
|
||||||
|
|
||||||
console.log('Content script loaded');
|
|
||||||
|
|
||||||
// Check if we're in a project chat by looking for the project link with forward slash
|
|
||||||
function isProjectChat() {
|
|
||||||
const projectLink = document.querySelector('a[href*="/project/"]');
|
|
||||||
if (!projectLink) {
|
|
||||||
console.log('Not in a project chat - no project link found');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Additional check: look for the forward slash separator in the project breadcrumb
|
|
||||||
const breadcrumbWithSlash = projectLink.querySelector('span.opacity-50');
|
|
||||||
if (!breadcrumbWithSlash || !breadcrumbWithSlash.textContent.includes('/')) {
|
|
||||||
console.log('Not in a project chat - no forward slash separator found');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ArtifactScraper = {
|
|
||||||
|
|
||||||
openArtifactList() {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
// Find the global artifact menu button (has the hamburger/list icon)
|
|
||||||
const artifactMenuButton = document.querySelector('button[aria-haspopup="menu"] svg[viewBox="0 0 256 256"] path[d*="M80,64a8,8,0,0,1,8-8H216"]');
|
|
||||||
const menuButton = artifactMenuButton?.closest('button');
|
|
||||||
|
|
||||||
if (!menuButton) {
|
|
||||||
reject(new Error('Global artifact menu button not found'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let hasResolved = false;
|
|
||||||
|
|
||||||
// Listen for menu opening
|
|
||||||
const observer = new MutationObserver((mutations) => {
|
|
||||||
mutations.forEach((mutation) => {
|
|
||||||
if (mutation.type === 'childList') {
|
|
||||||
const addedNodes = Array.from(mutation.addedNodes);
|
|
||||||
const menuAdded = addedNodes.some(node =>
|
|
||||||
node.nodeType === Node.ELEMENT_NODE &&
|
|
||||||
(node.querySelector('li[role="none"] div[role="menuitem"]') ||
|
|
||||||
node.getAttribute('role') === 'menu' ||
|
|
||||||
node.querySelector('[role="menu"]'))
|
|
||||||
);
|
|
||||||
|
|
||||||
if (menuAdded) {
|
|
||||||
console.log('Artifact list menu has been opened');
|
|
||||||
observer.disconnect();
|
|
||||||
if (!hasResolved) {
|
|
||||||
hasResolved = true;
|
|
||||||
resolve(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
observer.observe(document.body, { childList: true, subtree: true });
|
|
||||||
|
|
||||||
// Focus and send Enter key event to open menu. Click doesn't actually work,
|
|
||||||
// likely due to react pseudoevents.
|
|
||||||
menuButton.focus();
|
|
||||||
const enterEvent = new KeyboardEvent('keydown', {
|
|
||||||
key: 'Enter',
|
|
||||||
code: 'Enter',
|
|
||||||
bubbles: true,
|
|
||||||
cancelable: true
|
|
||||||
});
|
|
||||||
menuButton.dispatchEvent(enterEvent);
|
|
||||||
|
|
||||||
// Fallback timeout so we don't accidentally listen forever.
|
|
||||||
setTimeout(() => {
|
|
||||||
if (!hasResolved) {
|
|
||||||
observer.disconnect();
|
|
||||||
hasResolved = true;
|
|
||||||
reject(new Error('Failed to open artifact list menu - timeout'));
|
|
||||||
}
|
|
||||||
}, 3000);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
// Once we're sure the menu is open, this will give us the list of artifacts and tell
|
|
||||||
// us which one is currently selected.
|
|
||||||
getArtifactListItems() {
|
|
||||||
const menuItems = document.querySelectorAll('li[role="none"] div[role="menuitem"]');
|
|
||||||
const artifactHash = {};
|
|
||||||
|
|
||||||
if (menuItems.length === 0) {
|
|
||||||
console.log('No artifact list items found');
|
|
||||||
return artifactHash;
|
|
||||||
}
|
|
||||||
|
|
||||||
menuItems.forEach((item, index) => {
|
|
||||||
const filenameDiv = item.querySelector('div.line-clamp-2');
|
|
||||||
if (filenameDiv) {
|
|
||||||
const filename = filenameDiv.textContent.trim();
|
|
||||||
// Check for aria-selected attribute or other indicators of selection
|
|
||||||
const isSelected = item.getAttribute('aria-selected') === 'true' ||
|
|
||||||
item.classList.contains('bg-accent-secondary-100') ||
|
|
||||||
item.classList.contains('border-accent-secondary-100');
|
|
||||||
|
|
||||||
artifactHash[filename] = isSelected;
|
|
||||||
console.log(`Found artifact: ${filename} (${isSelected ? 'selected' : 'unselected'})`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return artifactHash;
|
|
||||||
},
|
|
||||||
|
|
||||||
// When the menu is open, this changes the active artifact to that with the given filename.
|
|
||||||
selectArtifactListItem(filename) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const menuItems = document.querySelectorAll('li[role="none"] div[role="menuitem"]');
|
|
||||||
let targetItem = null;
|
|
||||||
|
|
||||||
// Find the matching artifact by filename
|
|
||||||
menuItems.forEach(item => {
|
|
||||||
const filenameDiv = item.querySelector('div.line-clamp-2');
|
|
||||||
if (filenameDiv && filenameDiv.textContent.trim() === filename) {
|
|
||||||
targetItem = item;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!targetItem) {
|
|
||||||
reject(new Error(`Could not find artifact item for: ${filename}`));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`Selecting artifact: ${filename}`);
|
|
||||||
|
|
||||||
// Focus and send Enter key event to select the item
|
|
||||||
// Click may or may not work here, but keydown was needed elsewhere, so we're
|
|
||||||
// sticking to it here.
|
|
||||||
targetItem.focus();
|
|
||||||
const enterEvent = new KeyboardEvent('keydown', {
|
|
||||||
key: 'Enter',
|
|
||||||
code: 'Enter',
|
|
||||||
bubbles: true,
|
|
||||||
cancelable: true
|
|
||||||
});
|
|
||||||
targetItem.dispatchEvent(enterEvent);
|
|
||||||
|
|
||||||
// Wait for artifact to load
|
|
||||||
setTimeout(() => {
|
|
||||||
resolve(true);
|
|
||||||
}, 1000);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
// Get artifact content and handle selection if needed
|
|
||||||
getArtifact(filename, isCurrentlySelected) {
|
|
||||||
return new Promise(async (resolve, reject) => {
|
|
||||||
if (!isCurrentlySelected) {
|
|
||||||
const menuOpen = document.querySelector('li[role="none"] div[role="menuitem"]');
|
|
||||||
if (!menuOpen) {
|
|
||||||
try {
|
|
||||||
await this.openArtifactList();
|
|
||||||
} catch (error) {
|
|
||||||
reject(new Error(`Failed to open menu to select: ${filename} - ${error.message}`));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this.selectArtifactListItem(filename);
|
|
||||||
} catch (error) {
|
|
||||||
reject(new Error(`Failed to select artifact: ${filename} - ${error.message}`));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const codeBlock = document.querySelector('div.right-0 code');
|
|
||||||
if (!codeBlock) {
|
|
||||||
reject(new Error(`No code block found for artifact: ${filename}`));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
resolve(codeBlock.textContent || codeBlock.innerText || '');
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
// Main collection method
|
|
||||||
async collectArtifacts() {
|
|
||||||
const artifactCollection = {}; // We'll keep a map of filename: content here
|
|
||||||
// There's no path information in a Claude artifact, just filenames.
|
|
||||||
// However, the LLM itself is smart enough to know where the files should go.
|
|
||||||
// So our strategy is to make it save those paths to a files.txt artifact and store
|
|
||||||
// that separately for use in making a final map of artifacts to files for our zip export.
|
|
||||||
let fileMap = null;
|
|
||||||
|
|
||||||
await this.openArtifactList();
|
|
||||||
const artifactList = this.getArtifactListItems();
|
|
||||||
|
|
||||||
if (Object.keys(artifactList).length === 0) {
|
|
||||||
console.log('No artifacts found in list');
|
|
||||||
return { artifacts: artifactCollection, fileMap: fileMap };
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`Found ${Object.keys(artifactList).length} artifacts to collect`);
|
|
||||||
|
|
||||||
for (const [filename, isSelected] of Object.entries(artifactList)) {
|
|
||||||
try {
|
|
||||||
const content = await this.getArtifact(filename, isSelected);
|
|
||||||
|
|
||||||
if (filename === 'files.txt') {
|
|
||||||
// This is our special llm-generated mapping that we'll need to finish our export.
|
|
||||||
fileMap = content;
|
|
||||||
console.log(`✓ Found files.txt - storing in fileMap`);
|
|
||||||
} else {
|
|
||||||
artifactCollection[filename] = content;
|
|
||||||
console.log(`✓ Collected: ${filename}`);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log(`✗ Failed to collect: ${filename} - ${error.message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Small delay between artifacts
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 500));
|
|
||||||
}
|
|
||||||
|
|
||||||
return { artifacts: artifactCollection, fileMap: fileMap };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
function exportArtifacts(artifactCollection, fileMap) {
|
|
||||||
if (!artifactCollection || artifactCollection.length === 0) {
|
|
||||||
console.warn("No non-filemap artifacts were found. Ask the LLM to copy them from the project to this chat first.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!fileMap) {
|
|
||||||
console.warn("No filemap was found. Ask the LLM to generate one. See the README for a sample prompt.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Parse the file map into paths
|
|
||||||
const lines = fileMap.split('\n').filter(line => line.trim());
|
|
||||||
const pathMapping = {};
|
|
||||||
let currentPath = '';
|
|
||||||
|
|
||||||
lines.forEach(line => {
|
|
||||||
const trimmedLine = line.trim();
|
|
||||||
|
|
||||||
// Check if this is a directory (ends with /)
|
|
||||||
if (trimmedLine.endsWith('/')) {
|
|
||||||
currentPath = trimmedLine;
|
|
||||||
} else if (trimmedLine && !trimmedLine.startsWith('/')) {
|
|
||||||
if (line.startsWith(' ')) {
|
|
||||||
// File is indented, so it's in the current directory
|
|
||||||
pathMapping[trimmedLine] = currentPath + trimmedLine;
|
|
||||||
} else {
|
|
||||||
// File is at root level
|
|
||||||
pathMapping[trimmedLine] = trimmedLine;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create final map with full paths and contents
|
|
||||||
const finalMap = {};
|
|
||||||
Object.entries(artifactCollection).forEach(([filename, content]) => {
|
|
||||||
const fullPath = pathMapping[filename] || filename; // Use filename as-is if not in mapping
|
|
||||||
finalMap[fullPath] = content;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create zip file
|
|
||||||
const zip = new JSZip();
|
|
||||||
|
|
||||||
// Add each file to the zip
|
|
||||||
Object
|
|
||||||
.entries(finalMap)
|
|
||||||
.forEach(([fullPath, content]) => {
|
|
||||||
// Handle directory structure
|
|
||||||
const pathParts = fullPath.split('/');
|
|
||||||
|
|
||||||
if (pathParts.length > 1) {
|
|
||||||
// File is in a directory
|
|
||||||
const filename = pathParts.pop();
|
|
||||||
const folderPath = pathParts.join('/');
|
|
||||||
const folder = zip.folder(folderPath);
|
|
||||||
folder.file(filename, content);
|
|
||||||
} else {
|
|
||||||
// File is at root level
|
|
||||||
zip.file(fullPath, content);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Generate timestamp for filename
|
|
||||||
const now = new Date();
|
|
||||||
const timestamp = now.getFullYear() + '-' +
|
|
||||||
String(now.getMonth() + 1).padStart(2, '0') + '-' +
|
|
||||||
String(now.getDate()).padStart(2, '0');
|
|
||||||
const zipFilename = `${timestamp}_claude-artifacts.zip`;
|
|
||||||
|
|
||||||
// Generate and download the zip
|
|
||||||
zip.generateAsync({type: "blob"})
|
|
||||||
.then(function(content) {
|
|
||||||
// Create download link
|
|
||||||
const url = URL.createObjectURL(content);
|
|
||||||
const a = document.createElement('a');
|
|
||||||
a.href = url;
|
|
||||||
a.download = zipFilename;
|
|
||||||
document.body.appendChild(a);
|
|
||||||
a.click();
|
|
||||||
document.body.removeChild(a);
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
|
|
||||||
console.log(`✓ Downloaded: ${zipFilename}`);
|
|
||||||
})
|
|
||||||
.catch(function(error) {
|
|
||||||
console.error('Failed to generate zip:', error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Main function
|
|
||||||
async function main() {
|
|
||||||
console.log('Running artifact collector');
|
|
||||||
|
|
||||||
if (!isProjectChat()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await ArtifactScraper.collectArtifacts();
|
|
||||||
exportArtifacts(result.artifacts, result.fileMap);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run the main function
|
|
||||||
main();
|
|
||||||
})();
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
const ProjectDetector = {
|
||||||
|
isProjectChat() {
|
||||||
|
const projectLink = document.querySelector('a[href*="/project/"]');
|
||||||
|
if (!projectLink) {
|
||||||
|
console.log('Not in a project chat - no project link found');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Additional check: look for the forward slash separator in the project breadcrumb
|
||||||
|
const breadcrumbWithSlash = projectLink.querySelector('span.opacity-50');
|
||||||
|
if (!breadcrumbWithSlash || !breadcrumbWithSlash.textContent.includes('/')) {
|
||||||
|
console.log('Not in a project chat - no forward slash separator found');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};// No exports needed - will be in same scope after build
|
||||||
+113
@@ -0,0 +1,113 @@
|
|||||||
|
const ArtifactExporter = {
|
||||||
|
|
||||||
|
parseFileMap(fileMap) {
|
||||||
|
const lines = fileMap.split('\n').filter(line => line.trim());
|
||||||
|
const pathMapping = {};
|
||||||
|
let currentPath = '';
|
||||||
|
|
||||||
|
lines.forEach(line => {
|
||||||
|
const trimmedLine = line.trim();
|
||||||
|
|
||||||
|
// Check if this is a directory (ends with /)
|
||||||
|
if (trimmedLine.endsWith('/')) {
|
||||||
|
currentPath = trimmedLine;
|
||||||
|
} else if (trimmedLine && !trimmedLine.startsWith('/')) {
|
||||||
|
if (line.startsWith(' ')) {
|
||||||
|
// File is indented, so it's in the current directory
|
||||||
|
pathMapping[trimmedLine] = currentPath + trimmedLine;
|
||||||
|
} else {
|
||||||
|
// File is at root level
|
||||||
|
pathMapping[trimmedLine] = trimmedLine;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return pathMapping;
|
||||||
|
},
|
||||||
|
|
||||||
|
createFinalMapping(artifactCollection, pathMapping) {
|
||||||
|
const finalMap = {};
|
||||||
|
Object.entries(artifactCollection).forEach(([filename, content]) => {
|
||||||
|
const fullPath = pathMapping[filename] || filename; // Use filename as-is if not in mapping
|
||||||
|
finalMap[fullPath] = content;
|
||||||
|
});
|
||||||
|
return finalMap;
|
||||||
|
},
|
||||||
|
|
||||||
|
generateZipFilename() {
|
||||||
|
const now = new Date();
|
||||||
|
const timestamp = now.getFullYear() + '-' +
|
||||||
|
String(now.getMonth() + 1).padStart(2, '0') + '-' +
|
||||||
|
String(now.getDate()).padStart(2, '0');
|
||||||
|
return `${timestamp}_claude-artifacts.zip`;
|
||||||
|
},
|
||||||
|
|
||||||
|
addFilesToZip(zip, finalMap) {
|
||||||
|
Object.entries(finalMap).forEach(([fullPath, content]) => {
|
||||||
|
// Handle directory structure
|
||||||
|
const pathParts = fullPath.split('/');
|
||||||
|
|
||||||
|
if (pathParts.length > 1) {
|
||||||
|
// File is in a directory
|
||||||
|
const filename = pathParts.pop();
|
||||||
|
const folderPath = pathParts.join('/');
|
||||||
|
const folder = zip.folder(folderPath);
|
||||||
|
folder.file(filename, content);
|
||||||
|
} else {
|
||||||
|
// File is at root level
|
||||||
|
zip.file(fullPath, content);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
downloadZip(content, filename) {
|
||||||
|
const url = URL.createObjectURL(content);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = filename;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
|
||||||
|
console.log(`✓ Downloaded: ${filename}`);
|
||||||
|
},
|
||||||
|
|
||||||
|
exportArtifacts(artifactCollection, fileMap) {
|
||||||
|
// Validation
|
||||||
|
if (!artifactCollection || Object.keys(artifactCollection).length === 0) {
|
||||||
|
console.warn("No non-filemap artifacts were found. Ask the LLM to copy them from the project to this chat first.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!fileMap) {
|
||||||
|
console.warn("No filemap was found. Ask the LLM to generate one. See the README for a sample prompt.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the file map into paths
|
||||||
|
const pathMapping = this.parseFileMap(fileMap);
|
||||||
|
|
||||||
|
// Create final map with full paths and contents
|
||||||
|
const finalMap = this.createFinalMapping(artifactCollection, pathMapping);
|
||||||
|
|
||||||
|
// Create zip file
|
||||||
|
const zip = new JSZip();
|
||||||
|
|
||||||
|
// Add each file to the zip
|
||||||
|
this.addFilesToZip(zip, finalMap);
|
||||||
|
|
||||||
|
// Generate filename
|
||||||
|
const zipFilename = this.generateZipFilename();
|
||||||
|
|
||||||
|
// Generate and download the zip
|
||||||
|
zip.generateAsync({type: "blob"})
|
||||||
|
.then((content) => {
|
||||||
|
this.downloadZip(content, zipFilename);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed to generate zip:', error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// No exports needed - will be in same scope after build
|
||||||
+15
@@ -0,0 +1,15 @@
|
|||||||
|
async function main() {
|
||||||
|
console.log('Running artifact collector');
|
||||||
|
|
||||||
|
if (!ProjectDetector.isProjectChat()) {
|
||||||
|
console.log('Not in a project chat, exiting');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await ArtifactScraper.collectArtifacts();
|
||||||
|
ArtifactExporter.exportArtifacts(result.artifacts, result.fileMap);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error during artifact collection and export:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
+202
@@ -0,0 +1,202 @@
|
|||||||
|
const ArtifactScraper = {
|
||||||
|
|
||||||
|
openArtifactList() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
// Find the global artifact menu button (has the hamburger/list icon)
|
||||||
|
const artifactMenuButton = document.querySelector('button[aria-haspopup="menu"] svg[viewBox="0 0 256 256"] path[d*="M80,64a8,8,0,0,1,8-8H216"]');
|
||||||
|
const menuButton = artifactMenuButton?.closest('button');
|
||||||
|
|
||||||
|
if (!menuButton) {
|
||||||
|
reject(new Error('Global artifact menu button not found'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let hasResolved = false;
|
||||||
|
|
||||||
|
// Listen for menu opening
|
||||||
|
const observer = new MutationObserver((mutations) => {
|
||||||
|
mutations.forEach((mutation) => {
|
||||||
|
if (mutation.type === 'childList') {
|
||||||
|
const addedNodes = Array.from(mutation.addedNodes);
|
||||||
|
const menuAdded = addedNodes.some(node =>
|
||||||
|
node.nodeType === Node.ELEMENT_NODE &&
|
||||||
|
(node.querySelector('li[role="none"] div[role="menuitem"]') ||
|
||||||
|
node.getAttribute('role') === 'menu' ||
|
||||||
|
node.querySelector('[role="menu"]'))
|
||||||
|
);
|
||||||
|
|
||||||
|
if (menuAdded) {
|
||||||
|
console.log('Artifact list menu has been opened');
|
||||||
|
observer.disconnect();
|
||||||
|
if (!hasResolved) {
|
||||||
|
hasResolved = true;
|
||||||
|
resolve(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
observer.observe(document.body, { childList: true, subtree: true });
|
||||||
|
|
||||||
|
// Focus and send Enter key event to open menu. Click doesn't actually work,
|
||||||
|
// likely due to react pseudoevents.
|
||||||
|
menuButton.focus();
|
||||||
|
const enterEvent = new KeyboardEvent('keydown', {
|
||||||
|
key: 'Enter',
|
||||||
|
code: 'Enter',
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: true
|
||||||
|
});
|
||||||
|
menuButton.dispatchEvent(enterEvent);
|
||||||
|
|
||||||
|
// Fallback timeout so we don't accidentally listen forever.
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!hasResolved) {
|
||||||
|
observer.disconnect();
|
||||||
|
hasResolved = true;
|
||||||
|
reject(new Error('Failed to open artifact list menu - timeout'));
|
||||||
|
}
|
||||||
|
}, 3000);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
getArtifactListItems() {
|
||||||
|
const menuItems = document.querySelectorAll('li[role="none"] div[role="menuitem"]');
|
||||||
|
const artifactHash = {};
|
||||||
|
|
||||||
|
if (menuItems.length === 0) {
|
||||||
|
console.log('No artifact list items found');
|
||||||
|
return artifactHash;
|
||||||
|
}
|
||||||
|
|
||||||
|
menuItems.forEach((item, index) => {
|
||||||
|
const filenameDiv = item.querySelector('div.line-clamp-2');
|
||||||
|
if (filenameDiv) {
|
||||||
|
const filename = filenameDiv.textContent.trim();
|
||||||
|
// Check for aria-selected attribute or other indicators of selection
|
||||||
|
const isSelected = item.getAttribute('aria-selected') === 'true' ||
|
||||||
|
item.classList.contains('bg-accent-secondary-100') ||
|
||||||
|
item.classList.contains('border-accent-secondary-100');
|
||||||
|
|
||||||
|
artifactHash[filename] = isSelected;
|
||||||
|
console.log(`Found artifact: ${filename} (${isSelected ? 'selected' : 'unselected'})`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return artifactHash;
|
||||||
|
},
|
||||||
|
|
||||||
|
selectArtifactListItem(filename) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const menuItems = document.querySelectorAll('li[role="none"] div[role="menuitem"]');
|
||||||
|
let targetItem = null;
|
||||||
|
|
||||||
|
// Find the matching artifact by filename
|
||||||
|
menuItems.forEach(item => {
|
||||||
|
const filenameDiv = item.querySelector('div.line-clamp-2');
|
||||||
|
if (filenameDiv && filenameDiv.textContent.trim() === filename) {
|
||||||
|
targetItem = item;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!targetItem) {
|
||||||
|
reject(new Error(`Could not find artifact item for: ${filename}`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Selecting artifact: ${filename}`);
|
||||||
|
|
||||||
|
// Focus and send Enter key event to select the item
|
||||||
|
// Click may or may not work here, but keydown was needed elsewhere, so we're
|
||||||
|
// sticking to it here.
|
||||||
|
targetItem.focus();
|
||||||
|
const enterEvent = new KeyboardEvent('keydown', {
|
||||||
|
key: 'Enter',
|
||||||
|
code: 'Enter',
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: true
|
||||||
|
});
|
||||||
|
targetItem.dispatchEvent(enterEvent);
|
||||||
|
|
||||||
|
// Wait for artifact to load
|
||||||
|
setTimeout(() => {
|
||||||
|
resolve(true);
|
||||||
|
}, 1000);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
getArtifact(filename, isCurrentlySelected) {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
if (!isCurrentlySelected) {
|
||||||
|
const menuOpen = document.querySelector('li[role="none"] div[role="menuitem"]');
|
||||||
|
if (!menuOpen) {
|
||||||
|
try {
|
||||||
|
await this.openArtifactList();
|
||||||
|
} catch (error) {
|
||||||
|
reject(new Error(`Failed to open menu to select: ${filename} - ${error.message}`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.selectArtifactListItem(filename);
|
||||||
|
} catch (error) {
|
||||||
|
reject(new Error(`Failed to select artifact: ${filename} - ${error.message}`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const codeBlock = document.querySelector('div.right-0 code');
|
||||||
|
if (!codeBlock) {
|
||||||
|
reject(new Error(`No code block found for artifact: ${filename}`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(codeBlock.textContent || codeBlock.innerText || '');
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async collectArtifacts() {
|
||||||
|
const artifactCollection = {};
|
||||||
|
// There's no path information in a Claude artifact, just filenames.
|
||||||
|
// However, the LLM itself is smart enough to know where the files should go.
|
||||||
|
// So our strategy is to make it save those paths to a files.txt artifact and store
|
||||||
|
// that separately for use in making a final map of artifacts to files for our zip export.
|
||||||
|
let fileMap = null;
|
||||||
|
|
||||||
|
await this.openArtifactList();
|
||||||
|
const artifactList = this.getArtifactListItems();
|
||||||
|
|
||||||
|
if (Object.keys(artifactList).length === 0) {
|
||||||
|
console.log('No artifacts found in list');
|
||||||
|
return { artifacts: artifactCollection, fileMap: fileMap };
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Found ${Object.keys(artifactList).length} artifacts to collect`);
|
||||||
|
|
||||||
|
for (const [filename, isSelected] of Object.entries(artifactList)) {
|
||||||
|
try {
|
||||||
|
const content = await this.getArtifact(filename, isSelected);
|
||||||
|
|
||||||
|
if (filename === 'files.txt') {
|
||||||
|
// This is our special llm-generated mapping that we'll need to finish our export.
|
||||||
|
fileMap = content;
|
||||||
|
console.log(`✓ Found files.txt - storing in fileMap`);
|
||||||
|
} else {
|
||||||
|
artifactCollection[filename] = content;
|
||||||
|
console.log(`✓ Collected: ${filename}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(`✗ Failed to collect: ${filename} - ${error.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Small delay between artifacts
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 500));
|
||||||
|
}
|
||||||
|
|
||||||
|
return { artifacts: artifactCollection, fileMap: fileMap };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// No exports needed - will be in same scope after build
|
||||||
Reference in new issue
Block a user