Skip to content

Commit 8b10057

Browse files
committed
feat(site): use Cloudflare Workers AI models, add YouTube search, and expose full public MCP dataset
1 parent 8534bb0 commit 8b10057

9 files changed

Lines changed: 1548 additions & 427 deletions

File tree

‎apps/site/src/components/SearchModal.astro‎

Lines changed: 75 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
// SearchModal.astro — Command-palette search modal with "Ask agent" as first item, Recently Asked, Recently Viewed, and Sanity Semantic Search
2+
// SearchModal.astro: Command-palette search modal with "Ask agent" as first item, Recently Asked, Recently Viewed, and Sanity Semantic Search
33
---
44

55
<dialog
@@ -129,6 +129,13 @@
129129
>
130130
All
131131
</button>
132+
<button
133+
type="button"
134+
class="modal-filter-pill rounded-full px-2.5 py-0.5 text-[11px] font-medium border border-border bg-card text-foreground hover:bg-accent transition-colors cursor-pointer"
135+
data-type="course"
136+
>
137+
Courses
138+
</button>
132139
<button
133140
type="button"
134141
class="modal-filter-pill rounded-full px-2.5 py-0.5 text-[11px] font-medium border border-border bg-card text-foreground hover:bg-accent transition-colors cursor-pointer"
@@ -143,6 +150,13 @@
143150
>
144151
Podcasts
145152
</button>
153+
<button
154+
type="button"
155+
class="modal-filter-pill rounded-full px-2.5 py-0.5 text-[11px] font-medium border border-border bg-card text-foreground hover:bg-accent transition-colors cursor-pointer"
156+
data-type="youtube"
157+
>
158+
YouTube
159+
</button>
146160
<button
147161
type="button"
148162
class="modal-filter-pill rounded-full px-2.5 py-0.5 text-[11px] font-medium border border-border bg-card text-foreground hover:bg-accent transition-colors cursor-pointer"
@@ -255,6 +269,11 @@
255269
href: string;
256270
excerpt?: string;
257271
date?: string;
272+
youtubeUrl?: string;
273+
youtubeId?: string;
274+
thumbnailUrl?: string;
275+
duration?: string;
276+
viewCount?: number;
258277
}
259278

260279
interface ClairicExtractionInfo {
@@ -527,6 +546,18 @@
527546

528547
function getBadgeClasses(type: string): { label: string; badgeClass: string } {
529548
switch (type) {
549+
case "course":
550+
return {
551+
label: "Course",
552+
badgeClass:
553+
"bg-violet-500/10 text-violet-600 dark:text-violet-400 border-violet-500/20",
554+
};
555+
case "lesson":
556+
return {
557+
label: "Lesson",
558+
badgeClass:
559+
"bg-indigo-500/10 text-indigo-600 dark:text-indigo-400 border-indigo-500/20",
560+
};
530561
case "post":
531562
return {
532563
label: "Post",
@@ -539,6 +570,25 @@
539570
badgeClass:
540571
"bg-purple-500/10 text-purple-600 dark:text-purple-400 border-purple-500/20",
541572
};
573+
case "youtube":
574+
case "transcript":
575+
return {
576+
label: "YouTube",
577+
badgeClass:
578+
"bg-red-500/10 text-red-600 dark:text-red-400 border-red-500/20",
579+
};
580+
case "newsletter":
581+
return {
582+
label: "Newsletter",
583+
badgeClass:
584+
"bg-pink-500/10 text-pink-600 dark:text-pink-400 border-pink-500/20",
585+
};
586+
case "sponsor":
587+
return {
588+
label: "Sponsor",
589+
badgeClass:
590+
"bg-amber-500/10 text-amber-600 dark:text-amber-400 border-amber-500/20",
591+
};
542592
case "author":
543593
return {
544594
label: "Author",
@@ -589,20 +639,37 @@
589639
const li = document.createElement("li");
590640
li.setAttribute("role", "option");
591641
li.id = `search-item-${idx}`;
642+
const isExternal = hit.href.startsWith("http");
643+
644+
const thumbHtml = hit.thumbnailUrl
645+
? `
646+
<div class="relative shrink-0 w-16 aspect-video rounded overflow-hidden border border-border bg-black">
647+
<img src="${escapeHtml(hit.thumbnailUrl)}" alt="" loading="lazy" class="h-full w-full object-cover" />
648+
${hit.duration ? `<span class="absolute bottom-0.5 right-0.5 rounded bg-black/80 px-1 text-[9px] font-mono text-white">${escapeHtml(hit.duration)}</span>` : ""}
649+
</div>
650+
`
651+
: "";
592652

593653
li.innerHTML = `
594654
<a
595655
href="${hit.href}"
656+
${isExternal ? 'target="_blank" rel="noopener noreferrer"' : ""}
596657
data-index="${idx + 1}"
597-
class="search-result-item flex flex-col gap-1 rounded-lg px-3 py-2.5 hover:bg-accent transition-colors focus:bg-accent focus:outline-none cursor-pointer border border-transparent"
658+
class="search-result-item flex items-center gap-3 rounded-lg px-3 py-2.5 hover:bg-accent transition-colors focus:bg-accent focus:outline-none cursor-pointer border border-transparent"
598659
>
599-
<div class="flex items-center justify-between gap-2">
600-
<span class="font-medium text-sm line-clamp-1 text-foreground">${escapeHtml(hit.title)}</span>
601-
<span class="inline-flex shrink-0 items-center rounded-full border px-2 py-0.5 text-[10px] font-semibold ${badge.badgeClass}">
602-
${badge.label}
603-
</span>
660+
${thumbHtml}
661+
<div class="flex min-w-0 flex-1 flex-col gap-1">
662+
<div class="flex items-center justify-between gap-2">
663+
<span class="font-medium text-sm line-clamp-1 text-foreground">${escapeHtml(hit.title)}</span>
664+
<div class="flex shrink-0 items-center gap-1.5">
665+
${hit.youtubeUrl && hit.type !== "youtube" ? `<span class="inline-flex items-center rounded-full border border-red-500/20 bg-red-500/10 px-1.5 py-0.5 text-[9px] font-mono font-semibold text-red-600 dark:text-red-400">▶ YT</span>` : ""}
666+
<span class="inline-flex shrink-0 items-center rounded-full border px-2 py-0.5 text-[10px] font-semibold ${badge.badgeClass}">
667+
${badge.label}
668+
</span>
669+
</div>
670+
</div>
671+
${hit.excerpt ? `<p class="text-xs text-muted-foreground line-clamp-1">${escapeHtml(hit.excerpt)}</p>` : ""}
604672
</div>
605-
${hit.excerpt ? `<p class="text-xs text-muted-foreground line-clamp-1">${escapeHtml(hit.excerpt)}</p>` : ""}
606673
</a>
607674
`;
608675

‎apps/site/src/lib/portal-security.ts‎

Lines changed: 73 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,25 @@ const FORBIDDEN_GROQ_IDENTIFIERS = [
99
"distributionConfig",
1010
"trendSignal",
1111
"syndication",
12+
"opsDigestState",
13+
"pluginSecrets",
14+
"secrets.",
15+
"sponsorshipRequest",
16+
"gcsConfig",
17+
"remotionConfig",
18+
"contentConfig",
19+
"pipelineConfig",
20+
"previewSession",
21+
"previewUrlSecret",
22+
"sanity.fileAsset",
23+
"fileAsset",
24+
"sanity.imageAsset",
25+
"imageAsset",
26+
"sanity.videoAsset",
27+
"videoAsset",
28+
"media.tag",
29+
"private-vault",
30+
"collaborationThread",
1231
"private.",
1332
"drafts.",
1433
"versions.",
@@ -17,18 +36,35 @@ const FORBIDDEN_GROQ_IDENTIFIERS = [
1736
"signatureData",
1837
"guestEmail",
1938
"contactEmail",
39+
"coordinatorEmail",
40+
"allowedEmails",
2041
"stripeDeposit",
2142
"stripeBalance",
2243
"passionfroot",
2344
"sharedAssets",
2445
"preCallNotes",
2546
"runOfShowNotes",
2647
"guestQuestions",
48+
"roughCutUrl",
49+
"recordingUrl",
50+
"gmailThreadId",
51+
"gmailMessageId",
52+
"gmailDraftId",
53+
"automatedVideo",
54+
"contentIdea",
55+
'"short"',
56+
"'short'",
57+
"short-",
58+
"headshot",
59+
"speakingEvent",
60+
"talkAbstract",
61+
'"settings"',
62+
"'settings'",
2763
];
2864

2965
/**
3066
* Blocks any GROQ query from Agent AJ / MCP that references private CRM, booking,
31-
* trendSignal, draft, or system documents/fields.
67+
* trendSignal, draft, asset, or system documents/fields.
3268
*/
3369
export function isForbiddenPrivateQuery(query: string): boolean {
3470
if (!query || typeof query !== "string") {
@@ -43,24 +79,57 @@ export function isForbiddenPrivateQuery(query: string): boolean {
4379
return false;
4480
}
4581

82+
export const PUBLIC_MCP_DOCUMENT_TYPES = [
83+
"post",
84+
"podcast",
85+
"course",
86+
"lesson",
87+
"transcript",
88+
"author",
89+
"guest",
90+
"sponsor",
91+
"newsletter",
92+
"page",
93+
"podcastType",
94+
] as const;
95+
4696
/**
47-
* Blocks any document ID in `array_field_reader` that is in a non-public path
48-
* (`private.*`, `drafts.*`, `trend.*`, `versions.*`, `_.**`).
97+
* Enforces an AST-level public document allowlist on raw GROQ queries (`*` and `*[...]`)
98+
* so even wildcard queries (`*[true]`, `*[!(_type == "post")]`) cannot access non-public
99+
* or `private.*` documents.
49100
*/
101+
export function enforcePublicGroqFilter(query: string): string {
102+
const allowedTypesJson = JSON.stringify(PUBLIC_MCP_DOCUMENT_TYPES);
103+
const guard = `_type in ${allowedTypesJson} && !(_id in path("private.**")) && !(_id in path("drafts.**"))`;
104+
return query.replace(/\*\s*\[/g, `*[${guard} && `);
105+
}
50106
export function isForbiddenDocumentId(documentId: string): boolean {
51107
if (!documentId || typeof documentId !== "string") {
52108
return true;
53109
}
54110
const id = documentId.trim().toLowerCase();
55111
return (
112+
id === "settings" ||
56113
id.startsWith("private.") ||
57114
id.startsWith("drafts.") ||
58115
id.startsWith("trend.") ||
59116
id.startsWith("versions.") ||
60117
id.startsWith("_.") ||
118+
id.startsWith("secrets.") ||
119+
id.startsWith("file-") ||
120+
id.startsWith("image-") ||
61121
id.startsWith("booking-") ||
62122
id.startsWith("sponsorlead-") ||
63-
id.startsWith("sponsorpool-")
123+
id.startsWith("sponsorpool-") ||
124+
id.startsWith("opsdigest") ||
125+
id.startsWith("sponsorshiprequest") ||
126+
id.startsWith("previewsession") ||
127+
id.startsWith("short-") ||
128+
id.startsWith("event-") ||
129+
id.startsWith("talk-") ||
130+
id.startsWith("headshot-") ||
131+
id.startsWith("automatedvideo-") ||
132+
id.startsWith("contentidea-")
64133
);
65134
}
66135

‎apps/site/src/lib/sanity/queries/search.ts‎

Lines changed: 72 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,18 @@ import { defineQuery } from "groq";
66
* direct title/name matches strictly outrank long transcript matches.
77
*/
88
export const semanticSearchQuery = defineQuery(`
9-
*[_type in ["post", "podcast", "author", "guest"] && defined(slug.current) && ($type == null || $type == "all" || _type == $type)]
9+
*[
10+
_type in ["post", "podcast", "course", "lesson", "transcript", "author", "guest", "sponsor", "newsletter"] &&
11+
(defined(slug.current) || _type == "transcript") &&
12+
coalesce(archived, false) != true &&
13+
(_type != "transcript" || !defined(contentRef) || contentRef->_type in ["post", "podcast", "course", "lesson"]) &&
14+
(
15+
$type == null ||
16+
$type == "all" ||
17+
($type == "youtube" && (_type == "transcript" || defined(youtube) || defined(listenLinks.youtube) || defined(transcript->youtubeId))) ||
18+
_type == $type
19+
)
20+
]
1021
| score(
1122
boost(title == $searchTerm, 100),
1223
boost(name == $searchTerm, 100),
@@ -15,20 +26,33 @@ export const semanticSearchQuery = defineQuery(`
1526
boost(title match $searchTerm, 30),
1627
boost(name match $searchTerm, 30),
1728
boost(excerpt match $searchTerm + "*", 8),
29+
boost(summary match $searchTerm + "*", 8),
30+
boost(description match $searchTerm + "*", 6),
1831
text::semanticSimilarity($searchTerm)
1932
)
20-
| order(_score desc)[0...24] {
33+
| order(_score desc)[0...28] {
2134
_id,
2235
_type,
2336
"title": coalesce(title, name, "Untitled"),
24-
"slug": slug.current,
25-
"excerpt": coalesce(excerpt, transcript->summary),
26-
"transcriptSummary": transcript->summary,
27-
"youtube": coalesce(youtube, listenLinks.youtube, transcript->youtubeUrl),
37+
"slug": coalesce(slug.current, contentRef->slug.current, youtubeId),
38+
"courseSlug": select(
39+
_type == "lesson" => *[_type == "course" && references(^._id)][0].slug.current,
40+
contentRef->_type == "lesson" => *[_type == "course" && references(^.contentRef._ref)][0].slug.current,
41+
null
42+
),
43+
"parentType": contentRef->_type,
44+
"parentSlug": contentRef->slug.current,
45+
"excerpt": coalesce(excerpt, summary, description, transcript->summary),
46+
"transcriptSummary": coalesce(summary, transcript->summary),
47+
"youtube": coalesce(youtubeUrl, youtube, listenLinks.youtube, transcript->youtubeUrl, contentRef->youtube),
48+
"youtubeId": coalesce(youtubeId, transcript->youtubeId),
49+
"duration": coalesce(duration, transcript->duration),
50+
"durationSeconds": coalesce(durationSeconds, transcript->durationSeconds),
51+
"viewCount": coalesce(statistics.viewCount, statistics.youtube.viewCount, transcript->statistics.viewCount),
2852
listenLinks,
2953
"spotifyLink": spotify.link,
3054
coverImage,
31-
date,
55+
"date": coalesce(date, publishedAt, _createdAt),
3256
_score
3357
}
3458
`);
@@ -37,34 +61,61 @@ export const semanticSearchQuery = defineQuery(`
3761
* Fallback text-matching search query if dataset embeddings are pending or unconfigured.
3862
*/
3963
export const textSearchFallbackQuery = defineQuery(`
40-
*[_type in ["post", "podcast", "author", "guest"] && defined(slug.current) && ($type == null || $type == "all" || _type == $type) && (
41-
title match $searchTerm + "*" ||
42-
name match $searchTerm + "*" ||
43-
excerpt match $searchTerm + "*" ||
44-
transcript->summary match $searchTerm + "*" ||
45-
transcript->fullText match $searchTerm + "*"
46-
)]
64+
*[
65+
_type in ["post", "podcast", "course", "lesson", "transcript", "author", "guest", "sponsor", "newsletter"] &&
66+
(defined(slug.current) || _type == "transcript") &&
67+
coalesce(archived, false) != true &&
68+
(_type != "transcript" || !defined(contentRef) || contentRef->_type in ["post", "podcast", "course", "lesson"]) &&
69+
(
70+
$type == null ||
71+
$type == "all" ||
72+
($type == "youtube" && (_type == "transcript" || defined(youtube) || defined(listenLinks.youtube) || defined(transcript->youtubeId))) ||
73+
_type == $type
74+
) && (
75+
title match $searchTerm + "*" ||
76+
name match $searchTerm + "*" ||
77+
excerpt match $searchTerm + "*" ||
78+
summary match $searchTerm + "*" ||
79+
description match $searchTerm + "*" ||
80+
fullText match $searchTerm + "*" ||
81+
transcript->summary match $searchTerm + "*" ||
82+
transcript->fullText match $searchTerm + "*"
83+
)
84+
]
4785
| score(
4886
boost(title == $searchTerm, 100),
4987
boost(name == $searchTerm, 100),
5088
boost(title match $searchTerm + "*", 50),
5189
boost(name match $searchTerm + "*", 50),
5290
boost(title match $searchTerm, 30),
5391
boost(name match $searchTerm, 30),
54-
boost(excerpt match $searchTerm + "*", 8)
92+
boost(excerpt match $searchTerm + "*", 8),
93+
boost(summary match $searchTerm + "*", 8),
94+
boost(description match $searchTerm + "*", 6)
5595
)
56-
| order(_score desc)[0...24] {
96+
| order(_score desc)[0...28] {
5797
_id,
5898
_type,
5999
"title": coalesce(title, name, "Untitled"),
60-
"slug": slug.current,
61-
"excerpt": coalesce(excerpt, transcript->summary),
62-
"transcriptSummary": transcript->summary,
63-
"youtube": coalesce(youtube, listenLinks.youtube, transcript->youtubeUrl),
100+
"slug": coalesce(slug.current, contentRef->slug.current, youtubeId),
101+
"courseSlug": select(
102+
_type == "lesson" => *[_type == "course" && references(^._id)][0].slug.current,
103+
contentRef->_type == "lesson" => *[_type == "course" && references(^.contentRef._ref)][0].slug.current,
104+
null
105+
),
106+
"parentType": contentRef->_type,
107+
"parentSlug": contentRef->slug.current,
108+
"excerpt": coalesce(excerpt, summary, description, transcript->summary),
109+
"transcriptSummary": coalesce(summary, transcript->summary),
110+
"youtube": coalesce(youtubeUrl, youtube, listenLinks.youtube, transcript->youtubeUrl, contentRef->youtube),
111+
"youtubeId": coalesce(youtubeId, transcript->youtubeId),
112+
"duration": coalesce(duration, transcript->duration),
113+
"durationSeconds": coalesce(durationSeconds, transcript->durationSeconds),
114+
"viewCount": coalesce(statistics.viewCount, statistics.youtube.viewCount, transcript->statistics.viewCount),
64115
listenLinks,
65116
"spotifyLink": spotify.link,
66117
coverImage,
67-
date,
118+
"date": coalesce(date, publishedAt, _createdAt),
68119
_score
69120
}
70121
`);

0 commit comments

Comments
 (0)