From 106e754beb66e95e580351d544b3ca8702608133 Mon Sep 17 00:00:00 2001 From: Arik Chakma Date: Fri, 13 Jun 2025 14:12:15 +0600 Subject: [PATCH] wip --- .astro/types.d.ts | 1 - pnpm-lock.yaml | 30 +++++ .../GenerateGuide/AIGuideContent.tsx | 30 +---- .../GenerateGuide/GenerateAIGuide.tsx | 93 ++++----------- src/components/GenerateGuide/GetAIGuide.tsx | 95 ++++++++------- src/helper/generate-ai-guide.ts | 110 ++++++------------ src/pages/ai/guide/[slug].astro | 4 +- .../ai/guide/{guide.astro => index.astro} | 4 +- src/queries/{ai-document.ts => ai-guide.ts} | 33 +++--- 9 files changed, 164 insertions(+), 236 deletions(-) rename src/pages/ai/guide/{guide.astro => index.astro} (81%) rename src/queries/{ai-document.ts => ai-guide.ts} (60%) diff --git a/.astro/types.d.ts b/.astro/types.d.ts index 03d7cc43f..f964fe0cf 100644 --- a/.astro/types.d.ts +++ b/.astro/types.d.ts @@ -1,2 +1 @@ /// -/// \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0e6215c35..2aaa61835 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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: {} diff --git a/src/components/GenerateGuide/AIGuideContent.tsx b/src/components/GenerateGuide/AIGuideContent.tsx index 7555e35eb..41e591576 100644 --- a/src/components/GenerateGuide/AIGuideContent.tsx +++ b/src/components/GenerateGuide/AIGuideContent.tsx @@ -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 (
diff --git a/src/components/GenerateGuide/GenerateAIGuide.tsx b/src/components/GenerateGuide/GenerateAIGuide.tsx index d3eced2ed..00d5565da 100644 --- a/src/components/GenerateGuide/GenerateAIGuide.tsx +++ b/src/components/GenerateGuide/GenerateAIGuide.tsx @@ -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(''); + 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
{error}
; } - return ; + return ; } diff --git a/src/components/GenerateGuide/GetAIGuide.tsx b/src/components/GenerateGuide/GetAIGuide.tsx index 8ad82eaf4..21821f8f5 100644 --- a/src/components/GenerateGuide/GetAIGuide.tsx +++ b/src/components/GenerateGuide/GetAIGuide.tsx @@ -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 ; + return ; } diff --git a/src/helper/generate-ai-guide.ts b/src/helper/generate-ai-guide.ts index eaae37069..367a77967 100644 --- a/src/helper/generate-ai-guide.ts +++ b/src/helper/generate-ai-guide.ts @@ -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); diff --git a/src/pages/ai/guide/[slug].astro b/src/pages/ai/guide/[slug].astro index 9dfb346ee..4391b201d 100644 --- a/src/pages/ai/guide/[slug].astro +++ b/src/pages/ai/guide/[slug].astro @@ -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; >
- +
diff --git a/src/pages/ai/guide/guide.astro b/src/pages/ai/guide/index.astro similarity index 81% rename from src/pages/ai/guide/guide.astro rename to src/pages/ai/guide/index.astro index 382409e0e..f8b263f26 100644 --- a/src/pages/ai/guide/guide.astro +++ b/src/pages/ai/guide/index.astro @@ -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} > - + diff --git a/src/queries/ai-document.ts b/src/queries/ai-guide.ts similarity index 60% rename from src/queries/ai-document.ts rename to src/queries/ai-guide.ts index c577c0384..42271a51f 100644 --- a/src/queries/ai-document.ts +++ b/src/queries/ai-guide.ts @@ -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( - `/v1-get-ai-document/${params.documentSlug}`, +export function getAiGuideOptions(guideSlug: string) { + return queryOptions({ + queryKey: ['ai-guide', guideSlug], + queryFn: async () => { + const res = await httpGet( + `/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;