Skip to main content

When the AI Sees Smoke

Somewhere on a ridge, a camera's frames get flagged by the CONDOR AI system: possible smoke. On the operator's screen, that camera's arrow turns red and pulses, its tile grows an AI badge — and clicking that badge opens the AI Alert Modal: a chronological reel of the detection frames, with playback to watch the smoke develop, a magnifier to inspect it, and a one-click "Move Camera Here" to aim the real camera at the real spot.

The human is the judge; the modal is the evidence table. This lesson is how it's built.

Opening the case file

Everything starts with one store call. AIIndicatorBtn — the badge on tiles and the detail panel — calls openAiAlert(cameraId) on aiDetectionStore. The provider (context/AIAlertModalContext.tsx) watches aiAlertCameraId and springs into action:

  1. Fetches the detection frames via useAiDetection(cameraId)
  2. Joins the camera's SignalR group, so new frames stream in live while the modal is open — an unfolding detection updates before the operator's eyes
  3. Builds arrayOfImages — the frame URLs, oldest → newest, filtered by the image-type selector (all / 60° / 360°)

One context provider owns all of this; the component tree below it (AIAlertModalContent → header, slider, button bars) is deliberately dumb, reading everything from context. If you're changing modal behavior, you're almost certainly editing the context file, not a component.

The evidence reel

AiAlertSlider renders the frames in a Swiper carousel with three choices worth knowing:

  • It opens on the newest frame (arrayOfImages.length - 1) — the operator wants now first, history second — and shows a spinner until that newest image has actually preloaded.
  • Slides are virtual. Only a few DOM nodes exist at a time, so a long detection sequence (hundreds of frames) doesn't eat memory.
  • The magnifier is CSS, not canvas. When enabled, mouse position drives a 150 px lens whose background-position shifts across a 3×-scaled copy of the image. Cheap, sharp, no pixel copying.

Playback (AiAlertPlaybackControls) drives the same Swiper programmatically — a requestAnimationFrame loop advancing ~300 ms per frame across the last 25 frames, with play/pause, loop, a scrub slider, and a frame counter. Because arrayOfImages is oldest-first, playback naturally runs forward in time, ending on the newest frame.

"Move Camera Here"

The payoff button. moveCameraToAlertLocation() writes the detection's pan/tilt/zoom into cameraMovementStore as aiAlertMovementTarget and navigates to /?camera=<id>. From there the standard machinery takes over — the XYMouseTrackerOverlay watches that store value and executes the move, lease checks and all. The modal never talks to the PTZ API itself; it just posts a request on the bus like every other movement source.

Two layouts, one fullscreen trick

Collapsed mode shows the header up top and action buttons below the reel. Fullscreen swaps the bottom bar for AiModalFullscreenBtns, an overlay inside the image area wrapped in FadeAfterInactivityContainer — controls melt away while the operator studies frames and reappear on mouse movement. Fullscreen's overlay bar is hidden entirely on mobile and landscape, where swipe pagination replaces the arrow buttons (useMobilePagination = isMobile || isLandscape).

Who sees any of this

The entire AI surface is role-gated. Users without the AI role get no badges, no red arrows, no modal — see the arrow-state demo for the same gate on the map side.

Wrapping up

A detection flows: CONDOR → red badge → openAiAlert → context provider (fetch + SignalR join) → virtual-slide reel opening on the newest frame → optional playback and magnification → "Move Camera Here" posting to the movement bus. Evidence in, judgment out.

Next feature: the activity log — where operator actions become a shared record.