mirror of
https://github.com/kamranahmedse/developer-roadmap.git
synced 2026-09-24 15:00:31 +08:00
Add chat with roadmap page
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user