Files
gods-eye-view/src/data/pickedContext.js
T
Bilawal Sidhu a19897ad15 feat(voice): voice core — honest layer reach, spoken summary with on-screen card, narration, point-and-ask
- Voice layer manifest (src/voice/layerManifest.js) generates the layer enums,
  aliases and analyst field hints; a test fails when a catalog layer has no entry.
- analyst_query validates fields, operators, units, values and scopes, counts up
  to 250,000 loaded records per layer, reports complete/partial/unanswered, tells
  LAYER_OFF / NOT_READY / FEED_UNAVAILABLE apart from zero, refuses mismatched
  follow-ups and never lets a superseded query replace follow-up memory. Seven
  more layers expose analyst records.
- Tool results carry a code-built say line and a display card; the voice card in
  the mic control shows captions, plan steps and the result
  (src/voice/resultDisplay.js is the one adapter). Slow tools narrate their
  current step out of band; holding Space while the assistant talks cuts it off.
  Turn spans go to the debug log; caption transcription is metered; the log
  omits what was said unless GEV_VOICE_LOG_CONTENT=1.
- Point and ask: the Space-down pointer snapshot resolves "this", "that one" and
  "around here" through 'pointer' targets, and "the second one" through
  referent:n from the last numbered list.
- Prompt states each policy once (instructions + tools 48.5k -> 46.7k chars).
- Voice benchmark harness (scripts/qa-voice-bench.mjs, scripts/voice-bench/)
  shares its phrase table and grading with scripts/qa-voice-routing.mjs.
2026-09-28 21:19:23 -05:00

129 lines
3.9 KiB
JavaScript

import { getContextStore, isContextRecordActive } from './contextStore.js';
import { findPickOwner, resolvePickId } from './pickRegistry.js';
/**
* Resolve a `scene.pick()` result to the identity a person would name: which
* layer, which record, and its display label.
*
* Two lanes, in order:
* 1. Context-store carriers. Entity-backed layers (infrastructure GeoJSON,
* selected vessels, fires, installations) stamp `__gevContextId` on the
* picked object, and the store record already carries the label and
* position voice uses everywhere else.
* 2. Pick owners. Primitive-backed layers (aircraft, satellites, vessels)
* register an ownership predicate in `pickRegistry`; the owning layer's
* `findByQuery` turns the pick id into a live descriptor.
*
* Trails claim their pick ids only to make clicks inert, so they never
* resolve. Disabled layers never resolve: what the user cannot see is not
* what they are pointing at.
*/
const KIND_BY_LAYER = Object.freeze({
flights: 'aircraft',
military: 'aircraft',
'local-adsb': 'aircraft',
'ais-live-vessels': 'vessel',
satellites: 'satellite',
'local-firms': 'fire',
});
const IGNORED_OWNERS = new Set(['trails']);
function text(value) {
return String(value ?? '').trim();
}
function finite(value) {
if (value === null || value === undefined || value === '') return null;
const number = Number(value);
return Number.isFinite(number) ? number : null;
}
/** Kind of thing a layer holds, for deixis ("this aircraft", "this place"). */
export function pickedKindForLayer(layerId) {
return KIND_BY_LAYER[layerId] || 'feature';
}
function contextCarrierRecord(picked, dataManager) {
const store = getContextStore();
for (const carrier of [
picked?.id,
picked?.primitive?.id,
picked?.primitive,
]) {
const contextId =
carrier && typeof carrier === 'object' ? carrier.__gevContextId : null;
if (!contextId) continue;
const record = store.entities.get(contextId);
if (record && isContextRecordActive(record, dataManager)) return record;
}
return null;
}
function fromRecord(record) {
const layerId = record.layerId || null;
const id =
layerId === 'ais-live-vessels' && record.properties?.mmsi
? String(record.properties.mmsi)
: String(record.id);
return {
layerId,
id,
label:
text(record.label) ||
text(record.properties?.name) ||
text(record.layerName) ||
id,
kind: pickedKindForLayer(layerId),
lat: finite(record.latitude),
lon: finite(record.longitude),
};
}
function descriptorLabel(found, fallback) {
return (
text(found?.callsign) ||
text(found?.registration) ||
text(found?.name) ||
text(found?.icao24) ||
text(found?.mmsi) ||
text(found?.noradId) ||
fallback
);
}
/**
* @param {object|null} picked Result of `scene.pick()`.
* @param {object} [options]
* @param {object} [options.dataManager] Layer lifecycle (enabled state, modules).
* @returns {{layerId: string, id: string, label: string, kind: string, lat: number|null, lon: number|null, altitudeM?: number|null}|null}
*/
export function resolvePickedContext(picked, { dataManager = null } = {}) {
if (!picked) return null;
const record = contextCarrierRecord(picked, dataManager);
if (record) return fromRecord(record);
const pickId = resolvePickId(picked);
if (!pickId) return null;
const layerId = findPickOwner(pickId);
if (!layerId || IGNORED_OWNERS.has(layerId)) return null;
if (dataManager?.isEnabled && !dataManager.isEnabled(layerId)) return null;
const layer = dataManager?.layers?.get?.(layerId);
let found = null;
try {
found = layer?.module?.findByQuery?.(pickId) || null;
} catch {
found = null;
}
return {
layerId,
id: pickId,
label: descriptorLabel(found, text(layer?.name) || pickId),
kind: pickedKindForLayer(layerId),
lat: finite(found?.latitude),
lon: finite(found?.longitude),
altitudeM: finite(found?.altitudeM),
};
}