diff --git a/.astro/settings.json b/.astro/settings.json
index 867818ff4..4d5033b8e 100644
--- a/.astro/settings.json
+++ b/.astro/settings.json
@@ -3,6 +3,6 @@
"enabled": false
},
"_variables": {
- "lastUpdateCheck": 1747060270496
+ "lastUpdateCheck": 1748277554631
}
}
\ No newline at end of file
diff --git a/.astro/types.d.ts b/.astro/types.d.ts
index 03d7cc43f..f964fe0cf 100644
--- a/.astro/types.d.ts
+++ b/.astro/types.d.ts
@@ -1,2 +1 @@
///
-///
\ No newline at end of file
diff --git a/package.json b/package.json
index 94b23d214..6840c25fd 100644
--- a/package.json
+++ b/package.json
@@ -42,6 +42,14 @@
"@roadmapsh/editor": "workspace:*",
"@tailwindcss/vite": "^4.1.7",
"@tanstack/react-query": "^5.76.1",
+ "@tiptap/core": "^2.12.0",
+ "@tiptap/extension-document": "^2.12.0",
+ "@tiptap/extension-paragraph": "^2.12.0",
+ "@tiptap/extension-placeholder": "^2.12.0",
+ "@tiptap/extension-text": "^2.12.0",
+ "@tiptap/pm": "^2.12.0",
+ "@tiptap/react": "^2.12.0",
+ "@tiptap/suggestion": "^2.12.0",
"@types/react": "^19.1.4",
"@types/react-dom": "^19.1.5",
"astro": "^5.7.13",
@@ -80,6 +88,7 @@
"shiki": "^3.4.2",
"slugify": "^1.6.6",
"tailwind-merge": "^3.3.0",
+ "tippy.js": "^6.3.7",
"tailwindcss": "^4.1.7",
"tiptap-markdown": "^0.8.10",
"turndown": "^7.2.0",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 98dcdc64b..aa6421233 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -41,6 +41,30 @@ importers:
'@tanstack/react-query':
specifier: ^5.76.1
version: 5.76.1(react@19.1.0)
+ '@tiptap/core':
+ specifier: ^2.12.0
+ version: 2.12.0(@tiptap/pm@2.12.0)
+ '@tiptap/extension-document':
+ specifier: ^2.12.0
+ version: 2.12.0(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))
+ '@tiptap/extension-paragraph':
+ specifier: ^2.12.0
+ version: 2.12.0(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))
+ '@tiptap/extension-placeholder':
+ specifier: ^2.12.0
+ version: 2.12.0(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))(@tiptap/pm@2.12.0)
+ '@tiptap/extension-text':
+ specifier: ^2.12.0
+ version: 2.12.0(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))
+ '@tiptap/pm':
+ specifier: ^2.12.0
+ version: 2.12.0
+ '@tiptap/react':
+ specifier: ^2.12.0
+ version: 2.12.0(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))(@tiptap/pm@2.12.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ '@tiptap/suggestion':
+ specifier: ^2.12.0
+ version: 2.12.0(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))(@tiptap/pm@2.12.0)
'@types/react':
specifier: ^19.1.4
version: 19.1.4
@@ -158,6 +182,9 @@ importers:
tailwindcss:
specifier: ^4.1.7
version: 4.1.7
+ tippy.js:
+ specifier: ^6.3.7
+ version: 6.3.7
tiptap-markdown:
specifier: ^0.8.10
version: 0.8.10(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))
@@ -983,6 +1010,9 @@ packages:
engines: {node: '>=18'}
hasBin: true
+ '@popperjs/core@2.11.8':
+ resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==}
+
'@remirror/core-constants@3.0.0':
resolution: {integrity: sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==}
@@ -1396,9 +1426,56 @@ packages:
peerDependencies:
'@tiptap/pm': ^2.7.0
+ '@tiptap/extension-bubble-menu@2.12.0':
+ resolution: {integrity: sha512-DYijoE0igV0Oi+ZppFsp2UrQsM/4HZtmmpD78BJM9zfCbd1YvAUIxmzmXr8uqU18OHd1uQy+/zvuNoUNYyw67g==}
+ peerDependencies:
+ '@tiptap/core': ^2.7.0
+ '@tiptap/pm': ^2.7.0
+
+ '@tiptap/extension-document@2.12.0':
+ resolution: {integrity: sha512-sA1Q+mxDIv0Y3qQTBkYGwknNbDcGFiJ/fyAFholXpqbrcRx3GavwR/o0chBdsJZlFht0x7AWGwUYWvIo7wYilA==}
+ peerDependencies:
+ '@tiptap/core': ^2.7.0
+
+ '@tiptap/extension-floating-menu@2.12.0':
+ resolution: {integrity: sha512-BYpyZx/56KCDksWuJJbhki/uNgt9sACuSSZFH5AN1yS1ISD+EzIxqf6Pzzv8QCoNJ+KcRNVaZsOlOFaJGoyzag==}
+ peerDependencies:
+ '@tiptap/core': ^2.7.0
+ '@tiptap/pm': ^2.7.0
+
+ '@tiptap/extension-paragraph@2.12.0':
+ resolution: {integrity: sha512-QNK5cgewCunWFxpLlbvvoO1rrLgEtNKxiY79fctP9toV+e59R+1i1Q9lXC1O5mOfDgVxCb6uFDMsqmKhFjpPog==}
+ peerDependencies:
+ '@tiptap/core': ^2.7.0
+
+ '@tiptap/extension-placeholder@2.12.0':
+ resolution: {integrity: sha512-K7irDox4P+NLAMjVrJeG72f0sulsCRYpx1Cy4gxKCdi1LTivj5VkXa6MXmi42KTCwBu3pWajBctYIOAES1FTAA==}
+ peerDependencies:
+ '@tiptap/core': ^2.7.0
+ '@tiptap/pm': ^2.7.0
+
+ '@tiptap/extension-text@2.12.0':
+ resolution: {integrity: sha512-0ytN9V1tZYTXdiYDQg4FB2SQ56JAJC9r/65snefb9ztl+gZzDrIvih7CflHs1ic9PgyjexfMLeH+VzuMccNyZw==}
+ peerDependencies:
+ '@tiptap/core': ^2.7.0
+
'@tiptap/pm@2.12.0':
resolution: {integrity: sha512-TNzVwpeNzFfHAcYTOKqX9iU4fRxliyoZrCnERR+RRzeg7gWrXrCLubQt1WEx0sojMAfznshSL3M5HGsYjEbYwA==}
+ '@tiptap/react@2.12.0':
+ resolution: {integrity: sha512-D+PR+4kJO9h8AB/7XyQ/Anw8tqeS2ecv5QemBOCHi9JlMAjytauUrj6IfFBO9RbsCowlBjW5GnSpFhzpk2Gghg==}
+ peerDependencies:
+ '@tiptap/core': ^2.7.0
+ '@tiptap/pm': ^2.7.0
+ react: ^17.0.0 || ^18.0.0 || ^19.0.0
+ react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0
+
+ '@tiptap/suggestion@2.12.0':
+ resolution: {integrity: sha512-bsXLoZbjUo1oOF1Z+XSfoGzbcnrTcYtJdfylM/FerMLU9T12dhsM/Ri2SKLX4IR5D0HJ07FcsEHCrGEy8Y5y0A==}
+ peerDependencies:
+ '@tiptap/core': ^2.7.0
+ '@tiptap/pm': ^2.7.0
+
'@tybys/wasm-util@0.9.0':
resolution: {integrity: sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw==}
@@ -1527,6 +1604,9 @@ packages:
'@types/unist@3.0.3':
resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==}
+ '@types/use-sync-external-store@0.0.6':
+ resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==}
+
'@ungap/structured-clone@1.3.0':
resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==}
@@ -2859,6 +2939,7 @@ packages:
node-domexception@1.0.0:
resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
engines: {node: '>=10.5.0'}
+ deprecated: Use your platform's native DOMException instead
node-fetch-native@1.6.6:
resolution: {integrity: sha512-8Mc2HhqPdlIfedsuZoc3yioPuzp6b+L5jRCRY1QzuWZh2EGJVQrGppC6V6cF0bLdbW0+O2YpqCA25aF/1lvipQ==}
@@ -3546,6 +3627,9 @@ packages:
resolution: {integrity: sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==}
engines: {node: '>=12.0.0'}
+ tippy.js@6.3.7:
+ resolution: {integrity: sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==}
+
tiptap-markdown@0.8.10:
resolution: {integrity: sha512-iDVkR2BjAqkTDtFX0h94yVvE2AihCXlF0Q7RIXSJPRSR5I0PA1TMuAg6FHFpmqTn4tPxJ0by0CK7PUMlnFLGEQ==}
peerDependencies:
@@ -4606,6 +4690,8 @@ snapshots:
dependencies:
playwright: 1.52.0
+ '@popperjs/core@2.11.8': {}
+
'@remirror/core-constants@3.0.0': {}
'@resvg/resvg-js-android-arm-eabi@2.6.2':
@@ -4925,6 +5011,35 @@ snapshots:
dependencies:
'@tiptap/pm': 2.12.0
+ '@tiptap/extension-bubble-menu@2.12.0(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))(@tiptap/pm@2.12.0)':
+ dependencies:
+ '@tiptap/core': 2.12.0(@tiptap/pm@2.12.0)
+ '@tiptap/pm': 2.12.0
+ tippy.js: 6.3.7
+
+ '@tiptap/extension-document@2.12.0(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))':
+ dependencies:
+ '@tiptap/core': 2.12.0(@tiptap/pm@2.12.0)
+
+ '@tiptap/extension-floating-menu@2.12.0(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))(@tiptap/pm@2.12.0)':
+ dependencies:
+ '@tiptap/core': 2.12.0(@tiptap/pm@2.12.0)
+ '@tiptap/pm': 2.12.0
+ tippy.js: 6.3.7
+
+ '@tiptap/extension-paragraph@2.12.0(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))':
+ dependencies:
+ '@tiptap/core': 2.12.0(@tiptap/pm@2.12.0)
+
+ '@tiptap/extension-placeholder@2.12.0(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))(@tiptap/pm@2.12.0)':
+ dependencies:
+ '@tiptap/core': 2.12.0(@tiptap/pm@2.12.0)
+ '@tiptap/pm': 2.12.0
+
+ '@tiptap/extension-text@2.12.0(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))':
+ dependencies:
+ '@tiptap/core': 2.12.0(@tiptap/pm@2.12.0)
+
'@tiptap/pm@2.12.0':
dependencies:
prosemirror-changeset: 2.3.0
@@ -4946,6 +5061,23 @@ snapshots:
prosemirror-transform: 1.10.4
prosemirror-view: 1.39.2
+ '@tiptap/react@2.12.0(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))(@tiptap/pm@2.12.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
+ dependencies:
+ '@tiptap/core': 2.12.0(@tiptap/pm@2.12.0)
+ '@tiptap/extension-bubble-menu': 2.12.0(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))(@tiptap/pm@2.12.0)
+ '@tiptap/extension-floating-menu': 2.12.0(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))(@tiptap/pm@2.12.0)
+ '@tiptap/pm': 2.12.0
+ '@types/use-sync-external-store': 0.0.6
+ fast-deep-equal: 3.1.3
+ react: 19.1.0
+ react-dom: 19.1.0(react@19.1.0)
+ use-sync-external-store: 1.5.0(react@19.1.0)
+
+ '@tiptap/suggestion@2.12.0(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))(@tiptap/pm@2.12.0)':
+ dependencies:
+ '@tiptap/core': 2.12.0(@tiptap/pm@2.12.0)
+ '@tiptap/pm': 2.12.0
+
'@tybys/wasm-util@0.9.0':
dependencies:
tslib: 2.8.1
@@ -5092,6 +5224,8 @@ snapshots:
'@types/unist@3.0.3': {}
+ '@types/use-sync-external-store@0.0.6': {}
+
'@ungap/structured-clone@1.3.0': {}
'@vitejs/plugin-react@4.4.1(vite@6.3.5(@types/node@22.15.17)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.4))':
@@ -7409,6 +7543,10 @@ snapshots:
fdir: 6.4.4(picomatch@4.0.2)
picomatch: 4.0.2
+ tippy.js@6.3.7:
+ dependencies:
+ '@popperjs/core': 2.11.8
+
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/public/images/gifs/bot.gif b/public/images/gifs/bot.gif
new file mode 100644
index 000000000..b2a7b834f
Binary files /dev/null and b/public/images/gifs/bot.gif differ
diff --git a/public/images/gifs/wave.gif b/public/images/gifs/wave.gif
new file mode 100644
index 000000000..4066643d6
Binary files /dev/null and b/public/images/gifs/wave.gif differ
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..bd9bc41c2 100644
--- a/src/components/AITutor/AITutorSidebar.tsx
+++ b/src/components/AITutor/AITutorSidebar.tsx
@@ -1,9 +1,15 @@
+import {
+ BookOpen, Compass,
+ Plus,
+ Star,
+ X,
+ Zap
+} from 'lucide-react';
import { useEffect, useState } from 'react';
-import { BookOpen, Compass, Plus, Star, X, Zap } from 'lucide-react';
-import { AITutorLogo } from '../ReactIcons/AITutorLogo';
-import { UpgradeAccountModal } from '../Billing/UpgradeAccountModal';
-import { useIsPaidUser } from '../../queries/billing';
import { isLoggedIn } from '../../lib/jwt';
+import { useIsPaidUser } from '../../queries/billing';
+import { UpgradeAccountModal } from '../Billing/UpgradeAccountModal';
+import { AITutorLogo } from '../ReactIcons/AITutorLogo';
type AITutorSidebarProps = {
isFloating: boolean;
@@ -24,6 +30,12 @@ const sidebarItems = [
href: '/ai/courses',
icon: BookOpen,
},
+ // {
+ // key: 'chat',
+ // label: 'AI Chat',
+ // href: '/ai/chat',
+ // icon: Bot,
+ // },
{
key: 'staff-picks',
label: 'Staff Picks',
diff --git a/src/components/AuthenticationFlow/EmailLoginForm.tsx b/src/components/AuthenticationFlow/EmailLoginForm.tsx
index 8e7e2c0a3..75406960a 100644
--- a/src/components/AuthenticationFlow/EmailLoginForm.tsx
+++ b/src/components/AuthenticationFlow/EmailLoginForm.tsx
@@ -2,8 +2,9 @@ import type { FormEvent } from 'react';
import { useId, useState } from 'react';
import { httpPost } from '../../lib/http';
import {
- COURSE_PURCHASE_PARAM, FIRST_LOGIN_PARAM,
- setAuthToken
+ COURSE_PURCHASE_PARAM,
+ FIRST_LOGIN_PARAM,
+ setAuthToken,
} from '../../lib/jwt';
type EmailLoginFormProps = {
@@ -65,7 +66,11 @@ export function EmailLoginForm(props: EmailLoginFormProps) {
const passwordFieldId = `form:${useId()}`;
return (
-