Compare commits
10 Commits
fe4b3c3e1b
...
e68bfc1d12
| Author | SHA1 | Date | |
|---|---|---|---|
| e68bfc1d12 | |||
| 12e0f709bc | |||
| fbede823e5 | |||
| 111e3ee048 | |||
| 4a41a38cbc | |||
| 53df3702ea | |||
| 437c41ec51 | |||
| 28bbce8b79 | |||
| dfaec2ccd3 | |||
| eeb611b7af |
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
content.js
|
||||||
@ -35,7 +35,5 @@ There's a `<-` arrow on the top right you can click to open the sidebar if it's
|
|||||||
If you've followed the other steps, you should see some activity in the chat page's console, then see a zip file downloaded with all the artifacts correctly mapped inside.
|
If you've followed the other steps, you should see some activity in the chat page's console, then see a zip file downloaded with all the artifacts correctly mapped inside.
|
||||||
|
|
||||||
|
|
||||||
## Changes under consideration
|
## Known bugs
|
||||||
1. Optionally scrape project-level artifacts instead of chat-level artifacts
|
1. Fix the scraper to work with single-artifact chats that don't have a selector button. Not sure how we'll get the artifact name here.
|
||||||
2. Auto-open the sidebar as needed
|
|
||||||
3. Rename the zip file based on the project's name
|
|
||||||
|
|||||||
@ -2,7 +2,7 @@ browser.browserAction.onClicked.addListener((tab) => {
|
|||||||
console.log('Extension clicked - triggering artifact collection.');
|
console.log('Extension clicked - triggering artifact collection.');
|
||||||
|
|
||||||
// Check if we're on the correct domain and path
|
// Check if we're on the correct domain and path
|
||||||
if (!tab.url.startsWith('https://claude.ai/chat/')) {
|
if (!tab.url.startsWith('https://claude.ai/chat') && !tab.url.startsWith('https://claude.ai/project')) {
|
||||||
console.log('Not on claude.ai chat page');
|
console.log('Not on claude.ai chat page');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
35
bin/build
Executable file
35
bin/build
Executable file
@ -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"
|
||||||
File diff suppressed because one or more lines are too long
172
src/exporter.js
Normal file
172
src/exporter.js
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
const ArtifactExporter = {
|
||||||
|
|
||||||
|
parseFileMap(fileMap) {
|
||||||
|
const lines = fileMap.split('\n').filter(line => line.trim());
|
||||||
|
const pathMapping = {};
|
||||||
|
let currentPath = '';
|
||||||
|
|
||||||
|
lines.forEach(line => {
|
||||||
|
const trimmedLine = line.trim();
|
||||||
|
|
||||||
|
if (trimmedLine.endsWith('/')) {
|
||||||
|
currentPath = trimmedLine;
|
||||||
|
} else if (trimmedLine && !trimmedLine.startsWith('/')) {
|
||||||
|
if (line.startsWith(' ')) {
|
||||||
|
pathMapping[trimmedLine] = currentPath + trimmedLine;
|
||||||
|
} else {
|
||||||
|
pathMapping[trimmedLine] = trimmedLine;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return pathMapping;
|
||||||
|
},
|
||||||
|
|
||||||
|
extractFilenamesFromFileMap(fileMap) {
|
||||||
|
const lines = fileMap.split('\n').filter(line => line.trim());
|
||||||
|
const filenames = [];
|
||||||
|
|
||||||
|
lines.forEach(line => {
|
||||||
|
const trimmedLine = line.trim();
|
||||||
|
|
||||||
|
if (trimmedLine.endsWith('/') || !trimmedLine || trimmedLine.startsWith('/')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const filename = trimmedLine.replace(/^\s+/, '');
|
||||||
|
if (filename) {
|
||||||
|
filenames.push(filename);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return filenames.sort((a, b) => b.length - a.length);
|
||||||
|
},
|
||||||
|
|
||||||
|
extractFilenameFromArtifactName(artifactName, sortedFilenames) {
|
||||||
|
for (const filename of sortedFilenames) {
|
||||||
|
const lowerArtifactName = artifactName.toLowerCase();
|
||||||
|
const lowerFilename = filename.toLowerCase();
|
||||||
|
|
||||||
|
if (lowerArtifactName.includes(lowerFilename)) {
|
||||||
|
return filename;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return artifactName;
|
||||||
|
},
|
||||||
|
|
||||||
|
createFinalMapping(artifactCollection, pathMapping) {
|
||||||
|
const finalMap = {};
|
||||||
|
|
||||||
|
const sortedFilenames = this.extractFilenamesFromFileMap(Object.keys(pathMapping).join('\n'));
|
||||||
|
|
||||||
|
Object.entries(artifactCollection).forEach(([artifactName, content]) => {
|
||||||
|
const extractedFilename = this.extractFilenameFromArtifactName(artifactName, sortedFilenames);
|
||||||
|
const fullPath = pathMapping[extractedFilename] || extractedFilename;
|
||||||
|
|
||||||
|
if (artifactName !== extractedFilename) {
|
||||||
|
console.log(`Mapped artifact "${artifactName}" -> found filename "${extractedFilename}" -> "${fullPath}"`);
|
||||||
|
} else {
|
||||||
|
console.log(`Mapped artifact "${artifactName}" -> "${fullPath}"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
finalMap[fullPath] = content;
|
||||||
|
});
|
||||||
|
|
||||||
|
return finalMap;
|
||||||
|
},
|
||||||
|
|
||||||
|
toSnakeCase(str) {
|
||||||
|
return str
|
||||||
|
.replace(/([a-z])([A-Z])/g, '$1_$2') // camelCase to snake_case
|
||||||
|
.replace(/[\s\-\.]+/g, '_') // spaces, hyphens, dots to underscores
|
||||||
|
.replace(/[^\w]/g, '') // remove non-word characters
|
||||||
|
.toLowerCase() // convert to lowercase
|
||||||
|
.replace(/^_+|_+$/g, '') // trim leading/trailing underscores
|
||||||
|
.replace(/_+/g, '_'); // collapse multiple underscores
|
||||||
|
},
|
||||||
|
|
||||||
|
generateZipFilename() {
|
||||||
|
const now = new Date();
|
||||||
|
const timestamp = now.getFullYear() + '-' +
|
||||||
|
String(now.getMonth() + 1).padStart(2, '0') + '-' +
|
||||||
|
String(now.getDate()).padStart(2, '0') + '_' +
|
||||||
|
String(now.getHours()).padStart(2, '0') + '-' +
|
||||||
|
String(now.getMinutes()).padStart(2, '0') + '-' +
|
||||||
|
String(now.getSeconds()).padStart(2, '0');
|
||||||
|
|
||||||
|
// Get project name and convert to snake_case
|
||||||
|
const rawProjectName = ProjectMeta.getProjectName();
|
||||||
|
const projectName = (rawProjectName && rawProjectName.trim())
|
||||||
|
? this.toSnakeCase(rawProjectName.trim())
|
||||||
|
: 'claude_artifacts';
|
||||||
|
|
||||||
|
return `${timestamp}_${projectName}.zip`;
|
||||||
|
},
|
||||||
|
|
||||||
|
addFilesToZip(zip, finalMap) {
|
||||||
|
Object.entries(finalMap).forEach(([fullPath, content]) => {
|
||||||
|
const pathParts = fullPath.split('/');
|
||||||
|
|
||||||
|
if (pathParts.length > 1) {
|
||||||
|
const filename = pathParts.pop();
|
||||||
|
const folderPath = pathParts.join('/');
|
||||||
|
const folder = zip.folder(folderPath);
|
||||||
|
folder.file(filename, content);
|
||||||
|
} else {
|
||||||
|
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
src/main.js
Normal file
15
src/main.js
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
async function main() {
|
||||||
|
console.log('Running artifact collector');
|
||||||
|
|
||||||
|
if (!ProjectMeta.isScrapable()) {
|
||||||
|
console.log('Not in a scrapable context, 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
48
src/project_meta.js
Normal file
48
src/project_meta.js
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
const ProjectMeta = {
|
||||||
|
getProjectName() {
|
||||||
|
// First try to get from project link (chat pages)
|
||||||
|
const projectLink = document.querySelector('a[href*="/project/"]');
|
||||||
|
if (projectLink) {
|
||||||
|
// Get the full text and strip trailing spaces and forward slashes
|
||||||
|
const projectName = projectLink.innerText.replace(/\s*\/.*$/, '').trim();
|
||||||
|
if (projectName) {
|
||||||
|
return projectName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: try to get from H1 (project pages)
|
||||||
|
const projectH1 = document.querySelector('h1.font-ui-serif');
|
||||||
|
if (projectH1) {
|
||||||
|
return projectH1.textContent.trim() || 'Claude Project';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'Claude Project';
|
||||||
|
},
|
||||||
|
|
||||||
|
getChatName() {
|
||||||
|
const chatNameDiv = document.querySelector('button[data-testid="chat-menu-trigger"] div.truncate');
|
||||||
|
if (!chatNameDiv) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return chatNameDiv.textContent.trim() || null;
|
||||||
|
},
|
||||||
|
|
||||||
|
isChat() {
|
||||||
|
return Boolean(this.getChatName());
|
||||||
|
},
|
||||||
|
|
||||||
|
isProjectPage() {
|
||||||
|
const h2Elements = document.getElementsByTagName('h2');
|
||||||
|
const hasProjectKnowledge = Array.from(h2Elements).some(h2 =>
|
||||||
|
h2.textContent.trim() === 'Project knowledge'
|
||||||
|
);
|
||||||
|
return Boolean(this.getProjectName()) && hasProjectKnowledge;
|
||||||
|
},
|
||||||
|
|
||||||
|
isScrapable() {
|
||||||
|
return this.isChat() || this.isProjectPage();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// No exports needed - will be in same scope after build
|
||||||
366
src/scraper.js
Normal file
366
src/scraper.js
Normal file
@ -0,0 +1,366 @@
|
|||||||
|
const ChatArtifactScraper = {
|
||||||
|
|
||||||
|
getArtifactMenuButton() {
|
||||||
|
const artifactMenuButton = document.querySelector('button[aria-haspopup="menu"] svg[viewBox="0 0 256 256"] path[d*="M80,64a8,8,0,0,1,8-8H216"]');
|
||||||
|
return artifactMenuButton?.closest('button');
|
||||||
|
},
|
||||||
|
|
||||||
|
ensureArtifactSidebarOpen() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (this.getArtifactMenuButton()) {
|
||||||
|
resolve(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleButton = document.querySelector('button[data-state="closed"] svg[viewBox="0 0 20 20"] path[d*="M8.14648 4.64648C8.34176 4.45136"]');
|
||||||
|
const button = toggleButton?.closest('button');
|
||||||
|
|
||||||
|
if (!button) {
|
||||||
|
reject(new Error('Artifact sidebar toggle button not found'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let hasResolved = false;
|
||||||
|
|
||||||
|
const observer = new MutationObserver((mutations) => {
|
||||||
|
if (this.getArtifactMenuButton()) {
|
||||||
|
observer.disconnect();
|
||||||
|
if (!hasResolved) {
|
||||||
|
hasResolved = true;
|
||||||
|
resolve(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
observer.observe(document.body, { childList: true, subtree: true });
|
||||||
|
|
||||||
|
button.click();
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!hasResolved) {
|
||||||
|
observer.disconnect();
|
||||||
|
hasResolved = true;
|
||||||
|
reject(new Error('Failed to open artifact sidebar - timeout'));
|
||||||
|
}
|
||||||
|
}, 3000);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
openArtifactList() {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
try {
|
||||||
|
await this.ensureArtifactSidebarOpen();
|
||||||
|
} catch (error) {
|
||||||
|
reject(new Error(`Failed to open artifact sidebar: ${error.message}`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const menuButton = this.getArtifactMenuButton();
|
||||||
|
|
||||||
|
if (!menuButton) {
|
||||||
|
reject(new Error('Global artifact menu button not found'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let hasResolved = false;
|
||||||
|
|
||||||
|
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) {
|
||||||
|
observer.disconnect();
|
||||||
|
if (!hasResolved) {
|
||||||
|
hasResolved = true;
|
||||||
|
resolve(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
observer.observe(document.body, { childList: true, subtree: true });
|
||||||
|
|
||||||
|
menuButton.focus();
|
||||||
|
const enterEvent = new KeyboardEvent('keydown', {
|
||||||
|
key: 'Enter',
|
||||||
|
code: 'Enter',
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: true
|
||||||
|
});
|
||||||
|
menuButton.dispatchEvent(enterEvent);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!hasResolved) {
|
||||||
|
observer.disconnect();
|
||||||
|
hasResolved = true;
|
||||||
|
reject(new Error('Failed to open artifact list menu - timeout'));
|
||||||
|
}
|
||||||
|
}, 3000);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
getArtifactListItems() {
|
||||||
|
if (!this.getArtifactMenuButton()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const menuItems = document.querySelectorAll('li[role="none"] div[role="menuitem"]');
|
||||||
|
const artifactHash = {};
|
||||||
|
|
||||||
|
if (menuItems.length === 0) {
|
||||||
|
return artifactHash;
|
||||||
|
}
|
||||||
|
|
||||||
|
menuItems.forEach((item, index) => {
|
||||||
|
const filenameDiv = item.querySelector('div.line-clamp-2');
|
||||||
|
if (filenameDiv) {
|
||||||
|
const filename = filenameDiv.textContent.trim();
|
||||||
|
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;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
targetItem.focus();
|
||||||
|
const enterEvent = new KeyboardEvent('keydown', {
|
||||||
|
key: 'Enter',
|
||||||
|
code: 'Enter',
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: true
|
||||||
|
});
|
||||||
|
targetItem.dispatchEvent(enterEvent);
|
||||||
|
|
||||||
|
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 = {};
|
||||||
|
let fileMap = null;
|
||||||
|
|
||||||
|
await this.openArtifactList();
|
||||||
|
const artifactList = this.getArtifactListItems();
|
||||||
|
|
||||||
|
if (Object.keys(artifactList).length === 0) {
|
||||||
|
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.toLowerCase().indexOf('files.txt') !== -1) {
|
||||||
|
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}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 500));
|
||||||
|
}
|
||||||
|
|
||||||
|
return { artifacts: artifactCollection, fileMap: fileMap };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const ProjectArtifactScraper = {
|
||||||
|
getArtifactListItems() {
|
||||||
|
const gridContainer = document.querySelector('ul.grid');
|
||||||
|
const artifactHash = {};
|
||||||
|
|
||||||
|
if (!gridContainer) {
|
||||||
|
return artifactHash;
|
||||||
|
}
|
||||||
|
|
||||||
|
gridContainer.querySelectorAll('h3').forEach((titleElement) => {
|
||||||
|
const filename = titleElement.textContent.trim();
|
||||||
|
artifactHash[filename] = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
return artifactHash;
|
||||||
|
},
|
||||||
|
|
||||||
|
getArtifact(filename, isCurrentlySelected) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
// Find the h3 element with the matching filename
|
||||||
|
const gridContainer = document.querySelector('ul.grid');
|
||||||
|
if (!gridContainer) {
|
||||||
|
reject(new Error('Grid container not found'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let targetButton = null;
|
||||||
|
gridContainer.querySelectorAll('h3').forEach((titleElement) => {
|
||||||
|
if (titleElement.textContent.trim() === filename) {
|
||||||
|
// Find the clickable button that contains this h3
|
||||||
|
targetButton = titleElement.closest('button');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!targetButton) {
|
||||||
|
reject(new Error(`Could not find file button for: ${filename}`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Click the button to open the file modal
|
||||||
|
targetButton.click();
|
||||||
|
|
||||||
|
// Wait for modal to appear and extract content
|
||||||
|
let attempts = 0;
|
||||||
|
const maxAttempts = 30; // 3 seconds total
|
||||||
|
|
||||||
|
const handleModal = () => {
|
||||||
|
// Look for the modal dialog and the content div within it
|
||||||
|
const modalDialog = document.querySelector('div[role="dialog"][data-state="open"]');
|
||||||
|
|
||||||
|
if (modalDialog) {
|
||||||
|
const contentDiv = modalDialog.querySelector('div.whitespace-pre-wrap');
|
||||||
|
|
||||||
|
if (contentDiv) {
|
||||||
|
const content = contentDiv.textContent || contentDiv.innerText || '';
|
||||||
|
|
||||||
|
// Close the modal by clicking the close button
|
||||||
|
const closeButton = modalDialog.querySelector('button.relative.can-focus svg');
|
||||||
|
if (closeButton) {
|
||||||
|
closeButton.closest('button').click();
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(content);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
attempts++;
|
||||||
|
if (attempts < maxAttempts) {
|
||||||
|
setTimeout(handleModal, 100);
|
||||||
|
} else {
|
||||||
|
reject(new Error(`Modal with content not found for artifact: ${filename}`));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Start checking for modal after a brief delay
|
||||||
|
setTimeout(handleModal, 100);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async collectArtifacts() {
|
||||||
|
const artifactCollection = {};
|
||||||
|
let fileMap = null;
|
||||||
|
|
||||||
|
const artifactList = this.getArtifactListItems();
|
||||||
|
|
||||||
|
if (Object.keys(artifactList).length === 0) {
|
||||||
|
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.toLowerCase().indexOf('files.txt') !== -1) {
|
||||||
|
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}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 500));
|
||||||
|
}
|
||||||
|
|
||||||
|
return { artifacts: artifactCollection, fileMap: fileMap };
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
const ArtifactScraper = {
|
||||||
|
async collectArtifacts() {
|
||||||
|
if (ProjectMeta.isChat()) {
|
||||||
|
console.log('Detected chat - using ChatArtifactScraper');
|
||||||
|
return await ChatArtifactScraper.collectArtifacts();
|
||||||
|
} else if (ProjectMeta.isProjectPage()) {
|
||||||
|
console.log('Detected project page - using ProjectArtifactScraper');
|
||||||
|
return await ProjectArtifactScraper.collectArtifacts();
|
||||||
|
} else {
|
||||||
|
throw new Error('Unknown page type - not a chat or project page');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
// No exports needed - will be in same scope after build
|
||||||
Reference in New Issue
Block a user