Fix artifact handling - use base64 from response payload
Artifacts are returned as base64-encoded data directly in the job response
payload, not as URLs. Previous implementation incorrectly tried to fetch
artifacts from URLs via a proxy endpoint.
Changes:
- Remove /api/code/artifacts proxy endpoint (not needed)
- Update downloadArtifact() to decode base64 and trigger download
- Update viewArtifact() to decode base64 and display inline:
* Images: rendered as data URLs (data:image/png;base64,...)
* Videos: decoded to blob URLs with controls
* Text: decoded and displayed in <pre> blocks
- Update CLAUDE.md with correct artifact format and implementation details
- Add artifact response format example showing base64 data structure
Artifact format in response:
{
"artifacts": [{
"name": "output.png",
"type": "image/png",
"data": "base64string...",
"size": 12345
}]
}
This commit is contained in:
parent
5576179ffa
commit
7a54952191
3 changed files with 81 additions and 97 deletions
41
CLAUDE.md
41
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/<encoded_url>`
|
||||
- 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 `<pre>` 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
|
||||
|
|
|
|||
66
app.py
66
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/<path:artifact_url>", 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.
|
||||
|
|
|
|||
|
|
@ -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)';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue