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:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
@@ -93,6 +93,8 @@ export function AIQuizGenerator() {
|
||||
clearQuestionAnswerChatMessages();
|
||||
sessionId = storeQuestionAnswerChatMessages(questionAnswerChatMessages);
|
||||
}
|
||||
|
||||
window.location.href = `/ai/quiz/search?term=${title}&format=${selectedFormat}&id=${sessionId}`;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
|
@@ -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"
|
||||
>
|
||||
|
110
src/components/AIQuiz/GenerateAIQuiz.tsx
Normal file
110
src/components/AIQuiz/GenerateAIQuiz.tsx
Normal 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>;
|
||||
}
|
15
src/pages/ai/quiz/search.astro
Normal file
15
src/pages/ai/quiz/search.astro
Normal 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
231
src/queries/ai-quiz.ts
Normal 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;
|
||||
}
|
@@ -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;
|
||||
|
Reference in New Issue
Block a user