44 * SPDX-License-Identifier: MIT
55 */
66
7+ import { useMemo , memo } from 'react' ;
8+ import { getCocoLabel } from '../constants/cocoLabels' ;
9+
710export interface Detection {
811 id : string ;
912 label : string ;
@@ -20,43 +23,167 @@ export interface Position {
2023
2124export interface DetectionInfoProps {
2225 detections : Detection [ ] ;
26+ showGrouped ?: boolean ;
27+ }
28+
29+ interface GroupedDetection {
30+ label : string ;
31+ count : number ;
32+ minConfidence : number ;
33+ maxConfidence : number ;
34+ minDistance ?: number ;
35+ maxDistance ?: number ;
2336}
2437
25- export default function DetectionInfo ( { detections } : DetectionInfoProps ) {
38+ function DetectionInfo ( {
39+ detections,
40+ showGrouped = false ,
41+ } : DetectionInfoProps ) {
2642 if ( ! detections || detections . length === 0 ) {
2743 return null ;
2844 }
2945
46+ // Sort detections by distance (closest first)
47+ const sortedDetections = useMemo ( ( ) => {
48+ return [ ...detections ] . sort ( ( a , b ) => {
49+ // If both have distance, sort by distance
50+ if ( a . distance !== undefined && b . distance !== undefined ) {
51+ return a . distance - b . distance ;
52+ }
53+ // If only one has distance, put it first
54+ if ( a . distance !== undefined ) return - 1 ;
55+ if ( b . distance !== undefined ) return 1 ;
56+ // Otherwise maintain order
57+ return 0 ;
58+ } ) ;
59+ } , [ detections ] ) ;
60+
61+ // Group detections by label
62+ const groupedDetections = useMemo ( ( ) => {
63+ const groups = new Map < string , GroupedDetection > ( ) ;
64+
65+ sortedDetections . forEach ( ( det ) => {
66+ const labelName = getCocoLabel ( det . label ) ;
67+ const existing = groups . get ( labelName ) ;
68+
69+ if ( existing ) {
70+ existing . count ++ ;
71+ existing . minConfidence = Math . min (
72+ existing . minConfidence ,
73+ det . confidence
74+ ) ;
75+ existing . maxConfidence = Math . max (
76+ existing . maxConfidence ,
77+ det . confidence
78+ ) ;
79+
80+ if ( det . distance !== undefined ) {
81+ existing . minDistance =
82+ existing . minDistance !== undefined
83+ ? Math . min ( existing . minDistance , det . distance )
84+ : det . distance ;
85+ existing . maxDistance =
86+ existing . maxDistance !== undefined
87+ ? Math . max ( existing . maxDistance , det . distance )
88+ : det . distance ;
89+ }
90+ } else {
91+ groups . set ( labelName , {
92+ label : labelName ,
93+ count : 1 ,
94+ minConfidence : det . confidence ,
95+ maxConfidence : det . confidence ,
96+ minDistance : det . distance ,
97+ maxDistance : det . distance ,
98+ } ) ;
99+ }
100+ } ) ;
101+
102+ return Array . from ( groups . values ( ) ) ;
103+ } , [ sortedDetections ] ) ;
104+
105+ if ( showGrouped ) {
106+ return (
107+ < div className = "bg-[#2a2a2a] border border-[#404040] p-5 rounded-lg shadow-[0_4px_20px_rgba(0,0,0,0.4)]" >
108+ < h3 className = "my-0 mb-4 text-[#00d4ff] text-xl" >
109+ Detections ({ detections . length } objects)
110+ </ h3 >
111+ < div className = "max-h-96 overflow-y-auto space-y-2" >
112+ { groupedDetections . map ( ( group ) => (
113+ < GroupedDetectionCard key = { group . label } group = { group } />
114+ ) ) }
115+ </ div >
116+ </ div >
117+ ) ;
118+ }
119+
30120 return (
31121 < div className = "bg-[#2a2a2a] border border-[#404040] p-5 rounded-lg shadow-[0_4px_20px_rgba(0,0,0,0.4)]" >
32122 < h3 className = "my-0 mb-4 text-[#00d4ff] text-xl" >
33123 Latest Detections ({ detections . length } )
34124 </ h3 >
35- < div className = "flex flex-wrap gap-2.5" >
36- { detections . map ( ( detection ) => (
37- < div
38- key = { detection . id }
39- className = "flex items-center gap-2 px-3 py-2 bg-[#404040] rounded-md border-l-[3px] border-l-[#00d4ff] border border-[#555]"
40- >
41- < span className = "font-semibold text-[#e0e0e0]" >
42- { detection . label }
43- </ span >
44- < span className = "bg-gradient-to-br from-[#74b9ff] to-[#0984e3] text-white px-2 py-0.5 rounded text-xs font-semibold font-mono shadow-[0_2px_4px_rgba(116,185,255,0.3)]" >
45- { ( detection . confidence * 100 ) . toFixed ( 1 ) } %
46- </ span >
47- { detection . distance && (
48- < span className = "bg-gradient-to-br from-[#00d4aa] to-[#00b894] text-white px-2 py-0.5 rounded text-xs font-semibold font-mono shadow-[0_2px_4px_rgba(0,212,170,0.3)]" >
49- { detection . distance . toFixed ( 2 ) } m
50- </ span >
51- ) }
52- < span className = "bg-gradient-to-br from-orange-700 to-orange-800 text-white px-2 py-0.5 rounded text-xs font-semibold font-mono shadow-[0_2px_4px_rgba(116,185,255,0.3)]" >
53- x={ detection . position . x . toFixed ( 1 ) } m,y=
54- { detection . position . y . toFixed ( 1 ) } m,z=
55- { detection . position . z . toFixed ( 1 ) } m
56- </ span >
57- </ div >
58- ) ) }
125+ < div className = "max-h-96 overflow-y-auto" >
126+ < div className = "flex flex-wrap gap-2.5" >
127+ { sortedDetections . map ( ( detection ) => (
128+ < DetectionCard key = { detection . id } detection = { detection } />
129+ ) ) }
130+ </ div >
59131 </ div >
60132 </ div >
61133 ) ;
62134}
135+
136+ // Memoized detection card component
137+ const DetectionCard = memo ( ( { detection } : { detection : Detection } ) => {
138+ const labelName = getCocoLabel ( detection . label ) ;
139+
140+ return (
141+ < div className = "flex flex-col items-center gap-2 px-3 py-2 bg-[#404040] w-full rounded-md border-l-[3px] border-l-[#00d4ff] border border-[#555]" >
142+ < span className = "font-semibold text-[#e0e0e0]" > { labelName } </ span >
143+ < span className = "bg-gradient-to-br from-[#74b9ff] to-[#0984e3] text-white px-2 py-0.5 rounded text-xs font-semibold font-mono shadow-[0_2px_4px_rgba(116,185,255,0.3)]" >
144+ { ( detection . confidence * 100 ) . toFixed ( 1 ) } %
145+ </ span >
146+ { detection . distance !== undefined && (
147+ < span className = "bg-gradient-to-br from-[#00d4aa] to-[#00b894] text-white px-2 py-0.5 rounded text-xs font-semibold font-mono shadow-[0_2px_4px_rgba(0,212,170,0.3)]" >
148+ { detection . distance . toFixed ( 2 ) } m
149+ </ span >
150+ ) }
151+ < span className = "bg-gradient-to-br from-orange-700 to-orange-800 text-white px-2 py-0.5 rounded text-xs font-semibold font-mono shadow-[0_2px_4px_rgba(116,185,255,0.3)]" >
152+ x={ detection . position . x . toFixed ( 1 ) } m,y=
153+ { detection . position . y . toFixed ( 1 ) } m,z=
154+ { detection . position . z . toFixed ( 1 ) } m
155+ </ span >
156+ </ div >
157+ ) ;
158+ } ) ;
159+
160+ DetectionCard . displayName = 'DetectionCard' ;
161+
162+ // Memoized grouped detection card component
163+ const GroupedDetectionCard = memo ( ( { group } : { group : GroupedDetection } ) => {
164+ return (
165+ < div className = "flex items-center justify-between px-4 py-3 bg-[#404040] rounded-md border-l-[3px] border-l-[#00d4ff] border border-[#555]" >
166+ < div className = "flex items-center gap-3" >
167+ < span className = "font-semibold text-[#e0e0e0] text-lg" >
168+ { group . count } × { group . label }
169+ </ span >
170+ < span className = "text-[#888] text-xs" >
171+ { group . minConfidence === group . maxConfidence
172+ ? `${ ( group . minConfidence * 100 ) . toFixed ( 1 ) } %`
173+ : `${ ( group . minConfidence * 100 ) . toFixed ( 1 ) } %-${ ( group . maxConfidence * 100 ) . toFixed ( 1 ) } %` }
174+ </ span >
175+ </ div >
176+ { group . minDistance !== undefined && (
177+ < span className = "bg-gradient-to-br from-[#00d4aa] to-[#00b894] text-white px-3 py-1 rounded text-sm font-semibold shadow-[0_2px_4px_rgba(0,212,170,0.3)]" >
178+ { group . minDistance === group . maxDistance
179+ ? `${ group . minDistance . toFixed ( 2 ) } m`
180+ : `${ group . minDistance . toFixed ( 2 ) } -${ group . maxDistance ! . toFixed ( 2 ) } m` }
181+ </ span >
182+ ) }
183+ </ div >
184+ ) ;
185+ } ) ;
186+
187+ GroupedDetectionCard . displayName = 'GroupedDetectionCard' ;
188+
189+ export default memo ( DetectionInfo ) ;
0 commit comments