diff --git a/CLAUDE.md b/CLAUDE.md index 733eb26..a2928db 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -231,10 +231,29 @@ lang = un.detect_language("script.py") # Returns "python" OpenCompletion supports artifacts generated during code execution (compiled binaries, images, videos, etc.). -**Backend Implementation**: +**How Artifacts Work**: - Pass `artifacts: true` in execution requests to enable artifact collection -- Artifacts proxy endpoint: `/api/code/artifacts/` -- Handles authenticated download/viewing of artifacts +- Artifacts are returned as **base64-encoded data** directly in the job response payload +- No separate download endpoint needed - artifacts are embedded in the response + +**Artifact Response Format**: +```json +{ + "job_id": "job-xxx", + "status": "completed", + "stdout": "...", + "stderr": "...", + "exit_code": 0, + "artifacts": [ + { + "name": "output.png", + "type": "image/png", + "data": "base64string...", + "size": 12345 + } + ] +} +``` **Artifact Types**: - **Binaries**: Compiled executables (C, C++, Rust, Go, etc.) @@ -243,10 +262,13 @@ OpenCompletion supports artifacts generated during code execution (compiled bina - **Text/Data**: JSON, CSV, TXT output files **Frontend Features**: -- Download button for all artifact types -- View button for images/videos (disabled for binaries) -- Inline display of images/videos in chat -- File size and type information +- Download button decodes base64 and triggers browser download +- View button decodes base64 and displays inline: + - **Images**: Rendered as data URLs + - **Videos**: Rendered as blob URLs with controls + - **Text**: Decoded and displayed in formatted `
` blocks
+- View button disabled for binary executables
+- File size and type information displayed
 
 #### Frontend Integration
 
@@ -254,8 +276,9 @@ OpenCompletion supports artifacts generated during code execution (compiled bina
 - Execute code when user clicks play button with `artifacts: true` parameter
 - Display execution results inline below code block
 - Show stdout, stderr, and exit_code separately
-- Display artifacts with download/view buttons
-- Inline viewing of images and videos
+- Display artifacts section with download/view buttons
+- Decode base64 artifacts for inline viewing and downloads
+- Images displayed as data URLs, videos as blob URLs
 - Use syntax highlighting for output
 - Handle timeouts gracefully (60s default)
 - Support language auto-detection for fenced code blocks
diff --git a/app.py b/app.py
index 05d9277..b95e61a 100644
--- a/app.py
+++ b/app.py
@@ -1346,72 +1346,6 @@ def proxy_job_cancel(job_id):
         return jsonify({"error": "Failed to cancel job"}), 500
 
 
-@app.route("/api/code/artifacts/", methods=["GET"])
-def proxy_artifact_download(artifact_url):
-    """Proxy artifact download requests from Unsandbox API.
-
-    Artifacts are files generated during code execution (compiled binaries,
-    images, videos, etc.). This endpoint proxies the download with authentication.
-    """
-    import httpx
-
-    try:
-        # Check if credentials are configured
-        public_key = os.environ.get("UNSANDBOX_PUBLIC_KEY")
-        secret_key = os.environ.get("UNSANDBOX_SECRET_KEY")
-        if not public_key or not secret_key:
-            return jsonify({"error": "Code execution not configured"}), 503
-
-        # Decode the artifact URL (it's passed as part of the path)
-        import urllib.parse
-        full_artifact_url = urllib.parse.unquote(artifact_url)
-
-        # If it's a relative path, make it absolute
-        if not full_artifact_url.startswith("http"):
-            full_artifact_url = f"https://api.unsandbox.com{full_artifact_url}"
-
-        # Make authenticated request to download artifact
-        with httpx.Client(timeout=60.0) as client:
-            # Extract path for signing
-            from urllib.parse import urlparse
-            parsed = urlparse(full_artifact_url)
-            path = parsed.path
-
-            # Sign the request
-            import time
-            import hmac
-            import hashlib
-            timestamp = int(time.time())
-            message = f"{timestamp}:GET:{path}:"
-            signature = hmac.new(
-                secret_key.encode(),
-                message.encode(),
-                hashlib.sha256
-            ).hexdigest()
-
-            headers = {
-                "Authorization": f"Bearer {public_key}",
-                "X-Timestamp": str(timestamp),
-                "X-Signature": signature
-            }
-
-            response = client.get(full_artifact_url, headers=headers)
-            response.raise_for_status()
-
-            # Return the artifact with appropriate content type
-            return Response(
-                response.content,
-                mimetype=response.headers.get("Content-Type", "application/octet-stream"),
-                headers={
-                    "Content-Disposition": response.headers.get("Content-Disposition", "attachment")
-                }
-            )
-
-    except Exception as e:
-        print(f"Error downloading artifact: {e}")
-        return jsonify({"error": "Failed to download artifact"}), 500
-
-
 @app.route("/api/fix-code", methods=["POST"])
 def fix_code():
     """Auto-fix code errors by asking AI to fix issues based on stderr output.
diff --git a/templates/chat.html b/templates/chat.html
index ad4cb50..99924cc 100644
--- a/templates/chat.html
+++ b/templates/chat.html
@@ -2119,18 +2119,33 @@ function formatFileSize(bytes) {
 // Helper function to download artifact
 async function downloadArtifact(artifact) {
     try {
-        const artifactUrl = artifact.url || artifact.download_url;
-        if (!artifactUrl) {
-            alert('Artifact URL not available');
+        // Artifacts come as base64 in the response
+        const base64Data = artifact.data || artifact.content;
+        if (!base64Data) {
+            alert('Artifact data not available');
             return;
         }
 
-        // Use our backend proxy to download with authentication
-        const encodedUrl = encodeURIComponent(artifactUrl);
-        const proxyUrl = `/api/code/artifacts/${encodedUrl}`;
+        // Decode base64 to binary
+        const binaryString = atob(base64Data);
+        const bytes = new Uint8Array(binaryString.length);
+        for (let i = 0; i < binaryString.length; i++) {
+            bytes[i] = binaryString.charCodeAt(i);
+        }
 
-        // Open in new tab to trigger download
-        window.open(proxyUrl, '_blank');
+        // Determine mime type
+        const mimeType = artifact.mime_type || artifact.type || 'application/octet-stream';
+
+        // Create blob and download
+        const blob = new Blob([bytes], { type: mimeType });
+        const url = URL.createObjectURL(blob);
+        const a = document.createElement('a');
+        a.href = url;
+        a.download = artifact.name || artifact.filename || 'download';
+        document.body.appendChild(a);
+        a.click();
+        document.body.removeChild(a);
+        URL.revokeObjectURL(url);
     } catch (error) {
         console.error('Error downloading artifact:', error);
         alert('Failed to download artifact: ' + error.message);
@@ -2140,9 +2155,10 @@ async function downloadArtifact(artifact) {
 // Helper function to view artifact inline
 async function viewArtifact(artifact, artifactItem, isImage, isVideo) {
     try {
-        const artifactUrl = artifact.url || artifact.download_url;
-        if (!artifactUrl) {
-            alert('Artifact URL not available');
+        // Artifacts come as base64 in the response
+        const base64Data = artifact.data || artifact.content;
+        if (!base64Data) {
+            alert('Artifact data not available');
             return;
         }
 
@@ -2163,13 +2179,14 @@ async function viewArtifact(artifact, artifactItem, isImage, isVideo) {
         viewerDiv.style.maxWidth = '100%';
         viewerDiv.style.overflow = 'auto';
 
-        // Use our backend proxy for authenticated access
-        const encodedUrl = encodeURIComponent(artifactUrl);
-        const proxyUrl = `/api/code/artifacts/${encodedUrl}`;
+        // Determine mime type
+        const mimeType = artifact.mime_type || artifact.type || 'application/octet-stream';
 
         if (isImage) {
+            // Create data URL from base64
+            const dataUrl = `data:${mimeType};base64,${base64Data}`;
             const img = document.createElement('img');
-            img.src = proxyUrl;
+            img.src = dataUrl;
             img.style.maxWidth = '100%';
             img.style.height = 'auto';
             img.style.display = 'block';
@@ -2179,8 +2196,17 @@ async function viewArtifact(artifact, artifactItem, isImage, isVideo) {
             };
             viewerDiv.appendChild(img);
         } else if (isVideo) {
+            // Create blob URL from base64
+            const binaryString = atob(base64Data);
+            const bytes = new Uint8Array(binaryString.length);
+            for (let i = 0; i < binaryString.length; i++) {
+                bytes[i] = binaryString.charCodeAt(i);
+            }
+            const blob = new Blob([bytes], { type: mimeType });
+            const blobUrl = URL.createObjectURL(blob);
+
             const video = document.createElement('video');
-            video.src = proxyUrl;
+            video.src = blobUrl;
             video.controls = true;
             video.style.maxWidth = '100%';
             video.style.height = 'auto';
@@ -2188,21 +2214,22 @@ async function viewArtifact(artifact, artifactItem, isImage, isVideo) {
             video.onerror = () => {
                 viewerDiv.textContent = 'Failed to load video';
                 viewerDiv.style.color = 'var(--text-error)';
+                URL.revokeObjectURL(blobUrl);
             };
             viewerDiv.appendChild(video);
         } else {
-            // For other types, try to fetch and display as text
-            const response = await fetch(proxyUrl);
-            if (response.ok) {
-                const text = await response.text();
+            // For text types, decode and display
+            try {
+                const binaryString = atob(base64Data);
+                const text = decodeURIComponent(escape(binaryString));
                 const pre = document.createElement('pre');
                 pre.style.margin = '0';
                 pre.style.whiteSpace = 'pre-wrap';
                 pre.style.wordWrap = 'break-word';
                 pre.textContent = text;
                 viewerDiv.appendChild(pre);
-            } else {
-                viewerDiv.textContent = 'Failed to load content';
+            } catch (decodeError) {
+                viewerDiv.textContent = 'Failed to decode content';
                 viewerDiv.style.color = 'var(--text-error)';
             }
         }