diff --git a/src/components/GenerateCourse/AICourseLessonChat.tsx b/src/components/GenerateCourse/AICourseLessonChat.tsx index 66e1c674f..ae4c3f317 100644 --- a/src/components/GenerateCourse/AICourseLessonChat.tsx +++ b/src/components/GenerateCourse/AICourseLessonChat.tsx @@ -399,7 +399,7 @@ type AIChatCardProps = { html?: string; }; -function AIChatCard(props: AIChatCardProps) { +export function AIChatCard(props: AIChatCardProps) { const { role, content, html: defaultHtml } = props; const html = useMemo(() => { diff --git a/src/components/TopicDetail/TopicDetail.tsx b/src/components/TopicDetail/TopicDetail.tsx index 4483a16c7..eac458dd3 100644 --- a/src/components/TopicDetail/TopicDetail.tsx +++ b/src/components/TopicDetail/TopicDetail.tsx @@ -22,7 +22,16 @@ import type { RoadmapContentDocument, } from '../CustomRoadmap/CustomRoadmap'; import { markdownToHtml, sanitizeMarkdown } from '../../lib/markdown'; -import { Ban, Coins, FileText, HeartHandshake, Star, X } from 'lucide-react'; +import { + Ban, + BookIcon, + Coins, + FileText, + HeartHandshake, + SparklesIcon, + Star, + X, +} from 'lucide-react'; import { getUrlParams, parseUrl } from '../../lib/browser'; import { Spinner } from '../ReactIcons/Spinner'; import { GitHubIcon } from '../ReactIcons/GitHubIcon.tsx'; @@ -33,6 +42,13 @@ import { lockBodyScroll } from '../../lib/dom.ts'; import { TopicDetailLink } from './TopicDetailLink.tsx'; import { ResourceListSeparator } from './ResourceListSeparator.tsx'; import { PaidResourceDisclaimer } from './PaidResourceDisclaimer.tsx'; +import { + TopicDetailsTabs, + type AllowedTopicDetailsTabs, +} from './TopicDetailsTabs.tsx'; +import { TopicDetailAI } from './TopicDetailAI.tsx'; +import { cn } from '../../lib/classname.ts'; +import type { AIChatHistoryType } from '../GenerateCourse/AICourseLessonChat.tsx'; type TopicDetailProps = { resourceId?: string; @@ -53,6 +69,14 @@ type PaidResourceType = { const paidResourcesCache: Record = {}; +const defaultChatHistory: AIChatHistoryType[] = [ + { + role: 'assistant', + content: 'Hey, I am your AI instructor. How can I help you today? 🤖', + isDefault: true, + }, +]; + async function fetchRoadmapPaidResources(roadmapId: string) { if (paidResourcesCache[roadmapId]) { return paidResourcesCache[roadmapId]; @@ -93,6 +117,11 @@ export function TopicDetail(props: TopicDetailProps) { const [topicTitle, setTopicTitle] = useState(''); const [topicHtmlTitle, setTopicHtmlTitle] = useState(''); const [links, setLinks] = useState([]); + const [activeTab, setActiveTab] = + useState('content'); + const [aiChatHistory, setAiChatHistory] = + useState(defaultChatHistory); + const toast = useToast(); const [showPaidResourceDisclaimer, setShowPaidResourceDisclaimer] = @@ -108,14 +137,15 @@ export function TopicDetail(props: TopicDetailProps) { const [resourceType, setResourceType] = useState('roadmap'); const [paidResources, setPaidResources] = useState([]); - // Close the topic detail when user clicks outside the topic detail - useOutsideClick(topicRef, () => { + const handleClose = () => { setIsActive(false); - }); + setAiChatHistory(defaultChatHistory); + setActiveTab('content'); + }; - useKeydown('Escape', () => { - setIsActive(false); - }); + // Close the topic detail when user clicks outside the topic detail + useOutsideClick(topicRef, handleClose); + useKeydown('Escape', handleClose); useEffect(() => { if (resourceType !== 'roadmap' || !defaultResourceId) { @@ -344,7 +374,7 @@ export function TopicDetail(props: TopicDetailProps) {
{isLoading && (
@@ -359,9 +389,12 @@ export function TopicDetail(props: TopicDetailProps) { {!isContributing && !isLoading && !error && ( <> -
- {/* Actions for the topic */} -
+
+
{!isEmbed && ( { - setIsActive(false); - }} + onClose={handleClose} /> )}
- {/* Topic Content */} - {hasContent ? ( - <> -
- {topicTitle &&

{topicTitle}

} -
-
- - ) : ( - <> - {!canSubmitContribution && ( -
- -

- Empty Content -

-
- )} - {canSubmitContribution && ( -
- -

- Help us write this content -

-

- Write a brief introduction to this topic and submit a - link to a good article, podcast, video, or any other - self-vetted resource that helped you understand this - topic better. -

- - - Help us Write this Content - -
- )} - - )} + - {links.length > 0 && ( - <> - -
    - {links.map((link) => { - return ( -
  • - { - // if it is one of our roadmaps, we want to track the click - if (canSubmitContribution) { - const parsedUrl = parseUrl(link.url); - - window.fireEvent({ - category: 'TopicResourceClick', - action: `Click: ${parsedUrl.hostname}`, - label: `${resourceType} / ${resourceId} / ${topicId} / ${link.url}`, - }); - } - }} - /> -
  • - ); - })} -
- + {activeTab === 'ai' && ( + )} - {paidResourcesForTopic.length > 0 && ( + {activeTab === 'content' && ( <> - - -
    - {paidResourcesForTopic.map((resource) => { - return ( -
  • - -
  • - ); - })} -
- - {hasPaidScrimbaLinks && ( -
-
- - - Scrimba is offering{' '} - 20% off on - all courses for roadmap.sh users. - + {hasContent ? ( + <> +
+ {topicTitle &&

{topicTitle}

} +
-
+ + ) : ( + <> + {!canSubmitContribution && ( +
+ +

+ Empty Content +

+
+ )} + {canSubmitContribution && ( +
+ +

+ Help us write this content +

+

+ Write a brief introduction to this topic and submit + a link to a good article, podcast, video, or any + other self-vetted resource that helped you + understand this topic better. +

+ + + Help us Write this Content + +
+ )} + )} - {showPaidResourceDisclaimer && ( - { - localStorage.setItem( - PAID_RESOURCE_DISCLAIMER_HIDDEN, - 'true', - ); - setShowPaidResourceDisclaimer(false); - }} - /> + {links.length > 0 && ( + <> + +
    + {links.map((link) => { + return ( +
  • + { + // if it is one of our roadmaps, we want to track the click + if (canSubmitContribution) { + const parsedUrl = parseUrl(link.url); + + window.fireEvent({ + category: 'TopicResourceClick', + action: `Click: ${parsedUrl.hostname}`, + label: `${resourceType} / ${resourceId} / ${topicId} / ${link.url}`, + }); + } + }} + /> +
  • + ); + })} +
+ )} - - )} - {/* Contribution */} - {canSubmitContribution && - !hasEnoughLinks && - contributionUrl && - hasContent && ( -
-
-

- Find more resources using these pre-filled search - queries: -

-
- - - Google - + {paidResourcesForTopic.length > 0 && ( + <> + + +
    + {paidResourcesForTopic.map((resource) => { + return ( +
  • + +
  • + ); + })} +
+ + {hasPaidScrimbaLinks && ( +
+
+ + + Scrimba is offering{' '} + 20% off{' '} + on all courses for roadmap.sh users. + +
+
+ )} + + {showPaidResourceDisclaimer && ( + { + localStorage.setItem( + PAID_RESOURCE_DISCLAIMER_HIDDEN, + 'true', + ); + setShowPaidResourceDisclaimer(false); + }} + /> + )} + + )} + + {canSubmitContribution && + !hasEnoughLinks && + contributionUrl && + hasContent && ( +
+
+

+ Find more resources using these pre-filled search + queries: +

+ +
+ +

+ This popup should be a brief introductory paragraph + for the topic and a few links to good articles, + videos, or any other self-vetted resources. Please + consider submitting a PR to improve this content. +

- - YouTube + + Help us Improve this Content
-
- -

- This popup should be a brief introductory paragraph for - the topic and a few links to good articles, videos, or any - other self-vetted resources. Please consider submitting a - PR to improve this content. -

- - - Help us Improve this Content - -
- )} + )} + + )}
- {resourceId === 'devops' && ( + + {resourceId === 'devops' && activeTab === 'content' && (
)} - {/* Error */} {!isContributing && !isLoading && error && ( <> + ))} +
+
+ )} */} + + ); + })} + + {isStreamingMessage && !streamedMessage && ( + + )} + + {streamedMessage && ( + + )} +
+
+
+
+ +
+ {isLimitExceeded && isLoggedIn() && ( +
+ +

+ Limit reached for today + {isPaidUser ? '. Please wait until tomorrow.' : ''} +

+ {!isPaidUser && ( + + )} +
+ )} + {!isLoggedIn() && ( +
+ +

Please login to continue

+ +
+ )} + setMessage(e.target.value)} + autoFocus={true} + onKeyDown={(e) => { + // if (e.key === 'Enter' && !e.shiftKey) { + // handleChatSubmit(e as unknown as FormEvent); + // } + }} + // ref={textareaRef} + /> + + +
+ ); +} diff --git a/src/components/TopicDetail/TopicDetailsTabs.tsx b/src/components/TopicDetail/TopicDetailsTabs.tsx new file mode 100644 index 000000000..855bfe185 --- /dev/null +++ b/src/components/TopicDetail/TopicDetailsTabs.tsx @@ -0,0 +1,53 @@ +import { BookIcon, SparklesIcon, type LucideIcon } from 'lucide-react'; + +export type AllowedTopicDetailsTabs = 'content' | 'ai'; + +type TopicDetailsTabsProps = { + activeTab: AllowedTopicDetailsTabs; + setActiveTab: (tab: AllowedTopicDetailsTabs) => void; +}; + +export function TopicDetailsTabs(props: TopicDetailsTabsProps) { + const { activeTab, setActiveTab } = props; + + return ( +
+ setActiveTab('content')} + /> + setActiveTab('ai')} + /> +
+ ); +} + +type TopicDetailsTabProps = { + isActive: boolean; + icon: LucideIcon; + label: string; + onClick: () => void; +}; + +function TopicDetailsTab(props: TopicDetailsTabProps) { + const { isActive, icon: Icon, label, onClick } = props; + + return ( + + ); +}