mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-14 15:22:21 +00:00
fix(hud): AI summary in the UI language, canonical feed state, partial aggregation
The HUD sends its locale with the summary context; the proxy names that
language in the instructions and the input, English when missing or
unsupported. The model returns {summary, feedState}; the HUD shows the
summary only when feedState matches its own state for a view that is not
nominal, and appends its own translated state tag.
partial joins the shared feed-state table between fallback and nominal,
so one partial layer no longer reads off and partial plus nominal no
longer reads nominal.
This commit is contained in:
1 parent
423ca3ca5f
commit
563df3bad9
7 files changed
+386
-56
No files matched your search
+102
-23
@@ -1,7 +1,9 @@
|
||||
import {
|
||||
SNAPSHOT_FEED_STATES,
|
||||
feedProvenanceEnvelope,
|
||||
layerSnapshots,
|
||||
} from './data/layerSnapshot.js';
|
||||
import { DEFAULT_LOCALE, normalizeLocale } from './i18n/locale.js';
|
||||
|
||||
export const HUD_SUMMARY_UNCONFIGURED_CODE = 'OPENAI_NOT_CONFIGURED';
|
||||
|
||||
@@ -43,19 +45,83 @@ export function isHudSummaryUnconfigured(status, data) {
|
||||
}
|
||||
|
||||
/**
|
||||
* HUD summary model instructions. The five-word line may only describe
|
||||
* supplied labels, and must carry feed-state when any enabled layer is not
|
||||
* nominal.
|
||||
* HUD summary model instructions. The five words may only describe supplied
|
||||
* labels; the feed state comes back as its own canonical field, so the check
|
||||
* does not depend on the summary's language or wording.
|
||||
*/
|
||||
export const HUD_SUMMARY_INSTRUCTIONS = [
|
||||
"Write one concise intelligence-HUD summary for God's Eye View.",
|
||||
'Use only the supplied place, street, nearby-place, enabled-layer labels, and feedProvenance.',
|
||||
'Prefer the clearest named place and include a relevant enabled layer only when useful.',
|
||||
'If any enabled layer is not nominal, the five words MUST include that feedState token (STALE, DEGRADED, FALLBACK, LOADING, or UNAVAILABLE) and must not present the view as live.',
|
||||
'Set feedState to feedProvenance.overall exactly as given, or none when it is null.',
|
||||
'If feedProvenance.overall is not nominal, the summary must not present the view as live or healthy.',
|
||||
'Do not infer from coordinates or invent a place or feed state.',
|
||||
'Output exactly five words with no title, punctuation, markdown, or introductory phrase.',
|
||||
'The summary is exactly five words with no title, punctuation, markdown, or introductory phrase.',
|
||||
].join(' ');
|
||||
|
||||
/** feedState values the model may return; none stands for a null overall. */
|
||||
export const HUD_SUMMARY_FEED_STATES = Object.freeze([
|
||||
...SNAPSHOT_FEED_STATES,
|
||||
'none',
|
||||
]);
|
||||
|
||||
/** Responses API structured output: the five words plus the canonical state. */
|
||||
export const HUD_SUMMARY_RESPONSE_FORMAT = Object.freeze({
|
||||
type: 'json_schema',
|
||||
name: 'hud_summary',
|
||||
strict: true,
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
summary: { type: 'string' },
|
||||
feedState: { type: 'string', enum: [...HUD_SUMMARY_FEED_STATES] },
|
||||
},
|
||||
required: ['summary', 'feedState'],
|
||||
additionalProperties: false,
|
||||
},
|
||||
});
|
||||
|
||||
/** A shipped locale for the request, English when missing or unsupported. */
|
||||
export function hudSummaryLocale(value) {
|
||||
return normalizeLocale(value) || DEFAULT_LOCALE;
|
||||
}
|
||||
|
||||
/** The language a request's summary is written in, named in English. */
|
||||
function summaryLanguage(tag) {
|
||||
try {
|
||||
return new Intl.DisplayNames(['en'], { type: 'language' }).of(tag) || tag;
|
||||
} catch {
|
||||
return tag;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The instructions for one request: English as before, otherwise one more
|
||||
* sentence naming the language the five words are written in.
|
||||
* @param {unknown} locale The `locale` the HUD sent.
|
||||
* @returns {string}
|
||||
*/
|
||||
export function hudSummaryInstructions(locale) {
|
||||
const tag = hudSummaryLocale(locale);
|
||||
if (tag === DEFAULT_LOCALE) return HUD_SUMMARY_INSTRUCTIONS;
|
||||
const language = summaryLanguage(tag);
|
||||
return `${HUD_SUMMARY_INSTRUCTIONS} Write the summary in ${language} (${tag}), not English, giving places and layers their usual ${language} names; feedState stays the English value.`;
|
||||
}
|
||||
|
||||
/**
|
||||
* The model input: the HUD context, led by the summary language when it is
|
||||
* not English. A small model follows the language far more reliably when it
|
||||
* is part of the input as well as the instructions.
|
||||
* @param {object} context The HUD summary context.
|
||||
* @returns {string}
|
||||
*/
|
||||
export function hudSummaryInput(context) {
|
||||
const tag = hudSummaryLocale(context?.locale);
|
||||
const json = JSON.stringify(context ?? {});
|
||||
if (tag === DEFAULT_LOCALE) return json;
|
||||
return `Summary language: ${summaryLanguage(tag)} (${tag}).\n${json}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stamp HUD summary context with the same layer snapshots voice tools use.
|
||||
* @param {Array<object>} [layers] `DataLayerManager.getAll()` rows.
|
||||
@@ -83,32 +149,45 @@ export function hudSummaryLayerContext(layers = [], options) {
|
||||
}
|
||||
|
||||
/**
|
||||
* The feed state and up to two layer names for the HUD telemetry suffix when
|
||||
* an enabled feed is not nominal. Values stay English (`state` is a feed-state
|
||||
* id, `names` are `layer.name`); the HUD translates them for display.
|
||||
* The feed state and up to two layer names of a provenance envelope that is
|
||||
* not nominal, or null. Values stay English (`state` is a feed-state id,
|
||||
* `names` are `layer.name`); the HUD translates them for display.
|
||||
* @param {{ overall?: string|null, layers?: Array<object> }|null} envelope
|
||||
* @returns {{ state: string, names: string[] }|null}
|
||||
*/
|
||||
export function provenanceTag(envelope) {
|
||||
const overall = envelope?.overall;
|
||||
if (!overall || overall === 'nominal' || overall === 'off') return null;
|
||||
const names = (envelope.layers || [])
|
||||
.filter(
|
||||
(s) => s.feedState && s.feedState !== 'nominal' && s.feedState !== 'off',
|
||||
)
|
||||
.map((s) => String(s.name || s.id || 'LAYER'))
|
||||
.slice(0, 2);
|
||||
return { state: overall, names };
|
||||
}
|
||||
|
||||
/**
|
||||
* The feed-state tag for the HUD telemetry suffix when an enabled feed is not
|
||||
* nominal.
|
||||
* @param {Array<object>} [layers]
|
||||
* @param {{ now?: number }} [options]
|
||||
* @returns {{ state: string, names: string[] }|null}
|
||||
*/
|
||||
export function hudTelemetryProvenance(layers = [], options) {
|
||||
const snapshots = layerSnapshots(layers, options).filter((s) => s.enabled);
|
||||
const envelope = feedProvenanceEnvelope(snapshots, options);
|
||||
if (!envelope.overall || envelope.overall === 'nominal') return null;
|
||||
const names = envelope.layers
|
||||
.filter(
|
||||
(s) => s.feedState && s.feedState !== 'nominal' && s.feedState !== 'off',
|
||||
)
|
||||
.map((s) => String(s.name || s.id || 'LAYER'))
|
||||
.slice(0, 2);
|
||||
return { state: envelope.overall, names };
|
||||
return provenanceTag(feedProvenanceEnvelope(snapshots, options));
|
||||
}
|
||||
|
||||
/** Require the supplied non-nominal state before showing an AI summary. */
|
||||
export function hudSummaryMatchesProvenance(summary, provenance) {
|
||||
/**
|
||||
* Show an AI summary only when its canonical feedState is the HUD's own: a
|
||||
* view that is not nominal falls back on a missing or different state.
|
||||
* @param {{ feedState?: string|null }|null} response The proxy's reply.
|
||||
* @param {{ overall?: string|null }|null} provenance The context's envelope.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function hudSummaryMatchesProvenance(response, provenance) {
|
||||
const state = provenance?.overall;
|
||||
if (!state || state === 'nominal' || state === 'off') return true;
|
||||
return String(summary || '')
|
||||
.toUpperCase()
|
||||
.split(/\W+/)
|
||||
.includes(String(state).toUpperCase());
|
||||
return response?.feedState === state;
|
||||
}
|
||||
Reference in new issue
Block a user