diff --git a/local-tool/visiona-local/frontend/app.js b/local-tool/visiona-local/frontend/app.js index 75d7e75..04043d9 100644 --- a/local-tool/visiona-local/frontend/app.js +++ b/local-tool/visiona-local/frontend/app.js @@ -1,540 +1,55 @@ -// Edge AI Platform Installer — Wizard Controller v0.2 +// visionA Local — splash / bootstrap +// 職責:等 Go server 起來,拿到 URL 後跳轉到 Next.js 主 UI -// ── 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': '未安裝(選用)', +import { GetServerStatus, GetServerURL } from './wailsjs/go/main/App.js'; + +const statusEl = document.getElementById('status'); +const errorEl = document.getElementById('error'); + +const POLL_INTERVAL_MS = 500; +const MAX_WAIT_MS = 60_000; +const startTime = Date.now(); + +async function poll() { + if (Date.now() - startTime > MAX_WAIT_MS) { + showError('伺服器啟動逾時(60 秒),請檢查 log 或重新啟動應用程式。'); + return; } -}; - -// ── 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') + '

' + - '
'; - - try { - const devices = await window.go.main.Installer.DetectHardware(); - if (!devices || devices.length === 0) { - el.innerHTML = '

' + t('hardware.noDevices') + '

'; - } else { - el.innerHTML = devices.map(d => - '
' + - '
' + - '
' + - 'Kneron ' + (d.model || 'Unknown') + '' + - '' + (d.product || d.port || '') + '' + - '
' + - '
' - ).join(''); - } - } catch (err) { - el.innerHTML = '

Detection skipped: ' + err + '

'; - } -} - -document.getElementById('btn-rescan').addEventListener('click', () => { - detectHardware(); -}); - -// ── Step 6: Complete ────────────────────────────────────── - -document.getElementById('btn-launch').addEventListener('click', async () => { - try { - await window.go.main.Installer.LaunchServer(); - // After launching, if dashboard URL is configured, open it automatically - const dashUrl = installConfig.dashboardURL || await window.go.main.Installer.GetDashboardURL(); - if (dashUrl) { - // Give the server a moment to start, then open dashboard - setTimeout(async () => { - // Append relay token as query param if available - let url = dashUrl; - if (installConfig.relayToken) { - const sep = url.includes('?') ? '&' : '?'; - url = url + sep + 'token=' + encodeURIComponent(installConfig.relayToken); - } - await window.go.main.Installer.OpenBrowser(url); - }, 2000); - } else { - // No relay — open local - setTimeout(async () => { - const port = installConfig.serverPort || 3721; - await window.go.main.Installer.OpenBrowser('http://127.0.0.1:' + port); - }, 2000); - } - } catch (err) { - alert('Failed to launch: ' + err); - } -}); - -document.getElementById('btn-open-dashboard').addEventListener('click', async () => { - const dashUrl = installConfig.dashboardURL || await window.go.main.Installer.GetDashboardURL(); - if (dashUrl) { - let url = dashUrl; - if (installConfig.relayToken) { - const sep = url.includes('?') ? '&' : '?'; - url = url + sep + 'token=' + encodeURIComponent(installConfig.relayToken); - } - await window.go.main.Installer.OpenBrowser(url); - } -}); - -document.getElementById('btn-close').addEventListener('click', () => { - if (window.runtime && window.runtime.Quit) { - window.runtime.Quit(); - } else { - window.close(); - } -}); - -// ── Uninstall ───────────────────────────────────────────── - -document.getElementById('btn-uninstall').addEventListener('click', async () => { - try { - const confirmed = await window.go.main.Installer.ConfirmUninstall(); - if (!confirmed) return; - } catch (err) { - // Fallback to JS confirm if native dialog fails - if (!confirm(t('uninstall.confirm'))) return; - } - showStep(4); - document.getElementById('progress-title').textContent = t('uninstall.title'); - document.getElementById('progress-subtitle').textContent = t('uninstall.subtitle'); - document.getElementById('progress-log').innerHTML = ''; - document.getElementById('progress-fill').style.width = '0%'; - document.getElementById('progress-percent').textContent = '0%'; - - try { - await window.go.main.Installer.Uninstall(); - } catch (err) { - addLogLine('Error: ' + err, 'error'); - } -}); - -// ── Navigation Wiring ───────────────────────────────────── - -document.addEventListener('DOMContentLoaded', () => { - // Detect initial language from browser - const browserLang = navigator.language || navigator.userLanguage || 'en'; - const initialLang = browserLang.startsWith('zh') ? 'zh-TW' : 'en'; - setLanguage(initialLang); - - initWelcome(); - - // Language switcher - document.getElementById('lang-en').addEventListener('click', () => setLanguage('en')); - document.getElementById('lang-zh').addEventListener('click', () => setLanguage('zh-TW')); - - // Step 0 -> Step 1 - document.getElementById('btn-next-0').addEventListener('click', () => { - showStep(1); - document.getElementById('install-path').value = installConfig.installDir; - }); - - // Step 1 Back -> Step 0 - document.getElementById('btn-back-1').addEventListener('click', () => { - showStep(0); - }); - - // Step 1 -> Step 2 - document.getElementById('btn-next-1').addEventListener('click', async () => { - const msg = await window.go.main.Installer.ValidatePath(installConfig.installDir); - if (msg) { - const statusEl = document.getElementById('path-status'); - statusEl.textContent = msg; - statusEl.className = 'status-text error'; + const status = await GetServerStatus(); + if (status && status.lastError) { + showError('伺服器啟動失敗:' + status.lastError); return; } - showStep(2); - }); - - // Step 2 Back -> Step 1 - document.getElementById('btn-back-2').addEventListener('click', () => { - showStep(1); - }); - - // Step 2 Install -> Step 3 (Relay Config) - document.getElementById('btn-install').addEventListener('click', async () => { - installConfig.createSymlink = document.getElementById('comp-symlink').checked; - installConfig.installPythonEnv = document.getElementById('comp-python').checked; - installConfig.installLibusb = document.getElementById('comp-libusb').checked; - showStep(3); - - // Auto-generate relay token if empty - const tokenInput = document.getElementById('relay-token'); - if (!tokenInput.value.trim()) { - try { - const token = await window.go.main.Installer.GenerateToken(); - tokenInput.value = token; - } catch (err) { - console.error('GenerateToken failed:', err); - } + if (status && status.running && status.url) { + statusEl.textContent = '啟動完成,載入主介面...'; + window.location.replace(status.url + '/'); + return; } - }); - // Step 3 Back -> Step 2 - document.getElementById('btn-back-3').addEventListener('click', () => { - showStep(2); - }); - - // Regenerate token button - document.getElementById('btn-regen-token').addEventListener('click', async () => { - try { - const token = await window.go.main.Installer.GenerateToken(); - document.getElementById('relay-token').value = token; - } catch (err) { - console.error('GenerateToken failed:', err); + const url = await GetServerURL(); + if (url) { + statusEl.textContent = '啟動完成,載入主介面...'; + window.location.replace(url + '/'); + return; } - }); + } catch (e) { + // binding 尚未就緒時會 throw,忽略繼續輪詢 + } - // Step 3 Next -> collect relay fields -> Step 4 (Progress) -> start install - document.getElementById('btn-next-3').addEventListener('click', () => { - installConfig.relayURL = document.getElementById('relay-url').value.trim(); - installConfig.relayToken = document.getElementById('relay-token').value.trim(); - installConfig.dashboardURL = document.getElementById('dashboard-url').value.trim(); - const portVal = parseInt(document.getElementById('server-port').value, 10); - installConfig.serverPort = (portVal >= 1024 && portVal <= 65535) ? portVal : 3721; - startInstall(); - }); + setTimeout(poll, POLL_INTERVAL_MS); +} - // Step 5 Next -> Step 6 (Complete) - document.getElementById('btn-next-5').addEventListener('click', () => { - showStep(6); - document.getElementById('summary-path').textContent = installConfig.installDir; +function showError(msg) { + statusEl.hidden = true; + errorEl.textContent = msg; + errorEl.hidden = false; +} - const modelsEl = document.getElementById('summary-models'); - modelsEl.textContent = t('complete.installed'); - modelsEl.className = 'info-value status-ok'; - - const pyEl = document.getElementById('summary-python'); - pyEl.textContent = installConfig.installPythonEnv ? t('complete.installed') : t('complete.skipped'); - pyEl.className = 'info-value ' + (installConfig.installPythonEnv ? 'status-ok' : 'status-skipped'); - - const luEl = document.getElementById('summary-libusb'); - luEl.textContent = installConfig.installLibusb ? t('complete.installed') : t('complete.skipped'); - luEl.className = 'info-value ' + (installConfig.installLibusb ? 'status-ok' : 'status-skipped'); - - // Show "Open Dashboard" button if relay dashboard URL is configured - if (installConfig.dashboardURL) { - document.getElementById('btn-open-dashboard').style.display = 'inline-flex'; - } - }); -}); +// 等 Wails runtime 就緒再開始輪詢 +if (window.runtime) { + poll(); +} else { + window.addEventListener('load', () => setTimeout(poll, 200)); +} diff --git a/local-tool/visiona-local/frontend/index.html b/local-tool/visiona-local/frontend/index.html index 03ecb12..febf98d 100644 --- a/local-tool/visiona-local/frontend/index.html +++ b/local-tool/visiona-local/frontend/index.html @@ -3,256 +3,18 @@ - Edge AI Platform Installer + visionA Local
- -
-
-
Edge AI Platform
-
Installer v0.2.0
-
-
-
- 1 - - 2 - - 3 - - 4 - - 5 - - 6 - - 7 -
-
-
-
- - | - -
-
-
- -
- -
-

Edge AI Platform Installer

-

Set up your edge AI development environment with Kneron hardware support.

- -
-
Platform-
-
Python-
-
libusb-
-
FFmpeg-
-
- - - -
- -
-
- - -
-

Installation Path

-

Choose where to install Edge AI Platform.

- -
-
- - -
-

-
- -
- - -
-
- - -
-

Select Components

-

Choose which components to install.

- -
- - - - - -
- -
- - -
-
- - -
-

Relay Configuration

-

Configure the relay server for remote access. You can skip this and configure later.

- -
- - -
- -
- -
- - -
-

Auto-generated random token. Both the server and browser use this to authenticate with the relay.

-
- -
- - -

The HTTP URL to access the dashboard via relay. Opened after server launch.

-
- -
- - -
- -

Leave empty to skip relay configuration. You can set this later in the config file.

- -
- - -
-
- - -
-

Installing...

-

Please wait while components are being installed.

- -
-
-
-
- 0% -
-

Preparing installation...

- -
-
- - -
-

Hardware Detection

-

Connect your Kneron devices and scan for hardware.

- -
-
-
-

Scanning for devices...

-
- -
- -
- - -
-
- - -
-
- - - - -
-

Installation Complete

-

Edge AI Platform has been installed successfully.

- -
-
Install Location-
-
Edge AI ServerInstalled
-
Kneron Models-
-
Python Environment-
-
libusb-
-
- -
- - - -
-
-
+
+ +
+
正在啟動伺服器...
+ +
- - - - + diff --git a/local-tool/visiona-local/frontend/style.css b/local-tool/visiona-local/frontend/style.css index 0fae6e0..23d6927 100644 --- a/local-tool/visiona-local/frontend/style.css +++ b/local-tool/visiona-local/frontend/style.css @@ -1,508 +1,62 @@ -/* Edge AI Platform Installer — Modernized v0.2 */ +/* visionA Local — splash screen */ * { margin: 0; padding: 0; box-sizing: border-box; } -:root { - --primary: #6366f1; - --primary-hover: #4f46e5; - --primary-light: #e0e7ff; - --bg: #fafbff; - --surface: #ffffff; - --border: #e2e8f0; - --text: #1e293b; - --text-secondary: #64748b; - --success: #10b981; - --warning: #f59e0b; - --error: #ef4444; - --danger: #dc2626; - --danger-hover: #b91c1c; - --radius: 10px; -} - -body { - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", sans-serif; - background: var(--bg); - color: var(--text); - height: 100vh; - overflow: hidden; - user-select: none; - -webkit-user-select: none; +html, body { + height: 100%; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Microsoft JhengHei', sans-serif; + background: #0b1020; + color: #e6e8f0; + -webkit-font-smoothing: antialiased; } #app { + display: flex; + align-items: center; + justify-content: center; + min-height: 100vh; +} + +.splash { display: flex; flex-direction: column; - height: 100vh; -} - -/* ── Header ─────────────────────────── */ -#wizard-header { - display: flex; align-items: center; - justify-content: space-between; - padding: 14px 24px; - background: linear-gradient(135deg, #1e293b 0%, #334155 100%); - color: white; - --wails-draggable: drag; - gap: 16px; + gap: 24px; + padding: 48px; } -.header-left { display: flex; flex-direction: column; gap: 2px; min-width: 140px; } -.logo-text { font-size: 15px; font-weight: 700; letter-spacing: -0.3px; } -.version-text { font-size: 10px; opacity: 0.5; } - -.header-center { flex: 1; display: flex; justify-content: center; } - -.header-right { min-width: 100px; display: flex; justify-content: flex-end; } - -.step-indicators { display: flex; align-items: center; gap: 4px; } - -.step-dot { - width: 28px; height: 28px; - border-radius: 50%; - border: 2px solid rgba(255,255,255,0.25); - display: flex; align-items: center; justify-content: center; - font-size: 11px; font-weight: 700; - color: rgba(255,255,255,0.35); - transition: all 0.35s cubic-bezier(0.4, 0, 0.2, 1); -} -.step-dot.active { - border-color: var(--primary); - background: var(--primary); - color: white; - box-shadow: 0 0 12px rgba(99, 102, 241, 0.5); - transform: scale(1.1); -} -.step-dot.completed { - border-color: var(--success); - background: var(--success); - color: white; -} - -.step-line { - width: 14px; height: 2px; - background: rgba(255,255,255,0.15); - border-radius: 1px; - transition: background 0.3s; -} - -/* ── Language Switcher ─────────────── */ -.lang-switch { - display: flex; - align-items: center; - gap: 6px; - --wails-draggable: no-drag; -} -.lang-sep { - color: rgba(255,255,255,0.3); - font-size: 12px; -} -.lang-btn { - background: transparent; - border: 1px solid rgba(255,255,255,0.25); - color: rgba(255,255,255,0.6); - padding: 3px 10px; - border-radius: 6px; - font-size: 11px; +.logo { + font-size: 28px; font-weight: 600; - cursor: pointer; - transition: all 0.2s; -} -.lang-btn:hover { - border-color: rgba(255,255,255,0.5); - color: rgba(255,255,255,0.9); -} -.lang-btn.active { - background: rgba(255,255,255,0.15); - border-color: rgba(255,255,255,0.4); - color: white; -} - -/* ── Main Content ───────────────────── */ -main { - flex: 1; - position: relative; - overflow: hidden; -} - -.step { - position: absolute; - inset: 0; - padding: 32px 36px; - display: none; - flex-direction: column; - overflow-y: auto; - opacity: 0; - transform: translateX(20px); - transition: opacity 0.35s ease, transform 0.35s ease; -} -.step.active { - display: flex; - opacity: 1; - transform: translateX(0); -} - -h1 { - font-size: 22px; - font-weight: 700; - margin-bottom: 6px; - color: var(--text); -} - -.subtitle { - font-size: 13px; - color: var(--text-secondary); - margin-bottom: 24px; - line-height: 1.6; -} - -/* ── Info Card ──────────────────────── */ -.info-card { - background: var(--surface); - border: 1px solid var(--border); - border-radius: var(--radius); - padding: 14px 18px; - margin-bottom: 20px; - box-shadow: 0 1px 3px rgba(0,0,0,0.04); -} - -.info-row { - display: flex; - justify-content: space-between; - align-items: center; - padding: 7px 0; -} -.info-row + .info-row { border-top: 1px solid var(--border); } -.info-label { font-size: 13px; color: var(--text-secondary); } -.info-value { font-size: 13px; font-weight: 500; } - -.status-ok { color: var(--success); } -.status-installed { color: var(--success); } -.status-warn { color: var(--warning); } -.status-err { color: var(--error); } -.status-skipped { color: var(--text-secondary); } - -/* ── Warning Card ───────────────────── */ -.warning-card { - background: #fef3c7; - border: 1px solid #f59e0b; - border-radius: var(--radius); - padding: 14px 18px; - margin-bottom: 20px; - font-size: 13px; -} -.warning-card p { margin: 6px 0; color: #92400e; } -.existing-path { font-family: "SF Mono", Menlo, monospace; font-size: 12px; } - -/* ── Buttons ────────────────────────── */ -.actions { - margin-top: auto; - padding-top: 20px; - display: flex; - justify-content: flex-end; - gap: 10px; -} - -.btn { - padding: 9px 22px; - border: none; - border-radius: var(--radius); - font-size: 13px; - font-weight: 600; - cursor: pointer; - transition: all 0.2s ease; - outline: none; -} -.btn:disabled { opacity: 0.5; cursor: not-allowed; } - -.btn-primary { - background: var(--primary); - color: white; - box-shadow: 0 1px 3px rgba(99, 102, 241, 0.3); -} -.btn-primary:hover:not(:disabled) { - background: var(--primary-hover); - box-shadow: 0 2px 8px rgba(99, 102, 241, 0.4); - transform: translateY(-1px); -} - -.btn-secondary { - background: var(--border); - color: var(--text); -} -.btn-secondary:hover:not(:disabled) { background: #cbd5e1; } - -.btn-ghost { - background: transparent; - color: var(--text-secondary); - border: 1px solid var(--border); -} -.btn-ghost:hover:not(:disabled) { - background: var(--surface); - color: var(--text); - border-color: #cbd5e1; -} - -.btn-danger { background: var(--danger); color: white; } -.btn-danger:hover:not(:disabled) { background: var(--danger-hover); } - -.btn-warning { background: var(--warning); color: white; } -.btn-warning:hover:not(:disabled) { background: #d97706; } - -/* ── Form Groups ───────────────────── */ -.form-group { - margin-bottom: 18px; -} - -.field-label { - display: block; - font-size: 13px; - font-weight: 600; - color: var(--text); - margin-bottom: 6px; -} - -.field-hint { - font-size: 12px; - color: var(--text-secondary); - margin-top: 4px; - line-height: 1.5; - font-style: italic; -} - -/* ── Path Input ─────────────────────── */ -.path-input-group { - display: flex; - gap: 8px; - margin-bottom: 8px; -} - -.input-field { - flex: 1; - padding: 10px 14px; - border: 1px solid var(--border); - border-radius: var(--radius); - font-size: 13px; - background: var(--surface); - color: var(--text); - transition: border-color 0.2s, box-shadow 0.2s; - outline: none; -} -.input-field:focus { - border-color: var(--primary); - box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.12); -} -.input-field::placeholder { - color: #94a3b8; -} - -.status-text { - font-size: 12px; - color: var(--text-secondary); - margin-bottom: 12px; - min-height: 18px; -} -.status-text.error { color: var(--error); } - -/* ── Component List ─────────────────── */ -.component-list { - display: flex; - flex-direction: column; - gap: 6px; - margin-bottom: 20px; -} - -.component-item { - display: flex; - align-items: flex-start; - gap: 12px; - padding: 12px 14px; - background: var(--surface); - border: 1px solid var(--border); - border-radius: var(--radius); - cursor: pointer; - transition: border-color 0.2s, box-shadow 0.2s; -} -.component-item:hover { - border-color: var(--primary); - box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.08); -} -.component-item.required { opacity: 0.85; } - -.component-check { - display: flex; - align-items: center; - padding-top: 1px; -} -.component-check input[type="checkbox"] { margin: 0; } - -.component-info { display: flex; flex-direction: column; gap: 3px; } -.component-name { font-size: 13px; font-weight: 600; } -.component-desc { font-size: 11px; color: var(--text-secondary); line-height: 1.4; } - -/* ── Progress ───────────────────────── */ -.progress-container { - display: flex; - align-items: center; - gap: 14px; - margin-bottom: 10px; -} - -.progress-bar { - flex: 1; - height: 10px; - background: var(--border); - border-radius: 5px; - overflow: hidden; - position: relative; -} - -.progress-fill { - height: 100%; - background: linear-gradient(90deg, var(--primary), #818cf8, var(--primary)); - background-size: 200% 100%; - border-radius: 5px; - transition: width 0.4s ease; - animation: progress-pulse 2s ease-in-out infinite; -} - -@keyframes progress-pulse { - 0%, 100% { background-position: 0% 0%; } - 50% { background-position: 100% 0%; } -} - -.progress-percent { - font-size: 14px; - font-weight: 700; - color: var(--primary); - min-width: 40px; - text-align: right; -} - -.progress-message { - font-size: 13px; - color: var(--text-secondary); - margin-bottom: 14px; -} - -.log-area { - flex: 1; - min-height: 120px; - max-height: 220px; - background: #0f172a; - color: #94a3b8; - border-radius: var(--radius); - padding: 12px 14px; - font-family: "SF Mono", "Menlo", "Cascadia Code", monospace; - font-size: 11px; - line-height: 1.7; - overflow-y: auto; - box-shadow: inset 0 1px 4px rgba(0,0,0,0.2); -} - -.log-area .log-line { color: #94a3b8; } -.log-area .log-error { color: var(--error); } -.log-area .log-success { color: var(--success); } - -/* ── Hardware List ──────────────────── */ -.hardware-list { margin-bottom: 20px; } - -.device-card { - display: flex; - align-items: center; - gap: 14px; - padding: 14px 18px; - background: var(--surface); - border: 1px solid var(--border); - border-radius: var(--radius); - margin-bottom: 8px; - box-shadow: 0 1px 3px rgba(0,0,0,0.04); - transition: border-color 0.2s; -} -.device-card:hover { border-color: var(--primary); } - -.device-card .device-icon { - width: 40px; height: 40px; - background: var(--primary-light); - border-radius: 10px; - display: flex; align-items: center; justify-content: center; - font-size: 18px; - color: var(--primary); -} - -.device-card .device-info { display: flex; flex-direction: column; gap: 3px; } -.device-card .device-name { font-size: 14px; font-weight: 600; } -.device-card .device-detail { font-size: 11px; color: var(--text-secondary); } - -.device-scanning { - display: flex; - flex-direction: column; - align-items: center; - gap: 12px; - padding: 32px; - color: var(--text-secondary); - font-size: 13px; + letter-spacing: 0.02em; + color: #ffffff; } .spinner { - width: 28px; height: 28px; - border: 3px solid var(--border); - border-top-color: var(--primary); + width: 42px; + height: 42px; + border: 3px solid rgba(255, 255, 255, 0.12); + border-top-color: #6ea8ff; border-radius: 50%; - animation: spin 0.8s linear infinite; + animation: spin 0.9s linear infinite; } @keyframes spin { to { transform: rotate(360deg); } } -.no-devices { - text-align: center; - padding: 28px; - color: var(--text-secondary); +.status { + font-size: 14px; + color: #a7b0c6; +} + +.error { + max-width: 560px; + padding: 16px 20px; + background: rgba(220, 38, 38, 0.12); + border: 1px solid rgba(220, 38, 38, 0.4); + border-radius: 8px; + color: #fca5a5; font-size: 13px; -} - -/* ── Complete Icon ──────────────────── */ -.complete-icon { + line-height: 1.5; text-align: center; - margin-bottom: 12px; - color: var(--success); - animation: checkmark-pop 0.5s ease; -} - -@keyframes checkmark-pop { - 0% { transform: scale(0.5); opacity: 0; } - 60% { transform: scale(1.1); } - 100% { transform: scale(1); opacity: 1; } -} - -/* ── Summary List ──────────────────── */ -.summary-list { - background: var(--surface); - border: 1px solid var(--border); - border-radius: var(--radius); - padding: 14px 18px; - margin-bottom: 20px; -} - -/* ── Scrollbar ──────────────────────── */ -::-webkit-scrollbar { width: 6px; } -::-webkit-scrollbar-track { background: transparent; } -::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 3px; } -::-webkit-scrollbar-thumb:hover { background: #94a3b8; } - -/* ── Step transition for newly shown steps ── */ -@keyframes step-enter { - from { - opacity: 0; - transform: translateX(24px); - } - to { - opacity: 1; - transform: translateX(0); - } -} - -.step.active { - animation: step-enter 0.35s ease forwards; }