328 lines
11 KiB
JavaScript
328 lines
11 KiB
JavaScript
// This is free software for the public good of a permacomputer hosted at
|
|
// permacomputer.com, an always-on computer by the people, for the people.
|
|
// One which is durable, easy to repair, & distributed like tap water
|
|
// for machine learning intelligence.
|
|
//
|
|
// The permacomputer is community-owned infrastructure optimized around
|
|
// four values:
|
|
//
|
|
// TRUTH First principles, math & science, open source code freely distributed
|
|
// FREEDOM Voluntary partnerships, freedom from tyranny & corporate control
|
|
// HARMONY Minimal waste, self-renewing systems with diverse thriving connections
|
|
// LOVE Be yourself without hurting others, cooperation through natural law
|
|
//
|
|
// This software contributes to that vision by making machine learning
|
|
// accessible to everyone through a free, open, embeddable chat interface.
|
|
// Code is seeds to sprout on any abandoned technology.
|
|
|
|
import { CODE_EXEC_URL } from "./config.js";
|
|
import * as un from "./un.js";
|
|
|
|
// Helper function to sleep/wait
|
|
function sleep(ms) {
|
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
}
|
|
|
|
// Helper function to escape HTML for safe display
|
|
export function escapeHtml(text) {
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
// Function to execute code in sandbox
|
|
export async function executeCode(code, language, resultsContainer, playButton) {
|
|
// Update button state
|
|
playButton.textContent = 'Running...';
|
|
playButton.disabled = true;
|
|
|
|
// Clear previous results
|
|
resultsContainer.innerHTML = '<div style="color: var(--uncloseai-secondary-text);">Executing code...</div>';
|
|
|
|
try {
|
|
// If no language specified, default to Python
|
|
if (!language) {
|
|
language = 'python';
|
|
}
|
|
|
|
// Check if un.com API is configured
|
|
if (un.isConfigured()) {
|
|
await executeWithUnsandbox(code, language, resultsContainer);
|
|
} else {
|
|
await executeWithFallback(code, language, resultsContainer);
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('Error executing code:', error);
|
|
resultsContainer.innerHTML = `<div style="color: #dc3545;">Error: ${escapeHtml(error.message)}</div>`;
|
|
} finally {
|
|
// Reset button state
|
|
playButton.textContent = '▶ Run';
|
|
playButton.disabled = false;
|
|
}
|
|
}
|
|
|
|
// Execute using un.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 un.executeAsync(language, code);
|
|
|
|
// Create cancel button (shown after 3 seconds)
|
|
cancelButton = createCancelButton(resultsContainer, async () => {
|
|
if (currentJobId) {
|
|
try {
|
|
await un.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 un.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
|
|
export function displayExecutionResults(result, resultsContainer, language) {
|
|
// Format and display results
|
|
let outputHtml = '';
|
|
|
|
// Handle nested response structure - check if stdout is an object with nested data
|
|
let actualStdout = result.stdout;
|
|
let actualStderr = result.stderr;
|
|
|
|
// If stdout is an object (nested response), extract the actual stdout/stderr
|
|
if (typeof result.stdout === 'object' && result.stdout !== null) {
|
|
actualStdout = result.stdout.stdout || '';
|
|
actualStderr = result.stdout.stderr || '';
|
|
}
|
|
|
|
// Show language
|
|
if (language) {
|
|
outputHtml += `<div style="color: var(--uncloseai-primary-text); margin-bottom: 8px; font-weight: bold;">Language: ${language}</div>`;
|
|
}
|
|
|
|
// Show stdout
|
|
if (actualStdout) {
|
|
outputHtml += '<div style="color: #28a745; font-weight: bold;">Output:</div>';
|
|
outputHtml += `<div style="color: var(--uncloseai-primary-text); margin-left: 10px;">${escapeHtml(actualStdout)}</div>`;
|
|
}
|
|
|
|
// Show stderr if present
|
|
if (actualStderr) {
|
|
outputHtml += '<div style="color: #dc3545; font-weight: bold; margin-top: 8px;">Errors/Warnings:</div>';
|
|
outputHtml += `<div style="color: #dc3545; margin-left: 10px;">${escapeHtml(actualStderr)}</div>`;
|
|
}
|
|
|
|
// If no output at all
|
|
if (!actualStdout && !actualStderr) {
|
|
outputHtml += '<div style="color: var(--uncloseai-secondary-text);">(No output produced)</div>';
|
|
}
|
|
|
|
resultsContainer.innerHTML = outputHtml;
|
|
}
|
|
|
|
// Function to add code execution buttons to code blocks
|
|
export function addCodeExecutionButton(codeBlock) {
|
|
// Check if we already have an execution button
|
|
const pre = codeBlock.parentElement;
|
|
if (!pre || pre.tagName.toLowerCase() !== 'pre') return;
|
|
if (pre.querySelector('.uncloseai-run-code-btn')) return; // Already has button
|
|
|
|
// Try to detect language from the code block's class
|
|
let language = null;
|
|
const classes = codeBlock.className.split(' ');
|
|
for (const cls of classes) {
|
|
if (cls.startsWith('language-')) {
|
|
language = cls.replace('language-', '');
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Default to Python if no language specified
|
|
if (!language) {
|
|
language = 'python';
|
|
}
|
|
|
|
// Get the code content
|
|
const code = codeBlock.dataset.fullContent || codeBlock.textContent;
|
|
|
|
// Create run button
|
|
const runButton = document.createElement('button');
|
|
runButton.textContent = '▶ Run';
|
|
runButton.className = 'uncloseai-run-code-btn uncloseai-code-copy-btn'; // Reuse copy button styling
|
|
runButton.title = `Run ${language} code in sandbox`;
|
|
|
|
// Create results container
|
|
let resultsContainer = pre.parentElement.querySelector('.uncloseai-code-execution-results');
|
|
if (!resultsContainer) {
|
|
resultsContainer = document.createElement('div');
|
|
resultsContainer.className = 'uncloseai-code-execution-results';
|
|
resultsContainer.style.marginTop = '10px';
|
|
resultsContainer.style.padding = '10px';
|
|
resultsContainer.style.backgroundColor = 'var(--uncloseai-modal-background)';
|
|
resultsContainer.style.borderRadius = '5px';
|
|
resultsContainer.style.fontFamily = 'monospace';
|
|
resultsContainer.style.fontSize = '14px';
|
|
resultsContainer.style.whiteSpace = 'pre-wrap';
|
|
resultsContainer.style.wordWrap = 'break-word';
|
|
resultsContainer.style.display = 'none'; // Hidden initially
|
|
|
|
// Insert after the pre element
|
|
pre.parentElement.insertBefore(resultsContainer, pre.nextSibling);
|
|
}
|
|
|
|
runButton.onclick = async () => {
|
|
// Show results container
|
|
resultsContainer.style.display = 'block';
|
|
await executeCode(code, language, resultsContainer, runButton);
|
|
};
|
|
|
|
// Find existing copy button container or create one
|
|
let buttonContainer = pre.querySelector('.uncloseai-code-copy-btn')?.parentElement;
|
|
if (!buttonContainer) {
|
|
// Create button container if it doesn't exist
|
|
buttonContainer = document.createElement('div');
|
|
buttonContainer.style.position = 'absolute';
|
|
buttonContainer.style.top = '8px';
|
|
buttonContainer.style.right = '8px';
|
|
buttonContainer.style.display = 'flex';
|
|
buttonContainer.style.gap = '4px';
|
|
pre.style.position = 'relative';
|
|
pre.appendChild(buttonContainer);
|
|
}
|
|
|
|
// Add run button to container
|
|
buttonContainer.appendChild(runButton);
|
|
}
|