Roadmap to becoming a developer in 2022
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

250 lines
8.1 KiB

import '../GenerateRoadmap/GenerateRoadmap.css';
import { renderFlowJSON } from '../../../editor/renderer/renderer';
import { generateAICourseRoadmapStructure, readAIRoadmapStream, type ResultItem } from '../../lib/ai';
import { useCallback, useEffect, useRef, useState, type Dispatch, type SetStateAction, type MouseEvent } from 'react';
import type { AICourseViewMode } from './AICourseContent';
import { replaceChildren } from '../../lib/dom';
import { Frown, Loader2Icon } from 'lucide-react';
import { renderTopicProgress } from '../../lib/resource-progress';
import { queryClient } from '../../stores/query-client';
import { useQuery } from '@tanstack/react-query';
import { billingDetailsOptions } from '../../queries/billing';
import { AICourseOutlineHeader } from './AICourseOutlineHeader';
import type { AiCourse } from '../../lib/ai';
import { generateAIRoadmapFromText } from '../../utils/roadmapUtils'; // 'someUtilityFunction' kaldırıldı
export type AICourseRoadmapViewProps = {
done: string[];
courseSlug: string;
course: AiCourse;
isLoading: boolean;
onRegenerateOutline: (prompt?: string) => void;
setActiveModuleIndex: (index: number) => void;
setActiveLessonIndex: (index: number) => void;
setViewMode: (mode: AICourseViewMode) => void;
onUpgradeClick: () => void;
setExpandedModules: Dispatch<SetStateAction<Record<number, boolean>>>;
viewMode: AICourseViewMode;
};
export function AICourseRoadmapView(props: AICourseRoadmapViewProps) {
const {
done = [],
courseSlug,
course,
isLoading,
onRegenerateOutline,
setActiveModuleIndex,
setActiveLessonIndex,
setViewMode,
setExpandedModules,
onUpgradeClick,
viewMode,
} = props;
const containerEl = useRef<HTMLDivElement>(null);
const [roadmapStructure, setRoadmapStructure] = useState<ResultItem[]>([]);
const [isGenerating, setIsGenerating] = useState(false);
const [error, setError] = useState<string | null>(null);
const { data: userBillingDetails, isLoading: isBillingDetailsLoading } =
useQuery(billingDetailsOptions(), queryClient);
const isPaidUser = userBillingDetails?.status === 'active';
const generateAICourseRoadmap = async (courseSlug: string) => {
try {
const response = await fetch(
`${import.meta.env.PUBLIC_API_URL}/v1-generate-ai-course-roadmap/${courseSlug}`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include',
},
);
if (!response.ok) {
const data = await response.json();
console.error(
'Error generating course roadmap:',
data?.message || 'Something went wrong',
);
setError(data?.message || 'Something went wrong');
setIsGenerating(false);
return;
}
const reader = response.body?.getReader();
if (!reader) {
console.error('Failed to get reader from response');
setError('Something went wrong');
setIsGenerating(false);
return;
}
setIsGenerating(true);
await readAIRoadmapStream(reader, {
onStream: async (result) => {
const roadmap = generateAICourseRoadmapStructure(result, true);
const { nodes, edges } = generateAIRoadmapFromText({
nodes: roadmap.filter((item) => item.type === 'node' as any) as ResultItem[],
edges: roadmap.filter((item) => item.type === 'edge' as any) as ResultItem[],
});
const svg = await renderFlowJSON({ nodes, edges });
if (svg !== undefined && svg !== null) {
replaceChildren(containerEl.current!, svg); // Fixed usage
}
},
onStreamEnd: async (result) => {
const roadmap = generateAICourseRoadmapStructure(result, true);
const { nodes, edges } = generateAIRoadmapFromText({
nodes: roadmap.filter((item) => item.type === 'node' as any) as ResultItem[],
edges: roadmap.filter((item) => item.type === 'edge' as any) as ResultItem[],
});
const svg = await renderFlowJSON({ nodes, edges });
if (svg !== undefined && svg !== null) {
replaceChildren(containerEl.current!, svg); // Fixed usage
}
setRoadmapStructure(roadmap);
setIsGenerating(false);
done.forEach((id) => {
renderTopicProgress(id, 'done');
});
const modules = roadmap.filter((item) => item.type === 'topic');
for (const module of modules) {
const moduleId = module.id;
const isAllLessonsDone =
module?.children?.every((child) => done.includes(child.id)) ??
false;
if (isAllLessonsDone) {
renderTopicProgress(moduleId, 'done');
}
}
},
});
} catch (error) {
console.error('Error generating course roadmap:', error);
setError('Something went wrong');
setIsGenerating(false);
}
};
useEffect(() => {
if (!courseSlug) {
return;
}
generateAICourseRoadmap(courseSlug);
}, []);
const handleNodeClick = useCallback(
(e: MouseEvent<HTMLDivElement, unknown>) => {
if (isGenerating) {
return;
}
const target = e.target as SVGElement;
const targetGroup = (target?.closest('g') as SVGElement) || {};
const nodeId = targetGroup?.dataset?.nodeId;
const nodeType = targetGroup?.dataset?.type;
if (!nodeId || !nodeType) {
return null;
}
if (nodeType === 'topic') {
const topicIndex = roadmapStructure
.filter((item) => item.type === 'topic')
.findIndex((item) => item.id === nodeId);
setExpandedModules((prev) => {
const newState: Record<number, boolean> = {};
roadmapStructure.forEach((_, idx) => {
newState[idx] = false;
});
newState[topicIndex] = true;
return newState;
});
setActiveModuleIndex(topicIndex);
setActiveLessonIndex(0);
setViewMode('module');
return;
}
if (nodeType !== 'subtopic') {
return null;
}
const [moduleIndex, topicIndex] = nodeId.split('-').map(Number);
setExpandedModules((prev) => {
const newState: Record<number, boolean> = {};
roadmapStructure.forEach((_, idx) => {
newState[idx] = false;
});
newState[moduleIndex] = true;
return newState;
});
setActiveModuleIndex(moduleIndex);
setActiveLessonIndex(topicIndex);
setViewMode('module');
},
[
roadmapStructure,
setExpandedModules,
setActiveModuleIndex,
setActiveLessonIndex,
setViewMode,
],
);
return (
<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);
}}
viewMode={viewMode}
setViewMode={setViewMode}
/>
{isLoading && (
<div className="absolute inset-0 flex h-full w-full items-center justify-center">
<Loader2Icon className="h-10 w-10 animate-spin stroke-[3px]" />
</div>
)}
{error && !isGenerating && (
<div className="absolute inset-0 flex h-full w-full flex-col items-center justify-center">
<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">
{error || 'Something went wrong'}
</p>
{!isPaidUser && (error || '')?.includes('limit') && (
<button
onClick={onUpgradeClick}
className="mt-5 rounded-full bg-red-600 px-4 py-1 text-white hover:bg-red-700"
>
Upgrade Account
</button>
)}
</div>
)}
<div
id={'resource-svg-wrap'}
ref={containerEl}
onClick={handleNodeClick}
className="px-4 pb-2"
></div>
</div>
);
}