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:
- Fetches the detection frames via
useAiDetection(cameraId) - 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
- 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-positionshifts 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).
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.