Files
claude-artifact-scraper/content.js

251 lines
8.4 KiB
JavaScript

// Content script for artifact processing
(function() {
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;
}
console.log('Confirmed we are in a project chat');
return true;
}
// Static object containing all artifact scraping functionality
const ArtifactScraper = {
// Open the artifact list menu
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;
}
console.log('Opening artifact list menu...');
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 detected as 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
menuButton.focus();
const enterEvent = new KeyboardEvent('keydown', {
key: 'Enter',
code: 'Enter',
bubbles: true,
cancelable: true
});
menuButton.dispatchEvent(enterEvent);
// Fallback timeout
setTimeout(() => {
if (!hasResolved) {
observer.disconnect();
hasResolved = true;
reject(new Error('Failed to open artifact list menu - timeout'));
}
}, 3000);
});
},
// Parse the artifact list and return filename -> isSelected hash
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();
const isSelected = item.classList.contains('border-accent-secondary-100') ||
item.getAttribute('class').includes('border-accent-secondary-100');
artifactHash[filename] = isSelected;
console.log(`Found artifact: ${filename} (${isSelected ? 'selected' : 'unselected'})`);
}
});
return artifactHash;
},
// Select a specific artifact list item
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
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 not currently selected, we need to select it
if (!isCurrentlySelected) {
// Open menu if not already open
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;
}
}
// Select the artifact
try {
await this.selectArtifactListItem(filename);
} catch (error) {
reject(new Error(`Failed to select artifact: ${filename} - ${error.message}`));
return;
}
}
// Extract content from the code block using the updated selector
const codeBlock = document.querySelector('div.right-0 code');
if (!codeBlock) {
reject(new Error(`No code block found for artifact: ${filename}`));
return;
}
const content = codeBlock.textContent || codeBlock.innerText || '';
console.log(`Extracted content for ${filename}: ${content.length} characters`);
resolve(content);
});
},
// Main collection method
async collectArtifacts() {
console.log('Starting artifact collection...');
// Initialize empty collection
const artifactCollection = {};
// Open artifact list
await this.openArtifactList();
// Get list of artifacts
const artifactList = this.getArtifactListItems();
if (Object.keys(artifactList).length === 0) {
console.log('No artifacts found in list');
return;
}
console.log(`Found ${Object.keys(artifactList).length} artifacts to collect`);
// Iterate through artifacts and collect content
for (const [filename, isSelected] of Object.entries(artifactList)) {
console.log(`Processing artifact: ${filename}`);
try {
const content = await this.getArtifact(filename, isSelected);
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));
}
// Log final collection
console.log('=== ARTIFACT COLLECTION COMPLETE ===');
console.log(`Collected ${Object.keys(artifactCollection).length} artifacts:`);
console.log(artifactCollection);
return artifactCollection;
}
};
// Main function
async function main() {
console.log('Running artifact collector');
if (!isProjectChat()) {
return;
}
await ArtifactScraper.collectArtifacts();
}
// Run the main function
main();
})();