diff --git a/local-tool/frontend/src/app/settings/page.tsx b/local-tool/frontend/src/app/settings/page.tsx index 6bc655a..29938b6 100644 --- a/local-tool/frontend/src/app/settings/page.tsx +++ b/local-tool/frontend/src/app/settings/page.tsx @@ -21,12 +21,17 @@ import { useTranslation } from '@/lib/i18n'; import { ServerLogViewer } from '@/components/server-log-viewer'; import { ServerStatusDashboard } from '@/components/server-status-dashboard'; -// TODO(M2+): read these values from the backend via /api/system/info once that -// endpoint exists. For M2 we display placeholders so the Settings UI structure -// and i18n bindings can be validated without backend changes. -const DATA_DIR_PLACEHOLDER = '~/Library/Application Support/visiona-local'; -const MODELS_UPLOAD_PATH_PLACEHOLDER = '~/Library/Application Support/visiona-local/models'; -const BUNDLED_PYTHON_PLACEHOLDER = 'Bundled Python 3.11 (ready)'; +// TODO(M2+): read actual values from the backend via /api/system/info. +// 暫時用 userAgent 判斷平台顯示對應路徑。 +function getPlatformDataDir(): string { + if (typeof navigator === 'undefined') return '(unknown)'; + if (/Windows/i.test(navigator.userAgent)) return '%APPDATA%\\visiona-local'; + if (/Linux/i.test(navigator.userAgent)) return '~/.local/share/visiona-local'; + return '~/Library/Application Support/visiona-local'; +} +const DATA_DIR_PLACEHOLDER = getPlatformDataDir(); +const MODELS_UPLOAD_PATH_PLACEHOLDER = DATA_DIR_PLACEHOLDER + (DATA_DIR_PLACEHOLDER.includes('\\') ? '\\models' : '/models'); +const BUNDLED_PYTHON_PLACEHOLDER = 'Bundled Python 3.12 (ready)'; export default function SettingsPage() { const { t } = useTranslation(); diff --git a/local-tool/frontend/src/components/devices/device-card.tsx b/local-tool/frontend/src/components/devices/device-card.tsx index f423a40..23fa743 100644 --- a/local-tool/frontend/src/components/devices/device-card.tsx +++ b/local-tool/frontend/src/components/devices/device-card.tsx @@ -3,6 +3,7 @@ import Link from 'next/link'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; +import { Loader2 } from 'lucide-react'; import { DeviceStatusBadge } from './device-status'; import { useDeviceStore } from '@/stores/device-store'; import { useDevicePreferencesStore } from '@/stores/device-preferences-store'; @@ -16,22 +17,25 @@ interface DeviceCardProps { export function DeviceCard({ device, isFirstCard }: DeviceCardProps) { const { t } = useTranslation(); - const { connectDevice, disconnectDevice } = useDeviceStore(); + const { connectDevice, disconnectDevice, connectingId, disconnectingId } = useDeviceStore(); const prefs = useDevicePreferencesStore((s) => s.getPreferences(device.id)); const displayName = prefs.alias || device.name; const isConnected = device.status === 'connected' || device.status === 'flashing' || device.status === 'inferencing'; + const isConnecting = connectingId === device.id; + const isDisconnecting = disconnectingId === device.id; + const isBusy = isConnecting || isDisconnecting || connectingId !== null || disconnectingId !== null; return ( -
-
- {displayName} +
+
+ {displayName} {prefs.alias && (

{device.name}

)}
- +
@@ -55,25 +59,29 @@ export function DeviceCard({ device, isFirstCard }: DeviceCardProps) { {isConnected ? ( <> - ) : ( )}
diff --git a/local-tool/frontend/src/lib/i18n/en.ts b/local-tool/frontend/src/lib/i18n/en.ts index 0932601..8ca879a 100644 --- a/local-tool/frontend/src/lib/i18n/en.ts +++ b/local-tool/frontend/src/lib/i18n/en.ts @@ -67,6 +67,8 @@ export const en: TranslationDict = { subtitle: 'Manage your edge AI devices', scan: 'Scan Devices', scanning: 'Scanning...', + connecting: 'Connecting...', + disconnecting: 'Disconnecting...', noDevices: 'No devices detected. Make sure mock mode is enabled or connect a device.', type: 'Type', firmware: 'Firmware', diff --git a/local-tool/frontend/src/lib/i18n/zh-TW.ts b/local-tool/frontend/src/lib/i18n/zh-TW.ts index cf19659..f76d596 100644 --- a/local-tool/frontend/src/lib/i18n/zh-TW.ts +++ b/local-tool/frontend/src/lib/i18n/zh-TW.ts @@ -67,6 +67,8 @@ export const zhTW: TranslationDict = { subtitle: '管理你的 Edge AI 裝置', scan: '掃描裝置', scanning: '掃描中...', + connecting: '連線中...', + disconnecting: '斷線中...', noDevices: '未偵測到裝置。請確認已啟用 Mock 模式或連接裝置。', type: '類型', firmware: '韌體', diff --git a/local-tool/frontend/src/stores/device-store.ts b/local-tool/frontend/src/stores/device-store.ts index c931664..4133419 100644 --- a/local-tool/frontend/src/stores/device-store.ts +++ b/local-tool/frontend/src/stores/device-store.ts @@ -10,6 +10,8 @@ interface DeviceState { selectedDevice: Device | null; loading: boolean; scanning: boolean; + connectingId: string | null; + disconnectingId: string | null; fetchDevices: () => Promise; scanDevices: () => Promise; fetchDevice: (id: string) => Promise; @@ -23,6 +25,8 @@ export const useDeviceStore = create((set, get) => ({ selectedDevice: null, loading: false, scanning: false, + connectingId: null, + disconnectingId: null, scanDevices: async () => { set({ scanning: true }); @@ -66,39 +70,47 @@ export const useDeviceStore = create((set, get) => ({ }, connectDevice: async (id) => { - const res = await api.post(`/devices/${id}/connect`); - if (res.success) { - showSuccess(getTranslation().t('errors.deviceConnected')); - const name = get().devices.find((d) => d.id === id)?.name || id; - useActivityStore.getState().addActivity('device_connect', `Device connected: ${name}`); - } else { - const msg = res.error?.message || ''; - // 偵測 Kneron WinUSB driver 未安裝的特徵錯誤訊息(error 28 / KP_ERROR_CONNECT_FAILED) - // 若命中,給使用者明確指引去點「安裝 USB Driver」按鈕,而不只是顯示原始錯誤 - if (/winusb|error.{0,5}code.{0,5}28|KP_ERROR_CONNECT_FAILED/i.test(msg)) { - showApiError({ - code: 'DRIVER_NOT_INSTALLED', - message: - '連線失敗:Kneron WinUSB driver 尚未安裝。\n' + - '請點擊右上角「安裝 USB Driver」按鈕(需要系統管理員權限),安裝完成後重新點選連線。', - }); + set({ connectingId: id }); + try { + const res = await api.post(`/devices/${id}/connect`); + if (res.success) { + showSuccess(getTranslation().t('errors.deviceConnected')); + const name = get().devices.find((d) => d.id === id)?.name || id; + useActivityStore.getState().addActivity('device_connect', `Device connected: ${name}`); } else { - showApiError(res.error); + const msg = res.error?.message || ''; + if (/winusb|error.{0,5}code.{0,5}28|KP_ERROR_CONNECT_FAILED/i.test(msg)) { + showApiError({ + code: 'DRIVER_NOT_INSTALLED', + message: + '連線失敗:Kneron WinUSB driver 尚未安裝。\n' + + '請點擊右上角「安裝 USB Driver」按鈕(需要系統管理員權限),安裝完成後重新點選連線。', + }); + } else { + showApiError(res.error); + } } + } finally { + set({ connectingId: null }); + get().fetchDevices(); } - get().fetchDevices(); }, disconnectDevice: async (id) => { - const res = await api.post(`/devices/${id}/disconnect`); - if (res.success) { - showSuccess(getTranslation().t('errors.deviceDisconnected')); - const name = get().devices.find((d) => d.id === id)?.name || id; - useActivityStore.getState().addActivity('device_disconnect', `Device disconnected: ${name}`); - } else { - showApiError(res.error); + set({ disconnectingId: id }); + try { + const res = await api.post(`/devices/${id}/disconnect`); + if (res.success) { + showSuccess(getTranslation().t('errors.deviceDisconnected')); + const name = get().devices.find((d) => d.id === id)?.name || id; + useActivityStore.getState().addActivity('device_disconnect', `Device disconnected: ${name}`); + } else { + showApiError(res.error); + } + } finally { + set({ disconnectingId: null }); + get().fetchDevices(); } - get().fetchDevices(); }, handleEvent: (event) => {