Pre-open the artifact menu if it's closed
This commit is contained in:
@ -37,5 +37,4 @@ If you've followed the other steps, you should see some activity in the chat pag
|
||||
|
||||
## Changes under consideration
|
||||
1. Optionally scrape project-level artifacts instead of chat-level artifacts
|
||||
2. Auto-open the sidebar as needed
|
||||
3. Rename the zip file based on the project's name
|
||||
2. 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.
|
||||
|
||||
@ -1,10 +1,61 @@
|
||||
const ArtifactScraper = {
|
||||
|
||||
openArtifactList() {
|
||||
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) => {
|
||||
// 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 (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'));
|
||||
@ -13,7 +64,6 @@ const ArtifactScraper = {
|
||||
|
||||
let hasResolved = false;
|
||||
|
||||
// Listen for menu opening
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
mutations.forEach((mutation) => {
|
||||
if (mutation.type === 'childList') {
|
||||
@ -26,7 +76,6 @@ const ArtifactScraper = {
|
||||
);
|
||||
|
||||
if (menuAdded) {
|
||||
console.log('Artifact list menu has been opened');
|
||||
observer.disconnect();
|
||||
if (!hasResolved) {
|
||||
hasResolved = true;
|
||||
@ -39,8 +88,6 @@ const ArtifactScraper = {
|
||||
|
||||
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',
|
||||
@ -50,7 +97,6 @@ const ArtifactScraper = {
|
||||
});
|
||||
menuButton.dispatchEvent(enterEvent);
|
||||
|
||||
// Fallback timeout so we don't accidentally listen forever.
|
||||
setTimeout(() => {
|
||||
if (!hasResolved) {
|
||||
observer.disconnect();
|
||||
@ -62,11 +108,14 @@ const ArtifactScraper = {
|
||||
},
|
||||
|
||||
getArtifactListItems() {
|
||||
if (!this.getArtifactMenuButton()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@ -74,7 +123,6 @@ const ArtifactScraper = {
|
||||
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');
|
||||
@ -92,7 +140,6 @@ const ArtifactScraper = {
|
||||
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) {
|
||||
@ -105,11 +152,6 @@ const ArtifactScraper = {
|
||||
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',
|
||||
@ -119,7 +161,6 @@ const ArtifactScraper = {
|
||||
});
|
||||
targetItem.dispatchEvent(enterEvent);
|
||||
|
||||
// Wait for artifact to load
|
||||
setTimeout(() => {
|
||||
resolve(true);
|
||||
}, 1000);
|
||||
@ -159,17 +200,12 @@ const ArtifactScraper = {
|
||||
|
||||
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 };
|
||||
}
|
||||
|
||||
@ -180,7 +216,6 @@ const ArtifactScraper = {
|
||||
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 {
|
||||
@ -191,12 +226,10 @@ const ArtifactScraper = {
|
||||
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