fix: CVM server tag mapping + add city/country geocoding to spots
CVM server fixes (cvm-server/server.ts): - Use 'title' tag instead of 'name' to read spot names (app publishes 'title') - Search now checks 'title' tag (not just 'name') for keyword matches - City filter now also checks 'title' tag and event content - Country filter also checks event content - Features filter now checks 't' tags (app uses 't' not 'feature') - Validate spots with 't: skatespot' tag (same rule as the app) - Return 'location' field and proper features list in results - Image parsing now supports both 'image' (legacy) and 'imeta' (NIP-92) CreateSpotPage fix (src/pages/CreateSpotPage.tsx): - Add reverse geocoding via Nominatim on spot submit - Emit structured 'city' and 'country' tags so CVM can filter by them - Auto-fill location field from geocoding if user left it blank SasquatchGuide improvement (src/components/SasquatchGuide.tsx): - Add data model notes to system prompt explaining tag structure - Instruct AI to handle 0-result responses gracefully (encourage adding spots) - Use limit:20+ for city/country searches Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
This commit is contained in:
parent
252163625e
commit
84aa009b33
3 changed files with 77 additions and 11 deletions
|
|
@ -131,11 +131,15 @@ mcpServer.registerTool(
|
|||
},
|
||||
},
|
||||
async ({ city, country, difficulty, features, search, limit }) => {
|
||||
const filters: Filter[] = [{ kinds: [30818], limit: 500 }];
|
||||
|
||||
const raw = await queryNostr(filters);
|
||||
// Query all kind 30818 events; also try with #t filter for relays that support it
|
||||
const raw = await queryNostr([{ kinds: [30818], limit: 500 }]);
|
||||
let spots = deduplicateAddressable(raw);
|
||||
|
||||
// Only keep valid skate spots (must have 't: skatespot' tag — same rule as the app)
|
||||
spots = spots.filter((e) =>
|
||||
e.tags.some((t: string[]) => t[0] === "t" && t[1] === "skatespot")
|
||||
);
|
||||
|
||||
if (difficulty && difficulty !== "all") {
|
||||
spots = spots.filter(
|
||||
(e) => getTag(e, "difficulty")?.toLowerCase() === difficulty
|
||||
|
|
@ -146,8 +150,13 @@ mcpServer.registerTool(
|
|||
const q = city.toLowerCase();
|
||||
spots = spots.filter(
|
||||
(e) =>
|
||||
// 'city' dedicated tag (if present)
|
||||
(getTag(e, "city") ?? "").toLowerCase().includes(q) ||
|
||||
// 'location' free-text field (most spots have this)
|
||||
(getTag(e, "location") ?? "").toLowerCase().includes(q) ||
|
||||
// 'title' tag (spot name may include city)
|
||||
(getTag(e, "title") ?? "").toLowerCase().includes(q) ||
|
||||
// Event content / description
|
||||
e.content.toLowerCase().includes(q)
|
||||
);
|
||||
}
|
||||
|
|
@ -157,13 +166,18 @@ mcpServer.registerTool(
|
|||
spots = spots.filter(
|
||||
(e) =>
|
||||
(getTag(e, "country") ?? "").toLowerCase().includes(q) ||
|
||||
(getTag(e, "location") ?? "").toLowerCase().includes(q)
|
||||
(getTag(e, "location") ?? "").toLowerCase().includes(q) ||
|
||||
e.content.toLowerCase().includes(q)
|
||||
);
|
||||
}
|
||||
|
||||
if (features && features.length > 0) {
|
||||
spots = spots.filter((e) => {
|
||||
const spotFeatures = getTags(e, "feature").map((f) => f.toLowerCase());
|
||||
// Check 'feature' tags AND generic 't' tags (app uses 't' for features)
|
||||
const spotFeatures = [
|
||||
...getTags(e, "feature"),
|
||||
...getTags(e, "t"),
|
||||
].map((f) => f.toLowerCase());
|
||||
const spotContent = e.content.toLowerCase();
|
||||
return features.some(
|
||||
(f) =>
|
||||
|
|
@ -177,6 +191,8 @@ mcpServer.registerTool(
|
|||
const q = search.toLowerCase();
|
||||
spots = spots.filter(
|
||||
(e) =>
|
||||
// App publishes 'title' tag (not 'name') — check both for compatibility
|
||||
(getTag(e, "title") ?? "").toLowerCase().includes(q) ||
|
||||
(getTag(e, "name") ?? "").toLowerCase().includes(q) ||
|
||||
e.content.toLowerCase().includes(q)
|
||||
);
|
||||
|
|
@ -186,14 +202,24 @@ mcpServer.registerTool(
|
|||
spots = spots.slice(0, limit!);
|
||||
|
||||
const result = spots.map((e) => ({
|
||||
name: getTag(e, "name") ?? "Unnamed Spot",
|
||||
// App uses 'title' tag; fall back to 'name' for legacy events
|
||||
name: getTag(e, "title") ?? getTag(e, "name") ?? "Unnamed Spot",
|
||||
description: e.content.slice(0, 300),
|
||||
city: getTag(e, "city") ?? getTag(e, "c"),
|
||||
country: getTag(e, "country"),
|
||||
location: getTag(e, "location"),
|
||||
difficulty: getTag(e, "difficulty"),
|
||||
features: getTags(e, "feature"),
|
||||
category: getTag(e, "category"),
|
||||
// App uses 't' tags for features (street, park, rails, etc.)
|
||||
features: getTags(e, "t").filter((t) => t !== "skatespot"),
|
||||
geohash: getTag(e, "g"),
|
||||
image: getTag(e, "image") ?? getTag(e, "thumb"),
|
||||
// Support both 'image' (legacy) and 'imeta' (NIP-92, new)
|
||||
image: getTag(e, "image") ??
|
||||
(() => {
|
||||
const imeta = e.tags.find((t: string[]) => t[0] === "imeta");
|
||||
const urlPart = imeta?.find((p: string) => p.startsWith("url "));
|
||||
return urlPart ? urlPart.replace("url ", "") : undefined;
|
||||
})(),
|
||||
author_pubkey: e.pubkey,
|
||||
naddr: buildNaddr(e),
|
||||
url: `https://skate-spots.org/spot/${e.pubkey}/${getTag(e, "d") ?? ""}`,
|
||||
|
|
|
|||
|
|
@ -95,6 +95,12 @@ Args: city?(string), country?(string), difficulty?("beginner"|"intermediate"|"ad
|
|||
Example: {"tool":"find_spots","args":{"difficulty":"beginner","limit":5}}
|
||||
Example: {"tool":"find_spots","args":{"city":"Bangkok","search":"street"}}
|
||||
|
||||
IMPORTANT — Data model notes for find_spots:
|
||||
- Spots are filtered by: 'city' tag OR 'location' free-text (which may contain city name) OR spot description OR spot title
|
||||
- If city filter returns 0 results, it means no spots have been added in that city yet — tell the user that and encourage them to add spots via the map
|
||||
- The community is growing — 0 results means "none yet", not a server error
|
||||
- Always use limit:20 or higher to get broader results when searching by city/country
|
||||
|
||||
### find_events
|
||||
Find upcoming skateboarding events.
|
||||
Args: city?(string), country?(string), event_type?(string), from_date?(string YYYY-MM-DD), to_date?(string YYYY-MM-DD), limit?(number)
|
||||
|
|
@ -123,13 +129,15 @@ Example: {"tool":"get_community_stats","args":{}}
|
|||
- goskatespots.nostr1.com = primary SkateSpots relay (allowlisted)
|
||||
- The Lab = marketplace for coaches, filmers, video editors
|
||||
- Everything is free, no ads, no tracking 🛹
|
||||
- The community is growing — encourage users to add spots they know!
|
||||
|
||||
## Rules
|
||||
1. For ANY question about spots, events, coaches, gear, or stats → use a CVM tool
|
||||
2. Output the JSON tool call on its own line, nothing else — do not explain what you're doing
|
||||
3. For app/protocol questions → answer directly from your knowledge
|
||||
4. Be concise, skater-friendly, use emoji sparingly
|
||||
5. Never make up spot names, event dates, or user data`;
|
||||
5. Never make up spot names, event dates, or user data
|
||||
6. If results come back empty (count: 0), explain honestly that no spots have been added there yet and encourage the user to add them via the map at skate-spots.org/map`;
|
||||
|
||||
// ─── Detect if a question needs live CVM data ─────────────────────────────────
|
||||
|
||||
|
|
|
|||
|
|
@ -215,6 +215,30 @@ export default function CreateSpotPage() {
|
|||
.replace(/^-|-$/g, '')
|
||||
.slice(0, 50) + '-' + Date.now().toString(36);
|
||||
|
||||
// Reverse geocode to get structured city/country tags
|
||||
// This powers the CVM server's city/country filtering
|
||||
let cityTag = '';
|
||||
let countryTag = '';
|
||||
let locationStr = location.trim();
|
||||
try {
|
||||
const geoResp = await fetch(
|
||||
`https://nominatim.openstreetmap.org/reverse?lat=${lat}&lon=${lng}&format=json&zoom=10`,
|
||||
{ headers: { 'Accept-Language': 'en', 'User-Agent': 'SkateSpots/1.0' } }
|
||||
);
|
||||
if (geoResp.ok) {
|
||||
const geoData = await geoResp.json();
|
||||
const addr = geoData.address ?? {};
|
||||
cityTag = addr.city ?? addr.town ?? addr.village ?? addr.county ?? '';
|
||||
countryTag = addr.country ?? '';
|
||||
// If user didn't fill in location, auto-populate from geocoding
|
||||
if (!locationStr && cityTag) {
|
||||
locationStr = [cityTag, addr.state, countryTag].filter(Boolean).join(', ');
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Geocoding is best-effort — don't block spot creation
|
||||
}
|
||||
|
||||
// Build event tags
|
||||
const eventTags: string[][] = [
|
||||
['d', identifier], // Required for addressable events
|
||||
|
|
@ -226,8 +250,16 @@ export default function CreateSpotPage() {
|
|||
['lon', lng.toString()],
|
||||
];
|
||||
|
||||
if (location.trim()) {
|
||||
eventTags.push(['location', location.trim()]);
|
||||
if (locationStr) {
|
||||
eventTags.push(['location', locationStr]);
|
||||
}
|
||||
|
||||
// Structured city/country tags — critical for CVM server search
|
||||
if (cityTag) {
|
||||
eventTags.push(['city', cityTag]);
|
||||
}
|
||||
if (countryTag) {
|
||||
eventTags.push(['country', countryTag]);
|
||||
}
|
||||
|
||||
if (contactInfo.trim()) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue