From 7f65a3287763c1d28fb767536c4956ec03965a94 Mon Sep 17 00:00:00 2001 From: Said Turksever Date: Fri, 25 Sep 2026 17:51:58 +0100 Subject: [PATCH] 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 --- README.md | 7 +++++++ docs/CURRENT-STATE.md | 3 ++- scripts/qa-mapillary.mjs | 14 ++++---------- src/layers/mapillary/index.js | 3 ++- src/mapillaryMarkup.test.mjs | 6 +++--- src/ui/applicationShell.js | 1 - src/ui/mapillaryControls.js | 8 ++------ src/ui/mapillaryPresentation.js | 8 +++----- src/ui/mapillaryPresentation.test.mjs | 12 ++++++++---- src/ui/styles/mapillary.css | 25 ------------------------- src/ui/templates/layer-panels.html | 4 ---- 11 files changed, 31 insertions(+), 60 deletions(-) diff --git a/README.md b/README.md index 44859f908..59495b637 100644 --- a/README.md +++ b/README.md @@ -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. +**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. ## 🎖️ Field Missions diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index 690d5ce51..ba056bbc1 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -2697,7 +2697,8 @@ its criteria cannot be silently ignored. Street Level (Mapillary) lives in the right context rail next to CCTV as an 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, 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 diff --git a/scripts/qa-mapillary.mjs b/scripts/qa-mapillary.mjs index 36a1888d6..a862e3bd1 100644 --- a/scripts/qa-mapillary.mjs +++ b/scripts/qa-mapillary.mjs @@ -108,7 +108,7 @@ async function main() { classes: [...el.classList], width: Math.round(box.width), right: Math.round(box.right), - keyless: !document.getElementById('mly-keyless').hidden, + status: document.getElementById('mly-status').textContent, controlsDisabled: document.getElementById('mly-controls').disabled, bodyDisplay: getComputedStyle(document.getElementById('mly-body')) .display, @@ -152,7 +152,7 @@ async function main() { ); if (!status.configured) { await step( - 'keyless install gates the controls and names the token', + 'keyless install gates the controls and reports KEY REQUIRED', async () => { await page.evaluate(() => window.__godsEyeView.dataManager.setEnabled('mapillary', true, { @@ -161,14 +161,8 @@ async function main() { ); await sleep(800); const info = await panel(); - assert.equal(info.keyless, true); assert.equal(info.controlsDisabled, true); - assert.match( - await page.evaluate( - () => document.getElementById('mly-keyless').textContent, - ), - /MAPILLARY_CLIENT_TOKEN/, - ); + assert.equal(info.status, 'KEY REQUIRED'); }, ); console.log( @@ -217,7 +211,7 @@ async function main() { { timeout: 90_000 }, ); 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. await page.waitForFunction( () => document.body.innerHTML.includes('Mapillary contributors'), diff --git a/src/layers/mapillary/index.js b/src/layers/mapillary/index.js index 9cd211b5b..3e84c05bf 100644 --- a/src/layers/mapillary/index.js +++ b/src/layers/mapillary/index.js @@ -180,7 +180,8 @@ export function createMapillaryLayer({ source, services = {} }) { return { enabled: state.enabled, 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, coverage: { zoom: state.coverage.zoom, diff --git a/src/mapillaryMarkup.test.mjs b/src/mapillaryMarkup.test.mjs index 935453017..981a4d6ab 100644 --- a/src/mapillaryMarkup.test.mjs +++ b/src/mapillaryMarkup.test.mjs @@ -63,9 +63,9 @@ test('panel styles stay inside GEV conventions: no !important, no fixed panel', assert.deepEqual(fixed, ['.mly-viewer-wrap-expanded']); }); -test('keyless and no-planner states are real, gated UI rather than dead buttons', () => { - assert.match(html, /id="mly-keyless"[^>]*hidden/); - assert.match(html, /MAPILLARY_CLIENT_TOKEN/); +test('keyless and no-planner states gate the controls rather than leave dead buttons', () => { + // The key requirement is documented (README, .env.example), not repeated in the panel. + assert.doesNotMatch(html, /mly-keyless/); assert.match(html, /
]*role="alert"/); diff --git a/src/ui/applicationShell.js b/src/ui/applicationShell.js index da1df6fe8..155ac0e1c 100644 --- a/src/ui/applicationShell.js +++ b/src/ui/applicationShell.js @@ -900,7 +900,6 @@ export class StyleManager extends ShellFacade { }), setPanelCollapsed: (collapsed, options) => this.setPanelCollapsed('mapillary-panel', collapsed, options), - openKeySetup: () => document.getElementById('key-setup-chip')?.click(), showToast: (message) => this._showToast(message), }, }); diff --git a/src/ui/mapillaryControls.js b/src/ui/mapillaryControls.js index ce42e0379..1c3653b22 100644 --- a/src/ui/mapillaryControls.js +++ b/src/ui/mapillaryControls.js @@ -36,8 +36,6 @@ export class MapillaryControls { const byId = (id) => this.root?.querySelector(`#${id}`) || null; return { status: byId('mly-status'), - keyless: byId('mly-keyless'), - keylessBtn: byId('mly-keyless-btn'), controls: byId('mly-controls'), enableBtn: byId('mly-enable-btn'), lookBtn: byId('mly-look-btn'), @@ -82,7 +80,6 @@ export class MapillaryControls { if (!this.root) return; this.mapillary.attachViewerHost?.(el.viewer); - this.listen(el.keylessBtn, 'click', () => this.actions.openKeySetup?.()); this.listen(el.enableBtn, 'click', () => this._toggleEnabled()); this.listen(el.lookBtn, 'click', async () => { if (!(await this._ensureEnabled())) return; @@ -306,7 +303,7 @@ export class MapillaryControls { const view = presentMapillaryPanel(state); this._view = view; this._renderHeader(view); - this._renderEmptyState(view); + this._renderGate(view); this._renderQuery(view); this._renderResults(view); this._renderFilters(view); @@ -331,9 +328,8 @@ export class MapillaryControls { } } - _renderEmptyState(view) { + _renderGate(view) { const el = this._elements; - if (el.keyless) el.keyless.hidden = !view.keyless.visible; if (el.controls) el.controls.disabled = view.controlsDisabled; } diff --git a/src/ui/mapillaryPresentation.js b/src/ui/mapillaryPresentation.js index eeb9482aa..a0166c1cf 100644 --- a/src/ui/mapillaryPresentation.js +++ b/src/ui/mapillaryPresentation.js @@ -153,7 +153,9 @@ function presentMeta(state) { export function presentMapillaryPanel(state) { const enabled = state.enabled === 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 error = state.query.error || state.street.error || null; let answer = state.query.answer || ''; @@ -167,10 +169,6 @@ export function presentMapillaryPanel(state) { enabled, keyRequired, 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, enableButton: { text: enabled ? 'STREET LEVEL ON' : 'STREET LEVEL OFF', diff --git a/src/ui/mapillaryPresentation.test.mjs b/src/ui/mapillaryPresentation.test.mjs index a96963a4f..d7cfcbb82 100644 --- a/src/ui/mapillaryPresentation.test.mjs +++ b/src/ui/mapillaryPresentation.test.mjs @@ -86,10 +86,8 @@ function deepMerge(target, source) { 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 })); - assert.equal(view.keyless.visible, true); - assert.match(view.keyless.text, /MAPILLARY_CLIENT_TOKEN/); assert.equal(view.controlsDisabled, true); 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.match(view.query.hint, /Anthropic key/); assert.ok(view.query.placeholder.length < 40, 'placeholder fits the field'); - assert.equal(view.keyless.visible, 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', () => { const planning = presentMapillaryPanel( snapshot({ enabled: true, query: { busy: true, stage: 'planning' } }), diff --git a/src/ui/styles/mapillary.css b/src/ui/styles/mapillary.css index aed38ac55..57a3ce7ab 100644 --- a/src/ui/styles/mapillary.css +++ b/src/ui/styles/mapillary.css @@ -110,31 +110,6 @@ 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 ───────────────────────────────────────────────────────────────── */ .mly-query { display: flex; diff --git a/src/ui/templates/layer-panels.html b/src/ui/templates/layer-panels.html index 39467f4cb..93a544dd6 100644 --- a/src/ui/templates/layer-panels.html +++ b/src/ui/templates/layer-panels.html @@ -85,10 +85,6 @@
-