Add chat with roadmap page

This commit is contained in:
Kamran Ahmed
2025-07-03 15:34:04 +01:00
parent 5b16390245
commit 7c9cd39002
3 changed files with 178 additions and 0 deletions
@@ -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',
@@ -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<HTMLInputElement>(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 (
<div className={cn('flex w-full flex-col', className)}>
<input
ref={searchInputRef}
type="text"
placeholder="Search roadmaps..."
value={search}
onChange={(e) => 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"
/>
<div className="flex flex-col gap-8">
{/* Role-based roadmaps */}
{roleRoadmaps.length > 0 && (
<div>
<h2 className="mb-4 text-xs tracking-wide text-gray-400 uppercase">
Role-based Roadmaps
</h2>
<div className="grid grid-cols-1 gap-1.5 sm:grid-cols-2 md:grid-cols-3">
{roleRoadmaps.map((roadmap) => (
<a
key={roadmap.id}
href={`/${roadmap.id}/ai`}
className="relative truncate rounded-md border bg-white p-2.5 text-left text-sm shadow-xs hover:border-gray-400 hover:bg-gray-50"
>
{roadmap.title}
</a>
))}
{roleRoadmaps.length === 0 && (
<div className="col-span-full py-4 text-center">
<p className="text-sm text-gray-400">
No role-based roadmaps found.
</p>
</div>
)}
</div>
</div>
)}
{/* Skill-based roadmaps */}
{skillRoadmaps.length > 0 && (
<div>
<h2 className="mb-4 text-xs tracking-wide text-gray-400 uppercase">
Skill-based Roadmaps
</h2>
<div className="grid grid-cols-1 gap-1.5 sm:grid-cols-2 md:grid-cols-3">
{skillRoadmaps.map((roadmap) => (
<a
key={roadmap.id}
href={`/${roadmap.id}/ai`}
className="relative truncate rounded-md border bg-white p-2.5 text-left text-sm shadow-xs hover:border-gray-400 hover:bg-gray-50"
>
{roadmap.title}
</a>
))}
{skillRoadmaps.length === 0 && (
<div className="col-span-full py-4 text-center">
<p className="text-sm text-gray-400">
No skill-based roadmaps found.
</p>
</div>
)}
</div>
</div>
)}
</div>
</div>
);
}
+54
View File
@@ -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';
---
<SkeletonLayout
title='Roadmap AI Chat'
noIndex={true}
ogImageUrl={ogImage}
description='Chat with AI Tutor on any roadmap. Pick a roadmap to start chatting.'
>
<AITutorLayout activeTab='roadmap-chat' client:load>
<div class='mx-auto flex w-full max-w-3xl flex-grow flex-col p-4'>
<div class='mb-6 text-left'>
<h1 class='mb-2 text-4xl font-semibold max-md:text-xl'>
Chat with a Roadmap
</h1>
<p class='text-base text-gray-600 max-md:text-sm'>
Select a roadmap and start learning with AI as a guide.
</p>
</div>
<RoadmapChatListing client:load roadmaps={roadmaps} />
</div>
</AITutorLayout>
</SkeletonLayout>