Make Crew Agent genuinely crew-aware; swap bee emoji for Sasquatch avatar
Sasquatch now knows the crew it lives on. The agent is seeded with a rich
crew dossier — its own upcoming events, recent feed digest, roster size,
founder, open call, vibe, band, home spots, website/IG — and the system
prompt instructs it to answer crew-specific questions ("what's our next
jam?", "who runs this?", "what are we looking for?") directly from memory,
while still using CVM tools for wider-network discovery.
- useCrewAgentChat: expanded CrewContext + crew dossier + two-mode prompt
- CrewDetailPage: wires real crew events + feed digest into the agent ctx
- CrewAgentPanel: crew-specific suggestions, crew-aware copy
- Replaced 🐝 bee emoji with the Sasquatch mascot avatar everywhere
(panel header/rows/empty state, crew page button, CrewForm toggle)
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
This commit is contained in:
parent
0b786e546f
commit
878da94b7b
6 changed files with 192 additions and 32 deletions
|
|
@ -128,7 +128,7 @@ function MessageRow({ m, onNavigate }: { m: CrewAgentMessage; onNavigate: () =>
|
||||||
{isAgent && (
|
{isAgent && (
|
||||||
<Avatar className="h-8 w-8 flex-shrink-0">
|
<Avatar className="h-8 w-8 flex-shrink-0">
|
||||||
<AvatarImage src={CREW_AGENT_AVATAR} alt="Sasquatch" />
|
<AvatarImage src={CREW_AGENT_AVATAR} alt="Sasquatch" />
|
||||||
<AvatarFallback className="bg-teal-900/40 text-teal-300 text-xs">🐝</AvatarFallback>
|
<AvatarFallback className="bg-teal-900/40 text-teal-300 text-[10px] font-black">SQ</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
)}
|
)}
|
||||||
<div className={cn('flex max-w-[82%] flex-col', !isAgent && 'items-end')}>
|
<div className={cn('flex max-w-[82%] flex-col', !isAgent && 'items-end')}>
|
||||||
|
|
@ -149,9 +149,10 @@ function MessageRow({ m, onNavigate }: { m: CrewAgentMessage; onNavigate: () =>
|
||||||
}
|
}
|
||||||
|
|
||||||
const SUGGESTIONS = [
|
const SUGGESTIONS = [
|
||||||
|
"What's our next jam?",
|
||||||
|
'Who runs this crew?',
|
||||||
|
'What are we looking for?',
|
||||||
'Best spots near us?',
|
'Best spots near us?',
|
||||||
'Any events this month?',
|
|
||||||
'Find a filmer nearby',
|
|
||||||
"What's the weather for skating?",
|
"What's the weather for skating?",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -176,13 +177,17 @@ export function CrewAgentPanel({ open, onOpenChange, ctx }: CrewAgentPanelProps)
|
||||||
<DialogContent className="w-[calc(100%-1rem)] sm:max-w-md p-0 overflow-hidden bg-black border-teal-500/30 flex flex-col max-h-[85vh]">
|
<DialogContent className="w-[calc(100%-1rem)] sm:max-w-md p-0 overflow-hidden bg-black border-teal-500/30 flex flex-col max-h-[85vh]">
|
||||||
<DialogHeader className="p-4 pb-3 border-b border-white/5">
|
<DialogHeader className="p-4 pb-3 border-b border-white/5">
|
||||||
<DialogTitle className="flex items-center gap-2 text-white">
|
<DialogTitle className="flex items-center gap-2 text-white">
|
||||||
<span className="text-lg leading-none">🐝</span> Ask Sasquatch
|
<Avatar className="h-6 w-6">
|
||||||
|
<AvatarImage src={CREW_AGENT_AVATAR} alt="Sasquatch" />
|
||||||
|
<AvatarFallback className="bg-teal-900/40 text-teal-300 text-[9px] font-black">SQ</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
Ask Sasquatch
|
||||||
<span className="rounded-full bg-teal-500/15 px-1.5 py-0.5 text-[10px] font-bold uppercase text-teal-300">
|
<span className="rounded-full bg-teal-500/15 px-1.5 py-0.5 text-[10px] font-bold uppercase text-teal-300">
|
||||||
Crew Agent
|
Crew Agent
|
||||||
</span>
|
</span>
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogDescription className="text-xs">
|
<DialogDescription className="text-xs">
|
||||||
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.
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
|
|
@ -191,10 +196,11 @@ export function CrewAgentPanel({ open, onOpenChange, ctx }: CrewAgentPanelProps)
|
||||||
<div className="flex flex-col items-center justify-center gap-3 py-6 text-center">
|
<div className="flex flex-col items-center justify-center gap-3 py-6 text-center">
|
||||||
<Avatar className="h-12 w-12">
|
<Avatar className="h-12 w-12">
|
||||||
<AvatarImage src={CREW_AGENT_AVATAR} alt="Sasquatch" />
|
<AvatarImage src={CREW_AGENT_AVATAR} alt="Sasquatch" />
|
||||||
<AvatarFallback className="bg-teal-900/40 text-teal-300">🐝</AvatarFallback>
|
<AvatarFallback className="bg-teal-900/40 text-teal-300 font-black">SQ</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<p className="text-sm text-muted-foreground max-w-[16rem]">
|
<p className="text-sm text-muted-foreground max-w-[16rem]">
|
||||||
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.
|
||||||
</p>
|
</p>
|
||||||
<div className="flex flex-wrap justify-center gap-1.5">
|
<div className="flex flex-wrap justify-center gap-1.5">
|
||||||
{SUGGESTIONS.map((s) => (
|
{SUGGESTIONS.map((s) => (
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import { useUploadFile } from '@/hooks/useUploadFile';
|
||||||
import { useToast } from '@/hooks/useToast';
|
import { useToast } from '@/hooks/useToast';
|
||||||
import { useAuthor } from '@/hooks/useAuthor';
|
import { useAuthor } from '@/hooks/useAuthor';
|
||||||
import { genUserName } from '@/lib/genUserName';
|
import { genUserName } from '@/lib/genUserName';
|
||||||
|
import { CREW_AGENT_AVATAR } from '@/lib/crewAgent';
|
||||||
import { SpotPicker } from '@/components/SpotPicker';
|
import { SpotPicker } from '@/components/SpotPicker';
|
||||||
import { LabLinkField } from '@/components/LabLinkField';
|
import { LabLinkField } from '@/components/LabLinkField';
|
||||||
import { isCrewVideoUrl, parseCrewVideo } from '@/lib/videoEmbed';
|
import { isCrewVideoUrl, parseCrewVideo } from '@/lib/videoEmbed';
|
||||||
|
|
@ -779,13 +780,20 @@ export function CrewForm({ existing }: CrewFormProps) {
|
||||||
<div className="flex items-start justify-between gap-3">
|
<div className="flex items-start justify-between gap-3">
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<Label className="flex items-center gap-2 text-teal-400">
|
<Label className="flex items-center gap-2 text-teal-400">
|
||||||
<span className="text-base leading-none">🐝</span> Crew Agent
|
<img
|
||||||
|
src={CREW_AGENT_AVATAR}
|
||||||
|
alt="Sasquatch"
|
||||||
|
className="h-5 w-5 rounded-md object-cover"
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
Crew Agent
|
||||||
<span className="rounded-full bg-teal-500/15 px-1.5 py-0.5 text-[10px] font-bold uppercase text-teal-300">Beta</span>
|
<span className="rounded-full bg-teal-500/15 px-1.5 py-0.5 text-[10px] font-bold uppercase text-teal-300">Beta</span>
|
||||||
</Label>
|
</Label>
|
||||||
<p className="mt-1 text-xs text-gray-500">
|
<p className="mt-1 text-xs text-gray-500">
|
||||||
Add <strong className="text-gray-300">Sasquatch</strong>, an AI guide for your crew. Members can ask it
|
Add <strong className="text-gray-300">Sasquatch</strong>, an AI guide who actually knows your crew.
|
||||||
about nearby spots, events, coaches and gear — it answers with live SkateSpots data and real links.
|
Members can ask about <strong className="text-gray-300">your next jam, who runs the crew, what you're
|
||||||
Runs right in the browser; no keys or extra setup. It's early (free-tier AI, so occasionally slow).
|
looking for and your vibe</strong> — 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).
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
|
|
|
||||||
|
|
@ -11,20 +11,133 @@ export interface CrewAgentMessage {
|
||||||
receipts?: { tool: string; data: unknown };
|
receipts?: { tool: string; data: unknown };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** A crew-hosted event, flattened for the agent's context. */
|
||||||
|
export interface CrewAgentEvent {
|
||||||
|
title: string;
|
||||||
|
/** Unix seconds. */
|
||||||
|
start: number;
|
||||||
|
location?: string;
|
||||||
|
/** In-app path to the event, e.g. /event/<pubkey>/<d>. */
|
||||||
|
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 {
|
export interface CrewContext {
|
||||||
crewName: string;
|
crewName: string;
|
||||||
location?: string;
|
location?: string;
|
||||||
/** City derived from location (best-effort) — helps seed CVM queries. */
|
/** City derived from location (best-effort) — helps seed CVM queries. */
|
||||||
city?: string;
|
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 {
|
function buildCrewSystemPrompt(ctx: CrewContext): string {
|
||||||
const where = ctx.city || ctx.location || 'their local area';
|
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":"<name>","args":{}}
|
{"tool":"<name>","args":{}}
|
||||||
|
|
||||||
- find_spots — args: city?, country?, difficulty?, features?(string[]), search?, limit?
|
- 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)
|
- get_weather — args: city (required)
|
||||||
|
|
||||||
Rules:
|
Rules:
|
||||||
1. Data questions (spots/events/coaches/gear/stats/weather) → ALWAYS use a tool, never guess.
|
1. Questions about THIS crew (its events, roster, vibe, open call, founder, band, home spots, website) → answer from memory, NO tool.
|
||||||
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.
|
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. Be concise and skater-friendly. Never invent spot names, dates, or user data.
|
3. A tool call is ONE JSON object on its own line — nothing else.
|
||||||
4. 0 results = honest "none listed yet" + encourage adding content at skate-spots.org.`;
|
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
|
* Crew-scoped Sasquatch chat. Reuses the CVM data tools and the shared AI
|
||||||
* shared AI brain, seeded with the crew's context. Ephemeral/local — no keys,
|
* brain, but is seeded with a rich, crew-specific dossier (its own events,
|
||||||
* no publishing (that's Phase 3). Answers can carry structured `receipts` so
|
* feed digest, roster, open call, vibe, band, home spots) so it can answer
|
||||||
* the UI renders spot / Lab / event cards.
|
* 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) {
|
export function useCrewAgentChat(ctx: CrewContext) {
|
||||||
const cvm = useCVMClient();
|
const cvm = useCVMClient();
|
||||||
|
|
@ -91,7 +207,7 @@ export function useCrewAgentChat(ctx: CrewContext) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const system = buildCrewSystemPrompt(ctx);
|
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[] = [
|
const firstPass: ChatTurn[] = [
|
||||||
{ role: 'user', content: `${system}\n\n---\n\nMember: ${q}` },
|
{ role: 'user', content: `${system}\n\n---\n\nMember: ${q}` },
|
||||||
];
|
];
|
||||||
|
|
@ -119,7 +235,7 @@ export function useCrewAgentChat(ctx: CrewContext) {
|
||||||
const presented = await callSasquatchAI([
|
const presented = await callSasquatchAI([
|
||||||
{
|
{
|
||||||
role: 'user',
|
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}"
|
Member asked: "${q}"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
/**
|
/**
|
||||||
* Crew Agent identity (Phase 0)
|
* 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,
|
* 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
|
* so any client can reproduce the same agent identity for a given crew without
|
||||||
* storing a key anywhere.
|
* storing a key anywhere.
|
||||||
|
|
@ -75,7 +75,7 @@ export const CREW_AGENT_AVATAR =
|
||||||
*/
|
*/
|
||||||
export function buildCrewAgentProfile(crewName: string): EventTemplate {
|
export function buildCrewAgentProfile(crewName: string): EventTemplate {
|
||||||
const metadata = {
|
const metadata = {
|
||||||
name: `🐝 Sasquatch · ${crewName}`,
|
name: `Sasquatch · ${crewName}`,
|
||||||
display_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. 🛹`,
|
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,
|
picture: CREW_AGENT_AVATAR,
|
||||||
|
|
|
||||||
|
|
@ -1923,7 +1923,7 @@ export interface SkateCrew {
|
||||||
image?: string; // Crew logo / photo
|
image?: string; // Crew logo / photo
|
||||||
vibes: string[]; // Style/focus tags
|
vibes: string[]; // Style/focus tags
|
||||||
focus: CrewFocus; // Optional Lab scene (builder/photo/coaching) or 'mixed'
|
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)
|
founded?: string; // Year founded (cosmetic)
|
||||||
website?: string;
|
website?: string;
|
||||||
instagram?: string;
|
instagram?: string;
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ import { useCrewsActivity, crewCoordinate } from '@/hooks/useCrewActivity';
|
||||||
import { useCrewChatUnread } from '@/hooks/useCrewChatUnread';
|
import { useCrewChatUnread } from '@/hooks/useCrewChatUnread';
|
||||||
import { CrewChat } from '@/components/CrewChat';
|
import { CrewChat } from '@/components/CrewChat';
|
||||||
import { CrewAgentPanel } from '@/components/CrewAgentPanel';
|
import { CrewAgentPanel } from '@/components/CrewAgentPanel';
|
||||||
|
import { CREW_AGENT_AVATAR } from '@/lib/crewAgent';
|
||||||
import { CrewTreasuryCard } from '@/components/CrewTreasuryCard';
|
import { CrewTreasuryCard } from '@/components/CrewTreasuryCard';
|
||||||
import { CrewLookingForCard } from '@/components/CrewLookingFor';
|
import { CrewLookingForCard } from '@/components/CrewLookingFor';
|
||||||
import { CrewRequestToJoin } from '@/components/CrewRequestToJoin';
|
import { CrewRequestToJoin } from '@/components/CrewRequestToJoin';
|
||||||
|
|
@ -54,7 +55,7 @@ import { useCrewFeed } from '@/hooks/useCrewFeed';
|
||||||
import { useAuthor } from '@/hooks/useAuthor';
|
import { useAuthor } from '@/hooks/useAuthor';
|
||||||
import { useCurrentUser } from '@/hooks/useCurrentUser';
|
import { useCurrentUser } from '@/hooks/useCurrentUser';
|
||||||
import { genUserName } from '@/lib/genUserName';
|
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 { sanitizeUrl, urlDomain } from '@/lib/sanitizeUrl';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import NotFound from './NotFound';
|
import NotFound from './NotFound';
|
||||||
|
|
@ -336,8 +337,11 @@ export default function CrewDetailPage() {
|
||||||
onClick={() => setAgentOpen(true)}
|
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]"
|
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]"
|
||||||
>
|
>
|
||||||
<div className="flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-2xl bg-teal-500/15 text-2xl">
|
<div className="flex h-12 w-12 flex-shrink-0 items-center justify-center overflow-hidden rounded-2xl bg-teal-500/15">
|
||||||
🐝
|
<Avatar className="h-12 w-12 rounded-2xl">
|
||||||
|
<AvatarImage src={CREW_AGENT_AVATAR} alt="Sasquatch" className="object-cover" />
|
||||||
|
<AvatarFallback className="rounded-2xl bg-teal-900/40 text-teal-300 font-black">SQ</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<p className="flex items-center gap-2 font-black text-white">
|
<p className="flex items-center gap-2 font-black text-white">
|
||||||
|
|
@ -619,12 +623,38 @@ export default function CrewDetailPage() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 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.agentEnabled && (
|
||||||
<CrewAgentPanel
|
<CrewAgentPanel
|
||||||
open={agentOpen}
|
open={agentOpen}
|
||||||
onOpenChange={setAgentOpen}
|
onOpenChange={setAgentOpen}
|
||||||
ctx={{ crewName: crew.name, location: crew.location, city: crew.location }}
|
ctx={{
|
||||||
|
crewName: crew.name,
|
||||||
|
location: crew.location,
|
||||||
|
city: crew.location,
|
||||||
|
bio: crew.bio,
|
||||||
|
vibes: crew.vibes,
|
||||||
|
founderName,
|
||||||
|
memberCount: 1 + crew.members.length,
|
||||||
|
lookingFor: crew.lookingFor.map((s) => 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,
|
||||||
|
})),
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue