mirror of
https://github.com/kamranahmedse/developer-roadmap.git
synced 2025-09-25 08:35:42 +02:00
chore: ai tutor courses
This commit is contained in:
committed by
Kamran Ahmed
parent
23ab77b426
commit
d1f863eeac
@@ -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
|
||||
|
Reference in New Issue
Block a user