1
0
mirror of https://github.com/kamranahmedse/developer-roadmap.git synced 2025-09-25 00:21:28 +02:00

chore: ai tutor courses

This commit is contained in:
Arik Chakma
2025-09-05 13:57:35 +06:00
committed by Kamran Ahmed
parent 23ab77b426
commit d1f863eeac

View File

@@ -21,7 +21,7 @@ import type {
RoadmapContentDocument,
} from '../CustomRoadmap/CustomRoadmap';
import { markdownToHtml, sanitizeMarkdown } from '../../lib/markdown';
import { Ban, FileText, HeartHandshake, Star, X } from 'lucide-react';
import { Ban, BookOpen, FileText, HeartHandshake, Star, X } from 'lucide-react';
import { getUrlParams, parseUrl } from '../../lib/browser';
import { Spinner } from '../ReactIcons/Spinner';
import { GitHubIcon } from '../ReactIcons/GitHubIcon.tsx';
@@ -42,6 +42,11 @@ import { TopicProgressButton } from './TopicProgressButton.tsx';
import { CreateCourseModal } from './CreateCourseModal.tsx';
import { useChat } from '@ai-sdk/react';
import { topicDetailAiChatTransport } from '../../lib/ai.ts';
import { useQuery } from '@tanstack/react-query';
import { queryClient } from '../../stores/query-client.ts';
import { roadmapTreeMappingOptions } from '../../queries/roadmap-tree.ts';
import { aiLimitOptions } from '../../queries/ai-course.ts';
import { billingDetailsOptions } from '../../queries/billing.ts';
type PaidResourceType = {
_id?: string;
@@ -121,16 +126,14 @@ export function TopicDetail(props: TopicDetailProps) {
defaultActiveTab = 'content',
} = props;
const [hasEnoughLinks, setHasEnoughLinks] = useState(false);
const [contributionUrl, setContributionUrl] = useState('');
const [isActive, setIsActive] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [isTopicLoading, setIsTopicLoading] = useState(false);
const [isContributing, setIsContributing] = useState(false);
const [error, setError] = useState('');
const [topicHtml, setTopicHtml] = useState('');
const [hasContent, setHasContent] = useState(false);
const [topicTitle, setTopicTitle] = useState('');
const [topicHtmlTitle, setTopicHtmlTitle] = useState('');
const [links, setLinks] = useState<RoadmapContentDocument['links']>([]);
const [activeTab, setActiveTab] =
useState<AllowedTopicDetailsTabs>(defaultActiveTab);
@@ -160,6 +163,40 @@ export function TopicDetail(props: TopicDetailProps) {
transport: topicDetailAiChatTransport,
});
const sanitizedTopicId = topicId?.includes('@')
? topicId?.split('@')?.[1]
: topicId;
const { data: roadmapTreeMapping, isLoading: isRoadmapTreeMappingLoading } =
useQuery(
{
...roadmapTreeMappingOptions(resourceId),
select: (data) => {
const node = data.find(
(mapping) => mapping.nodeId === sanitizedTopicId,
);
return node;
},
enabled: !!sanitizedTopicId && !isCustomResource,
},
queryClient,
);
const { data: tokenUsage, isLoading: isTokenUsageLoading } = useQuery(
aiLimitOptions(),
queryClient,
);
const { data: userBillingDetails, isLoading: isBillingDetailsLoading } =
useQuery(billingDetailsOptions(), queryClient);
const isLimitExceeded = (tokenUsage?.used || 0) >= (tokenUsage?.limit || 0);
const isPaidUser = userBillingDetails?.status === 'active';
const isLoading =
isTopicLoading ||
isRoadmapTreeMappingLoading ||
isTokenUsageLoading ||
isBillingDetailsLoading;
const handleClose = () => {
onClose?.();
setIsActive(false);
@@ -237,7 +274,7 @@ export function TopicDetail(props: TopicDetailProps) {
// Load the topic detail when the topic detail is active
useLoadTopic(({ topicId, resourceType, resourceId, isCustomResource }) => {
setError('');
setIsLoading(true);
setIsTopicLoading(true);
setIsActive(true);
setTopicId(topicId);
@@ -273,7 +310,7 @@ export function TopicDetail(props: TopicDetailProps) {
.then(({ response }) => {
if (!response) {
setError('Topic not found.');
setIsLoading(false);
setIsTopicLoading(false);
return;
}
let topicHtml = '';
@@ -353,8 +390,6 @@ export function TopicDetail(props: TopicDetailProps) {
setLinks(listLinks);
setHasContent(topicHasContent);
setContributionUrl(contributionUrl);
setHasEnoughLinks(links.length >= 3);
setTopicHtmlTitle(titleElem?.textContent || '');
if (!topicHasContent && renderer === 'editor') {
setActiveTab('ai');
@@ -371,12 +406,12 @@ export function TopicDetail(props: TopicDetailProps) {
topicHtml = markdownToHtml(sanitizedMarkdown, false);
}
setIsLoading(false);
setIsTopicLoading(false);
setTopicHtml(topicHtml);
})
.catch((err) => {
setError('Something went wrong. Please try again later.');
setIsLoading(false);
setIsTopicLoading(false);
});
});
@@ -391,9 +426,6 @@ export function TopicDetail(props: TopicDetailProps) {
return null;
}
const tnsLink =
'https://thenewstack.io/devops/?utm_source=roadmap.sh&utm_medium=Referral&utm_campaign=Topic';
const paidResourcesForTopic = paidResources.filter((resource) => {
const normalizedTopicId =
topicId.indexOf('@') !== -1 ? topicId.split('@')[1] : topicId;
@@ -401,6 +433,8 @@ export function TopicDetail(props: TopicDetailProps) {
});
const shouldShowAiTab = !isCustomResource && resourceType === 'roadmap';
const subjects = roadmapTreeMapping?.subjects || [];
const hasSubjects = subjects.length > 0;
const hasDataCampResources = paidResources.some((resource) =>
resource.title.toLowerCase().includes('datacamp'),
@@ -615,6 +649,45 @@ export function TopicDetail(props: TopicDetailProps) {
</>
)}
{hasSubjects && (
<>
<ResourceListSeparator
text="AI Tutor Courses"
className="text-blue-600"
icon={BookOpen}
/>
<ul className="mt-4 ml-3 flex flex-wrap gap-1 text-sm">
{subjects.map((subject) => {
return (
<li key={subject}>
<a
key={subject}
target="_blank"
onClick={(e) => {
if (!isLoggedIn()) {
e.preventDefault();
showLoginPopup();
return;
}
if (isLimitExceeded && !isPaidUser) {
e.preventDefault();
setShowUpgradeModal(true);
return;
}
}}
href={`/ai/course/search?term=${subject}&src=topic`}
className="flex items-center gap-1 rounded-md border border-gray-300 bg-gray-100 px-2 py-1 hover:bg-gray-200 hover:text-black"
>
{subject}
</a>
</li>
);
})}
</ul>
</>
)}
{paidResourcesForTopic.length > 0 && (
<>
<ResourceListSeparator