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}
/>
-
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
+
+
+
+
+
+
+
+
+ );
+}
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,
+ };
+ },
+ });
+}