mirror of
https://github.com/kamranahmedse/developer-roadmap.git
synced 2025-09-03 06:12:53 +02:00
wip
This commit is contained in:
@@ -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,
|
||||
});
|
||||
|
||||
|
37
src/components/AIChatHistory/AIChatHistory.tsx
Normal file
37
src/components/AIChatHistory/AIChatHistory.tsx
Normal 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>
|
||||
);
|
||||
}
|
22
src/components/AIChatHistory/AIChatLayout.tsx
Normal file
22
src/components/AIChatHistory/AIChatLayout.tsx
Normal 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>
|
||||
);
|
||||
}
|
@@ -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;
|
18
src/pages/ai/chat/[chatId].astro
Normal file
18
src/pages/ai/chat/[chatId].astro
Normal 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>
|
@@ -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>
|
||||
|
57
src/queries/chat-history.ts
Normal file
57
src/queries/chat-history.ts
Normal 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(),
|
||||
});
|
||||
}
|
Reference in New Issue
Block a user