From 697d637db8b10de6f1c6da3839aa6fb7cfe375bc Mon Sep 17 00:00:00 2001 From: Arik Chakma Date: Sat, 17 May 2025 02:30:15 +0600 Subject: [PATCH] wip: roadmap chat --- pnpm-lock.yaml | 8 -- src/components/AITutor/AITutorLayout.tsx | 15 +++- src/components/AITutor/AITutorSidebar.tsx | 16 +++- .../RoadmapAIChat/RoadmapAIChat.tsx | 79 +++++++++++++++++++ src/pages/ai/chat/[roadmapId].astro | 27 +++++++ src/queries/roadmap.ts | 23 ++++++ 6 files changed, 155 insertions(+), 13 deletions(-) create mode 100644 src/components/RoadmapAIChat/RoadmapAIChat.tsx create mode 100644 src/pages/ai/chat/[roadmapId].astro create mode 100644 src/queries/roadmap.ts diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b117f313b..8ef01a5bd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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) diff --git a/src/components/AITutor/AITutorLayout.tsx b/src/components/AITutor/AITutorLayout.tsx index 068956cfe..3c1e0a127 100644 --- a/src/components/AITutor/AITutorLayout.tsx +++ b/src/components/AITutor/AITutorLayout.tsx @@ -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 ( <> -
+
@@ -27,13 +29,18 @@ export function AITutorLayout(props: AITutorLayoutProps) {
-
+
setIsSidebarFloating(false)} isFloating={isSidebarFloating} activeTab={activeTab} /> -
+
{children}
diff --git a/src/components/AITutor/AITutorSidebar.tsx b/src/components/AITutor/AITutorSidebar.tsx index 471d75849..b77e7e2b5 100644 --- a/src/components/AITutor/AITutorSidebar.tsx +++ b/src/components/AITutor/AITutorSidebar.tsx @@ -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', diff --git a/src/components/RoadmapAIChat/RoadmapAIChat.tsx b/src/components/RoadmapAIChat/RoadmapAIChat.tsx new file mode 100644 index 000000000..143dec7e4 --- /dev/null +++ b/src/components/RoadmapAIChat/RoadmapAIChat.tsx @@ -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(null); + + useEffect(() => { + if (!data || !roadmapContainerRef.current) { + return; + } + + setIsLoading(false); + roadmapContainerRef.current.replaceChildren(data.svg); + }, [data]); + + return ( +
+
+ {isLoading && ( +
+ +
+ )} +
+
+ +
+
+ + + AI Chat + +
+ +
+
+
+
+
+ +
{ + e.preventDefault(); + }} + > + + + +
+
+ ); +} diff --git a/src/pages/ai/chat/[roadmapId].astro b/src/pages/ai/chat/[roadmapId].astro new file mode 100644 index 000000000..e46a24faf --- /dev/null +++ b/src/pages/ai/chat/[roadmapId].astro @@ -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; +--- + + + + + + + diff --git a/src/queries/roadmap.ts b/src/queries/roadmap.ts new file mode 100644 index 000000000..642e206d7 --- /dev/null +++ b/src/queries/roadmap.ts @@ -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, + }; + }, + }); +}