diff --git a/src/components/GenerateCourse/AICourse.tsx b/src/components/GenerateCourse/AICourse.tsx index 8a92cf99f..5fce355b4 100644 --- a/src/components/GenerateCourse/AICourse.tsx +++ b/src/components/GenerateCourse/AICourse.tsx @@ -1,10 +1,15 @@ import { SearchIcon, WandIcon } from 'lucide-react'; -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { cn } from '../../lib/classname'; import { isLoggedIn } from '../../lib/jwt'; import { showLoginPopup } from '../../lib/popup'; import { UserCoursesList } from './UserCoursesList'; import { FineTuneCourse } from './FineTuneCourse'; +import { + getCourseFineTuneData, + getLastSessionId, + storeFineTuneData, +} from '../../lib/ai'; export const difficultyLevels = [ 'beginner', @@ -19,6 +24,27 @@ export function AICourse(props: AICourseProps) { const [keyword, setKeyword] = useState(''); const [difficulty, setDifficulty] = useState<DifficultyLevel>('beginner'); + const [hasFineTuneData, setHasFineTuneData] = useState(false); + const [about, setAbout] = useState(''); + const [goal, setGoal] = useState(''); + const [customInstructions, setCustomInstructions] = useState(''); + + useEffect(() => { + const lastSessionId = getLastSessionId(); + if (!lastSessionId) { + return; + } + + const fineTuneData = getCourseFineTuneData(lastSessionId); + if (!fineTuneData) { + return; + } + + // setAbout(fineTuneData.about); + // setGoal(fineTuneData.goal); + // setCustomInstructions(fineTuneData.customInstructions); + }, []); + const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter' && keyword.trim()) { onSubmit(); @@ -31,7 +57,15 @@ export function AICourse(props: AICourseProps) { return; } - window.location.href = `/ai-tutor/search?term=${encodeURIComponent(keyword)}&difficulty=${difficulty}`; + const sessionId = hasFineTuneData + ? storeFineTuneData({ + about, + goal, + customInstructions, + }) + : ''; + + window.location.href = `/ai-tutor/search?term=${encodeURIComponent(keyword)}&difficulty=${difficulty}&id=${sessionId}`; } return ( @@ -99,7 +133,16 @@ export function AICourse(props: AICourseProps) { </div> </div> - <FineTuneCourse /> + <FineTuneCourse + hasFineTuneData={hasFineTuneData} + setHasFineTuneData={setHasFineTuneData} + about={about} + goal={goal} + customInstructions={customInstructions} + setAbout={setAbout} + setGoal={setGoal} + setCustomInstructions={setCustomInstructions} + /> <button type="submit" diff --git a/src/components/GenerateCourse/FineTuneCourse.tsx b/src/components/GenerateCourse/FineTuneCourse.tsx index 6cf09361a..bc023ec14 100644 --- a/src/components/GenerateCourse/FineTuneCourse.tsx +++ b/src/components/GenerateCourse/FineTuneCourse.tsx @@ -14,7 +14,7 @@ function Question(props: QuestionProps) { return ( <div className="flex flex-col"> - <label className="bg-gray-100 border-y px-4 py-2.5 text-sm font-medium text-gray-700"> + <label className="border-y bg-gray-100 px-4 py-2.5 text-sm font-medium text-gray-700"> {label} </label> <textarea @@ -28,26 +28,46 @@ function Question(props: QuestionProps) { ); } -export function FineTuneCourse() { - const [isFineTuning, setIsFineTuning] = useState(false); +type FineTuneCourseProps = { + hasFineTuneData: boolean; + about: string; + goal: string; + customInstructions: string; - const [about, setAbout] = useState(''); - const [goal, setGoal] = useState(''); - const [customInstructions, setCustomInstructions] = useState(''); + setHasFineTuneData: (hasMetadata: boolean) => void; + setAbout: (about: string) => void; + setGoal: (goal: string) => void; + setCustomInstructions: (customInstructions: string) => void; +}; + +export function FineTuneCourse(props: FineTuneCourseProps) { + const { + about, + goal, + customInstructions, + hasFineTuneData, + setAbout, + setGoal, + setCustomInstructions, + setHasFineTuneData, + } = props; return ( <div className="flex flex-col overflow-hidden rounded-lg border border-gray-200 transition-all"> <label className={cn( 'group flex cursor-pointer select-none flex-row items-center gap-2.5 px-4 py-3 text-left text-gray-500 transition-colors hover:bg-gray-100 focus:outline-none', - isFineTuning && 'bg-gray-100', + hasFineTuneData && 'bg-gray-100', )} > <input id="fine-tune-checkbox" type="checkbox" className="h-4 w-4 group-hover:fill-current" - onChange={() => setIsFineTuning(!isFineTuning)} + checked={hasFineTuneData} + onChange={() => { + setHasFineTuneData(!hasFineTuneData); + }} /> Tell us more to tailor the course (optional){' '} <span className="ml-auto rounded-md bg-gray-400 px-2 py-0.5 text-xs text-white"> @@ -55,24 +75,24 @@ export function FineTuneCourse() { </span> </label> - {isFineTuning && ( + {hasFineTuneData && ( <div className="mt-0 flex flex-col"> <Question label="Tell us about your self" - placeholder="e.g. I have a background in marketing and I already have some basic knowledge of coding." + placeholder="e.g. I am a frontend developer and have good knowledge of HTML, CSS, and JavaScript." value={about} onChange={setAbout} autoFocus={true} /> <Question label="What is your goal with this course?" - placeholder="e.g. I want to learn about advanced topics with focus on practical examples." + placeholder="e.g. I want to be able to build Node.js APIs with Express.js and MongoDB." value={goal} onChange={setGoal} /> <Question label="Custom Instructions (Optional)" - placeholder="Give instructions to the AI as if you were giving them to a friend." + placeholder="Give additional instructions to the AI as if you were giving them to a friend." value={customInstructions} onChange={setCustomInstructions} /> diff --git a/src/components/GenerateCourse/GenerateAICourse.tsx b/src/components/GenerateCourse/GenerateAICourse.tsx index 8687d7944..95ea38935 100644 --- a/src/components/GenerateCourse/GenerateAICourse.tsx +++ b/src/components/GenerateCourse/GenerateAICourse.tsx @@ -1,7 +1,7 @@ import { useEffect, useState } from 'react'; import { getUrlParams } from '../../lib/browser'; import { isLoggedIn } from '../../lib/jwt'; -import { type AiCourse } from '../../lib/ai'; +import { getCourseFineTuneData, type AiCourse } from '../../lib/ai'; import { AICourseContent } from './AICourseContent'; import { generateCourse } from '../../helper/generate-ai-course'; import { useQuery } from '@tanstack/react-query'; @@ -13,6 +13,10 @@ type GenerateAICourseProps = {}; export function GenerateAICourse(props: GenerateAICourseProps) { const [term, setTerm] = useState(''); const [difficulty, setDifficulty] = useState(''); + const [sessionId, setSessionId] = useState(''); + const [goal, setGoal] = useState(''); + const [about, setAbout] = useState(''); + const [customInstructions, setCustomInstructions] = useState(''); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(''); @@ -54,16 +58,47 @@ export function GenerateAICourse(props: GenerateAICourseProps) { setTerm(paramsTerm); setDifficulty(paramsDifficulty); - handleGenerateCourse({ term: paramsTerm, difficulty: paramsDifficulty }); + + const sessionId = params?.id; + setSessionId(sessionId); + + let paramsGoal = ''; + let paramsAbout = ''; + let paramsCustomInstructions = ''; + + if (sessionId) { + const fineTuneData = getCourseFineTuneData(sessionId); + if (fineTuneData) { + paramsGoal = fineTuneData.goal; + paramsAbout = fineTuneData.about; + paramsCustomInstructions = fineTuneData.customInstructions; + + setGoal(paramsGoal); + setAbout(paramsAbout); + setCustomInstructions(paramsCustomInstructions); + } + } + + handleGenerateCourse({ + term: paramsTerm, + difficulty: paramsDifficulty, + instructions: paramsCustomInstructions, + goal: paramsGoal, + about: paramsAbout, + }); }, [term, difficulty]); const handleGenerateCourse = async (options: { term: string; difficulty: string; + instructions?: string; + goal?: string; + about?: string; isForce?: boolean; prompt?: string; }) => { - const { term, difficulty, isForce, prompt } = options; + const { term, difficulty, isForce, prompt, instructions, goal, about } = + options; if (!isLoggedIn()) { window.location.href = '/ai-tutor'; @@ -79,6 +114,9 @@ export function GenerateAICourse(props: GenerateAICourseProps) { onCourseChange: setCourse, onLoadingChange: setIsLoading, onError: setError, + instructions, + goal, + about, isForce, prompt, }); diff --git a/src/helper/generate-ai-course.ts b/src/helper/generate-ai-course.ts index 6adab7e9c..6ae70b431 100644 --- a/src/helper/generate-ai-course.ts +++ b/src/helper/generate-ai-course.ts @@ -12,6 +12,9 @@ type GenerateCourseOptions = { slug?: string; isForce?: boolean; prompt?: string; + instructions?: string; + goal?: string; + about?: string; onCourseIdChange?: (courseId: string) => void; onCourseSlugChange?: (courseSlug: string) => void; onCourseChange?: (course: AiCourse, rawData: string) => void; @@ -31,6 +34,9 @@ export async function generateCourse(options: GenerateCourseOptions) { onError, isForce = false, prompt, + instructions, + goal, + about, } = options; onLoadingChange?.(true); @@ -76,6 +82,9 @@ export async function generateCourse(options: GenerateCourseOptions) { difficulty, isForce, customPrompt: prompt, + instructions, + goal, + about, }), credentials: 'include', }, diff --git a/src/lib/ai.ts b/src/lib/ai.ts index f4f895d73..a76337636 100644 --- a/src/lib/ai.ts +++ b/src/lib/ai.ts @@ -55,6 +55,45 @@ export function generateAiCourseStructure( }; } +type CourseFineTuneData = { + about: string; + goal: string; + customInstructions: string; +}; + +export function storeFineTuneData(meta: CourseFineTuneData) { + const sessionId = Date.now().toString(); + + sessionStorage.setItem(sessionId, JSON.stringify(meta)); + sessionStorage.setItem('lastSessionId', sessionId); + + return sessionId; +} + +export function getCourseFineTuneData( + sessionId: string, +): CourseFineTuneData | null { + const meta = sessionStorage.getItem(sessionId); + if (!meta) { + return null; + } + + return JSON.parse(meta); +} + +export function getLastSessionId(): string | null { + return sessionStorage.getItem('lastSessionId'); +} + +export function clearFineTuneData() { + const sessionId = getLastSessionId(); + if (sessionId) { + sessionStorage.removeItem(sessionId); + } + + sessionStorage.removeItem('lastSessionId'); +} + const NEW_LINE = '\n'.charCodeAt(0); export async function readAIRoadmapStream(