fix(local-tool): Review minor m1/m2/m4 修復

m1(i18n 混用不一致):
- flash-dialog.tsx 所有英文字串改繁中(載入模型 / 選擇模型 / 硬體不相容 / 開始載入 / 關閉 / 完成)
- flash-progress.tsx 同理(模型載入失敗 / 重試 / 正在準備載入 / 模型載入完成)

m2(Dialog 關閉防護不完整):
- 改用 isInProgress 判斷(isFlashing || progress 未到 100%)+ 沒 error
  而非只看 isFlashing,涵蓋「API 回應了但 WS 進度還在跑」的情況

m4(WS 3 秒 timeout 應 reject):
- connectAndWait timeout 改 reject + Error message
- flash-dialog handleFlash 加 try/catch 捕捉 WS 連線失敗
  → 呼叫 setError 讓 UI 顯示錯誤而非靜默卡住

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
jim800121chen 2026-04-12 20:21:18 +08:00
parent 3c6971febd
commit c24a04cdb2
3 changed files with 34 additions and 26 deletions

View File

@ -39,6 +39,7 @@ export function FlashDialog({ deviceId }: FlashDialogProps) {
const device = devices.find((d) => d.id === deviceId);
const selectedModel = models.find((m) => m.id === selectedModelId);
// S2: 資料載入前預設 compatible=true避免在 model/device 還沒載入時就顯示不相容警告
const compatible = useMemo(() => {
if (!selectedModel || !device) return true;
return isModelCompatible(selectedModel.supportedHardware, device.type);
@ -56,30 +57,38 @@ export function FlashDialog({ deviceId }: FlashDialogProps) {
const handleFlash = async () => {
if (!selectedModelId) return;
// 1. Create WebSocket and wait for it to open
try {
await connectAndWait();
// 2. Then start flash (POST) — now WS is listening
} catch (e) {
const msg = e instanceof Error ? e.message : String(e);
reset();
useFlashStore.getState().setError(msg);
return;
}
await startFlash(deviceId, selectedModelId);
};
// m2 fix: 進度尚未完成時(不管 isFlashing 狀態如何)都阻止關閉
const isInProgress = (isFlashing || (progress !== null && progress.percent < 100)) && !error;
return (
<Dialog open={open} onOpenChange={(v) => {
if (!v && isFlashing && !error) return;
if (!v && isInProgress) return;
setOpen(v);
}}>
<DialogTrigger asChild>
<Button data-tour-id="flash-model-btn">Flash Model</Button>
<Button data-tour-id="flash-model-btn"></Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Flash Model to Device</DialogTitle>
<DialogTitle></DialogTitle>
</DialogHeader>
<div className="space-y-4">
{!isFlashing && !progress && !error ? (
<>
<Select value={selectedModelId} onValueChange={setSelectedModelId}>
<SelectTrigger>
<SelectValue placeholder="Select a model" />
<SelectValue placeholder="選擇模型" />
</SelectTrigger>
<SelectContent>
{models.map((m) => (
@ -95,9 +104,9 @@ export function FlashDialog({ deviceId }: FlashDialogProps) {
<div className="flex gap-2 items-start">
<TriangleAlertIcon className="h-5 w-5 text-yellow-600 mt-0.5 shrink-0" />
<div className="text-sm">
<p className="font-medium text-yellow-800">Hardware Incompatible</p>
<p className="font-medium text-yellow-800"></p>
<p className="text-yellow-700">
This model may not be compatible with {device ? getHardwareType(device.type) : 'this device'}.
{device ? getHardwareType(device.type) : '此裝置'}
</p>
</div>
</div>
@ -110,10 +119,10 @@ export function FlashDialog({ deviceId }: FlashDialogProps) {
className="w-full"
>
{!selectedModelId
? 'Select a model'
? '請先選擇模型'
: !compatible
? 'Incompatible — Cannot Flash'
: 'Start Flash'}
? '不相容 — 無法載入'
: '開始載入'}
</Button>
</>
) : (
@ -130,7 +139,7 @@ export function FlashDialog({ deviceId }: FlashDialogProps) {
setOpen(false);
}}
>
{error ? 'Close' : 'Done'}
{error ? '關閉' : '完成'}
</Button>
)}
</div>

View File

@ -19,14 +19,14 @@ export function FlashProgress({ progress, error, onRetry }: FlashProgressProps)
<div className="flex gap-2 items-start">
<XCircle className="h-5 w-5 text-red-600 mt-0.5 shrink-0" />
<div className="flex-1">
<p className="font-medium text-red-800">Flash Failed</p>
<p className="font-medium text-red-800"></p>
<p className="text-sm text-red-700 mt-1">{error}</p>
</div>
</div>
</div>
{onRetry && (
<Button onClick={onRetry} className="w-full" variant="outline">
Retry
</Button>
)}
</div>
@ -37,7 +37,7 @@ export function FlashProgress({ progress, error, onRetry }: FlashProgressProps)
return (
<div className="space-y-2 text-center">
<div className="animate-pulse text-sm text-muted-foreground">
Preparing flash...
...
</div>
<Progress value={0} />
</div>
@ -53,7 +53,7 @@ export function FlashProgress({ progress, error, onRetry }: FlashProgressProps)
<Progress value={progress.percent} />
<p className="text-sm text-muted-foreground">{progress.message}</p>
{progress.percent >= 100 && (
<p className="text-sm font-medium text-green-600">Flash Complete!</p>
<p className="text-sm font-medium text-green-600"></p>
)}
</div>
);

View File

@ -29,16 +29,15 @@ export function useFlashProgress(deviceId: string) {
*/
const connectAndWait = useCallback(
() =>
new Promise<void>((resolve) => {
// Close any existing connection
new Promise<void>((resolve, reject) => {
wsRef.current?.close();
let resolved = false;
let settled = false;
const doResolve = () => {
if (!resolved) {
resolved = true;
resolve();
}
if (!settled) { settled = true; resolve(); }
};
const doReject = (reason: string) => {
if (!settled) { settled = true; reject(new Error(reason)); }
};
const ws = createWebSocket(
@ -52,8 +51,8 @@ export function useFlashProgress(deviceId: string) {
);
wsRef.current = ws;
// Safety timeout — don't block forever
setTimeout(doResolve, 3000);
// m4 fix: timeout 時 reject 而非 resolve讓 UI 顯示錯誤而非靜默繼續
setTimeout(() => doReject('WebSocket 連線逾時3 秒),請確認伺服器是否正常運作'), 3000);
}),
[deviceId, updateProgress],
);