diff --git a/.astro/types.d.ts b/.astro/types.d.ts index f964fe0cf..03d7cc43f 100644 --- a/.astro/types.d.ts +++ b/.astro/types.d.ts @@ -1 +1,2 @@ /// +/// \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index d3b317b51..6827afd40 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,5 +2,13 @@ "prettier.documentSelectors": ["**/*.astro"], "[astro]": { "editor.defaultFormatter": "esbenp.prettier-vscode" - } + }, + "tailwindCSS.experimental.classRegex": [ + ["\\b\\w+[cC]lassName\\s*=\\s*[\"']([^\"']*)[\"']"], + ["\\b\\w+[cC]lassName\\s*=\\s*`([^`]*)`"], + ["[\\w]+[cC]lassName[\"']?\\s*:\\s*[\"']([^\"']*)[\"']"], + ["[\\w]+[cC]lassName[\"']?\\s*:\\s*`([^`]*)`"], + ["cva\\(((?:[^()]|\\([^()]*\\))*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"], + ["cx\\(((?:[^()]|\\([^()]*\\))*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)"] + ] } diff --git a/package.json b/package.json index 7fc07c199..bd177097e 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "@nanostores/react": "^1.0.0", "@napi-rs/image": "^1.9.2", "@radix-ui/react-dropdown-menu": "^2.1.15", + "@radix-ui/react-popover": "^1.1.14", "@resvg/resvg-js": "^2.6.2", "@roadmapsh/editor": "workspace:*", "@tailwindcss/vite": "^4.1.7", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 969483f4c..b80184638 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -32,6 +32,9 @@ importers: '@radix-ui/react-dropdown-menu': specifier: ^2.1.15 version: 2.1.15(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-popover': + specifier: ^1.1.14 + version: 1.1.14(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@resvg/resvg-js': specifier: ^2.6.2 version: 2.6.2 @@ -1154,6 +1157,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-popover@1.1.14': + resolution: {integrity: sha512-ODz16+1iIbGUfFEfKx2HTPKizg2MN39uIOV8MXeHnmdd3i/N9Wt7vU46wbHsqA0xoaQyXVcs0KIlBdOA2Y95bw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-popper@1.2.7': resolution: {integrity: sha512-IUFAccz1JyKcf/RjB552PlWwxjeCJB8/4KxT7EhBHOJM+mN7LdW+B3kacJXILm32xawcMMjb2i0cIZpo+f9kiQ==} peerDependencies: @@ -5178,6 +5194,29 @@ snapshots: '@types/react': 19.1.4 '@types/react-dom': 19.1.5(@types/react@19.1.4) + '@radix-ui/react-popover@1.1.14(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.4)(react@19.1.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.4)(react@19.1.0) + '@radix-ui/react-dismissable-layer': 1.1.10(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-focus-guards': 1.1.2(@types/react@19.1.4)(react@19.1.0) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-id': 1.1.1(@types/react@19.1.4)(react@19.1.0) + '@radix-ui/react-popper': 1.2.7(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-presence': 1.1.4(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-slot': 1.2.3(@types/react@19.1.4)(react@19.1.0) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.4)(react@19.1.0) + aria-hidden: 1.2.6 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + react-remove-scroll: 2.7.1(@types/react@19.1.4)(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.4 + '@types/react-dom': 19.1.5(@types/react@19.1.4) + '@radix-ui/react-popper@1.2.7(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: '@floating-ui/react-dom': 2.1.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0) diff --git a/src/components/AIChat/AIChat.tsx b/src/components/AIChat/AIChat.tsx index 9a6540938..bb06812a5 100644 --- a/src/components/AIChat/AIChat.tsx +++ b/src/components/AIChat/AIChat.tsx @@ -31,13 +31,13 @@ import { type MessagePartRenderer, } from '../../lib/render-chat-message'; import { RoadmapRecommendations } from '../RoadmapAIChat/RoadmapRecommendations'; -import type { RoadmapAIChatHistoryType } from '../RoadmapAIChat/RoadmapAIChat'; import { AIChatCourse } from './AIChatCouse'; import { showLoginPopup } from '../../lib/popup'; import { UpgradeAccountModal } from '../Billing/UpgradeAccountModal'; import { readChatStream } from '../../lib/chat'; import { chatHistoryOptions } from '../../queries/chat-history'; import { cn } from '../../lib/classname'; +import type { RoadmapAIChatHistoryType } from '../../hooks/use-roadmap-ai-chat'; export const aiChatRenderer: Record = { 'roadmap-recommendations': (options) => { diff --git a/src/components/AIChat/ChatHistory.tsx b/src/components/AIChat/ChatHistory.tsx index e16222301..e770b437c 100644 --- a/src/components/AIChat/ChatHistory.tsx +++ b/src/components/AIChat/ChatHistory.tsx @@ -8,8 +8,8 @@ import { RotateCwIcon, } from 'lucide-react'; import { useCopyText } from '../../hooks/use-copy-text'; -import type { RoadmapAIChatHistoryType } from '../RoadmapAIChat/RoadmapAIChat'; import { Tooltip } from '../Tooltip'; +import type { RoadmapAIChatHistoryType } from '../../hooks/use-roadmap-ai-chat'; type ChatHistoryProps = { chatHistory: RoadmapAIChatHistoryType[]; diff --git a/src/components/AIChatHistory/AIChatHistory.tsx b/src/components/AIChatHistory/AIChatHistory.tsx index 12982c9f2..b68866426 100644 --- a/src/components/AIChatHistory/AIChatHistory.tsx +++ b/src/components/AIChatHistory/AIChatHistory.tsx @@ -1,7 +1,7 @@ import { useQuery } from '@tanstack/react-query'; import { queryClient } from '../../stores/query-client'; import { chatHistoryOptions } from '../../queries/chat-history'; -import { AIChat } from '../AIChat/AIChat'; +import { AIChat, aiChatRenderer } from '../AIChat/AIChat'; import { Loader2Icon } from 'lucide-react'; import { useEffect, useState } from 'react'; import { AIChatLayout } from './AIChatLayout'; @@ -24,7 +24,7 @@ export function AIChatHistory(props: AIChatHistoryProps) { ); const { data, error: chatHistoryError } = useQuery( - chatHistoryOptions(chatHistoryId), + chatHistoryOptions(chatHistoryId, aiChatRenderer), queryClient, ); const { diff --git a/src/components/AIChatHistory/ChatHistoryGroup.tsx b/src/components/AIChatHistory/ChatHistoryGroup.tsx new file mode 100644 index 000000000..e7faeab00 --- /dev/null +++ b/src/components/AIChatHistory/ChatHistoryGroup.tsx @@ -0,0 +1,42 @@ +import type { ChatHistoryWithoutMessages } from '../../queries/chat-history'; +import { ChatHistoryItem } from './ChatHistoryItem'; + +type ChatHistoryGroupProps = { + title: string; + histories: ChatHistoryWithoutMessages[]; + activeChatHistoryId?: string; + onChatHistoryClick: (id: string) => void; + onDelete: (id: string) => void; +}; + +export function ChatHistoryGroup(props: ChatHistoryGroupProps) { + const { + title, + histories, + activeChatHistoryId, + onChatHistoryClick, + onDelete, + } = props; + + return ( +
+

{title}

+ +
    + {histories.map((chatHistory) => { + return ( + { + onDelete?.(chatHistory._id); + }} + /> + ); + })} +
+
+ ); +} diff --git a/src/components/AIChatHistory/ListChatHistory.tsx b/src/components/AIChatHistory/ListChatHistory.tsx index 28bc91dad..6d8547070 100644 --- a/src/components/AIChatHistory/ListChatHistory.tsx +++ b/src/components/AIChatHistory/ListChatHistory.tsx @@ -1,8 +1,5 @@ import { useInfiniteQuery } from '@tanstack/react-query'; -import { - listChatHistoryOptions, - type ChatHistoryWithoutMessages, -} from '../../queries/chat-history'; +import { listChatHistoryOptions } from '../../queries/chat-history'; import { queryClient } from '../../stores/query-client'; import { ChatHistoryItem } from './ChatHistoryItem'; import { @@ -13,13 +10,15 @@ import { SearchIcon, XIcon, } from 'lucide-react'; -import { DateTime } from 'luxon'; import { useEffect, useLayoutEffect, useMemo, useState } from 'react'; import { useDebounceValue } from '../../hooks/use-debounce'; import { ListChatHistorySkeleton } from './ListChatHistorySkeleton'; import { ChatHistoryError } from './ChatHistoryError'; import { cn } from '../../lib/classname'; import { getTailwindScreenDimension } from '../../lib/is-mobile'; +import { groupChatHistory } from '../../helper/grouping'; +import { SearchAIChatHistory } from './SearchAIChatHistory'; +import { ChatHistoryGroup } from './ChatHistoryGroup'; type ListChatHistoryProps = { activeChatHistoryId?: string; @@ -62,44 +61,8 @@ export function ListChatHistory(props: ListChatHistoryProps) { }, [data?.pages]); const groupedChatHistory = useMemo(() => { - const today = DateTime.now().startOf('day'); const allHistories = data?.pages?.flatMap((page) => page.data); - - return allHistories?.reduce( - (acc, chatHistory) => { - const updatedAt = DateTime.fromJSDate( - new Date(chatHistory.updatedAt), - ).startOf('day'); - const diffInDays = Math.abs(updatedAt.diff(today, 'days').days); - - if (diffInDays === 0) { - acc.today.histories.push(chatHistory); - } else if (diffInDays <= 7) { - acc.last7Days.histories.push(chatHistory); - } else { - acc.older.histories.push(chatHistory); - } - - return acc; - }, - { - today: { - title: 'Today', - histories: [], - }, - last7Days: { - title: 'Last 7 Days', - histories: [], - }, - older: { - title: 'Older', - histories: [], - }, - } as Record< - string, - { title: string; histories: ChatHistoryWithoutMessages[] } - >, - ); + return groupChatHistory(allHistories ?? []); }, [data?.pages]); if (!isOpen) { @@ -160,7 +123,7 @@ export function ListChatHistory(props: ListChatHistoryProps) { New Chat - @@ -179,31 +142,22 @@ export function ListChatHistory(props: ListChatHistoryProps) { } return ( -
-

{value.title}

+ { + if (isMobile) { + setIsOpen(false); + } -
    - {value.histories.map((chatHistory) => { - return ( - { - if (isMobile) { - setIsOpen(false); - } - - onChatHistoryClick(id); - }} - onDelete={() => { - onDelete?.(chatHistory._id); - }} - /> - ); - })} -
-
+ onChatHistoryClick(id); + }} + onDelete={(id) => { + onDelete?.(id); + }} + /> ); })} @@ -232,60 +186,3 @@ export function ListChatHistory(props: ListChatHistoryProps) { ); } - -type SearchInputProps = { - onSearch: (search: string) => void; - isLoading?: boolean; -}; - -function SearchInput(props: SearchInputProps) { - const { onSearch, isLoading } = props; - - const [search, setSearch] = useState(''); - const debouncedSearch = useDebounceValue(search, 300); - - useEffect(() => { - onSearch(debouncedSearch); - }, [debouncedSearch, onSearch]); - - return ( -
{ - e.preventDefault(); - onSearch(search); - }} - > - setSearch(e.target.value)} - /> - -
- {isLoading ? ( - - ) : ( - - )} -
- {search && ( -
- -
- )} -
- ); -} diff --git a/src/components/AIChatHistory/SearchAIChatHistory.tsx b/src/components/AIChatHistory/SearchAIChatHistory.tsx new file mode 100644 index 000000000..84dee4d9c --- /dev/null +++ b/src/components/AIChatHistory/SearchAIChatHistory.tsx @@ -0,0 +1,66 @@ +import { useEffect, useState } from 'react'; +import { useDebounceValue } from '../../hooks/use-debounce'; +import { Loader2Icon, XIcon, SearchIcon } from 'lucide-react'; +import { cn } from '../../lib/classname'; + +type SearchAIChatHistoryProps = { + onSearch: (search: string) => void; + isLoading?: boolean; + className?: string; + inputClassName?: string; +}; + +export function SearchAIChatHistory(props: SearchAIChatHistoryProps) { + const { onSearch, isLoading, className, inputClassName } = props; + + const [search, setSearch] = useState(''); + const debouncedSearch = useDebounceValue(search, 300); + + useEffect(() => { + onSearch(debouncedSearch); + }, [debouncedSearch, onSearch]); + + return ( +
{ + e.preventDefault(); + onSearch(search); + }} + > + setSearch(e.target.value)} + /> + +
+ {isLoading ? ( + + ) : ( + + )} +
+ {search && ( +
+ +
+ )} +
+ ); +} diff --git a/src/components/Popover.tsx b/src/components/Popover.tsx new file mode 100644 index 000000000..b82c046cd --- /dev/null +++ b/src/components/Popover.tsx @@ -0,0 +1,28 @@ +import * as PopoverPrimitive from '@radix-ui/react-popover'; +import * as React from 'react'; +import { cn } from '../lib/classname'; + +const Popover = PopoverPrimitive.Root; + +const PopoverTrigger = PopoverPrimitive.Trigger; + +const PopoverContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, align = 'center', sideOffset = 4, ...props }, ref) => ( + + + +)); +PopoverContent.displayName = PopoverPrimitive.Content.displayName; + +export { Popover, PopoverContent, PopoverTrigger }; diff --git a/src/components/RoadmapAIChat/RoadmapAIChat.tsx b/src/components/RoadmapAIChat/RoadmapAIChat.tsx index 0f026ed20..7a159b175 100644 --- a/src/components/RoadmapAIChat/RoadmapAIChat.tsx +++ b/src/components/RoadmapAIChat/RoadmapAIChat.tsx @@ -307,6 +307,7 @@ export function RoadmapAIChat(props: RoadmapAIChatProps) { setActiveTab('chat'); }} selectedTopicId={selectedTopicId} + roadmapId={roadmapId} /> {activeTab === 'topic' && selectedTopicId && ( diff --git a/src/components/RoadmapAIChat/RoadmapAIChatHeader.tsx b/src/components/RoadmapAIChat/RoadmapAIChatHeader.tsx index e9bcc65c3..726fd6eb8 100644 --- a/src/components/RoadmapAIChat/RoadmapAIChatHeader.tsx +++ b/src/components/RoadmapAIChat/RoadmapAIChatHeader.tsx @@ -10,6 +10,7 @@ import { getPercentage } from '../../lib/number'; import { AILimitsPopup } from '../GenerateCourse/AILimitsPopup'; import { cn } from '../../lib/classname'; import { useKeydown } from '../../hooks/use-keydown'; +import { RoadmapAIChatHistory } from '../RoadmapAIChatHistory/RoadmapAIChatHistory'; type RoadmapAIChatHeaderProps = { isLoading: boolean; @@ -23,6 +24,8 @@ type RoadmapAIChatHeaderProps = { onTabChange: (tab: RoadmapAIChatTab) => void; onCloseTopic: () => void; selectedTopicId: string | null; + + roadmapId: string; }; type TabButtonProps = { @@ -76,6 +79,7 @@ export function RoadmapAIChatHeader(props: RoadmapAIChatHeaderProps) { onTabChange, onCloseTopic, selectedTopicId, + roadmapId, } = props; const [showAILimitsPopup, setShowAILimitsPopup] = useState(false); @@ -170,6 +174,8 @@ export function RoadmapAIChatHeader(props: RoadmapAIChatHeaderProps) { )} + + )} diff --git a/src/components/RoadmapAIChatHistory/RoadmapAIChatHistory.tsx b/src/components/RoadmapAIChatHistory/RoadmapAIChatHistory.tsx new file mode 100644 index 000000000..8a36604db --- /dev/null +++ b/src/components/RoadmapAIChatHistory/RoadmapAIChatHistory.tsx @@ -0,0 +1,126 @@ +import { HistoryIcon, Loader2Icon, PlusIcon } from 'lucide-react'; +import { Popover, PopoverContent, PopoverTrigger } from '../Popover'; +import { useMemo, useState } from 'react'; +import { useInfiniteQuery } from '@tanstack/react-query'; +import { listChatHistoryOptions } from '../../queries/chat-history'; +import { isLoggedIn } from '../../lib/jwt'; +import { groupChatHistory } from '../../helper/grouping'; +import { ChatHistoryGroup } from '../AIChatHistory/ChatHistoryGroup'; +import { queryClient } from '../../stores/query-client'; +import { SearchAIChatHistory } from '../AIChatHistory/SearchAIChatHistory'; + +type RoadmapAIChatHistoryProps = { + roadmapId: string; + activeChatHistoryId?: string; + onChatHistoryClick: (id: string) => void; + onDelete?: (id: string) => void; +}; + +export function RoadmapAIChatHistory(props: RoadmapAIChatHistoryProps) { + const { roadmapId, activeChatHistoryId, onChatHistoryClick, onDelete } = + props; + + const [isOpen, setIsOpen] = useState(true); + const [query, setQuery] = useState(''); + + const { + data: chatHistory, + hasNextPage, + fetchNextPage, + isFetchingNextPage, + isLoading: isLoadingInfiniteQuery, + } = useInfiniteQuery( + { + ...listChatHistoryOptions({ + roadmapId, + query, + }), + enabled: !!roadmapId && isLoggedIn() && isOpen, + }, + queryClient, + ); + + const groupedChatHistory = useMemo(() => { + const allHistories = chatHistory?.pages?.flatMap((page) => page.data); + return groupChatHistory(allHistories ?? []); + }, [chatHistory?.pages]); + const isEmptyHistory = Object.values(groupedChatHistory ?? {}).every( + (group) => group.histories.length === 0, + ); + + return ( + + + + + + + +
+ {isEmptyHistory && ( +
+

No chat history

+
+ )} + + {Object.entries(groupedChatHistory ?? {}).map(([key, value]) => { + if (value.histories.length === 0) { + return null; + } + + return ( + { + onChatHistoryClick(id); + }} + onDelete={(id) => { + onDelete?.(id); + }} + /> + ); + })} + + {hasNextPage && ( +
+ +
+ )} +
+ +
+ +
+
+
+ ); +} diff --git a/src/helper/grouping.ts b/src/helper/grouping.ts new file mode 100644 index 000000000..2bdab5b8a --- /dev/null +++ b/src/helper/grouping.ts @@ -0,0 +1,42 @@ +import { DateTime } from 'luxon'; +import type { ChatHistoryWithoutMessages } from '../queries/chat-history'; + +export function groupChatHistory(chatHistories: ChatHistoryWithoutMessages[]) { + const today = DateTime.now().startOf('day'); + + return chatHistories?.reduce( + (acc, chatHistory) => { + const updatedAt = DateTime.fromJSDate( + new Date(chatHistory.updatedAt), + ).startOf('day'); + const diffInDays = Math.abs(updatedAt.diff(today, 'days').days); + + if (diffInDays === 0) { + acc.today.histories.push(chatHistory); + } else if (diffInDays <= 7) { + acc.last7Days.histories.push(chatHistory); + } else { + acc.older.histories.push(chatHistory); + } + + return acc; + }, + { + today: { + title: 'Today', + histories: [], + }, + last7Days: { + title: 'Last 7 Days', + histories: [], + }, + older: { + title: 'Older', + histories: [], + }, + } as Record< + string, + { title: string; histories: ChatHistoryWithoutMessages[] } + >, + ); +} diff --git a/src/hooks/use-roadmap-ai-chat.tsx b/src/hooks/use-roadmap-ai-chat.tsx index 55ba716a5..cdc440265 100644 --- a/src/hooks/use-roadmap-ai-chat.tsx +++ b/src/hooks/use-roadmap-ai-chat.tsx @@ -17,6 +17,46 @@ import { ShareResourceLink } from '../components/RoadmapAIChat/ShareResourceLink import { RoadmapRecommendations } from '../components/RoadmapAIChat/RoadmapRecommendations'; import type { AllowedAIChatRole } from '../components/GenerateCourse/AICourseLessonChat'; +type RoadmapAIChatRendererOptions = { + totalTopicCount: number; + roadmapId: string; + onSelectTopic: (topicId: string, topicTitle: string) => void; +}; + +export function roadmapAIChatRenderer( + options: RoadmapAIChatRendererOptions, +): Record { + const { totalTopicCount, roadmapId, onSelectTopic } = options; + + return { + 'user-progress': () => ( + + ), + 'update-progress': (opts) => ( + + ), + 'roadmap-topics': (opts) => ( + { + const title = text.split(' > ').pop(); + if (!title) { + return; + } + + onSelectTopic(topicId, title); + }} + {...opts} + /> + ), + 'resource-progress-link': () => , + 'roadmap-recommendations': (opts) => , + }; +} + export type RoadmapAIChatHistoryType = { role: AllowedAIChatRole; isDefault?: boolean; @@ -31,15 +71,22 @@ type Options = { totalTopicCount: number; scrollareaRef: React.RefObject; onSelectTopic: (topicId: string, topicTitle: string) => void; + defaultMessages?: RoadmapAIChatHistoryType[]; }; export function useRoadmapAIChat(options: Options) { - const { roadmapId, totalTopicCount, scrollareaRef, onSelectTopic } = options; + const { + roadmapId, + totalTopicCount, + scrollareaRef, + onSelectTopic, + defaultMessages, + } = options; const toast = useToast(); const [aiChatHistory, setAiChatHistory] = useState< RoadmapAIChatHistoryType[] - >([]); + >(defaultMessages ?? []); const [isStreamingMessage, setIsStreamingMessage] = useState(false); const [streamedMessage, setStreamedMessage] = useState(null); diff --git a/src/pages/[roadmapId]/ai.astro b/src/pages/[roadmapId]/ai.astro index d52b8be6d..8ebffb1c6 100644 --- a/src/pages/[roadmapId]/ai.astro +++ b/src/pages/[roadmapId]/ai.astro @@ -1,29 +1,35 @@ --- -import { type RoadmapFrontmatter, getRoadmapIds } from '../../lib/roadmap'; +import { CheckSubscriptionVerification } from '../../components/Billing/CheckSubscriptionVerification'; +import { RoadmapAIChat } from '../../components/RoadmapAIChat/RoadmapAIChat'; +import SkeletonLayout from '../../layouts/SkeletonLayout.astro'; +import { AITutorLayout } from '../../components/AITutor/AITutorLayout'; +import { getRoadmapById } from '../../lib/roadmap'; -export const prerender = true; +export const prerender = false; -export async function getStaticPaths() { - const roadmapIds = await getRoadmapIds(); - - return roadmapIds.map((roadmapId) => ({ - params: { roadmapId }, - })); -} - -interface Params extends Record { +type Props = { roadmapId: string; -} +}; -const { roadmapId } = Astro.params as Params; -const roadmapFile = await import( - `../../data/roadmaps/${roadmapId}/${roadmapId}.md` -); +const { roadmapId } = Astro.params as Props; -const roadmapData = roadmapFile.frontmatter as RoadmapFrontmatter; -if (roadmapData.renderer !== 'editor') { - return Astro.rewrite(`/404`); -} +const roadmapDetail = await getRoadmapById(roadmapId); -return Astro.rewrite(`/ai/chat/${roadmapId}`); +const canonicalUrl = `https://roadmap.sh/${roadmapId}/ai`; +const roadmapBriefTitle = roadmapDetail.frontmatter.briefTitle; --- + + + + + + + diff --git a/src/pages/[roadmapId]/chat.astro b/src/pages/[roadmapId]/chat.astro deleted file mode 100644 index f49dddcba..000000000 --- a/src/pages/[roadmapId]/chat.astro +++ /dev/null @@ -1,33 +0,0 @@ ---- -import { CheckSubscriptionVerification } from '../../components/Billing/CheckSubscriptionVerification'; -import { RoadmapAIChat } from '../../components/RoadmapAIChat/RoadmapAIChat'; -import SkeletonLayout from '../../layouts/SkeletonLayout.astro'; -import { AITutorLayout } from '../../components/AITutor/AITutorLayout'; -import { getRoadmapById } from '../../lib/roadmap'; - -type Props = { - roadmapId: string; -}; - -const { roadmapId } = Astro.params as Props; - -const roadmapDetail = await getRoadmapById(roadmapId); - -const canonicalUrl = `https://roadmap.sh/${roadmapId}/ai`; -const roadmapBriefTitle = roadmapDetail.frontmatter.briefTitle; ---- - - - - - - - diff --git a/src/queries/chat-history.ts b/src/queries/chat-history.ts index cc4b420fe..e65834339 100644 --- a/src/queries/chat-history.ts +++ b/src/queries/chat-history.ts @@ -1,10 +1,13 @@ import { infiniteQueryOptions, queryOptions } from '@tanstack/react-query'; import { httpGet } from '../lib/query-http'; import { isLoggedIn } from '../lib/jwt'; -import type { RoadmapAIChatHistoryType } from '../components/RoadmapAIChat/RoadmapAIChat'; import { markdownToHtml } from '../lib/markdown'; import { aiChatRenderer } from '../components/AIChat/AIChat'; -import { renderMessage } from '../lib/render-chat-message'; +import { + type MessagePartRenderer, + renderMessage, +} from '../lib/render-chat-message'; +import type { RoadmapAIChatHistoryType } from '../hooks/use-roadmap-ai-chat'; export type ChatHistoryMessage = { _id: string; @@ -16,6 +19,7 @@ export interface ChatHistoryDocument { _id: string; userId: string; + roadmapId?: string; title: string; messages: ChatHistoryMessage[]; @@ -23,7 +27,10 @@ export interface ChatHistoryDocument { updatedAt: Date; } -export function chatHistoryOptions(chatHistoryId?: string) { +export function chatHistoryOptions( + chatHistoryId?: string, + renderer?: Record, +) { return queryOptions({ queryKey: ['chat-history-details', chatHistoryId], queryFn: async () => { @@ -44,7 +51,7 @@ export function chatHistoryOptions(chatHistoryId?: string) { html: markdownToHtml(message.content), }), ...(message.role === 'assistant' && { - jsx: await renderMessage(message.content, aiChatRenderer, { + jsx: await renderMessage(message.content, renderer ?? {}, { isLoading: false, }), }), @@ -64,6 +71,7 @@ type ListChatHistoryQuery = { perPage?: string; currPage?: string; query?: string; + roadmapId?: string; }; export type ChatHistoryWithoutMessages = Omit; @@ -79,6 +87,7 @@ type ListChatHistoryResponse = { export function listChatHistoryOptions( query: ListChatHistoryQuery = { query: '', + roadmapId: '', }, ) { return infiniteQueryOptions({ @@ -86,6 +95,7 @@ export function listChatHistoryOptions( queryFn: ({ pageParam }) => { return httpGet('/v1-list-chat-history', { ...(query?.query ? { query: query.query } : {}), + ...(query?.roadmapId ? { roadmapId: query.roadmapId } : {}), ...(pageParam ? { currPage: pageParam } : {}), perPage: '21', });