Skip to content

Commit ec7ba5a

Browse files
committed
docs: swap MotionStateMachineDiagram SVG for an AI-generated raster (test)
Test of using Nano Banana Pro for technical diagrams instead of hand-coded SVG. Built a tight ``diagram_spec`` JSON pinning every node label, every arrow label, every bullet, and every URL string; constraints set ``do_not_change_text: true`` and listed all 22 exact-text strings the model has to reproduce. Generated at 16:9, 2K, on a flat dark UI canvas (NOT photographic, NOT 3D). Output is honestly better than I predicted: - All 4 state nodes correct: Idle / Scoring / Fire event / Cooldown - All 4 subtitles correct: "waiting for frames", "scene > threshold?", "MotionEvent emitted" (camelCase preserved), "30s quiet window" - All 4 arrow labels correct including the ``≥`` symbol in "score ≥ threshold" - Center label "PER-CAMERA / Motion FSM" with correct letter-spacing - Side panel "Delivery" with three correctly-coloured bullets (green/amber/blue) and exact sublabels including the curly braces in "POST /cameras/{id}/motion" - Active states (Scoring, Fire event) carry an amber outer glow, default states stay flat — visual hierarchy preserved Implementation: - Generated via Nano Banana Pro from ``docs/image-specs/motion-fsm-diagram.json``. Spec uses the ``diagram_spec`` schema from the JSON-prompting skill, with explicit ``layout_lock: true``, ``allow_auto_routing: false``, and a ``rendering_notes`` field forbidding photographic / 3D / illustrative styling. - ``frontend/public/images/motion-fsm.{webp,jpg}`` — 1920x1080, 61 KB WebP / 187 KB JPEG. - ``MotionDetection.jsx``: removed the ``MotionStateMachineDiagram`` import, replaced the ``<MotionStateMachineDiagram />`` call with a ``<figure className="docs-diagram"><picture>…</picture><figcaption className="docs-diagram-caption">…</figcaption></figure>``. Reuses the existing ``.docs-diagram`` frame styling so the image sits in the same matte card the other in-page diagrams use. - New ``.docs-diagram-image`` class — full-width, auto height, small inner radius so the image's own grid background reads cleanly inside the figure's outer card. - The original ``MotionStateMachineDiagram`` React component in ``DocsDiagrams.jsx`` is intact and unused — easy revert by flipping the import + JSX back if the raster turns out to age badly under any rendering condition. Verification: ``npm run build`` clean (407ms). Visual confirmation on the live site after CI deploys. This is a *test* — if it reads as inaccurate or stylistically off in production, revert is one git command.
1 parent b2bafa0 commit ec7ba5a

5 files changed

Lines changed: 251 additions & 2 deletions

File tree

Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
{
2+
"diagram_spec": {
3+
"meta": {
4+
"spec_version": "1.0.0",
5+
"title": "Motion Detection State Machine",
6+
"description": "Per-camera FSM showing the Idle → Scoring → Fire event → Cooldown loop, with a side panel describing how the emitted MotionEvent is delivered.",
7+
"tags": ["motion", "state_machine", "fsm", "per_camera"]
8+
},
9+
"canvas": {
10+
"width": 1920,
11+
"height": 1080,
12+
"unit": "px",
13+
"direction": "circular",
14+
"background_color": "#0a0a0f",
15+
"style_notes": "Dark theme. All nodes are rounded rectangles with subtle borders. Crisp 1.5px arrows in amber. Use Inter or similar geometric sans-serif for labels. Apply a soft outer glow to active-state nodes (Scoring, Fire event) in amber. Subtle grid texture in deep teal-black background, very low contrast."
16+
},
17+
"semantics": {
18+
"diagram_type": "state_machine",
19+
"primary_relationship": "control_flow_loop",
20+
"swimlanes": []
21+
},
22+
"nodes": [
23+
{
24+
"id": "node_idle",
25+
"label": "Idle",
26+
"subtitle": "waiting for frames",
27+
"role": "process",
28+
"position": {"x": 960, "y": 280},
29+
"size": {"width": 220, "height": 90},
30+
"style": {
31+
"shape": "rounded_rectangle",
32+
"fill_color": "#161620",
33+
"border_color": "rgba(255,255,255,0.10)",
34+
"border_width": 1,
35+
"corner_radius": 12,
36+
"title_color": "#ffffff",
37+
"subtitle_color": "rgba(255,255,255,0.55)",
38+
"accent": "default"
39+
}
40+
},
41+
{
42+
"id": "node_scoring",
43+
"label": "Scoring",
44+
"subtitle": "scene > threshold?",
45+
"role": "process",
46+
"position": {"x": 1280, "y": 540},
47+
"size": {"width": 220, "height": 90},
48+
"style": {
49+
"shape": "rounded_rectangle",
50+
"fill_color": "rgba(245, 158, 11, 0.10)",
51+
"border_color": "rgba(245, 158, 11, 0.45)",
52+
"border_width": 1.5,
53+
"corner_radius": 12,
54+
"title_color": "#f5a209",
55+
"subtitle_color": "rgba(255,255,255,0.55)",
56+
"accent": "amber",
57+
"glow": "soft_amber"
58+
}
59+
},
60+
{
61+
"id": "node_fire",
62+
"label": "Fire event",
63+
"subtitle": "MotionEvent emitted",
64+
"role": "process",
65+
"position": {"x": 960, "y": 800},
66+
"size": {"width": 220, "height": 90},
67+
"style": {
68+
"shape": "rounded_rectangle",
69+
"fill_color": "rgba(245, 158, 11, 0.10)",
70+
"border_color": "rgba(245, 158, 11, 0.45)",
71+
"border_width": 1.5,
72+
"corner_radius": 12,
73+
"title_color": "#f5a209",
74+
"subtitle_color": "rgba(255,255,255,0.55)",
75+
"accent": "amber",
76+
"glow": "soft_amber"
77+
}
78+
},
79+
{
80+
"id": "node_cooldown",
81+
"label": "Cooldown",
82+
"subtitle": "30s quiet window",
83+
"role": "process",
84+
"position": {"x": 640, "y": 540},
85+
"size": {"width": 220, "height": 90},
86+
"style": {
87+
"shape": "rounded_rectangle",
88+
"fill_color": "#161620",
89+
"border_color": "rgba(255,255,255,0.10)",
90+
"border_width": 1,
91+
"corner_radius": 12,
92+
"title_color": "#ffffff",
93+
"subtitle_color": "rgba(255,255,255,0.55)",
94+
"accent": "default"
95+
}
96+
}
97+
],
98+
"center_label": {
99+
"position": {"x": 960, "y": 540},
100+
"eyebrow": "PER-CAMERA",
101+
"title": "Motion FSM",
102+
"eyebrow_color": "rgba(255,255,255,0.45)",
103+
"title_color": "#ffffff",
104+
"eyebrow_letter_spacing": "0.12em",
105+
"title_size": 18,
106+
"title_weight": "semibold"
107+
},
108+
"edges": [
109+
{
110+
"id": "e_idle_to_scoring",
111+
"from": "node_idle",
112+
"to": "node_scoring",
113+
"label": "frame in",
114+
"label_color": "rgba(255,255,255,0.65)",
115+
"label_position": "outside_curve_top_right",
116+
"style": {
117+
"line_type": "curved_clockwise",
118+
"stroke_color": "#f59e0b",
119+
"stroke_width": 1.5,
120+
"arrowhead": "filled_triangle",
121+
"curvature": "arc_around_center"
122+
}
123+
},
124+
{
125+
"id": "e_scoring_to_fire",
126+
"from": "node_scoring",
127+
"to": "node_fire",
128+
"label": "score ≥ threshold",
129+
"label_color": "rgba(255,255,255,0.65)",
130+
"label_position": "outside_curve_bottom_right",
131+
"style": {
132+
"line_type": "curved_clockwise",
133+
"stroke_color": "#f59e0b",
134+
"stroke_width": 1.5,
135+
"arrowhead": "filled_triangle",
136+
"curvature": "arc_around_center"
137+
}
138+
},
139+
{
140+
"id": "e_fire_to_cooldown",
141+
"from": "node_fire",
142+
"to": "node_cooldown",
143+
"label": "delivered",
144+
"label_color": "rgba(255,255,255,0.65)",
145+
"label_position": "outside_curve_bottom_left",
146+
"style": {
147+
"line_type": "curved_clockwise",
148+
"stroke_color": "#f59e0b",
149+
"stroke_width": 1.5,
150+
"arrowhead": "filled_triangle",
151+
"curvature": "arc_around_center"
152+
}
153+
},
154+
{
155+
"id": "e_cooldown_to_idle",
156+
"from": "node_cooldown",
157+
"to": "node_idle",
158+
"label": "elapsed",
159+
"label_color": "rgba(255,255,255,0.65)",
160+
"label_position": "outside_curve_top_left",
161+
"style": {
162+
"line_type": "curved_clockwise",
163+
"stroke_color": "#f59e0b",
164+
"stroke_width": 1.5,
165+
"arrowhead": "filled_triangle",
166+
"curvature": "arc_around_center"
167+
}
168+
}
169+
],
170+
"side_panel": {
171+
"position": {"x": 1640, "y": 360},
172+
"width": 220,
173+
"height": 360,
174+
"background": "#161620",
175+
"border_color": "rgba(255,255,255,0.10)",
176+
"border_radius": 12,
177+
"title": "Delivery",
178+
"title_color": "#ffffff",
179+
"title_size": 14,
180+
"title_weight": "semibold",
181+
"title_underline_color": "rgba(255,255,255,0.10)",
182+
"items": [
183+
{
184+
"bullet_color": "#22c55e",
185+
"label": "primary",
186+
"label_color": "#ffffff",
187+
"sublabel": "WebSocket — low latency",
188+
"sublabel_color": "rgba(255,255,255,0.55)"
189+
},
190+
{
191+
"bullet_color": "#f59e0b",
192+
"label": "fallback",
193+
"label_color": "#ffffff",
194+
"sublabel": "POST /cameras/{id}/motion",
195+
"sublabel_color": "rgba(255,255,255,0.55)"
196+
},
197+
{
198+
"bullet_color": "#3b82f6",
199+
"label": "consumed by",
200+
"label_color": "#ffffff",
201+
"sublabel": "dashboard + MCP agents",
202+
"sublabel_color": "rgba(255,255,255,0.55)"
203+
}
204+
]
205+
},
206+
"constraints": {
207+
"layout_lock": true,
208+
"allow_auto_routing": false,
209+
"exact_text": [
210+
"Idle", "waiting for frames",
211+
"Scoring", "scene > threshold?",
212+
"Fire event", "MotionEvent emitted",
213+
"Cooldown", "30s quiet window",
214+
"frame in", "score ≥ threshold", "delivered", "elapsed",
215+
"PER-CAMERA", "Motion FSM",
216+
"Delivery", "primary", "WebSocket — low latency",
217+
"fallback", "POST /cameras/{id}/motion",
218+
"consumed by", "dashboard + MCP agents"
219+
],
220+
"do_not_change_text": true,
221+
"do_not_misspell_anything": true,
222+
"rendering_notes": "Render as a clean technical diagram in a premium dark UI style. Crisp typography (Inter / system sans-serif), perfect alignment, no hand-drawn or rough look. Output should look like a high-end product-doc diagram, not an illustration. Aspect ratio 16:9. NO photographic imagery, NO 3D rendering, NO illustrative styling — pure flat technical diagram on a dark background."
223+
}
224+
}
225+
}
187 KB
Loading
60.1 KB
Loading

frontend/src/pages/docs/MotionDetection.jsx

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { MotionStateMachineDiagram } from "../../components/DocsDiagrams"
21
import { useDocs } from "./context"
32

43

@@ -23,7 +22,22 @@ function MotionDetection() {
2322
<li>The event is sent over the persistent WebSocket to Command Center. If the socket is down, it falls back to <code>POST /api/cameras/{"{id}"}/motion</code></li>
2423
<li>A per-camera cooldown timer prevents flapping (identical wind-blown tree, flickering light) from spamming events</li>
2524
</ol>
26-
<MotionStateMachineDiagram />
25+
<figure className="docs-diagram">
26+
<picture>
27+
<source srcSet="/images/motion-fsm.webp" type="image/webp" />
28+
<img
29+
src="/images/motion-fsm.jpg"
30+
alt="Motion detection state machine: Idle, Scoring, Fire event, Cooldown — looping clockwise. Side panel labelled Delivery shows three branches: primary (WebSocket — low latency), fallback (POST /cameras/{id}/motion), and consumed by (dashboard + MCP agents)."
31+
className="docs-diagram-image"
32+
width="1920"
33+
height="1080"
34+
loading="lazy"
35+
/>
36+
</picture>
37+
<figcaption className="docs-diagram-caption">
38+
The state machine runs once per camera. The cooldown prevents a waving branch or flickering light from hammering the events channel — tune the threshold to control sensitivity, the cooldown to control chatter.
39+
</figcaption>
40+
</figure>
2741

2842
<h3>Configuration</h3>
2943
<div className="docs-plans-table">

frontend/src/styles/landing.css

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1443,6 +1443,16 @@
14431443
overflow: hidden;
14441444
}
14451445

1446+
/* AI-generated diagram image — when a docs section ships a raster
1447+
diagram instead of the in-component SVG (see MotionDetection.jsx).
1448+
Behaves identically to the inline SVGs from a layout perspective. */
1449+
.docs-diagram-image {
1450+
width: 100%;
1451+
height: auto;
1452+
display: block;
1453+
border-radius: 8px;
1454+
}
1455+
14461456
.docs-diagram::before {
14471457
/* A faint horizontal accent bar at the top edge, matching the site's
14481458
gradient language without competing with the diagram itself. */

0 commit comments

Comments
 (0)