diff --git a/CLAUDE.md b/CLAUDE.md index 7138bd6..4340c62 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -28,3 +28,50 @@ ## Makefile Best Practices - Avoid variable substitutions - don't be afraid to be unDRY in the Makefile so engineers can copy and paste - Use tabs not spaces, and for fuck sake be happy about it + +## Running OpenCompletion + +### Environment Setup +- Use `vars.sh` to set up environment variables +- Required: MODEL_ENDPOINT_x and MODEL_API_KEY_x variables for AI models +- Run with: `source vars.sh && python app.py` + +### Makefile Commands +- `make venv` - Create virtual environment and install dependencies +- `make init-db` - Initialize database tables +- `make test` - Run all tests +- `make dev-setup` - Install development dependencies + +## OpenCompletion Architecture + +### Frontend Structure +- Main chat interface is in `templates/chat.html` +- Base template with CSS is in `templates/base.html` +- JavaScript code is inline in chat.html for real-time chat functionality +- Uses Socket.IO for WebSocket communication +- Uses marked.js for Markdown rendering and DOMPurify for XSS protection +- Code blocks are rendered with highlight.js for syntax highlighting + +### Code Block Rendering +- Code blocks are processed in messages after markdown conversion +- Copy buttons are added via `addCopyButtonToCodeBlock()` function (line 1176 in chat.html) +- Code blocks support: + - Syntax highlighting via highlight.js + - Line numbers via `addLineNumbers()` function + - Truncation for long code blocks via `truncateCodeBlock()` function + - Copy functionality that preserves full content even when truncated + +### Message Processing Flow +1. Messages received via Socket.IO events (chat_message, message_chunk for streaming) +2. Markdown converted to HTML using marked.js +3. HTML sanitized with DOMPurify +4. Code blocks enhanced with copy buttons, syntax highlighting, and line numbers + +### Code Execution Integration (New) +- Integration with unfirecracker-code-executor service on cammy.foxhop.net +- Service supports 38 working languages with auto-detection +- Endpoints: + - `/execute` - Execute code with specified language + - `/run` - Auto-detect language and execute +- Add play button next to copy button for code blocks +- Display execution results inline below code blocks diff --git a/templates/chat.html b/templates/chat.html index 856e861..f00e44c 100644 --- a/templates/chat.html +++ b/templates/chat.html @@ -85,6 +85,7 @@ // Constants const API_KEY = "dummy-api-key"; const TTS_API_URL = "https://speech.ai.unturf.com/v1/audio/speech"; +const CODE_EXEC_URL = "https://cammy-black.foxhop.net"; // Code execution service URL (served via Caddy) const urlParams = new URLSearchParams(window.location.search); let username = urlParams.get("username") || "guest"; // Default to "guest" if no username in URL @@ -1177,6 +1178,13 @@ function addCopyButtonToCodeBlock(block) { // Check if the full content is stored in a data attribute, otherwise use textContent const contentToCopy = block.dataset.fullContent || block.textContent; + // Create a container for the buttons + const buttonContainer = document.createElement('div'); + buttonContainer.classList.add('code-block-button-container'); + buttonContainer.style.display = 'flex'; + buttonContainer.style.gap = '8px'; + buttonContainer.style.marginBottom = '8px'; + // Create a button to copy the code block's content const copyButton = document.createElement('button'); copyButton.textContent = 'Copy'; @@ -1194,8 +1202,142 @@ function addCopyButtonToCodeBlock(block) { }); }; - // Insert the button before the code block - block.parentNode.insertBefore(copyButton, block); + // Create a button to execute the code block's content + const playButton = document.createElement('button'); + playButton.textContent = '▶ Run'; + playButton.classList.add('play-button'); + playButton.onclick = function() { + executeCodeBlock(contentToCopy, block, playButton); + }; + + // Add buttons to container + buttonContainer.appendChild(copyButton); + buttonContainer.appendChild(playButton); + + // Insert the button container before the code block + block.parentNode.insertBefore(buttonContainer, block); +} + +// Function to execute code block content +async function executeCodeBlock(code, blockElement, playButton) { + // Update button state + playButton.textContent = 'Running...'; + playButton.disabled = true; + + // Check if we already have a results container + let resultsContainer = blockElement.parentNode.querySelector('.code-execution-results'); + if (!resultsContainer) { + // Create results container + resultsContainer = document.createElement('div'); + resultsContainer.classList.add('code-execution-results'); + resultsContainer.style.marginTop = '10px'; + resultsContainer.style.padding = '10px'; + resultsContainer.style.backgroundColor = '#f0f0f0'; + resultsContainer.style.borderRadius = '5px'; + resultsContainer.style.fontFamily = 'monospace'; + resultsContainer.style.fontSize = '14px'; + resultsContainer.style.whiteSpace = 'pre-wrap'; + resultsContainer.style.wordWrap = 'break-word'; + + // Insert after the code block + blockElement.parentNode.insertBefore(resultsContainer, blockElement.nextSibling); + } + + // Clear previous results + resultsContainer.innerHTML = '
Executing code...
'; + + try { + // Try to detect language from the code block's class + let language = null; + const classes = blockElement.className.split(' '); + for (const cls of classes) { + if (cls.startsWith('language-')) { + language = cls.replace('language-', ''); + break; + } + } + + // If no language specified (no class on code block), default to Python + if (!language) { + language = 'python'; + } + + // Use /execute endpoint with specified or default language + const response = await fetch(`${CODE_EXEC_URL}/execute`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + language: language, + code: code + }) + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const result = await response.json(); + + // 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 (result.language) { + outputHtml += `
Language: ${result.language}
`; + } + + if (result.success) { + // Show stdout + if (actualStdout) { + outputHtml += '
Output:
'; + outputHtml += `
${escapeHtml(actualStdout)}
`; + } + + // Show stderr if present + if (actualStderr) { + outputHtml += '
Errors/Warnings:
'; + outputHtml += `
${escapeHtml(actualStderr)}
`; + } + + // If no output at all + if (!actualStdout && !actualStderr) { + outputHtml += '
(No output produced)
'; + } + } else { + // Execution failed + outputHtml += '
Execution Failed:
'; + outputHtml += `
${escapeHtml(result.error || result.stderr || 'Unknown error')}
`; + } + + resultsContainer.innerHTML = outputHtml; + + } catch (error) { + console.error('Error executing code:', error); + resultsContainer.innerHTML = `
Error: ${escapeHtml(error.message)}
`; + } finally { + // Reset button state + playButton.textContent = '▶ Run'; + playButton.disabled = false; + } +} + +// Helper function to escape HTML +function escapeHtml(text) { + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; } function addLineNumbers(block) { diff --git a/test_code_execution.html b/test_code_execution.html new file mode 100644 index 0000000..36f9ac0 --- /dev/null +++ b/test_code_execution.html @@ -0,0 +1,199 @@ + + + + + + Code Execution Test + + + + + +

Code Execution Service Test

+ +
+

Test 1: Python (Auto-detect)

+
print("Hello from Python!")
+for i in range(3):
+    print(f"Count: {i}")
+ +
+
+ +
+

Test 2: JavaScript (Specified)

+
console.log("Hello from JavaScript!");
+const arr = [1, 2, 3];
+arr.forEach(n => console.log(`Number: ${n}`));
+ +
+
+ +
+

Test 3: Ruby

+
puts "Hello from Ruby!"
+3.times do |i|
+  puts "Iteration #{i}"
+end
+ +
+
+ +
+

Test 4: Go

+
package main
+import "fmt"
+func main() {
+    fmt.Println("Hello from Go!")
+}
+ +
+
+ +
+

Test 5: C++

+
#include <iostream>
+using namespace std;
+int main() {
+    cout << "Hello from C++!" << endl;
+    return 0;
+}
+ +
+
+ + + + \ No newline at end of file