1
0
mirror of https://github.com/kamranahmedse/developer-roadmap.git synced 2025-09-03 14:22:41 +02:00
This commit is contained in:
Arik Chakma
2025-06-13 15:38:43 +06:00
parent 106e754beb
commit 9a58991ad1
6 changed files with 100 additions and 45 deletions

View 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>
);
}

View File

@@ -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} />;
}

View File

@@ -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) {

View File

@@ -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>

View File

@@ -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>

View File

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