diff --git a/src/components/GenerateGuide/AIGuide.tsx b/src/components/GenerateGuide/AIGuide.tsx new file mode 100644 index 000000000..fa7de5d4b --- /dev/null +++ b/src/components/GenerateGuide/AIGuide.tsx @@ -0,0 +1,33 @@ +import { useState } from 'react'; +import { AITutorLayout } from '../AITutor/AITutorLayout'; +import { AIGuideContent } from './AIGuideContent'; +import { useQuery } from '@tanstack/react-query'; +import { getAiGuideOptions } from '../../queries/ai-guide'; +import { queryClient } from '../../stores/query-client'; +import { GenerateAIGuide } from './GenerateAIGuide'; + +type AIGuideProps = { + guideSlug?: string; +}; + +export function AIGuide(props: AIGuideProps) { + const { guideSlug: defaultGuideSlug } = props; + const [guideSlug, setGuideSlug] = useState(defaultGuideSlug); + + // only fetch the guide if the guideSlug is provided + // otherwise we are still generating the guide + const { data: aiGuide } = useQuery(getAiGuideOptions(guideSlug), queryClient); + + return ( + +
+ {guideSlug && } + {!guideSlug && } +
+
Chat Window
+
+ ); +} diff --git a/src/components/GenerateGuide/GenerateAIGuide.tsx b/src/components/GenerateGuide/GenerateAIGuide.tsx index 00d5565da..5d2d8012d 100644 --- a/src/components/GenerateGuide/GenerateAIGuide.tsx +++ b/src/components/GenerateGuide/GenerateAIGuide.tsx @@ -1,38 +1,29 @@ -import { useQuery } from '@tanstack/react-query'; -import { useEffect, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { generateGuide } from '../../helper/generate-ai-guide'; import { getCourseFineTuneData } from '../../lib/ai'; import { getUrlParams } from '../../lib/browser'; import { isLoggedIn } from '../../lib/jwt'; -import { queryClient } from '../../stores/query-client'; import { AIGuideContent } from './AIGuideContent'; +import { Loader2Icon } from 'lucide-react'; +import { queryClient } from '../../stores/query-client'; import { getAiGuideOptions } from '../../queries/ai-guide'; -type GenerateAIGuideProps = {}; +type GenerateAIGuideProps = { + onGuideSlugChange?: (guideSlug: string) => void; +}; export function GenerateAIGuide(props: GenerateAIGuideProps) { - const [term, setTerm] = useState(''); - const [depth, setDepth] = useState(''); + const { onGuideSlugChange } = props; const [isLoading, setIsLoading] = useState(true); + const [isStreaming, setIsStreaming] = useState(false); const [error, setError] = useState(''); - const [documentSlug, setDocumentSlug] = useState(''); + const [content, setContent] = useState(''); const [html, setHtml] = useState(''); - - // Once the course is generated, we fetch the course from the database - // so that we get the up-to-date course data and also so that we - // can reload the changes (e.g. progress) etc using queryClient.setQueryData - const { data: aiGuide } = useQuery( - getAiGuideOptions(documentSlug), - queryClient, - ); + const htmlRef = useRef(''); useEffect(() => { - if (term || depth) { - return; - } - const params = getUrlParams(); const paramsTerm = params?.term; const paramsDepth = params?.depth; @@ -63,7 +54,7 @@ export function GenerateAIGuide(props: GenerateAIGuideProps) { about: paramsAbout, src: paramsSrc, }); - }, [term, depth]); + }, []); const handleGenerateDocument = async (options: { term: string; @@ -86,10 +77,29 @@ export function GenerateAIGuide(props: GenerateAIGuideProps) { await generateGuide({ term, depth, - slug: documentSlug, - onGuideSlugChange: (slug) => { - setDocumentSlug(slug); - window.history.replaceState(null, '', `/ai/guide/${slug}`); + onDetailsChange: (details) => { + const { guideId, guideSlug, creatorId, title } = details; + + const guideData = { + _id: guideId, + userId: creatorId, + title, + html: htmlRef.current, + keyword: term, + difficulty: depth, + content, + viewCount: 0, + createdAt: new Date(), + updatedAt: new Date(), + }; + + queryClient.setQueryData( + getAiGuideOptions(guideSlug).queryKey, + guideData, + ); + + onGuideSlugChange?.(guideSlug); + window.history.replaceState(null, '', `/ai/guide/${guideSlug}`); }, onLoadingChange: setIsLoading, onError: setError, @@ -99,7 +109,11 @@ export function GenerateAIGuide(props: GenerateAIGuideProps) { isForce, prompt, src, - onHtmlChange: setHtml, + onHtmlChange: (html) => { + htmlRef.current = html; + setHtml(html); + }, + onStreamingChange: setIsStreaming, }); }; @@ -107,5 +121,13 @@ export function GenerateAIGuide(props: GenerateAIGuideProps) { return
{error}
; } + if (isLoading) { + return ( +
+ +
+ ); + } + return ; } diff --git a/src/helper/generate-ai-guide.ts b/src/helper/generate-ai-guide.ts index 367a77967..74c07fa3c 100644 --- a/src/helper/generate-ai-guide.ts +++ b/src/helper/generate-ai-guide.ts @@ -4,6 +4,13 @@ import { getAiCourseLimitOptions } from '../queries/ai-course'; import { readChatStream } from '../lib/chat'; import { markdownToHtmlWithHighlighting } from '../lib/markdown'; +type GuideDetails = { + guideId: string; + guideSlug: string; + creatorId: string; + title: string; +}; + type GenerateGuideOptions = { term: string; depth: string; @@ -13,14 +20,14 @@ type GenerateGuideOptions = { instructions?: string; goal?: string; about?: string; - onGuideIdChange?: (guideId: string) => void; onGuideSlugChange?: (guideSlug: string) => void; onGuideChange?: (guide: string) => void; onLoadingChange?: (isLoading: boolean) => void; - onCreatorIdChange?: (creatorId: string) => void; onError?: (error: string) => void; src?: string; onHtmlChange?: (html: string) => void; + onStreamingChange?: (isStreaming: boolean) => void; + onDetailsChange?: (details: GuideDetails) => void; }; export async function generateGuide(options: GenerateGuideOptions) { @@ -28,12 +35,9 @@ export async function generateGuide(options: GenerateGuideOptions) { term, slug, depth, - onGuideIdChange, - onGuideSlugChange, onGuideChange, onLoadingChange, onError, - onCreatorIdChange, isForce = false, prompt, instructions, @@ -41,6 +45,8 @@ export async function generateGuide(options: GenerateGuideOptions) { about, src = 'search', onHtmlChange, + onStreamingChange, + onDetailsChange, } = options; onLoadingChange?.(true); @@ -107,16 +113,18 @@ export async function generateGuide(options: GenerateGuideOptions) { return; } + onLoadingChange?.(false); + onStreamingChange?.(true); await readChatStream(stream, { onMessage: async (message) => { onGuideChange?.(message); onHtmlChange?.(await markdownToHtmlWithHighlighting(message)); }, onMessageEnd: async (message) => { - onLoadingChange?.(false); onGuideChange?.(message); onHtmlChange?.(await markdownToHtmlWithHighlighting(message)); queryClient.invalidateQueries(getAiCourseLimitOptions()); + onStreamingChange?.(false); }, onDetails: async (details) => { const detailsJson = JSON.parse(details); @@ -124,9 +132,7 @@ export async function generateGuide(options: GenerateGuideOptions) { throw new Error('Invalid details'); } - onGuideIdChange?.(detailsJson?.guideId); - onGuideSlugChange?.(detailsJson?.guideSlug); - onCreatorIdChange?.(detailsJson?.creatorId); + onDetailsChange?.(detailsJson); }, }); } catch (error: any) { diff --git a/src/pages/ai/guide/[slug].astro b/src/pages/ai/guide/[slug].astro index 4391b201d..669ff36c5 100644 --- a/src/pages/ai/guide/[slug].astro +++ b/src/pages/ai/guide/[slug].astro @@ -1,6 +1,5 @@ --- -import { AITutorLayout } from '../../../components/AITutor/AITutorLayout'; -import { GetAIGuide } from '../../../components/GenerateGuide/GetAIGuide'; +import { AIGuide } from '../../../components/GenerateGuide/AIGuide'; import SkeletonLayout from '../../../layouts/SkeletonLayout.astro'; export const prerender = false; @@ -19,8 +18,5 @@ const { slug } = Astro.params as Params; keywords={['ai', 'tutor', 'education', 'learning']} canonicalUrl={`/ai/document/${slug}`} > - -
- -
+ diff --git a/src/pages/ai/guide/index.astro b/src/pages/ai/guide/index.astro index f8b263f26..405ac8d4d 100644 --- a/src/pages/ai/guide/index.astro +++ b/src/pages/ai/guide/index.astro @@ -1,6 +1,7 @@ --- import { AITutorLayout } from '../../../components/AITutor/AITutorLayout'; import { CheckSubscriptionVerification } from '../../../components/Billing/CheckSubscriptionVerification'; +import { AIGuide } from '../../../components/GenerateGuide/AIGuide'; import { GenerateAIGuide } from '../../../components/GenerateGuide/GenerateAIGuide'; import SkeletonLayout from '../../../layouts/SkeletonLayout.astro'; --- @@ -13,8 +14,5 @@ import SkeletonLayout from '../../../layouts/SkeletonLayout.astro'; canonicalUrl='/ai/document' noIndex={true} > - - - - + diff --git a/src/queries/ai-guide.ts b/src/queries/ai-guide.ts index 42271a51f..4a5c4610b 100644 --- a/src/queries/ai-guide.ts +++ b/src/queries/ai-guide.ts @@ -18,7 +18,7 @@ export interface AIGuideDocument { type GetAIGuideResponse = AIGuideDocument; -export function getAiGuideOptions(guideSlug: string) { +export function getAiGuideOptions(guideSlug?: string) { return queryOptions({ queryKey: ['ai-guide', guideSlug], queryFn: async () => {