mirror of
https://github.com/kamranahmedse/developer-roadmap.git
synced 2025-09-03 06:12:53 +02:00
Merge global history
This commit is contained in:
@@ -18,7 +18,6 @@ import { useMutation, useQuery } from '@tanstack/react-query';
|
|||||||
import { queryClient } from '../../stores/query-client';
|
import { queryClient } from '../../stores/query-client';
|
||||||
import { billingDetailsOptions } from '../../queries/billing';
|
import { billingDetailsOptions } from '../../queries/billing';
|
||||||
import { useToast } from '../../hooks/use-toast';
|
import { useToast } from '../../hooks/use-toast';
|
||||||
import { readStream } from '../../lib/ai';
|
|
||||||
import { markdownToHtml } from '../../lib/markdown';
|
import { markdownToHtml } from '../../lib/markdown';
|
||||||
import { ChatHistory } from './ChatHistory';
|
import { ChatHistory } from './ChatHistory';
|
||||||
import { PersonalizedResponseForm } from './PersonalizedResponseForm';
|
import { PersonalizedResponseForm } from './PersonalizedResponseForm';
|
||||||
@@ -443,7 +442,7 @@ export function AIChat(props: AIChatProps) {
|
|||||||
<div className="pointer-events-auto flex items-center justify-between gap-2">
|
<div className="pointer-events-auto flex items-center justify-between gap-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<LockIcon className="size-4" strokeWidth={2.5} />
|
<LockIcon className="size-4" strokeWidth={2.5} />
|
||||||
<p>Upgrade to Pro to keep your conversations.</p>
|
<p>Your chat history is not saved.</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
@@ -3,11 +3,12 @@ import { queryClient } from '../../stores/query-client';
|
|||||||
import { chatHistoryOptions } from '../../queries/chat-history';
|
import { chatHistoryOptions } from '../../queries/chat-history';
|
||||||
import { AIChat, aiChatRenderer } from '../AIChat/AIChat';
|
import { AIChat, aiChatRenderer } from '../AIChat/AIChat';
|
||||||
import { Loader2Icon } from 'lucide-react';
|
import { Loader2Icon } from 'lucide-react';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState, useCallback } from 'react';
|
||||||
import { AIChatLayout } from './AIChatLayout';
|
import { AIChatLayout } from './AIChatLayout';
|
||||||
import { ListChatHistory } from './ListChatHistory';
|
import { ListChatHistory } from './ListChatHistory';
|
||||||
import { billingDetailsOptions } from '../../queries/billing';
|
import { billingDetailsOptions } from '../../queries/billing';
|
||||||
import { ChatHistoryError } from './ChatHistoryError';
|
import { ChatHistoryError } from './ChatHistoryError';
|
||||||
|
import { useClientMount } from '../../hooks/use-client-mount';
|
||||||
|
|
||||||
type AIChatHistoryProps = {
|
type AIChatHistoryProps = {
|
||||||
chatHistoryId?: string;
|
chatHistoryId?: string;
|
||||||
@@ -16,8 +17,8 @@ type AIChatHistoryProps = {
|
|||||||
export function AIChatHistory(props: AIChatHistoryProps) {
|
export function AIChatHistory(props: AIChatHistoryProps) {
|
||||||
const { chatHistoryId: defaultChatHistoryId } = props;
|
const { chatHistoryId: defaultChatHistoryId } = props;
|
||||||
|
|
||||||
|
const isClientMounted = useClientMount();
|
||||||
const [keyTrigger, setKeyTrigger] = useState(0);
|
const [keyTrigger, setKeyTrigger] = useState(0);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
|
||||||
const [isChatHistoryLoading, setIsChatHistoryLoading] = useState(true);
|
const [isChatHistoryLoading, setIsChatHistoryLoading] = useState(true);
|
||||||
const [chatHistoryId, setChatHistoryId] = useState<string | undefined>(
|
const [chatHistoryId, setChatHistoryId] = useState<string | undefined>(
|
||||||
defaultChatHistoryId || undefined,
|
defaultChatHistoryId || undefined,
|
||||||
@@ -27,16 +28,60 @@ export function AIChatHistory(props: AIChatHistoryProps) {
|
|||||||
chatHistoryOptions(chatHistoryId, aiChatRenderer),
|
chatHistoryOptions(chatHistoryId, aiChatRenderer),
|
||||||
queryClient,
|
queryClient,
|
||||||
);
|
);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: userBillingDetails,
|
data: userBillingDetails,
|
||||||
isLoading: isBillingDetailsLoading,
|
isLoading: isBillingDetailsLoading,
|
||||||
error: billingDetailsError,
|
error: billingDetailsError,
|
||||||
} = useQuery(billingDetailsOptions(), queryClient);
|
} = useQuery(billingDetailsOptions(), queryClient);
|
||||||
|
|
||||||
|
const handleChatHistoryClick = useCallback(
|
||||||
|
(nextChatHistoryId: string | null) => {
|
||||||
|
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');
|
||||||
|
setKeyTrigger((key) => key + 1);
|
||||||
|
},
|
||||||
|
[chatHistoryId],
|
||||||
|
);
|
||||||
|
|
||||||
const isPaidUser = userBillingDetails?.status === 'active';
|
const isPaidUser = userBillingDetails?.status === 'active';
|
||||||
|
|
||||||
|
const hasError = chatHistoryError || billingDetailsError;
|
||||||
|
|
||||||
|
const showLoader = isChatHistoryLoading && !hasError;
|
||||||
|
const showError = !isChatHistoryLoading && Boolean(hasError);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!chatHistoryId) {
|
if (!chatHistoryId) {
|
||||||
setIsLoading(false);
|
|
||||||
setIsChatHistoryLoading(false);
|
setIsChatHistoryLoading(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -45,107 +90,70 @@ export function AIChatHistory(props: AIChatHistoryProps) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsLoading(false);
|
|
||||||
setIsChatHistoryLoading(false);
|
setIsChatHistoryLoading(false);
|
||||||
}, [data, chatHistoryId]);
|
}, [data, chatHistoryId]);
|
||||||
|
|
||||||
const showGlobalLoader = isLoading || isBillingDetailsLoading;
|
|
||||||
const hasError = chatHistoryError || billingDetailsError;
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!hasError) {
|
if (!hasError) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsLoading(false);
|
|
||||||
setIsChatHistoryLoading(false);
|
setIsChatHistoryLoading(false);
|
||||||
}, [hasError]);
|
}, [hasError]);
|
||||||
|
|
||||||
|
if (!isClientMounted || isBillingDetailsLoading) {
|
||||||
|
return (
|
||||||
|
<AIChatLayout>
|
||||||
|
<div className="relative flex grow">
|
||||||
|
<div className="absolute inset-0 z-20 flex items-center justify-center">
|
||||||
|
<Loader2Icon className="h-8 w-8 animate-spin stroke-[2.5] text-gray-400/80" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AIChatLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AIChatLayout>
|
<AIChatLayout>
|
||||||
<div className="relative flex grow">
|
<div className="relative flex grow">
|
||||||
{showGlobalLoader && (
|
{isPaidUser && (
|
||||||
<div className="absolute inset-0 z-20 flex items-center justify-center">
|
<ListChatHistory
|
||||||
<Loader2Icon className="h-8 w-8 animate-spin stroke-[2.5]" />
|
activeChatHistoryId={chatHistoryId}
|
||||||
</div>
|
onChatHistoryClick={handleChatHistoryClick}
|
||||||
|
onDelete={handleDelete}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!showGlobalLoader && (
|
<div className="relative flex grow">
|
||||||
<>
|
{showLoader && (
|
||||||
{isPaidUser && (
|
<div className="absolute inset-0 z-20 flex items-center justify-center">
|
||||||
<ListChatHistory
|
<Loader2Icon className="h-8 w-8 animate-spin stroke-[2.5] text-gray-400/80" />
|
||||||
activeChatHistoryId={chatHistoryId}
|
|
||||||
onChatHistoryClick={(chatHistoryId) => {
|
|
||||||
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');
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="relative flex grow">
|
|
||||||
{isChatHistoryLoading && !hasError && (
|
|
||||||
<div className="absolute inset-0 z-20 flex items-center justify-center">
|
|
||||||
<Loader2Icon className="h-8 w-8 animate-spin stroke-[2.5]" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!isChatHistoryLoading && hasError && (
|
|
||||||
<div className="absolute inset-0 z-20 flex items-center justify-center">
|
|
||||||
<ChatHistoryError error={hasError} className="mt-0" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!isChatHistoryLoading && !hasError && (
|
|
||||||
<AIChat
|
|
||||||
key={keyTrigger}
|
|
||||||
messages={data?.messages}
|
|
||||||
chatHistoryId={chatHistoryId}
|
|
||||||
setChatHistoryId={(id) => {
|
|
||||||
setChatHistoryId(id);
|
|
||||||
window.history.replaceState(null, '', `/ai/chat/${id}`);
|
|
||||||
queryClient.invalidateQueries({
|
|
||||||
predicate: (query) => {
|
|
||||||
return query.queryKey[0] === 'list-chat-history';
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
)}
|
||||||
)}
|
|
||||||
|
{showError && (
|
||||||
|
<div className="absolute inset-0 z-20 flex items-center justify-center">
|
||||||
|
<ChatHistoryError error={hasError} className="mt-0" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!showLoader && !showError && (
|
||||||
|
<AIChat
|
||||||
|
key={keyTrigger}
|
||||||
|
messages={data?.messages}
|
||||||
|
chatHistoryId={chatHistoryId}
|
||||||
|
setChatHistoryId={(id) => {
|
||||||
|
setChatHistoryId(id);
|
||||||
|
window.history.replaceState(null, '', `/ai/chat/${id}`);
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
predicate: (query) => {
|
||||||
|
return query.queryKey[0] === 'list-chat-history';
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</AIChatLayout>
|
</AIChatLayout>
|
||||||
);
|
);
|
||||||
|
@@ -36,6 +36,7 @@ export function ListChatHistory(props: ListChatHistoryProps) {
|
|||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
const deviceType = getTailwindScreenDimension();
|
const deviceType = getTailwindScreenDimension();
|
||||||
const isMediumSize = ['sm', 'md'].includes(deviceType);
|
const isMediumSize = ['sm', 'md'].includes(deviceType);
|
||||||
|
|
||||||
setIsOpen(!isMediumSize);
|
setIsOpen(!isMediumSize);
|
||||||
setIsMobile(isMediumSize);
|
setIsMobile(isMediumSize);
|
||||||
}, []);
|
}, []);
|
||||||
@@ -111,7 +112,7 @@ export function ListChatHistory(props: ListChatHistoryProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
className="flex w-full items-center justify-center gap-2 rounded-lg bg-black p-2 text-sm text-white"
|
className="flex w-full items-center hover:opacity-80 justify-center gap-2 rounded-lg bg-black p-2 text-sm text-white"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (isMobile) {
|
if (isMobile) {
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
@@ -130,7 +131,7 @@ export function ListChatHistory(props: ListChatHistoryProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="scrollbar-track-transparent scrollbar-thin scrollbar-thumb-gray-300 -mx-2 mt-6 grow space-y-4 overflow-y-scroll px-2">
|
<div className="scrollbar-track-transparent scrollbar-thin scrollbar-thumb-gray-300 -mx-2 mt-6 grow space-y-4 overflow-y-scroll px-2">
|
||||||
{isEmptyHistory && (
|
{isEmptyHistory && !isLoadingInfiniteQuery && (
|
||||||
<div className="flex items-center justify-center">
|
<div className="flex items-center justify-center">
|
||||||
<p className="text-sm text-gray-500">No chat history</p>
|
<p className="text-sm text-gray-500">No chat history</p>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -23,6 +23,8 @@ declare global {
|
|||||||
window.fireEvent = (props) => {
|
window.fireEvent = (props) => {
|
||||||
const { action, category, label, value, callback } = 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)) {
|
if (['course', 'ai_tutor'].includes(category)) {
|
||||||
const url = new URL(import.meta.env.PUBLIC_API_URL);
|
const url = new URL(import.meta.env.PUBLIC_API_URL);
|
||||||
url.pathname = '/api/_t';
|
url.pathname = '/api/_t';
|
||||||
@@ -30,6 +32,7 @@ window.fireEvent = (props) => {
|
|||||||
url.searchParams.set('category', category);
|
url.searchParams.set('category', category);
|
||||||
url.searchParams.set('label', label ?? '');
|
url.searchParams.set('label', label ?? '');
|
||||||
url.searchParams.set('value', value ?? '');
|
url.searchParams.set('value', value ?? '');
|
||||||
|
url.searchParams.set('event_id', eventId);
|
||||||
|
|
||||||
httpPost(url.toString(), {}).catch(console.error);
|
httpPost(url.toString(), {}).catch(console.error);
|
||||||
}
|
}
|
||||||
@@ -49,6 +52,8 @@ window.fireEvent = (props) => {
|
|||||||
event_category: category,
|
event_category: category,
|
||||||
event_label: label,
|
event_label: label,
|
||||||
value: value,
|
value: value,
|
||||||
|
event_id: eventId,
|
||||||
|
source: 'client',
|
||||||
...(callback ? { event_callback: callback } : {}),
|
...(callback ? { event_callback: callback } : {}),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@@ -3,14 +3,22 @@ import { CheckSubscriptionVerification } from '../../components/Billing/CheckSub
|
|||||||
import { RoadmapAIChat } from '../../components/RoadmapAIChat/RoadmapAIChat';
|
import { RoadmapAIChat } from '../../components/RoadmapAIChat/RoadmapAIChat';
|
||||||
import SkeletonLayout from '../../layouts/SkeletonLayout.astro';
|
import SkeletonLayout from '../../layouts/SkeletonLayout.astro';
|
||||||
import { AITutorLayout } from '../../components/AITutor/AITutorLayout';
|
import { AITutorLayout } from '../../components/AITutor/AITutorLayout';
|
||||||
import { getRoadmapById } from '../../lib/roadmap';
|
import { getRoadmapById, getRoadmapIds } from '../../lib/roadmap';
|
||||||
|
|
||||||
export const prerender = false;
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
roadmapId: string;
|
roadmapId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const prerender = false;
|
||||||
|
|
||||||
|
export async function getStaticPaths() {
|
||||||
|
const roadmapIds = await getRoadmapIds();
|
||||||
|
|
||||||
|
return roadmapIds.map((roadmapId) => ({
|
||||||
|
params: { roadmapId },
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
const { roadmapId } = Astro.params as Props;
|
const { roadmapId } = Astro.params as Props;
|
||||||
|
|
||||||
const roadmapDetail = await getRoadmapById(roadmapId);
|
const roadmapDetail = await getRoadmapById(roadmapId);
|
||||||
|
Reference in New Issue
Block a user