Greasy Fork

来自缓存

Greasy Fork is available in English.

Claude Artifact Downloader

Download Claude artifacts in mass, by range, or individually

当前为 2025-06-28 提交的版本,查看 最新版本

// ==UserScript==
// @name         Claude Artifact Downloader
// @namespace    http://tampermonkey.net/
// @version      1.0.0
// @description  Download Claude artifacts in mass, by range, or individually
// @author       anassk
// @license      MIT
// @match        https://claude.ai/*
// @noframes
// @grant        GM_registerMenuCommand
// @grant        GM_notification
// @grant        GM_xmlhttpRequest
// ==/UserScript==

(function() {
    'use strict';

    // Only run in the main window, not in frames/iframes
    if (window !== window.top) {
        console.log('Claude Artifact Downloader: Skipping execution in frame/iframe');
        return;
    }

    // State tracking
    let artifactPanelOpen = false;

    // Utility function for clicking elements as specified in documentation
    function click(element) {
        const rect = element.getBoundingClientRect();
        const x = rect.left + rect.width / 2;
        const y = rect.top + rect.height / 2;
        element.dispatchEvent(new PointerEvent('pointerdown', {bubbles: true, clientX: x, clientY: y, pointerId: 1, button: 0, buttons: 1}));
        element.dispatchEvent(new PointerEvent('pointerup', {bubbles: true, clientX: x, clientY: y, pointerId: 1, button: 0, buttons: 0}));
        element.dispatchEvent(new MouseEvent('click', {bubbles: true, clientX: x, clientY: y, button: 0, buttons: 0}));
    }

    // Utility function to wait for element
    async function waitForElement(selector, timeout = 5000) {
        const startTime = Date.now();
        while (Date.now() - startTime < timeout) {
            const element = document.querySelector(selector);
            if (element) return element;
            await sleep(100);
        }
        throw new Error(`Element ${selector} not found after ${timeout}ms`);
    }

    // Sleep utility
    function sleep(ms) {
        return new Promise(resolve => setTimeout(resolve, ms));
    }

    // Show notification
    function notify(message, type = 'info') {
        GM_notification({
            text: message,
            title: 'Claude Artifact Downloader',
            timeout: 3000
        });
        console.log(`[Claude Artifact Downloader] ${message}`);
    }

    // Check if artifact panel is already open
    function isArtifactPanelOpen() {
        const indicators = [
            'div.border.border-border-300.font-medium.rounded-lg.overflow-hidden', // Download area
            'button[aria-haspopup="menu"].h-8.w-8.rounded-md.-mx-1:has(svg)', // Artifact menu button
        ];

        return indicators.some(selector => document.querySelector(selector));
    }

    // Step 1: Click first artifact (only if not already open)
    async function ensureArtifactPanelOpen() {
        try {
            if (isArtifactPanelOpen()) {
                console.log('Artifact panel already open');
                artifactPanelOpen = true;
                return true;
            }

            const artifact = await waitForElement('button:has(.artifact-block-cell)', 3000);
            click(artifact);
            console.log('Clicked first artifact');
            await sleep(500); // Wait for artifact to load
            artifactPanelOpen = true;
            return true;
        } catch (error) {
            // Try alternative selectors for artifacts
            try {
                const altSelectors = [
                    '[data-testid*="artifact"]',
                    '.artifact',
                    '[class*="artifact"]'
                ];

                for (const selector of altSelectors) {
                    try {
                        const altArtifact = await waitForElement(selector, 1000);
                        click(altArtifact);
                        console.log(`Clicked artifact using selector: ${selector}`);
                        await sleep(500);
                        artifactPanelOpen = true;
                        return true;
                    } catch (e) {
                        continue;
                    }
                }

                throw new Error('No artifact elements found');
            } catch (altError) {
                notify('No artifacts found', 'error');
                throw new Error('No artifacts found in conversation');
            }
        }
    }

    // Step 2: Open artifact menu (only for multiple artifacts)
    async function openArtifactMenu() {
        try {
            // First check if menu is already open
            const openMenu = document.querySelector('[role="menu"][data-state="open"]');
            if (openMenu && openMenu.querySelectorAll('li[role="none"]').length > 0) {
                console.log('Artifact menu already open');
                return true;
            }

            const parentDiv = document.querySelector('div.pr-2:nth-child(1)');
            if (parentDiv) {
                const menuButton = parentDiv.querySelector('button');
                if (menuButton) {
                    const firstChild = menuButton.querySelector('*');
                    click(firstChild);
                    console.log('Opened artifact menu');
                    await sleep(300);
                    return true;
                }
            }
            return false; // Menu not found (single artifact)
        } catch (error) {
            console.log('Artifact menu not available (likely single artifact)');
            return false;
        }
    }

    // Step 3: Switch to specific artifact
    async function switchToArtifact(index) {
        try {
            const menuButton = document.querySelector('button[aria-haspopup="menu"].h-8.w-8.rounded-md.-mx-1:has(svg)');
            if (!menuButton) return false;

            const menuId = menuButton.getAttribute('aria-controls');
            const menu = document.getElementById(menuId);
            if (!menu) return false;

            const items = menu.querySelectorAll('li[role="none"]');
            if (index >= items.length) {
                throw new Error(`Artifact index ${index} out of range (max: ${items.length - 1})`);
            }

            const menuItem = items[index].querySelector('[role="menuitem"]');
            if (menuItem) {
                click(menuItem);
                console.log(`Switched to artifact ${index}`);
                await sleep(500);
                return true;
            }
            return false;
        } catch (error) {
            console.error('Error switching artifact:', error);
            throw error;
        }
    }

    // Step 4: Open download menu
    async function openDownloadMenu() {
        try {
            // Check if download menu is already open
            const openDownloadMenu = document.querySelector('[role="menu"][data-state="open"]');
            if (openDownloadMenu && openDownloadMenu.querySelectorAll('a[download], a[href^="blob:"]').length > 0) {
                console.log('Download menu already open');
                return true;
            }

            const parentDiv = await waitForElement('div.border.border-border-300.font-medium.rounded-lg.overflow-hidden');
            const buttons = parentDiv.querySelectorAll('button');
            const targetButton = buttons[1]; // Second button

            if (targetButton) {
                const firstChild = targetButton.querySelector('*');
                click(firstChild);
                console.log('Opened download menu');
                await sleep(300);
                return true;
            }
            throw new Error('Download button not found');
        } catch (error) {
            console.error('Error opening download menu:', error);
            throw error;
        }
    }

    // Step 5: Download files
    async function downloadFiles() {
        try {
            const dropdown = await waitForElement('[role="menu"][data-state="open"]', 2000);
            const downloadLinks = dropdown.querySelectorAll('a[download], a[href^="blob:"]');

            console.log(`Found ${downloadLinks.length} download links`);
            if (downloadLinks.length === 0) {
                throw new Error('No download links found');
            }

            downloadLinks.forEach(link => link.click());
            console.log('Downloaded files');
            await sleep(100);

            // Close the download menu by clicking elsewhere or pressing escape
            document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }));
            await sleep(200);

            return downloadLinks.length;
        } catch (error) {
            console.error('Error downloading files:', error);
            throw error;
        }
    }

    // Get artifact information
    async function getArtifactInfo() {
        try {
            await ensureArtifactPanelOpen();
            const hasMenu = await openArtifactMenu();

            if (!hasMenu) {
                notify('Single artifact detected');
                return { count: 1, hasMultiple: false };
            }

            // Count artifacts in menu
            const menuButton = document.querySelector('button[aria-haspopup="menu"].h-8.w-8.rounded-md.-mx-1:has(svg)');
            if (!menuButton) {
                return { count: 1, hasMultiple: false };
            }

            const menuId = menuButton.getAttribute('aria-controls');
            const menu = document.getElementById(menuId);
            if (!menu) {
                return { count: 1, hasMultiple: false };
            }

            const items = menu.querySelectorAll('li[role="none"]');

            notify(`Found ${items.length} artifacts`);
            return { count: items.length, hasMultiple: true };
        } catch (error) {
            console.error('Error getting artifact info:', error);
            throw error;
        }
    }

    // Download single artifact
    async function downloadSingleArtifact(index, artifactInfo) {
        try {
            if (artifactInfo.hasMultiple) {
                await openArtifactMenu();
                await switchToArtifact(index);
            }

            await openDownloadMenu();
            const fileCount = await downloadFiles();

            notify(`Downloaded ${fileCount} file(s) from artifact ${index + 1}`);
            return true;
        } catch (error) {
            console.error(`Error downloading artifact ${index}:`, error);
            notify(`Failed to download artifact ${index + 1}`, 'error');
            return false;
        }
    }

    // Reset state when page changes
    function resetState() {
        artifactPanelOpen = false;
        console.log('State reset');
    }

    // Main operations
    async function downloadAll() {
        try {
            resetState();
            notify('Starting download all artifacts...');
            const artifactInfo = await getArtifactInfo();

            let successCount = 0;
            for (let i = 0; i < artifactInfo.count; i++) {
                const success = await downloadSingleArtifact(i, artifactInfo);
                if (success) successCount++;

                // Add delay between downloads
                if (i < artifactInfo.count - 1) {
                    await sleep(1000);
                }
            }

            notify(`Downloaded ${successCount}/${artifactInfo.count} artifacts successfully`,
                   successCount === artifactInfo.count ? 'success' : 'warning');
        } catch (error) {
            console.error('Error in downloadAll:', error);
            notify('Failed to download all artifacts', 'error');
        }
    }

    async function downloadRange() {
        try {
            resetState();
            const artifactInfo = await getArtifactInfo();

            const startStr = prompt(`Enter start artifact number (1-${artifactInfo.count}):`);
            if (!startStr) return;

            const endStr = prompt(`Enter end artifact number (${startStr}-${artifactInfo.count}):`);
            if (!endStr) return;

            const start = parseInt(startStr) - 1; // Convert to 0-based index
            const end = parseInt(endStr) - 1;

            if (isNaN(start) || isNaN(end) || start < 0 || end >= artifactInfo.count || start > end) {
                notify('Invalid range', 'error');
                return;
            }

            notify(`Downloading artifacts ${start + 1} to ${end + 1}...`);

            let successCount = 0;
            for (let i = start; i <= end; i++) {
                const success = await downloadSingleArtifact(i, artifactInfo);
                if (success) successCount++;

                // Add delay between downloads
                if (i < end) {
                    await sleep(1000);
                }
            }

            notify(`Downloaded ${successCount}/${end - start + 1} artifacts successfully`,
                   successCount === (end - start + 1) ? 'success' : 'warning');
        } catch (error) {
            console.error('Error in downloadRange:', error);
            notify('Failed to download range', 'error');
        }
    }

    async function downloadSingle() {
        try {
            resetState();
            const artifactInfo = await getArtifactInfo();

            if (artifactInfo.count === 1) {
                notify('Only one artifact available, downloading...');
                await downloadSingleArtifact(0, artifactInfo);
                return;
            }

            // Show list of artifacts with their actual names if possible
            let artifactList = 'Available artifacts:\n';

            // Try to get artifact names from the menu
            if (artifactInfo.hasMultiple) {
                await openArtifactMenu();
                const menuButton = document.querySelector('button[aria-haspopup="menu"].h-8.w-8.rounded-md.-mx-1:has(svg)');
                if (menuButton) {
                    const menuId = menuButton.getAttribute('aria-controls');
                    const menu = document.getElementById(menuId);
                    if (menu) {
                        const items = menu.querySelectorAll('li[role="none"]');
                        items.forEach((item, index) => {
                            const titleDiv = item.querySelector('.line-clamp-2');
                            const typeSpan = item.querySelector('.text-text-300');
                            const title = titleDiv ? titleDiv.textContent.trim() : `Artifact ${index + 1}`;
                            const type = typeSpan ? typeSpan.textContent.trim() : '';
                            artifactList += `${index + 1}. ${title} ${type ? `(${type})` : ''}\n`;
                        });
                    }
                }
            } else {
                artifactList += '1. Artifact 1\n';
            }

            const choice = prompt(artifactList + '\nEnter artifact number to download:');
            if (!choice) return;

            const index = parseInt(choice) - 1;
            if (isNaN(index) || index < 0 || index >= artifactInfo.count) {
                notify('Invalid artifact number', 'error');
                return;
            }

            await downloadSingleArtifact(index, artifactInfo);
        } catch (error) {
            console.error('Error in downloadSingle:', error);
            notify('Failed to download single artifact', 'error');
        }
    }

    async function listArtifacts() {
        try {
            resetState();
            const artifactInfo = await getArtifactInfo();

            let message = `Total artifacts: ${artifactInfo.count}\n\n`;

            if (artifactInfo.hasMultiple) {
                message += 'Available artifacts:\n';

                // Get artifact details from menu
                await openArtifactMenu();
                const menuButton = document.querySelector('button[aria-haspopup="menu"].h-8.w-8.rounded-md.-mx-1:has(svg)');
                if (menuButton) {
                    const menuId = menuButton.getAttribute('aria-controls');
                    const menu = document.getElementById(menuId);
                    if (menu) {
                        const items = menu.querySelectorAll('li[role="none"]');
                        items.forEach((item, index) => {
                            const titleDiv = item.querySelector('.line-clamp-2');
                            const typeSpan = item.querySelector('.text-text-300');
                            const title = titleDiv ? titleDiv.textContent.trim() : `Artifact ${index + 1}`;
                            const type = typeSpan ? typeSpan.textContent.trim() : '';
                            message += `${index + 1}. ${title}\n   ${type}\n\n`;
                        });
                    }
                }
            } else {
                message += 'Single artifact available for download';
            }

            alert(message);
            notify('Artifact list displayed');
        } catch (error) {
            console.error('Error in listArtifacts:', error);
            notify('Failed to list artifacts', 'error');
        }
    }

    // Listen for page navigation to reset state
    let currentUrl = window.location.href;
    setInterval(() => {
        if (window.location.href !== currentUrl) {
            currentUrl = window.location.href;
            resetState();
        }
    }, 1000);

    // Register menu commands
    GM_registerMenuCommand('Download All', downloadAll);
    GM_registerMenuCommand('Download Range', downloadRange);
    GM_registerMenuCommand('Download Single', downloadSingle);
    GM_registerMenuCommand('List All', listArtifacts);

    // Initial notification
    console.log('Claude Artifact Downloader v1.0.3 loaded. Fixed multiple execution issue. Use Tampermonkey menu to access commands.');
})();