1
0
mirror of https://github.com/kamranahmedse/developer-roadmap.git synced 2025-09-03 06:12:53 +02:00
This commit is contained in:
Arik Chakma
2025-06-05 12:30:39 +06:00
parent c55e037e86
commit c3160bdf3c
7 changed files with 172 additions and 31 deletions

View File

@@ -43,21 +43,46 @@ import { AIChatCourse } from './AIChatCouse';
import { showLoginPopup } from '../../lib/popup';
import { UpgradeAccountModal } from '../Billing/UpgradeAccountModal';
import { readChatStream } from '../../lib/chat';
import type { ChatHistoryDocument } from '../../queries/chat-history';
export const aiChatRenderer: Record<string, MessagePartRenderer> = {
'roadmap-recommendations': (options) => {
return <RoadmapRecommendations {...options} />;
},
'generate-course': (options) => {
return <AIChatCourse {...options} />;
},
};
type AIChatProps = {
chatHistory?: Pick<ChatHistoryDocument, '_id' | 'title'>;
messages?: RoadmapAIChatHistoryType[];
};
export function AIChat(props: AIChatProps) {
const { chatHistory: defaultDetails, messages: defaultMessages } = props;
export function AIChat() {
const toast = useToast();
const [chatDetails, setChatDetails] = useState<{
chatHistoryId: string;
title: string;
} | null>(null);
} | null>(
defaultDetails
? {
chatHistoryId: defaultDetails._id,
title: defaultDetails.title,
}
: null,
);
const [message, setMessage] = useState('');
const [isStreamingMessage, setIsStreamingMessage] = useState(false);
const [streamedMessage, setStreamedMessage] =
useState<React.ReactNode | null>(null);
const [aiChatHistory, setAiChatHistory] = useState<
RoadmapAIChatHistoryType[]
>([]);
>(defaultMessages ?? []);
const [showUpgradeModal, setShowUpgradeModal] = useState(false);
const [isPersonalizedResponseFormOpen, setIsPersonalizedResponseFormOpen] =
@@ -145,17 +170,6 @@ export function AIChat() {
});
}, [scrollableContainerRef]);
const renderer: Record<string, MessagePartRenderer> = useMemo(() => {
return {
'roadmap-recommendations': (options) => {
return <RoadmapRecommendations {...options} />;
},
'generate-course': (options) => {
return <AIChatCourse {...options} />;
},
};
}, []);
const completeAIChat = async (
messages: RoadmapAIChatHistoryType[],
force: boolean = false,
@@ -197,7 +211,7 @@ export function AIChat() {
await readChatStream(reader, {
onMessage: async (content) => {
const jsx = await renderMessage(content, renderer, {
const jsx = await renderMessage(content, aiChatRenderer, {
isLoading: true,
});
@@ -208,7 +222,7 @@ export function AIChat() {
scrollToBottom();
},
onMessageEnd: async (content) => {
const jsx = await renderMessage(content, renderer, {
const jsx = await renderMessage(content, aiChatRenderer, {
isLoading: false,
});

View File

@@ -0,0 +1,37 @@
import { useQuery } from '@tanstack/react-query';
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 { AIChatLayout } from './AIChatLayout';
type AIChatHistoryProps = {
chatHistoryId: string;
};
export function AIChatHistory(props: AIChatHistoryProps) {
const { chatHistoryId } = props;
const [isLoading, setIsLoading] = useState(true);
const { data } = useQuery(chatHistoryOptions(chatHistoryId), queryClient);
useEffect(() => {
if (!data) {
return;
}
setIsLoading(false);
}, [data]);
return (
<AIChatLayout>
{isLoading && (
<div className="flex flex-1 items-center justify-center">
<Loader2Icon className="h-4 w-4 animate-spin" />
</div>
)}
{!isLoading && <AIChat messages={data?.messages} chatHistory={data} />}
</AIChatLayout>
);
}

View File

@@ -0,0 +1,22 @@
import { AITutorLayout } from '../AITutor/AITutorLayout';
import { CheckSubscriptionVerification } from '../Billing/CheckSubscriptionVerification';
import { Loader2Icon } from 'lucide-react';
type AIChatLayoutProps = {
children: React.ReactNode;
};
export function AIChatLayout(props: AIChatLayoutProps) {
const { children } = props;
return (
<AITutorLayout
activeTab="chat"
wrapperClassName="flex-row p-0 lg:p-0 overflow-hidden"
containerClassName="h-[calc(100vh-49px)] overflow-hidden"
>
{children}
<CheckSubscriptionVerification />
</AITutorLayout>
);
}

View File

@@ -1,9 +1,9 @@
---
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';
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;

View File

@@ -0,0 +1,18 @@
---
import SkeletonLayout from '../../../layouts/SkeletonLayout.astro';
import { AIChatHistory } from '../../../components/AIChatHistory/AIChatHistory';
type Props = {
chatId: string;
};
const { chatId } = Astro.params as Props;
---
<SkeletonLayout
title='AI Chat'
noIndex={true}
description='Learn anything with AI Tutor. Pick a topic, choose a difficulty level and the AI will guide you through the learning process.'
>
<AIChatHistory client:load chatHistoryId={chatId} />
</SkeletonLayout>

View File

@@ -1,8 +1,7 @@
---
import SkeletonLayout from '../../../layouts/SkeletonLayout.astro';
import { AIChat } from '../../../components/AIChat/AIChat';
import { AITutorLayout } from '../../../components/AITutor/AITutorLayout';
import { CheckSubscriptionVerification } from '../../../components/Billing/CheckSubscriptionVerification';
import { AIChatLayout } from '../../../components/AIChatHistory/AIChatLayout';
---
<SkeletonLayout
@@ -10,13 +9,7 @@ import { CheckSubscriptionVerification } from '../../../components/Billing/Check
noIndex={true}
description='Learn anything with AI Tutor. Pick a topic, choose a difficulty level and the AI will guide you through the learning process.'
>
<AITutorLayout
activeTab='chat'
wrapperClassName='flex-row p-0 lg:p-0 overflow-hidden'
client:load
containerClassName='h-[calc(100vh-49px)] overflow-hidden'
>
<AIChatLayout client:load>
<AIChat client:load />
<CheckSubscriptionVerification client:load />
</AITutorLayout>
</AIChatLayout>
</SkeletonLayout>

View File

@@ -0,0 +1,57 @@
import { queryOptions } from '@tanstack/react-query';
import { httpGet } from '../lib/query-http';
import { isLoggedIn } from '../lib/jwt';
import type { RoadmapAIChatHistoryType } from '../components/RoadmapAIChat/RoadmapAIChat';
import { markdownToHtml } from '../lib/markdown';
import { aiChatRenderer } from '../components/AIChat/AIChat';
import { renderMessage } from '../lib/render-chat-message';
export type ChatHistoryMessage = {
_id: string;
role: 'user' | 'assistant';
content: string;
};
export interface ChatHistoryDocument {
_id: string;
userId: string;
title: string;
messages: ChatHistoryMessage[];
createdAt: Date;
updatedAt: Date;
}
export function chatHistoryOptions(chatHistoryId: string) {
return queryOptions({
queryKey: ['chat-history', chatHistoryId],
queryFn: async () => {
const data = await httpGet<ChatHistoryDocument>(
`/v1-chat-history/${chatHistoryId}`,
);
const messages: RoadmapAIChatHistoryType[] = [];
for (const message of data.messages) {
messages.push({
role: message.role,
content: message.content,
...(message.role === 'user' && {
html: markdownToHtml(message.content),
}),
...(message.role === 'assistant' && {
jsx: await renderMessage(message.content, aiChatRenderer, {
isLoading: false,
}),
}),
});
}
return {
...data,
messages,
};
},
enabled: !!isLoggedIn(),
});
}