diff --git a/public/src/code-execution.js b/public/src/code-execution.js
index bb1f031..013e18c 100644
--- a/public/src/code-execution.js
+++ b/public/src/code-execution.js
@@ -1,5 +1,6 @@
// Code execution sandbox functionality
import { CODE_EXEC_URL } from "./config.js";
+import * as unsandbox from "./unsandbox.js";
// Helper function to sleep/wait
function sleep(ms) {
@@ -28,90 +29,11 @@ export async function executeCode(code, language, resultsContainer, playButton)
language = 'python';
}
- // 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: 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 = `
${escapeHtml(errorMsg)}
`;
- if (partialOutput) {
- outputHtml += 'Partial output before timeout:
';
- outputHtml += `${escapeHtml(partialOutput)}
`;
- }
- 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';
- }
+ // Check if unsandbox.com API is configured
+ if (unsandbox.isConfigured()) {
+ await executeWithUnsandbox(code, language, resultsContainer);
+ } else {
+ await executeWithFallback(code, language, resultsContainer);
}
} 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 = `${escapeHtml(errorMsg)}
`;
+ if (partialOutput) {
+ outputHtml += 'Partial output:
';
+ outputHtml += `${escapeHtml(partialOutput)}
`;
+ }
+ resultsContainer.innerHTML = outputHtml;
+}
+
// Helper function to display execution results
export function displayExecutionResults(result, resultsContainer, language) {
// Format and display results
diff --git a/public/src/languages/en.js b/public/src/languages/en.js
index 850fc4b..7030d6e 100644
--- a/public/src/languages/en.js
+++ b/public/src/languages/en.js
@@ -112,4 +112,9 @@ export const en = {
pleaseEnterTranslateText: "Please enter some text to translate!",
aiModalNotAvailable: "AI modal not available in this context",
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-...)",
};
\ No newline at end of file
diff --git a/public/src/uncloseai-embed-modal.js b/public/src/uncloseai-embed-modal.js
index 79c5249..036c692 100644
--- a/public/src/uncloseai-embed-modal.js
+++ b/public/src/uncloseai-embed-modal.js
@@ -572,6 +572,76 @@ async function openUncloseaiEmbeddedModalNew() {
apiConfigSection.appendChild(customAPIToggle);
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
const actionsSection = document.createElement("div");
actionsSection.className = "uncloseai-section";
@@ -650,6 +720,7 @@ async function openUncloseaiEmbeddedModalNew() {
const rightColumn = document.createElement("div");
rightColumn.className = "uncloseai-settings-column";
rightColumn.appendChild(apiConfigSection);
+ rightColumn.appendChild(unsandboxSection);
// Quick actions span both columns
actionsSection.className = "uncloseai-section uncloseai-actions-full-width";
diff --git a/public/src/unsandbox.js b/public/src/unsandbox.js
new file mode 100644
index 0000000..76fd7c5
--- /dev/null
+++ b/public/src/unsandbox.js
@@ -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 || [];
+}