1
0
mirror of https://github.com/kamranahmedse/developer-roadmap.git synced 2025-09-01 05:21:43 +02:00
This commit is contained in:
Arik Chakma
2025-07-03 02:07:48 +06:00
parent 9f15ca1e53
commit 920d0512f6
5 changed files with 184 additions and 44 deletions

View File

@@ -168,10 +168,11 @@ export function QuestionTitle(props: QuestionTitleProps) {
type QuestionExplanationProps = {
explanation: string;
title?: string;
};
export function QuestionExplanation(props: QuestionExplanationProps) {
const { explanation } = props;
const { explanation, title } = props;
const explanationHtml = markdownToHtml(explanation, false);
@@ -179,7 +180,7 @@ export function QuestionExplanation(props: QuestionExplanationProps) {
<div className="mt-4 rounded-xl bg-gray-100 p-4">
<p className="flex items-center gap-2 text-lg text-gray-600">
<InfoIcon className="size-4" />
Explanation
{title || 'Explanation'}
</p>
<div
className={cn(markdownClassName, 'mt-0.5')}

View File

@@ -1,17 +1,13 @@
import type { QuizQuestion } from '../../queries/ai-quiz';
import { type QuizQuestion } from '../../queries/ai-quiz';
import { cn } from '../../lib/classname';
import { InfoIcon, Loader2Icon } from 'lucide-react';
import { markdownToHtml } from '../../lib/markdown';
import { Loader2Icon } from 'lucide-react';
import { QuestionExplanation, QuestionTitle } from './AIMCQQuestion';
import { useState } from 'react';
import { useMutation } from '@tanstack/react-query';
import { httpPost } from '../../lib/query-http';
import { queryClient } from '../../stores/query-client';
import type { QuestionState } from './AIQuizContent';
import { useVerifyAnswer } from '../../hooks/use-verify-answer';
type VerifyQuizAnswerResponse = {
isCorrect?: boolean;
correctAnswer?: string;
export type VerifyQuizAnswerResponse = {
status: 'correct' | 'incorrect' | 'can_be_improved';
feedback: string;
};
type AIOpenEndedQuestionProps = {
@@ -46,41 +42,39 @@ export function AIOpenEndedQuestion(props: AIOpenEndedQuestionProps) {
} = questionState;
const {
mutate: verifyAnswer,
isPending: isVerifying,
data: verifyAnswerData,
} = useMutation(
{
mutationFn: (answer: string) => {
return httpPost<VerifyQuizAnswerResponse>(
`/v1-verify-quiz-answer/${quizSlug}`,
{
question: question.title,
userAnswer,
},
);
},
onSuccess: (data) => {
setCorrectAnswer(data.correctAnswer ?? '');
onSubmit?.(data.isCorrect ? 'correct' : 'incorrect');
},
},
queryClient,
);
verifyAnswer,
data: verificationData,
status: verifyStatus,
} = useVerifyAnswer({
quizSlug,
question: questionText,
userAnswer,
onFinish: (data) => {
if (!data || !data.status) {
console.error('No data or status', data);
onSubmit('incorrect');
return;
}
const handleSubmit = () => {
setCorrectAnswer(data.feedback || '');
onSubmit(data.status);
},
});
const handleSubmit = async () => {
if (isSubmitted) {
onNext?.();
return;
}
verifyAnswer(userAnswer);
await verifyAnswer();
};
const canSubmit = userAnswer.trim().length > 0;
const markdownClassName =
'prose prose-lg prose-p:text-lg prose-p:font-normal prose-p:my-0 prose-pre:my-0 prose-p:prose-code:text-base! prose-p:prose-code:px-2 prose-p:prose-code:py-0.5 prose-p:prose-code:rounded-lg prose-p:prose-code:border prose-p:prose-code:border-black text-left text-black';
const isVerifying =
verifyStatus === 'loading' || verifyStatus === 'streaming';
const feedback = verificationData?.feedback || correctAnswer;
const feedbackStatus = verificationData?.status || status;
return (
<div>
@@ -93,9 +87,14 @@ export function AIOpenEndedQuestion(props: AIOpenEndedQuestionProps) {
'focus:border-gray-400 focus:ring-0 focus:outline-none',
isSubmitted && 'bg-gray-50',
isSubmitted &&
status === 'correct' &&
feedbackStatus === 'correct' &&
'border-green-500 bg-green-50',
isSubmitted && status === 'incorrect' && 'border-red-500 bg-red-50',
isSubmitted &&
feedbackStatus === 'incorrect' &&
'border-red-500 bg-red-50',
isSubmitted &&
feedbackStatus === 'can_be_improved' &&
'border-yellow-500 bg-yellow-50',
)}
placeholder="Type your answer here..."
value={userAnswer}
@@ -104,8 +103,15 @@ export function AIOpenEndedQuestion(props: AIOpenEndedQuestionProps) {
/>
</div>
{!isVerifying && correctAnswer && (
<QuestionExplanation explanation={correctAnswer} />
{feedback && (
<QuestionExplanation
title={
feedbackStatus === 'can_be_improved'
? 'Can be improved'
: 'Feedback'
}
explanation={feedback}
/>
)}
<button

View File

@@ -14,7 +14,7 @@ export type QuestionState = {
selectedOptions?: number[];
userAnswer?: string;
correctAnswer?: string;
status: 'correct' | 'incorrect' | 'skipped' | 'pending';
status: 'correct' | 'incorrect' | 'skipped' | 'pending' | 'can_be_improved';
};
const DEFAULT_QUESTION_STATE: QuestionState = {

View File

@@ -0,0 +1,133 @@
import { useCallback, useRef, useState } from 'react';
import { removeAuthToken } from '../lib/jwt';
import { readChatStream } from '../lib/chat';
import { flushSync } from 'react-dom';
import type { VerifyQuizAnswerResponse } from '../components/AIQuiz/AIOpenEndedQuestion';
type VerifyAnswerResponse = {
status?: VerifyQuizAnswerResponse['status'];
feedback?: string;
};
type UseVerifyAnswerOptions = {
quizSlug: string;
question: string;
userAnswer: string;
onError?: (error: Error) => void;
onFinish?: (data: VerifyAnswerResponse) => void;
};
export function useVerifyAnswer(options: UseVerifyAnswerOptions) {
const { quizSlug, question, userAnswer, onError, onFinish } = options;
const abortControllerRef = useRef<AbortController | null>(null);
const contentRef = useRef<VerifyAnswerResponse | null>(null);
const [data, setData] = useState<VerifyAnswerResponse | null>(null);
const [status, setStatus] = useState<
'idle' | 'streaming' | 'loading' | 'ready' | 'error'
>('idle');
const verifyAnswer = useCallback(async () => {
try {
setStatus('loading');
abortControllerRef.current?.abort();
abortControllerRef.current = new AbortController();
const response = await fetch(
`${import.meta.env.PUBLIC_API_URL}/v1-verify-quiz-answer/${quizSlug}`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ question, userAnswer }),
signal: abortControllerRef.current?.signal,
credentials: 'include',
},
);
if (!response.ok) {
const data = await response.json();
setStatus('error');
if (data.status === 401) {
removeAuthToken();
window.location.reload();
}
throw new Error(data?.message || 'Something went wrong');
}
const stream = response.body;
if (!stream) {
setStatus('error');
throw new Error('Something went wrong');
}
await readChatStream(stream, {
onMessage: async (content) => {
flushSync(() => {
setStatus('streaming');
contentRef.current = parseVerifyAIQuizAnswerResponse(content);
setData(contentRef.current);
});
},
onMessageEnd: async () => {
flushSync(() => {
setStatus('ready');
});
},
});
setStatus('idle');
abortControllerRef.current = null;
if (!contentRef.current) {
setStatus('error');
throw new Error('Something went wrong');
}
onFinish?.(contentRef.current);
} catch (error) {
if (abortControllerRef.current?.signal.aborted) {
// we don't want to show error if the user stops the chat
// so we just return
return;
}
onError?.(error as Error);
setStatus('error');
}
}, [quizSlug, question, userAnswer, onError]);
const stop = useCallback(() => {
if (!abortControllerRef.current) {
return;
}
abortControllerRef.current.abort();
abortControllerRef.current = null;
}, []);
return {
data,
status,
stop,
verifyAnswer,
};
}
export function parseVerifyAIQuizAnswerResponse(
response: string,
): VerifyQuizAnswerResponse {
const statusRegex = /<status>(.*?)<\/status>/;
const status = response.match(statusRegex)?.[1]?.trim();
const responseWithoutStatus = response.replace(statusRegex, '').trim();
return {
status: status as VerifyQuizAnswerResponse['status'],
feedback: responseWithoutStatus,
};
}

View File

@@ -5,6 +5,7 @@ import { queryClient } from '../stores/query-client';
import { getAiCourseLimitOptions } from './ai-course';
import { queryOptions } from '@tanstack/react-query';
import { httpGet } from '../lib/query-http';
import type { VerifyQuizAnswerResponse } from '../components/AIQuiz/AIOpenEndedQuestion';
type QuizDetails = {
quizId: string;
@@ -113,7 +114,6 @@ export async function generateAIQuiz(options: GenerateAIQuizOptions) {
onQuestionsChange?.(questions);
},
onMessageEnd: async (result) => {
console.log('FINAL RESULT:', result);
queryClient.invalidateQueries(getAiCourseLimitOptions());
onStreamingChange?.(false);
},