@@ -17,8 +17,23 @@ document.addEventListener('DOMContentLoaded', async function() {
1717
1818 let client = null ;
1919 let autoDescribeInterval = null ;
20+ let autoDetectInterval = null ;
2021 let currentStream = null ;
2122
23+ const DEFAULT_COLORS = [ '#93CCEA' , '#2A9D8F' , '#E9C46A' , '#E76F51' , '#9B5DE5' ] ;
24+ const MAX_DETECTIONS = 5 ;
25+ let detections = [ ] ;
26+ let detectionResults = { } ;
27+
28+ const detectionCanvas = document . getElementById ( 'detectionCanvas' ) ;
29+ const detectionCtx = detectionCanvas . getContext ( '2d' ) ;
30+ const detectionList = document . getElementById ( 'detectionList' ) ;
31+ const addDetectionBtn = document . getElementById ( 'addDetectionBtn' ) ;
32+ const detectNowBtn = document . getElementById ( 'detectNowBtn' ) ;
33+ const autoDetectCheckbox = document . getElementById ( 'autoDetect' ) ;
34+ const detectionToggle = document . getElementById ( 'detectionToggle' ) ;
35+ const detectionSection = document . querySelector ( '.detection-section' ) ;
36+
2237 window . reasoningConsole = new ReasoningConsole ( {
2338 startCollapsed : false
2439 } ) ;
@@ -331,6 +346,257 @@ document.addEventListener('DOMContentLoaded', async function() {
331346
332347 loadSavedPreferences ( ) ;
333348
349+ detectionToggle . addEventListener ( 'click' , ( ) => {
350+ detectionSection . classList . toggle ( 'collapsed' ) ;
351+ savePreference ( 'detectionCollapsed' , detectionSection . classList . contains ( 'collapsed' ) ) ;
352+ } ) ;
353+
354+ function getNextColor ( ) {
355+ return DEFAULT_COLORS [ detections . length % DEFAULT_COLORS . length ] ;
356+ }
357+
358+ function createDetectionRow ( target = '' , color = null , enabled = true ) {
359+ const id = Date . now ( ) + Math . random ( ) ;
360+ const rowColor = color || getNextColor ( ) ;
361+
362+ const row = document . createElement ( 'div' ) ;
363+ row . className = 'detection-row' ;
364+ row . dataset . id = id ;
365+
366+ row . innerHTML = `
367+ <input type="text" placeholder="e.g., person, red car, coffee mug" value="${ target } ">
368+ <input type="color" value="${ rowColor } ">
369+ <label class="toggle-switch">
370+ <input type="checkbox" ${ enabled ? 'checked' : '' } >
371+ <span class="toggle-slider"></span>
372+ </label>
373+ <button class="btn-delete-detection">✕</button>
374+ ` ;
375+
376+ const textInput = row . querySelector ( 'input[type="text"]' ) ;
377+ const colorInput = row . querySelector ( 'input[type="color"]' ) ;
378+ const toggleInput = row . querySelector ( 'input[type="checkbox"]' ) ;
379+ const deleteBtn = row . querySelector ( '.btn-delete-detection' ) ;
380+
381+ textInput . addEventListener ( 'input' , ( ) => {
382+ updateDetectionData ( ) ;
383+ clearDetectionResults ( id ) ;
384+ } ) ;
385+ colorInput . addEventListener ( 'input' , ( ) => {
386+ updateDetectionData ( ) ;
387+ drawDetectionBoxes ( ) ;
388+ } ) ;
389+ toggleInput . addEventListener ( 'change' , ( ) => {
390+ updateDetectionData ( ) ;
391+ drawDetectionBoxes ( ) ;
392+ } ) ;
393+ deleteBtn . addEventListener ( 'click' , ( ) => {
394+ row . remove ( ) ;
395+ delete detectionResults [ id ] ;
396+ updateDetectionData ( ) ;
397+ drawDetectionBoxes ( ) ;
398+ updateAddButton ( ) ;
399+ } ) ;
400+
401+ detectionList . appendChild ( row ) ;
402+ updateDetectionData ( ) ;
403+ updateAddButton ( ) ;
404+
405+ return row ;
406+ }
407+
408+ function updateDetectionData ( ) {
409+ detections = [ ] ;
410+ const rows = detectionList . querySelectorAll ( '.detection-row' ) ;
411+ rows . forEach ( row => {
412+ const id = row . dataset . id ;
413+ const target = row . querySelector ( 'input[type="text"]' ) . value . trim ( ) ;
414+ const color = row . querySelector ( 'input[type="color"]' ) . value ;
415+ const enabled = row . querySelector ( 'input[type="checkbox"]' ) . checked ;
416+ detections . push ( { id, target, color, enabled } ) ;
417+ } ) ;
418+ savePreference ( 'detections' , detections ) ;
419+ }
420+
421+ function clearDetectionResults ( id ) {
422+ delete detectionResults [ id ] ;
423+ drawDetectionBoxes ( ) ;
424+ }
425+
426+ function updateAddButton ( ) {
427+ addDetectionBtn . disabled = detections . length >= MAX_DETECTIONS ;
428+ }
429+
430+ function resizeCanvas ( ) {
431+ const rect = video . getBoundingClientRect ( ) ;
432+ detectionCanvas . width = rect . width ;
433+ detectionCanvas . height = rect . height ;
434+ drawDetectionBoxes ( ) ;
435+ }
436+
437+ function drawDetectionBoxes ( ) {
438+ detectionCtx . clearRect ( 0 , 0 , detectionCanvas . width , detectionCanvas . height ) ;
439+
440+ const canvasWidth = detectionCanvas . width ;
441+ const canvasHeight = detectionCanvas . height ;
442+
443+ detections . forEach ( detection => {
444+ if ( ! detection . enabled || ! detection . target ) return ;
445+
446+ const results = detectionResults [ detection . id ] ;
447+ if ( ! results || ! results . objects || results . objects . length === 0 ) return ;
448+
449+ const count = results . objects . length ;
450+
451+ results . objects . forEach ( ( obj , idx ) => {
452+ const x = obj . x_min * canvasWidth ;
453+ const y = obj . y_min * canvasHeight ;
454+ const width = ( obj . x_max - obj . x_min ) * canvasWidth ;
455+ const height = ( obj . y_max - obj . y_min ) * canvasHeight ;
456+
457+ detectionCtx . strokeStyle = detection . color ;
458+ detectionCtx . lineWidth = 4 ;
459+ detectionCtx . strokeRect ( x , y , width , height ) ;
460+
461+ const labelText = count > 1 ? `${ detection . target } (${ idx + 1 } )` : detection . target ;
462+ detectionCtx . font = 'bold 14px system-ui, -apple-system, sans-serif' ;
463+ const textMetrics = detectionCtx . measureText ( labelText ) ;
464+ const labelPadX = 10 ;
465+ const labelPadY = 6 ;
466+ const labelHeight = 14 + labelPadY * 2 ;
467+ const labelWidth = textMetrics . width + labelPadX * 2 ;
468+
469+ const labelX = x ;
470+ const labelY = y - labelHeight ;
471+
472+ detectionCtx . fillStyle = detection . color ;
473+ detectionCtx . beginPath ( ) ;
474+ detectionCtx . roundRect ( labelX , labelY , labelWidth , labelHeight , 4 ) ;
475+ detectionCtx . fill ( ) ;
476+
477+ detectionCtx . fillStyle = '#FFFFFF' ;
478+ detectionCtx . fillText ( labelText , labelX + labelPadX , labelY + labelPadY + 12 ) ;
479+ } ) ;
480+ } ) ;
481+ }
482+
483+ async function runDetection ( ) {
484+ if ( ! client ) {
485+ window . reasoningConsole . logError ( 'No API key configured' ) ;
486+ updateStatus ( 'Please configure your Moondream API key' , 'error' ) ;
487+ window . apiKeyManager . showModal ( ) ;
488+ return ;
489+ }
490+
491+ const activeDetections = detections . filter ( d => d . enabled && d . target ) ;
492+ if ( activeDetections . length === 0 ) {
493+ window . reasoningConsole . logInfo ( 'No active detections configured' ) ;
494+ return ;
495+ }
496+
497+ detectNowBtn . disabled = true ;
498+ updateStatus ( 'Running detection...' ) ;
499+
500+ const startTime = Date . now ( ) ;
501+
502+ try {
503+ const promises = activeDetections . map ( async ( detection ) => {
504+ try {
505+ const result = await client . detectInVideo ( video , detection . target ) ;
506+ detectionResults [ detection . id ] = result ;
507+ window . reasoningConsole . logInfo ( `Detected ${ result . objects . length } "${ detection . target } "` ) ;
508+ return { id : detection . id , success : true , count : result . objects . length } ;
509+ } catch ( error ) {
510+ window . reasoningConsole . logError ( `Detection failed for "${ detection . target } ": ${ error . message } ` ) ;
511+ return { id : detection . id , success : false , error : error . message } ;
512+ }
513+ } ) ;
514+
515+ const results = await Promise . all ( promises ) ;
516+ const elapsed = Date . now ( ) - startTime ;
517+
518+ drawDetectionBoxes ( ) ;
519+
520+ const successCount = results . filter ( r => r . success ) . length ;
521+ const totalObjects = results . filter ( r => r . success ) . reduce ( ( sum , r ) => sum + r . count , 0 ) ;
522+
523+ updateStatus ( `Detected ${ totalObjects } object(s) across ${ successCount } target(s) in ${ elapsed } ms` , 'success' ) ;
524+ window . reasoningConsole . logDecision ( 'Detection complete' , `${ totalObjects } objects found in ${ elapsed } ms` ) ;
525+
526+ updateJsonOutput ( {
527+ type : 'object_detection' ,
528+ timestamp : new Date ( ) . toISOString ( ) ,
529+ latency_ms : elapsed ,
530+ detections : activeDetections . map ( d => ( {
531+ target : d . target ,
532+ color : d . color ,
533+ results : detectionResults [ d . id ]
534+ } ) )
535+ } ) ;
536+
537+ } catch ( error ) {
538+ window . reasoningConsole . logError ( error . message ) ;
539+ updateStatus ( 'Detection error: ' + error . message , 'error' ) ;
540+ } finally {
541+ detectNowBtn . disabled = false ;
542+ }
543+ }
544+
545+ function toggleAutoDetect ( ) {
546+ if ( autoDetectCheckbox . checked ) {
547+ const interval = parseInt ( autoIntervalInput . value ) * 1000 ;
548+ autoDetectInterval = setInterval ( runDetection , interval ) ;
549+ window . reasoningConsole . logInfo ( `Auto-detect enabled: every ${ autoIntervalInput . value } s` ) ;
550+ } else {
551+ clearInterval ( autoDetectInterval ) ;
552+ autoDetectInterval = null ;
553+ window . reasoningConsole . logInfo ( 'Auto-detect disabled' ) ;
554+ }
555+ }
556+
557+ addDetectionBtn . addEventListener ( 'click' , ( ) => {
558+ if ( detections . length < MAX_DETECTIONS ) {
559+ createDetectionRow ( ) ;
560+ }
561+ } ) ;
562+
563+ detectNowBtn . addEventListener ( 'click' , runDetection ) ;
564+
565+ autoDetectCheckbox . addEventListener ( 'change' , ( ) => {
566+ savePreference ( 'autoDetect' , autoDetectCheckbox . checked ) ;
567+ toggleAutoDetect ( ) ;
568+ } ) ;
569+
570+ video . addEventListener ( 'loadedmetadata' , resizeCanvas ) ;
571+ video . addEventListener ( 'resize' , resizeCanvas ) ;
572+ window . addEventListener ( 'resize' , resizeCanvas ) ;
573+
574+ function loadDetectionPreferences ( ) {
575+ if ( window . VRPPrefs ) {
576+ const collapsed = VRPPrefs . getToolPref ( TOOL_ID , 'detectionCollapsed' , false ) ;
577+ const savedDetections = VRPPrefs . getToolPref ( TOOL_ID , 'detections' , [ ] ) ;
578+ const savedAutoDetect = VRPPrefs . getToolPref ( TOOL_ID , 'autoDetect' , false ) ;
579+
580+ if ( collapsed ) {
581+ detectionSection . classList . add ( 'collapsed' ) ;
582+ }
583+
584+ if ( savedDetections . length > 0 ) {
585+ savedDetections . forEach ( d => {
586+ createDetectionRow ( d . target , d . color , d . enabled ) ;
587+ } ) ;
588+ }
589+
590+ autoDetectCheckbox . checked = savedAutoDetect ;
591+ if ( savedAutoDetect ) {
592+ setTimeout ( toggleAutoDetect , 2500 ) ;
593+ }
594+ }
595+ }
596+
597+ loadDetectionPreferences ( ) ;
598+ setTimeout ( resizeCanvas , 500 ) ;
599+
334600 if ( window . VideoSourceAdapter ) {
335601 VideoSourceAdapter . init ( {
336602 videoElement : video ,
0 commit comments