Fix artifact API - use correct parameter and field names

Correct the artifact implementation to match Unsandbox API:
- Request parameter: return_artifact (boolean, singular)
- Response field: artifacts (array, plural)
- Data field: content_base64 (not data/content)

Changes:
- Backend: Changed artifacts to return_artifact in request body
- Frontend: Use return_artifact in execute request
- Frontend: Look for artifacts array in response (not artifact singleton)
- Frontend: Decode content_base64 field (not data/content)
- Frontend: Use filename field (primary) over name
- Remove debug console.log statements
- Update CLAUDE.md with correct API contract

The API parameter is singular but response is plural - this is intentional
design by Unsandbox API. Request enables artifact collection, response
returns array of generated artifacts.
This commit is contained in:
russell@unturf.com 2026-01-20 09:07:25 -05:00
parent b53ee2168b
commit c6e6e81c3c
3 changed files with 31 additions and 34 deletions

View file

@ -232,9 +232,10 @@ lang = un.detect_language("script.py") # Returns "python"
OpenCompletion supports artifacts generated during code execution (compiled binaries, images, videos, etc.).
**How Artifacts Work**:
- Pass `artifacts: true` in execution requests to enable artifact collection
- Artifacts are returned as **base64-encoded data** directly in the job response payload
- Pass `return_artifact: true` (boolean) in execution requests to enable artifact collection
- Response includes `artifacts` array with base64-encoded data
- No separate download endpoint needed - artifacts are embedded in the response
- Note: Parameter is singular `return_artifact` but response field is plural `artifacts`
**Artifact Response Format**:
```json
@ -246,15 +247,21 @@ OpenCompletion supports artifacts generated during code execution (compiled bina
"exit_code": 0,
"artifacts": [
{
"name": "output.png",
"type": "image/png",
"data": "base64string...",
"filename": "output.png",
"mime_type": "image/png",
"content_base64": "iVBORw0KGgoAAAANS...",
"size": 12345
}
]
}
```
**Artifact Fields**:
- `filename`: Original filename
- `mime_type`: MIME type (e.g., "image/png", "application/octet-stream")
- `content_base64`: Base64-encoded file content
- `size`: File size in bytes
**Artifact Types**:
- **Binaries**: Compiled executables (C, C++, Rust, Go, etc.)
- **Images**: PNG, JPG, GIF, SVG generated by code
@ -262,22 +269,22 @@ OpenCompletion supports artifacts generated during code execution (compiled bina
- **Text/Data**: JSON, CSV, TXT output files
**Frontend Features**:
- Download button decodes base64 and triggers browser download
- View button decodes base64 and displays inline:
- **Images**: Rendered as data URLs
- Download button decodes `content_base64` and triggers browser download
- View button decodes `content_base64` and displays inline:
- **Images**: Rendered as data URLs (`data:image/png;base64,...`)
- **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
- File size and MIME type information displayed
#### Frontend Integration
- Add play button (▶) next to copy button on code blocks
- Execute code when user clicks play button with `artifacts: true` parameter
- Execute code when user clicks play button with `return_artifact: true` parameter
- Display execution results inline below code block
- Show stdout, stderr, and exit_code separately
- Display artifacts section with download/view buttons
- Decode base64 artifacts for inline viewing and downloads
- Display artifact section with download/view buttons
- Decode `content_base64` field for inline viewing and downloads
- Images displayed as data URLs, videos as blob URLs
- Use syntax highlighting for output
- Handle timeouts gracefully (60s default)

2
app.py
View file

@ -1276,7 +1276,7 @@ def proxy_code_execute():
request_body = {
"language": language,
"code": code,
"return_artifacts": True,
"return_artifact": True,
}
# Use SDK's internal _make_request for full parameter support

View file

@ -1708,7 +1708,7 @@ async function executeCodeBlock(code, blockElement, playButton) {
body: JSON.stringify({
language: language,
code: code,
artifacts: true // Request artifacts (binaries, images, videos, etc)
return_artifact: true // Request artifacts (binaries, images, videos, etc)
})
});
@ -1946,10 +1946,6 @@ function displayExecutionResults(result, resultsContainer, language, code) {
// Handle artifacts (binaries, images, videos, etc.)
if (artifacts && artifacts.length > 0) {
// Debug: log artifact structure
console.log('Artifacts received:', artifacts);
console.log('First artifact:', artifacts[0]);
const artifactsDiv = document.createElement('div');
artifactsDiv.style.marginTop = '12px';
artifactsDiv.style.borderTop = '1px solid var(--border-color)';
@ -1963,9 +1959,6 @@ function displayExecutionResults(result, resultsContainer, language, code) {
artifactsDiv.appendChild(artifactsTitle);
artifacts.forEach((artifact, index) => {
// Debug: log each artifact
console.log(`Artifact ${index}:`, artifact);
console.log(`Artifact ${index} keys:`, Object.keys(artifact));
const artifactItem = document.createElement('div');
artifactItem.style.marginBottom = '8px';
artifactItem.style.padding = '8px';
@ -1976,17 +1969,18 @@ function displayExecutionResults(result, resultsContainer, language, code) {
const artifactName = document.createElement('div');
artifactName.style.fontWeight = 'bold';
artifactName.style.marginBottom = '4px';
artifactName.textContent = artifact.name || artifact.filename || `Artifact ${index + 1}`;
artifactName.textContent = artifact.filename || artifact.name || `Artifact ${index + 1}`;
artifactItem.appendChild(artifactName);
// Artifact type/size info
if (artifact.type || artifact.size) {
const mimeType = artifact.mime_type || artifact.type || '';
if (mimeType || artifact.size) {
const artifactInfo = document.createElement('div');
artifactInfo.style.fontSize = '12px';
artifactInfo.style.color = 'var(--text-muted)';
artifactInfo.style.marginBottom = '8px';
let infoText = '';
if (artifact.type) infoText += `Type: ${artifact.type}`;
if (mimeType) infoText += `Type: ${mimeType}`;
if (artifact.size) infoText += ` | Size: ${formatFileSize(artifact.size)}`;
artifactInfo.textContent = infoText;
artifactItem.appendChild(artifactInfo);
@ -1998,7 +1992,6 @@ function displayExecutionResults(result, resultsContainer, language, code) {
buttonsDiv.style.gap = '8px';
// Determine if artifact is viewable (images, videos, text)
const mimeType = artifact.mime_type || artifact.type || '';
const isImage = mimeType.startsWith('image/');
const isVideo = mimeType.startsWith('video/');
const isBinary = mimeType.includes('octet-stream') || mimeType.includes('executable');
@ -2126,12 +2119,8 @@ function formatFileSize(bytes) {
// Helper function to download artifact
async function downloadArtifact(artifact) {
try {
console.log('Download artifact called with:', artifact);
console.log('Artifact keys:', Object.keys(artifact));
// Artifacts come as base64 in the response
const base64Data = artifact.data || artifact.content;
console.log('Base64 data found:', base64Data ? `yes (${base64Data.length} chars)` : 'no');
// Artifacts come as base64 in the response (field: content_base64)
const base64Data = artifact.content_base64 || artifact.data || artifact.content;
if (!base64Data) {
console.error('No base64 data in artifact:', artifact);
@ -2154,7 +2143,7 @@ async function downloadArtifact(artifact) {
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = artifact.name || artifact.filename || 'download';
a.download = artifact.filename || artifact.name || 'download';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
@ -2168,9 +2157,10 @@ async function downloadArtifact(artifact) {
// Helper function to view artifact inline
async function viewArtifact(artifact, artifactItem, isImage, isVideo) {
try {
// Artifacts come as base64 in the response
const base64Data = artifact.data || artifact.content;
// Artifacts come as base64 in the response (field: content_base64)
const base64Data = artifact.content_base64 || artifact.data || artifact.content;
if (!base64Data) {
console.error('No base64 data in artifact:', artifact);
alert('Artifact data not available');
return;
}