}
function hideAlert(id) { document.getElementById(id).className = 'alert'; }
+// Returns true if the current provider has a saved key in config
+function hasSavedKey() {
+ return systemData.has_provider && systemData.active_provider === selectedProvider;
+}
+
// ── Step 0: System Check ──────────────────────────────────────────
async function runSystemCheck() {
document.getElementById('system-rows').innerHTML = `
@@ -870,7 +1081,7 @@
Launch Your Twin 🚀
const data = await r.json();
systemData = data;
- // Update OS-sensitive labels once we know the OS
+ // OS-sensitive labels
const isMac = data.os === 'mac';
const deviceLabel = isMac ? 'Mac' : 'Pi';
document.getElementById('btn-save-soul').textContent = `Save to ${deviceLabel}`;
@@ -878,25 +1089,26 @@
Launch Your Twin 🚀
? 'This installs a launchd agent so PicoClaw starts automatically on login and restarts if it crashes.'
: 'This installs a systemd service so PicoClaw starts automatically on boot and restarts if it crashes.';
+ // FIX: check actual value for Disk/RAM — 'unavailable' means backend couldn't read it
const rows = [
- ['PicoClaw', data.picoclaw_installed, data.picoclaw_version || 'Not found'],
- ['Disk Space', true, data.disk_space],
- ['RAM', true, data.ram],
+ ['PicoClaw', data.picoclaw_installed, data.picoclaw_version || 'Not found'],
+ ['Disk Space', data.disk_space && data.disk_space !== 'unavailable', data.disk_space || 'unavailable'],
+ ['RAM', data.ram && data.ram !== 'unavailable', data.ram || 'unavailable'],
['LLM Provider', data.has_provider, data.active_model ? `${data.active_model} (${data.active_provider})` : 'Not set'],
- ['Telegram', data.has_telegram, data.telegram_token ? `Token: ${data.telegram_token}` : 'Not set'],
- ['SOUL.md', data.has_soul, data.has_soul ? 'Found' : 'Not created'],
- ['Service', data.service_status === 'active', data.service_status],
+ ['Telegram', data.has_telegram, data.telegram_token ? `Token: ${data.telegram_token}` : 'Not set'],
+ ['SOUL.md', data.has_soul, data.has_soul ? 'Found' : 'Not created'],
+ ['Service', data.service_status === 'active', data.service_status],
];
document.getElementById('system-rows').innerHTML = rows.map(([label, ok, val]) => `
- ${label}
-
- ${val}
-
- ${ok ? '✓ OK' : '○ Pending'}
-
+
+ ${label}
+ ${val}
+
+ ${ok ? '✓ OK' : '○ Pending'}
+
`).join('');
if (!data.picoclaw_installed) {
@@ -907,12 +1119,15 @@
Launch Your Twin 🚀
document.getElementById('install-picoclaw-section').style.display = 'none';
hideAlert('sys-alert');
document.getElementById('btn-sys-next').disabled = false;
- if (data.checklist.system) markDone(0);
+ if (data.checklist.system) markDone(0);
if (data.checklist.provider) { markDone(1); state.llm = true; }
if (data.checklist.telegram) { markDone(2); state.telegram = true; }
- if (data.checklist.soul) { markDone(3); state.soul = true; }
- if (data.checklist.service) { markDone(4); state.service = true; }
+ if (data.checklist.soul) { markDone(3); state.soul = true; }
+ if (data.checklist.service) { markDone(4); state.service = true; }
state.system = true;
+ // Show quick actions on system check if service is running
+ const qa = document.getElementById('quick-actions');
+ if (qa) qa.style.display = data.service_status === 'active' ? 'block' : 'none';
}
}
@@ -930,40 +1145,106 @@
Launch Your Twin 🚀
document.getElementById('llm-model').innerHTML = info.models
.map(([v, l]) => ``).join('');
}
- document.getElementById('llm-key').value = '';
- hideAlert('llm-alert');
+ // Don't clear the key field — but update the saved-key status indicator
+ updateKeyStatus();
+}
+
+function updateKeyStatus() {
+ const saved = hasSavedKey();
+ const keyEl = document.getElementById('key-status');
+ keyEl.className = saved ? 'key-status visible' : 'key-status';
+ // Enable Load Models if there's a saved key or user typed one
+ const typed = document.getElementById('llm-key').value.trim().length >= 10;
+ document.getElementById('btn-load-models').disabled = !saved && !typed;
+ // Relabel the validate button based on context
+ document.getElementById('btn-validate-llm').textContent = saved
+ ? 'Save Model'
+ : 'Validate Key';
+}
+
+function onKeyInput() {
+ updateKeyStatus();
}
async function validateLLM() {
const key = document.getElementById('llm-key').value.trim();
const model = document.getElementById('llm-model').value;
- if (!key) { showAlert('llm-alert', 'error', 'Please enter your API key'); return; }
+ if (!model) { showAlert('llm-alert', 'error', 'Please select a model'); return; }
+
+ // Allow empty key only if we have a saved one for this provider
+ if (!key && !hasSavedKey()) {
+ showAlert('llm-alert', 'error', 'Please enter your API key');
+ return;
+ }
const btn = document.getElementById('btn-validate-llm');
- btn.innerHTML = ' Validating...';
+ btn.innerHTML = ' Saving...';
btn.disabled = true;
const fd = new FormData();
fd.append('provider', selectedProvider || 'openrouter');
- fd.append('api_key', key);
fd.append('model', model);
+ if (key) fd.append('api_key', key); // only send if user typed one; backend falls back to saved key
const r = await fetch('/api/validate-llm', { method: 'POST', body: fd });
const data = await r.json();
- btn.innerHTML = 'Validate Key';
+ btn.innerHTML = hasSavedKey() ? 'Save Model' : 'Validate Key';
btn.disabled = false;
if (data.ok) {
- showAlert('llm-alert', 'success', '✓ ' + data.message + ` - using ${model}`);
- document.getElementById('btn-llm-next').disabled = false;
+ // Update systemData so hasSavedKey() stays accurate for the new provider
+ systemData.has_provider = true;
+ systemData.active_provider = selectedProvider;
+ systemData.active_model = model;
+ updateKeyStatus();
markDone(1); state.llm = true;
+ document.getElementById('btn-llm-next').disabled = false;
+
+ // If service is already running, restart it so the new model takes effect
+ if (state.service) {
+ showAlert('llm-alert', 'info', `✓ Model saved — restarting agent to apply ${model}...`);
+ btn.innerHTML = ' Restarting...';
+ btn.disabled = true;
+ const rr = await fetch('/api/restart-service', { method: 'POST' });
+ const rd = await rr.json();
+ btn.innerHTML = hasSavedKey() ? 'Save Model' : 'Validate Key';
+ btn.disabled = false;
+ if (rd.ok) {
+ showAlert('llm-alert', 'success', `✓ Model updated & agent restarted — now using ${model}`);
+ } else {
+ showAlert('llm-alert', 'warn', `✓ Model saved but restart failed: ${rd.message}`);
+ }
+ } else {
+ showAlert('llm-alert', 'success', '✓ ' + data.message + ` — using ${model}`);
+ }
} else {
showAlert('llm-alert', 'error', '✗ ' + data.message);
markError(1);
}
}
+async function loadModels() {
+ const key = document.getElementById('llm-key').value.trim();
+ const btn = document.getElementById('btn-load-models');
+ btn.innerHTML = '';
+ btn.disabled = true;
+
+ const fd = new FormData();
+ fd.append('provider', selectedProvider);
+ if (key) fd.append('api_key', key); // omit if empty — backend uses saved key
+
+ const r = await fetch('/api/models', { method: 'POST', body: fd });
+ const data = await r.json();
+ btn.innerHTML = '↻ Load Models';
+ btn.disabled = false;
+
+ if (!data.ok) { showAlert('llm-alert', 'error', '✗ ' + data.message); return; }
+ allModels = data.models;
+ filterModels();
+ showAlert('llm-alert', 'success', `✓ Loaded ${data.models.length} models`);
+}
+
// ── Step 2: Telegram ─────────────────────────────────────────────
async function validateTelegram() {
const token = document.getElementById('tg-token').value.trim();
@@ -1034,7 +1315,7 @@
Launch Your Twin 🚀
const r = await fetch('/api/save-soul', { method: 'POST', body: fd });
const data = await r.json();
if (data.ok) {
- showAlert('soul-save-alert', 'success', '✓ SOUL.md saved to your Pi at ' + data.message.split('to ')[1]);
+ showAlert('soul-save-alert', 'success', '✓ SOUL.md saved to your device at ' + data.message.split('to ')[1]);
document.getElementById('btn-soul-next').disabled = false;
markDone(3); state.soul = true;
} else { showAlert('soul-save-alert', 'error', '✗ ' + data.message); }
@@ -1044,6 +1325,8 @@
Launch Your Twin 🚀
async function loadFinalChecklist() {
const r = await fetch('/api/system-check');
const data = await r.json();
+ systemData = data; // keep in sync
+
const items = [
['PicoClaw installed', data.picoclaw_installed],
['LLM provider configured', data.has_provider],
@@ -1057,19 +1340,15 @@
Launch Your Twin 🚀
${label}
`).join('');
- // If service is already running, hide the install card and show success
if (data.service_status === 'active') {
document.getElementById('service-install-card').style.display = 'none';
document.getElementById('launch-success').style.display = 'block';
document.getElementById('progress').style.width = '100%';
markDone(4);
-
- const isMac = data.os === 'mac';
- document.getElementById('cmd-hint').textContent = isMac ? 'Useful commands on your Mac:' : 'Useful commands on your Pi:';
- document.getElementById('cmd-status').textContent = isMac ? 'launchctl list | grep picoclaw' : 'systemctl --user status picoclaw';
- document.getElementById('cmd-restart').textContent = isMac
- ? 'launchctl unload ~/Library/LaunchAgents/com.picoclaw.agent.plist && launchctl load ~/Library/LaunchAgents/com.picoclaw.agent.plist'
- : 'systemctl --user restart picoclaw';
+ populateOSCommands(data.os === 'mac');
+ } else {
+ document.getElementById('service-install-card').style.display = 'block';
+ document.getElementById('launch-success').style.display = 'none';
}
}
@@ -1079,28 +1358,22 @@
Launch Your Twin 🚀
const data = await r.json();
if (data.ok) {
showAlert('service-alert', 'success', '✓ ' + data.message);
- // Hide the install card — service is already running
- document.getElementById('service-install-card').style.display = 'none';
- document.getElementById('launch-success').style.display = 'block';
- document.getElementById('progress').style.width = '100%';
markDone(4); state.service = true;
-
- // Populate OS-specific commands
- const isMac = systemData.os === 'mac';
- document.getElementById('cmd-hint').textContent = isMac
- ? 'Useful commands on your Mac:'
- : 'Useful commands on your Pi:';
- document.getElementById('cmd-status').textContent = isMac
- ? 'launchctl list | grep picoclaw'
- : 'systemctl --user status picoclaw';
- document.getElementById('cmd-restart').textContent = isMac
- ? 'launchctl unload ~/Library/LaunchAgents/com.picoclaw.agent.plist && launchctl load ~/Library/LaunchAgents/com.picoclaw.agent.plist'
- : 'systemctl --user restart picoclaw';
-
- loadFinalChecklist();
+ // Re-fetch system status so checklist shows real service state
+ await loadFinalChecklist();
} else { showAlert('service-alert', 'error', '✗ ' + data.message); }
}
+function populateOSCommands(isMac) {
+ document.getElementById('cmd-hint').textContent = isMac ? 'Useful commands on your Mac:' : 'Useful commands on your Pi:';
+ document.getElementById('cmd-status').textContent = isMac
+ ? 'launchctl list | grep picoclaw'
+ : 'systemctl --user status picoclaw';
+ document.getElementById('cmd-restart').textContent = isMac
+ ? 'launchctl unload ~/Library/LaunchAgents/com.picoclaw.agent.plist && launchctl load ~/Library/LaunchAgents/com.picoclaw.agent.plist'
+ : 'systemctl --user restart picoclaw';
+}
+
function populateLLM() {
if (systemData.active_provider) selectProvider(systemData.active_provider);
if (systemData.active_model) {
@@ -1122,6 +1395,8 @@
Launch Your Twin 🚀
document.getElementById('btn-llm-next').disabled = false;
markDone(1);
}
+ // Always call this so Load Models button and key-status reflect current state
+ updateKeyStatus();
}
function populateTelegram() {
@@ -1160,23 +1435,52 @@