// Edge AI Platform Installer — Wizard Controller v0.2 // ── i18n Dictionary ─────────────────────────────────────── const i18n = { en: { 'welcome.title': 'Edge AI Platform Installer', 'welcome.subtitle': 'Set up your edge AI development environment with Kneron hardware support.', 'path.title': 'Installation Path', 'path.subtitle': 'Choose where to install Edge AI Platform.', 'path.browse': 'Browse', 'path.required': 'Installation path is required.', 'path.valid': 'Path is valid.', 'components.title': 'Select Components', 'components.subtitle': 'Choose which components to install.', 'components.server': 'Edge AI Server', 'components.serverDesc': 'Core server binary for hardware communication (~10 MB)', 'components.models': 'Kneron Models', 'components.modelsDesc': 'Pre-trained NEF model files for KL520/KL720 (~50 MB)', 'components.python': 'Python Environment', 'components.pythonDesc': 'Python venv with Kneron PLUS SDK and dependencies (~200 MB)', 'components.libusb': 'libusb', 'components.libusbDesc': 'USB library required for Kneron device communication', 'components.symlink': 'CLI Symlink', 'components.symlinkDesc': "Add 'edge-ai' command to /usr/local/bin", 'relay.title': 'Relay Configuration', 'relay.subtitle': 'Configure the relay server for remote access. You can skip this and configure later.', 'relay.url': 'Relay URL', 'relay.token': 'Relay Token', 'relay.port': 'Server Port', 'relay.hint': 'Leave empty to skip relay configuration. You can set this later in the config file.', 'progress.title': 'Installing...', 'progress.subtitle': 'Please wait while components are being installed.', 'progress.preparing': 'Preparing installation...', 'hardware.title': 'Hardware Detection', 'hardware.subtitle': 'Connect your Kneron devices and scan for hardware.', 'hardware.scanning': 'Scanning for devices...', 'hardware.noDevices': 'No Kneron devices found. Connect a device and try again.', 'hardware.rescan': 'Rescan', 'complete.title': 'Installation Complete', 'complete.subtitle': 'Edge AI Platform has been installed successfully.', 'complete.location': 'Install Location', 'complete.server': 'Edge AI Server', 'complete.models': 'Kneron Models', 'complete.python': 'Python Environment', 'complete.libusb': 'libusb', 'complete.installed': 'Installed', 'complete.skipped': 'Skipped', 'btn.next': 'Next', 'btn.back': 'Back', 'btn.install': 'Install', 'btn.launch': 'Launch Server', 'btn.openDashboard': 'Open Dashboard', 'btn.close': 'Close', 'relay.tokenHint': 'Auto-generated random token. Both the server and browser use this to authenticate with the relay.', 'relay.dashboardUrl': 'Dashboard URL', 'relay.dashboardHint': 'The HTTP URL to access the dashboard via relay. Opened after server launch.', 'existing.detected': 'Existing installation detected', 'existing.desc': 'An existing installation was found. You can uninstall it or install over it.', 'existing.uninstall': 'Uninstall', 'uninstall.title': 'Uninstalling...', 'uninstall.subtitle': 'Removing installed files.', 'uninstall.confirm': 'This will remove the Edge AI Platform and all installed files. Continue?', 'uninstall.complete': 'Uninstall complete. System dependencies (Python, libusb) were preserved.', 'system.platform': 'Platform', 'system.python': 'Python', 'system.libusb': 'libusb', 'system.ffmpeg': 'FFmpeg', 'status.installed': 'Installed', 'status.notFound': 'Not found', 'status.notInstalled': 'Not installed', 'status.optional': 'Not installed (optional)', }, 'zh-TW': { 'welcome.title': 'Edge AI 平台安裝程式', 'welcome.subtitle': '設定您的邊緣 AI 開發環境,支援 Kneron 硬體。', 'path.title': '安裝路徑', 'path.subtitle': '選擇 Edge AI 平台的安裝位置。', 'path.browse': '瀏覽', 'path.required': '安裝路徑為必填。', 'path.valid': '路徑有效。', 'components.title': '選擇元件', 'components.subtitle': '選擇要安裝的元件。', 'components.server': 'Edge AI 伺服器', 'components.serverDesc': '硬體通訊核心伺服器程式 (~10 MB)', 'components.models': 'Kneron 模型', 'components.modelsDesc': 'KL520/KL720 預訓練 NEF 模型檔案 (~50 MB)', 'components.python': 'Python 環境', 'components.pythonDesc': '包含 Kneron PLUS SDK 的 Python 虛擬環境 (~200 MB)', 'components.libusb': 'libusb', 'components.libusbDesc': 'Kneron 裝置通訊所需的 USB 函式庫', 'components.symlink': 'CLI 捷徑', 'components.symlinkDesc': "新增 'edge-ai' 指令到 /usr/local/bin", 'relay.title': 'Relay 設定', 'relay.subtitle': '設定 Relay 伺服器以進行遠端存取。可以跳過稍後再設定。', 'relay.url': 'Relay URL', 'relay.token': 'Relay Token', 'relay.port': '伺服器連接埠', 'relay.hint': '留空可跳過 Relay 設定,稍後可在設定檔中修改。', 'progress.title': '安裝中...', 'progress.subtitle': '正在安裝元件,請稍候。', 'progress.preparing': '準備安裝中...', 'hardware.title': '硬體偵測', 'hardware.subtitle': '連接您的 Kneron 裝置並掃描硬體。', 'hardware.scanning': '正在掃描裝置...', 'hardware.noDevices': '未偵測到 Kneron 裝置。請連接裝置後再試。', 'hardware.rescan': '重新掃描', 'complete.title': '安裝完成', 'complete.subtitle': 'Edge AI 平台已成功安裝。', 'complete.location': '安裝位置', 'complete.server': 'Edge AI 伺服器', 'complete.models': 'Kneron 模型', 'complete.python': 'Python 環境', 'complete.libusb': 'libusb', 'complete.installed': '已安裝', 'complete.skipped': '已跳過', 'btn.next': '下一步', 'btn.back': '上一步', 'btn.install': '安裝', 'btn.launch': '啟動伺服器', 'btn.openDashboard': '開啟控制台', 'btn.close': '關閉', 'relay.tokenHint': '自動產生的隨機 Token。伺服器和瀏覽器都透過此 Token 向 Relay 驗證身份。', 'relay.dashboardUrl': 'Dashboard URL', 'relay.dashboardHint': '透過 Relay 存取 Dashboard 的 HTTP URL。啟動伺服器後會自動開啟。', 'existing.detected': '偵測到既有安裝', 'existing.desc': '發現既有安裝。您可以解除安裝或覆蓋安裝。', 'existing.uninstall': '解除安裝', 'uninstall.title': '解除安裝中...', 'uninstall.subtitle': '正在移除已安裝的檔案。', 'uninstall.confirm': '這將移除 Edge AI 平台及所有已安裝的檔案。是否繼續?', 'uninstall.complete': '解除安裝完成。系統相依套件(Python、libusb)已保留。', 'system.platform': '平台', 'system.python': 'Python', 'system.libusb': 'libusb', 'system.ffmpeg': 'FFmpeg', 'status.installed': '已安裝', 'status.notFound': '未找到', 'status.notInstalled': '未安裝', 'status.optional': '未安裝(選用)', } }; // ── State ───────────────────────────────────────────────── let currentStep = 0; let systemInfo = null; let installConfig = { installDir: '', createSymlink: true, installPythonEnv: true, installLibusb: true, relayURL: '', relayToken: '', dashboardURL: '', serverPort: 3721, language: 'en', }; // ── i18n Functions ──────────────────────────────────────── function t(key) { const dict = i18n[installConfig.language] || i18n.en; return dict[key] || i18n.en[key] || key; } function setLanguage(lang) { installConfig.language = lang; // Update active button state document.getElementById('lang-en').classList.toggle('active', lang === 'en'); document.getElementById('lang-zh').classList.toggle('active', lang === 'zh-TW'); // Update all elements with data-i18n attribute document.querySelectorAll('[data-i18n]').forEach(el => { const key = el.getAttribute('data-i18n'); const text = t(key); if (text) { el.textContent = text; } }); // Update html lang attribute document.documentElement.lang = lang === 'zh-TW' ? 'zh-TW' : 'en'; } // ── Step Navigation ─────────────────────────────────────── function showStep(n) { document.querySelectorAll('.step').forEach(s => s.classList.remove('active')); document.querySelectorAll('.step-dot').forEach((d, i) => { d.classList.remove('active', 'completed'); if (i < n) d.classList.add('completed'); if (i === n) d.classList.add('active'); }); const step = document.getElementById('step-' + n); if (step) step.classList.add('active'); currentStep = n; } // ── Step 0: Welcome ─────────────────────────────────────── async function initWelcome() { try { systemInfo = await window.go.main.Installer.GetSystemInfo(); document.getElementById('info-platform').textContent = systemInfo.os + ' / ' + systemInfo.arch; const pyEl = document.getElementById('info-python'); if (systemInfo.pythonAvailable) { pyEl.textContent = systemInfo.pythonVersion; pyEl.className = 'info-value status-ok'; } else { pyEl.textContent = t('status.notFound'); pyEl.className = 'info-value status-warn'; } const luEl = document.getElementById('info-libusb'); luEl.textContent = systemInfo.libusbInstalled ? t('status.installed') : t('status.notInstalled'); luEl.className = 'info-value ' + (systemInfo.libusbInstalled ? 'status-ok' : 'status-warn'); const ffEl = document.getElementById('info-ffmpeg'); ffEl.textContent = systemInfo.ffmpegAvailable ? t('status.installed') : t('status.optional'); ffEl.className = 'info-value ' + (systemInfo.ffmpegAvailable ? 'status-ok' : ''); installConfig.installDir = systemInfo.defaultDir; if (systemInfo.existingInstall) { document.getElementById('existing-install').style.display = 'block'; } } catch (err) { console.error('GetSystemInfo failed:', err); } } // ── Step 1: Path ────────────────────────────────────────── document.getElementById('btn-browse').addEventListener('click', async () => { try { const dir = await window.go.main.Installer.BrowseDirectory(); if (dir) { document.getElementById('install-path').value = dir; installConfig.installDir = dir; const msg = await window.go.main.Installer.ValidatePath(dir); const statusEl = document.getElementById('path-status'); if (msg) { statusEl.textContent = msg; statusEl.className = 'status-text error'; } else { statusEl.textContent = t('path.valid'); statusEl.className = 'status-text'; } } } catch (err) { console.error('BrowseDirectory failed:', err); } }); // ── Step 4: Install Progress ────────────────────────────── function addLogLine(message, type) { const log = document.getElementById('progress-log'); const line = document.createElement('div'); line.className = 'log-' + (type || 'line'); line.textContent = message; log.appendChild(line); log.scrollTop = log.scrollHeight; } async function startInstall() { showStep(4); document.getElementById('progress-title').textContent = t('progress.title'); document.getElementById('progress-subtitle').textContent = t('progress.subtitle'); document.getElementById('progress-log').innerHTML = ''; document.getElementById('progress-fill').style.width = '0%'; document.getElementById('progress-percent').textContent = '0%'; document.getElementById('progress-message').textContent = t('progress.preparing'); try { await window.go.main.Installer.StartInstall(installConfig); } catch (err) { addLogLine('Error: ' + err, 'error'); } } if (window.runtime && window.runtime.EventsOn) { window.runtime.EventsOn('install:progress', (event) => { const fill = document.getElementById('progress-fill'); const percent = document.getElementById('progress-percent'); const message = document.getElementById('progress-message'); fill.style.width = event.percent + '%'; percent.textContent = Math.round(event.percent) + '%'; message.textContent = event.message; addLogLine(event.message, event.isError ? 'error' : (event.isComplete ? 'success' : 'line')); if (event.isComplete && !event.isError) { setTimeout(() => { showStep(5); detectHardware(); }, 500); } }); window.runtime.EventsOn('uninstall:progress', (event) => { const message = document.getElementById('progress-message'); const fill = document.getElementById('progress-fill'); const percent = document.getElementById('progress-percent'); fill.style.width = event.percent + '%'; percent.textContent = Math.round(event.percent) + '%'; message.textContent = event.message; addLogLine(event.message, event.isError ? 'error' : 'line'); if (event.isComplete) { document.getElementById('progress-title').textContent = t('uninstall.title').replace('...', ''); document.getElementById('progress-subtitle').textContent = t('uninstall.complete'); addLogLine(t('uninstall.complete'), 'success'); } }); } // ── Step 5: Hardware Detection ──────────────────────────── async function detectHardware() { const el = document.getElementById('hardware-results'); el.innerHTML = '
' + t('hardware.scanning') + '
' + '' + t('hardware.noDevices') + '
Detection skipped: ' + err + '