1
0
mirror of https://github.com/kamranahmedse/developer-roadmap.git synced 2025-09-03 06:12:53 +02:00
This commit is contained in:
Arik Chakma
2025-06-13 14:12:15 +06:00
parent bdc9dffcd2
commit 106e754beb
9 changed files with 164 additions and 236 deletions

1
.astro/types.d.ts vendored
View File

@@ -1,2 +1 @@
/// <reference types="astro/client" />
/// <reference path="content.d.ts" />

30
pnpm-lock.yaml generated
View File

@@ -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
@@ -270,6 +273,9 @@ importers:
prettier-plugin-tailwindcss:
specifier: ^0.6.11
version: 0.6.11(prettier-plugin-astro@0.14.1)(prettier@3.5.3)
tailwind-scrollbar:
specifier: ^4.0.2
version: 4.0.2(react@19.1.0)(tailwindcss@4.1.7)
tsx:
specifier: ^4.19.4
version: 4.19.4
@@ -3923,6 +3929,11 @@ packages:
engines: {node: '>=14'}
hasBin: true
prism-react-renderer@2.4.1:
resolution: {integrity: sha512-ey8Ls/+Di31eqzUxC46h8MksNuGx/n0AAC8uKpwFau4RPDYLuE3EXTp8N8G2vX2N7UC/+IXeNUnlWBGGcAG+Ig==}
peerDependencies:
react: '>=16.0.0'
prismjs@1.30.0:
resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==}
engines: {node: '>=6'}
@@ -4359,6 +4370,12 @@ packages:
tailwind-merge@3.3.0:
resolution: {integrity: sha512-fyW/pEfcQSiigd5SNn0nApUOxx0zB/dm6UDU/rEwc2c3sX2smWUNbapHv+QRqLGVp9GWX3THIa7MUGPo+YkDzQ==}
tailwind-scrollbar@4.0.2:
resolution: {integrity: sha512-wAQiIxAPqk0MNTPptVe/xoyWi27y+NRGnTwvn4PQnbvB9kp8QUBiGl/wsfoVBHnQxTmhXJSNt9NHTmcz9EivFA==}
engines: {node: '>=12.13.0'}
peerDependencies:
tailwindcss: 4.x
tailwindcss@4.1.5:
resolution: {integrity: sha512-nYtSPfWGDiWgCkwQG/m+aX83XCwf62sBgg3bIlNiiOcggnS1x3uVRDAuyelBFL+vJdOPPCGElxv9DjHJjRHiVA==}
@@ -8495,6 +8512,12 @@ snapshots:
prettier@3.5.3: {}
prism-react-renderer@2.4.1(react@19.1.0):
dependencies:
'@types/prismjs': 1.26.5
clsx: 2.1.1
react: 19.1.0
prismjs@1.30.0: {}
prompts@2.4.2:
@@ -9154,6 +9177,13 @@ snapshots:
tailwind-merge@3.3.0: {}
tailwind-scrollbar@4.0.2(react@19.1.0)(tailwindcss@4.1.7):
dependencies:
prism-react-renderer: 2.4.1(react@19.1.0)
tailwindcss: 4.1.7
transitivePeerDependencies:
- react
tailwindcss@4.1.5: {}
tailwindcss@4.1.7: {}

View File

@@ -1,34 +1,16 @@
import { useEffect, useState } from 'react';
import {
markdownToHtml,
markdownToHtmlWithHighlighting,
} from '../../lib/markdown';
import './AIDocumentContent.css';
import './AIGuideContent.css';
type AIDocumentContentProps = {
document: string;
type AIGuideContentProps = {
html: string;
};
export function AIDocumentContent(props: AIDocumentContentProps) {
const { document } = props;
const [html, setHtml] = useState('');
useEffect(() => {
const html = markdownToHtmlWithHighlighting(document)
.then((html) => {
setHtml(html);
})
.catch((e) => {
console.error(e);
return markdownToHtml(document, false);
});
}, [document]);
export function AIGuideContent(props: AIGuideContentProps) {
const { html } = props;
return (
<div className="mx-auto w-full max-w-4xl">
<div
className="course-content [&>h1]:text-balance prose prose-lg prose-headings:mb-3 prose-headings:mt-8 prose-blockquote:font-normal prose-pre:rounded-2xl prose-pre:text-lg prose-li:my-1 prose-thead:border-zinc-800 prose-tr:border-zinc-800 max-lg:prose-h2:mt-3 max-lg:prose-h2:text-lg max-lg:prose-h3:text-base max-lg:prose-pre:px-3 max-lg:prose-pre:text-sm mt-8 max-w-full text-black max-lg:mt-4 max-lg:text-base"
className="course-content prose prose-lg prose-headings:mb-3 prose-headings:mt-8 prose-blockquote:font-normal prose-pre:rounded-2xl prose-pre:text-lg prose-li:my-1 prose-thead:border-zinc-800 prose-tr:border-zinc-800 max-lg:prose-h2:mt-3 max-lg:prose-h2:text-lg max-lg:prose-h3:text-base max-lg:prose-pre:px-3 max-lg:prose-pre:text-sm mt-8 max-w-full text-black max-lg:mt-4 max-lg:text-base [&>h1]:text-balance"
dangerouslySetInnerHTML={{ __html: html }}
/>
</div>

View File

@@ -4,85 +4,70 @@ import { generateGuide } from '../../helper/generate-ai-guide';
import { getCourseFineTuneData } from '../../lib/ai';
import { getUrlParams } from '../../lib/browser';
import { isLoggedIn } from '../../lib/jwt';
import { getAiCourseOptions } from '../../queries/ai-course';
import { queryClient } from '../../stores/query-client';
import { AIDocumentContent } from './AIGuideContent';
import { AIGuideContent } from './AIGuideContent';
import { getAiGuideOptions } from '../../queries/ai-guide';
type GenerateAIGuideProps = {};
export function GenerateAIGuide(props: GenerateAIGuideProps) {
const [term, setTerm] = useState('');
const [difficulty, setDifficulty] = useState('');
const [sessionId, setSessionId] = useState('');
const [goal, setGoal] = useState('');
const [about, setAbout] = useState('');
const [customInstructions, setCustomInstructions] = useState('');
const [depth, setDepth] = useState('');
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState('');
const [creatorId, setCreatorId] = useState('');
const [documentId, setDocumentId] = useState('');
const [documentSlug, setDocumentSlug] = useState('');
const [document, setDocument] = useState<string>('');
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: aiCourse } = useQuery(
getAiCourseOptions({ aiCourseSlug: documentSlug }),
const { data: aiGuide } = useQuery(
getAiGuideOptions(documentSlug),
queryClient,
);
useEffect(() => {
if (term || difficulty) {
if (term || depth) {
return;
}
const params = getUrlParams();
const paramsTerm = params?.term;
const paramsDifficulty = params?.difficulty;
const paramsDepth = params?.depth;
const paramsSrc = params?.src || 'search';
if (!paramsTerm || !paramsDifficulty) {
if (!paramsTerm || !paramsDepth) {
return;
}
setTerm(paramsTerm);
setDifficulty(paramsDifficulty);
const sessionId = params?.id;
setSessionId(sessionId);
let paramsGoal = '';
let paramsAbout = '';
let paramsCustomInstructions = '';
const sessionId = params?.id;
if (sessionId) {
const fineTuneData = getCourseFineTuneData(sessionId);
if (fineTuneData) {
paramsGoal = fineTuneData.goal;
paramsAbout = fineTuneData.about;
paramsCustomInstructions = fineTuneData.customInstructions;
setGoal(paramsGoal);
setAbout(paramsAbout);
setCustomInstructions(paramsCustomInstructions);
}
}
handleGenerateDocument({
term: paramsTerm,
difficulty: paramsDifficulty,
depth: paramsDepth,
instructions: paramsCustomInstructions,
goal: paramsGoal,
about: paramsAbout,
src: paramsSrc,
});
}, [term, difficulty]);
}, [term, depth]);
const handleGenerateDocument = async (options: {
term: string;
difficulty: string;
depth: string;
instructions?: string;
goal?: string;
about?: string;
@@ -90,30 +75,22 @@ export function GenerateAIGuide(props: GenerateAIGuideProps) {
prompt?: string;
src?: string;
}) => {
const {
term,
difficulty,
isForce,
prompt,
instructions,
goal,
about,
src,
} = options;
const { term, depth, isForce, prompt, instructions, goal, about, src } =
options;
if (!isLoggedIn()) {
window.location.href = '/ai';
return;
}
await generateDocument({
await generateGuide({
term,
difficulty,
depth,
slug: documentSlug,
onDocumentIdChange: setDocumentId,
onDocumentSlugChange: setDocumentSlug,
onCreatorIdChange: setCreatorId,
onDocumentChange: setDocument,
onGuideSlugChange: (slug) => {
setDocumentSlug(slug);
window.history.replaceState(null, '', `/ai/guide/${slug}`);
},
onLoadingChange: setIsLoading,
onError: setError,
instructions,
@@ -122,37 +99,13 @@ export function GenerateAIGuide(props: GenerateAIGuideProps) {
isForce,
prompt,
src,
onHtmlChange: setHtml,
});
};
useEffect(() => {
const handlePopState = (e: PopStateEvent) => {
const { documentId, documentSlug, term, difficulty } = e.state || {};
if (!documentId || !documentSlug) {
window.location.reload();
return;
}
setDocumentId(documentId);
setDocumentSlug(documentSlug);
setTerm(term);
setDifficulty(difficulty);
setIsLoading(true);
handleGenerateDocument({ term, difficulty }).finally(() => {
setIsLoading(false);
});
};
window.addEventListener('popstate', handlePopState);
return () => {
window.removeEventListener('popstate', handlePopState);
};
}, []);
if (error) {
return <div className="text-red-500">{error}</div>;
}
return <AIDocumentContent document={document} />;
return <AIGuideContent html={html} />;
}

View File

@@ -1,36 +1,35 @@
import { useQuery } from '@tanstack/react-query';
import { useEffect, useState } from 'react';
import { generateDocument } from '../../helper/generate-ai-guide';
import { queryClient } from '../../stores/query-client';
import { getAiDocumentOptions } from '../../queries/ai-document';
import { AIDocumentContent } from './AIGuideContent';
import { AIGuideContent } from './AIGuideContent';
import { getAiGuideOptions } from '../../queries/ai-guide';
type GetAIDocumentProps = {
type GetAIGuideProps = {
slug: string;
};
export function GetAIDocument(props: GetAIDocumentProps) {
export function GetAIGuide(props: GetAIGuideProps) {
const { slug: documentSlug } = props;
const [isLoading, setIsLoading] = useState(true);
const [isRegenerating, setIsRegenerating] = useState(false);
const [error, setError] = useState('');
const { data: aiDocument, error: queryError } = useQuery(
const { data: aiGuide, error: queryError } = useQuery(
{
...getAiDocumentOptions({ documentSlug: documentSlug }),
...getAiGuideOptions(documentSlug),
enabled: !!documentSlug,
},
queryClient,
);
useEffect(() => {
if (!aiDocument) {
if (!aiGuide) {
return;
}
setIsLoading(false);
}, [aiDocument]);
}, [aiGuide]);
useEffect(() => {
if (!queryError) {
@@ -42,46 +41,44 @@ export function GetAIDocument(props: GetAIDocumentProps) {
}, [queryError]);
const handleRegenerateDocument = async (prompt?: string) => {
if (!aiDocument) {
return;
}
queryClient.setQueryData(
getAiDocumentOptions({ documentSlug: documentSlug }).queryKey,
{
...aiDocument,
title: '',
difficulty: '',
modules: [],
},
);
await generateDocument({
term: aiDocument.keyword,
difficulty: aiDocument.difficulty,
slug: documentSlug,
prompt,
onDocumentChange: (document) => {
queryClient.setQueryData(
getAiDocumentOptions({ documentSlug: documentSlug }).queryKey,
{
...aiDocument,
title: aiDocument.title,
difficulty: aiDocument.difficulty,
content: document,
},
);
},
onLoadingChange: (isNewLoading) => {
setIsRegenerating(isNewLoading);
if (!isNewLoading) {
// TODO: Update progress
}
},
onError: setError,
isForce: true,
});
// if (!aiDocument) {
// return;
// }
// queryClient.setQueryData(
// getAiDocumentOptions({ documentSlug: documentSlug }).queryKey,
// {
// ...aiDocument,
// title: '',
// difficulty: '',
// modules: [],
// },
// );
// await generateDocument({
// term: aiDocument.keyword,
// difficulty: aiDocument.difficulty,
// slug: documentSlug,
// prompt,
// onDocumentChange: (document) => {
// queryClient.setQueryData(
// getAiDocumentOptions({ documentSlug: documentSlug }).queryKey,
// {
// ...aiDocument,
// title: aiDocument.title,
// difficulty: aiDocument.difficulty,
// content: document,
// },
// );
// },
// onLoadingChange: (isNewLoading) => {
// setIsRegenerating(isNewLoading);
// if (!isNewLoading) {
// // TODO: Update progress
// }
// },
// onError: setError,
// isForce: true,
// });
};
return <AIDocumentContent document={aiDocument?.content || ''} />;
return <AIGuideContent html={aiGuide?.html || ''} />;
}

View File

@@ -1,33 +1,36 @@
import { readStream } from '../lib/ai';
import { queryClient } from '../stores/query-client';
import { getAiCourseLimitOptions } from '../queries/ai-course';
import { readChatStream } from '../lib/chat';
import { markdownToHtmlWithHighlighting } from '../lib/markdown';
type GenerateGuideOptions = {
term: string;
difficulty: string;
depth: string;
slug?: string;
isForce?: boolean;
prompt?: string;
instructions?: string;
goal?: string;
about?: string;
onDocumentIdChange?: (documentId: string) => void;
onDocumentSlugChange?: (documentSlug: string) => void;
onDocumentChange?: (document: string) => void;
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;
};
export async function generateGuide(options: GenerateGuideOptions) {
const {
term,
slug,
difficulty,
onDocumentIdChange,
onDocumentSlugChange,
onDocumentChange,
depth,
onGuideIdChange,
onGuideSlugChange,
onGuideChange,
onLoadingChange,
onError,
onCreatorIdChange,
@@ -37,10 +40,11 @@ export async function generateGuide(options: GenerateGuideOptions) {
goal,
about,
src = 'search',
onHtmlChange,
} = options;
onLoadingChange?.(true);
onDocumentChange?.('');
onGuideChange?.('');
onError?.('');
try {
@@ -48,7 +52,7 @@ export async function generateGuide(options: GenerateGuideOptions) {
if (slug && isForce) {
response = await fetch(
`${import.meta.env.PUBLIC_API_URL}/v1-regenerate-ai-document/${slug}`,
`${import.meta.env.PUBLIC_API_URL}/v1-regenerate-ai-guide/${slug}`,
{
method: 'POST',
headers: {
@@ -63,7 +67,7 @@ export async function generateGuide(options: GenerateGuideOptions) {
);
} else {
response = await fetch(
`${import.meta.env.PUBLIC_API_URL}/v1-generate-ai-document`,
`${import.meta.env.PUBLIC_API_URL}/v1-generate-ai-guide`,
{
method: 'POST',
headers: {
@@ -71,7 +75,7 @@ export async function generateGuide(options: GenerateGuideOptions) {
},
body: JSON.stringify({
keyword: term,
difficulty,
depth,
isForce,
customPrompt: prompt,
instructions,
@@ -95,19 +99,6 @@ export async function generateGuide(options: GenerateGuideOptions) {
return;
}
// const reader = response.body?.getReader();
// if (!reader) {
// console.error('Failed to get reader from response');
// onError?.('Something went wrong');
// onLoadingChange?.(false);
// return;
// }
// const DOCUMENT_ID_REGEX = new RegExp('@DOCID:(\\w+)@');
// const DOCUMENT_SLUG_REGEX = new RegExp(/@DOCSLUG:([\w-]+)@/);
// const CREATOR_ID_REGEX = new RegExp('@CREATORID:(\\w+)@');
const stream = response.body;
if (!stream) {
console.error('Failed to get stream from response');
@@ -116,55 +107,28 @@ export async function generateGuide(options: GenerateGuideOptions) {
return;
}
// await readStream(reader, {
// onStream: async (result) => {
// if (result.includes('@DOCID') || result.includes('@DOCSLUG')) {
// const documentIdMatch = result.match(DOCUMENT_ID_REGEX);
// const documentSlugMatch = result.match(DOCUMENT_SLUG_REGEX);
// const creatorIdMatch = result.match(CREATOR_ID_REGEX);
// const extractedDocumentId = documentIdMatch?.[1] || '';
// const extractedDocumentSlug = documentSlugMatch?.[1] || '';
// const extractedCreatorId = creatorIdMatch?.[1] || '';
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());
},
onDetails: async (details) => {
const detailsJson = JSON.parse(details);
if (!detailsJson?.guideId || !detailsJson?.guideSlug) {
throw new Error('Invalid details');
}
// if (extractedDocumentSlug) {
// window.history.replaceState(
// {
// documentId: extractedDocumentId,
// documentSlug: extractedDocumentSlug,
// term,
// difficulty,
// },
// '',
// `${origin}/ai/document/${extractedDocumentSlug}`,
// );
// }
// result = result
// .replace(DOCUMENT_ID_REGEX, '')
// .replace(DOCUMENT_SLUG_REGEX, '')
// .replace(CREATOR_ID_REGEX, '');
// onDocumentIdChange?.(extractedDocumentId);
// onDocumentSlugChange?.(extractedDocumentSlug);
// onCreatorIdChange?.(extractedCreatorId);
// }
// try {
// onDocumentChange?.(result);
// } catch (e) {
// console.error('Error parsing streamed course content:', e);
// }
// },
// onStreamEnd: async (result) => {
// result = result
// .replace(DOCUMENT_ID_REGEX, '')
// .replace(DOCUMENT_SLUG_REGEX, '')
// .replace(CREATOR_ID_REGEX, '');
// onLoadingChange?.(false);
// queryClient.invalidateQueries(getAiCourseLimitOptions());
// },
// });
onGuideIdChange?.(detailsJson?.guideId);
onGuideSlugChange?.(detailsJson?.guideSlug);
onCreatorIdChange?.(detailsJson?.creatorId);
},
});
} catch (error: any) {
onError?.(error?.message || 'Something went wrong');
console.error('Error in course generation:', error);

View File

@@ -1,6 +1,6 @@
---
import { AITutorLayout } from '../../../components/AITutor/AITutorLayout';
import { GetAIDocument } from '../../../components/GenerateGuide/GetAIGuide';
import { GetAIGuide } from '../../../components/GenerateGuide/GetAIGuide';
import SkeletonLayout from '../../../layouts/SkeletonLayout.astro';
export const prerender = false;
@@ -21,6 +21,6 @@ const { slug } = Astro.params as Params;
>
<AITutorLayout client:load>
<div slot='course-announcement'></div>
<GetAIDocument client:load slug={slug} />
<GetAIGuide client:load slug={slug} />
</AITutorLayout>
</SkeletonLayout>

View File

@@ -1,7 +1,7 @@
---
import { AITutorLayout } from '../../../components/AITutor/AITutorLayout';
import { CheckSubscriptionVerification } from '../../../components/Billing/CheckSubscriptionVerification';
import { GenerateAIDocument } from '../../../components/GenerateGuide/GenerateAIGuide';
import { GenerateAIGuide } from '../../../components/GenerateGuide/GenerateAIGuide';
import SkeletonLayout from '../../../layouts/SkeletonLayout.astro';
---
@@ -14,7 +14,7 @@ import SkeletonLayout from '../../../layouts/SkeletonLayout.astro';
noIndex={true}
>
<AITutorLayout client:load>
<GenerateAIDocument client:load />
<GenerateAIGuide client:load />
<CheckSubscriptionVerification client:load />
</AITutorLayout>
</SkeletonLayout>

View File

@@ -1,11 +1,9 @@
import { httpGet } from '../lib/query-http';
import { isLoggedIn } from '../lib/jwt';
import { queryOptions } from '@tanstack/react-query';
import { markdownToHtmlWithHighlighting } from '../lib/markdown';
type GetAIDocumentParams = {
documentSlug: string;
};
export interface AIDocumentDocument {
export interface AIGuideDocument {
_id: string;
userId: string;
title: string;
@@ -18,18 +16,23 @@ export interface AIDocumentDocument {
updatedAt: Date;
}
type GetAIDocumentResponse = AIDocumentDocument;
type GetAIGuideResponse = AIGuideDocument;
export function getAiDocumentOptions(params: GetAIDocumentParams) {
return {
queryKey: ['ai-document', params],
queryFn: () => {
return httpGet<GetAIDocumentResponse>(
`/v1-get-ai-document/${params.documentSlug}`,
export function getAiGuideOptions(guideSlug: string) {
return queryOptions({
queryKey: ['ai-guide', guideSlug],
queryFn: async () => {
const res = await httpGet<GetAIGuideResponse>(
`/v1-get-ai-guide/${guideSlug}`,
);
return {
...res,
html: await markdownToHtmlWithHighlighting(res.content),
};
},
enabled: !!params.documentSlug,
};
enabled: !!guideSlug,
});
}
export type ListUserAiDocumentsQuery = {
@@ -39,7 +42,7 @@ export type ListUserAiDocumentsQuery = {
};
type ListUserAiDocumentsResponse = {
data: AIDocumentDocument[];
data: AIGuideDocument[];
totalCount: number;
totalPages: number;
currPage: number;