diff --git a/src/components/CrewAgentPanel.tsx b/src/components/CrewAgentPanel.tsx
index 9be0dac..e9d6b01 100644
--- a/src/components/CrewAgentPanel.tsx
+++ b/src/components/CrewAgentPanel.tsx
@@ -128,7 +128,7 @@ function MessageRow({ m, onNavigate }: { m: CrewAgentMessage; onNavigate: () =>
{isAgent && (
- ๐
+ SQ
)}
@@ -149,9 +149,10 @@ function MessageRow({ m, onNavigate }: { m: CrewAgentMessage; onNavigate: () =>
}
const SUGGESTIONS = [
+ "What's our next jam?",
+ 'Who runs this crew?',
+ 'What are we looking for?',
'Best spots near us?',
- 'Any events this month?',
- 'Find a filmer nearby',
"What's the weather for skating?",
];
@@ -176,13 +177,17 @@ export function CrewAgentPanel({ open, onOpenChange, ctx }: CrewAgentPanelProps)
- ๐ Ask Sasquatch
+
+
+ SQ
+
+ Ask Sasquatch
Crew Agent
- Your crew's AI guide โ spots, events, coaches & gear, with live data.
+ Knows this crew โ its jams, roster & open call โ plus the wider scene with live data.
@@ -191,10 +196,11 @@ export function CrewAgentPanel({ open, onOpenChange, ctx }: CrewAgentPanelProps)
- ๐
+ SQ
- Yo! I'm Sasquatch, {ctx.crewName}'s guide. Ask me anything about your local scene.
+ Yo! I'm Sasquatch, {ctx.crewName}'s guide. Ask me about the crew โ our next jam,
+ who runs it, what we're after โ or the wider scene.
{SUGGESTIONS.map((s) => (
diff --git a/src/components/CrewForm.tsx b/src/components/CrewForm.tsx
index 1e28a95..d4b28c1 100644
--- a/src/components/CrewForm.tsx
+++ b/src/components/CrewForm.tsx
@@ -14,6 +14,7 @@ import { useUploadFile } from '@/hooks/useUploadFile';
import { useToast } from '@/hooks/useToast';
import { useAuthor } from '@/hooks/useAuthor';
import { genUserName } from '@/lib/genUserName';
+import { CREW_AGENT_AVATAR } from '@/lib/crewAgent';
import { SpotPicker } from '@/components/SpotPicker';
import { LabLinkField } from '@/components/LabLinkField';
import { isCrewVideoUrl, parseCrewVideo } from '@/lib/videoEmbed';
@@ -779,13 +780,20 @@ export function CrewForm({ existing }: CrewFormProps) {
- ๐ Crew Agent
+
+ Crew Agent
Beta
- Add Sasquatch , an AI guide for your crew. Members can ask it
- about nearby spots, events, coaches and gear โ it answers with live SkateSpots data and real links.
- Runs right in the browser; no keys or extra setup. It's early (free-tier AI, so occasionally slow).
+ Add Sasquatch , an AI guide who actually knows your crew.
+ Members can ask about your next jam, who runs the crew, what you're
+ looking for and your vibe โ plus nearby spots, events, coaches and gear from live SkateSpots
+ data. Runs right in the browser; no keys or setup. It's early (free-tier AI, so occasionally slow).
/. */
+ path?: string;
+}
+
+/** A recent crew-feed post, flattened for the agent's context. */
+export interface CrewAgentPost {
+ author: string;
+ /** Short excerpt of the note. */
+ text: string;
+ /** Unix seconds. */
+ createdAt: number;
+}
+
export interface CrewContext {
crewName: string;
location?: string;
/** City derived from location (best-effort) โ helps seed CVM queries. */
city?: string;
+ /** Freeform crew manifesto / about. */
+ bio?: string;
+ /** Style / focus tags. */
+ vibes?: string[];
+ /** Founder display name. */
+ founderName?: string;
+ /** Total member count (founder + listed members). */
+ memberCount?: number;
+ /** Human-readable open-call labels, e.g. ["Filmer", "New Members"]. */
+ lookingFor?: string[];
+ /** Crew band name, if the crew doubles as a music project. */
+ bandName?: string;
+ /** Number of home spots linked to the crew. */
+ homeSpotCount?: number;
+ /** Crew website. */
+ website?: string;
+ /** Crew Instagram handle. */
+ instagram?: string;
+ /** The crew's OWN upcoming/recent events โ soonest first. */
+ events?: CrewAgentEvent[];
+ /** A short digest of the crew's recent feed activity. */
+ recentPosts?: CrewAgentPost[];
+}
+
+function fmtDate(unix: number): string {
+ try {
+ return new Date(unix * 1000).toLocaleDateString(undefined, {
+ weekday: 'short',
+ month: 'short',
+ day: 'numeric',
+ });
+ } catch {
+ return 'TBD';
+ }
+}
+
+/**
+ * Build a compact, human-readable digest of everything Sasquatch knows about
+ * THIS crew. This is the crew's "memory" โ the stuff no global bot could know โ
+ * and is injected into every turn so the agent answers crew questions directly.
+ */
+function buildCrewDossier(ctx: CrewContext): string {
+ const lines: string[] = [];
+ const where = ctx.location || 'unknown';
+
+ lines.push(`Crew name: ${ctx.crewName}`);
+ lines.push(`Home turf: ${where}`);
+ if (ctx.founderName) lines.push(`Founded/run by: ${ctx.founderName}`);
+ if (typeof ctx.memberCount === 'number') {
+ lines.push(`Roster size: ${ctx.memberCount} ${ctx.memberCount === 1 ? 'member' : 'members'}`);
+ }
+ if (ctx.vibes?.length) lines.push(`Vibe / style: ${ctx.vibes.join(', ')}`);
+ if (ctx.bio) lines.push(`About: ${ctx.bio.slice(0, 400)}`);
+ if (ctx.bandName) lines.push(`Crew band: ${ctx.bandName}`);
+ if (typeof ctx.homeSpotCount === 'number' && ctx.homeSpotCount > 0) {
+ lines.push(`Home spots on file: ${ctx.homeSpotCount}`);
+ }
+ if (ctx.lookingFor?.length) {
+ lines.push(`Open call โ the crew is looking for: ${ctx.lookingFor.join(', ')}`);
+ }
+ if (ctx.website) lines.push(`Website: ${ctx.website}`);
+ if (ctx.instagram) lines.push(`Instagram: @${ctx.instagram.replace('@', '')}`);
+
+ // The crew's OWN events โ the highest-value crew-specific data.
+ if (ctx.events?.length) {
+ const now = Math.floor(Date.now() / 1000);
+ const upcoming = ctx.events.filter((e) => e.start >= now);
+ const list = (upcoming.length ? upcoming : ctx.events).slice(0, 6);
+ lines.push(
+ `\nCrew's own events (${upcoming.length ? 'upcoming' : 'most recent'}):`,
+ );
+ for (const e of list) {
+ const loc = e.location ? ` @ ${e.location}` : '';
+ lines.push(`- "${e.title}" โ ${fmtDate(e.start)}${loc}`);
+ }
+ } else {
+ lines.push('\nCrew events: none hosted yet.');
+ }
+
+ // Recent feed digest โ gives the agent a sense of what the crew is up to.
+ if (ctx.recentPosts?.length) {
+ lines.push('\nRecent crew feed activity:');
+ for (const p of ctx.recentPosts.slice(0, 5)) {
+ lines.push(`- ${p.author}: "${p.text.slice(0, 120).replace(/\s+/g, ' ')}"`);
+ }
+ }
+
+ return lines.join('\n');
}
function buildCrewSystemPrompt(ctx: CrewContext): string {
const where = ctx.city || ctx.location || 'their local area';
- return `You are ๐ Sasquatch, the AI guide for the "${ctx.crewName}" skate crew on SkateSpots (a decentralized skateboarding platform on Nostr). You're chatting inside the crew's page. Chill, skater-friendly, concise.
+ const dossier = buildCrewDossier(ctx);
-The crew's home turf is: ${ctx.location || 'unknown'}. When a member asks about "near us", "here", "local", or doesn't give a city, assume ${where}.
+ return `You are Sasquatch, the AI guide for the "${ctx.crewName}" skate crew on SkateSpots (a decentralized skateboarding platform on Nostr). You're chatting inside the crew's own page with its members. Chill, skater-friendly, concise, and you know THIS crew personally.
-## Tools โ query live data. Respond with ONLY a JSON tool call when you need data, nothing else on that line:
+## What you know about this crew (your memory)
+${dossier}
+
+When a member asks about "us", "our crew", "here", "our next jam", "who runs this", "what are we looking for", "our vibe", etc. โ answer DIRECTLY from the crew memory above. Do NOT call a tool for questions about the crew itself. If the memory doesn't cover it, say so honestly.
+
+## Tools โ for discovering things OUTSIDE the crew (the wider SkateSpots network).
+Respond with ONLY a JSON tool call when you need external data, nothing else on that line:
{"tool":"","args":{}}
- find_spots โ args: city?, country?, difficulty?, features?(string[]), search?, limit?
@@ -36,17 +149,20 @@ The crew's home turf is: ${ctx.location || 'unknown'}. When a member asks about
- get_weather โ args: city (required)
Rules:
-1. Data questions (spots/events/coaches/gear/stats/weather) โ ALWAYS use a tool, never guess.
-2. Tool call = ONE JSON object on its own line. For local queries seed the args with the crew's city (${where}) and limit:20.
-3. Be concise and skater-friendly. Never invent spot names, dates, or user data.
-4. 0 results = honest "none listed yet" + encourage adding content at skate-spots.org.`;
+1. Questions about THIS crew (its events, roster, vibe, open call, founder, band, home spots, website) โ answer from memory, NO tool.
+2. Questions about the wider scene (spots/events/coaches/gear/crews near us, stats, weather) โ ALWAYS use a tool, never guess. Seed local queries with the crew's city (${where}) and limit:20.
+3. A tool call is ONE JSON object on its own line โ nothing else.
+4. Be concise and skater-friendly. Never invent spot names, dates, member names, or user data.
+5. 0 results (or empty crew data) = honest "none listed yet" + encourage adding content at skate-spots.org.`;
}
/**
- * Crew-scoped Sasquatch chat (Phase 2). Reuses the CVM data tools and the
- * shared AI brain, seeded with the crew's context. Ephemeral/local โ no keys,
- * no publishing (that's Phase 3). Answers can carry structured `receipts` so
- * the UI renders spot / Lab / event cards.
+ * Crew-scoped Sasquatch chat. Reuses the CVM data tools and the shared AI
+ * brain, but is seeded with a rich, crew-specific dossier (its own events,
+ * feed digest, roster, open call, vibe, band, home spots) so it can answer
+ * questions about the crew itself directly โ not just re-skin the global bot.
+ * Ephemeral/local โ no keys, no publishing. Answers can carry structured
+ * `receipts` so the UI renders spot / Lab / event cards.
*/
export function useCrewAgentChat(ctx: CrewContext) {
const cvm = useCVMClient();
@@ -91,7 +207,7 @@ export function useCrewAgentChat(ctx: CrewContext) {
try {
const system = buildCrewSystemPrompt(ctx);
- // First pass โ AI answers directly or emits a tool call.
+ // First pass โ AI answers directly from crew memory or emits a tool call.
const firstPass: ChatTurn[] = [
{ role: 'user', content: `${system}\n\n---\n\nMember: ${q}` },
];
@@ -119,7 +235,7 @@ export function useCrewAgentChat(ctx: CrewContext) {
const presented = await callSasquatchAI([
{
role: 'user',
- content: `You are ๐ Sasquatch. Present this live data to the crew member naturally and concisely. Do NOT output any JSON. Plain conversational text with short bullet points.
+ content: `You are Sasquatch, guide for the "${ctx.crewName}" crew. Present this live data to the crew member naturally and concisely. Do NOT output any JSON. Plain conversational text with short bullet points.
Member asked: "${q}"
diff --git a/src/lib/crewAgent.ts b/src/lib/crewAgent.ts
index 80b9fbd..7aff1e6 100644
--- a/src/lib/crewAgent.ts
+++ b/src/lib/crewAgent.ts
@@ -1,7 +1,7 @@
/**
* Crew Agent identity (Phase 0)
* โโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
- * The Crew Agent ("๐ Sasquatch") is a Nostr identity that belongs to a crew.
+ * The Crew Agent ("Sasquatch") is a Nostr identity that belongs to a crew.
* Its key is DETERMINISTICALLY DERIVED from the crew's addressable coordinate,
* so any client can reproduce the same agent identity for a given crew without
* storing a key anywhere.
@@ -75,7 +75,7 @@ export const CREW_AGENT_AVATAR =
*/
export function buildCrewAgentProfile(crewName: string): EventTemplate {
const metadata = {
- name: `๐ Sasquatch ยท ${crewName}`,
+ name: `Sasquatch ยท ${crewName}`,
display_name: `Sasquatch ยท ${crewName}`,
about: `The AI crew guide for ${crewName} on SkateSpots. Ask me about spots, events, coaches, gear and more โ I answer with live data. ๐น`,
picture: CREW_AGENT_AVATAR,
diff --git a/src/lib/types.ts b/src/lib/types.ts
index e487777..7f78bf1 100644
--- a/src/lib/types.ts
+++ b/src/lib/types.ts
@@ -1923,7 +1923,7 @@ export interface SkateCrew {
image?: string; // Crew logo / photo
vibes: string[]; // Style/focus tags
focus: CrewFocus; // Optional Lab scene (builder/photo/coaching) or 'mixed'
- agentEnabled: boolean; // Whether the crew's AI agent ("๐ Sasquatch") is on
+ agentEnabled: boolean; // Whether the crew's AI agent ("Sasquatch") is on
founded?: string; // Year founded (cosmetic)
website?: string;
instagram?: string;
diff --git a/src/pages/CrewDetailPage.tsx b/src/pages/CrewDetailPage.tsx
index 9ed4b0f..3cd7bd4 100644
--- a/src/pages/CrewDetailPage.tsx
+++ b/src/pages/CrewDetailPage.tsx
@@ -22,6 +22,7 @@ import { useCrewsActivity, crewCoordinate } from '@/hooks/useCrewActivity';
import { useCrewChatUnread } from '@/hooks/useCrewChatUnread';
import { CrewChat } from '@/components/CrewChat';
import { CrewAgentPanel } from '@/components/CrewAgentPanel';
+import { CREW_AGENT_AVATAR } from '@/lib/crewAgent';
import { CrewTreasuryCard } from '@/components/CrewTreasuryCard';
import { CrewLookingForCard } from '@/components/CrewLookingFor';
import { CrewRequestToJoin } from '@/components/CrewRequestToJoin';
@@ -54,7 +55,7 @@ import { useCrewFeed } from '@/hooks/useCrewFeed';
import { useAuthor } from '@/hooks/useAuthor';
import { useCurrentUser } from '@/hooks/useCurrentUser';
import { genUserName } from '@/lib/genUserName';
-import { SKATE_CREW_KIND, SKATE_SPOT_KIND, SHOW_CREW_TREASURY, type CrewRole } from '@/lib/types';
+import { SKATE_CREW_KIND, SKATE_SPOT_KIND, SHOW_CREW_TREASURY, CREW_LOOKING_FOR_LABELS, type CrewRole } from '@/lib/types';
import { sanitizeUrl, urlDomain } from '@/lib/sanitizeUrl';
import { cn } from '@/lib/utils';
import NotFound from './NotFound';
@@ -336,8 +337,11 @@ export default function CrewDetailPage() {
onClick={() => setAgentOpen(true)}
className="group flex w-full items-center gap-3 rounded-2xl border-2 border-teal-500/30 bg-gradient-to-br from-teal-500/10 to-transparent p-4 text-left transition-all hover:border-teal-500/60 hover:from-teal-500/15 active:scale-[0.99]"
>
-
- ๐
+
@@ -619,12 +623,38 @@ export default function CrewDetailPage() {
- {/* Crew Agent chat panel */}
+ {/* Crew Agent chat panel โ seeded with the crew's OWN data so Sasquatch
+ can answer crew-specific questions (its events, roster, open call,
+ vibe, band, home spots) directly, not just re-skin the global bot. */}
{crew.agentEnabled && (
CREW_LOOKING_FOR_LABELS[s]?.label ?? s),
+ bandName: crew.bandName,
+ homeSpotCount: crew.spotRefs.length,
+ website: crew.website,
+ instagram: crew.instagram,
+ events: (crewEvents ?? []).map((e) => ({
+ title: e.title,
+ start: e.start,
+ location: e.location,
+ path: `/event/${e.pubkey}/${e.identifier}`,
+ })),
+ recentPosts: (feed ?? []).slice(0, 5).map((ev) => ({
+ author: genUserName(ev.pubkey),
+ text: ev.content ?? '',
+ createdAt: ev.created_at,
+ })),
+ }}
/>
)}