From 0e66361a0dc4c31b4957d0be8751a3265615ef9e Mon Sep 17 00:00:00 2001 From: Kamran Ahmed Date: Wed, 11 Jun 2025 13:49:17 +0100 Subject: [PATCH 1/7] Add client id from the client side --- src/components/Analytics/analytics.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/components/Analytics/analytics.ts b/src/components/Analytics/analytics.ts index 39c1a9137..e58034615 100644 --- a/src/components/Analytics/analytics.ts +++ b/src/components/Analytics/analytics.ts @@ -23,6 +23,8 @@ declare global { window.fireEvent = (props) => { const { action, category, label, value, callback } = props; + const eventId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; + if (['course', 'ai_tutor'].includes(category)) { const url = new URL(import.meta.env.PUBLIC_API_URL); url.pathname = '/api/_t'; @@ -30,6 +32,7 @@ window.fireEvent = (props) => { url.searchParams.set('category', category); url.searchParams.set('label', label ?? ''); url.searchParams.set('value', value ?? ''); + url.searchParams.set('event_id', eventId); httpPost(url.toString(), {}).catch(console.error); } @@ -49,6 +52,8 @@ window.fireEvent = (props) => { event_category: category, event_label: label, value: value, + event_id: eventId, + source: 'client', ...(callback ? { event_callback: callback } : {}), }); }; From ab888e8f738df176e87e972e5f59a0355b6ed2e5 Mon Sep 17 00:00:00 2001 From: Kamran Ahmed Date: Wed, 11 Jun 2025 14:45:20 +0100 Subject: [PATCH 2/7] Chat history UI --- .../AIChatHistory/AIChatHistory.tsx | 160 +++++++++--------- 1 file changed, 81 insertions(+), 79 deletions(-) diff --git a/src/components/AIChatHistory/AIChatHistory.tsx b/src/components/AIChatHistory/AIChatHistory.tsx index 12982c9f2..1f365b623 100644 --- a/src/components/AIChatHistory/AIChatHistory.tsx +++ b/src/components/AIChatHistory/AIChatHistory.tsx @@ -61,91 +61,93 @@ export function AIChatHistory(props: AIChatHistoryProps) { setIsChatHistoryLoading(false); }, [hasError]); + if (isLoading || isBillingDetailsLoading) { + return ( + +
+
+ +
+
+
+ ); + } + return (
- {showGlobalLoader && ( -
- -
+ {isPaidUser && ( + { + setKeyTrigger((keyTrigger) => keyTrigger + 1); + + if (chatHistoryId === null) { + setChatHistoryId(undefined); + window.history.replaceState(null, '', '/ai/chat'); + return; + } + + // so that we can show the loading state when the chat history is not fetched yet + // it will help us to avoid the flash of content + const hasAlreadyFetched = queryClient.getQueryData( + chatHistoryOptions(chatHistoryId).queryKey, + ); + + if (!hasAlreadyFetched) { + setIsChatHistoryLoading(true); + } + + setChatHistoryId(chatHistoryId); + window.history.replaceState( + null, + '', + `/ai/chat/${chatHistoryId}`, + ); + }} + onDelete={(deletedChatHistoryId) => { + const isCurrentChatHistory = + deletedChatHistoryId === chatHistoryId; + if (!isCurrentChatHistory) { + return; + } + + setChatHistoryId(undefined); + window.history.replaceState(null, '', '/ai/chat'); + }} + /> )} - {!showGlobalLoader && ( - <> - {isPaidUser && ( - { - setKeyTrigger((keyTrigger) => keyTrigger + 1); - - if (chatHistoryId === null) { - setChatHistoryId(undefined); - window.history.replaceState(null, '', '/ai/chat'); - return; - } - - // so that we can show the loading state when the chat history is not fetched yet - // it will help us to avoid the flash of content - const hasAlreadyFetched = queryClient.getQueryData( - chatHistoryOptions(chatHistoryId).queryKey, - ); - - if (!hasAlreadyFetched) { - setIsChatHistoryLoading(true); - } - - setChatHistoryId(chatHistoryId); - window.history.replaceState( - null, - '', - `/ai/chat/${chatHistoryId}`, - ); - }} - onDelete={(deletedChatHistoryId) => { - const isCurrentChatHistory = - deletedChatHistoryId === chatHistoryId; - if (!isCurrentChatHistory) { - return; - } - - setChatHistoryId(undefined); - window.history.replaceState(null, '', '/ai/chat'); - }} - /> - )} - -
- {isChatHistoryLoading && !hasError && ( -
- -
- )} - - {!isChatHistoryLoading && hasError && ( -
- -
- )} - - {!isChatHistoryLoading && !hasError && ( - { - setChatHistoryId(id); - window.history.replaceState(null, '', `/ai/chat/${id}`); - queryClient.invalidateQueries({ - predicate: (query) => { - return query.queryKey[0] === 'list-chat-history'; - }, - }); - }} - /> - )} +
+ {isChatHistoryLoading && !hasError && ( +
+
- - )} + )} + + {!isChatHistoryLoading && hasError && ( +
+ +
+ )} + + {!isChatHistoryLoading && !hasError && ( + { + setChatHistoryId(id); + window.history.replaceState(null, '', `/ai/chat/${id}`); + queryClient.invalidateQueries({ + predicate: (query) => { + return query.queryKey[0] === 'list-chat-history'; + }, + }); + }} + /> + )} +
); From 8cf0a7b9275ca25c972b09db9c589dc36085aff7 Mon Sep 17 00:00:00 2001 From: Kamran Ahmed Date: Wed, 11 Jun 2025 15:20:48 +0100 Subject: [PATCH 3/7] Update chat history --- src/components/AIChatHistory/AIChatHistory.tsx | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/components/AIChatHistory/AIChatHistory.tsx b/src/components/AIChatHistory/AIChatHistory.tsx index 1f365b623..6a9ea706b 100644 --- a/src/components/AIChatHistory/AIChatHistory.tsx +++ b/src/components/AIChatHistory/AIChatHistory.tsx @@ -8,6 +8,7 @@ import { AIChatLayout } from './AIChatLayout'; import { ListChatHistory } from './ListChatHistory'; import { billingDetailsOptions } from '../../queries/billing'; import { ChatHistoryError } from './ChatHistoryError'; +import { useClientMount } from '../../hooks/use-client-mount'; type AIChatHistoryProps = { chatHistoryId?: string; @@ -16,8 +17,8 @@ type AIChatHistoryProps = { export function AIChatHistory(props: AIChatHistoryProps) { const { chatHistoryId: defaultChatHistoryId } = props; + const isClientMounted = useClientMount(); const [keyTrigger, setKeyTrigger] = useState(0); - const [isLoading, setIsLoading] = useState(true); const [isChatHistoryLoading, setIsChatHistoryLoading] = useState(true); const [chatHistoryId, setChatHistoryId] = useState( defaultChatHistoryId || undefined, @@ -27,6 +28,7 @@ export function AIChatHistory(props: AIChatHistoryProps) { chatHistoryOptions(chatHistoryId), queryClient, ); + const { data: userBillingDetails, isLoading: isBillingDetailsLoading, @@ -36,7 +38,6 @@ export function AIChatHistory(props: AIChatHistoryProps) { useEffect(() => { if (!chatHistoryId) { - setIsLoading(false); setIsChatHistoryLoading(false); return; } @@ -45,11 +46,9 @@ export function AIChatHistory(props: AIChatHistoryProps) { return; } - setIsLoading(false); setIsChatHistoryLoading(false); }, [data, chatHistoryId]); - const showGlobalLoader = isLoading || isBillingDetailsLoading; const hasError = chatHistoryError || billingDetailsError; useEffect(() => { @@ -57,11 +56,10 @@ export function AIChatHistory(props: AIChatHistoryProps) { return; } - setIsLoading(false); setIsChatHistoryLoading(false); }, [hasError]); - if (isLoading || isBillingDetailsLoading) { + if (!isClientMounted || isBillingDetailsLoading) { return (
From 0ff370c6844d089d362573985ab26d2670402adb Mon Sep 17 00:00:00 2001 From: Kamran Ahmed Date: Wed, 11 Jun 2025 15:54:11 +0100 Subject: [PATCH 4/7] Update chat history --- .../AIChatHistory/AIChatHistory.tsx | 96 ++++++++++--------- .../AIChatHistory/ListChatHistory.tsx | 2 +- 2 files changed, 54 insertions(+), 44 deletions(-) diff --git a/src/components/AIChatHistory/AIChatHistory.tsx b/src/components/AIChatHistory/AIChatHistory.tsx index 6a9ea706b..01456d932 100644 --- a/src/components/AIChatHistory/AIChatHistory.tsx +++ b/src/components/AIChatHistory/AIChatHistory.tsx @@ -3,7 +3,7 @@ import { queryClient } from '../../stores/query-client'; import { chatHistoryOptions } from '../../queries/chat-history'; import { AIChat } from '../AIChat/AIChat'; import { Loader2Icon } from 'lucide-react'; -import { useEffect, useState } from 'react'; +import { useEffect, useState, useCallback } from 'react'; import { AIChatLayout } from './AIChatLayout'; import { ListChatHistory } from './ListChatHistory'; import { billingDetailsOptions } from '../../queries/billing'; @@ -34,8 +34,54 @@ export function AIChatHistory(props: AIChatHistoryProps) { isLoading: isBillingDetailsLoading, error: billingDetailsError, } = useQuery(billingDetailsOptions(), queryClient); + + // extracted callbacks to keep JSX tidy while preserving behaviour + const handleChatHistoryClick = useCallback( + (nextChatHistoryId: string | null) => { + // bump key to force fresh AIChat mount on each history switch + setKeyTrigger((key) => key + 1); + + if (nextChatHistoryId === null) { + setChatHistoryId(undefined); + window.history.replaceState(null, '', '/ai/chat'); + return; + } + + // show loader only if the chat history hasn't been fetched before (avoids UI flash) + const hasAlreadyFetched = queryClient.getQueryData( + chatHistoryOptions(nextChatHistoryId).queryKey, + ); + + if (!hasAlreadyFetched) { + setIsChatHistoryLoading(true); + } + + setChatHistoryId(nextChatHistoryId); + window.history.replaceState(null, '', `/ai/chat/${nextChatHistoryId}`); + }, + [], + ); + + const handleDelete = useCallback( + (deletedChatHistoryId: string) => { + if (deletedChatHistoryId !== chatHistoryId) { + return; + } + + setChatHistoryId(undefined); + window.history.replaceState(null, '', '/ai/chat'); + }, + [chatHistoryId], + ); + const isPaidUser = userBillingDetails?.status === 'active'; + const hasError = chatHistoryError || billingDetailsError; + + // derived UI states to make JSX clearer + const showLoader = isChatHistoryLoading && !hasError; + const showError = !isChatHistoryLoading && Boolean(hasError); + useEffect(() => { if (!chatHistoryId) { setIsChatHistoryLoading(false); @@ -49,8 +95,6 @@ export function AIChatHistory(props: AIChatHistoryProps) { setIsChatHistoryLoading(false); }, [data, chatHistoryId]); - const hasError = chatHistoryError || billingDetailsError; - useEffect(() => { if (!hasError) { return; @@ -77,59 +121,25 @@ export function AIChatHistory(props: AIChatHistoryProps) { {isPaidUser && ( { - setKeyTrigger((keyTrigger) => keyTrigger + 1); - - if (chatHistoryId === null) { - setChatHistoryId(undefined); - window.history.replaceState(null, '', '/ai/chat'); - return; - } - - // so that we can show the loading state when the chat history is not fetched yet - // it will help us to avoid the flash of content - const hasAlreadyFetched = queryClient.getQueryData( - chatHistoryOptions(chatHistoryId).queryKey, - ); - - if (!hasAlreadyFetched) { - setIsChatHistoryLoading(true); - } - - setChatHistoryId(chatHistoryId); - window.history.replaceState( - null, - '', - `/ai/chat/${chatHistoryId}`, - ); - }} - onDelete={(deletedChatHistoryId) => { - const isCurrentChatHistory = - deletedChatHistoryId === chatHistoryId; - if (!isCurrentChatHistory) { - return; - } - - setChatHistoryId(undefined); - window.history.replaceState(null, '', '/ai/chat'); - }} + onChatHistoryClick={handleChatHistoryClick} + onDelete={handleDelete} /> )}
- {isChatHistoryLoading && !hasError && ( + {showLoader && (
- +
)} - {!isChatHistoryLoading && hasError && ( + {showError && (
)} - {!isChatHistoryLoading && !hasError && ( + {!showLoader && !showError && (
- {isEmptyHistory && ( + {isEmptyHistory && !isLoadingInfiniteQuery && (

No chat history

From 7ed5d90c13dcd65233e18e52433c18263a51d01e Mon Sep 17 00:00:00 2001 From: Kamran Ahmed Date: Wed, 11 Jun 2025 15:54:28 +0100 Subject: [PATCH 5/7] Update chat history --- src/components/AIChatHistory/AIChatHistory.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/components/AIChatHistory/AIChatHistory.tsx b/src/components/AIChatHistory/AIChatHistory.tsx index 01456d932..9787c9d5c 100644 --- a/src/components/AIChatHistory/AIChatHistory.tsx +++ b/src/components/AIChatHistory/AIChatHistory.tsx @@ -35,10 +35,8 @@ export function AIChatHistory(props: AIChatHistoryProps) { error: billingDetailsError, } = useQuery(billingDetailsOptions(), queryClient); - // extracted callbacks to keep JSX tidy while preserving behaviour const handleChatHistoryClick = useCallback( (nextChatHistoryId: string | null) => { - // bump key to force fresh AIChat mount on each history switch setKeyTrigger((key) => key + 1); if (nextChatHistoryId === null) { From aeb184cd057d96a7427ea999376063e18303a54d Mon Sep 17 00:00:00 2001 From: Kamran Ahmed Date: Wed, 11 Jun 2025 15:58:07 +0100 Subject: [PATCH 6/7] Fix ai chat not working --- src/pages/[roadmapId]/ai.astro | 42 +++++++++++++++++++++----------- src/pages/[roadmapId]/chat.astro | 33 ------------------------- 2 files changed, 28 insertions(+), 47 deletions(-) delete mode 100644 src/pages/[roadmapId]/chat.astro diff --git a/src/pages/[roadmapId]/ai.astro b/src/pages/[roadmapId]/ai.astro index d52b8be6d..ac5793536 100644 --- a/src/pages/[roadmapId]/ai.astro +++ b/src/pages/[roadmapId]/ai.astro @@ -1,5 +1,13 @@ --- -import { type RoadmapFrontmatter, getRoadmapIds } from '../../lib/roadmap'; +import { CheckSubscriptionVerification } from '../../components/Billing/CheckSubscriptionVerification'; +import { RoadmapAIChat } from '../../components/RoadmapAIChat/RoadmapAIChat'; +import SkeletonLayout from '../../layouts/SkeletonLayout.astro'; +import { AITutorLayout } from '../../components/AITutor/AITutorLayout'; +import { getRoadmapById, getRoadmapIds } from '../../lib/roadmap'; + +type Props = { + roadmapId: string; +}; export const prerender = true; @@ -11,19 +19,25 @@ export async function getStaticPaths() { })); } -interface Params extends Record { - roadmapId: string; -} +const { roadmapId } = Astro.params as Props; -const { roadmapId } = Astro.params as Params; -const roadmapFile = await import( - `../../data/roadmaps/${roadmapId}/${roadmapId}.md` -); +const roadmapDetail = await getRoadmapById(roadmapId); -const roadmapData = roadmapFile.frontmatter as RoadmapFrontmatter; -if (roadmapData.renderer !== 'editor') { - return Astro.rewrite(`/404`); -} - -return Astro.rewrite(`/ai/chat/${roadmapId}`); +const canonicalUrl = `https://roadmap.sh/${roadmapId}/ai`; +const roadmapBriefTitle = roadmapDetail.frontmatter.briefTitle; --- + + + + + + + diff --git a/src/pages/[roadmapId]/chat.astro b/src/pages/[roadmapId]/chat.astro deleted file mode 100644 index f49dddcba..000000000 --- a/src/pages/[roadmapId]/chat.astro +++ /dev/null @@ -1,33 +0,0 @@ ---- -import { CheckSubscriptionVerification } from '../../components/Billing/CheckSubscriptionVerification'; -import { RoadmapAIChat } from '../../components/RoadmapAIChat/RoadmapAIChat'; -import SkeletonLayout from '../../layouts/SkeletonLayout.astro'; -import { AITutorLayout } from '../../components/AITutor/AITutorLayout'; -import { getRoadmapById } from '../../lib/roadmap'; - -type Props = { - roadmapId: string; -}; - -const { roadmapId } = Astro.params as Props; - -const roadmapDetail = await getRoadmapById(roadmapId); - -const canonicalUrl = `https://roadmap.sh/${roadmapId}/ai`; -const roadmapBriefTitle = roadmapDetail.frontmatter.briefTitle; ---- - - - - - - - From c1415e55b75dcae024e1b75f17757744acc86b03 Mon Sep 17 00:00:00 2001 From: Kamran Ahmed Date: Wed, 11 Jun 2025 18:20:48 +0100 Subject: [PATCH 7/7] Update --- src/components/AIChat/AIChat.tsx | 3 +-- src/components/AIChatHistory/AIChatHistory.tsx | 2 +- src/components/AIChatHistory/ListChatHistory.tsx | 3 ++- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/AIChat/AIChat.tsx b/src/components/AIChat/AIChat.tsx index 9a6540938..440ab2384 100644 --- a/src/components/AIChat/AIChat.tsx +++ b/src/components/AIChat/AIChat.tsx @@ -18,7 +18,6 @@ import { useMutation, useQuery } from '@tanstack/react-query'; import { queryClient } from '../../stores/query-client'; import { billingDetailsOptions } from '../../queries/billing'; import { useToast } from '../../hooks/use-toast'; -import { readStream } from '../../lib/ai'; import { markdownToHtml } from '../../lib/markdown'; import { ChatHistory } from './ChatHistory'; import { PersonalizedResponseForm } from './PersonalizedResponseForm'; @@ -444,7 +443,7 @@ export function AIChat(props: AIChatProps) {
-

Upgrade to Pro to keep your conversations.

+

Your chat history is not saved.