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
|
## Changes under consideration
|
||||||
1. Optionally scrape project-level artifacts instead of chat-level artifacts
|
1. Optionally scrape project-level artifacts instead of chat-level artifacts
|
||||||
2. Auto-open the sidebar as needed
|
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.
|
||||||
3. Rename the zip file based on the project's name
|
|
||||||
|
|||||||
@ -1,10 +1,61 @@
|
|||||||
const ArtifactScraper = {
|
const ArtifactScraper = {
|
||||||
|
|
||||||
openArtifactList() {
|
getArtifactMenuButton() {
|
||||||
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 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');
|
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) {
|
if (!menuButton) {
|
||||||
reject(new Error('Global artifact menu button not found'));
|
reject(new Error('Global artifact menu button not found'));
|
||||||
@ -13,7 +64,6 @@ const ArtifactScraper = {
|
|||||||
|
|
||||||
let hasResolved = false;
|
let hasResolved = false;
|
||||||
|
|
||||||
// Listen for menu opening
|
|
||||||
const observer = new MutationObserver((mutations) => {
|
const observer = new MutationObserver((mutations) => {
|
||||||
mutations.forEach((mutation) => {
|
mutations.forEach((mutation) => {
|
||||||
if (mutation.type === 'childList') {
|
if (mutation.type === 'childList') {
|
||||||
@ -26,7 +76,6 @@ const ArtifactScraper = {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (menuAdded) {
|
if (menuAdded) {
|
||||||
console.log('Artifact list menu has been opened');
|
|
||||||
observer.disconnect();
|
observer.disconnect();
|
||||||
if (!hasResolved) {
|
if (!hasResolved) {
|
||||||
hasResolved = true;
|
hasResolved = true;
|
||||||
@ -39,8 +88,6 @@ const ArtifactScraper = {
|
|||||||
|
|
||||||
observer.observe(document.body, { childList: true, subtree: 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();
|
menuButton.focus();
|
||||||
const enterEvent = new KeyboardEvent('keydown', {
|
const enterEvent = new KeyboardEvent('keydown', {
|
||||||
key: 'Enter',
|
key: 'Enter',
|
||||||
@ -50,7 +97,6 @@ const ArtifactScraper = {
|
|||||||
});
|
});
|
||||||
menuButton.dispatchEvent(enterEvent);
|
menuButton.dispatchEvent(enterEvent);
|
||||||
|
|
||||||
// Fallback timeout so we don't accidentally listen forever.
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (!hasResolved) {
|
if (!hasResolved) {
|
||||||
observer.disconnect();
|
observer.disconnect();
|
||||||
@ -62,11 +108,14 @@ const ArtifactScraper = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
getArtifactListItems() {
|
getArtifactListItems() {
|
||||||
|
if (!this.getArtifactMenuButton()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
const menuItems = document.querySelectorAll('li[role="none"] div[role="menuitem"]');
|
const menuItems = document.querySelectorAll('li[role="none"] div[role="menuitem"]');
|
||||||
const artifactHash = {};
|
const artifactHash = {};
|
||||||
|
|
||||||
if (menuItems.length === 0) {
|
if (menuItems.length === 0) {
|
||||||
console.log('No artifact list items found');
|
|
||||||
return artifactHash;
|
return artifactHash;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -74,7 +123,6 @@ const ArtifactScraper = {
|
|||||||
const filenameDiv = item.querySelector('div.line-clamp-2');
|
const filenameDiv = item.querySelector('div.line-clamp-2');
|
||||||
if (filenameDiv) {
|
if (filenameDiv) {
|
||||||
const filename = filenameDiv.textContent.trim();
|
const filename = filenameDiv.textContent.trim();
|
||||||
// Check for aria-selected attribute or other indicators of selection
|
|
||||||
const isSelected = item.getAttribute('aria-selected') === 'true' ||
|
const isSelected = item.getAttribute('aria-selected') === 'true' ||
|
||||||
item.classList.contains('bg-accent-secondary-100') ||
|
item.classList.contains('bg-accent-secondary-100') ||
|
||||||
item.classList.contains('border-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"]');
|
const menuItems = document.querySelectorAll('li[role="none"] div[role="menuitem"]');
|
||||||
let targetItem = null;
|
let targetItem = null;
|
||||||
|
|
||||||
// Find the matching artifact by filename
|
|
||||||
menuItems.forEach(item => {
|
menuItems.forEach(item => {
|
||||||
const filenameDiv = item.querySelector('div.line-clamp-2');
|
const filenameDiv = item.querySelector('div.line-clamp-2');
|
||||||
if (filenameDiv && filenameDiv.textContent.trim() === filename) {
|
if (filenameDiv && filenameDiv.textContent.trim() === filename) {
|
||||||
@ -105,11 +152,6 @@ const ArtifactScraper = {
|
|||||||
return;
|
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();
|
targetItem.focus();
|
||||||
const enterEvent = new KeyboardEvent('keydown', {
|
const enterEvent = new KeyboardEvent('keydown', {
|
||||||
key: 'Enter',
|
key: 'Enter',
|
||||||
@ -119,7 +161,6 @@ const ArtifactScraper = {
|
|||||||
});
|
});
|
||||||
targetItem.dispatchEvent(enterEvent);
|
targetItem.dispatchEvent(enterEvent);
|
||||||
|
|
||||||
// Wait for artifact to load
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
resolve(true);
|
resolve(true);
|
||||||
}, 1000);
|
}, 1000);
|
||||||
@ -159,17 +200,12 @@ const ArtifactScraper = {
|
|||||||
|
|
||||||
async collectArtifacts() {
|
async collectArtifacts() {
|
||||||
const artifactCollection = {};
|
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;
|
let fileMap = null;
|
||||||
|
|
||||||
await this.openArtifactList();
|
await this.openArtifactList();
|
||||||
const artifactList = this.getArtifactListItems();
|
const artifactList = this.getArtifactListItems();
|
||||||
|
|
||||||
if (Object.keys(artifactList).length === 0) {
|
if (Object.keys(artifactList).length === 0) {
|
||||||
console.log('No artifacts found in list');
|
|
||||||
return { artifacts: artifactCollection, fileMap: fileMap };
|
return { artifacts: artifactCollection, fileMap: fileMap };
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -180,7 +216,6 @@ const ArtifactScraper = {
|
|||||||
const content = await this.getArtifact(filename, isSelected);
|
const content = await this.getArtifact(filename, isSelected);
|
||||||
|
|
||||||
if (filename === 'files.txt') {
|
if (filename === 'files.txt') {
|
||||||
// This is our special llm-generated mapping that we'll need to finish our export.
|
|
||||||
fileMap = content;
|
fileMap = content;
|
||||||
console.log(`✓ Found files.txt - storing in fileMap`);
|
console.log(`✓ Found files.txt - storing in fileMap`);
|
||||||
} else {
|
} else {
|
||||||
@ -191,12 +226,10 @@ const ArtifactScraper = {
|
|||||||
console.log(`✗ Failed to collect: ${filename} - ${error.message}`);
|
console.log(`✗ Failed to collect: ${filename} - ${error.message}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Small delay between artifacts
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 500));
|
await new Promise(resolve => setTimeout(resolve, 500));
|
||||||
}
|
}
|
||||||
|
|
||||||
return { artifacts: artifactCollection, fileMap: fileMap };
|
return { artifacts: artifactCollection, fileMap: fileMap };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// No exports needed - will be in same scope after build
|
// No exports needed - will be in same scope after build
|
||||||
|
|||||||
Reference in New Issue
Block a user