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:
parent
3c6971febd
commit
c24a04cdb2
@ -39,6 +39,7 @@ export function FlashDialog({ deviceId }: FlashDialogProps) {
|
|||||||
const device = devices.find((d) => d.id === deviceId);
|
const device = devices.find((d) => d.id === deviceId);
|
||||||
const selectedModel = models.find((m) => m.id === selectedModelId);
|
const selectedModel = models.find((m) => m.id === selectedModelId);
|
||||||
|
|
||||||
|
// S2: 資料載入前預設 compatible=true,避免在 model/device 還沒載入時就顯示不相容警告
|
||||||
const compatible = useMemo(() => {
|
const compatible = useMemo(() => {
|
||||||
if (!selectedModel || !device) return true;
|
if (!selectedModel || !device) return true;
|
||||||
return isModelCompatible(selectedModel.supportedHardware, device.type);
|
return isModelCompatible(selectedModel.supportedHardware, device.type);
|
||||||
@ -56,30 +57,38 @@ export function FlashDialog({ deviceId }: FlashDialogProps) {
|
|||||||
|
|
||||||
const handleFlash = async () => {
|
const handleFlash = async () => {
|
||||||
if (!selectedModelId) return;
|
if (!selectedModelId) return;
|
||||||
// 1. Create WebSocket and wait for it to open
|
try {
|
||||||
await connectAndWait();
|
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);
|
await startFlash(deviceId, selectedModelId);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// m2 fix: 進度尚未完成時(不管 isFlashing 狀態如何)都阻止關閉
|
||||||
|
const isInProgress = (isFlashing || (progress !== null && progress.percent < 100)) && !error;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={(v) => {
|
<Dialog open={open} onOpenChange={(v) => {
|
||||||
if (!v && isFlashing && !error) return;
|
if (!v && isInProgress) return;
|
||||||
setOpen(v);
|
setOpen(v);
|
||||||
}}>
|
}}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button data-tour-id="flash-model-btn">Flash Model</Button>
|
<Button data-tour-id="flash-model-btn">載入模型</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Flash Model to Device</DialogTitle>
|
<DialogTitle>載入模型到裝置</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{!isFlashing && !progress && !error ? (
|
{!isFlashing && !progress && !error ? (
|
||||||
<>
|
<>
|
||||||
<Select value={selectedModelId} onValueChange={setSelectedModelId}>
|
<Select value={selectedModelId} onValueChange={setSelectedModelId}>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder="Select a model" />
|
<SelectValue placeholder="選擇模型" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{models.map((m) => (
|
{models.map((m) => (
|
||||||
@ -95,9 +104,9 @@ export function FlashDialog({ deviceId }: FlashDialogProps) {
|
|||||||
<div className="flex gap-2 items-start">
|
<div className="flex gap-2 items-start">
|
||||||
<TriangleAlertIcon className="h-5 w-5 text-yellow-600 mt-0.5 shrink-0" />
|
<TriangleAlertIcon className="h-5 w-5 text-yellow-600 mt-0.5 shrink-0" />
|
||||||
<div className="text-sm">
|
<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">
|
<p className="text-yellow-700">
|
||||||
This model may not be compatible with {device ? getHardwareType(device.type) : 'this device'}.
|
此模型可能與 {device ? getHardwareType(device.type) : '此裝置'} 不相容。
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -110,10 +119,10 @@ export function FlashDialog({ deviceId }: FlashDialogProps) {
|
|||||||
className="w-full"
|
className="w-full"
|
||||||
>
|
>
|
||||||
{!selectedModelId
|
{!selectedModelId
|
||||||
? 'Select a model'
|
? '請先選擇模型'
|
||||||
: !compatible
|
: !compatible
|
||||||
? 'Incompatible — Cannot Flash'
|
? '不相容 — 無法載入'
|
||||||
: 'Start Flash'}
|
: '開始載入'}
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
@ -130,7 +139,7 @@ export function FlashDialog({ deviceId }: FlashDialogProps) {
|
|||||||
setOpen(false);
|
setOpen(false);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{error ? 'Close' : 'Done'}
|
{error ? '關閉' : '完成'}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -19,14 +19,14 @@ export function FlashProgress({ progress, error, onRetry }: FlashProgressProps)
|
|||||||
<div className="flex gap-2 items-start">
|
<div className="flex gap-2 items-start">
|
||||||
<XCircle className="h-5 w-5 text-red-600 mt-0.5 shrink-0" />
|
<XCircle className="h-5 w-5 text-red-600 mt-0.5 shrink-0" />
|
||||||
<div className="flex-1">
|
<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>
|
<p className="text-sm text-red-700 mt-1">{error}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{onRetry && (
|
{onRetry && (
|
||||||
<Button onClick={onRetry} className="w-full" variant="outline">
|
<Button onClick={onRetry} className="w-full" variant="outline">
|
||||||
Retry
|
重試
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -37,7 +37,7 @@ export function FlashProgress({ progress, error, onRetry }: FlashProgressProps)
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-2 text-center">
|
<div className="space-y-2 text-center">
|
||||||
<div className="animate-pulse text-sm text-muted-foreground">
|
<div className="animate-pulse text-sm text-muted-foreground">
|
||||||
Preparing flash...
|
正在準備載入...
|
||||||
</div>
|
</div>
|
||||||
<Progress value={0} />
|
<Progress value={0} />
|
||||||
</div>
|
</div>
|
||||||
@ -53,7 +53,7 @@ export function FlashProgress({ progress, error, onRetry }: FlashProgressProps)
|
|||||||
<Progress value={progress.percent} />
|
<Progress value={progress.percent} />
|
||||||
<p className="text-sm text-muted-foreground">{progress.message}</p>
|
<p className="text-sm text-muted-foreground">{progress.message}</p>
|
||||||
{progress.percent >= 100 && (
|
{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>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -29,16 +29,15 @@ export function useFlashProgress(deviceId: string) {
|
|||||||
*/
|
*/
|
||||||
const connectAndWait = useCallback(
|
const connectAndWait = useCallback(
|
||||||
() =>
|
() =>
|
||||||
new Promise<void>((resolve) => {
|
new Promise<void>((resolve, reject) => {
|
||||||
// Close any existing connection
|
|
||||||
wsRef.current?.close();
|
wsRef.current?.close();
|
||||||
|
|
||||||
let resolved = false;
|
let settled = false;
|
||||||
const doResolve = () => {
|
const doResolve = () => {
|
||||||
if (!resolved) {
|
if (!settled) { settled = true; resolve(); }
|
||||||
resolved = true;
|
};
|
||||||
resolve();
|
const doReject = (reason: string) => {
|
||||||
}
|
if (!settled) { settled = true; reject(new Error(reason)); }
|
||||||
};
|
};
|
||||||
|
|
||||||
const ws = createWebSocket(
|
const ws = createWebSocket(
|
||||||
@ -52,8 +51,8 @@ export function useFlashProgress(deviceId: string) {
|
|||||||
);
|
);
|
||||||
wsRef.current = ws;
|
wsRef.current = ws;
|
||||||
|
|
||||||
// Safety timeout — don't block forever
|
// m4 fix: timeout 時 reject 而非 resolve,讓 UI 顯示錯誤而非靜默繼續
|
||||||
setTimeout(doResolve, 3000);
|
setTimeout(() => doReject('WebSocket 連線逾時(3 秒),請確認伺服器是否正常運作'), 3000);
|
||||||
}),
|
}),
|
||||||
[deviceId, updateProgress],
|
[deviceId, updateProgress],
|
||||||
);
|
);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user