feat: change interface of analysis video log
This commit is contained in:
@@ -14,10 +14,13 @@ function StatSkeleton() {
|
|||||||
|
|
||||||
function DetectionLogSkeleton() {
|
function DetectionLogSkeleton() {
|
||||||
return (
|
return (
|
||||||
<div className="rounded-md border bg-card p-4">
|
<div className="rounded-md border bg-card p-3">
|
||||||
<div className="mb-3 flex items-start justify-between gap-4">
|
<div className="flex gap-3">
|
||||||
|
<Skeleton className="size-20 shrink-0 rounded-md" />
|
||||||
|
<div className="min-w-0 flex-1 space-y-2">
|
||||||
|
<div className="flex items-start justify-between gap-3">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Skeleton className="h-4 w-36" />
|
<Skeleton className="h-5 w-28" />
|
||||||
<Skeleton className="h-3 w-20" />
|
<Skeleton className="h-3 w-20" />
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
@@ -25,11 +28,11 @@ function DetectionLogSkeleton() {
|
|||||||
<Skeleton className="h-3 w-10" />
|
<Skeleton className="h-3 w-10" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
|
||||||
<Skeleton className="h-3 w-32" />
|
<Skeleton className="h-3 w-32" />
|
||||||
<Skeleton className="h-3 w-44" />
|
<Skeleton className="h-3 w-44" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useRef } from 'react';
|
import { useEffect, useRef } from 'react';
|
||||||
import { Activity, Film, MapPin } from 'lucide-react';
|
import { Activity, Film, ImageIcon, MapPin } from 'lucide-react';
|
||||||
|
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||||
import { DetectionResultLog } from '@/types';
|
import { Skeleton } from '@/components/ui/skeleton';
|
||||||
|
import { useProtectedMediaObjectUrl } from '@/hooks/useProtectedMedia';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
import type { DetectionResultLog } from '@/types';
|
||||||
|
|
||||||
interface DetectionLogsProps {
|
interface DetectionLogsProps {
|
||||||
logs: DetectionResultLog[];
|
logs: DetectionResultLog[];
|
||||||
@@ -19,6 +22,47 @@ const formatVideoTime = (seconds: number) => {
|
|||||||
return `${minutes}:${secs.toString().padStart(2, '0')}`;
|
return `${minutes}:${secs.toString().padStart(2, '0')}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function DetectionLogThumbnail({
|
||||||
|
url,
|
||||||
|
label,
|
||||||
|
}: {
|
||||||
|
url?: string | null;
|
||||||
|
label: string;
|
||||||
|
}) {
|
||||||
|
const { objectUrl, isLoading, isError } = useProtectedMediaObjectUrl(url);
|
||||||
|
|
||||||
|
if (!url) {
|
||||||
|
return (
|
||||||
|
<div className="flex size-20 shrink-0 items-center justify-center rounded-md border border-dashed border-border/70 bg-muted/20 text-muted-foreground/60">
|
||||||
|
<ImageIcon className="size-5" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <Skeleton className="size-20 shrink-0 rounded-md" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isError || !objectUrl) {
|
||||||
|
return (
|
||||||
|
<div className="flex size-20 shrink-0 items-center justify-center rounded-md border border-dashed bg-muted/20 text-[10px] text-muted-foreground">
|
||||||
|
N/A
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="size-20 shrink-0 overflow-hidden rounded-md border bg-muted/20">
|
||||||
|
<img
|
||||||
|
src={objectUrl}
|
||||||
|
alt={label}
|
||||||
|
className="size-full object-cover"
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const DetectionLogs = ({ logs, activeLogId, onSeek }: DetectionLogsProps) => {
|
const DetectionLogs = ({ logs, activeLogId, onSeek }: DetectionLogsProps) => {
|
||||||
const activeLogRef = useRef<HTMLButtonElement | null>(null);
|
const activeLogRef = useRef<HTMLButtonElement | null>(null);
|
||||||
|
|
||||||
@@ -49,6 +93,7 @@ const DetectionLogs = ({ logs, activeLogId, onSeek }: DetectionLogsProps) => {
|
|||||||
const timestampSeconds = log.frame.timestamp_seconds;
|
const timestampSeconds = log.frame.timestamp_seconds;
|
||||||
const { latitude, longitude } = log.location;
|
const { latitude, longitude } = log.location;
|
||||||
const isActive = activeLogId === log.id;
|
const isActive = activeLogId === log.id;
|
||||||
|
const label = log.detection.display_name;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
@@ -57,19 +102,27 @@ const DetectionLogs = ({ logs, activeLogId, onSeek }: DetectionLogsProps) => {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => onSeek(log)}
|
onClick={() => onSeek(log)}
|
||||||
className={cn(
|
className={cn(
|
||||||
'w-full rounded-md border bg-card p-4 text-left transition-colors hover:border-primary',
|
'w-full rounded-md border bg-card p-3 text-left transition-colors hover:border-primary',
|
||||||
isActive && 'border-primary bg-primary/5',
|
isActive && 'border-primary bg-primary/5',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="mb-3 flex items-start justify-between gap-4">
|
<div className="flex gap-3">
|
||||||
<div>
|
<DetectionLogThumbnail
|
||||||
<div className="text-[13px] font-bold">
|
url={log.detection.context_image_url}
|
||||||
{log.detection.display_name} ID: {log.id}
|
label={label}
|
||||||
</div>
|
/>
|
||||||
<div className="text-[11px] font-medium text-muted-foreground">
|
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<div className="mb-2 flex items-start justify-between gap-3">
|
||||||
|
<div className="min-w-0 space-y-1">
|
||||||
|
<p className="text-sm font-semibold text-foreground">
|
||||||
|
{label}
|
||||||
|
</p>
|
||||||
|
<p className="text-[11px] text-muted-foreground">
|
||||||
Frame {log.frame.number}
|
Frame {log.frame.number}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div className="shrink-0 text-right">
|
<div className="shrink-0 text-right">
|
||||||
<div className="text-xs font-bold">
|
<div className="text-xs font-bold">
|
||||||
{formatVideoTime(timestampSeconds)}
|
{formatVideoTime(timestampSeconds)}
|
||||||
@@ -80,20 +133,20 @@ const DetectionLogs = ({ logs, activeLogId, onSeek }: DetectionLogsProps) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2 text-[12px] font-medium text-muted-foreground/90">
|
<div className="space-y-1.5 text-[12px] text-muted-foreground">
|
||||||
<div>
|
<p>{(log.detection.confidence * 100).toFixed(1)}%</p>
|
||||||
Confidence: {(log.detection.confidence * 100).toFixed(1)}%
|
|
||||||
</div>
|
|
||||||
{typeof latitude === 'number' &&
|
{typeof latitude === 'number' &&
|
||||||
typeof longitude === 'number' && (
|
typeof longitude === 'number' && (
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
<MapPin className="h-3 w-3" />
|
<MapPin className="h-3 w-3 shrink-0" />
|
||||||
<span>
|
<span className="truncate font-mono text-[11px]">
|
||||||
{latitude.toFixed(8)}, {longitude.toFixed(8)}
|
{latitude.toFixed(8)}, {longitude.toFixed(8)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -45,7 +45,9 @@ export const videoService = {
|
|||||||
* Get analysis results for a video
|
* Get analysis results for a video
|
||||||
*/
|
*/
|
||||||
getVideoResults: async (videoId: string): Promise<CompletedVideoResult> => {
|
getVideoResults: async (videoId: string): Promise<CompletedVideoResult> => {
|
||||||
const response = await axiosClient.get(API_ROUTES.VIDEOS.RESULTS(videoId));
|
const response = await axiosClient.get<CompletedVideoResult>(
|
||||||
|
API_ROUTES.VIDEOS.RESULTS(videoId),
|
||||||
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -2,12 +2,12 @@ import { DetectionClassCount, DetectionResultLog } from './detection';
|
|||||||
|
|
||||||
export type CompletedVideoResult = {
|
export type CompletedVideoResult = {
|
||||||
video_id: string;
|
video_id: string;
|
||||||
status: 'completed' | string;
|
status: 'completed';
|
||||||
processed_video_url: string | null;
|
raw_video_url: string;
|
||||||
raw_video_url: string | null;
|
processed_video_url: string;
|
||||||
summary: {
|
summary: {
|
||||||
fps?: number;
|
fps: number;
|
||||||
duration_seconds?: number;
|
duration_seconds: number;
|
||||||
total_detections: number;
|
total_detections: number;
|
||||||
};
|
};
|
||||||
defect_counts: DetectionClassCount[];
|
defect_counts: DetectionClassCount[];
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export type DetectionResultLog = {
|
|||||||
class_name: string;
|
class_name: string;
|
||||||
display_name: string;
|
display_name: string;
|
||||||
confidence: number;
|
confidence: number;
|
||||||
|
context_image_url: string;
|
||||||
};
|
};
|
||||||
frame: {
|
frame: {
|
||||||
number: number;
|
number: number;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
export * from './routes';
|
export * from './routes';
|
||||||
export * from './date';
|
export * from './date';
|
||||||
export * from './person';
|
export * from './person';
|
||||||
|
export * from './media';
|
||||||
13
src/utils/media.ts
Normal file
13
src/utils/media.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { ENV_CONSTANT } from '@/constants/secrect.constant';
|
||||||
|
|
||||||
|
export function resolveMediaUrl(url?: string | null) {
|
||||||
|
if (!url?.trim()) return null;
|
||||||
|
|
||||||
|
const value = url.trim();
|
||||||
|
if (/^https?:\/\//i.test(value)) return value;
|
||||||
|
|
||||||
|
const baseUrl = ENV_CONSTANT.BASE_API_URL?.replace(/\/$/, '');
|
||||||
|
const path = value.startsWith('/') ? value : `/${value}`;
|
||||||
|
|
||||||
|
return baseUrl ? `${baseUrl}${path}` : path;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user