mirror of
https://github.com/kamranahmedse/developer-roadmap.git
synced 2025-09-01 05:21:43 +02:00
wip
This commit is contained in:
@@ -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')}
|
||||
|
@@ -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
|
||||
|
@@ -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 = {
|
||||
|
133
src/hooks/use-verify-answer.ts
Normal file
133
src/hooks/use-verify-answer.ts
Normal 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,
|
||||
};
|
||||
}
|
@@ -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);
|
||||
},
|
||||
|
Reference in New Issue
Block a user