ui(mapillary): drop the keyless empty state

The panel no longer shows a separate keyless call-to-action; the fieldset
gate and the layer-row tooltip already cover the missing-key case. The
planner flag is tri-state (null until status answers).

Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
Said TurkseverandClaude Fable 5.1 committed 2026-09-25 17:51:58 +01:00
1 parent 10a00873ca
commit 7f65a32877
11 files changed
+31 -60

No files matched your search

+7
View File
@@ -330,6 +330,13 @@ _The Space Missions layer replaying a Falcon 9 ascent — labeled `RECONSTRUCTED
> Needs a free **Mapillary client token**; the plain-English query box additionally needs an **Anthropic key**. Both go in POWER UP. > Needs a free **Mapillary client token**; the plain-English query box additionally needs an **Anthropic key**. Both go in POWER UP.
**Getting the keys**
1. **Mapillary client token (required, free).** Sign in at [mapillary.com/dashboard/developers](https://www.mapillary.com/dashboard/developers), choose **Register application**, give it any name and website, tick **Read** access, and copy the **Client Token** (it starts with `MLY|`). Paste it into **POWER UP → Mapillary**, or set `MAPILLARY_CLIENT_TOKEN` in `.env` and restart `npm run dev`.
2. **Anthropic key (optional).** Create one at [console.anthropic.com](https://console.anthropic.com/settings/keys) and add it in **POWER UP → Anthropic** or as `ANTHROPIC_API_KEY` in `.env`. It stays on the server; only the plain-English query box uses it.
Without the Mapillary token the STREET LEVEL panel reads **KEY REQUIRED** and its controls stay greyed out; without the Anthropic key everything except the query box works.
Turn on **Street Level (Mapillary)** under DATA LAYERS → Cameras, or open the STREET LEVEL strip in the right rail. Mapillary's coverage draws as ground-hugging lines (recent captures bright green, older ones dim, 360° panoramas magenta); click a line for its image cones and a cone to open the photo in the embedded MapillaryJS viewer. **EXPAND** grows the viewer to most of the screen and **CAMERA FOLLOWS VIEW** drives the globe camera from the panorama. With an Anthropic key, type a request such as _"stop signs in downtown Detroit"_ and the matching detections stream onto the globe tile by tile with Mapillary's own icons; click one to see the photo with the detection outlined. Mapillary blurs faces and licence plates before publishing imagery, and the globe credits "© Mapillary contributors, CC BY-SA 4.0" while the layer is on. Turn on **Street Level (Mapillary)** under DATA LAYERS → Cameras, or open the STREET LEVEL strip in the right rail. Mapillary's coverage draws as ground-hugging lines (recent captures bright green, older ones dim, 360° panoramas magenta); click a line for its image cones and a cone to open the photo in the embedded MapillaryJS viewer. **EXPAND** grows the viewer to most of the screen and **CAMERA FOLLOWS VIEW** drives the globe camera from the panorama. With an Anthropic key, type a request such as _"stop signs in downtown Detroit"_ and the matching detections stream onto the globe tile by tile with Mapillary's own icons; click one to see the photo with the detection outlined. Mapillary blurs faces and licence plates before publishing imagery, and the globe credits "© Mapillary contributors, CC BY-SA 4.0" while the layer is on.
## 🎖️ Field Missions ## 🎖️ Field Missions
+2 -1
View File
@@ -2697,7 +2697,8 @@ its criteria cannot be silently ignored.
Street Level (Mapillary) lives in the right context rail next to CCTV as an Street Level (Mapillary) lives in the right context rail next to CCTV as an
ordinary collapsible panel and starts collapsed. Without `MAPILLARY_CLIENT_TOKEN` ordinary collapsible panel and starts collapsed. Without `MAPILLARY_CLIENT_TOKEN`
the panel shows a gated empty state naming the variable; with it, coverage the header status reads KEY REQUIRED and the controls stay disabled (key setup
is documented in the README's Street Level section); with it, coverage
draws as ground-clamped sequence lines (z0–5 overview points from orbit, draws as ground-clamped sequence lines (z0–5 overview points from orbit,
z11–14 sequences below 60 km) and the proxy strips the unused `image` point z11–14 sequences below 60 km) and the proxy strips the unused `image` point
layer from z14 tiles in transit (12 MB → ~80 KB). The plain-English query box layer from z14 tiles in transit (12 MB → ~80 KB). The plain-English query box
+4 -10
View File
@@ -108,7 +108,7 @@ async function main() {
classes: [...el.classList], classes: [...el.classList],
width: Math.round(box.width), width: Math.round(box.width),
right: Math.round(box.right), right: Math.round(box.right),
keyless: !document.getElementById('mly-keyless').hidden, status: document.getElementById('mly-status').textContent,
controlsDisabled: document.getElementById('mly-controls').disabled, controlsDisabled: document.getElementById('mly-controls').disabled,
bodyDisplay: getComputedStyle(document.getElementById('mly-body')) bodyDisplay: getComputedStyle(document.getElementById('mly-body'))
.display, .display,
@@ -152,7 +152,7 @@ async function main() {
); );
if (!status.configured) { if (!status.configured) {
await step( await step(
'keyless install gates the controls and names the token', 'keyless install gates the controls and reports KEY REQUIRED',
async () => { async () => {
await page.evaluate(() => await page.evaluate(() =>
window.__godsEyeView.dataManager.setEnabled('mapillary', true, { window.__godsEyeView.dataManager.setEnabled('mapillary', true, {
@@ -161,14 +161,8 @@ async function main() {
); );
await sleep(800); await sleep(800);
const info = await panel(); const info = await panel();
assert.equal(info.keyless, true);
assert.equal(info.controlsDisabled, true); assert.equal(info.controlsDisabled, true);
assert.match( assert.equal(info.status, 'KEY REQUIRED');
await page.evaluate(
() => document.getElementById('mly-keyless').textContent,
),
/MAPILLARY_CLIENT_TOKEN/,
);
}, },
); );
console.log( console.log(
@@ -217,7 +211,7 @@ async function main() {
{ timeout: 90_000 }, { timeout: 90_000 },
); );
const info = await panel(); const info = await panel();
assert.equal(info.keyless, false); assert.equal(info.controlsDisabled, false);
// Cesium paints on-screen credits a frame or two after they register. // Cesium paints on-screen credits a frame or two after they register.
await page.waitForFunction( await page.waitForFunction(
() => document.body.innerHTML.includes('Mapillary</a> contributors'), () => document.body.innerHTML.includes('Mapillary</a> contributors'),
+2 -1
View File
@@ -180,7 +180,8 @@ export function createMapillaryLayer({ source, services = {} }) {
return { return {
enabled: state.enabled, enabled: state.enabled,
keyRequired: state.keyRequired, keyRequired: state.keyRequired,
planner: state.status?.planner === true, // null until the status call answers (the layer initialises lazily).
planner: state.status ? state.status.planner === true : null,
plannerModel: state.status?.plannerModel || null, plannerModel: state.status?.plannerModel || null,
coverage: { coverage: {
zoom: state.coverage.zoom, zoom: state.coverage.zoom,
+3 -3
View File
@@ -63,9 +63,9 @@ test('panel styles stay inside GEV conventions: no !important, no fixed panel',
assert.deepEqual(fixed, ['.mly-viewer-wrap-expanded']); assert.deepEqual(fixed, ['.mly-viewer-wrap-expanded']);
}); });
test('keyless and no-planner states are real, gated UI rather than dead buttons', () => { test('keyless and no-planner states gate the controls rather than leave dead buttons', () => {
assert.match(html, /id="mly-keyless"[^>]*hidden/); // The key requirement is documented (README, .env.example), not repeated in the panel.
assert.match(html, /MAPILLARY_CLIENT_TOKEN/); assert.doesNotMatch(html, /mly-keyless/);
assert.match(html, /<fieldset id="mly-controls"/); assert.match(html, /<fieldset id="mly-controls"/);
assert.match(html, /<ul id="mly-legend"/); assert.match(html, /<ul id="mly-legend"/);
assert.match(html, /id="mly-error"[^>]*role="alert"/); assert.match(html, /id="mly-error"[^>]*role="alert"/);
-1
View File
@@ -900,7 +900,6 @@ export class StyleManager extends ShellFacade {
}), }),
setPanelCollapsed: (collapsed, options) => setPanelCollapsed: (collapsed, options) =>
this.setPanelCollapsed('mapillary-panel', collapsed, options), this.setPanelCollapsed('mapillary-panel', collapsed, options),
openKeySetup: () => document.getElementById('key-setup-chip')?.click(),
showToast: (message) => this._showToast(message), showToast: (message) => this._showToast(message),
}, },
}); });
+2 -6
View File
@@ -36,8 +36,6 @@ export class MapillaryControls {
const byId = (id) => this.root?.querySelector(`#${id}`) || null; const byId = (id) => this.root?.querySelector(`#${id}`) || null;
return { return {
status: byId('mly-status'), status: byId('mly-status'),
keyless: byId('mly-keyless'),
keylessBtn: byId('mly-keyless-btn'),
controls: byId('mly-controls'), controls: byId('mly-controls'),
enableBtn: byId('mly-enable-btn'), enableBtn: byId('mly-enable-btn'),
lookBtn: byId('mly-look-btn'), lookBtn: byId('mly-look-btn'),
@@ -82,7 +80,6 @@ export class MapillaryControls {
if (!this.root) return; if (!this.root) return;
this.mapillary.attachViewerHost?.(el.viewer); this.mapillary.attachViewerHost?.(el.viewer);
this.listen(el.keylessBtn, 'click', () => this.actions.openKeySetup?.());
this.listen(el.enableBtn, 'click', () => this._toggleEnabled()); this.listen(el.enableBtn, 'click', () => this._toggleEnabled());
this.listen(el.lookBtn, 'click', async () => { this.listen(el.lookBtn, 'click', async () => {
if (!(await this._ensureEnabled())) return; if (!(await this._ensureEnabled())) return;
@@ -306,7 +303,7 @@ export class MapillaryControls {
const view = presentMapillaryPanel(state); const view = presentMapillaryPanel(state);
this._view = view; this._view = view;
this._renderHeader(view); this._renderHeader(view);
this._renderEmptyState(view); this._renderGate(view);
this._renderQuery(view); this._renderQuery(view);
this._renderResults(view); this._renderResults(view);
this._renderFilters(view); this._renderFilters(view);
@@ -331,9 +328,8 @@ export class MapillaryControls {
} }
} }
_renderEmptyState(view) { _renderGate(view) {
const el = this._elements; const el = this._elements;
if (el.keyless) el.keyless.hidden = !view.keyless.visible;
if (el.controls) el.controls.disabled = view.controlsDisabled; if (el.controls) el.controls.disabled = view.controlsDisabled;
} }
+3 -5
View File
@@ -153,7 +153,9 @@ function presentMeta(state) {
export function presentMapillaryPanel(state) { export function presentMapillaryPanel(state) {
const enabled = state.enabled === true; const enabled = state.enabled === true;
const keyRequired = state.keyRequired === true; const keyRequired = state.keyRequired === true;
const planner = state.planner === true; // Unknown (status not fetched yet) reads as available: the server answers
// a missing key with its own error, and the panel must not claim one is missing.
const planner = state.planner !== false;
const busy = state.query.busy === true; const busy = state.query.busy === true;
const error = state.query.error || state.street.error || null; const error = state.query.error || state.street.error || null;
let answer = state.query.answer || ''; let answer = state.query.answer || '';
@@ -167,10 +169,6 @@ export function presentMapillaryPanel(state) {
enabled, enabled,
keyRequired, keyRequired,
status: presentStatus(state), status: presentStatus(state),
keyless: {
visible: keyRequired,
text: 'Street Level needs a free Mapillary client token (MAPILLARY_CLIENT_TOKEN). Coverage, photos and queries stay off until one is added.',
},
controlsDisabled: keyRequired, controlsDisabled: keyRequired,
enableButton: { enableButton: {
text: enabled ? 'STREET LEVEL ON' : 'STREET LEVEL OFF', text: enabled ? 'STREET LEVEL ON' : 'STREET LEVEL OFF',
+8 -4
View File
@@ -86,10 +86,8 @@ function deepMerge(target, source) {
return out; return out;
} }
test('a missing Mapillary token gates every control and names the variable', () => { test('a missing Mapillary token gates every control and flags KEY REQUIRED', () => {
const view = presentMapillaryPanel(snapshot({ keyRequired: true })); const view = presentMapillaryPanel(snapshot({ keyRequired: true }));
assert.equal(view.keyless.visible, true);
assert.match(view.keyless.text, /MAPILLARY_CLIENT_TOKEN/);
assert.equal(view.controlsDisabled, true); assert.equal(view.controlsDisabled, true);
assert.deepEqual(view.status, { text: 'KEY REQUIRED', tone: 'warn' }); assert.deepEqual(view.status, { text: 'KEY REQUIRED', tone: 'warn' });
}); });
@@ -103,10 +101,16 @@ test('without an Anthropic key the query box explains itself and stays disabled'
assert.equal(view.query.hintWarn, true); assert.equal(view.query.hintWarn, true);
assert.match(view.query.hint, /Anthropic key/); assert.match(view.query.hint, /Anthropic key/);
assert.ok(view.query.placeholder.length < 40, 'placeholder fits the field'); assert.ok(view.query.placeholder.length < 40, 'placeholder fits the field');
assert.equal(view.keyless.visible, false);
assert.equal(view.controlsDisabled, false); assert.equal(view.controlsDisabled, false);
}); });
test('before the status call answers the query box does not claim a missing key', () => {
const view = presentMapillaryPanel(snapshot({ planner: null }));
assert.equal(view.query.inputDisabled, false);
assert.equal(view.query.hintWarn, false);
assert.doesNotMatch(view.query.placeholder, /Anthropic/);
});
test('a running query turns ASK into STOP and shows indeterminate progress per stage', () => { test('a running query turns ASK into STOP and shows indeterminate progress per stage', () => {
const planning = presentMapillaryPanel( const planning = presentMapillaryPanel(
snapshot({ enabled: true, query: { busy: true, stage: 'planning' } }), snapshot({ enabled: true, query: { busy: true, stage: 'planning' } }),
-25
View File
@@ -110,31 +110,6 @@
pointer-events: none; pointer-events: none;
} }
/* ── Keyless empty state ─────────────────────────────────────────────────── */
.mly-keyless {
display: flex;
flex-direction: column;
gap: 8px;
padding: 10px 12px;
border: 1px solid rgba(255, 179, 0, 0.45);
border-radius: 9px;
background: rgba(255, 179, 0, 0.06);
}
.mly-keyless-text {
margin: 0;
font-family: var(--font-sans);
font-size: 0.72rem;
line-height: 1.4;
color: var(--text-primary);
}
.mly-keyless-text code {
font-family: var(--font-mono);
font-size: 0.66rem;
color: #ffb300;
}
/* ── Query ───────────────────────────────────────────────────────────────── */ /* ── Query ───────────────────────────────────────────────────────────────── */
.mly-query { .mly-query {
display: flex; display: flex;
-4
View File
@@ -85,10 +85,6 @@
<button class="panel-collapse-btn" data-collapse-target="mapillary-panel" title="Expand panel">◀</button> <button class="panel-collapse-btn" data-collapse-target="mapillary-panel" title="Expand panel">◀</button>
</div> </div>
<div id="mly-body" class="mly-body"> <div id="mly-body" class="mly-body">
<div id="mly-keyless" class="mly-keyless" hidden>
<p class="mly-keyless-text">Street Level needs a free Mapillary client token (<code>MAPILLARY_CLIENT_TOKEN</code>). Coverage, photos and queries stay off until one is added.</p>
<button id="mly-keyless-btn" class="scene-btn" type="button">ADD KEY IN POWER UP</button>
</div>
<fieldset id="mly-controls" class="mly-controls"> <fieldset id="mly-controls" class="mly-controls">
<div class="cctv-controls"> <div class="cctv-controls">
<button id="mly-enable-btn" class="scene-btn" type="button" aria-pressed="false" title="Turn the Street Level layer on or off">STREET LEVEL OFF</button> <button id="mly-enable-btn" class="scene-btn" type="button" aria-pressed="false" title="Turn the Street Level layer on or off">STREET LEVEL OFF</button>