314 lines
11 KiB
JavaScript
314 lines
11 KiB
JavaScript
// Main application logic
|
|
|
|
// Global action handlers
|
|
window.handleKill = async function(id) {
|
|
if (!confirm('Are you sure you want to kill this instance?')) return;
|
|
|
|
// Find the button and show loading state
|
|
const button = event.target;
|
|
const originalText = button.textContent;
|
|
button.disabled = true;
|
|
button.innerHTML = '<span class="spinner" style="width: 1rem; height: 1rem; border-width: 2px; display: inline-block; vertical-align: middle;"></span> Terminating...';
|
|
|
|
try {
|
|
await api.killInstance(id);
|
|
|
|
// Show success state
|
|
button.innerHTML = '✓ Terminated';
|
|
button.classList.remove('btn-danger');
|
|
button.classList.add('btn-secondary');
|
|
|
|
// Refresh after a short delay
|
|
setTimeout(() => {
|
|
router.handleRoute(router.currentRoute);
|
|
}, 1000);
|
|
} catch (error) {
|
|
// Restore button state on error
|
|
button.disabled = false;
|
|
button.textContent = originalText;
|
|
alert('Failed to kill instance: ' + error.message);
|
|
}
|
|
};
|
|
|
|
window.handleRestart = async function(id) {
|
|
// Find the button and show loading state
|
|
const button = event.target;
|
|
const originalText = button.textContent;
|
|
button.disabled = true;
|
|
button.innerHTML = '<span class="spinner" style="width: 1rem; height: 1rem; border-width: 2px; display: inline-block; vertical-align: middle;"></span> Restarting...';
|
|
|
|
try {
|
|
await api.restartInstance(id);
|
|
|
|
// Show intermediate states
|
|
button.innerHTML = '<span class="spinner" style="width: 1rem; height: 1rem; border-width: 2px; display: inline-block; vertical-align: middle;"></span> Starting...';
|
|
|
|
// Wait a bit then show success
|
|
setTimeout(() => {
|
|
button.innerHTML = '✓ Running';
|
|
button.classList.remove('btn-primary');
|
|
button.classList.add('btn-success');
|
|
}, 1500);
|
|
|
|
// Refresh current view
|
|
setTimeout(() => {
|
|
router.handleRoute(router.currentRoute);
|
|
}, 2500);
|
|
} catch (error) {
|
|
// Restore button state on error
|
|
button.disabled = false;
|
|
button.textContent = originalText;
|
|
alert('Failed to kill instance: ' + error.message);
|
|
}
|
|
};
|
|
|
|
// Modal management
|
|
const modal = {
|
|
element: null,
|
|
|
|
init() {
|
|
this.element = document.getElementById('new-run-modal');
|
|
|
|
// Close button
|
|
document.getElementById('modal-close').addEventListener('click', () => this.close());
|
|
document.getElementById('cancel-launch').addEventListener('click', () => this.close());
|
|
|
|
// Close on overlay click
|
|
this.element.querySelector('.modal-overlay').addEventListener('click', () => this.close());
|
|
|
|
// Form submission
|
|
document.getElementById('launch-form').addEventListener('submit', (e) => {
|
|
e.preventDefault();
|
|
this.handleLaunch();
|
|
});
|
|
|
|
// File browser buttons - use HTML5 file input
|
|
document.getElementById('browse-workspace').addEventListener('click', () => {
|
|
this.browseDirectory('workspace');
|
|
});
|
|
|
|
document.getElementById('browse-binary').addEventListener('click', () => {
|
|
this.browseFile('g3-binary-path');
|
|
});
|
|
|
|
// Provider change updates model options
|
|
document.getElementById('provider').addEventListener('change', (e) => {
|
|
this.updateModelOptions(e.target.value);
|
|
});
|
|
},
|
|
|
|
browseDirectory(inputId) {
|
|
// Create a hidden file input with directory picker
|
|
const input = document.createElement('input');
|
|
input.type = 'file';
|
|
input.webkitdirectory = true;
|
|
input.directory = true;
|
|
input.multiple = false;
|
|
|
|
input.onchange = (e) => {
|
|
const files = e.target.files;
|
|
if (files.length > 0) {
|
|
// Get the directory path from the first file
|
|
const path = files[0].webkitRelativePath.split('/')[0];
|
|
// In browser context, we get relative path, so we need to construct full path
|
|
// For now, just use the directory name and let user adjust
|
|
document.getElementById(inputId).value = path;
|
|
}
|
|
};
|
|
|
|
input.click();
|
|
},
|
|
|
|
browseFile(inputId) {
|
|
// Create a hidden file input
|
|
const input = document.createElement('input');
|
|
input.type = 'file';
|
|
input.accept = '*';
|
|
|
|
input.onchange = (e) => {
|
|
const files = e.target.files;
|
|
if (files.length > 0) {
|
|
// Get the file name
|
|
// Note: For security reasons, browsers don't give us the full path
|
|
// User will need to type the full path manually
|
|
document.getElementById(inputId).value = files[0].name;
|
|
}
|
|
};
|
|
|
|
input.click();
|
|
},
|
|
|
|
open() {
|
|
// Load saved state
|
|
const form = document.getElementById('launch-form');
|
|
if (state.lastWorkspace) {
|
|
form.workspace.value = state.lastWorkspace;
|
|
}
|
|
if (state.g3BinaryPath) {
|
|
form.g3_binary_path.value = state.g3BinaryPath;
|
|
}
|
|
form.provider.value = state.lastProvider;
|
|
this.updateModelOptions(state.lastProvider);
|
|
form.model.value = state.lastModel;
|
|
|
|
this.element.classList.remove('hidden');
|
|
},
|
|
|
|
close() {
|
|
this.element.classList.add('hidden');
|
|
},
|
|
|
|
updateModelOptions(provider) {
|
|
const modelSelect = document.getElementById('model');
|
|
const models = {
|
|
databricks: [
|
|
{ value: 'databricks-claude-sonnet-4-5', label: 'databricks-claude-sonnet-4-5' },
|
|
{ value: 'databricks-meta-llama-3-1-405b-instruct', label: 'databricks-meta-llama-3-1-405b-instruct' }
|
|
],
|
|
anthropic: [
|
|
{ value: 'claude-3-5-sonnet-20241022', label: 'claude-3-5-sonnet-20241022' },
|
|
{ value: 'claude-3-opus-20240229', label: 'claude-3-opus-20240229' }
|
|
],
|
|
local: [
|
|
{ value: 'local-model', label: 'Local Model' }
|
|
]
|
|
};
|
|
|
|
modelSelect.innerHTML = '';
|
|
for (const model of models[provider] || []) {
|
|
const option = document.createElement('option');
|
|
option.value = model.value;
|
|
option.textContent = model.label;
|
|
modelSelect.appendChild(option);
|
|
}
|
|
},
|
|
|
|
async handleLaunch() {
|
|
const form = document.getElementById('launch-form');
|
|
const formData = new FormData(form);
|
|
|
|
const data = {
|
|
prompt: formData.get('prompt'),
|
|
workspace: formData.get('workspace'),
|
|
provider: formData.get('provider'),
|
|
model: formData.get('model'),
|
|
mode: formData.get('mode'),
|
|
g3_binary_path: formData.get('g3_binary_path') || null
|
|
};
|
|
|
|
try {
|
|
// Show loading state
|
|
const submitBtn = form.querySelector('button[type="submit"]');
|
|
const modalBody = this.element.querySelector('.modal-body');
|
|
submitBtn.disabled = true;
|
|
submitBtn.innerHTML = '<span class="spinner" style="width: 1rem; height: 1rem; border-width: 2px; display: inline-block; vertical-align: middle;"></span> Starting g3 instance...';
|
|
|
|
const response = await api.launchInstance(data);
|
|
|
|
// Show intermediate state
|
|
submitBtn.innerHTML = '<span class="spinner" style="width: 1rem; height: 1rem; border-width: 2px; display: inline-block; vertical-align: middle;"></span> Waiting for process...';
|
|
|
|
// Wait a bit to let the process start
|
|
await new Promise(resolve => setTimeout(resolve, 1500));
|
|
submitBtn.innerHTML = '✓ Instance started!';
|
|
|
|
// Save state
|
|
state.updateLaunchDefaults(
|
|
data.workspace,
|
|
data.provider,
|
|
data.model,
|
|
data.g3_binary_path
|
|
);
|
|
|
|
// Close modal and navigate home
|
|
this.close();
|
|
router.navigate('/');
|
|
|
|
// Reset form
|
|
form.reset();
|
|
submitBtn.disabled = false;
|
|
submitBtn.textContent = 'Start Instance';
|
|
} catch (error) {
|
|
// Display detailed error message in modal
|
|
const errorDiv = document.createElement('div');
|
|
errorDiv.className = 'error-message';
|
|
errorDiv.style.cssText = 'background: #fee; border: 1px solid #fcc; color: #c33; padding: 1rem; margin: 1rem 0; border-radius: 0.5rem;';
|
|
|
|
let errorMessage = 'Failed to launch instance';
|
|
if (error.message) {
|
|
errorMessage += ': ' + error.message;
|
|
}
|
|
|
|
// Check for specific error types
|
|
if (error.message && error.message.includes('400')) {
|
|
errorMessage = 'Invalid configuration. Please check that the g3 binary path exists and is executable, and that the workspace directory is valid.';
|
|
} else if (error.message && error.message.includes('500')) {
|
|
errorMessage = 'Server error while launching instance. Check console logs for details.';
|
|
}
|
|
|
|
errorDiv.textContent = errorMessage;
|
|
|
|
// Remove any existing error messages
|
|
const existingError = modalBody.querySelector('.error-message');
|
|
if (existingError) existingError.remove();
|
|
|
|
// Insert error message at the top of modal body
|
|
modalBody.insertBefore(errorDiv, modalBody.firstChild);
|
|
|
|
const submitBtn = form.querySelector('button[type="submit"]');
|
|
submitBtn.disabled = false;
|
|
submitBtn.textContent = 'Start Instance';
|
|
}
|
|
}
|
|
};
|
|
|
|
// Theme toggle
|
|
function initTheme() {
|
|
const themeToggle = document.getElementById('theme-toggle');
|
|
|
|
themeToggle.addEventListener('click', () => {
|
|
const newTheme = state.theme === 'dark' ? 'light' : 'dark';
|
|
state.setTheme(newTheme);
|
|
themeToggle.textContent = newTheme === 'dark' ? '🌙' : '☀️';
|
|
});
|
|
|
|
// Set initial theme
|
|
document.body.className = state.theme;
|
|
themeToggle.textContent = state.theme === 'dark' ? '🌙' : '☀️';
|
|
}
|
|
|
|
// Initialize app
|
|
async function init() {
|
|
// Prevent double initialization
|
|
if (window.g3Initialized) {
|
|
return;
|
|
}
|
|
window.g3Initialized = true;
|
|
|
|
// Load state
|
|
await state.load();
|
|
|
|
// Initialize theme
|
|
initTheme();
|
|
|
|
// Initialize modal
|
|
modal.init();
|
|
|
|
// New Run button
|
|
document.getElementById('new-run-btn').addEventListener('click', () => {
|
|
modal.open();
|
|
});
|
|
|
|
// Initialize router
|
|
router.init();
|
|
}
|
|
|
|
// Simplified initialization - call exactly once when DOM is ready
|
|
if (document.readyState === 'loading') {
|
|
// DOM still loading, wait for DOMContentLoaded
|
|
document.addEventListener('DOMContentLoaded', init, { once: true });
|
|
} else {
|
|
// DOM already loaded (interactive or complete), init immediately
|
|
init();
|
|
}
|