visionA/local-tool/frontend/src/hooks/use-flash-progress.ts
jim800121chen 44711753ae feat(local-tool): 推論功能完整搬入 — flash 模組 + workspace 推論介面
## 後端(Phase 1)
新增 flash 模組(從 edge-ai-platform 搬入):
- server/internal/flash/service.go:StartFlash + 模型相容性檢查 + 晶片 NEF 解析
- server/internal/flash/progress.go:Flash 進度追蹤器
- server/internal/api/ws/flash_ws.go:WebSocket 推送 flash 進度
- device_handler.go:新增 FlashDevice method + flashSvc 欄位
- router.go:新增 POST /api/devices/:id/flash + WS /ws/devices/:id/flash-progress
- main.go:初始化 flash.NewService 並傳入 router

推論/攝影機/MJPEG/inference WebSocket 之前 M1 已搬好,不需改動。
Python bridge (kneron_bridge.py) 與 edge-ai-platform 完全相同,不需改動。

## 前端 store + hooks(Phase 2)
- stores/flash-store.ts(新):Zustand store — startFlash / updateProgress / retryFlash / reset
- hooks/use-flash-progress.ts(新):WebSocket hook 接收 flash 進度

inference-store / camera-store / inference types / use-inference-stream / use-websocket
之前 M1 已搬好,不需改動。

## 前端 UI 元件(Phase 3)
- components/devices/flash-dialog.tsx(新):模型載入對話框 + 硬體相容性檢查
- components/devices/flash-progress.tsx(新):Flash 進度條 + 錯誤重試

camera-inference-view / camera-feed / camera-overlay / source-selector /
inference-panel / performance-metrics / classification-result / confidence-slider /
video-progress / batch-image-thumbnails 之前 M1 已搬好。

## 前端頁面整合(Phase 4)
- workspace/page.tsx:繁中硬編碼、顯示已載入模型名稱
- workspace/[deviceId]/workspace-client.tsx:加入 FlashDialog 按鈕 + 繁中硬編碼
- devices/[id]/device-detail-client.tsx:加入 FlashDialog + 「進入工作區」按鈕(模型已載入才顯示)
- device-card.tsx:已連線 + 模型已載入時顯示「工作區」快捷按鈕

## 使用者操作流程
裝置列表 → 連線 → 管理 → 載入模型 → 進入工作區 → 選攝影機/圖片/影片 → 開始推論 → 看 bounding box / FPS / latency
或:裝置列表 → 工作區(已有模型)→ 直接推論

## 不搬的東西
- cluster/* 全部不搬(已砍 cluster 功能)
- relay / tunnel 相關不搬

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 20:07:09 +08:00

69 lines
1.8 KiB
TypeScript

'use client';
import { useEffect, useRef, useCallback } from 'react';
import { createWebSocket } from '@/lib/ws';
import { useFlashStore } from '@/stores/flash-store';
import type { FlashProgress } from '@/types/device';
/**
* Manages flash progress WebSocket.
* Returns a `connectAndWait` callback that creates the WebSocket and
* returns a promise that resolves once the WS is open.
*/
export function useFlashProgress(deviceId: string) {
const updateProgress = useFlashStore((s) => s.updateProgress);
const wsRef = useRef<ReturnType<typeof createWebSocket> | null>(null);
// Cleanup on unmount
useEffect(() => {
return () => {
wsRef.current?.close();
wsRef.current = null;
};
}, [deviceId]);
/**
* Creates the WebSocket connection and returns a promise that resolves
* once the connection is open. This is called imperatively (not via
* useEffect) to avoid React render-cycle timing issues.
*/
const connectAndWait = useCallback(
() =>
new Promise<void>((resolve) => {
// Close any existing connection
wsRef.current?.close();
let resolved = false;
const doResolve = () => {
if (!resolved) {
resolved = true;
resolve();
}
};
const ws = createWebSocket(
`/ws/devices/${deviceId}/flash-progress`,
(data) => {
updateProgress(data as FlashProgress);
},
() => {
doResolve();
},
);
wsRef.current = ws;
// Safety timeout — don't block forever
setTimeout(doResolve, 3000);
}),
[deviceId, updateProgress],
);
/** Close the WebSocket connection. */
const disconnect = useCallback(() => {
wsRef.current?.close();
wsRef.current = null;
}, []);
return { connectAndWait, disconnect };
}