mirror of
https://github.com/kamranahmedse/developer-roadmap.git
synced 2025-09-03 14:22:41 +02:00
wip
This commit is contained in:
33
src/components/GenerateGuide/AIGuide.tsx
Normal file
33
src/components/GenerateGuide/AIGuide.tsx
Normal file
@@ -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 (
|
||||
<AITutorLayout
|
||||
wrapperClassName="flex-row p-0 lg:p-0 overflow-hidden"
|
||||
containerClassName="h-[calc(100vh-49px)] overflow-hidden"
|
||||
>
|
||||
<div className="grow overflow-y-auto p-4 pt-0">
|
||||
{guideSlug && <AIGuideContent html={aiGuide?.html || ''} />}
|
||||
{!guideSlug && <GenerateAIGuide onGuideSlugChange={setGuideSlug} />}
|
||||
</div>
|
||||
<div className="w-full max-w-[40%]">Chat Window</div>
|
||||
</AITutorLayout>
|
||||
);
|
||||
}
|
@@ -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<string>('');
|
||||
|
||||
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 <div className="text-red-500">{error}</div>;
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center">
|
||||
<Loader2Icon className="size-6 animate-spin" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <AIGuideContent html={html} />;
|
||||
}
|
||||
|
@@ -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) {
|
||||
|
@@ -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}`}
|
||||
>
|
||||
<AITutorLayout client:load>
|
||||
<div slot='course-announcement'></div>
|
||||
<GetAIGuide client:load slug={slug} />
|
||||
</AITutorLayout>
|
||||
<AIGuide client:load guideSlug={slug} />
|
||||
</SkeletonLayout>
|
||||
|
@@ -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}
|
||||
>
|
||||
<AITutorLayout client:load>
|
||||
<GenerateAIGuide client:load />
|
||||
<CheckSubscriptionVerification client:load />
|
||||
</AITutorLayout>
|
||||
<AIGuide client:load />
|
||||
</SkeletonLayout>
|
||||
|
@@ -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 () => {
|
||||
|
Reference in New Issue
Block a user