1
0
mirror of https://github.com/kamranahmedse/developer-roadmap.git synced 2025-09-26 00:49:02 +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, RoadmapContentDocument,
} from '../CustomRoadmap/CustomRoadmap'; } from '../CustomRoadmap/CustomRoadmap';
import { markdownToHtml, sanitizeMarkdown } from '../../lib/markdown'; 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 { getUrlParams, parseUrl } from '../../lib/browser';
import { Spinner } from '../ReactIcons/Spinner'; import { Spinner } from '../ReactIcons/Spinner';
import { GitHubIcon } from '../ReactIcons/GitHubIcon.tsx'; import { GitHubIcon } from '../ReactIcons/GitHubIcon.tsx';
@@ -42,6 +42,11 @@ import { TopicProgressButton } from './TopicProgressButton.tsx';
import { CreateCourseModal } from './CreateCourseModal.tsx'; import { CreateCourseModal } from './CreateCourseModal.tsx';
import { useChat } from '@ai-sdk/react'; import { useChat } from '@ai-sdk/react';
import { topicDetailAiChatTransport } from '../../lib/ai.ts'; 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 = { type PaidResourceType = {
_id?: string; _id?: string;
@@ -121,16 +126,14 @@ export function TopicDetail(props: TopicDetailProps) {
defaultActiveTab = 'content', defaultActiveTab = 'content',
} = props; } = props;
const [hasEnoughLinks, setHasEnoughLinks] = useState(false);
const [contributionUrl, setContributionUrl] = useState(''); const [contributionUrl, setContributionUrl] = useState('');
const [isActive, setIsActive] = useState(false); const [isActive, setIsActive] = useState(false);
const [isLoading, setIsLoading] = useState(false); const [isTopicLoading, setIsTopicLoading] = useState(false);
const [isContributing, setIsContributing] = useState(false); const [isContributing, setIsContributing] = useState(false);
const [error, setError] = useState(''); const [error, setError] = useState('');
const [topicHtml, setTopicHtml] = useState(''); const [topicHtml, setTopicHtml] = useState('');
const [hasContent, setHasContent] = useState(false); const [hasContent, setHasContent] = useState(false);
const [topicTitle, setTopicTitle] = useState(''); const [topicTitle, setTopicTitle] = useState('');
const [topicHtmlTitle, setTopicHtmlTitle] = useState('');
const [links, setLinks] = useState<RoadmapContentDocument['links']>([]); const [links, setLinks] = useState<RoadmapContentDocument['links']>([]);
const [activeTab, setActiveTab] = const [activeTab, setActiveTab] =
useState<AllowedTopicDetailsTabs>(defaultActiveTab); useState<AllowedTopicDetailsTabs>(defaultActiveTab);
@@ -160,6 +163,40 @@ export function TopicDetail(props: TopicDetailProps) {
transport: topicDetailAiChatTransport, 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 = () => { const handleClose = () => {
onClose?.(); onClose?.();
setIsActive(false); setIsActive(false);
@@ -237,7 +274,7 @@ export function TopicDetail(props: TopicDetailProps) {
// Load the topic detail when the topic detail is active // Load the topic detail when the topic detail is active
useLoadTopic(({ topicId, resourceType, resourceId, isCustomResource }) => { useLoadTopic(({ topicId, resourceType, resourceId, isCustomResource }) => {
setError(''); setError('');
setIsLoading(true); setIsTopicLoading(true);
setIsActive(true); setIsActive(true);
setTopicId(topicId); setTopicId(topicId);
@@ -273,7 +310,7 @@ export function TopicDetail(props: TopicDetailProps) {
.then(({ response }) => { .then(({ response }) => {
if (!response) { if (!response) {
setError('Topic not found.'); setError('Topic not found.');
setIsLoading(false); setIsTopicLoading(false);
return; return;
} }
let topicHtml = ''; let topicHtml = '';
@@ -353,8 +390,6 @@ export function TopicDetail(props: TopicDetailProps) {
setLinks(listLinks); setLinks(listLinks);
setHasContent(topicHasContent); setHasContent(topicHasContent);
setContributionUrl(contributionUrl); setContributionUrl(contributionUrl);
setHasEnoughLinks(links.length >= 3);
setTopicHtmlTitle(titleElem?.textContent || '');
if (!topicHasContent && renderer === 'editor') { if (!topicHasContent && renderer === 'editor') {
setActiveTab('ai'); setActiveTab('ai');
@@ -371,12 +406,12 @@ export function TopicDetail(props: TopicDetailProps) {
topicHtml = markdownToHtml(sanitizedMarkdown, false); topicHtml = markdownToHtml(sanitizedMarkdown, false);
} }
setIsLoading(false); setIsTopicLoading(false);
setTopicHtml(topicHtml); setTopicHtml(topicHtml);
}) })
.catch((err) => { .catch((err) => {
setError('Something went wrong. Please try again later.'); setError('Something went wrong. Please try again later.');
setIsLoading(false); setIsTopicLoading(false);
}); });
}); });
@@ -391,9 +426,6 @@ export function TopicDetail(props: TopicDetailProps) {
return null; return null;
} }
const tnsLink =
'https://thenewstack.io/devops/?utm_source=roadmap.sh&utm_medium=Referral&utm_campaign=Topic';
const paidResourcesForTopic = paidResources.filter((resource) => { const paidResourcesForTopic = paidResources.filter((resource) => {
const normalizedTopicId = const normalizedTopicId =
topicId.indexOf('@') !== -1 ? topicId.split('@')[1] : topicId; topicId.indexOf('@') !== -1 ? topicId.split('@')[1] : topicId;
@@ -401,6 +433,8 @@ export function TopicDetail(props: TopicDetailProps) {
}); });
const shouldShowAiTab = !isCustomResource && resourceType === 'roadmap'; const shouldShowAiTab = !isCustomResource && resourceType === 'roadmap';
const subjects = roadmapTreeMapping?.subjects || [];
const hasSubjects = subjects.length > 0;
const hasDataCampResources = paidResources.some((resource) => const hasDataCampResources = paidResources.some((resource) =>
resource.title.toLowerCase().includes('datacamp'), 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 && ( {paidResourcesForTopic.length > 0 && (
<> <>
<ResourceListSeparator <ResourceListSeparator