@@ -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 ) {
0 commit comments