1
0
mirror of https://github.com/kamranahmedse/developer-roadmap.git synced 2025-09-03 06:12:53 +02:00

wip: questions parser

This commit is contained in:
Arik Chakma
2025-07-01 19:55:13 +06:00
parent abf58dabcd
commit 9423f45586
7 changed files with 369 additions and 14 deletions

View File

@@ -1,12 +1,10 @@
import { AITutorLayout } from '../AITutor/AITutorLayout';
import { AIQuizLayout } from './AIQuizLayout';
import { GenerateAIQuiz } from './GenerateAIQuiz';
export function AIQuiz() {
return (
<AITutorLayout
wrapperClassName="flex-row p-0 lg:p-0 relative overflow-hidden bg-white"
containerClassName="h-[calc(100vh-49px)] overflow-hidden relative"
>
<h2>AI Quiz</h2>
</AITutorLayout>
<AIQuizLayout>
<GenerateAIQuiz />
</AIQuizLayout>
);
}

View File

@@ -93,6 +93,8 @@ export function AIQuizGenerator() {
clearQuestionAnswerChatMessages();
sessionId = storeQuestionAnswerChatMessages(questionAnswerChatMessages);
}
window.location.href = `/ai/quiz/search?term=${title}&format=${selectedFormat}&id=${sessionId}`;
};
useEffect(() => {

View File

@@ -8,6 +8,7 @@ export function AIQuizLayout(props: AIQuizLayoutProps) {
const { children } = props;
return (
<AITutorLayout
activeTab="quiz"
wrapperClassName="flex-row p-0 lg:p-0 relative overflow-hidden bg-white"
containerClassName="h-[calc(100vh-49px)] overflow-hidden relative"
>

View File

@@ -0,0 +1,110 @@
import { useEffect, useRef, useState } from 'react';
import { getUrlParams } from '../../lib/browser';
import { isLoggedIn } from '../../lib/jwt';
import { queryClient } from '../../stores/query-client';
import { LoadingChip } from '../LoadingChip';
import type { QuestionAnswerChatMessage } from '../ContentGenerator/QuestionAnswerChat';
import { getQuestionAnswerChatMessages } from '../../lib/ai-questions';
import { aiRoadmapOptions, generateAIRoadmap } from '../../queries/ai-roadmap';
import { generateAIQuiz, type QuizQuestion } from '../../queries/ai-quiz';
type GenerateAIQuizProps = {
onQuizSlugChange?: (quizSlug: string) => void;
};
export function GenerateAIQuiz(props: GenerateAIQuizProps) {
const { onQuizSlugChange } = props;
const [isLoading, setIsLoading] = useState(true);
const [isStreaming, setIsStreaming] = useState(false);
const [error, setError] = useState('');
const [questions, setQuestions] = useState<QuizQuestion[]>([]);
useEffect(() => {
const params = getUrlParams();
const paramsTerm = params?.term;
const paramsFormat = params?.format;
const paramsSrc = params?.src || 'search';
if (!paramsTerm) {
return;
}
let questionAndAnswers: QuestionAnswerChatMessage[] = [];
const sessionId = params?.id;
if (sessionId) {
questionAndAnswers = getQuestionAnswerChatMessages(sessionId);
}
handleGenerateDocument({
term: paramsTerm,
format: paramsFormat,
src: paramsSrc,
questionAndAnswers,
});
}, []);
const handleGenerateDocument = async (options: {
term: string;
format: string;
isForce?: boolean;
prompt?: string;
src?: string;
questionAndAnswers?: QuestionAnswerChatMessage[];
}) => {
const { term, format, isForce, prompt, src, questionAndAnswers } = options;
if (!isLoggedIn()) {
window.location.href = '/ai';
return;
}
await generateAIQuiz({
term,
format,
isForce,
prompt,
questionAndAnswers,
onDetailsChange: (details) => {
// const { quizId, quizSlug, title, userId } = details;
// const aiRoadmapData = {
// _id: quizId,
// userId,
// title,
// term,
// data: content,
// questionAndAnswers,
// viewCount: 0,
// svgHtml: svgRef.current || '',
// lastVisitedAt: new Date(),
// createdAt: new Date(),
// updatedAt: new Date(),
// };
// queryClient.setQueryData(
// aiRoadmapOptions(roadmapSlug).queryKey,
// aiRoadmapData,
// );
// onQuizSlugChange?.(roadmapSlug);
// window.history.replaceState(null, '', `/ai-roadmaps/${roadmapSlug}`);
},
onLoadingChange: setIsLoading,
onError: setError,
onStreamingChange: setIsStreaming,
onQuestionsChange: setQuestions,
});
};
if (error) {
return <div className="text-red-500">{error}</div>;
}
if (isLoading) {
return (
<div className="flex h-full w-full items-center justify-center">
<LoadingChip message="Please wait..." />
</div>
);
}
return <div>GenerateAIQuiz</div>;
}

View File

@@ -0,0 +1,15 @@
---
import { AIQuiz } from '../../../components/AIQuiz/AIQuiz';
import SkeletonLayout from '../../../layouts/SkeletonLayout.astro';
---
<SkeletonLayout
title='AI Quiz'
briefTitle='AI Quiz'
description='AI Quiz'
keywords={['ai', 'quiz', 'education', 'learning']}
canonicalUrl='/ai/quiz'
noIndex={true}
>
<AIQuiz client:load />
</SkeletonLayout>

231
src/queries/ai-quiz.ts Normal file
View File

@@ -0,0 +1,231 @@
import { nanoid } from 'nanoid';
import type { QuestionAnswerChatMessage } from '../components/ContentGenerator/QuestionAnswerChat';
import { readChatStream } from '../lib/chat';
import { queryClient } from '../stores/query-client';
import { getAiCourseLimitOptions } from './ai-course';
type QuizDetails = {
quizId: string;
quizSlug: string;
userId: string;
title: string;
};
type GenerateAIQuizOptions = {
term: string;
format: string;
isForce?: boolean;
prompt?: string;
questionAndAnswers?: QuestionAnswerChatMessage[];
quizSlug?: string;
onQuestionsChange?: (questions: QuizQuestion[]) => void;
onDetailsChange?: (details: QuizDetails) => void;
onLoadingChange?: (isLoading: boolean) => void;
onStreamingChange?: (isStreaming: boolean) => void;
onError?: (error: string) => void;
onFinish?: () => void;
};
export async function generateAIQuiz(options: GenerateAIQuizOptions) {
const {
term,
format,
quizSlug,
onLoadingChange,
onError,
isForce = false,
prompt,
onDetailsChange,
onFinish,
questionAndAnswers,
onStreamingChange,
onQuestionsChange,
} = options;
onLoadingChange?.(true);
onStreamingChange?.(false);
try {
let response = null;
if (quizSlug && isForce) {
response = await fetch(
`${import.meta.env.PUBLIC_API_URL}/v1-regenerate-ai-quiz/${quizSlug}`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include',
body: JSON.stringify({
prompt,
}),
},
);
} else {
response = await fetch(
`${import.meta.env.PUBLIC_API_URL}/v1-generate-ai-quiz`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
keyword: term,
format,
isForce,
customPrompt: prompt,
questionAndAnswers,
}),
credentials: 'include',
},
);
}
if (!response.ok) {
const data = await response.json();
console.error(
'Error generating quiz:',
data?.message || 'Something went wrong',
);
onLoadingChange?.(false);
onError?.(data?.message || 'Something went wrong');
return;
}
const stream = response.body;
if (!stream) {
console.error('Failed to get stream from response');
onError?.('Something went wrong');
onLoadingChange?.(false);
return;
}
onLoadingChange?.(false);
onStreamingChange?.(true);
await readChatStream(stream, {
onMessage: async (message) => {
const questions = generateAiQuizQuestions(message);
console.log(questions);
onQuestionsChange?.(questions);
},
onMessageEnd: async (result) => {
console.log('FINAL RESULT:', result);
queryClient.invalidateQueries(getAiCourseLimitOptions());
onStreamingChange?.(false);
},
onDetails: async (details) => {
if (!details?.quizId || !details?.quizSlug) {
throw new Error('Invalid details');
}
onDetailsChange?.(details);
},
});
onFinish?.();
} catch (error: any) {
onError?.(error?.message || 'Something went wrong');
console.error('Error in quiz generation:', error);
onLoadingChange?.(false);
onStreamingChange?.(false);
}
}
export type QuizQuestion = {
id: string;
title: string;
type: 'mcq' | 'open-ended';
options: {
id: string;
title: string;
isCorrect: boolean;
}[];
answerExplanation?: string;
};
export function generateAiQuizQuestions(questionData: string): QuizQuestion[] {
const questions: QuizQuestion[] = [];
const lines = questionData.split('\n').map((line) => line.trim());
let currentQuestion: QuizQuestion | null = null;
let context: 'question' | 'explanation' | 'option' | null = null;
const addCurrentQuestion = () => {
if (!currentQuestion) {
return;
}
questions.push(currentQuestion);
currentQuestion = null;
};
for (const line of lines) {
if (!line) {
continue;
}
if (line.startsWith('###')) {
addCurrentQuestion();
currentQuestion = {
id: nanoid(),
title: line.slice(3).trim(),
type: 'open-ended',
options: [],
};
context = 'question';
} else if (line.startsWith('##')) {
if (!currentQuestion) {
continue;
}
currentQuestion.answerExplanation = line.slice(2).trim();
context = 'explanation';
} else if (line.startsWith('#')) {
addCurrentQuestion();
const title = line.slice(1).trim();
currentQuestion = {
id: nanoid(),
title,
type: 'mcq',
options: [],
};
context = 'question';
} else if (line.startsWith('-')) {
if (!currentQuestion) {
continue;
}
const rawOption = line.slice(1).trim();
const isCorrect = rawOption.startsWith('*');
const title = rawOption.slice(isCorrect ? 1 : 0).trim();
currentQuestion.options.push({
id: nanoid(),
title,
isCorrect,
});
context = 'option';
} else {
if (!currentQuestion) {
continue;
}
if (context === 'question') {
currentQuestion.title += `\n${line}`;
} else if (context === 'explanation') {
currentQuestion.answerExplanation =
(currentQuestion?.answerExplanation || '') + `\n${line}`;
} else if (context === 'option') {
const lastOption = currentQuestion.options.at(-1);
if (lastOption) {
lastOption.title += `\n${line}`;
}
}
}
}
addCurrentQuestion();
return questions;
}

View File

@@ -2,6 +2,11 @@ import { queryOptions } from '@tanstack/react-query';
import { httpGet } from '../lib/query-http';
import { generateAICourseRoadmapStructure } from '../lib/ai';
import { generateAIRoadmapFromText, renderFlowJSON } from '@roadmapsh/editor';
import { queryClient } from '../stores/query-client';
import { getAiCourseLimitOptions } from '../queries/ai-course';
import { readChatStream } from '../lib/chat';
import type { QuestionAnswerChatMessage } from '../components/ContentGenerator/QuestionAnswerChat';
import { isLoggedIn } from '../lib/jwt';
export interface AIRoadmapDocument {
_id: string;
@@ -47,13 +52,6 @@ export function aiRoadmapOptions(roadmapSlug?: string) {
});
}
import { queryClient } from '../stores/query-client';
import { getAiCourseLimitOptions } from '../queries/ai-course';
import { readChatStream } from '../lib/chat';
import type { QuestionAnswerChatMessage } from '../components/ContentGenerator/QuestionAnswerChat';
import type { AIGuideDocument } from './ai-guide';
import { isLoggedIn } from '../lib/jwt';
type RoadmapDetails = {
roadmapId: string;
roadmapSlug: string;