import { useQuery } from '@tanstack/react-query'; import { BookOpen, Bot, Hammer, HelpCircle, LockIcon, Send, User2, X, XIcon, } from 'lucide-react'; import { Fragment, useCallback, useEffect, useMemo, useRef, useState, type FormEvent, } from 'react'; import { flushSync } from 'react-dom'; import TextareaAutosize from 'react-textarea-autosize'; 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'; import { ResizablePanel } from './Resizeable'; import { Spinner } from '../ReactIcons/Spinner'; 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; isGeneratingLesson?: boolean; defaultQuestions?: string[]; courseAIChatHistory: AIChatHistoryType[]; setCourseAIChatHistory: (history: AIChatHistoryType[]) => void; onClose: () => void; isAIChatsOpen: boolean; setIsAIChatsOpen: (isOpen: boolean) => void; }; export function AICourseLessonChat(props: AICourseLessonChatProps) { const { courseSlug, moduleTitle, lessonTitle, onUpgradeClick, isDisabled, defaultQuestions = [], courseAIChatHistory, setCourseAIChatHistory, onClose, isAIChatsOpen, setIsAIChatsOpen, isGeneratingLesson, } = props; const toast = useToast(); const scrollareaRef = useRef(null); const textareaRef = useRef(null); 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

{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) => ( ))}
)}
); })} {isStreamingMessage && !streamedMessage && ( )} {streamedMessage && ( )}
{isLimitExceeded && (

Limit reached for today {isPaidUser ? '. Please wait until tomorrow.' : ''}

{!isPaidUser && ( )}
)} setMessage(e.target.value)} autoFocus={true} onKeyDown={(e) => { if (e.key === 'Enter' && !e.shiftKey) { handleChatSubmit(e as unknown as FormEvent); } }} ref={textareaRef} />
); } 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 (
{role === 'user' ? ( ) : ( )}
); } 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;