├── manifest.json ├── LICENSE ├── banner.js ├── content.js ├── README.md ├── background.js └── jszip.min.js /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "Claude Artifact Downloader", 4 | "version": "1.0", 5 | "description": "Download artifacts from Claude chat conversations as a ZIP file", 6 | "permissions": ["tabs", "downloads", "storage", "webRequest"], 7 | "host_permissions": ["https://claude.ai/*", "https://api.claude.ai/*"], 8 | "background": { 9 | "service_worker": "background.js" 10 | }, 11 | "content_scripts": [ 12 | { 13 | "run_at": "document_end", 14 | "matches": ["https://claude.ai/*"], 15 | "js": ["banner.js", "content.js"] 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2024 Ashwanth Kumar 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /banner.js: -------------------------------------------------------------------------------- 1 | // Copied from claudesave 2 | function createBanner(message, type = "error", timeout = 8000) { 3 | const banner = document.createElement("article"); 4 | banner.className = type === "error" ? "error" : "success"; 5 | banner.style.cssText = ` 6 | position: fixed; 7 | top: 0; 8 | left: 0; 9 | width: 100%; 10 | margin: 0; 11 | padding: 1rem; 12 | text-align: center; 13 | z-index: 9999; 14 | animation: slideDown 0.5s ease-out; 15 | `; 16 | banner.innerHTML = `
${message}
`; 17 | document.body.prepend(banner); 18 | 19 | // Set a timeout to remove the banner after 8 seconds 20 | setTimeout(() => { 21 | banner.style.animation = "slideUp 0.5s ease-in"; 22 | setTimeout(() => banner.remove(), 500); 23 | }, timeout); 24 | } 25 | 26 | // This adds the banner styles to the page 27 | const style = document.createElement("style"); 28 | style.textContent = ` 29 | @keyframes slideDown { 30 | from { transform: translateY(-100%); } 31 | to { transform: translateY(0); } 32 | } 33 | @keyframes slideUp { 34 | from { transform: translateY(0); } 35 | to { transform: translateY(-100%); } 36 | } 37 | article.error { 38 | background-color: #d30c00; 39 | border-color: #d30c00; 40 | color: white; 41 | } 42 | article.success { 43 | background-color: #125019; 44 | border-color: #125019; 45 | color: black white; 46 | } 47 | `; 48 | document.head.appendChild(style); 49 | -------------------------------------------------------------------------------- /content.js: -------------------------------------------------------------------------------- 1 | function addDownloadButton() { 2 | let buttonContainer = document.querySelector( 3 | ".flex.min-w-0.items-center.max-md\\:text-sm", 4 | ); 5 | 6 | if ( 7 | buttonContainer && 8 | !buttonContainer.querySelector(".claude-download-button") 9 | ) { 10 | let faLink = document.createElement("link"); 11 | faLink.rel = "stylesheet"; 12 | faLink.href = 13 | "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css"; 14 | document.head.appendChild(faLink); 15 | 16 | let downloadContainer = document.createElement("div"); 17 | downloadContainer.className = 18 | "claude-download-container ml-1 flex items-center"; 19 | 20 | let downloadButton = createButton( 21 | "download", 22 | "Download artifacts", 23 | "downloadArtifacts", 24 | ); 25 | 26 | let optionsDropdown = createOptionsDropdown(); 27 | 28 | downloadContainer.appendChild(optionsDropdown); 29 | downloadContainer.appendChild(downloadButton); 30 | buttonContainer.appendChild(downloadContainer); 31 | } 32 | } 33 | 34 | function createButton(icon, text, onClick) { 35 | let button = document.createElement("button"); 36 | button.className = 37 | "claude-download-button ml-1 flex items-center rounded-md bg-gray-100 py-1 px-3 text-sm font-medium text-gray-800 hover:bg-gray-200 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"; 38 | button.innerHTML = `${text}`; 39 | button.onclick = window[onClick]; 40 | return button; 41 | } 42 | 43 | function createOptionsDropdown() { 44 | let select = document.createElement("select"); 45 | select.className = 46 | "claude-download-options rounded-md bg-gray-100 py-1 px-2 text-sm font-medium text-gray-800 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"; 47 | 48 | let flatOption = document.createElement("option"); 49 | flatOption.value = "flat"; 50 | flatOption.textContent = "Flat structure"; 51 | 52 | let structuredOption = document.createElement("option"); 53 | structuredOption.value = "structured"; 54 | structuredOption.textContent = "Inferred structure"; 55 | 56 | select.appendChild(flatOption); 57 | select.appendChild(structuredOption); 58 | 59 | return select; 60 | } 61 | 62 | function downloadArtifacts() { 63 | const url = new URL(window.location.href); 64 | const uuid = url.pathname.split("/").pop(); 65 | const optionsDropdown = document.querySelector(".claude-download-options"); 66 | const useDirectoryStructure = optionsDropdown.value === "structured"; 67 | chrome.runtime.sendMessage({ 68 | action: "downloadArtifacts", 69 | uuid: uuid, 70 | useDirectoryStructure: useDirectoryStructure, 71 | }); 72 | } 73 | 74 | function checkAndAddShareButtons() { 75 | if (window.location.href.startsWith("https://claude.ai/chat/")) { 76 | const maxAttempts = 15; 77 | let attempts = 0; 78 | 79 | function tryAddButtons() { 80 | if (attempts < maxAttempts) { 81 | addDownloadButton(); 82 | if (!document.querySelector(".claude-download-button")) { 83 | attempts++; 84 | setTimeout(tryAddButtons, 1000); 85 | } 86 | } else { 87 | console.log("Failed to add share buttons after maximum attempts"); 88 | } 89 | } 90 | tryAddButtons(); 91 | } 92 | } 93 | 94 | // for already cached pages 95 | checkAndAddShareButtons(); 96 | 97 | // Listen for messages from the background script 98 | chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { 99 | if (request.action === "artifactsProcessed") { 100 | if (request.success) { 101 | createBanner(request.message, "success", 1000); 102 | } else if (request.failure) { 103 | createBanner(request.message, "error", 1000); 104 | } 105 | } else if (request.action === "checkAndAddDownloadButton") { 106 | // Observe DOM changes to add the button when the container is available 107 | checkAndAddShareButtons(); 108 | } 109 | return true; 110 | }); 111 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Claude Artifact Downloader 2 | 3 | ## Table of Contents 4 | 1. [Introduction](#introduction) 5 | 2. [Features](#features) 6 | 3. [Installation](#installation) 7 | 4. [Usage](#usage) 8 | 5. [Project Structure](#project-structure) 9 | 6. [How It Works](#how-it-works) 10 | 7. [Contributing](#contributing) 11 | 8. [Troubleshooting](#troubleshooting) 12 | 9. [License](#license) 13 | 14 | ## Introduction 15 | 16 | Claude Artifact Downloader is a Chrome extension designed to enhance your experience with the Claude AI chat interface. It allows users to easily download all artifacts (code snippets, diagrams, etc.) generated during a conversation with Claude as a single ZIP file. This tool is particularly useful for developers, researchers, and anyone who frequently uses Claude for generating or discussing code and other technical content. 17 | 18 | > This project was inspired and uses some code snippets from https://github.com/hamelsmu/claudesave. Thank you Hamel and other authors. 19 | 20 | ## Features 21 | 22 | - Adds a "Download Artifacts" button directly to the Claude chat interface 23 | - Extracts all artifacts from the current conversation 24 | - Packages artifacts into a single ZIP file for easy download 25 | - Preserves the chronological order of artifacts 26 | - Handles duplicate artifact names with intelligent suffixing 27 | - Assigns appropriate file extensions based on content type 28 | 29 | ## Installation 30 | 31 | To install the Claude Artifact Downloader extension: 32 | 33 | 1. Clone this repository or download the source code. 34 | 2. Open Google Chrome and navigate to `chrome://extensions`. 35 | 3. Enable "Developer mode" using the toggle switch in the top right corner. 36 | 4. Click "Load unpacked" and select the directory containing the extension files. 37 | 5. The extension should now appear in your list of installed extensions. 38 | 39 | ## Usage 40 | 41 | 1. Navigate to the Claude AI chat interface (https://claude.ai). 42 | 2. Start or continue a conversation with Claude. 43 | 3. When you're ready to download artifacts, look for the "Download Artifacts" button in the chat interface (usually near the top of the page). 44 | 4. Click the "Download Artifacts" button. 45 | 5. The extension will process the conversation and generate a ZIP file containing all artifacts. 46 | 6. Choose a location to save the ZIP file when prompted by your browser. 47 | 48 | ## Project Structure 49 | 50 | The project consists of the following key files: 51 | 52 | - `manifest.json`: Defines the extension's properties and permissions. 53 | - `background.js`: Contains the main logic for extracting and processing artifacts. 54 | - `content.js`: Handles the injection of the download button into the Claude interface. 55 | - `jszip.min.js`: Third-party library for creating ZIP files in the browser. 56 | 57 | ## How It Works 58 | 59 | 1. The extension adds a "Download Artifacts" button to the Claude chat interface. 60 | 2. When clicked, it extracts the conversation UUID from the current URL. 61 | 3. It then retrieves the chat data from Chrome's local storage. 62 | 4. The chat messages are processed recursively, starting from the most recent root message. 63 | 5. Artifacts are extracted from each message using regular expressions. 64 | 6. Each artifact is added to a ZIP file with a unique filename based on its title, language, and message index. 65 | 7. The ZIP file is then offered for download. 66 | 67 | ## Contributing 68 | 69 | Contributions to the Claude Artifact Downloader are welcome! If you'd like to contribute: 70 | 71 | 1. Fork the repository. 72 | 2. Create a new branch for your feature or bug fix. 73 | 3. Make your changes and commit them with clear, descriptive messages. 74 | 4. Push your changes to your fork. 75 | 5. Submit a pull request with a description of your changes. 76 | 77 | Please ensure your code adheres to the existing style and includes appropriate comments. 78 | 79 | ## Troubleshooting 80 | 81 | - If no artifacts are found, try refreshing the Claude chat page and attempting the download again. 82 | - Ensure you have the latest version of Google Chrome installed. 83 | - If you encounter any issues, check the browser console for error messages and report them in the project's issue tracker. 84 | 85 | ## License 86 | 87 | [MIT License](LICENSE) 88 | 89 | --- 90 | 91 | For any questions, issues, or feature requests, please open an issue in the project repository. 92 | -------------------------------------------------------------------------------- /background.js: -------------------------------------------------------------------------------- 1 | // background.js 2 | importScripts("jszip.min.js"); 3 | 4 | chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { 5 | if (request.action === "downloadArtifacts") { 6 | chrome.storage.local.get([`chat_${request.uuid}`], (result) => { 7 | const payload = result[`chat_${request.uuid}`]; 8 | if (!payload) { 9 | const msg = "No payload found, try refreshing the page."; 10 | chrome.tabs.sendMessage(sender.tab.id, { 11 | action: "artifactsProcessed", 12 | message: msg, 13 | }); 14 | return; 15 | } 16 | 17 | const chatData = payload; 18 | console.log(payload); 19 | const zip = new JSZip(); 20 | let artifactCount = 0; 21 | const usedNames = new Set(); 22 | 23 | // Find the most recent root message 24 | const mostRecentRootMessage = payload.chat_messages 25 | .filter( 26 | (message) => 27 | message.parent_message_uuid === 28 | "00000000-0000-4000-8000-000000000000", 29 | ) 30 | .reduce((latest, current) => { 31 | return new Date(current.updated_at) > new Date(latest.updated_at) 32 | ? current 33 | : latest; 34 | }); 35 | 36 | // Use the directory structure option from the request 37 | const useDirectoryStructure = request.useDirectoryStructure; 38 | 39 | // Start processing from the most recent root message 40 | if (mostRecentRootMessage) { 41 | artifactCount = processMessage( 42 | mostRecentRootMessage, 43 | payload, 44 | zip, 45 | usedNames, 46 | 0, 47 | useDirectoryStructure, 48 | ); 49 | } 50 | 51 | if (artifactCount === 0) { 52 | const msg = "No artifacts found in this conversation."; 53 | chrome.tabs.sendMessage(sender.tab.id, { 54 | action: "artifactsProcessed", 55 | success: true, 56 | message: msg, 57 | }); 58 | return; 59 | } 60 | 61 | zip.generateAsync({ type: "blob" }).then((content) => { 62 | const reader = new FileReader(); 63 | reader.onload = function (e) { 64 | const arrayBuffer = e.target.result; 65 | chrome.downloads.download( 66 | { 67 | url: 68 | "data:application/zip;base64," + 69 | arrayBufferToBase64(arrayBuffer), 70 | filename: `${chatData.name}.zip`, 71 | saveAs: true, 72 | }, 73 | (downloadId) => { 74 | if (chrome.runtime.lastError) { 75 | console.error(chrome.runtime.lastError); 76 | chrome.tabs.sendMessage(sender.tab.id, { 77 | action: "artifactsProcessed", 78 | failure: true, 79 | message: "Error downloading artifacts.", 80 | }); 81 | } else { 82 | chrome.tabs.sendMessage(sender.tab.id, { 83 | action: "artifactsProcessed", 84 | success: true, 85 | message: `${artifactCount} artifacts downloaded successfully.`, 86 | }); 87 | } 88 | }, 89 | ); 90 | }; 91 | reader.readAsArrayBuffer(content); 92 | }); 93 | }); 94 | } 95 | }); 96 | 97 | function processMessage( 98 | message, 99 | payload, 100 | zip, 101 | usedNames, 102 | artifactCount, 103 | useDirectoryStructure, 104 | depth = 0, 105 | ) { 106 | // Process assistant messages 107 | if (message.sender === "assistant" && message.text) { 108 | try { 109 | const artifacts = extractArtifacts(message.text); 110 | artifacts.forEach((artifact, artifactIndex) => { 111 | artifactCount++; 112 | const fileName = getUniqueFileName( 113 | artifact.title, 114 | artifact.language, 115 | message.index, 116 | usedNames, 117 | useDirectoryStructure, 118 | ); 119 | zip.file(fileName, artifact.content); 120 | console.log(`Added artifact: ${fileName}`); 121 | }); 122 | } catch (error) { 123 | console.error(`Error processing message ${message.uuid}:`, error); 124 | } 125 | } 126 | 127 | // Prevent excessive recursion 128 | if (depth > 100) { 129 | console.warn( 130 | "Maximum recursion depth reached. Stopping message processing.", 131 | ); 132 | return artifactCount; 133 | } 134 | 135 | // Find child messages 136 | const childMessages = payload.chat_messages.filter( 137 | (m) => m.parent_message_uuid === message.uuid, 138 | ); 139 | 140 | // Process child messages in chronological order 141 | childMessages 142 | .sort((a, b) => new Date(a.created_at) - new Date(b.created_at)) 143 | .forEach((childMessage) => { 144 | artifactCount = processMessage( 145 | childMessage, 146 | payload, 147 | zip, 148 | usedNames, 149 | artifactCount, 150 | useDirectoryStructure, 151 | depth + 1, 152 | ); 153 | }); 154 | 155 | return artifactCount; 156 | } 157 | 158 | function extractArtifacts(text) { 159 | const artifactRegex = />>=y=v>>>24,p-=y,!(16&(y=v>>>16&255))){if(0==(64&y)){v=_[(65535&v)+(d&(1<