Skip to content

Commit 547f693

Browse files
committed
fix(site): compact mobile fastpath badge and add rich preview thumbnails in Agent AJ drawer
1 parent 7d8cb45 commit 547f693

4 files changed

Lines changed: 206 additions & 71 deletions

File tree

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

Lines changed: 105 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -707,13 +707,22 @@ import AjAnimatedMascot from "@/components/AjAnimatedMascot.astro";
707707
.replace(/^https?:\/\/[a-z0-9-]+-codingcatdev-dev\.codingcatdev\.workers\.dev(?=\/|$)/i, "");
708708
}
709709

710+
interface PreviewCardItem {
711+
title: string;
712+
url: string;
713+
type?: string;
714+
excerpt?: string;
715+
youtubeUrl?: string;
716+
youtubeId?: string;
717+
thumbnailUrl?: string;
718+
duration?: string;
719+
viewCount?: number;
720+
listenLinks?: Record<string, string>;
721+
}
722+
710723
function renderRichAssistantContent(container: HTMLElement, rawText: string) {
711-
const providerMap = new Map<string, Record<string, string>>();
712-
const providerItems: Array<{
713-
title: string;
714-
url: string;
715-
listenLinks: Record<string, string>;
716-
}> = [];
724+
const previewMap = new Map<string, PreviewCardItem>();
725+
const previewItems: PreviewCardItem[] = [];
717726
const trailerMatch = rawText.match(
718727
/<!--AJ_PODCAST_PROVIDERS:([\s\S]*?)-->/,
719728
);
@@ -722,10 +731,14 @@ import AjAnimatedMascot from "@/components/AjAnimatedMascot.astro";
722731
const parsed = JSON.parse(trailerMatch[1]);
723732
if (Array.isArray(parsed)) {
724733
for (const item of parsed) {
725-
if (item?.url && item?.listenLinks) {
734+
if (item?.url && item?.title) {
726735
const normUrl = normalizeInternalPath(item.url);
727-
providerMap.set(normUrl, item.listenLinks);
728-
providerItems.push({ ...item, url: normUrl });
736+
const normalizedItem: PreviewCardItem = {
737+
...item,
738+
url: normUrl,
739+
};
740+
previewMap.set(normUrl, normalizedItem);
741+
previewItems.push(normalizedItem);
729742
}
730743
}
731744
}
@@ -752,48 +765,64 @@ import AjAnimatedMascot from "@/components/AjAnimatedMascot.astro";
752765
}
753766

754767
const internalLinkRegex =
755-
/\[([^\]]+)\]\(((\/(?:podcast|post|course)\/[a-zA-Z0-9_\-.\/]+))\)/gi;
756-
const internalLinks: Array<{
757-
title: string;
758-
url: string;
759-
type: "podcast" | "post" | "course" | "lesson";
760-
}> = [];
768+
/\[([^\]]+)\]\(((\/(?:podcast|post|course|author|guest|newsletter|sponsor)\/[a-zA-Z0-9_\-.\/]+))\)/gi;
769+
const internalLinks: PreviewCardItem[] = [];
761770
let intMatch: RegExpExecArray | null;
762771
while ((intMatch = internalLinkRegex.exec(cleanedText)) !== null) {
763772
const linkUrl = intMatch[2];
764773
const linkTitle = intMatch[1];
765-
const type = linkUrl.startsWith("/podcast")
774+
const fromTrailer = previewMap.get(linkUrl);
775+
const inferredType = linkUrl.startsWith("/podcast")
766776
? "podcast"
767777
: linkUrl.startsWith("/course") && linkUrl.includes("/lesson/")
768778
? "lesson"
769779
: linkUrl.startsWith("/course")
770780
? "course"
771-
: "post";
781+
: linkUrl.startsWith("/author")
782+
? "author"
783+
: linkUrl.startsWith("/guest")
784+
? "guest"
785+
: linkUrl.startsWith("/newsletter")
786+
? "newsletter"
787+
: "post";
772788
if (!internalLinks.some((l) => l.url === linkUrl)) {
773-
internalLinks.push({ title: linkTitle, url: linkUrl, type });
789+
internalLinks.push({
790+
...fromTrailer,
791+
title: fromTrailer?.title || linkTitle,
792+
url: linkUrl,
793+
type: fromTrailer?.type || inferredType,
794+
});
774795
}
775796
}
776797

777-
for (const pItem of providerItems) {
778-
if (
779-
!internalLinks.some((l) => l.url === pItem.url) &&
780-
(internalLinks.length === 0 ||
781-
cleanedText
782-
.toLowerCase()
783-
.includes(pItem.title.toLowerCase().slice(0, 24)))
784-
) {
798+
for (const pItem of previewItems) {
799+
if (!internalLinks.some((l) => l.url === pItem.url)) {
785800
internalLinks.push({
786-
title: pItem.title,
787-
url: pItem.url,
788-
type: "podcast",
801+
...pItem,
802+
type:
803+
pItem.type ||
804+
(pItem.url.startsWith("/podcast")
805+
? "podcast"
806+
: pItem.url.startsWith("/course") && pItem.url.includes("/lesson/")
807+
? "lesson"
808+
: pItem.url.startsWith("/course")
809+
? "course"
810+
: "post"),
789811
});
790812
}
791813
}
792814

815+
// Avoid duplicate standalone YouTube cards for videos already shown on rich preview cards
816+
for (const item of internalLinks) {
817+
if (item.youtubeId) {
818+
youtubeIds.delete(item.youtubeId);
819+
}
820+
}
821+
793822
let html = escapeHtml(cleanedText);
794823
html = html.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
795824
html = html.replace(
796-
/^[ \t]*-[ \t]+(.*)$/gm,
825+
/^[ \t]*[*-][ \t]+(.*)$/gm,
797826
"<li class='ml-4 list-disc'>$1</li>",
798827
);
799828
html = html.replace(
@@ -839,9 +868,10 @@ import AjAnimatedMascot from "@/components/AjAnimatedMascot.astro";
839868

840869
if (internalLinks.length > 0) {
841870
cardsHtml += `<div class="mt-3 flex flex-col gap-2">`;
842-
for (const link of internalLinks) {
871+
for (const link of internalLinks.slice(0, 6)) {
843872
const isPodcast = link.type === "podcast";
844-
const linksForEp = providerMap.get(link.url);
873+
const isExternal = link.url.startsWith("http");
874+
const linksForEp = link.listenLinks;
845875
let providerBadgesHtml = "";
846876
if (isPodcast && linksForEp) {
847877
const badges = PROVIDER_CONFIG.filter((p) =>
@@ -877,15 +907,40 @@ import AjAnimatedMascot from "@/components/AjAnimatedMascot.astro";
877907
? "Course"
878908
: link.type === "lesson"
879909
? "Lesson"
880-
: "Blog Post";
910+
: link.type === "newsletter"
911+
? "Newsletter"
912+
: link.type === "author"
913+
? "Author"
914+
: link.type === "guest"
915+
? "Guest"
916+
: "Blog Post";
917+
918+
const thumbHtml = link.thumbnailUrl
919+
? `
920+
<div class="relative shrink-0 w-20 aspect-video rounded-lg overflow-hidden border border-border bg-black">
921+
<img src="${escapeHtml(link.thumbnailUrl)}" alt="" loading="lazy" class="h-full w-full object-cover" />
922+
${link.duration ? `<span class="absolute bottom-0.5 right-0.5 rounded bg-black/80 px-1 text-[9px] font-mono text-white">${escapeHtml(link.duration)}</span>` : ""}
923+
</div>
924+
`
925+
: "";
926+
927+
const ytBadgeHtml = link.youtubeUrl
928+
? `<span class="inline-flex shrink-0 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>`
929+
: "";
930+
881931
cardsHtml += `
882932
<div class="flex flex-col p-2.5 rounded-xl border border-border bg-background/70 hover:border-primary/50 transition-all text-left">
883-
<a href="${link.url}" class="aj-internal-nav group flex items-center justify-between gap-2.5 no-underline">
884-
<div class="min-w-0">
933+
<a href="${escapeHtml(link.url)}" ${isExternal ? 'target="_blank" rel="noopener noreferrer"' : ""} class="${isExternal ? "" : "aj-internal-nav "}group flex items-center justify-between gap-2.5 no-underline">
934+
${thumbHtml}
935+
<div class="min-w-0 flex-1">
885936
<div class="text-xs font-semibold text-foreground group-hover:text-primary transition-colors truncate">${escapeHtml(link.title)}</div>
886-
<div class="text-[10px] font-mono text-muted-foreground uppercase tracking-wider">${typeBadgeLabel}</div>
937+
<div class="mt-0.5 flex items-center gap-1.5">
938+
<span class="text-[10px] font-mono text-muted-foreground uppercase tracking-wider">${typeBadgeLabel}</span>
939+
${ytBadgeHtml}
940+
</div>
941+
${link.excerpt ? `<div class="mt-0.5 text-[11px] text-muted-foreground line-clamp-1">${escapeHtml(link.excerpt)}</div>` : ""}
887942
</div>
888-
<span class="shrink-0 text-xs font-medium text-primary">Open →</span>
943+
<span class="shrink-0 text-xs font-medium text-primary">${isExternal ? "Watch →" : "Open →"}</span>
889944
</a>
890945
${providerBadgesHtml}
891946
</div>
@@ -897,6 +952,15 @@ import AjAnimatedMascot from "@/components/AjAnimatedMascot.astro";
897952
container.innerHTML = html + cardsHtml;
898953
}
899954

955+
function renderTelemetryBadge(el: HTMLElement | null | undefined, rawTelemetry: string) {
956+
if (!el || !rawTelemetry) return;
957+
const cleanDetail = rawTelemetry.replace(/^⚡\s*/, "").trim();
958+
el.title = cleanDetail;
959+
el.setAttribute("aria-label", cleanDetail);
960+
el.innerHTML = `<span aria-hidden="true">⚡</span><span class="hidden sm:inline"> ${escapeHtml(cleanDetail)}</span>`;
961+
el.classList.remove("hidden");
962+
}
963+
900964
function appendMessageElement(
901965
role: "user" | "assistant",
902966
initialText = "",
@@ -921,7 +985,7 @@ import AjAnimatedMascot from "@/components/AjAnimatedMascot.astro";
921985
telemetryEl.className =
922986
"font-mono text-[10px] text-primary/90 flex items-center gap-1.5";
923987
if (telemetryText) {
924-
telemetryEl.textContent = telemetryText;
988+
renderTelemetryBadge(telemetryEl, telemetryText);
925989
} else {
926990
telemetryEl.classList.add("hidden");
927991
}
@@ -957,8 +1021,7 @@ import AjAnimatedMascot from "@/components/AjAnimatedMascot.astro";
9571021
renderRichAssistantContent(contentEl, msg.text);
9581022
}
9591023
if (msg.telemetry && headerTelemetryPill) {
960-
headerTelemetryPill.textContent = msg.telemetry;
961-
headerTelemetryPill.classList.remove("hidden");
1024+
renderTelemetryBadge(headerTelemetryPill, msg.telemetry);
9621025
}
9631026
}
9641027
if (messagesContainer) {
@@ -1014,14 +1077,8 @@ import AjAnimatedMascot from "@/components/AjAnimatedMascot.astro";
10141077
if (extSource && extIntent) {
10151078
telemetryLabel = `⚡ ${extSource}: ${extIntent} (${extLatency ?? "0"}ms)`;
10161079
assistantEntry.telemetry = telemetryLabel;
1017-
if (telemetryEl) {
1018-
telemetryEl.textContent = telemetryLabel;
1019-
telemetryEl.classList.remove("hidden");
1020-
}
1021-
if (headerTelemetryPill) {
1022-
headerTelemetryPill.textContent = telemetryLabel;
1023-
headerTelemetryPill.classList.remove("hidden");
1024-
}
1080+
renderTelemetryBadge(telemetryEl, telemetryLabel);
1081+
renderTelemetryBadge(headerTelemetryPill, telemetryLabel);
10251082
}
10261083

10271084
if (!res.ok) {

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@
175175
<span
176176
id="modal-clairic-badge"
177177
class="hidden shrink-0 rounded-full border border-primary/30 bg-primary/10 px-2 py-0.5 font-mono text-[10px] font-medium text-primary"
178+
title="Extraction telemetry"
178179
></span>
179180
</div>
180181

@@ -730,7 +731,10 @@
730731
if (clairicBadge) {
731732
if (data.extraction) {
732733
clairicBadge.classList.remove("hidden");
733-
clairicBadge.textContent = `⚡ ${data.extraction.source}: ${data.extraction.intent} (${data.extraction.latencyMs}ms)`;
734+
const detailText = `${data.extraction.source}: ${data.extraction.intent} (${data.extraction.latencyMs}ms)`;
735+
clairicBadge.title = detailText;
736+
clairicBadge.setAttribute("aria-label", detailText);
737+
clairicBadge.innerHTML = `<span aria-hidden="true">⚡</span><span class="hidden sm:inline"> ${escapeHtml(detailText)}</span>`;
734738
} else {
735739
clairicBadge.classList.add("hidden");
736740
}

0 commit comments

Comments
 (0)