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)
|
||||
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
|
||||
Args: none
|
||||
Example: {"tool":"get_community_stats","args":{}}
|
||||
|
|
@ -183,7 +189,7 @@ Example: {"tool":"get_weather","args":{"city":"Barcelona"}}
|
|||
## Rules
|
||||
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
|
||||
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
|
||||
5. Never invent spot names, event dates, or user data
|
||||
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
|
||||
- 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).
|
||||
- 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)
|
||||
- Tours = guided local/national/world skate tours
|
||||
- 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',
|
||||
'gear', 'for sale', 'buy', 'sell', 'how many', 'stats', 'count',
|
||||
'list', 'nearby', 'close to', 'around', 'available',
|
||||
'crews in', 'crews near', 'find crew', 'find a crew', 'crews around',
|
||||
];
|
||||
return liveKeywords.some((kw) => q.includes(kw));
|
||||
}
|
||||
|
|
@ -497,6 +504,9 @@ export function SasquatchGuide({ message, onDismiss }: SasquatchGuideProps) {
|
|||
case 'find_gear':
|
||||
toolResult = await cvm.findGear(toolArgs as Parameters<typeof cvm.findGear>[0]);
|
||||
break;
|
||||
case 'find_crews':
|
||||
toolResult = await cvm.findCrews(toolArgs as Parameters<typeof cvm.findCrews>[0]);
|
||||
break;
|
||||
case 'get_community_stats':
|
||||
toolResult = await cvm.getCommunityStats();
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -74,6 +74,21 @@ export interface GearResult {
|
|||
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 {
|
||||
spots: number;
|
||||
upcoming_events: number;
|
||||
|
|
@ -81,6 +96,7 @@ export interface CommunityStats {
|
|||
filmers: number;
|
||||
editors: number;
|
||||
gear_listings: number;
|
||||
crews: number;
|
||||
relays_queried: string[];
|
||||
queried_at: string;
|
||||
}
|
||||
|
|
@ -440,18 +456,95 @@ export function useCVMClient() {
|
|||
[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 signal = AbortSignal.any([AbortSignal.timeout(10000)]);
|
||||
const relayGroup = nostr.group(DATA_RELAYS);
|
||||
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: [31923], limit: 500, since: now }], { signal }),
|
||||
relayGroup.query([{ kinds: [35001], limit: 500 }], { signal }),
|
||||
relayGroup.query([{ kinds: [35002], limit: 500 }], { signal }),
|
||||
relayGroup.query([{ kinds: [35003], limit: 500 }], { signal }),
|
||||
relayGroup.query([{ kinds: [30402], limit: 500, '#t': ['skateboarding'] }], { signal }),
|
||||
relayGroup.query([{ kinds: [35006], limit: 500 }], { signal }),
|
||||
]);
|
||||
|
||||
return {
|
||||
|
|
@ -463,6 +556,9 @@ export function useCVMClient() {
|
|||
filmers: deduplicateAddressable(filmers as typeof filmers & { tags: string[][] }[]).length,
|
||||
editors: deduplicateAddressable(editors as typeof editors & { 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,
|
||||
queried_at: new Date().toISOString(),
|
||||
};
|
||||
|
|
@ -473,6 +569,7 @@ export function useCVMClient() {
|
|||
findEvents,
|
||||
findCoaches,
|
||||
findGear,
|
||||
findCrews,
|
||||
getCommunityStats,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue