Validated secubox-image.sh and secubox-sysupgrade.sh scripts: - Fixed curl redirect issue: ASU API returns 301 redirects - Added -L flag to 9 curl calls across both scripts - Verified all device profiles valid (mochabin, espressobin, x86-64) - Confirmed POSIX sh compatibility for sysupgrade script - Validated first-boot script syntax Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
296 lines
10 KiB
JavaScript
296 lines
10 KiB
JavaScript
'use strict';
|
|
'require view';
|
|
'require rpc';
|
|
'require ui';
|
|
|
|
var callGetProviders = rpc.declare({
|
|
object: 'luci.ai-gateway',
|
|
method: 'get_providers',
|
|
expect: {}
|
|
});
|
|
|
|
var callSetProvider = rpc.declare({
|
|
object: 'luci.ai-gateway',
|
|
method: 'set_provider',
|
|
params: ['provider', 'enabled', 'api_key'],
|
|
expect: {}
|
|
});
|
|
|
|
var callTestProvider = rpc.declare({
|
|
object: 'luci.ai-gateway',
|
|
method: 'test_provider',
|
|
params: ['provider'],
|
|
expect: {}
|
|
});
|
|
|
|
var kissCSS = `
|
|
.providers-container { padding: 20px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; }
|
|
.providers-container h2 { margin: 0 0 8px 0; }
|
|
.providers-container .subtitle { color: var(--text-secondary, #64748b); margin-bottom: 24px; }
|
|
|
|
.provider-list { display: flex; flex-direction: column; gap: 16px; }
|
|
.provider-item { background: var(--bg-secondary, #f8fafc); border: 1px solid var(--border-color, #e2e8f0); border-radius: 12px; padding: 20px; }
|
|
.provider-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; }
|
|
.provider-title { display: flex; align-items: center; gap: 12px; }
|
|
.provider-name { font-size: 1.2em; font-weight: 600; text-transform: capitalize; }
|
|
.provider-badge { padding: 4px 10px; border-radius: 6px; font-size: 0.75em; font-weight: 500; }
|
|
.badge-local { background: #dcfce7; color: #16a34a; }
|
|
.badge-eu { background: #dbeafe; color: #2563eb; }
|
|
.badge-cloud { background: #fef3c7; color: #d97706; }
|
|
|
|
.provider-meta { display: flex; gap: 16px; font-size: 0.9em; color: var(--text-secondary, #64748b); margin-bottom: 16px; }
|
|
.provider-meta span { display: flex; align-items: center; gap: 4px; }
|
|
|
|
.provider-controls { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; }
|
|
.provider-controls input[type="text"], .provider-controls input[type="password"] {
|
|
padding: 8px 12px; border: 1px solid var(--border-color, #e2e8f0); border-radius: 6px;
|
|
font-size: 0.9em; min-width: 300px; background: var(--bg-primary, white);
|
|
}
|
|
|
|
.toggle-switch { position: relative; width: 48px; height: 24px; }
|
|
.toggle-switch input { opacity: 0; width: 0; height: 0; }
|
|
.toggle-slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0;
|
|
background: #cbd5e1; border-radius: 24px; transition: 0.3s; }
|
|
.toggle-slider:before { position: absolute; content: ""; height: 18px; width: 18px; left: 3px; bottom: 3px;
|
|
background: white; border-radius: 50%; transition: 0.3s; }
|
|
.toggle-switch input:checked + .toggle-slider { background: #22c55e; }
|
|
.toggle-switch input:checked + .toggle-slider:before { transform: translateX(24px); }
|
|
|
|
.btn { padding: 8px 16px; border-radius: 6px; font-weight: 500; cursor: pointer; border: none; font-size: 0.9em; }
|
|
.btn-primary { background: #3b82f6; color: white; }
|
|
.btn-primary:hover { background: #2563eb; }
|
|
.btn-secondary { background: #64748b; color: white; }
|
|
.btn-secondary:hover { background: #475569; }
|
|
.btn-success { background: #22c55e; color: white; }
|
|
.btn-success:hover { background: #16a34a; }
|
|
|
|
.status-indicator { padding: 4px 10px; border-radius: 6px; font-size: 0.8em; font-weight: 500; }
|
|
.status-available { background: #dcfce7; color: #16a34a; }
|
|
.status-configured { background: #dbeafe; color: #2563eb; }
|
|
.status-unavailable { background: #fee2e2; color: #dc2626; }
|
|
.status-disabled { background: #f1f5f9; color: #64748b; }
|
|
.status-no_api_key { background: #fef3c7; color: #d97706; }
|
|
|
|
.test-result { margin-top: 12px; padding: 12px; border-radius: 6px; font-size: 0.9em; }
|
|
.test-success { background: #dcfce7; color: #166534; }
|
|
.test-failure { background: #fee2e2; color: #991b1b; }
|
|
|
|
.info-text { font-size: 0.85em; color: var(--text-secondary, #64748b); margin-top: 8px; }
|
|
|
|
@media (prefers-color-scheme: dark) {
|
|
.provider-item { background: #1e293b; border-color: #334155; }
|
|
.provider-controls input { background: #0f172a; border-color: #334155; color: #f1f5f9; }
|
|
}
|
|
`;
|
|
|
|
var providerInfo = {
|
|
localai: {
|
|
name: 'LocalAI',
|
|
description: 'On-device inference via LocalAI. No API key required.',
|
|
tier: 'local',
|
|
tierLabel: 'LOCAL_ONLY',
|
|
badgeClass: 'badge-local',
|
|
needsKey: false
|
|
},
|
|
mistral: {
|
|
name: 'Mistral AI',
|
|
description: 'EU-based AI provider (France). GDPR compliant, sovereign cloud.',
|
|
tier: 'sanitized',
|
|
tierLabel: 'SANITIZED',
|
|
badgeClass: 'badge-eu',
|
|
needsKey: true,
|
|
keyUrl: 'https://console.mistral.ai/api-keys/'
|
|
},
|
|
claude: {
|
|
name: 'Claude (Anthropic)',
|
|
description: 'Anthropic Claude models. US-based.',
|
|
tier: 'cloud',
|
|
tierLabel: 'CLOUD_DIRECT',
|
|
badgeClass: 'badge-cloud',
|
|
needsKey: true,
|
|
keyUrl: 'https://console.anthropic.com/settings/keys'
|
|
},
|
|
openai: {
|
|
name: 'OpenAI (GPT)',
|
|
description: 'OpenAI GPT models. US-based.',
|
|
tier: 'cloud',
|
|
tierLabel: 'CLOUD_DIRECT',
|
|
badgeClass: 'badge-cloud',
|
|
needsKey: true,
|
|
keyUrl: 'https://platform.openai.com/api-keys'
|
|
},
|
|
gemini: {
|
|
name: 'Google Gemini',
|
|
description: 'Google Gemini models. US-based.',
|
|
tier: 'cloud',
|
|
tierLabel: 'CLOUD_DIRECT',
|
|
badgeClass: 'badge-cloud',
|
|
needsKey: true,
|
|
keyUrl: 'https://aistudio.google.com/app/apikey'
|
|
},
|
|
xai: {
|
|
name: 'xAI (Grok)',
|
|
description: 'xAI Grok models. US-based.',
|
|
tier: 'cloud',
|
|
tierLabel: 'CLOUD_DIRECT',
|
|
badgeClass: 'badge-cloud',
|
|
needsKey: true,
|
|
keyUrl: 'https://console.x.ai/'
|
|
}
|
|
};
|
|
|
|
return view.extend({
|
|
title: 'AI Providers',
|
|
|
|
load: function() {
|
|
return callGetProviders();
|
|
},
|
|
|
|
render: function(data) {
|
|
var providers = data.providers || data || [];
|
|
var container = E('div', { 'class': 'providers-container' });
|
|
|
|
container.appendChild(E('style', {}, kissCSS));
|
|
|
|
container.appendChild(E('h2', {}, 'AI Providers'));
|
|
container.appendChild(E('p', { 'class': 'subtitle' },
|
|
'Configure AI providers in priority order. LocalAI is always enabled for on-device inference.'));
|
|
|
|
var providerList = E('div', { 'class': 'provider-list' });
|
|
|
|
providers.forEach(function(provider) {
|
|
var info = providerInfo[provider.name] || {};
|
|
var item = E('div', { 'class': 'provider-item', 'data-provider': provider.name });
|
|
|
|
// Header
|
|
var header = E('div', { 'class': 'provider-header' }, [
|
|
E('div', { 'class': 'provider-title' }, [
|
|
E('span', { 'class': 'provider-name' }, info.name || provider.name),
|
|
E('span', { 'class': 'provider-badge ' + (info.badgeClass || 'badge-cloud') }, info.tierLabel || 'CLOUD')
|
|
]),
|
|
E('span', { 'class': 'status-indicator status-' + (provider.status || 'disabled') },
|
|
(provider.status || 'disabled').replace(/_/g, ' '))
|
|
]);
|
|
item.appendChild(header);
|
|
|
|
// Meta
|
|
var meta = E('div', { 'class': 'provider-meta' }, [
|
|
E('span', {}, ['Priority: ', String(provider.priority)]),
|
|
E('span', {}, ['Classification: ', (provider.classification || '-').toUpperCase()])
|
|
]);
|
|
item.appendChild(meta);
|
|
|
|
if (info.description) {
|
|
item.appendChild(E('p', { 'class': 'info-text' }, info.description));
|
|
}
|
|
|
|
// Controls
|
|
var controls = E('div', { 'class': 'provider-controls' });
|
|
|
|
// Enable toggle
|
|
var toggleId = 'toggle-' + provider.name;
|
|
var toggle = E('label', { 'class': 'toggle-switch' }, [
|
|
E('input', {
|
|
'type': 'checkbox',
|
|
'id': toggleId,
|
|
'checked': provider.enabled,
|
|
'change': this.handleToggle.bind(this, provider.name)
|
|
}),
|
|
E('span', { 'class': 'toggle-slider' })
|
|
]);
|
|
controls.appendChild(toggle);
|
|
controls.appendChild(E('label', { 'for': toggleId, 'style': 'cursor: pointer; margin-right: 16px;' },
|
|
provider.enabled ? 'Enabled' : 'Disabled'));
|
|
|
|
// API Key input (if needed)
|
|
if (info.needsKey) {
|
|
var keyInput = E('input', {
|
|
'type': 'password',
|
|
'id': 'key-' + provider.name,
|
|
'placeholder': 'Enter API key...',
|
|
'autocomplete': 'off'
|
|
});
|
|
controls.appendChild(keyInput);
|
|
|
|
controls.appendChild(E('button', {
|
|
'class': 'btn btn-primary',
|
|
'click': this.handleSaveKey.bind(this, provider.name)
|
|
}, 'Save Key'));
|
|
|
|
controls.appendChild(E('button', {
|
|
'class': 'btn btn-secondary',
|
|
'click': this.handleTest.bind(this, provider.name)
|
|
}, 'Test'));
|
|
|
|
if (info.keyUrl) {
|
|
controls.appendChild(E('a', {
|
|
'href': info.keyUrl,
|
|
'target': '_blank',
|
|
'style': 'font-size: 0.85em; color: #3b82f6;'
|
|
}, 'Get API Key'));
|
|
}
|
|
} else {
|
|
// LocalAI - just test button
|
|
controls.appendChild(E('button', {
|
|
'class': 'btn btn-secondary',
|
|
'click': this.handleTest.bind(this, provider.name)
|
|
}, 'Test Connection'));
|
|
}
|
|
|
|
item.appendChild(controls);
|
|
|
|
// Test result placeholder
|
|
item.appendChild(E('div', { 'class': 'test-result-container', 'id': 'result-' + provider.name }));
|
|
|
|
providerList.appendChild(item);
|
|
}.bind(this));
|
|
|
|
container.appendChild(providerList);
|
|
|
|
return container;
|
|
},
|
|
|
|
handleToggle: function(providerName, ev) {
|
|
var enabled = ev.target.checked ? '1' : '0';
|
|
callSetProvider(providerName, enabled, '').then(function() {
|
|
ui.addNotification(null, E('p', {},
|
|
providerName + ' ' + (enabled === '1' ? 'enabled' : 'disabled')), 'success');
|
|
});
|
|
},
|
|
|
|
handleSaveKey: function(providerName) {
|
|
var keyInput = document.getElementById('key-' + providerName);
|
|
var apiKey = keyInput ? keyInput.value : '';
|
|
|
|
if (!apiKey) {
|
|
ui.addNotification(null, E('p', {}, 'Please enter an API key'), 'warning');
|
|
return;
|
|
}
|
|
|
|
callSetProvider(providerName, '', apiKey).then(function() {
|
|
keyInput.value = '';
|
|
ui.addNotification(null, E('p', {}, 'API key saved for ' + providerName), 'success');
|
|
window.location.reload();
|
|
});
|
|
},
|
|
|
|
handleTest: function(providerName) {
|
|
var resultContainer = document.getElementById('result-' + providerName);
|
|
resultContainer.innerHTML = '<div class="test-result">Testing...</div>';
|
|
|
|
callTestProvider(providerName).then(function(result) {
|
|
var success = result.success;
|
|
var output = result.output || (success ? 'Provider is available' : 'Test failed');
|
|
|
|
resultContainer.innerHTML = '';
|
|
resultContainer.appendChild(E('div', {
|
|
'class': 'test-result ' + (success ? 'test-success' : 'test-failure')
|
|
}, output));
|
|
}).catch(function(err) {
|
|
resultContainer.innerHTML = '';
|
|
resultContainer.appendChild(E('div', { 'class': 'test-result test-failure' },
|
|
'Test error: ' + String(err)));
|
|
});
|
|
}
|
|
});
|