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 () => {