detection 畫框、classification 改在影像右上角疊標籤(不畫框), 並在推論面板提供即時切換解析方式與上傳 label 檔。 - 新增 InferenceOverlay 依 taskType 分派;ClassificationOverlay 用 DOM 而非 canvas,讓 CJK 排版與 aria-live 交給瀏覽器處理 - 標籤防閃爍:挑戰者需連續 3 幀居冠才切換,或信心度領先 15% 直接切; 低於門檻立即清空(顯示過期標籤比空白更糟) - 身分比對優先用 classIndex,避免換 label 檔時被誤判為換類別 - 推論設定卡片:即時切 detection/classification、上傳 .txt label、 清除 label,皆不需重燒 - classification-result 原本無條件渲染,導致 detection 模式下側欄 永遠顯示「分類結果」與空圖表,改為依 taskType 切換 - 清掉 camera-overlay 每幀執行的 debug console.log - 補 ResizeObserver stub,原本任何渲染 InferencePanel 的測試都會 在 jsdom 直接拋錯 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
103 lines
3.6 KiB
TypeScript
103 lines
3.6 KiB
TypeScript
'use client';
|
|
|
|
import { useEffect } from 'react';
|
|
import Link from 'next/link';
|
|
import { Button } from '@/components/ui/button';
|
|
import { CameraInferenceView } from '@/components/camera/camera-inference-view';
|
|
import { InferencePanel } from '@/components/inference/inference-panel';
|
|
import { FlashDialog } from '@/components/devices/flash-dialog';
|
|
import { useDeviceStore } from '@/stores/device-store';
|
|
import { useInferenceStore } from '@/stores/inference-store';
|
|
import { useInferenceOptionsStore } from '@/stores/inference-options-store';
|
|
import { useInferenceStream } from '@/hooks/use-inference-stream';
|
|
import { useCameraStore } from '@/stores/camera-store';
|
|
import { useResolvedParams } from '@/hooks/use-resolved-params';
|
|
import { api } from '@/lib/api';
|
|
|
|
export default function WorkspaceClient() {
|
|
const { deviceId } = useResolvedParams();
|
|
const { selectedDevice, fetchDevice } = useDeviceStore();
|
|
const { isRunning, setRunning, reset } = useInferenceStore();
|
|
const resetInferenceOptions = useInferenceOptionsStore((s) => s.reset);
|
|
const { isStreaming, sourceType } = useCameraStore();
|
|
|
|
// For image/video mode, inference runs automatically as part of the pipeline
|
|
const isMediaMode = sourceType === 'image' || sourceType === 'video' || sourceType === 'batch_image';
|
|
|
|
// Enable WebSocket stream when inference is running OR when media pipeline is active
|
|
useInferenceStream(deviceId, isRunning || (isStreaming && isMediaMode));
|
|
|
|
// Auto-set isRunning when media upload starts streaming
|
|
useEffect(() => {
|
|
if (isStreaming && isMediaMode) {
|
|
setRunning(true);
|
|
}
|
|
}, [isStreaming, isMediaMode, setRunning]);
|
|
|
|
const { fetchCameras } = useCameraStore();
|
|
|
|
useEffect(() => {
|
|
if (deviceId) {
|
|
fetchDevice(deviceId);
|
|
fetchCameras();
|
|
}
|
|
return () => {
|
|
reset();
|
|
// Options are per-session and per-device; leaving them set would apply a
|
|
// label mapping uploaded for one device to the next one opened.
|
|
resetInferenceOptions();
|
|
};
|
|
}, [deviceId, fetchDevice, fetchCameras, reset, resetInferenceOptions]);
|
|
|
|
const handleStartInference = async () => {
|
|
await api.post(`/devices/${deviceId}/inference/start`);
|
|
setRunning(true);
|
|
};
|
|
|
|
const handleStopInference = async () => {
|
|
await api.post(`/devices/${deviceId}/inference/stop`);
|
|
setRunning(false);
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-2">
|
|
<Link href={`/devices/${deviceId}`}>
|
|
<Button variant="ghost" size="sm">{'← 返回'}</Button>
|
|
</Link>
|
|
<h1 className="text-xl font-bold">
|
|
{'工作區:'} {selectedDevice?.name || deviceId}
|
|
</h1>
|
|
</div>
|
|
<div className="flex gap-2">
|
|
<FlashDialog deviceId={deviceId} />
|
|
{/* Only show manual inference controls in camera mode */}
|
|
{!isMediaMode && (
|
|
<>
|
|
{isRunning ? (
|
|
<Button variant="destructive" onClick={handleStopInference}>
|
|
停止推論
|
|
</Button>
|
|
) : (
|
|
<Button onClick={handleStartInference} disabled={!isStreaming} data-tour-id="start-inference-btn">
|
|
開始推論
|
|
</Button>
|
|
)}
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex gap-4 min-h-0">
|
|
<div className="flex-1 min-w-0">
|
|
<CameraInferenceView deviceId={deviceId} />
|
|
</div>
|
|
<div className="w-80 shrink-0">
|
|
<InferencePanel deviceId={deviceId} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|