diff --git a/src/components/GenerateCourse/AICourseContent.tsx b/src/components/GenerateCourse/AICourseContent.tsx index e0364fe33..e2abc32dc 100644 --- a/src/components/GenerateCourse/AICourseContent.tsx +++ b/src/components/GenerateCourse/AICourseContent.tsx @@ -5,8 +5,9 @@ import { CircleOff, Menu, X, - Map, MessageCircleOffIcon, - MessageCircleIcon + Map, + MessageCircleOffIcon, + MessageCircleIcon, } from 'lucide-react'; import { useEffect, useState } from 'react'; import { type AiCourse } from '../../lib/ai'; @@ -21,6 +22,9 @@ import { AILimitsPopup } from './AILimitsPopup'; import { AICourseOutlineView } from './AICourseOutlineView'; import { AICourseRoadmapView } from './AICourseRoadmapView'; import { AICourseFooter } from './AICourseFooter'; +import { ForkCourseAlert } from './ForkCourseAlert'; +import { ForkCourseConfirmation } from './ForkCourseConfirmation'; +import { useAuth } from '../../hooks/use-auth'; type AICourseContentProps = { courseSlug?: string; @@ -28,12 +32,20 @@ type AICourseContentProps = { isLoading: boolean; error?: string; onRegenerateOutline: (prompt?: string) => void; + creatorId?: string; }; export type AICourseViewMode = 'module' | 'outline' | 'roadmap'; export function AICourseContent(props: AICourseContentProps) { - const { course, courseSlug, isLoading, error, onRegenerateOutline } = props; + const { + course, + courseSlug, + isLoading, + error, + onRegenerateOutline, + creatorId, + } = props; const [showUpgradeModal, setShowUpgradeModal] = useState(false); const [showAILimitsPopup, setShowAILimitsPopup] = useState(false); @@ -43,8 +55,10 @@ export function AICourseContent(props: AICourseContentProps) { const [activeLessonIndex, setActiveLessonIndex] = useState(0); const [sidebarOpen, setSidebarOpen] = useState(false); const [viewMode, setViewMode] = useState('outline'); + const [isForkingCourse, setIsForkingCourse] = useState(false); const { isPaidUser } = useIsPaidUser(); + const currentUser = useAuth(); const aiCourseProgress = course.done || []; @@ -202,7 +216,7 @@ export function AICourseContent(props: AICourseContentProps) {
Create a course with AI @@ -214,6 +228,7 @@ export function AICourseContent(props: AICourseContentProps) { } const isViewingLesson = viewMode === 'module'; + const isForkable = !!currentUser?.id && currentUser.id !== creatorId; return (
@@ -272,7 +287,7 @@ export function AICourseContent(props: AICourseContentProps) {
-

+

{course.title || 'Loading Course...'}

@@ -342,7 +357,7 @@ export function AICourseContent(props: AICourseContentProps) { width: `${finishedPercentage}%`, }} className={cn( - 'absolute bottom-0 left-0 top-0', + 'absolute top-0 bottom-0 left-0', 'bg-gray-200/50', )} > @@ -420,6 +435,27 @@ export function AICourseContent(props: AICourseContentProps) { )} key={`${courseSlug}-${viewMode}`} > + {isForkable && + courseSlug && + (viewMode === 'outline' || viewMode === 'roadmap') && ( + { + setIsForkingCourse(true); + }} + /> + )} + + {isForkingCourse && ( + { + setIsForkingCourse(false); + }} + courseSlug={courseSlug!} + /> + )} + {viewMode === 'module' && ( setShowUpgradeModal(true)} isAIChatsOpen={isAIChatsOpen} setIsAIChatsOpen={setIsAIChatsOpen} + isForkable={isForkable} + onForkCourse={() => { + setIsForkingCourse(true); + }} /> )} @@ -450,6 +490,10 @@ export function AICourseContent(props: AICourseContentProps) { setViewMode={setViewMode} setExpandedModules={setExpandedModules} viewMode={viewMode} + isForkable={isForkable} + onForkCourse={() => { + setIsForkingCourse(true); + }} /> )} diff --git a/src/components/GenerateCourse/AICourseLesson.tsx b/src/components/GenerateCourse/AICourseLesson.tsx index 3264c729d..3a10b5680 100644 --- a/src/components/GenerateCourse/AICourseLesson.tsx +++ b/src/components/GenerateCourse/AICourseLesson.tsx @@ -70,6 +70,9 @@ type AICourseLessonProps = { isAIChatsOpen: boolean; setIsAIChatsOpen: (isOpen: boolean) => void; + + isForkable: boolean; + onForkCourse: () => void; }; export function AICourseLesson(props: AICourseLessonProps) { @@ -91,6 +94,9 @@ export function AICourseLesson(props: AICourseLessonProps) { isAIChatsOpen, setIsAIChatsOpen, + + isForkable, + onForkCourse, } = props; const [isLoading, setIsLoading] = useState(true); @@ -108,8 +114,7 @@ export function AICourseLesson(props: AICourseLessonProps) { >([ { role: 'assistant', - content: - 'Hey, I am your AI instructor. How can I help you today? 🤖', + content: 'Hey, I am your AI instructor. How can I help you today? 🤖', isDefault: true, }, ]); @@ -205,7 +210,7 @@ export function AICourseLesson(props: AICourseLessonProps) { const questions = getQuestionsFromResult(result); setDefaultQuestions(questions); - + const newResult = result.replace( /=START_QUESTIONS=.*?=END_QUESTIONS=/, '', @@ -284,7 +289,7 @@ export function AICourseLesson(props: AICourseLessonProps) {
{(isGenerating || isLoading) && ( -
+
{!isGenerating && !isLoading && ( -
+
-

+

{currentLessonTitle?.replace(/^Lesson\s*?\d+[\.:]\s*/, '')}

{!error && isLoggedIn() && (
)} diff --git a/src/components/GenerateCourse/AICourseOutlineHeader.tsx b/src/components/GenerateCourse/AICourseOutlineHeader.tsx index a841c99bb..386efc7cb 100644 --- a/src/components/GenerateCourse/AICourseOutlineHeader.tsx +++ b/src/components/GenerateCourse/AICourseOutlineHeader.tsx @@ -10,11 +10,20 @@ type AICourseOutlineHeaderProps = { onRegenerateOutline: (prompt?: string) => void; viewMode: AICourseViewMode; setViewMode: (mode: AICourseViewMode) => void; + isForkable: boolean; + onForkCourse: () => void; }; export function AICourseOutlineHeader(props: AICourseOutlineHeaderProps) { - const { course, isLoading, onRegenerateOutline, viewMode, setViewMode } = - props; + const { + course, + isLoading, + onRegenerateOutline, + viewMode, + setViewMode, + isForkable, + onForkCourse, + } = props; return (
-

+

{course.title || 'Loading course ..'}

-

+

{course.title ? course.difficulty : 'Please wait ..'}

-
+
{!isLoading && ( <> - +
+
+ ); +} diff --git a/src/components/GenerateCourse/ForkCourseConfirmation.tsx b/src/components/GenerateCourse/ForkCourseConfirmation.tsx new file mode 100644 index 000000000..b0c19f67b --- /dev/null +++ b/src/components/GenerateCourse/ForkCourseConfirmation.tsx @@ -0,0 +1,84 @@ +import { GitForkIcon, Loader2Icon } from 'lucide-react'; +import { Modal } from '../Modal'; +import type { AICourseDocument } from '../../queries/ai-course'; +import { useMutation } from '@tanstack/react-query'; +import { queryClient } from '../../stores/query-client'; +import { httpPost } from '../../lib/query-http'; +import { useToast } from '../../hooks/use-toast'; +import { useState } from 'react'; + +type ForkAICourseParams = { + aiCourseSlug: string; +}; + +type ForkAICourseBody = {}; + +type ForkAICourseQuery = {}; + +type ForkAICourseResponse = AICourseDocument; + +type ForkCourseConfirmationProps = { + onClose: () => void; + courseSlug: string; +}; + +export function ForkCourseConfirmation(props: ForkCourseConfirmationProps) { + const { onClose, courseSlug } = props; + + const toast = useToast(); + const [isPending, setIsPending] = useState(false); + const { mutate: forkCourse } = useMutation( + { + mutationFn: async () => { + setIsPending(true); + return httpPost( + `${import.meta.env.PUBLIC_API_URL}/v1-fork-ai-course/${courseSlug}`, + {}, + ); + }, + onSuccess(data) { + window.location.href = `/ai/${data.slug}`; + }, + onError(error) { + toast.error(error?.message || 'Failed to fork course'); + setIsPending(false); + }, + }, + queryClient, + ); + + return ( + {} : onClose}> +
+ +

Fork Course

+

+ Forking this course will create a new course with the same content. +

+ +
+ + + +
+
+
+ ); +} diff --git a/src/components/GenerateCourse/GetAICourse.tsx b/src/components/GenerateCourse/GetAICourse.tsx index e9b1c98a3..dfe45de9f 100644 --- a/src/components/GenerateCourse/GetAICourse.tsx +++ b/src/components/GenerateCourse/GetAICourse.tsx @@ -102,6 +102,7 @@ export function GetAICourse(props: GetAICourseProps) { courseSlug={courseSlug} error={error} onRegenerateOutline={handleRegenerateCourse} + creatorId={aiCourse?.userId} /> ); } diff --git a/src/components/GenerateCourse/ModifyCoursePrompt.tsx b/src/components/GenerateCourse/ModifyCoursePrompt.tsx index 35583635d..94824f822 100644 --- a/src/components/GenerateCourse/ModifyCoursePrompt.tsx +++ b/src/components/GenerateCourse/ModifyCoursePrompt.tsx @@ -4,10 +4,17 @@ import { Modal } from '../Modal'; export type ModifyCoursePromptProps = { onClose: () => void; onSubmit: (prompt: string) => void; + title?: string; + description?: string; }; export function ModifyCoursePrompt(props: ModifyCoursePromptProps) { - const { onClose, onSubmit } = props; + const { + onClose, + onSubmit, + title = 'Give AI more context', + description = 'Pass additional information to the AI to generate a course outline.', + } = props; const [prompt, setPrompt] = useState(''); @@ -25,12 +32,8 @@ export function ModifyCoursePrompt(props: ModifyCoursePromptProps) { >
-

- Give AI more context -

-

- Pass additional information to the AI to generate a course outline. -

+

{title}

+

{description}