fix(voice): localize the area, imagery and OSM cards and tidy OSM Places

From review:
- Card lines the actions write in English (area basis, part counts, merged
  duplicates, OSM caveats, the imagery window) are shown in the UI
  language through the analyst card's note path; the model still gets the
  English. One OSM place reads "1 hospital".
- Same-named clarification choices show their region code (WA, DC).
- OSM Places shows as unavailable once the server says no Overpass is
  configured, and its points are depth-tested past 2.5 km so they never
  show through the globe.
- find_imagery puts the operator's sources and mode back when a box is
  refused, and a voice search cancelled mid-flight leaves "Search
  cancelled" instead of an empty strip.
- The README lists OSM Places with the other Utilities.
This commit is contained in:
Manjunath committed 2026-11-03 19:36:48 +05:30
1 parent 34647758fc
commit 40d8839589
22 files changed
+535 -27

No files matched your search

+4
View File
@@ -268,6 +268,8 @@ export async function findImagery(
: args.source === 'any'
? { hls: true, viirs: true }
: { hls: true };
// The operator's own settings, put back if the box is refused.
const before = layer.getSnapshot();
layer.setSources(sources);
layer.setMode?.('image');
@@ -279,6 +281,8 @@ export async function findImagery(
if (!accepted) {
const reason =
layer.getSnapshot().boxError || 'That area cannot be searched.';
if (before?.sources) layer.setSources(before.sources);
if (before?.mode) layer.setMode?.(before.mode);
return fail('BOX_REFUSED', `${reason}. Zoom in or pick a smaller area.`, {
...(areaName ? { area: areaName } : {}),
});
+13
View File
@@ -47,6 +47,8 @@ function fakeLayer({
pins: { a: { key: null } },
shown: { a: null },
notes: [],
sources: { hls: false, viirs: true },
mode: 'basemap',
};
const listeners = new Set();
const calls = [];
@@ -92,9 +94,11 @@ function fakeLayer({
},
setSources(sources) {
calls.push(['setSources', sources]);
set({ sources: { ...sources } });
},
setMode(mode) {
calls.push(['setMode', mode]);
set({ mode });
},
setAssignment(slot, key) {
calls.push(['setAssignment', slot, key]);
@@ -271,6 +275,15 @@ test('a box the layer refuses is refused with its reason, and the layer is turne
assert.deepEqual(toggles, [true]);
});
test('a refused box leaves the operator panel sources and mode as they were', async () => {
const layer = fakeLayer({ refuse: 'Box is 1,200 km wide · limit 1,000 km' });
const result = await findImagery(deps(layer).value, {});
assert.equal(result.code, 'BOX_REFUSED');
const snap = layer.getSnapshot();
assert.deepEqual(snap.sources, { hls: false, viirs: true });
assert.equal(snap.mode, 'basemap');
});
test('a superseded search is cancelled before anything is pinned', async () => {
const layer = fakeLayer();
let current = true;
+2
View File
@@ -211,6 +211,7 @@ export async function osmQuery(
ok: true,
action: 'osm_query',
kind: preset.label,
kindSingular: singularLabel(preset.label),
count,
complete: false,
countScope: 'bounding-box',
@@ -290,6 +291,7 @@ export async function osmQuery(
ok: true,
action: 'osm_query',
kind: preset.label,
kindSingular: singularLabel(preset.label),
count,
complete,
area: areaName,
+80 -12
View File
@@ -293,6 +293,14 @@ const FIXED_NOTES = new Map(
msg(
'the count is a lower bound because at least one layer reached its retained limit.',
),
// Area, imagery and OSM card lines (areaActions, imageryActions,
// osmActions); the model gets the English.
msg('buffer sized to the place'),
msg('enclosing mapped area'),
msg('the map was cleared while looking it up'),
msg('OpenStreetMap coverage varies by place'),
msg('the catalog covers the last 30 days'),
msg('there are more'),
].map((text) => [text, text]),
);
@@ -302,7 +310,58 @@ const LAYER_STATUS_NOTES = Object.freeze({
error: (label) => t('{label} error', { label }),
});
const countOf = (text) => Number(String(text).replace(/,/g, ''));
const NOTE_PATTERNS = [
[
/^([\d,]+) m around the landmark$/,
([, radius]) =>
t('{radius} m around the landmark', {
radius: formatNumber(countOf(radius)),
}),
],
[
/^([\d,]+) parts$/,
([, n]) =>
tn(countOf(n), '{count} part', '{count} parts', {
count: formatNumber(countOf(n)),
}),
],
[
/^([\d,]+) of ([\d,]+) parts drawn$/,
([, drawn, total]) =>
tn(
countOf(total),
'{drawn} of {total} part drawn',
'{drawn} of {total} parts drawn',
{
drawn: formatNumber(countOf(drawn)),
total: formatNumber(countOf(total)),
},
),
],
[
/^([\d,]+) duplicate mappings merged$/,
([, n]) =>
tn(
countOf(n),
'{count} duplicate mapping merged',
'{count} duplicate mappings merged',
{ count: formatNumber(countOf(n)) },
),
],
[
/^listed the first ([\d,]+)$/,
([, limit]) =>
t('listed the first {limit}', { limit: formatNumber(countOf(limit)) }),
],
[
/^counted over the bounding box of (.+), not its outline$/,
([, area]) =>
t('counted over the bounding box of {area}, not its outline', {
area: spokenLabel(area, 48),
}),
],
[
/^counted the first ([\d,]+) loaded records$/,
([, returned]) =>
@@ -373,6 +432,11 @@ const NOTE_PATTERNS = [
],
];
/** A card line from a producer's English text, or null when absent. */
function shownLine(text) {
return text ? shownNote(String(text)) : null;
}
function shownNote(text) {
if (FIXED_NOTES.has(text)) return t(FIXED_NOTES.get(text));
for (const [pattern, show] of NOTE_PATTERNS) {
@@ -465,18 +529,18 @@ function areaCard(result, args = {}) {
title: `${result.name ? spokenLabel(result.name, 48) : t('Area', { context: 'voice area' })}${result.country ? `, ${spokenLabel(result.country, 24)}` : ''}`,
lines: [
Number.isFinite(km2) && km2 > 0 ? `${formatNumber(km2)} km²` : null,
display.basis,
display.parts,
display.drawnParts,
shownLine(display.basis),
shownLine(display.parts),
shownLine(display.drawnParts),
result.notDrawn
? t('Not drawn: {parts}', { parts: result.notDrawn })
? t('Not drawn: {parts}', { parts: shownNote(result.notDrawn) })
: null,
].filter(Boolean),
chips: [
...(result.approximate ? [{ label: t('approximate') }] : []),
...(result.partial ? [{ label: shownFeedState('partial') }] : []),
],
notes: notesOf(display.caveat),
notes: notesOf(display.caveat).map(shownNote),
sources: display.source ? [{ label: display.source }] : [],
},
referents: [],
@@ -509,7 +573,7 @@ function imageryCard(result) {
total: formatNumber(Number(result.total) || 0),
},
),
capitalized(display.window),
capitalized(shownLine(display.window)),
]
: [
tn(
@@ -518,7 +582,7 @@ function imageryCard(result) {
'{n} acquisitions searched',
{ n: formatNumber(Number(result.total) || 0) },
),
capitalized(display.window),
capitalized(shownLine(display.window)),
];
return {
display: {
@@ -528,7 +592,7 @@ function imageryCard(result) {
lines: lines.filter(Boolean),
chips:
shown && !shown.onMap ? [{ label: shownFeedState('loading') }] : [],
notes: notesOf(display.caveat),
notes: notesOf(display.caveat).map(shownNote),
},
referents: [],
};
@@ -538,8 +602,12 @@ function osmCard(result) {
if (!result?.ok) return null;
const display = result.display || {};
const count = Number(result.count);
// The kind is the preset's English noun, the model's data.
const kind = spokenLabel(result.kind || 'places', 32);
// The kind is the preset's English noun, the model's data; one place takes
// its singular ("1 hospital").
const kind = spokenLabel(
(count === 1 && result.kindSingular) || result.kind || 'places',
32,
);
const values = { count: formatNumber(count), kind };
const title = !Number.isFinite(count)
? t('Unknown number of {kind}', { kind })
@@ -555,9 +623,9 @@ function osmCard(result) {
result.area
? t('In {area}', { area: spokenLabel(result.area, 48) })
: null,
display.merged,
shownLine(display.merged),
].filter(Boolean),
notes: notesOf(display.caveat),
notes: notesOf(display.caveat).map(shownNote),
sources: display.source ? [{ label: display.source }] : [],
},
referents: Array.isArray(result.referents) ? result.referents : [],
@@ -280,3 +280,66 @@ test('every locationId the voice tools accept is a preset city', () => {
assert.ok(ids.size > 0, 'found the locationId enums');
for (const id of ids) assert.ok(Object.hasOwn(CITY_POIS, id), id);
});
test('area, imagery and OSM card lines are shown in the UI language', () => {
const area = {
ok: true,
name: 'Ferry Building',
approximate: true,
display: {
basis: '250 m around the landmark',
parts: '3 parts',
drawnParts: '2 of 3 parts drawn',
source: 'approximate outline',
},
notDrawn: 'the map was cleared while looking it up',
};
const areaLines = inLocale('de', () => presentResult('resolve_area', area).display.lines);
assert.ok(areaLines.includes('250 m um das Wahrzeichen'), areaLines.join(' | '));
assert.ok(areaLines.includes('Teile: 3'));
assert.ok(areaLines.includes('Gezeichnete Teile: 2 von 3'));
assert.ok(areaLines.some((line) => line.includes('die Karte wurde während der Suche geleert')));
const osm = {
ok: true,
kind: 'hospitals',
kindSingular: 'hospital',
count: 1,
complete: false,
area: 'Kathmandu',
display: {
source: 'OpenStreetMap',
merged: '2 duplicate mappings merged',
caveat: 'listed the first 500; there are more',
},
};
const osmCard = inLocale('de', () => presentResult('osm_query', osm).display);
assert.match(osmCard.title, /\bhospital\b/);
assert.doesNotMatch(osmCard.title, /hospitals/, 'one place takes the singular');
assert.equal(presentResult('osm_query', { ...osm, complete: true }).display.title, '1 hospital');
assert.ok(osmCard.lines.includes('Zusammengeführte Doppelkartierungen: 2'));
assert.deepEqual(osmCard.notes, ['die ersten 500 aufgelistet', 'es gibt weitere']);
const imagery = {
ok: true,
total: 4,
count: 0,
display: { window: 'the catalog covers the last 30 days' },
};
const imageryLines = inLocale('de', () => presentResult('find_imagery', imagery).display.lines);
assert.ok(imageryLines.includes('Der Katalog umfasst die letzten 30 Tage'), imageryLines.join(' | '));
});
test('same-named clarification choices show their region code', () => {
const asked = {
ok: false,
needsClarification: true,
code: 'AMBIGUOUS_AREA',
candidates: [
{ candidateId: 'ne:state:united-states-of-america-washington-wa', name: 'Washington', level: 'admin1', region: 'WA', country: 'United States of America' },
{ candidateId: 'ne:state:united-states-of-america-washington-dc', name: 'Washington', level: 'admin1', region: 'DC', country: 'United States of America' },
],
};
const lines = presentResult('resolve_area', asked, { query: 'Washington' }).display.lines;
assert.match(lines[0], /\bWA\b/);
assert.match(lines[1], /\bDC\b/);
assert.notEqual(lines[0], lines[1]);
});