Add unsandbox.com API key settings to modal
- Add public/src/unsandbox.js browser SDK with HMAC-SHA256 auth - Add settings UI for unsandbox public/secret keys in modal - Refactor code-execution.js to use SDK when keys configured - Falls back to unauthenticated endpoint when not configured - Add English translations for new settings
This commit is contained in:
parent
a00c7fd9e2
commit
52b08894c6
4 changed files with 459 additions and 84 deletions
|
|
@ -1,5 +1,6 @@
|
||||||
// Code execution sandbox functionality
|
// Code execution sandbox functionality
|
||||||
import { CODE_EXEC_URL } from "./config.js";
|
import { CODE_EXEC_URL } from "./config.js";
|
||||||
|
import * as unsandbox from "./unsandbox.js";
|
||||||
|
|
||||||
// Helper function to sleep/wait
|
// Helper function to sleep/wait
|
||||||
function sleep(ms) {
|
function sleep(ms) {
|
||||||
|
|
@ -28,90 +29,11 @@ export async function executeCode(code, language, resultsContainer, playButton)
|
||||||
language = 'python';
|
language = 'python';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use /execute/async endpoint with polling
|
// Check if unsandbox.com API is configured
|
||||||
const asyncResponse = await fetch(`${CODE_EXEC_URL}/execute/async`, {
|
if (unsandbox.isConfigured()) {
|
||||||
method: 'POST',
|
await executeWithUnsandbox(code, language, resultsContainer);
|
||||||
headers: {
|
} else {
|
||||||
'Content-Type': 'application/json',
|
await executeWithFallback(code, language, resultsContainer);
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
language: language,
|
|
||||||
code: code
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!asyncResponse.ok) {
|
|
||||||
throw new Error(`HTTP error! status: ${asyncResponse.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { job_id } = await asyncResponse.json();
|
|
||||||
|
|
||||||
// Poll for results: 300ms, 750ms, 1450ms, 2350ms, 3000ms, 4600ms, 6600ms+
|
|
||||||
const delays = [300, 450, 700, 900, 650, 1600, 2000];
|
|
||||||
let pollCount = 0;
|
|
||||||
let cancelButtonShown = false;
|
|
||||||
|
|
||||||
// Create cancel button (hidden initially)
|
|
||||||
let cancelButton = resultsContainer.querySelector('.uncloseai-cancel-execution-btn');
|
|
||||||
if (!cancelButton) {
|
|
||||||
cancelButton = document.createElement('button');
|
|
||||||
cancelButton.textContent = 'Cancel';
|
|
||||||
cancelButton.className = 'uncloseai-cancel-execution-btn uncloseai-btn-small';
|
|
||||||
cancelButton.style.display = 'none';
|
|
||||||
cancelButton.style.marginTop = '8px';
|
|
||||||
cancelButton.onclick = async () => {
|
|
||||||
try {
|
|
||||||
await fetch(`${CODE_EXEC_URL}/jobs/${job_id}`, { method: 'DELETE' });
|
|
||||||
cancelButton.disabled = true;
|
|
||||||
cancelButton.textContent = 'Cancelling...';
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error cancelling job:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
resultsContainer.appendChild(cancelButton);
|
|
||||||
}
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
await sleep(delays[Math.min(pollCount, delays.length - 1)]);
|
|
||||||
pollCount++;
|
|
||||||
|
|
||||||
const jobResponse = await fetch(`${CODE_EXEC_URL}/jobs/${job_id}`);
|
|
||||||
if (!jobResponse.ok) {
|
|
||||||
throw new Error(`Failed to fetch job status: ${jobResponse.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const job = await jobResponse.json();
|
|
||||||
|
|
||||||
if (job.status !== 'pending' && job.status !== 'running') {
|
|
||||||
// Job finished - hide cancel button
|
|
||||||
if (cancelButton) {
|
|
||||||
cancelButton.style.display = 'none';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (job.status === 'completed') {
|
|
||||||
const result = job.result;
|
|
||||||
displayExecutionResults(result, resultsContainer, language);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// timeout or cancelled
|
|
||||||
const errorMsg = job.result?.error || 'Execution failed';
|
|
||||||
const partialOutput = job.result?.partial_output;
|
|
||||||
|
|
||||||
let outputHtml = `<div style="color: #dc3545; font-weight: bold;">${escapeHtml(errorMsg)}</div>`;
|
|
||||||
if (partialOutput) {
|
|
||||||
outputHtml += '<div style="color: var(--uncloseai-secondary-text); margin-top: 8px;">Partial output before timeout:</div>';
|
|
||||||
outputHtml += `<div style="color: var(--uncloseai-primary-text); margin-left: 10px;">${escapeHtml(partialOutput)}</div>`;
|
|
||||||
}
|
|
||||||
resultsContainer.innerHTML = outputHtml;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show cancel button after poll #5 (3000ms) if still running
|
|
||||||
if (!cancelButtonShown && pollCount === 5) {
|
|
||||||
cancelButtonShown = true;
|
|
||||||
cancelButton.style.display = 'inline-block';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -124,6 +46,157 @@ export async function executeCode(code, language, resultsContainer, playButton)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Execute using unsandbox.com SDK (authenticated)
|
||||||
|
async function executeWithUnsandbox(code, language, resultsContainer) {
|
||||||
|
let pollCount = 0;
|
||||||
|
let cancelButton = null;
|
||||||
|
let currentJobId = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Start async execution to get job_id for cancel button
|
||||||
|
currentJobId = await unsandbox.executeAsync(language, code);
|
||||||
|
|
||||||
|
// Create cancel button (shown after 3 seconds)
|
||||||
|
cancelButton = createCancelButton(resultsContainer, async () => {
|
||||||
|
if (currentJobId) {
|
||||||
|
try {
|
||||||
|
await unsandbox.cancelJob(currentJobId);
|
||||||
|
cancelButton.disabled = true;
|
||||||
|
cancelButton.textContent = 'Cancelling...';
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error cancelling job:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wait for job with progress callback
|
||||||
|
const result = await unsandbox.waitForJob(currentJobId, null, null, (count) => {
|
||||||
|
pollCount = count;
|
||||||
|
// Show cancel button after poll #5 (around 3 seconds)
|
||||||
|
if (count >= 5 && cancelButton) {
|
||||||
|
cancelButton.style.display = 'inline-block';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Hide cancel button
|
||||||
|
if (cancelButton) {
|
||||||
|
cancelButton.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display result
|
||||||
|
if (result.status === 'completed') {
|
||||||
|
displayExecutionResults(result, resultsContainer, language);
|
||||||
|
} else {
|
||||||
|
displayErrorResult(result, resultsContainer);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
if (cancelButton) {
|
||||||
|
cancelButton.style.display = 'none';
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute using fallback endpoint (unauthenticated)
|
||||||
|
async function executeWithFallback(code, language, resultsContainer) {
|
||||||
|
// Use /execute/async endpoint with polling
|
||||||
|
const asyncResponse = await fetch(`${CODE_EXEC_URL}/execute/async`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
language: language,
|
||||||
|
code: code
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!asyncResponse.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${asyncResponse.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { job_id } = await asyncResponse.json();
|
||||||
|
|
||||||
|
// Poll for results using same delays as SDK
|
||||||
|
const delays = [300, 450, 700, 900, 650, 1600, 2000];
|
||||||
|
let pollCount = 0;
|
||||||
|
|
||||||
|
// Create cancel button
|
||||||
|
const cancelButton = createCancelButton(resultsContainer, async () => {
|
||||||
|
try {
|
||||||
|
await fetch(`${CODE_EXEC_URL}/jobs/${job_id}`, { method: 'DELETE' });
|
||||||
|
cancelButton.disabled = true;
|
||||||
|
cancelButton.textContent = 'Cancelling...';
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error cancelling job:', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
await sleep(delays[Math.min(pollCount, delays.length - 1)]);
|
||||||
|
pollCount++;
|
||||||
|
|
||||||
|
const jobResponse = await fetch(`${CODE_EXEC_URL}/jobs/${job_id}`);
|
||||||
|
if (!jobResponse.ok) {
|
||||||
|
throw new Error(`Failed to fetch job status: ${jobResponse.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const job = await jobResponse.json();
|
||||||
|
|
||||||
|
if (job.status !== 'pending' && job.status !== 'running') {
|
||||||
|
// Job finished - hide cancel button
|
||||||
|
cancelButton.style.display = 'none';
|
||||||
|
|
||||||
|
if (job.status === 'completed') {
|
||||||
|
const result = job.result || job;
|
||||||
|
displayExecutionResults(result, resultsContainer, language);
|
||||||
|
} else {
|
||||||
|
displayErrorResult(job.result || job, resultsContainer);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show cancel button after poll #5 (around 3 seconds)
|
||||||
|
if (pollCount === 5) {
|
||||||
|
cancelButton.style.display = 'inline-block';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create cancel button helper
|
||||||
|
function createCancelButton(resultsContainer, onCancel) {
|
||||||
|
let cancelButton = resultsContainer.querySelector('.uncloseai-cancel-execution-btn');
|
||||||
|
if (!cancelButton) {
|
||||||
|
cancelButton = document.createElement('button');
|
||||||
|
cancelButton.textContent = 'Cancel';
|
||||||
|
cancelButton.className = 'uncloseai-cancel-execution-btn uncloseai-btn-small';
|
||||||
|
cancelButton.style.display = 'none';
|
||||||
|
cancelButton.style.marginTop = '8px';
|
||||||
|
cancelButton.onclick = onCancel;
|
||||||
|
resultsContainer.appendChild(cancelButton);
|
||||||
|
} else {
|
||||||
|
cancelButton.disabled = false;
|
||||||
|
cancelButton.textContent = 'Cancel';
|
||||||
|
cancelButton.style.display = 'none';
|
||||||
|
cancelButton.onclick = onCancel;
|
||||||
|
}
|
||||||
|
return cancelButton;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display error result
|
||||||
|
function displayErrorResult(result, resultsContainer) {
|
||||||
|
const errorMsg = result.error || result.stderr || result.message || `Execution ${result.status || 'failed'}`;
|
||||||
|
const partialOutput = result.stdout || result.partial_output;
|
||||||
|
|
||||||
|
let outputHtml = `<div style="color: #dc3545; font-weight: bold;">${escapeHtml(errorMsg)}</div>`;
|
||||||
|
if (partialOutput) {
|
||||||
|
outputHtml += '<div style="color: var(--uncloseai-secondary-text); margin-top: 8px;">Partial output:</div>';
|
||||||
|
outputHtml += `<div style="color: var(--uncloseai-primary-text); margin-left: 10px;">${escapeHtml(partialOutput)}</div>`;
|
||||||
|
}
|
||||||
|
resultsContainer.innerHTML = outputHtml;
|
||||||
|
}
|
||||||
|
|
||||||
// Helper function to display execution results
|
// Helper function to display execution results
|
||||||
export function displayExecutionResults(result, resultsContainer, language) {
|
export function displayExecutionResults(result, resultsContainer, language) {
|
||||||
// Format and display results
|
// Format and display results
|
||||||
|
|
|
||||||
|
|
@ -112,4 +112,9 @@ export const en = {
|
||||||
pleaseEnterTranslateText: "Please enter some text to translate!",
|
pleaseEnterTranslateText: "Please enter some text to translate!",
|
||||||
aiModalNotAvailable: "AI modal not available in this context",
|
aiModalNotAvailable: "AI modal not available in this context",
|
||||||
useCustomAPI: "Use a custom openai compatible endpoint.",
|
useCustomAPI: "Use a custom openai compatible endpoint.",
|
||||||
|
|
||||||
|
// Unsandbox code execution settings
|
||||||
|
useUnsandbox: "Use unsandbox.com API keys for code execution.",
|
||||||
|
unsandboxPublicKey: "Public Key (unsb-pk-...)",
|
||||||
|
unsandboxSecretKey: "Secret Key (unsb-sk-...)",
|
||||||
};
|
};
|
||||||
|
|
@ -572,6 +572,76 @@ async function openUncloseaiEmbeddedModalNew() {
|
||||||
apiConfigSection.appendChild(customAPIToggle);
|
apiConfigSection.appendChild(customAPIToggle);
|
||||||
apiConfigSection.appendChild(apiConfigInputs);
|
apiConfigSection.appendChild(apiConfigInputs);
|
||||||
|
|
||||||
|
// Unsandbox Code Execution section
|
||||||
|
const unsandboxSection = document.createElement("div");
|
||||||
|
unsandboxSection.className = "uncloseai-section";
|
||||||
|
|
||||||
|
// Toggle for unsandbox API keys
|
||||||
|
const unsandboxToggle = document.createElement("div");
|
||||||
|
unsandboxToggle.className = "uncloseai-toggle-container";
|
||||||
|
|
||||||
|
const unsandboxCheckbox = document.createElement("input");
|
||||||
|
unsandboxCheckbox.type = "checkbox";
|
||||||
|
unsandboxCheckbox.id = "unsandbox-toggle";
|
||||||
|
unsandboxCheckbox.checked = localStorage.getItem("useUnsandbox") === "true";
|
||||||
|
|
||||||
|
const unsandboxToggleLabel = document.createElement("label");
|
||||||
|
unsandboxToggleLabel.htmlFor = "unsandbox-toggle";
|
||||||
|
unsandboxToggleLabel.textContent = getUIText("useUnsandbox");
|
||||||
|
unsandboxToggleLabel.className = "uncloseai-toggle-label";
|
||||||
|
|
||||||
|
unsandboxToggle.appendChild(unsandboxCheckbox);
|
||||||
|
unsandboxToggle.appendChild(unsandboxToggleLabel);
|
||||||
|
|
||||||
|
// Public Key input
|
||||||
|
const unsandboxPublicKeyContainer = document.createElement("div");
|
||||||
|
unsandboxPublicKeyContainer.className = "uncloseai-input-container";
|
||||||
|
|
||||||
|
const unsandboxPublicKeyInput = document.createElement("input");
|
||||||
|
unsandboxPublicKeyInput.type = "text";
|
||||||
|
unsandboxPublicKeyInput.placeholder = getUIText("unsandboxPublicKey");
|
||||||
|
unsandboxPublicKeyInput.value = localStorage.getItem("unsandboxPublicKey") || "";
|
||||||
|
unsandboxPublicKeyInput.className = "uncloseai-input";
|
||||||
|
|
||||||
|
unsandboxPublicKeyContainer.appendChild(unsandboxPublicKeyInput);
|
||||||
|
|
||||||
|
// Secret Key input
|
||||||
|
const unsandboxSecretKeyContainer = document.createElement("div");
|
||||||
|
unsandboxSecretKeyContainer.className = "uncloseai-input-container";
|
||||||
|
|
||||||
|
const unsandboxSecretKeyInput = document.createElement("input");
|
||||||
|
unsandboxSecretKeyInput.type = "password";
|
||||||
|
unsandboxSecretKeyInput.placeholder = getUIText("unsandboxSecretKey");
|
||||||
|
unsandboxSecretKeyInput.value = localStorage.getItem("unsandboxSecretKey") || "";
|
||||||
|
unsandboxSecretKeyInput.className = "uncloseai-input";
|
||||||
|
|
||||||
|
unsandboxSecretKeyContainer.appendChild(unsandboxSecretKeyInput);
|
||||||
|
|
||||||
|
// Unsandbox inputs container
|
||||||
|
const unsandboxInputs = document.createElement("div");
|
||||||
|
unsandboxInputs.className = "uncloseai-api-inputs";
|
||||||
|
unsandboxInputs.style.display = unsandboxCheckbox.checked ? "block" : "none";
|
||||||
|
unsandboxInputs.appendChild(unsandboxPublicKeyContainer);
|
||||||
|
unsandboxInputs.appendChild(unsandboxSecretKeyContainer);
|
||||||
|
|
||||||
|
// Event handlers for unsandbox
|
||||||
|
unsandboxCheckbox.onchange = () => {
|
||||||
|
const useUnsandbox = unsandboxCheckbox.checked;
|
||||||
|
localStorage.setItem("useUnsandbox", useUnsandbox.toString());
|
||||||
|
unsandboxInputs.style.display = useUnsandbox ? "block" : "none";
|
||||||
|
};
|
||||||
|
|
||||||
|
unsandboxPublicKeyInput.onchange = () => {
|
||||||
|
localStorage.setItem("unsandboxPublicKey", unsandboxPublicKeyInput.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
unsandboxSecretKeyInput.onchange = () => {
|
||||||
|
localStorage.setItem("unsandboxSecretKey", unsandboxSecretKeyInput.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
unsandboxSection.appendChild(unsandboxToggle);
|
||||||
|
unsandboxSection.appendChild(unsandboxInputs);
|
||||||
|
|
||||||
// Action buttons
|
// Action buttons
|
||||||
const actionsSection = document.createElement("div");
|
const actionsSection = document.createElement("div");
|
||||||
actionsSection.className = "uncloseai-section";
|
actionsSection.className = "uncloseai-section";
|
||||||
|
|
@ -650,6 +720,7 @@ async function openUncloseaiEmbeddedModalNew() {
|
||||||
const rightColumn = document.createElement("div");
|
const rightColumn = document.createElement("div");
|
||||||
rightColumn.className = "uncloseai-settings-column";
|
rightColumn.className = "uncloseai-settings-column";
|
||||||
rightColumn.appendChild(apiConfigSection);
|
rightColumn.appendChild(apiConfigSection);
|
||||||
|
rightColumn.appendChild(unsandboxSection);
|
||||||
|
|
||||||
// Quick actions span both columns
|
// Quick actions span both columns
|
||||||
actionsSection.className = "uncloseai-section uncloseai-actions-full-width";
|
actionsSection.className = "uncloseai-section uncloseai-actions-full-width";
|
||||||
|
|
|
||||||
226
public/src/unsandbox.js
Normal file
226
public/src/unsandbox.js
Normal file
|
|
@ -0,0 +1,226 @@
|
||||||
|
/**
|
||||||
|
* Unsandbox.com Browser SDK
|
||||||
|
* Browser-compatible wrapper for unsandbox.com API
|
||||||
|
* Based on official SDK patterns from lib/un.js
|
||||||
|
*/
|
||||||
|
|
||||||
|
const API_BASE = "https://api.unsandbox.com";
|
||||||
|
const POLL_DELAYS_MS = [300, 450, 700, 900, 650, 1600, 2000];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get credentials from localStorage
|
||||||
|
*/
|
||||||
|
export function getCredentials() {
|
||||||
|
const useUnsandbox = localStorage.getItem("useUnsandbox") === "true";
|
||||||
|
if (!useUnsandbox) return null;
|
||||||
|
|
||||||
|
const publicKey = localStorage.getItem("unsandboxPublicKey");
|
||||||
|
const secretKey = localStorage.getItem("unsandboxSecretKey");
|
||||||
|
|
||||||
|
if (publicKey && secretKey) {
|
||||||
|
return { publicKey, secretKey };
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if unsandbox.com API is configured
|
||||||
|
*/
|
||||||
|
export function isConfigured() {
|
||||||
|
return getCredentials() !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sign a request using HMAC-SHA256 (Web Crypto API)
|
||||||
|
* Message format: "timestamp:METHOD:path:body"
|
||||||
|
*/
|
||||||
|
async function signRequest(secretKey, timestamp, method, path, body = "") {
|
||||||
|
const message = `${timestamp}:${method}:${path}:${body}`;
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
const keyData = encoder.encode(secretKey);
|
||||||
|
const messageData = encoder.encode(message);
|
||||||
|
|
||||||
|
const cryptoKey = await crypto.subtle.importKey(
|
||||||
|
"raw",
|
||||||
|
keyData,
|
||||||
|
{ name: "HMAC", hash: "SHA-256" },
|
||||||
|
false,
|
||||||
|
["sign"]
|
||||||
|
);
|
||||||
|
|
||||||
|
const signature = await crypto.subtle.sign("HMAC", cryptoKey, messageData);
|
||||||
|
const hashArray = Array.from(new Uint8Array(signature));
|
||||||
|
return hashArray.map(b => b.toString(16).padStart(2, "0")).join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make an authenticated request to the unsandbox.com API
|
||||||
|
*/
|
||||||
|
async function makeRequest(method, path, publicKey, secretKey, data = null) {
|
||||||
|
const timestamp = Math.floor(Date.now() / 1000);
|
||||||
|
const body = data ? JSON.stringify(data) : "";
|
||||||
|
const signature = await signRequest(secretKey, timestamp, method, path, data ? body : "");
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
"Authorization": `Bearer ${publicKey}`,
|
||||||
|
"X-Timestamp": timestamp.toString(),
|
||||||
|
"X-Signature": signature,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
};
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
method,
|
||||||
|
headers,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
options.body = body;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(`${API_BASE}${path}`, options);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute code and wait for completion
|
||||||
|
*/
|
||||||
|
export async function executeCode(language, code, publicKey = null, secretKey = null) {
|
||||||
|
// Use provided credentials or get from localStorage
|
||||||
|
if (!publicKey || !secretKey) {
|
||||||
|
const creds = getCredentials();
|
||||||
|
if (!creds) {
|
||||||
|
throw new Error("No unsandbox.com credentials configured");
|
||||||
|
}
|
||||||
|
publicKey = creds.publicKey;
|
||||||
|
secretKey = creds.secretKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await makeRequest("POST", "/execute", publicKey, secretKey, {
|
||||||
|
language,
|
||||||
|
code,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check if we got immediate result or need to poll
|
||||||
|
const status = response.status;
|
||||||
|
if (status === "completed" || status === "failed" || status === "timeout" || status === "cancelled") {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Need to poll
|
||||||
|
const jobId = response.job_id;
|
||||||
|
if (!jobId) {
|
||||||
|
throw new Error("No job_id returned from execute endpoint");
|
||||||
|
}
|
||||||
|
|
||||||
|
return waitForJob(jobId, publicKey, secretKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute code asynchronously (returns job_id immediately)
|
||||||
|
*/
|
||||||
|
export async function executeAsync(language, code, publicKey = null, secretKey = null) {
|
||||||
|
if (!publicKey || !secretKey) {
|
||||||
|
const creds = getCredentials();
|
||||||
|
if (!creds) {
|
||||||
|
throw new Error("No unsandbox.com credentials configured");
|
||||||
|
}
|
||||||
|
publicKey = creds.publicKey;
|
||||||
|
secretKey = creds.secretKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await makeRequest("POST", "/execute", publicKey, secretKey, {
|
||||||
|
language,
|
||||||
|
code,
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.job_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get job status/result
|
||||||
|
*/
|
||||||
|
export async function getJob(jobId, publicKey = null, secretKey = null) {
|
||||||
|
if (!publicKey || !secretKey) {
|
||||||
|
const creds = getCredentials();
|
||||||
|
if (!creds) {
|
||||||
|
throw new Error("No unsandbox.com credentials configured");
|
||||||
|
}
|
||||||
|
publicKey = creds.publicKey;
|
||||||
|
secretKey = creds.secretKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
return makeRequest("GET", `/jobs/${jobId}`, publicKey, secretKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wait for job completion with exponential backoff polling
|
||||||
|
*/
|
||||||
|
export async function waitForJob(jobId, publicKey = null, secretKey = null, onPoll = null) {
|
||||||
|
if (!publicKey || !secretKey) {
|
||||||
|
const creds = getCredentials();
|
||||||
|
if (!creds) {
|
||||||
|
throw new Error("No unsandbox.com credentials configured");
|
||||||
|
}
|
||||||
|
publicKey = creds.publicKey;
|
||||||
|
secretKey = creds.secretKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
let pollCount = 0;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
// Sleep before polling
|
||||||
|
const delayIdx = Math.min(pollCount, POLL_DELAYS_MS.length - 1);
|
||||||
|
await new Promise(resolve => setTimeout(resolve, POLL_DELAYS_MS[delayIdx]));
|
||||||
|
pollCount++;
|
||||||
|
|
||||||
|
const response = await getJob(jobId, publicKey, secretKey);
|
||||||
|
|
||||||
|
// Callback for progress updates
|
||||||
|
if (onPoll) {
|
||||||
|
onPoll(pollCount, response);
|
||||||
|
}
|
||||||
|
|
||||||
|
const status = response.status;
|
||||||
|
if (status === "completed" || status === "failed" || status === "timeout" || status === "cancelled") {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancel a running job
|
||||||
|
*/
|
||||||
|
export async function cancelJob(jobId, publicKey = null, secretKey = null) {
|
||||||
|
if (!publicKey || !secretKey) {
|
||||||
|
const creds = getCredentials();
|
||||||
|
if (!creds) {
|
||||||
|
throw new Error("No unsandbox.com credentials configured");
|
||||||
|
}
|
||||||
|
publicKey = creds.publicKey;
|
||||||
|
secretKey = creds.secretKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
return makeRequest("DELETE", `/jobs/${jobId}`, publicKey, secretKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get supported languages
|
||||||
|
*/
|
||||||
|
export async function getLanguages(publicKey = null, secretKey = null) {
|
||||||
|
if (!publicKey || !secretKey) {
|
||||||
|
const creds = getCredentials();
|
||||||
|
if (!creds) {
|
||||||
|
throw new Error("No unsandbox.com credentials configured");
|
||||||
|
}
|
||||||
|
publicKey = creds.publicKey;
|
||||||
|
secretKey = creds.secretKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await makeRequest("GET", "/languages", publicKey, secretKey);
|
||||||
|
return response.languages || [];
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue