diff --git a/src/components/AITutor/AITutorSidebar.tsx b/src/components/AITutor/AITutorSidebar.tsx index 0bb5b801a..5be788fe8 100644 --- a/src/components/AITutor/AITutorSidebar.tsx +++ b/src/components/AITutor/AITutorSidebar.tsx @@ -1,6 +1,7 @@ import { BookOpen, Compass, + Map, MessageCircle, Plus, Star, @@ -39,6 +40,12 @@ const sidebarItems = [ href: '/ai/chat', icon: MessageCircle, }, + { + key: 'roadmap-chat', + label: 'Roadmap Chat', + href: '/ai/roadmap-chat', + icon: Map, + }, { key: 'library', label: 'My Learning', diff --git a/src/components/RoadmapAIChat/RoadmapChatListing.tsx b/src/components/RoadmapAIChat/RoadmapChatListing.tsx new file mode 100644 index 000000000..9f39051ad --- /dev/null +++ b/src/components/RoadmapAIChat/RoadmapChatListing.tsx @@ -0,0 +1,117 @@ +import { useState, useMemo, useEffect, useRef } from 'react'; +import { cn } from '../../lib/classname'; + +export type RoadmapChatListingItem = { + id: string; + title: string; + description?: string; + category?: 'role' | 'skill' | string; // default to string for flexibility +}; + +type RoadmapChatListingProps = { + roadmaps: RoadmapChatListingItem[]; + className?: string; +}; + +export default function RoadmapChatListing(props: RoadmapChatListingProps) { + const { roadmaps, className } = props; + + const [search, setSearch] = useState(''); + const searchInputRef = useRef(null); + + // Auto-focus the search input + useEffect(() => { + if (searchInputRef.current) { + searchInputRef.current.focus(); + } + }, []); + + const filteredRoadmaps = useMemo(() => { + const term = search.trim().toLowerCase(); + if (!term) { + return roadmaps; + } + + return roadmaps.filter((roadmap) => { + return ( + roadmap.title.toLowerCase().includes(term) || + roadmap.id.toLowerCase().includes(term) + ); + }); + }, [search, roadmaps]); + + // Separate into role-based and skill-based + const roleRoadmaps = filteredRoadmaps.filter((rm) => rm.category === 'role'); + const skillRoadmaps = filteredRoadmaps.filter( + (rm) => rm.category === 'skill', + ); + + return ( +
+ setSearch(e.target.value)} + className="mb-6 w-full rounded-lg border border-gray-300 bg-white px-4 py-2.5 text-sm placeholder-gray-400 focus:border-gray-300 focus:outline-none" + /> + +
+ {/* Role-based roadmaps */} + {roleRoadmaps.length > 0 && ( +
+

+ Role-based Roadmaps +

+
+ {roleRoadmaps.map((roadmap) => ( + + {roadmap.title} + + ))} + {roleRoadmaps.length === 0 && ( +
+

+ No role-based roadmaps found. +

+
+ )} +
+
+ )} + + {/* Skill-based roadmaps */} + {skillRoadmaps.length > 0 && ( +
+

+ Skill-based Roadmaps +

+
+ {skillRoadmaps.map((roadmap) => ( + + {roadmap.title} + + ))} + {skillRoadmaps.length === 0 && ( +
+

+ No skill-based roadmaps found. +

+
+ )} +
+
+ )} +
+
+ ); +} diff --git a/src/pages/ai/roadmap-chat.astro b/src/pages/ai/roadmap-chat.astro new file mode 100644 index 000000000..d5bca4ed3 --- /dev/null +++ b/src/pages/ai/roadmap-chat.astro @@ -0,0 +1,54 @@ +--- +import SkeletonLayout from '../../layouts/SkeletonLayout.astro'; +import { AITutorLayout } from '../../components/AITutor/AITutorLayout'; +import RoadmapChatListing from '../../components/RoadmapAIChat/RoadmapChatListing'; +import { + getRoadmapIds, + getRoadmapById, + resourceTitleFromId, +} from '../../lib/roadmap'; + +const ids = await getRoadmapIds(); +const roadmapsRaw = await Promise.all(ids.map((id) => getRoadmapById(id))); + +const roadmaps = roadmapsRaw.map((rm) => { + const tags = rm.frontmatter.tags || []; + const category = tags.includes('role-roadmap') + ? 'role' + : tags.includes('skill-roadmap') + ? 'skill' + : 'other'; + + return { + id: rm.id, + title: rm.frontmatter.title || resourceTitleFromId(rm.id), + description: + rm.frontmatter.briefDescription || rm.frontmatter.description || '', + category, + }; +}); + +const ogImage = 'https://roadmap.sh/og-images/ai-tutor.png'; +--- + + + +
+
+

+ Chat with a Roadmap +

+

+ Select a roadmap and start learning with AI as a guide. +

+
+ +
+
+
+