Fix Sasquatch spot results: honest count + collapse duplicates
Two bugs from "spots in bangkok" (said "Found 20" but showed 6, with repeats): 1. Misleading count: formatLocalResults hard-capped the visible list to 6 while the headline reported the full count. Now shows up to 10 and appends a truthful "…and N more — see them all on the map" line, so the number and the list always agree. 2. Duplicate spots: coordinate dedupe (kind:pubkey:d) can't catch the same real-world spot re-posted under a different identifier or by another account (e.g. "Yellow Park – Sukhumvit 50" appearing twice). Added deduplicateByIdentity() — a second-pass dedupe on a normalized name + geohash/location signature, keeping the freshest event — applied in findSpots so the count and list are clean. Both fixes flow through the AI tool-call path and the offline fallback path. Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
This commit is contained in:
parent
ada4bfee5e
commit
8b7d9ca4ea
2 changed files with 44 additions and 2 deletions
|
|
@ -414,7 +414,11 @@ function formatLocalResults(intent: LocalIntent, result: unknown): string {
|
||||||
return `No ${label} listed there yet 🦧 — be the first to add some at [skate-spots.org](https://skate-spots.org).`;
|
return `No ${label} listed there yet 🦧 — be the first to add some at [skate-spots.org](https://skate-spots.org).`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const lines = items.slice(0, 6).map((it) => {
|
// Show a generous slice, then honestly summarise the rest so the headline
|
||||||
|
// count and the visible list never disagree.
|
||||||
|
const MAX_SHOWN = 10;
|
||||||
|
const shown = items.slice(0, MAX_SHOWN);
|
||||||
|
const lines = shown.map((it) => {
|
||||||
const name = (it.name ?? it.title ?? it.role ?? 'Unnamed') as string;
|
const name = (it.name ?? it.title ?? it.role ?? 'Unnamed') as string;
|
||||||
const url = it.url as string | undefined;
|
const url = it.url as string | undefined;
|
||||||
const sub = (it.location ?? it.city ?? it.start ?? '') as string;
|
const sub = (it.location ?? it.city ?? it.start ?? '') as string;
|
||||||
|
|
@ -429,7 +433,13 @@ function formatLocalResults(intent: LocalIntent, result: unknown): string {
|
||||||
intent.tool === 'find_crews' ? '👥' :
|
intent.tool === 'find_crews' ? '👥' :
|
||||||
intent.tool === 'find_shops' ? '🏪' :
|
intent.tool === 'find_shops' ? '🏪' :
|
||||||
intent.tool === 'find_tours' ? '🗺️' : '🛍️';
|
intent.tool === 'find_tours' ? '🗺️' : '🛍️';
|
||||||
return `${emoji} Found **${items.length}** ${label}:\n${lines}`;
|
|
||||||
|
const remaining = items.length - shown.length;
|
||||||
|
const more = remaining > 0
|
||||||
|
? `\n\n…and **${remaining}** more — [see them all on the map](https://skate-spots.org/map) 🗺️`
|
||||||
|
: '';
|
||||||
|
|
||||||
|
return `${emoji} Found **${items.length}** ${label}:\n${lines}${more}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -182,6 +182,34 @@ function deduplicateAddressable(events: { pubkey: string; kind: number; tags: st
|
||||||
return Array.from(seen.values());
|
return Array.from(seen.values());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Normalise a string for fuzzy matching: lowercase, trim, collapse whitespace. */
|
||||||
|
function normalizeForDedupe(s: string | undefined): string {
|
||||||
|
return (s ?? '').toLowerCase().replace(/\s+/g, ' ').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collapse spots that are the SAME real-world spot published as different
|
||||||
|
* addressable events — e.g. the same place re-posted under a new `d` identifier
|
||||||
|
* or by two different accounts. Coordinate dedupe (kind:pubkey:d) can't catch
|
||||||
|
* these because the coordinates differ, so we also dedupe on a
|
||||||
|
* name + location/geohash signature and keep the freshest event.
|
||||||
|
*/
|
||||||
|
function deduplicateByIdentity<T extends { tags: string[][]; created_at: number }>(spots: T[]): T[] {
|
||||||
|
const seen = new Map<string, T>();
|
||||||
|
for (const e of spots) {
|
||||||
|
const name = normalizeForDedupe(getTag(e, 'title') ?? getTag(e, 'name'));
|
||||||
|
// Prefer geohash (precise) then location text as the place signature.
|
||||||
|
const place = normalizeForDedupe(getTag(e, 'g') ?? getTag(e, 'location') ?? getTag(e, 'city'));
|
||||||
|
// No name → can't confidently match; keep it (unique key on coordinate).
|
||||||
|
const key = name ? `${name}|${place}` : `__unique__${buildNaddr(e as unknown as { kind: number; pubkey: string; tags: string[][] })}`;
|
||||||
|
const existing = seen.get(key);
|
||||||
|
if (!existing || e.created_at > existing.created_at) {
|
||||||
|
seen.set(key, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Array.from(seen.values());
|
||||||
|
}
|
||||||
|
|
||||||
function buildNaddr(event: { kind: number; pubkey: string; tags: string[][] }): string {
|
function buildNaddr(event: { kind: number; pubkey: string; tags: string[][] }): string {
|
||||||
return `${event.kind}:${event.pubkey}:${getTag(event, 'd') ?? ''}`;
|
return `${event.kind}:${event.pubkey}:${getTag(event, 'd') ?? ''}`;
|
||||||
}
|
}
|
||||||
|
|
@ -215,6 +243,10 @@ export function useCVMClient() {
|
||||||
e.tags.some((t) => t[0] === 't' && t[1] === 'skatespot')
|
e.tags.some((t) => t[0] === 't' && t[1] === 'skatespot')
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Collapse the same real-world spot re-posted under a different id/author
|
||||||
|
// (coordinate dedupe above can't catch those). Keeps the freshest copy.
|
||||||
|
spots = deduplicateByIdentity(spots as typeof spots & { tags: string[][]; created_at: number }[]);
|
||||||
|
|
||||||
if (params.difficulty && params.difficulty !== 'all') {
|
if (params.difficulty && params.difficulty !== 'all') {
|
||||||
spots = spots.filter(
|
spots = spots.filter(
|
||||||
(e) => getTag(e, 'difficulty')?.toLowerCase() === params.difficulty
|
(e) => getTag(e, 'difficulty')?.toLowerCase() === params.difficulty
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue