|
|
|
@@ -1,5 +1,6 @@
|
|
|
|
import { useAuth } from "@/_core/hooks/useAuth";
|
|
|
|
import { useAuth } from "@/_core/hooks/useAuth";
|
|
|
|
import { trpc } from "@/lib/trpc";
|
|
|
|
import { trpc } from "@/lib/trpc";
|
|
|
|
|
|
|
|
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
|
|
|
import { Badge } from "@/components/ui/badge";
|
|
|
|
import { Badge } from "@/components/ui/badge";
|
|
|
|
import { Button } from "@/components/ui/button";
|
|
|
|
import { Button } from "@/components/ui/button";
|
|
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
|
|
@@ -572,6 +573,7 @@ export default function LiveCamera() {
|
|
|
|
const [sessionMode, setSessionMode] = useState<SessionMode>("practice");
|
|
|
|
const [sessionMode, setSessionMode] = useState<SessionMode>("practice");
|
|
|
|
const [analyzing, setAnalyzing] = useState(false);
|
|
|
|
const [analyzing, setAnalyzing] = useState(false);
|
|
|
|
const [saving, setSaving] = useState(false);
|
|
|
|
const [saving, setSaving] = useState(false);
|
|
|
|
|
|
|
|
const [leaveStatus, setLeaveStatus] = useState<"idle" | "analyzing" | "saving" | "safe" | "failed">("idle");
|
|
|
|
const [immersivePreview, setImmersivePreview] = useState(false);
|
|
|
|
const [immersivePreview, setImmersivePreview] = useState(false);
|
|
|
|
const [liveScore, setLiveScore] = useState<PoseScore | null>(null);
|
|
|
|
const [liveScore, setLiveScore] = useState<PoseScore | null>(null);
|
|
|
|
const [currentAction, setCurrentAction] = useState<ActionType>("unknown");
|
|
|
|
const [currentAction, setCurrentAction] = useState<ActionType>("unknown");
|
|
|
|
@@ -979,6 +981,7 @@ export default function LiveCamera() {
|
|
|
|
analyzingRef.current = true;
|
|
|
|
analyzingRef.current = true;
|
|
|
|
setAnalyzing(true);
|
|
|
|
setAnalyzing(true);
|
|
|
|
setSaving(false);
|
|
|
|
setSaving(false);
|
|
|
|
|
|
|
|
setLeaveStatus("analyzing");
|
|
|
|
setSegments([]);
|
|
|
|
setSegments([]);
|
|
|
|
segmentsRef.current = [];
|
|
|
|
segmentsRef.current = [];
|
|
|
|
currentSegmentRef.current = null;
|
|
|
|
currentSegmentRef.current = null;
|
|
|
|
@@ -1049,6 +1052,7 @@ export default function LiveCamera() {
|
|
|
|
} catch (error: any) {
|
|
|
|
} catch (error: any) {
|
|
|
|
analyzingRef.current = false;
|
|
|
|
analyzingRef.current = false;
|
|
|
|
setAnalyzing(false);
|
|
|
|
setAnalyzing(false);
|
|
|
|
|
|
|
|
setLeaveStatus("idle");
|
|
|
|
await stopSessionRecorder();
|
|
|
|
await stopSessionRecorder();
|
|
|
|
toast.error(`实时分析启动失败: ${error?.message || "未知错误"}`);
|
|
|
|
toast.error(`实时分析启动失败: ${error?.message || "未知错误"}`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@@ -1059,6 +1063,7 @@ export default function LiveCamera() {
|
|
|
|
analyzingRef.current = false;
|
|
|
|
analyzingRef.current = false;
|
|
|
|
setAnalyzing(false);
|
|
|
|
setAnalyzing(false);
|
|
|
|
setSaving(true);
|
|
|
|
setSaving(true);
|
|
|
|
|
|
|
|
setLeaveStatus("saving");
|
|
|
|
|
|
|
|
|
|
|
|
if (animationRef.current) {
|
|
|
|
if (animationRef.current) {
|
|
|
|
cancelAnimationFrame(animationRef.current);
|
|
|
|
cancelAnimationFrame(animationRef.current);
|
|
|
|
@@ -1071,15 +1076,32 @@ export default function LiveCamera() {
|
|
|
|
poseRef.current = null;
|
|
|
|
poseRef.current = null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
await persistSession();
|
|
|
|
await persistSession();
|
|
|
|
|
|
|
|
setLeaveStatus("safe");
|
|
|
|
toast.success("实时分析已保存,并同步写入训练记录");
|
|
|
|
toast.success("实时分析已保存,并同步写入训练记录");
|
|
|
|
await liveSessionsQuery.refetch();
|
|
|
|
await liveSessionsQuery.refetch();
|
|
|
|
} catch (error: any) {
|
|
|
|
} catch (error: any) {
|
|
|
|
|
|
|
|
setLeaveStatus("failed");
|
|
|
|
toast.error(`保存实时分析失败: ${error?.message || "未知错误"}`);
|
|
|
|
toast.error(`保存实时分析失败: ${error?.message || "未知错误"}`);
|
|
|
|
} finally {
|
|
|
|
} finally {
|
|
|
|
setSaving(false);
|
|
|
|
setSaving(false);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}, [liveSessionsQuery, persistSession]);
|
|
|
|
}, [liveSessionsQuery, persistSession]);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
|
|
if (!analyzing && !saving) {
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const handleBeforeUnload = (event: BeforeUnloadEvent) => {
|
|
|
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
|
|
|
event.returnValue = "实时分析数据仍在处理中,请先等待保存完成。";
|
|
|
|
|
|
|
|
return event.returnValue;
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
window.addEventListener("beforeunload", handleBeforeUnload);
|
|
|
|
|
|
|
|
return () => window.removeEventListener("beforeunload", handleBeforeUnload);
|
|
|
|
|
|
|
|
}, [analyzing, saving]);
|
|
|
|
|
|
|
|
|
|
|
|
const handleSetupComplete = useCallback(async () => {
|
|
|
|
const handleSetupComplete = useCallback(async () => {
|
|
|
|
setShowSetupGuide(false);
|
|
|
|
setShowSetupGuide(false);
|
|
|
|
await startCamera(facing, zoomTargetRef.current, qualityPreset);
|
|
|
|
await startCamera(facing, zoomTargetRef.current, qualityPreset);
|
|
|
|
@@ -1217,6 +1239,46 @@ export default function LiveCamera() {
|
|
|
|
</DialogContent>
|
|
|
|
</DialogContent>
|
|
|
|
</Dialog>
|
|
|
|
</Dialog>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
{leaveStatus === "analyzing" ? (
|
|
|
|
|
|
|
|
<Alert>
|
|
|
|
|
|
|
|
<Activity className="h-4 w-4" />
|
|
|
|
|
|
|
|
<AlertTitle>分析进行中</AlertTitle>
|
|
|
|
|
|
|
|
<AlertDescription>
|
|
|
|
|
|
|
|
当前仍在采集和识别动作数据,请先不要关闭浏览器或切走页面。
|
|
|
|
|
|
|
|
</AlertDescription>
|
|
|
|
|
|
|
|
</Alert>
|
|
|
|
|
|
|
|
) : null}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
{leaveStatus === "saving" ? (
|
|
|
|
|
|
|
|
<Alert>
|
|
|
|
|
|
|
|
<Activity className="h-4 w-4" />
|
|
|
|
|
|
|
|
<AlertTitle>正在保存分析结果</AlertTitle>
|
|
|
|
|
|
|
|
<AlertDescription>
|
|
|
|
|
|
|
|
视频、动作区间和训练记录正在提交,请暂时停留当前页面;保存完成后会提示你可以离开。
|
|
|
|
|
|
|
|
</AlertDescription>
|
|
|
|
|
|
|
|
</Alert>
|
|
|
|
|
|
|
|
) : null}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
{leaveStatus === "safe" ? (
|
|
|
|
|
|
|
|
<Alert>
|
|
|
|
|
|
|
|
<CheckCircle2 className="h-4 w-4" />
|
|
|
|
|
|
|
|
<AlertTitle>分析结果已保存</AlertTitle>
|
|
|
|
|
|
|
|
<AlertDescription>
|
|
|
|
|
|
|
|
当前分析数据已经提交完成。现在可以关闭浏览器、返回上一页,或切换到其他页面,不会影响已保存的数据。
|
|
|
|
|
|
|
|
</AlertDescription>
|
|
|
|
|
|
|
|
</Alert>
|
|
|
|
|
|
|
|
) : null}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
{leaveStatus === "failed" ? (
|
|
|
|
|
|
|
|
<Alert>
|
|
|
|
|
|
|
|
<Activity className="h-4 w-4" />
|
|
|
|
|
|
|
|
<AlertTitle>分析保存失败</AlertTitle>
|
|
|
|
|
|
|
|
<AlertDescription>
|
|
|
|
|
|
|
|
当前会话还没有完整写入,请先留在本页并重新尝试结束分析或检查网络状态。
|
|
|
|
|
|
|
|
</AlertDescription>
|
|
|
|
|
|
|
|
</Alert>
|
|
|
|
|
|
|
|
) : null}
|
|
|
|
|
|
|
|
|
|
|
|
<section className="rounded-[28px] border border-border/60 bg-[radial-gradient(circle_at_top_left,_rgba(249,115,22,0.16),_transparent_32%),linear-gradient(135deg,rgba(12,18,24,0.98),rgba(26,31,43,0.96))] p-5 text-white shadow-xl shadow-black/10 md:p-7">
|
|
|
|
<section className="rounded-[28px] border border-border/60 bg-[radial-gradient(circle_at_top_left,_rgba(249,115,22,0.16),_transparent_32%),linear-gradient(135deg,rgba(12,18,24,0.98),rgba(26,31,43,0.96))] p-5 text-white shadow-xl shadow-black/10 md:p-7">
|
|
|
|
<div className="flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between">
|
|
|
|
<div className="flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between">
|
|
|
|
<div className="space-y-3">
|
|
|
|
<div className="space-y-3">
|
|
|
|
|