1
0
mirror of https://github.com/kamranahmedse/developer-roadmap.git synced 2025-09-08 00:00:42 +02:00

wip: roadmap chat

This commit is contained in:
Arik Chakma
2025-05-17 02:30:15 +06:00
parent 6e1e334406
commit 697d637db8
6 changed files with 155 additions and 13 deletions

8
pnpm-lock.yaml generated
View File

@@ -261,9 +261,6 @@ importers:
tailwind-merge:
specifier: ^3.2.0
version: 3.2.0
tinykeys:
specifier: ^3.0.0
version: 3.0.0
unified:
specifier: ^11.0.5
version: 11.0.5
@@ -3485,9 +3482,6 @@ packages:
resolution: {integrity: sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==}
engines: {node: '>=12.0.0'}
tinykeys@3.0.0:
resolution: {integrity: sha512-nazawuGv5zx6MuDfDY0rmfXjuOGhD5XU2z0GLURQ1nzl0RUe9OuCJq+0u8xxJZINHe+mr7nw8PWYYZ9WhMFujw==}
tiptap-markdown@0.8.10:
resolution: {integrity: sha512-iDVkR2BjAqkTDtFX0h94yVvE2AihCXlF0Q7RIXSJPRSR5I0PA1TMuAg6FHFpmqTn4tPxJ0by0CK7PUMlnFLGEQ==}
peerDependencies:
@@ -7306,8 +7300,6 @@ snapshots:
fdir: 6.4.4(picomatch@4.0.2)
picomatch: 4.0.2
tinykeys@3.0.0: {}
tiptap-markdown@0.8.10(@tiptap/core@2.12.0(@tiptap/pm@2.12.0)):
dependencies:
'@tiptap/core': 2.12.0(@tiptap/pm@2.12.0)

View File

@@ -2,20 +2,22 @@ import { Menu } from 'lucide-react';
import { useState } from 'react';
import { AITutorSidebar, type AITutorTab } from './AITutorSidebar';
import { RoadmapLogoIcon } from '../ReactIcons/RoadmapLogo';
import { cn } from '../../lib/classname';
type AITutorLayoutProps = {
children: React.ReactNode;
activeTab: AITutorTab;
wrapperClassName?: string;
};
export function AITutorLayout(props: AITutorLayoutProps) {
const { children, activeTab } = props;
const { children, activeTab, wrapperClassName } = props;
const [isSidebarFloating, setIsSidebarFloating] = useState(false);
return (
<>
<div className="flex flex-row items-center justify-between border-b border-slate-200 px-4 py-3 lg:hidden sticky top-0 bg-white z-10">
<div className="sticky top-0 z-10 flex flex-row items-center justify-between border-b border-slate-200 bg-white px-4 py-3 lg:hidden">
<a href="/" className="flex flex-row items-center gap-1.5">
<RoadmapLogoIcon className="size-6 text-gray-500" color="black" />
</a>
@@ -27,13 +29,18 @@ export function AITutorLayout(props: AITutorLayoutProps) {
</button>
</div>
<div className="flex flex-grow lg:h-screen flex-row">
<div className="flex flex-grow flex-row lg:h-screen">
<AITutorSidebar
onClose={() => setIsSidebarFloating(false)}
isFloating={isSidebarFloating}
activeTab={activeTab}
/>
<div className="flex flex-grow flex-col overflow-y-scroll bg-gray-100 p-3 lg:px-4 lg:py-4">
<div
className={cn(
'flex flex-grow flex-col overflow-y-scroll bg-gray-100 p-3 lg:p-4',
wrapperClassName,
)}
>
{children}
</div>
</div>

View File

@@ -1,5 +1,13 @@
import { useEffect, useState } from 'react';
import { BookOpen, Compass, Plus, Star, X, Zap } from 'lucide-react';
import {
BookOpen,
BotMessageSquareIcon,
Compass,
Plus,
Star,
X,
Zap,
} from 'lucide-react';
import { AITutorLogo } from '../ReactIcons/AITutorLogo';
import { UpgradeAccountModal } from '../Billing/UpgradeAccountModal';
import { useIsPaidUser } from '../../queries/billing';
@@ -24,6 +32,12 @@ const sidebarItems = [
href: '/ai/courses',
icon: BookOpen,
},
{
key: 'chat',
label: 'AI Chat',
href: '/ai/chat',
icon: BotMessageSquareIcon,
},
{
key: 'staff-picks',
label: 'Staff Picks',

View File

@@ -0,0 +1,79 @@
import { useQuery } from '@tanstack/react-query';
import { roadmapJSONOptions } from '../../queries/roadmap';
import { queryClient } from '../../stores/query-client';
import { useEffect, useRef, useState } from 'react';
import { Spinner } from '../ReactIcons/Spinner';
import { BotIcon, SendIcon } from 'lucide-react';
import TextareaAutosize from 'react-textarea-autosize';
import { cn } from '../../lib/classname';
type RoadmapAIChatProps = {
roadmapId: string;
};
export function RoadmapAIChat(props: RoadmapAIChatProps) {
const { roadmapId } = props;
const [isLoading, setIsLoading] = useState(true);
const { data } = useQuery(roadmapJSONOptions(roadmapId), queryClient);
const roadmapContainerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!data || !roadmapContainerRef.current) {
return;
}
setIsLoading(false);
roadmapContainerRef.current.replaceChildren(data.svg);
}, [data]);
return (
<div className="grid grow grid-cols-2">
<div className="h-full overflow-y-auto">
{isLoading && (
<div className="flex h-full w-full items-center justify-center">
<Spinner
className="h-6 w-6 animate-spin sm:h-12 sm:w-12"
isDualRing={false}
/>
</div>
)}
<div ref={roadmapContainerRef} />
</div>
<div className="flex h-full flex-col">
<div className="flex min-h-[46px] items-center justify-between gap-2 border-gray-200 px-3 py-2 text-sm">
<span className="flex items-center gap-2 text-sm">
<BotIcon className="size-4 shrink-0 text-black" />
<span>AI Chat</span>
</span>
</div>
<div className="relative grow overflow-y-auto">
<div className="absolute inset-0 flex flex-col">
<div className="h-[1000px] w-full bg-red-100" />
</div>
</div>
<form
className="relative flex items-start border-t border-gray-200 text-sm"
onSubmit={(e) => {
e.preventDefault();
}}
>
<TextareaAutosize
className="h-full min-h-[41px] grow resize-none bg-transparent px-4 py-2 focus:outline-hidden"
placeholder="Ask AI anything about the roadmap..."
autoFocus={true}
/>
<button
type="submit"
className="flex aspect-square size-[41px] items-center justify-center text-zinc-500 hover:text-black disabled:cursor-not-allowed disabled:opacity-50"
>
<SendIcon className="size-4 stroke-[2.5]" />
</button>
</form>
</div>
</div>
);
}

View File

@@ -0,0 +1,27 @@
---
import { CheckSubscriptionVerification } from '../../../components/Billing/CheckSubscriptionVerification';
import { RoadmapAIChat } from '../../../components/RoadmapAIChat/RoadmapAIChat';
import SkeletonLayout from '../../../layouts/SkeletonLayout.astro';
import { AITutorLayout } from '../../../components/AITutor/AITutorLayout';
type Props = {
roadmapId: string;
};
const { roadmapId } = Astro.params as Props;
---
<SkeletonLayout
title='Roadmap AI Chat'
noIndex={true}
description='Learn anything with AI Tutor. Pick a topic, choose a difficulty level and the AI will guide you through the learning process.'
>
<AITutorLayout
activeTab='chat'
wrapperClassName='flex-row p-0 lg:p-0 overflow-hidden'
client:load
>
<RoadmapAIChat roadmapId={roadmapId} client:load />
<CheckSubscriptionVerification client:load />
</AITutorLayout>
</SkeletonLayout>

23
src/queries/roadmap.ts Normal file
View File

@@ -0,0 +1,23 @@
import { queryOptions } from '@tanstack/react-query';
import { httpGet } from '../lib/query-http';
import { type Node, type Edge, renderFlowJSON } from '@roadmapsh/editor';
export function roadmapJSONOptions(roadmapId: string) {
return queryOptions({
queryKey: ['roadmap-json', roadmapId],
queryFn: async () => {
const baseUrl = import.meta.env.PUBLIC_APP_URL;
const roadmapJSON = await httpGet<{
nodes: Node[];
edges: Edge[];
}>(`${baseUrl}/${roadmapId}.json`);
const svg = await renderFlowJSON(roadmapJSON);
return {
json: roadmapJSON,
svg,
};
},
});
}