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 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>
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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],
|
||||
);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user