- {(isGenerating || isLoading) && (
-
-
-
- )}
-
-
-
- Lesson {activeLessonIndex + 1} of {totalLessons}
-
+
+
+
+
+
+ {(isGenerating || isLoading) && (
+
+
+
+ )}
- {!isGenerating && !isLoading && (
-
-
{
- generateAiCourseContent(true, prompt);
- }}
- />
-
-
- )}
-
-
-
- {currentLessonTitle?.replace(/^Lesson\s*?\d+[\.:]\s*/, '')}
-
-
- {!error && isLoggedIn() && (
-
- )}
+
+
+
+ {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
+
+
+ )}
- {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 && (
+ {!isLoading && !isGenerating && !error && (
+
+ )}
+
+
+
+
+ Previous{' '}
+ Lesson
+
+
+
{
- onUpgrade();
+ if (!isLessonDone) {
+ toggleDone(undefined, {
+ onSuccess: () => {
+ onGoToNextLesson();
+ },
+ });
+ } else {
+ onGoToNextLesson();
+ }
}}
- className="rounded-full bg-red-600 px-4 py-1 text-white hover:bg-red-700"
+ 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',
+ )}
>
- Upgrade Account
+ {isTogglingDone ? (
+ <>
+
+ Please wait ...
+ >
+ ) : (
+ <>
+ Next{' '}
+ Lesson
+
+ >
+ )}
- )}
+
- ) : (
-
{error}
- )}
-
- )}
- {!isLoggedIn() && (
-
-
-
- Please login to generate course content
-
+
+
+
+ {isAIChatsOpen && (
+ <>
+
+ setIsAIChatsOpen(false)}
+ isAIChatsOpen={isAIChatsOpen}
+ setIsAIChatsOpen={setIsAIChatsOpen}
+ />
+ >
)}
-
- {!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
-
- >
- )}
-
-
-
-
-
- {!isGenerating && !isLoading && (
-
- )}
+
);
}
diff --git a/src/components/GenerateCourse/AICourseFollowUp.css b/src/components/GenerateCourse/AICourseLessonChat.css
similarity index 100%
rename from src/components/GenerateCourse/AICourseFollowUp.css
rename to src/components/GenerateCourse/AICourseLessonChat.css
diff --git a/src/components/GenerateCourse/AICourseFollowUpPopover.tsx b/src/components/GenerateCourse/AICourseLessonChat.tsx
similarity index 50%
rename from src/components/GenerateCourse/AICourseFollowUpPopover.tsx
rename to src/components/GenerateCourse/AICourseLessonChat.tsx
index 35b49051f..fc7cb3984 100644
--- a/src/components/GenerateCourse/AICourseFollowUpPopover.tsx
+++ b/src/components/GenerateCourse/AICourseLessonChat.tsx
@@ -6,11 +6,21 @@ import {
HelpCircle,
LockIcon,
Send,
+ User2,
+ X,
+ XIcon,
} from 'lucide-react';
-import { useEffect, useMemo, useRef, useState, type FormEvent } from 'react';
+import {
+ Fragment,
+ 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';
@@ -22,6 +32,8 @@ import {
import { getAiCourseLimitOptions } from '../../queries/ai-course';
import { queryClient } from '../../stores/query-client';
import { billingDetailsOptions } from '../../queries/billing';
+import { ResizablePanel } from './Resizeable';
+import { Spinner } from '../ReactIcons/Spinner';
export type AllowedAIChatRole = 'user' | 'assistant';
export type AIChatHistoryType = {
@@ -31,40 +43,53 @@ export type AIChatHistoryType = {
html?: string;
};
-type AICourseFollowUpPopoverProps = {
+type AICourseLessonChatProps = {
courseSlug: string;
moduleTitle: string;
lessonTitle: string;
+ onUpgradeClick: () => void;
+ isDisabled?: boolean;
+ isGeneratingLesson?: boolean;
+
+ defaultQuestions?: string[];
courseAIChatHistory: AIChatHistoryType[];
- setCourseAIChatHistory: (value: AIChatHistoryType[]) => void;
+ setCourseAIChatHistory: (history: AIChatHistoryType[]) => void;
- onOutsideClick?: () => void;
- onUpgradeClick: () => void;
+ onClose: () => void;
+
+ isAIChatsOpen: boolean;
+ setIsAIChatsOpen: (isOpen: boolean) => void;
};
-export function AICourseFollowUpPopover(props: AICourseFollowUpPopoverProps) {
+export function AICourseLessonChat(props: AICourseLessonChatProps) {
const {
courseSlug,
moduleTitle,
lessonTitle,
- onOutsideClick,
onUpgradeClick,
+ isDisabled,
+ defaultQuestions = [],
courseAIChatHistory,
setCourseAIChatHistory,
+
+ onClose,
+
+ isAIChatsOpen,
+ setIsAIChatsOpen,
+
+ isGeneratingLesson,
} = props;
const toast = useToast();
- const containerRef = useRef
(null);
const scrollareaRef = useRef(null);
+ const textareaRef = useRef(null);
const [isStreamingMessage, setIsStreamingMessage] = useState(false);
const [message, setMessage] = useState('');
const [streamedMessage, setStreamedMessage] = useState('');
- useOutsideClick(containerRef, onOutsideClick);
-
const { data: tokenUsage, isLoading } = useQuery(
getAiCourseLimitOptions(),
queryClient,
@@ -107,12 +132,12 @@ export function AICourseFollowUpPopover(props: AICourseFollowUpPopoverProps) {
completeCourseAIChat(newMessages);
};
- const scrollToBottom = () => {
+ const scrollToBottom = useCallback(() => {
scrollareaRef.current?.scrollTo({
top: scrollareaRef.current.scrollHeight,
behavior: 'smooth',
});
- };
+ }, [scrollareaRef]);
const completeCourseAIChat = async (messages: AIChatHistoryType[]) => {
setIsStreamingMessage(true);
@@ -191,103 +216,156 @@ export function AICourseFollowUpPopover(props: AICourseFollowUpPopoverProps) {
}, []);
return (
-
-
-
Course AI
-
-
-
-
-
- {courseAIChatHistory.map((chat, index) => {
- return (
- <>
-
+
+
+
- {chat.isDefault && (
-
-
- {capabilities.map((capability, index) => (
-
- ))}
-
-
- )}
- >
- );
- })}
-
- {isStreamingMessage && !streamedMessage && (
-
- )}
+
+
Course AI
+
+
+
+
- {streamedMessage && (
-
+
+
+
+ {isGeneratingLesson && (
+
+
+
+
+ Generating lesson...
+
+
+
)}
+
+ {courseAIChatHistory.map((chat, index) => {
+ return (
+
+
+
+ {chat.isDefault && defaultQuestions?.length > 1 && (
+
+
+ Some questions you might have about this lesson.
+
+
+ {defaultQuestions.map((question, index) => (
+ {
+ flushSync(() => {
+ setMessage(question);
+ });
+
+ textareaRef.current?.focus();
+ }}
+ >
+ {question}
+
+ ))}
+
+
+ )}
+
+ );
+ })}
+
+ {isStreamingMessage && !streamedMessage && (
+
+ )}
+
+ {streamedMessage && (
+
+ )}
+
-
-
-
+ {isLimitExceeded && (
+
+
+
+ Limit reached for today
+ {isPaidUser ? '. Please wait until tomorrow.' : ''}
+
+ {!isPaidUser && (
+
{
+ onUpgradeClick();
+ }}
+ className="rounded-md bg-white px-2 py-1 text-xs font-medium text-black hover:bg-gray-300"
+ >
+ Upgrade for more
+
+ )}
+
+ )}
+
setMessage(e.target.value)}
+ autoFocus={true}
+ onKeyDown={(e) => {
+ if (e.key === 'Enter' && !e.shiftKey) {
+ handleChatSubmit(e as unknown as FormEvent);
+ }
+ }}
+ ref={textareaRef}
+ />
+
+
+
+
+
+
);
}
@@ -324,7 +402,11 @@ function AIChatCard(props: AIChatCardProps) {
: 'bg-yellow-400 text-black',
)}
>
-
+ {role === 'user' ? (
+
+ ) : (
+
+ )}
) => (
+
+);
+
+const ResizablePanel = ResizablePrimitive.Panel;
+
+const ResizableHandle = ({
+ withHandle,
+ className,
+ ...props
+}: React.ComponentProps
& {
+ withHandle?: boolean;
+}) => (
+ div]:rotate-90',
+ className,
+ )}
+ {...props}
+ >
+ {withHandle && (
+
+
+
+ )}
+
+);
+
+export { ResizablePanelGroup, ResizablePanel, ResizableHandle };