import { useQuery } from '@tanstack/react-query'; import { BookOpen, Bot, Code, Globe, Hammer, HelpCircle, LockIcon, Send, } from 'lucide-react'; import { 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'; export type AllowedAIChatRole = 'user' | 'assistant'; export type AIChatHistoryType = { role: AllowedAIChatRole; content: string; isDefault?: boolean; html?: string; }; type AICourseFollowUpPopoverProps = { courseSlug: string; moduleTitle: string; lessonTitle: string; courseAIChatHistory: AIChatHistoryType[]; setCourseAIChatHistory: (value: AIChatHistoryType[]) => void; onOutsideClick?: () => void; onUpgradeClick: () => void; }; export function AICourseFollowUpPopover(props: AICourseFollowUpPopoverProps) { const { courseSlug, moduleTitle, lessonTitle, onOutsideClick, onUpgradeClick, courseAIChatHistory, setCourseAIChatHistory, } = props; const toast = useToast(); const containerRef = useRef(null); const scrollareaRef = 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, ); const isLimitExceeded = (tokenUsage?.used || 0) >= (tokenUsage?.limit || 0); 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 = () => { scrollareaRef.current?.scrollTo({ top: scrollareaRef.current.scrollHeight, behavior: 'smooth', }); }; 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 && ( )}
{isLimitExceeded && (

Limit reached for today

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