computer-scienceangular-roadmapbackend-roadmapblockchain-roadmapdba-roadmapdeveloper-roadmapdevops-roadmapfrontend-roadmapgo-roadmaphactoberfestjava-roadmapjavascript-roadmapnodejs-roadmappython-roadmapqa-roadmapreact-roadmaproadmapstudy-planvue-roadmapweb3-roadmap
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
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> |
|
); |
|
}
|
|
|