import { useEffect, useMemo, useRef, useState } from 'react'; import { useKeydown } from '../../hooks/use-keydown'; import { useOutsideClick } from '../../hooks/use-outside-click'; import { markdownToHtml } from '../../lib/markdown'; import { Ban, Contact, FileText, X, ArrowRight } from 'lucide-react'; import { Spinner } from '../ReactIcons/Spinner'; import type { RoadmapNodeDetails } from './GenerateRoadmap'; import { isLoggedIn, removeAuthToken } from '../../lib/jwt'; import { cn } from '../../lib/classname'; import { showLoginPopup } from '../../lib/popup'; import { readAIRoadmapContentStream } from '../../lib/ai'; type RoadmapTopicDetailProps = RoadmapNodeDetails & { onClose?: () => void; roadmapId: string; topicLimitUsed: number; topicLimit: number; onTopicContentGenerateComplete?: () => void; onConfigureOpenAI?: () => void; }; export function RoadmapTopicDetail(props: RoadmapTopicDetailProps) { const { onClose, roadmapId, nodeTitle, parentTitle, topicLimit, topicLimitUsed, onTopicContentGenerateComplete, onConfigureOpenAI, } = props; const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(''); const [topicHtml, setTopicHtml] = useState(''); const [isStreaming, setIsStreaming] = useState(false); const topicRef = useRef(null); const abortController = useMemo(() => new AbortController(), []); const generateAiRoadmapTopicContent = async () => { setIsLoading(true); setError(''); if (!isLoggedIn()) { return; } if (!roadmapId || !nodeTitle) { setIsLoading(false); setError('Invalid roadmap id or node title'); return; } const response = await fetch( `${import.meta.env.PUBLIC_API_URL}/v1-generate-ai-roadmap-content/${roadmapId}`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, credentials: 'include', body: JSON.stringify({ nodeTitle, parentTitle, }), signal: abortController.signal, }, ); if (!response.ok) { const data = await response.json(); setError(data?.message || 'Something went wrong'); setIsLoading(false); // Logout user if token is invalid if (data.status === 401) { removeAuthToken(); window.location.reload(); } } const reader = response.body?.getReader(); if (!reader) { setIsLoading(false); setError('Something went wrong'); return; } setIsLoading(false); setIsStreaming(true); await readAIRoadmapContentStream(reader, { onStream: async (result) => { setTopicHtml(markdownToHtml(result, false)); }, onStreamEnd(roadmap) { setIsStreaming(false); }, }); onTopicContentGenerateComplete?.(); }; // Close the topic detail when user clicks outside the topic detail useOutsideClick(topicRef, () => { onClose?.(); }); useKeydown('Escape', () => { onClose?.(); }); useEffect(() => { if (!topicRef?.current) { return; } topicRef?.current?.focus(); generateAiRoadmapTopicContent().finally(() => {}); return () => { abortController.abort(); }; }, []); const hasContent = topicHtml?.length > 0; return (
{isLoggedIn() && (
{topicLimitUsed} of {topicLimit} {' '} topics generated
)} {isLoggedIn() && isLoading && (
)} {!isLoggedIn() && (

You must be logged in

Sign up or login to generate topic content.

)} {!isLoading && !error && ( <>
{hasContent ? (
{!isStreaming && ( )}
) : (

Empty Content

)} )} {/* Error */} {!isLoading && error && ( <>

{error}

)}
); }