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:
German Khokhlov committed 2026-10-30 12:37:43 +04:00
1 parent 423ca3ca5f
commit 563df3bad9
7 files changed
+386 -56

No files matched your search

+102 -23
View File
@@ -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;
}