-
- Lesson {activeLessonIndex + 1} of {totalLessons}
+ {!isGenerating && !isLoading && (
+
+ {
+ generateAiCourseContent(true, prompt);
+ }}
+ />
+ toggleDone()}
+ >
+ {isTogglingDone ? (
+ <>
+
+ Please wait ...
+ >
+ ) : (
+ <>
+ {isLessonDone ? (
+ <>
+
+ Mark as Undone
+ >
+ ) : (
+ <>
+
+ Mark as Done
+ >
+ )}
+ >
+ )}
+
+
+ )}
- {!isGenerating && !isLoading && (
-
-
{
- generateAiCourseContent(true, prompt);
- }}
- />
+
+ {currentLessonTitle?.replace(/^Lesson\s*?\d+[\.:]\s*/, '')}
+
+
+ {!error && isLoggedIn() && (
+
+ )}
+
+ {error && isLoggedIn() && (
+
+ {error.includes('reached the limit') ? (
+
+
+ Limit reached
+
+
+ You have reached the AI usage limit for today.
+ {!isPaidUser && (
+ <>Please upgrade your account to continue.>
+ )}
+ {isPaidUser && (
+ <> Please wait until tomorrow to continue.>
+ )}
+
+
+ {!isPaidUser && (
+
{
+ onUpgrade();
+ }}
+ className="rounded-full bg-red-600 px-4 py-1 text-white hover:bg-red-700"
+ >
+ Upgrade Account
+
+ )}
+
+ ) : (
+
{error}
+ )}
+
+ )}
+
+ {!isLoggedIn() && (
+
+
+
+ Please login to generate course content
+
+
+ )}
+
+ {!isLoading && !isGenerating && !error && (
+
+ )}
+
+
+
+
+ Previous Lesson
+
+
+
{
+ if (!isLessonDone) {
+ toggleDone(undefined, {
+ onSuccess: () => {
+ onGoToNextLesson();
+ },
+ });
+ } else {
+ onGoToNextLesson();
+ }
+ }}
+ disabled={cantGoForward || isTogglingDone}
className={cn(
- 'flex items-center gap-1.5 rounded-full bg-black py-1 pl-2 pr-3 text-sm text-white hover:bg-gray-800 disabled:opacity-50 max-lg:text-xs',
- isLessonDone
- ? 'bg-red-500 hover:bg-red-600'
- : 'bg-green-500 hover:bg-green-600',
+ 'flex items-center rounded-full px-4 py-2 disabled:opacity-50 max-lg:px-3 max-lg:py-1.5 max-lg:text-sm',
+ cantGoForward
+ ? 'cursor-not-allowed text-gray-400'
+ : 'bg-gray-800 text-white hover:bg-gray-700',
)}
- onClick={() => toggleDone()}
>
{isTogglingDone ? (
<>
@@ -254,145 +383,27 @@ export function AICourseLesson(props: AICourseLessonProps) {
>
) : (
<>
- {isLessonDone ? (
- <>
-
- Mark as Undone
- >
- ) : (
- <>
-
- Mark as Done
- >
- )}
+ Next Lesson
+
>
)}
- )}
-
-
-
- {currentLessonTitle?.replace(/^Lesson\s*?\d+[\.:]\s*/, '')}
-
-
- {!error && isLoggedIn() && (
-
- )}
-
- {error && isLoggedIn() && (
-
- {error.includes('reached the limit') ? (
-
-
- Limit reached
-
-
- You have reached the AI usage limit for today.
- {!isPaidUser && <>Please upgrade your account to continue.>}
- {isPaidUser && <> Please wait until tomorrow to continue.>}
-
-
- {!isPaidUser && (
-
{
- onUpgrade();
- }}
- className="rounded-full bg-red-600 px-4 py-1 text-white hover:bg-red-700"
- >
- Upgrade Account
-
- )}
-
- ) : (
-
{error}
- )}
- )}
-
- {!isLoggedIn() && (
-
-
-
- Please login to generate course content
-
-
- )}
-
- {!isLoading && !isGenerating && !error && (
-
- )}
-
-
-
-
- Previous Lesson
-
-
-
{
- if (!isLessonDone) {
- toggleDone(undefined, {
- onSuccess: () => {
- onGoToNextLesson();
- },
- });
- } else {
- onGoToNextLesson();
- }
- }}
- disabled={cantGoForward || isTogglingDone}
- className={cn(
- 'flex items-center rounded-full px-4 py-2 disabled:opacity-50 max-lg:px-3 max-lg:py-1.5 max-lg:text-sm',
- cantGoForward
- ? 'cursor-not-allowed text-gray-400'
- : 'bg-gray-800 text-white hover:bg-gray-700',
- )}
- >
- {isTogglingDone ? (
- <>
-
- Please wait ...
- >
- ) : (
- <>
- Next Lesson
-
- >
- )}
-
+
+ AI can make mistakes, check important info.
- {!isGenerating && !isLoading && (
-
- )}
+
);
}
diff --git a/src/components/GenerateCourse/AICourseLessonChat.tsx b/src/components/GenerateCourse/AICourseLessonChat.tsx
new file mode 100644
index 000000000..3b5be326a
--- /dev/null
+++ b/src/components/GenerateCourse/AICourseLessonChat.tsx
@@ -0,0 +1,407 @@
+import { useQuery } from '@tanstack/react-query';
+import {
+ BookOpen,
+ Bot,
+ Hammer,
+ HelpCircle,
+ LockIcon,
+ Send,
+} from 'lucide-react';
+import {
+ useCallback,
+ useEffect,
+ useMemo,
+ useRef,
+ useState,
+ type FormEvent,
+} from 'react';
+import { flushSync } from 'react-dom';
+import TextareaAutosize from 'react-textarea-autosize';
+import { useOutsideClick } from '../../hooks/use-outside-click';
+import { useToast } from '../../hooks/use-toast';
+import { readStream } from '../../lib/ai';
+import { cn } from '../../lib/classname';
+import { isLoggedIn, removeAuthToken } from '../../lib/jwt';
+import {
+ markdownToHtml,
+ markdownToHtmlWithHighlighting,
+} from '../../lib/markdown';
+import { getAiCourseLimitOptions } from '../../queries/ai-course';
+import { queryClient } from '../../stores/query-client';
+import { billingDetailsOptions } from '../../queries/billing';
+
+export type AllowedAIChatRole = 'user' | 'assistant';
+export type AIChatHistoryType = {
+ role: AllowedAIChatRole;
+ content: string;
+ isDefault?: boolean;
+ html?: string;
+};
+
+type AICourseLessonChatProps = {
+ courseSlug: string;
+ moduleTitle: string;
+ lessonTitle: string;
+ onUpgradeClick: () => void;
+ isDisabled?: boolean;
+};
+
+export function AICourseLessonChat(props: AICourseLessonChatProps) {
+ const { courseSlug, moduleTitle, lessonTitle, onUpgradeClick, isDisabled } =
+ props;
+
+ const toast = useToast();
+ const scrollareaRef = useRef
(null);
+
+ const [courseAIChatHistory, setCourseAIChatHistory] = useState<
+ AIChatHistoryType[]
+ >([
+ {
+ role: 'assistant',
+ content:
+ 'Hey, I am your AI instructor. Here are some examples of what you can ask me about 🤖',
+ isDefault: true,
+ },
+ ]);
+
+ const [isStreamingMessage, setIsStreamingMessage] = useState(false);
+ const [message, setMessage] = useState('');
+ const [streamedMessage, setStreamedMessage] = useState('');
+
+ const { data: tokenUsage, isLoading } = useQuery(
+ getAiCourseLimitOptions(),
+ queryClient,
+ );
+
+ const { data: userBillingDetails, isLoading: isBillingDetailsLoading } =
+ useQuery(billingDetailsOptions(), queryClient);
+
+ const isLimitExceeded = (tokenUsage?.used || 0) >= (tokenUsage?.limit || 0);
+ const isPaidUser = userBillingDetails?.status === 'active';
+
+ const handleChatSubmit = (e: FormEvent) => {
+ e.preventDefault();
+
+ const trimmedMessage = message.trim();
+ if (
+ !trimmedMessage ||
+ isStreamingMessage ||
+ !isLoggedIn() ||
+ isLimitExceeded ||
+ isLoading
+ ) {
+ return;
+ }
+
+ const newMessages: AIChatHistoryType[] = [
+ ...courseAIChatHistory,
+ {
+ role: 'user',
+ content: trimmedMessage,
+ },
+ ];
+
+ flushSync(() => {
+ setCourseAIChatHistory(newMessages);
+ setMessage('');
+ });
+
+ scrollToBottom();
+ completeCourseAIChat(newMessages);
+ };
+
+ const scrollToBottom = useCallback(() => {
+ scrollareaRef.current?.scrollTo({
+ top: scrollareaRef.current.scrollHeight,
+ behavior: 'smooth',
+ });
+ }, [scrollareaRef]);
+
+ const completeCourseAIChat = async (messages: AIChatHistoryType[]) => {
+ setIsStreamingMessage(true);
+
+ const response = await fetch(
+ `${import.meta.env.PUBLIC_API_URL}/v1-follow-up-ai-course/${courseSlug}`,
+ {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ credentials: 'include',
+ body: JSON.stringify({
+ moduleTitle,
+ lessonTitle,
+ messages: messages.slice(-10),
+ }),
+ },
+ );
+
+ if (!response.ok) {
+ const data = await response.json();
+
+ toast.error(data?.message || 'Something went wrong');
+ setCourseAIChatHistory([...messages].slice(0, messages.length - 1));
+ setIsStreamingMessage(false);
+
+ if (data.status === 401) {
+ removeAuthToken();
+ window.location.reload();
+ }
+ }
+
+ const reader = response.body?.getReader();
+
+ if (!reader) {
+ setIsStreamingMessage(false);
+ toast.error('Something went wrong');
+ return;
+ }
+
+ await readStream(reader, {
+ onStream: async (content) => {
+ flushSync(() => {
+ setStreamedMessage(content);
+ });
+
+ scrollToBottom();
+ },
+ onStreamEnd: async (content) => {
+ const newMessages: AIChatHistoryType[] = [
+ ...messages,
+ {
+ role: 'assistant',
+ content,
+ html: await markdownToHtmlWithHighlighting(content),
+ },
+ ];
+
+ flushSync(() => {
+ setStreamedMessage('');
+ setIsStreamingMessage(false);
+ setCourseAIChatHistory(newMessages);
+ });
+
+ queryClient.invalidateQueries(getAiCourseLimitOptions());
+ scrollToBottom();
+ },
+ });
+
+ setIsStreamingMessage(false);
+ };
+
+ useEffect(() => {
+ scrollToBottom();
+ }, []);
+
+ return (
+
+
+
+
Course AI
+
+
+
+
+
+
+ {courseAIChatHistory.map((chat, index) => {
+ return (
+ <>
+
+
+ {chat.isDefault && (
+
+
+ {capabilities.map((capability, index) => (
+
+ ))}
+
+
+ )}
+ >
+ );
+ })}
+
+ {isStreamingMessage && !streamedMessage && (
+
+ )}
+
+ {streamedMessage && (
+
+ )}
+
+
+
+
+
+
+
+
+ );
+}
+
+type AIChatCardProps = {
+ role: AllowedAIChatRole;
+ content: string;
+ html?: string;
+};
+
+function AIChatCard(props: AIChatCardProps) {
+ const { role, content, html: defaultHtml } = props;
+
+ const html = useMemo(() => {
+ if (defaultHtml) {
+ return defaultHtml;
+ }
+
+ return markdownToHtml(content, false);
+ }, [content, defaultHtml]);
+
+ return (
+
+ );
+}
+
+type CapabilityCardProps = {
+ icon: React.ReactNode;
+ title: string;
+ description: string;
+ className?: string;
+};
+
+function CapabilityCard({
+ icon,
+ title,
+ description,
+ className,
+}: CapabilityCardProps) {
+ return (
+
+
+ {icon}
+
+ {title}
+
+
+
{description}
+
+ );
+}
+
+const capabilities = [
+ {
+ icon: (
+
+ ),
+ title: 'Clarify Concepts',
+ description: "If you don't understand a concept, ask me to clarify it",
+ },
+ {
+ icon: (
+
+ ),
+ title: 'More Details',
+ description: 'Get deeper insights about topics covered in the lesson',
+ },
+ {
+ icon: (
+
+ ),
+ title: 'Real-world Examples',
+ description: 'Ask for real-world examples to understand better',
+ },
+ {
+ icon: ,
+ title: 'Best Practices',
+ description: 'Learn about best practices and common pitfalls',
+ },
+] as const;