Map and outline view to share header

feat/ai-roadmap
Kamran Ahmed 1 month ago
parent 4b560c80de
commit 5d1e796b3d
  1. 3
      src/components/GenerateCourse/AICourseContent.tsx
  2. 35
      src/components/GenerateCourse/AICourseOutlineHeader.tsx
  3. 28
      src/components/GenerateCourse/AICourseOutlineView.tsx
  4. 29
      src/components/GenerateCourse/AICourseRoadmapView.tsx

@ -435,6 +435,9 @@ export function AICourseContent(props: AICourseContentProps) {
<AICourseRoadmapView <AICourseRoadmapView
done={course.done} done={course.done}
courseSlug={courseSlug!} courseSlug={courseSlug!}
course={course}
isLoading={isLoading}
onRegenerateOutline={onRegenerateOutline}
setActiveModuleIndex={setActiveModuleIndex} setActiveModuleIndex={setActiveModuleIndex}
setActiveLessonIndex={setActiveLessonIndex} setActiveLessonIndex={setActiveLessonIndex}
setViewMode={setViewMode} setViewMode={setViewMode}

@ -0,0 +1,35 @@
import { cn } from '../../lib/classname';
import type { AiCourse } from '../../lib/ai';
import { RegenerateOutline } from './RegenerateOutline';
type AICourseOutlineHeaderProps = {
course: AiCourse;
isLoading: boolean;
onRegenerateOutline: (prompt?: string) => void;
};
export function AICourseOutlineHeader(props: AICourseOutlineHeaderProps) {
const { course, isLoading, onRegenerateOutline } = props;
return (
<div
className={cn(
'relative mb-1 flex items-start justify-between border-b border-gray-100 p-6 max-lg:hidden',
isLoading && 'striped-loader',
)}
>
<div>
<h2 className="mb-1 text-balance text-2xl font-bold max-lg:text-lg max-lg:leading-tight">
{course.title || 'Loading course ..'}
</h2>
<p className="text-sm capitalize text-gray-500">
{course.title ? course.difficulty : 'Please wait ..'}
</p>
</div>
{!isLoading && (
<RegenerateOutline onRegenerateOutline={onRegenerateOutline} />
)}
</div>
);
}

@ -1,4 +1,3 @@
import { RegenerateOutline } from './RegenerateOutline';
import { cn } from '../../lib/classname'; import { cn } from '../../lib/classname';
import type { AiCourse } from '../../lib/ai'; import type { AiCourse } from '../../lib/ai';
import { slugify } from '../../lib/slugger'; import { slugify } from '../../lib/slugger';
@ -6,6 +5,7 @@ import { CheckIcon } from '../ReactIcons/CheckIcon';
import type { Dispatch, SetStateAction } from 'react'; import type { Dispatch, SetStateAction } from 'react';
import { Loader2Icon } from 'lucide-react'; import { Loader2Icon } from 'lucide-react';
import type { AICourseViewMode } from './AICourseContent'; import type { AICourseViewMode } from './AICourseContent';
import { AICourseOutlineHeader } from './AICourseOutlineHeader';
type AICourseOutlineViewProps = { type AICourseOutlineViewProps = {
course: AiCourse; course: AiCourse;
@ -33,26 +33,12 @@ export function AICourseOutlineView(props: AICourseOutlineViewProps) {
const aiCourseProgress = course.done || []; const aiCourseProgress = course.done || [];
return ( return (
<div className="mx-auto rounded-xl border border-gray-200 bg-white shadow-sm lg:max-w-3xl"> <div className="mx-auto rounded-xl border border-gray-200 bg-white shadow-sm lg:max-w-5xl">
<div <AICourseOutlineHeader
className={cn( course={course}
'relative mb-1 flex items-start justify-between border-b border-gray-100 p-6 max-lg:hidden', isLoading={isLoading}
isLoading && 'striped-loader', onRegenerateOutline={onRegenerateOutline}
)} />
>
<div>
<h2 className="mb-1 text-balance text-2xl font-bold max-lg:text-lg max-lg:leading-tight">
{course.title || 'Loading course ..'}
</h2>
<p className="text-sm capitalize text-gray-500">
{course.title ? course.difficulty : 'Please wait ..'}
</p>
</div>
{!isLoading && (
<RegenerateOutline onRegenerateOutline={onRegenerateOutline} />
)}
</div>
{course.title ? ( {course.title ? (
<div className="flex flex-col p-6 max-lg:mt-0.5 max-lg:p-4"> <div className="flex flex-col p-6 max-lg:mt-0.5 max-lg:p-4">
{course.modules.map((courseModule, moduleIdx) => { {course.modules.map((courseModule, moduleIdx) => {

@ -22,10 +22,15 @@ import { renderTopicProgress } from '../../lib/resource-progress';
import { queryClient } from '../../stores/query-client'; import { queryClient } from '../../stores/query-client';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { billingDetailsOptions } from '../../queries/billing'; import { billingDetailsOptions } from '../../queries/billing';
import { AICourseOutlineHeader } from './AICourseOutlineHeader';
import type { AiCourse } from '../../lib/ai';
export type AICourseRoadmapViewProps = { export type AICourseRoadmapViewProps = {
done: string[]; done: string[];
courseSlug: string; courseSlug: string;
course: AiCourse;
isLoading: boolean;
onRegenerateOutline: (prompt?: string) => void;
setActiveModuleIndex: (index: number) => void; setActiveModuleIndex: (index: number) => void;
setActiveLessonIndex: (index: number) => void; setActiveLessonIndex: (index: number) => void;
setViewMode: (mode: AICourseViewMode) => void; setViewMode: (mode: AICourseViewMode) => void;
@ -37,6 +42,9 @@ export function AICourseRoadmapView(props: AICourseRoadmapViewProps) {
const { const {
done = [], done = [],
courseSlug, courseSlug,
course,
isLoading,
onRegenerateOutline,
setActiveModuleIndex, setActiveModuleIndex,
setActiveLessonIndex, setActiveLessonIndex,
setViewMode, setViewMode,
@ -47,7 +55,6 @@ export function AICourseRoadmapView(props: AICourseRoadmapViewProps) {
const containerEl = useRef<HTMLDivElement>(null); const containerEl = useRef<HTMLDivElement>(null);
const [roadmapStructure, setRoadmapStructure] = useState<ResultItem[]>([]); const [roadmapStructure, setRoadmapStructure] = useState<ResultItem[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [isGenerating, setIsGenerating] = useState(false); const [isGenerating, setIsGenerating] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
@ -76,7 +83,7 @@ export function AICourseRoadmapView(props: AICourseRoadmapViewProps) {
data?.message || 'Something went wrong', data?.message || 'Something went wrong',
); );
setError(data?.message || 'Something went wrong'); setError(data?.message || 'Something went wrong');
setIsLoading(false); setIsGenerating(false);
return; return;
} }
@ -84,11 +91,10 @@ export function AICourseRoadmapView(props: AICourseRoadmapViewProps) {
if (!reader) { if (!reader) {
console.error('Failed to get reader from response'); console.error('Failed to get reader from response');
setError('Something went wrong'); setError('Something went wrong');
setIsLoading(false); setIsGenerating(false);
return; return;
} }
setIsLoading(false);
setIsGenerating(true); setIsGenerating(true);
await readAIRoadmapStream(reader, { await readAIRoadmapStream(reader, {
onStream: async (result) => { onStream: async (result) => {
@ -124,7 +130,7 @@ export function AICourseRoadmapView(props: AICourseRoadmapViewProps) {
} catch (error) { } catch (error) {
console.error('Error generating course roadmap:', error); console.error('Error generating course roadmap:', error);
setError('Something went wrong'); setError('Something went wrong');
setIsLoading(false); setIsGenerating(false);
} }
}; };
@ -138,7 +144,7 @@ export function AICourseRoadmapView(props: AICourseRoadmapViewProps) {
const handleNodeClick = useCallback( const handleNodeClick = useCallback(
(e: MouseEvent<HTMLDivElement, unknown>) => { (e: MouseEvent<HTMLDivElement, unknown>) => {
if (isLoading || isGenerating) { if (isGenerating) {
return; return;
} }
@ -189,7 +195,6 @@ export function AICourseRoadmapView(props: AICourseRoadmapViewProps) {
setViewMode('module'); setViewMode('module');
}, },
[ [
isLoading,
roadmapStructure, roadmapStructure,
setExpandedModules, setExpandedModules,
setActiveModuleIndex, setActiveModuleIndex,
@ -200,13 +205,21 @@ export function AICourseRoadmapView(props: AICourseRoadmapViewProps) {
return ( return (
<div className="relative mx-auto min-h-[500px] rounded-xl border border-gray-200 bg-white shadow-sm lg:max-w-5xl"> <div className="relative mx-auto min-h-[500px] rounded-xl border border-gray-200 bg-white shadow-sm lg:max-w-5xl">
<AICourseOutlineHeader
course={course}
isLoading={isLoading}
onRegenerateOutline={(prompt) => {
setViewMode('outline');
onRegenerateOutline(prompt);
}}
/>
{isLoading && ( {isLoading && (
<div className="absolute inset-0 flex h-full w-full items-center justify-center"> <div className="absolute inset-0 flex h-full w-full items-center justify-center">
<Loader2Icon className="h-10 w-10 animate-spin stroke-[3px]" /> <Loader2Icon className="h-10 w-10 animate-spin stroke-[3px]" />
</div> </div>
)} )}
{error && !isLoading && !isGenerating && ( {error && !isGenerating && (
<div className="absolute inset-0 flex h-full w-full flex-col items-center justify-center"> <div className="absolute inset-0 flex h-full w-full flex-col items-center justify-center">
<Frown className="size-20 text-red-500" /> <Frown className="size-20 text-red-500" />
<p className="mx-auto mt-5 max-w-[250px] text-balance text-center text-base text-red-500"> <p className="mx-auto mt-5 max-w-[250px] text-balance text-center text-base text-red-500">

Loading…
Cancel
Save