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:
committed by
Kamran Ahmed
parent
23ab77b426
commit
d1f863eeac
@@ -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
|
||||||
|
Reference in New Issue
Block a user