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:
8
pnpm-lock.yaml
generated
8
pnpm-lock.yaml
generated
@@ -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)
|
||||
|
@@ -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>
|
||||
|
@@ -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',
|
||||
|
79
src/components/RoadmapAIChat/RoadmapAIChat.tsx
Normal file
79
src/components/RoadmapAIChat/RoadmapAIChat.tsx
Normal 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>
|
||||
);
|
||||
}
|
27
src/pages/ai/chat/[roadmapId].astro
Normal file
27
src/pages/ai/chat/[roadmapId].astro
Normal 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
23
src/queries/roadmap.ts
Normal 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,
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
Reference in New Issue
Block a user