CVM find_crews: Sasquatch can now live-search crews
- useCVMClient: add findCrews (queries kind 35006 across data relays, filters by city/country/vibe/search, returns CrewResult with member_count, vibes, url); add crews to getCommunityStats + CrewResult type - SasquatchGuide: register find_crews tool in system prompt + switch handler, add crew keywords to needsLiveData, update app-facts (crews are searchable + mention cross-linking and Zap-the-Crew) Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
This commit is contained in:
parent
f881c6513f
commit
dec2e730d2
2 changed files with 110 additions and 3 deletions
|
|
@ -170,6 +170,12 @@ Example: {"tool":"find_coaches","args":{"location":"Bangkok","limit":20}}
|
||||||
Args: category?(string), condition?("new"|"like_new"|"good"|"fair"|"any"), max_price_sats?(number), search?(string), limit?(number)
|
Args: category?(string), condition?("new"|"like_new"|"good"|"fair"|"any"), max_price_sats?(number), search?(string), limit?(number)
|
||||||
Example: {"tool":"find_gear","args":{"category":"deck"}}
|
Example: {"tool":"find_gear","args":{"category":"deck"}}
|
||||||
|
|
||||||
|
### find_crews
|
||||||
|
Find skate crews (community groups). Args: city?(string), country?(string), vibe?(string e.g. "street"|"transition"|"diy"), search?(string), limit?(number)
|
||||||
|
- For country queries use the main city too (Thailand → city:"Bangkok")
|
||||||
|
- Always use limit:20+ for city/country searches
|
||||||
|
Example: {"tool":"find_crews","args":{"city":"Barcelona","limit":20}}
|
||||||
|
|
||||||
### get_community_stats
|
### get_community_stats
|
||||||
Args: none
|
Args: none
|
||||||
Example: {"tool":"get_community_stats","args":{}}
|
Example: {"tool":"get_community_stats","args":{}}
|
||||||
|
|
@ -183,7 +189,7 @@ Example: {"tool":"get_weather","args":{"city":"Barcelona"}}
|
||||||
## Rules
|
## Rules
|
||||||
1. Data questions (spots/events/coaches/gear/stats) → ALWAYS use a tool, never guess
|
1. Data questions (spots/events/coaches/gear/stats) → ALWAYS use a tool, never guess
|
||||||
2. Tool call = ONE JSON object on its own line, nothing else before or after
|
2. Tool call = ONE JSON object on its own line, nothing else before or after
|
||||||
3. App/protocol/how-to questions → answer directly (no tool). Crews, Stash, Wallet, Tours, Shops, Supporters have NO live search tool — answer these from the App facts below and point users to the right page.
|
3. App/protocol/how-to questions → answer directly (no tool). Stash, Wallet, Tours, Shops, Supporters have NO live search tool — answer these from the App facts below and point users to the right page. (Crews DO have find_crews — use it for "find/show crews".)
|
||||||
4. Be concise, skater-friendly
|
4. Be concise, skater-friendly
|
||||||
5. Never invent spot names, event dates, or user data
|
5. Never invent spot names, event dates, or user data
|
||||||
6. 0 results = honest "none listed yet" + encourage adding content
|
6. 0 results = honest "none listed yet" + encourage adding content
|
||||||
|
|
@ -192,7 +198,7 @@ Example: {"tool":"get_weather","args":{"city":"Barcelona"}}
|
||||||
- Nostr = decentralized protocol; your cryptographic keys ARE your identity — one login works everywhere, nobody can ban you
|
- Nostr = decentralized protocol; your cryptographic keys ARE your identity — one login works everywhere, nobody can ban you
|
||||||
- Map (the homepage) = discover spots, shops, gear, tours, events and crews worldwide. Toggle layers via the Layers control. Add a spot with the + button.
|
- Map (the homepage) = discover spots, shops, gear, tours, events and crews worldwide. Toggle layers via the Layers control. Add a spot with the + button.
|
||||||
- The Lab = hire skate-scene pros: Coaches, Filmers, Photographers, Video Editors, and ramp/park/DIY Builders. Providers can be an individual OR a collective (crew/studio) — collective members confirm with their own key (green = confirmed, amber = pending).
|
- The Lab = hire skate-scene pros: Coaches, Filmers, Photographers, Video Editors, and ramp/park/DIY Builders. Providers can be an individual OR a collective (crew/studio) — collective members confirm with their own key (green = confirmed, amber = pending).
|
||||||
- Crews = your map-anchored skate family (who you skate with, NOT for hire). A crew has home turf on the map (teal pins), home spots, members, vibes and a manifesto. Create one at /crews → "Start a Crew". Crews reuse the same member-confirmation trust as Lab collectives.
|
- Crews = your map-anchored skate family (who you skate with, NOT for hire). A crew has home turf on the map (teal pins), home spots, members, vibes and a manifesto. Create one at /crews → "Start a Crew". Crews reuse the same member-confirmation trust as Lab collectives. Use find_crews to search them. A crew can cross-link its Lab service listings, and you can "Zap the Crew" to split a tip across confirmed members.
|
||||||
- Events = find/join skate events & comps (map layer + Events page)
|
- Events = find/join skate events & comps (map layer + Events page)
|
||||||
- Tours = guided local/national/world skate tours
|
- Tours = guided local/national/world skate tours
|
||||||
- Skate Shops = local shops on the map
|
- Skate Shops = local shops on the map
|
||||||
|
|
@ -219,6 +225,7 @@ function needsLiveData(question: string): boolean {
|
||||||
'events in', 'events near', 'upcoming', 'coaches in', 'filmers in',
|
'events in', 'events near', 'upcoming', 'coaches in', 'filmers in',
|
||||||
'gear', 'for sale', 'buy', 'sell', 'how many', 'stats', 'count',
|
'gear', 'for sale', 'buy', 'sell', 'how many', 'stats', 'count',
|
||||||
'list', 'nearby', 'close to', 'around', 'available',
|
'list', 'nearby', 'close to', 'around', 'available',
|
||||||
|
'crews in', 'crews near', 'find crew', 'find a crew', 'crews around',
|
||||||
];
|
];
|
||||||
return liveKeywords.some((kw) => q.includes(kw));
|
return liveKeywords.some((kw) => q.includes(kw));
|
||||||
}
|
}
|
||||||
|
|
@ -497,6 +504,9 @@ export function SasquatchGuide({ message, onDismiss }: SasquatchGuideProps) {
|
||||||
case 'find_gear':
|
case 'find_gear':
|
||||||
toolResult = await cvm.findGear(toolArgs as Parameters<typeof cvm.findGear>[0]);
|
toolResult = await cvm.findGear(toolArgs as Parameters<typeof cvm.findGear>[0]);
|
||||||
break;
|
break;
|
||||||
|
case 'find_crews':
|
||||||
|
toolResult = await cvm.findCrews(toolArgs as Parameters<typeof cvm.findCrews>[0]);
|
||||||
|
break;
|
||||||
case 'get_community_stats':
|
case 'get_community_stats':
|
||||||
toolResult = await cvm.getCommunityStats();
|
toolResult = await cvm.getCommunityStats();
|
||||||
break;
|
break;
|
||||||
|
|
|
||||||
|
|
@ -74,6 +74,21 @@ export interface GearResult {
|
||||||
created_at: string;
|
created_at: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CrewResult {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
location?: string;
|
||||||
|
vibes: string[];
|
||||||
|
member_count: number;
|
||||||
|
founded?: string;
|
||||||
|
geohash?: string;
|
||||||
|
image?: string;
|
||||||
|
founder_pubkey: string;
|
||||||
|
naddr: string;
|
||||||
|
url: string;
|
||||||
|
created_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface CommunityStats {
|
export interface CommunityStats {
|
||||||
spots: number;
|
spots: number;
|
||||||
upcoming_events: number;
|
upcoming_events: number;
|
||||||
|
|
@ -81,6 +96,7 @@ export interface CommunityStats {
|
||||||
filmers: number;
|
filmers: number;
|
||||||
editors: number;
|
editors: number;
|
||||||
gear_listings: number;
|
gear_listings: number;
|
||||||
|
crews: number;
|
||||||
relays_queried: string[];
|
relays_queried: string[];
|
||||||
queried_at: string;
|
queried_at: string;
|
||||||
}
|
}
|
||||||
|
|
@ -440,18 +456,95 @@ export function useCVMClient() {
|
||||||
[nostr]
|
[nostr]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const findCrews = useCallback(
|
||||||
|
async (params: {
|
||||||
|
city?: string;
|
||||||
|
country?: string;
|
||||||
|
vibe?: string;
|
||||||
|
search?: string;
|
||||||
|
limit?: number;
|
||||||
|
}): Promise<{ count: number; crews: CrewResult[] }> => {
|
||||||
|
const signal = AbortSignal.any([AbortSignal.timeout(10000)]);
|
||||||
|
const relayGroup = nostr.group(DATA_RELAYS);
|
||||||
|
|
||||||
|
const raw = await relayGroup.query(
|
||||||
|
[{ kinds: [35006], limit: 500 }],
|
||||||
|
{ signal }
|
||||||
|
);
|
||||||
|
|
||||||
|
let crews = deduplicateAddressable(raw as typeof raw & { tags: string[][] }[]);
|
||||||
|
|
||||||
|
// Only valid crews
|
||||||
|
crews = crews.filter((e) => e.tags.some((t) => t[0] === 't' && t[1] === 'skatecrew'));
|
||||||
|
|
||||||
|
const locMatch = (e: typeof crews[0], q: string) =>
|
||||||
|
(getTag(e, 'location') ?? '').toLowerCase().includes(q) ||
|
||||||
|
(getTag(e, 'title') ?? '').toLowerCase().includes(q) ||
|
||||||
|
(e as unknown as { content: string }).content.toLowerCase().includes(q);
|
||||||
|
|
||||||
|
if (params.city) {
|
||||||
|
const q = params.city.toLowerCase();
|
||||||
|
crews = crews.filter((e) => locMatch(e, q));
|
||||||
|
}
|
||||||
|
if (params.country) {
|
||||||
|
const q = params.country.toLowerCase();
|
||||||
|
crews = crews.filter((e) => locMatch(e, q));
|
||||||
|
}
|
||||||
|
if (params.vibe) {
|
||||||
|
const q = params.vibe.toLowerCase();
|
||||||
|
crews = crews.filter((e) =>
|
||||||
|
getTags(e, 't').some((t) => t !== 'skatecrew' && t.toLowerCase().includes(q))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (params.search) {
|
||||||
|
const q = params.search.toLowerCase();
|
||||||
|
crews = crews.filter(
|
||||||
|
(e) =>
|
||||||
|
(getTag(e, 'title') ?? '').toLowerCase().includes(q) ||
|
||||||
|
(e as unknown as { content: string }).content.toLowerCase().includes(q) ||
|
||||||
|
getTags(e, 't').some((t) => t.toLowerCase().includes(q))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
crews.sort((a, b) => b.created_at - a.created_at);
|
||||||
|
crews = crews.slice(0, params.limit ?? 10);
|
||||||
|
|
||||||
|
const result: CrewResult[] = crews.map((e) => {
|
||||||
|
const ev = e as unknown as { content: string; pubkey: string; kind: number; tags: string[][]; created_at: number };
|
||||||
|
return {
|
||||||
|
name: getTag(ev, 'title') ?? getTag(ev, 'name') ?? 'Unnamed Crew',
|
||||||
|
description: ev.content.slice(0, 300),
|
||||||
|
location: getTag(ev, 'location'),
|
||||||
|
vibes: getTags(ev, 't').filter((t) => t !== 'skatecrew'),
|
||||||
|
member_count: ev.tags.filter((t) => t[0] === 'p').length,
|
||||||
|
founded: getTag(ev, 'founded'),
|
||||||
|
geohash: getTag(ev, 'g'),
|
||||||
|
image: getTag(ev, 'image'),
|
||||||
|
founder_pubkey: ev.pubkey,
|
||||||
|
naddr: buildNaddr(ev),
|
||||||
|
url: `https://skate-spots.org/crew/${ev.pubkey}/${getTag(ev, 'd') ?? ''}`,
|
||||||
|
created_at: new Date(ev.created_at * 1000).toISOString(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return { count: result.length, crews: result };
|
||||||
|
},
|
||||||
|
[nostr]
|
||||||
|
);
|
||||||
|
|
||||||
const getCommunityStats = useCallback(async (): Promise<CommunityStats> => {
|
const getCommunityStats = useCallback(async (): Promise<CommunityStats> => {
|
||||||
const signal = AbortSignal.any([AbortSignal.timeout(10000)]);
|
const signal = AbortSignal.any([AbortSignal.timeout(10000)]);
|
||||||
const relayGroup = nostr.group(DATA_RELAYS);
|
const relayGroup = nostr.group(DATA_RELAYS);
|
||||||
const now = Math.floor(Date.now() / 1000);
|
const now = Math.floor(Date.now() / 1000);
|
||||||
|
|
||||||
const [spots, events, coaches, filmers, editors, gear] = await Promise.all([
|
const [spots, events, coaches, filmers, editors, gear, crews] = await Promise.all([
|
||||||
relayGroup.query([{ kinds: [30818], limit: 2000 }], { signal }),
|
relayGroup.query([{ kinds: [30818], limit: 2000 }], { signal }),
|
||||||
relayGroup.query([{ kinds: [31923], limit: 500, since: now }], { signal }),
|
relayGroup.query([{ kinds: [31923], limit: 500, since: now }], { signal }),
|
||||||
relayGroup.query([{ kinds: [35001], limit: 500 }], { signal }),
|
relayGroup.query([{ kinds: [35001], limit: 500 }], { signal }),
|
||||||
relayGroup.query([{ kinds: [35002], limit: 500 }], { signal }),
|
relayGroup.query([{ kinds: [35002], limit: 500 }], { signal }),
|
||||||
relayGroup.query([{ kinds: [35003], limit: 500 }], { signal }),
|
relayGroup.query([{ kinds: [35003], limit: 500 }], { signal }),
|
||||||
relayGroup.query([{ kinds: [30402], limit: 500, '#t': ['skateboarding'] }], { signal }),
|
relayGroup.query([{ kinds: [30402], limit: 500, '#t': ['skateboarding'] }], { signal }),
|
||||||
|
relayGroup.query([{ kinds: [35006], limit: 500 }], { signal }),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -463,6 +556,9 @@ export function useCVMClient() {
|
||||||
filmers: deduplicateAddressable(filmers as typeof filmers & { tags: string[][] }[]).length,
|
filmers: deduplicateAddressable(filmers as typeof filmers & { tags: string[][] }[]).length,
|
||||||
editors: deduplicateAddressable(editors as typeof editors & { tags: string[][] }[]).length,
|
editors: deduplicateAddressable(editors as typeof editors & { tags: string[][] }[]).length,
|
||||||
gear_listings: deduplicateAddressable(gear as typeof gear & { tags: string[][] }[]).length,
|
gear_listings: deduplicateAddressable(gear as typeof gear & { tags: string[][] }[]).length,
|
||||||
|
crews: deduplicateAddressable(crews as typeof crews & { tags: string[][] }[]).filter(
|
||||||
|
(e) => e.tags.some((t) => t[0] === 't' && t[1] === 'skatecrew')
|
||||||
|
).length,
|
||||||
relays_queried: DATA_RELAYS,
|
relays_queried: DATA_RELAYS,
|
||||||
queried_at: new Date().toISOString(),
|
queried_at: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
|
|
@ -473,6 +569,7 @@ export function useCVMClient() {
|
||||||
findEvents,
|
findEvents,
|
||||||
findCoaches,
|
findCoaches,
|
||||||
findGear,
|
findGear,
|
||||||
|
findCrews,
|
||||||
getCommunityStats,
|
getCommunityStats,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue