2 Commits

19 changed files with 214 additions and 349 deletions

View File

@@ -1,27 +1,13 @@
'use client';
import { useMemo } from 'react';
import { useParams, useRouter } from 'next/navigation';
import { ArrowLeft, TrendingUp } from 'lucide-react';
import VideoPlayerSection from '@/components/videoPlayerSection';
import { PageHeader } from '@/components/page-header';
import { Button } from '@/components/ui/button';
import { CompletedVideoResult, DetectionType } from '@/types';
import { getDetectionModeConfig } from '@/constants/detectionModeConfig';
import { useVideoResultsQuery } from '../hooks/useVideoResults';
import { VideoResultsSkeleton } from './components/VideoResultsSkeleton';
const inferDetectionType = (data: CompletedVideoResult): DetectionType => {
if (data.detection_mode) return data.detection_mode as DetectionType;
const signboards = data.summary?.unique_signboards || 0;
const potholes = data.summary?.unique_potholes || 0;
if (signboards > 0 && potholes > 0) return 'pot-sign-detection';
if (signboards > 0) return 'sign-board-detection';
return 'pothole-detection';
};
export default function VideoResultsPage() {
const router = useRouter();
const { videoId } = useParams() as { videoId: string };
@@ -33,30 +19,19 @@ export default function VideoResultsPage() {
error: queryError,
} = useVideoResultsQuery(videoId);
const detectionType = useMemo<DetectionType>(
() =>
detectionData ? inferDetectionType(detectionData) : 'pothole-detection',
[detectionData],
);
const error = isError
? queryError instanceof Error
? queryError.message
: 'Failed to load results'
: null;
const getTitle = () => {
const config = getDetectionModeConfig(detectionType);
return `${config.label} Results`;
};
if (isLoading) {
return <VideoResultsSkeleton />;
}
if (error) {
return (
<div className="min-h-[60vh] flex items-center justify-center">
<div className="flex min-h-[60vh] items-center justify-center">
<p className="text-sm text-destructive">{error}</p>
</div>
);
@@ -65,7 +40,7 @@ export default function VideoResultsPage() {
return (
<div className="space-y-8">
<PageHeader
title={'Detection Results'}
title="Detection Results"
description={`Video ID: ${videoId}`}
icon={TrendingUp}
actions={

View File

@@ -90,8 +90,8 @@ export function TicketOverviewCard({ ticket }: { ticket: TicketDetail }) {
</MetaValue>
</OverviewField>
<OverviewField label="Chainage">
<MetaValue>{ticket.chainage_id || '-'}</MetaValue>
<OverviewField label="Chainage" >
<MetaValue>{ticket.chainage_name}</MetaValue>
</OverviewField>
<OverviewField label="Uploaded By">

View File

@@ -88,7 +88,6 @@ function buildTicketListItem(
!event.id ||
!event.ticket_name ||
!event.status ||
!event.default_defect_class ||
event.detection_count === undefined ||
!event.updated_at
) {
@@ -101,7 +100,7 @@ function buildTicketListItem(
chainage_id: event.chainage_id ?? null,
chainage_name: event.chainage_name ?? null,
status: event.status,
default_defect_class: event.default_defect_class,
default_defect_class: event.default_defect_class ?? null,
assigned_to_name: event.assigned_to_name ?? null,
detection_count: event.detection_count,
created_by_name: event.created_by_name ?? null,

View File

@@ -63,8 +63,8 @@ export function useTicketDetailQuery(
return useQuery({
queryKey: ticketKeys.detail(ticketId ?? '', defectClass),
queryFn: () =>
ticketService.getTicketDetail(ticketId as string, defectClass as string),
enabled: Boolean(ticketId && defectClass),
ticketService.getTicketDetail(ticketId as string, defectClass),
enabled: Boolean(ticketId),
});
}

View File

@@ -44,9 +44,11 @@ export default function TicketPage() {
const handleView = useCallback(
(ticket: TicketListItem) => {
router.push(
`/ticket/${ticket.id}?defect_class=${encodeURIComponent(ticket.default_defect_class)}`,
);
const defectClassQuery = ticket.default_defect_class
? `?defect_class=${encodeURIComponent(ticket.default_defect_class)}`
: '';
router.push(`/ticket/${ticket.id}${defectClassQuery}`);
},
[router],
);

View File

@@ -1,48 +1,36 @@
'use client';
import { Activity, AlertTriangle, MapPin, SignpostBig } from 'lucide-react';
import { Activity, MapPin } from 'lucide-react';
import type { CompletedVideoResult, DetectionResultLog } from '@/types';
import { getDefectVisual } from './defectVisualConfig';
interface CurrentDetectionBarProps {
activeLog?: DetectionResultLog;
summary: CompletedVideoResult['summary'];
data: CompletedVideoResult;
}
export default function CurrentDetectionBar({
activeLog,
summary,
data,
}: CurrentDetectionBarProps) {
const currentCounts = activeLog?.cumulative_counts || summary;
const countItems = [
{
label: 'Potholes',
value: currentCounts.unique_potholes || 0,
icon: AlertTriangle,
className: 'text-orange-500',
iconClassName: 'bg-orange-500/10 text-orange-500',
},
{
label: 'Signboards',
value: currentCounts.unique_signboards || 0,
icon: SignpostBig,
className: 'text-blue-500',
iconClassName: 'bg-blue-500/10 text-blue-500',
},
{
label: 'Total',
value: currentCounts.total_detections || 0,
icon: Activity,
className: 'text-green-500',
iconClassName: 'bg-green-500/10 text-green-500',
},
];
const latitude = activeLog?.latitude;
const longitude = activeLog?.longitude;
const defectCounts = activeLog
? activeLog.running_counts.by_class
: data.defect_counts;
const totalDetections = activeLog
? activeLog.running_counts.total
: data.summary.total_detections;
const { latitude, longitude } = activeLog?.location ?? {
latitude: undefined,
longitude: undefined,
};
const coordinates =
typeof latitude === 'number' && typeof longitude === 'number'
? `${latitude.toFixed(7)}, ${longitude.toFixed(7)}`
: undefined;
const gridColumns =
defectCounts.length >= 3 ? 'sm:grid-cols-3' : 'sm:grid-cols-2';
return (
<section className="overflow-hidden rounded-lg border bg-card/80">
@@ -67,33 +55,48 @@ export default function CurrentDetectionBar({
) : null}
</div>
<div className="grid grid-cols-1 gap-2 p-3 sm:grid-cols-3">
{countItems.map((item) => {
const Icon = item.icon;
<div className={`grid grid-cols-1 gap-2 p-3 ${gridColumns}`}>
{defectCounts.map((item) => {
const visual = getDefectVisual(item.class_name);
const Icon = visual.icon;
return (
<div
key={item.label}
key={item.class_name}
className="flex items-center justify-between gap-3 rounded-md bg-muted/25 px-3 py-2.5 ring-1 ring-border/60"
>
<div className="flex min-w-0 items-center gap-3">
<span
className={`flex size-8 shrink-0 items-center justify-center rounded-md ${item.iconClassName}`}
className={`flex size-8 shrink-0 items-center justify-center rounded-md ${visual.iconClassName}`}
>
<Icon className="size-4" />
</span>
<p className="truncate text-xs font-semibold uppercase tracking-wide text-muted-foreground">
{item.label}
{item.display_name}
</p>
</div>
<p
className={`text-2xl font-bold leading-none ${item.className}`}
className={`text-2xl font-bold leading-none ${visual.colorClassName}`}
>
{item.value}
{item.count}
</p>
</div>
);
})}
<div className="flex items-center justify-between gap-3 rounded-md bg-muted/25 px-3 py-2.5 ring-1 ring-border/60">
<div className="flex min-w-0 items-center gap-3">
<span className="flex size-8 shrink-0 items-center justify-center rounded-md bg-green-500/10 text-green-500">
<Activity className="size-4" />
</span>
<p className="truncate text-xs font-semibold uppercase tracking-wide text-muted-foreground">
Total
</p>
</div>
<p className="text-2xl font-bold leading-none text-green-500">
{totalDetections}
</p>
</div>
</div>
</section>
);

View File

@@ -0,0 +1,54 @@
'use client';
import {
Activity,
AlertTriangle,
CircleAlert,
CircleDot,
Droplets,
LucideIcon,
SignpostBig,
} from 'lucide-react';
type DefectVisualConfig = {
icon: LucideIcon;
colorClassName: string;
iconClassName: string;
};
const DEFECT_VISUALS: Record<string, DefectVisualConfig> = {
manhole_cover: {
icon: CircleDot,
colorClassName: 'text-zinc-500',
iconClassName: 'bg-zinc-500/10 text-zinc-500',
},
pothole: {
icon: AlertTriangle,
colorClassName: 'text-orange-500',
iconClassName: 'bg-orange-500/10 text-orange-500',
},
road_crack: {
icon: CircleAlert,
colorClassName: 'text-rose-500',
iconClassName: 'bg-rose-500/10 text-rose-500',
},
sign_board: {
icon: SignpostBig,
colorClassName: 'text-blue-500',
iconClassName: 'bg-blue-500/10 text-blue-500',
},
water_puddle: {
icon: Droplets,
colorClassName: 'text-cyan-500',
iconClassName: 'bg-cyan-500/10 text-cyan-500',
},
};
const DEFAULT_VISUAL: DefectVisualConfig = {
icon: Activity,
colorClassName: 'text-green-500',
iconClassName: 'bg-green-500/10 text-green-500',
};
export const getDefectVisual = (className: string) =>
DEFECT_VISUALS[className] || DEFAULT_VISUAL;

View File

@@ -2,10 +2,8 @@
import { useEffect, useRef } from 'react';
import { Activity, Film, MapPin } from 'lucide-react';
import { Badge } from '@/components/ui/badge';
import { ScrollArea } from '@/components/ui/scroll-area';
import { DetectionResultLog } from '@/types';
import { DETECTION_TYPES } from '@/constants/detectionModeConfig';
import { cn } from '@/lib/utils';
interface DetectionLogsProps {
@@ -34,7 +32,7 @@ const DetectionLogs = ({ logs, activeLogId, onSeek }: DetectionLogsProps) => {
return (
<div className="space-y-4">
<div className="flex items-center justify-between">
<h4 className="text-sm font-bold flex items-center gap-2">
<h4 className="flex items-center gap-2 text-sm font-bold">
<Activity className="h-4 w-4" />
Detection Logs
</h4>
@@ -43,20 +41,18 @@ const DetectionLogs = ({ logs, activeLogId, onSeek }: DetectionLogsProps) => {
<div className="space-y-3 p-4">
{logs.length === 0 ? (
<div className="flex flex-col items-center justify-center py-12 text-muted-foreground">
<Film className="h-8 w-8 mb-2 opacity-20" />
<Film className="mb-2 h-8 w-8 opacity-20" />
<p className="text-xs">No detections found</p>
</div>
) : (
logs.map((log) => {
const typeId = (log.type || '').toLowerCase();
const typeConfig = DETECTION_TYPES[typeId];
const label =
log.label || typeConfig?.label || typeId.replace(/_/g, ' ');
const timestampSeconds = log.frame.timestamp_seconds;
const { latitude, longitude } = log.location;
const isActive = activeLogId === log.id;
return (
<button
key={`${log.id}-${log.frame}`}
key={`${log.id}-${log.frame.number}`}
ref={isActive ? activeLogRef : null}
type="button"
onClick={() => onSeek(log)}
@@ -65,35 +61,35 @@ const DetectionLogs = ({ logs, activeLogId, onSeek }: DetectionLogsProps) => {
isActive && 'border-primary bg-primary/5',
)}
>
<div className="flex items-start justify-between gap-4 mb-3">
<div className="mb-3 flex items-start justify-between gap-4">
<div>
<div className="text-[13px] font-bold">
{label} ID: {log.id}
{log.detection.display_name} ID: {log.id}
</div>
<div className="text-[11px] text-muted-foreground font-medium">
Frame {log.frame}
<div className="text-[11px] font-medium text-muted-foreground">
Frame {log.frame.number}
</div>
</div>
<div className="text-right shrink-0">
<div className="shrink-0 text-right">
<div className="text-xs font-bold">
{formatVideoTime(log.timestamp_seconds)}
{formatVideoTime(timestampSeconds)}
</div>
<div className="text-[11px] text-muted-foreground">
{log.timestamp_seconds.toFixed(2)}s
{timestampSeconds.toFixed(2)}s
</div>
</div>
</div>
<div className="space-y-2 text-[12px] text-muted-foreground/90 font-medium">
{typeof log.confidence === 'number' && (
<div>Confidence: {(log.confidence * 100).toFixed(1)}%</div>
)}
{typeof log.latitude === 'number' &&
typeof log.longitude === 'number' && (
<div className="space-y-2 text-[12px] font-medium text-muted-foreground/90">
<div>
Confidence: {(log.detection.confidence * 100).toFixed(1)}%
</div>
{typeof latitude === 'number' &&
typeof longitude === 'number' && (
<div className="flex items-center gap-1.5">
<MapPin className="h-3 w-3" />
<span>
{log.latitude.toFixed(8)}, {log.longitude.toFixed(8)}
{latitude.toFixed(8)}, {longitude.toFixed(8)}
</span>
</div>
)}

View File

@@ -1,81 +1,80 @@
'use client';
import {
Activity,
AlertTriangle,
Clock,
Gauge,
SignpostBig,
} from 'lucide-react';
import { Activity, Clock, Gauge } from 'lucide-react';
import { CompletedVideoResult } from '@/types';
const formatDuration = (seconds: number) => {
const safeSeconds = Number.isFinite(seconds) ? seconds : 0;
const minutes = Math.floor(safeSeconds / 60);
const secs = Math.floor(safeSeconds % 60);
import { getDefectVisual } from './defectVisualConfig';
const formatDuration = (seconds?: number) => {
if (typeof seconds !== 'number' || !Number.isFinite(seconds)) return '-';
const minutes = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${minutes}:${secs.toString().padStart(2, '0')}`;
};
const formatFps = (fps?: number) =>
typeof fps === 'number' && Number.isFinite(fps) ? fps.toFixed(1) : '-';
interface ResultStatsGridProps {
data: CompletedVideoResult;
}
export default function ResultStatsGrid({ data }: ResultStatsGridProps) {
const defectStats = data.defect_counts.map((item) => {
const visual = getDefectVisual(item.class_name);
return {
label: item.display_name,
value: item.count,
icon: visual.icon,
color: visual.colorClassName,
bgColor: visual.iconClassName,
};
});
const stats = [
{
label: 'Total Detections',
value: data.summary.total_detections || 0,
value: data.summary.total_detections,
icon: Activity,
color: 'text-green-500',
bgColor: 'bg-green-500/10',
},
{
label: 'Potholes',
value: data.summary.unique_potholes || 0,
icon: AlertTriangle,
color: 'text-orange-500',
bgColor: 'bg-orange-500/10',
},
{
label: 'Signboards',
value: data.summary.unique_signboards || 0,
icon: SignpostBig,
color: 'text-blue-500',
bgColor: 'bg-blue-500/10',
bgColor: 'bg-green-500/10 text-green-500',
},
...defectStats,
{
label: 'FPS',
value: data.fps.toFixed(1),
value: formatFps(data.summary.fps),
icon: Gauge,
color: 'text-purple-500',
bgColor: 'bg-purple-500/10',
bgColor: 'bg-purple-500/10 text-purple-500',
},
{
label: 'Duration',
value: formatDuration(data.duration_seconds),
value: formatDuration(data.summary.duration_seconds),
icon: Clock,
color: 'text-cyan-500',
bgColor: 'bg-cyan-500/10',
bgColor: 'bg-cyan-500/10 text-cyan-500',
},
];
return (
<div className="grid grid-cols-2 md:grid-cols-5 gap-3">
<div className="grid grid-cols-2 gap-3 md:grid-cols-3 xl:grid-cols-5">
{stats.map((stat) => {
const Icon = stat.icon;
return (
<div
key={stat.label}
className="p-4 rounded-lg bg-muted/30 border border-muted flex flex-col items-center text-center"
className="flex flex-col items-center rounded-lg border border-muted bg-muted/30 p-4 text-center"
>
<div className={`p-2 rounded-md mb-2 ${stat.bgColor}`}>
<Icon className={`h-5 w-5 ${stat.color}`} />
<div className={`mb-2 rounded-md p-2 ${stat.bgColor}`}>
<Icon className="h-5 w-5" />
</div>
<div className={`text-xl font-bold ${stat.color}`}>
{stat.value}
</div>
<div className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground mt-1">
<div className="mt-1 text-[10px] font-bold uppercase tracking-wider text-muted-foreground">
{stat.label}
</div>
</div>

View File

@@ -14,7 +14,10 @@ export function useDetectionPlayback({
}: UseDetectionPlaybackParams) {
const shouldPauseAfterSeekRef = useRef(false);
const sortedLogs = useMemo(
() => [...(logs || [])].sort((a, b) => a.timestamp_seconds - b.timestamp_seconds),
() =>
[...(logs || [])].sort(
(a, b) => a.frame.timestamp_seconds - b.frame.timestamp_seconds,
),
[logs],
);
const [activeLog, setActiveLog] = useState<DetectionResultLog | undefined>(
@@ -27,7 +30,7 @@ export function useDetectionPlayback({
shouldPauseAfterSeekRef.current = true;
video.pause();
video.currentTime = log.timestamp_seconds;
video.currentTime = log.frame.timestamp_seconds;
setActiveLog(log);
};
@@ -44,7 +47,7 @@ export function useDetectionPlayback({
if (!video || sortedLogs.length === 0) return;
const currentLog = sortedLogs.findLast(
(log) => log.timestamp_seconds <= video.currentTime,
(log) => log.frame.timestamp_seconds <= video.currentTime,
);
if (currentLog && currentLog.id !== activeLog?.id) {

View File

@@ -21,9 +21,7 @@ type VideoPlayerSectionProps = {
data: CompletedVideoResult;
};
export default function VideoPlayerSection({
data,
}: VideoPlayerSectionProps) {
export default function VideoPlayerSection({ data }: VideoPlayerSectionProps) {
const videoRef = useRef<HTMLVideoElement>(null);
const {
activeLog,
@@ -45,9 +43,9 @@ export default function VideoPlayerSection({
return (
<div className="space-y-6">
<Card className="overflow-hidden">
<CardHeader className="pb-4 border-b">
<CardHeader className="border-b pb-4">
<div className="flex items-center gap-3">
<div className="p-2 rounded bg-secondary">
<div className="rounded bg-secondary p-2">
<Film className="h-5 w-5 text-primary" />
</div>
<div>
@@ -61,8 +59,8 @@ export default function VideoPlayerSection({
</div>
</CardHeader>
<CardContent className="pt-6">
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="lg:col-span-2 space-y-6">
<div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
<div className="space-y-6 lg:col-span-2">
<AnnotatedVideoPlayer
videoRef={videoRef}
videoUrl={videoUrl}
@@ -73,10 +71,7 @@ export default function VideoPlayerSection({
onSeeked={handleSeeked}
/>
<CurrentDetectionBar
activeLog={activeLog}
summary={data.summary}
/>
<CurrentDetectionBar activeLog={activeLog} data={data} />
</div>
<DetectionLogs
@@ -91,7 +86,6 @@ export default function VideoPlayerSection({
</div>
</CardContent>
</Card>
</div>
);
}

View File

@@ -54,7 +54,8 @@ export const API_ROUTES = {
},
TICKETS: {
BASE: '/biz/api/v1/tickets',
DETAIL: (id: string, defectClass: string) =>
DETAIL: (id: string) => `/biz/api/v1/tickets/${id}`,
CLASS_DETAIL: (id: string, defectClass: string) =>
`/biz/api/v1/tickets/${id}/class-detail?defect_class=${defectClass}`,
DEFECT_CLASSES: (id: string) => `/biz/api/v1/tickets/${id}/defect-classes`,
ASSIGN: (id: string) => `/biz/api/v1/tickets/${id}/assign`,

View File

@@ -1,149 +0,0 @@
/**
* Detection mode configurations to support dynamic UI behavior
*/
export interface DetectionTypeConfig {
id: string;
label: string;
color: string;
}
export interface DetectionModeConfig {
id: string;
label: string;
enabledTypes: string[]; // IDs of detection types to show in this mode
}
export const DETECTION_TYPES: Record<string, DetectionTypeConfig> = {
pothole: {
id: 'pothole',
label: 'Pothole',
color: 'var(--chart-1)',
},
defected_sign_board: {
id: 'defected_sign_board',
label: 'Defect Sign Board',
color: 'var(--chart-2)',
},
road_crack: {
id: 'road_crack',
label: 'Road Crack',
color: 'var(--chart-3)',
},
damaged_road_marking: {
id: 'damaged_road_marking',
label: 'Damage Road Mark',
color: 'var(--chart-4)',
},
good_sign_board: {
id: 'good_sign_board',
label: 'Good Sign Board',
color: 'var(--chart-5)',
},
drain_issue: {
id: 'drain_issue',
label: 'Drain Issue',
color: 'var(--chart-6)',
},
good_culvert: {
id: 'good_culvert',
label: 'Good Culvert',
color: 'var(--chart-5)',
},
defective_culvert: {
id: 'defective_culvert',
label: 'Defective Culvert',
color: 'var(--chart-8)',
},
};
export const DETECTION_MODES: Record<string, DetectionModeConfig> = {
yolo: {
id: 'yolo',
label: 'YOLO Detection',
enabledTypes: [
'pothole',
'defected_sign_board',
'road_crack',
'damaged_road_marking',
'good_sign_board',
'drain_issue',
],
},
yolo_vl: {
id: 'yolo_vl',
label: 'YOLO-VL Detection',
enabledTypes: [
'pothole',
'defected_sign_board',
'road_crack',
'damaged_road_marking',
'good_sign_board',
'drain_issue',
],
},
'pothole-detection': {
id: 'pothole-detection',
label: 'Pothole Detection',
enabledTypes: [
'pothole',
'road_crack',
'damaged_road_marking',
'drain_issue',
],
},
'sign-board-detection': {
id: 'sign-board-detection',
label: 'Signboard Detection',
enabledTypes: ['defected_sign_board', 'good_sign_board'],
},
'pot-sign-detection': {
id: 'pot-sign-detection',
label: 'Pothole & Signboard Detection',
enabledTypes: [
'pothole',
'defected_sign_board',
'road_crack',
'damaged_road_marking',
'good_sign_board',
'drain_issue',
],
},
culvert_detection: {
id: 'culvert_detection',
label: 'Culvert Detection',
enabledTypes: ['good_culvert', 'defective_culvert'],
},
gemini_video: {
id: 'gemini_video',
label: 'Gemini AI Analysis',
enabledTypes: [
'pothole',
'defected_sign_board',
'road_crack',
'damaged_road_marking',
'good_sign_board',
'drain_issue',
'good_culvert',
'defective_culvert',
],
},
};
// Fallback config for unknown modes
export const DEFAULT_DETECTION_MODE: DetectionModeConfig =
DETECTION_MODES['pot-sign-detection'];
export const getDetectionModeConfig = (mode?: string): DetectionModeConfig => {
if (!mode) return DEFAULT_DETECTION_MODE;
return DETECTION_MODES[mode] || DEFAULT_DETECTION_MODE;
};
export const getEnabledDetectionTypes = (
mode?: string,
): DetectionTypeConfig[] => {
const config = getDetectionModeConfig(mode);
return config.enabledTypes
.map((typeId) => DETECTION_TYPES[typeId])
.filter(Boolean);
};

View File

@@ -35,11 +35,13 @@ export const ticketService = {
getTicketDetail: async (
ticketId: string,
defectClass: string,
defectClass?: string,
): Promise<TicketDetail> => {
const response = await axiosClient.get<TicketDetail>(
API_ROUTES.TICKETS.DETAIL(ticketId, defectClass),
);
const path = defectClass
? API_ROUTES.TICKETS.CLASS_DETAIL(ticketId, defectClass)
: API_ROUTES.TICKETS.DETAIL(ticketId);
const response = await axiosClient.get<TicketDetail>(path);
return response.data;
},

View File

@@ -1,19 +1,15 @@
import { DetectionResultLog } from './detection';
import { DetectionClassCount, DetectionResultLog } from './detection';
export type CompletedVideoResult = {
video_id: string;
status: 'completed' | string;
// annotated_video_url: string;
processed_video_url: string | null;
raw_video_url: string | null;
fps: number;
duration_seconds: number;
detection_mode?: string;
summary: {
fps?: number;
duration_seconds?: number;
total_detections: number;
unique_potholes?: number;
unique_signboards?: number;
[key: string]: number | undefined;
};
defect_counts: DetectionClassCount[];
logs: DetectionResultLog[];
};

View File

@@ -1,43 +1,32 @@
/**
* Detection types for map and display
*/
export interface Detection {
id: number;
video_id: string;
type: string;
class: string;
confidence: number;
latitude: number | null;
longitude: number | null;
frame_number: number;
timestamp_ms: number;
}
export type DetectionType =
| 'pothole-detection'
| 'sign-board-detection'
| 'pot-sign-detection'
| 'culvert_detection'
| 'yolo'
| 'yolo_vl'
| 'sam3'
| 'yoloe'
| 'yoloe_trained_vl'
| 'gemini_video';
export type DetectionClassCount = {
class_name: string;
display_name: string;
count: number;
unique_count: number;
};
export type DetectionResultLog = {
id: string;
type: string;
label?: string;
frame: number;
timestamp_seconds: number;
confidence?: number;
latitude?: number | null;
longitude?: number | null;
cumulative_counts?: {
unique_potholes?: number;
unique_signboards?: number;
total_detections?: number;
[key: string]: number | undefined;
detection: {
id: number;
class_name: string;
display_name: string;
confidence: number;
};
frame: {
number: number;
timestamp_seconds: number;
};
location: {
latitude: number | null;
longitude: number | null;
};
running_counts: {
total: number;
by_class: Array<{
class_name: string;
display_name: string;
count: number;
}>;
};
};

View File

@@ -71,6 +71,7 @@ export interface TicketDetail {
ticket_name?: string;
video_id: string;
chainage_id: string | null;
chainage_name: string | null;
status: TicketStatus;
defect_class: string;
defect_display_name: string;

View File

@@ -14,7 +14,7 @@ export type TicketTableStatusEvent = {
chainage_id?: string | null;
chainage_name?: string | null;
status?: TicketStatus;
default_defect_class?: string;
default_defect_class?: string | null;
assigned_to_name?: string | null;
detection_count?: number;
created_by_name?: string | null;

View File

@@ -12,7 +12,7 @@ export interface TicketListItem {
chainage_id: string | null;
chainage_name: string | null;
status: TicketStatus;
default_defect_class: string;
default_defect_class: string | null;
assigned_to_name: string | null;
detection_count: number;
created_by_name: string | null;