const { useState, useEffect } = React; const renderTextWithBold = (text) => { if (!text) return null; const segments = text.split(/(\*\*[^*]+\*\*)/g); return segments.map((segment, index) => { const match = segment.match(/^\*\*(.+)\*\*$/); if (match) { return ( {match[1]} ); } return {segment}; }); }; const App = () => { const [topic, setTopic] = useState(() => { if (typeof window === "undefined") return ""; try { const storedTopic = window.sessionStorage.getItem("kc_preloaded_topic"); if (storedTopic) return storedTopic; } catch { // ignore storage issues } return ""; }); const [plan, setPlan] = useState(null); const [currentStepIndex, setCurrentStepIndex] = useState(-1); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const [mode, setMode] = useState("idle"); // idle | overview | slides | summary const [secondsElapsed, setSecondsElapsed] = useState(0); const [isSpeaking, setIsSpeaking] = useState(false); const [quizMode, setQuizMode] = useState(false); const [quizQuestionIndex, setQuizQuestionIndex] = useState(0); const [quizSelectedIndex, setQuizSelectedIndex] = useState(null); const [quizFeedback, setQuizFeedback] = useState(null); const [completedQuizzes, setCompletedQuizzes] = useState({}); const [planReady, setPlanReady] = useState(false); const [loadingHintIndex, setLoadingHintIndex] = useState(0); const [quizResults, setQuizResults] = useState({}); const [courseCompleted, setCourseCompleted] = useState(false); const [stepCorrectCount, setStepCorrectCount] = useState(0); const [showStepScore, setShowStepScore] = useState(false); const [reviewMode, setReviewMode] = useState(false); const [quizAnswers, setQuizAnswers] = useState({}); const [bootstrapDone, setBootstrapDone] = useState(false); const loadingHints = [ "Your learning coach is sketching the clearest path for this topic…", "We’re picking the key ideas so you don’t have to read long, boring explanations…", "We’re shaping mini quizzes that check real understanding, not just memorisation…", ]; const handleGeneratePlan = () => { if (!topic.trim()) { setError("Please enter a topic."); setPlan(null); return; } setError(null); setIsLoading(true); setPlan(null); setCurrentStepIndex(-1); setMode("idle"); setQuizMode(false); setQuizQuestionIndex(0); setQuizSelectedIndex(null); setQuizFeedback(null); setCompletedQuizzes({}); setPlanReady(false); setQuizResults({}); setCourseCompleted(false); setStepCorrectCount(0); setShowStepScore(false); setReviewMode(false); setQuizAnswers({}); let apiBase = ""; if (typeof window !== "undefined" && window.KC_API_BASE_PATH) { apiBase = window.KC_API_BASE_PATH.replace(/\/$/, ""); } const apiPath = `${apiBase || ""}/learning-plan`; fetch(apiPath, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ topic: topic.trim() }), }) .then(async (res) => { if (!res.ok) { const data = await res.json().catch(() => ({})); const message = data.error || "Something went wrong while creating your plan."; throw new Error(message); } return res.json(); }) .then((data) => { setPlan(data); setCurrentStepIndex(-1); // Go straight to the lesson plan overview once it is ready. setMode("overview"); setSecondsElapsed(0); setQuizMode(false); setQuizQuestionIndex(0); setQuizSelectedIndex(null); setQuizFeedback(null); setCompletedQuizzes({}); setPlanReady(true); }) .catch((err) => { console.error(err); setError(err.message); }) .finally(() => { setIsLoading(false); }); }; const handleNewLearning = () => { setTopic(""); setPlan(null); setCurrentStepIndex(-1); setMode("idle"); setIsLoading(false); setError(null); setSecondsElapsed(0); setIsSpeaking(false); setQuizMode(false); setQuizQuestionIndex(0); setQuizSelectedIndex(null); setQuizFeedback(null); setCompletedQuizzes({}); setPlanReady(false); setQuizResults({}); setCourseCompleted(false); setStepCorrectCount(0); setShowStepScore(false); setReviewMode(false); setQuizAnswers({}); }; const selectedStep = plan && currentStepIndex >= 0 && plan.steps?.[currentStepIndex] ? plan.steps[currentStepIndex] : null; const totalSteps = plan?.steps?.length || 0; const progressPercent = mode === "slides" && totalSteps > 0 && currentStepIndex >= 0 ? ((currentStepIndex + 1) / totalSteps) * 100 : 0; const stepCount = plan?.steps?.length || 0; const totalQuizQuestions = stepCount * 2; const handleStartSlides = () => { if (!plan || !plan.steps || plan.steps.length === 0) return; setCurrentStepIndex(0); setMode("slides"); setQuizMode(false); setQuizQuestionIndex(0); setQuizSelectedIndex(null); setQuizFeedback(null); setShowStepScore(false); setReviewMode(false); }; const handlePlayAudio = () => { if (!selectedStep || !selectedStep.content) return; if (typeof window === "undefined" || !window.speechSynthesis) { setError("Audio playback is not supported in this browser."); return; } const synth = window.speechSynthesis; synth.cancel(); const utterance = new SpeechSynthesisUtterance(selectedStep.content); utterance.onend = () => setIsSpeaking(false); utterance.onerror = () => setIsSpeaking(false); setIsSpeaking(true); synth.speak(utterance); }; useEffect(() => { if (bootstrapDone) return; if (typeof window === "undefined") return; setBootstrapDone(true); try { const storedPlan = window.sessionStorage.getItem("kc_preloaded_plan"); const storedTopic = window.sessionStorage.getItem("kc_preloaded_topic"); if (storedTopic && !topic) { setTopic(storedTopic); } if (storedPlan) { const parsed = JSON.parse(storedPlan); setPlan(parsed); setCurrentStepIndex(-1); setMode("overview"); setSecondsElapsed(0); setQuizMode(false); setQuizQuestionIndex(0); setQuizSelectedIndex(null); setQuizFeedback(null); setCompletedQuizzes({}); setPlanReady(true); window.sessionStorage.removeItem("kc_preloaded_plan"); window.sessionStorage.removeItem("kc_preloaded_topic"); return; } } catch (e) { // If anything goes wrong, we fall back to normal manual flow. console.error(e); } }, [bootstrapDone, topic]); const handleStopAudio = () => { if (typeof window === "undefined" || !window.speechSynthesis) return; window.speechSynthesis.cancel(); setIsSpeaking(false); }; useEffect(() => { if (mode !== "slides") return; const id = setInterval(() => { setSecondsElapsed((prev) => prev + 1); }, 1000); return () => clearInterval(id); }, [mode]); const formatSeconds = (totalSeconds) => { const minutes = Math.floor(totalSeconds / 60) .toString() .padStart(2, "0"); const seconds = (totalSeconds % 60).toString().padStart(2, "0"); return `${minutes}:${seconds}`; }; const questions = selectedStep?.quiz?.questions || []; const currentQuestion = quizMode && questions.length > 0 ? questions[Math.min(quizQuestionIndex, questions.length - 1)] : null; const stepKey = selectedStep && (selectedStep.id || String(currentStepIndex)); const hasQuiz = questions.length > 0; const isQuizDoneForStep = hasQuiz ? !!(stepKey && completedQuizzes[stepKey]) : true; const stepResult = stepKey ? quizResults[stepKey] : null; const isLastStep = plan && plan.steps && currentStepIndex === plan.steps.length - 1; useEffect(() => { if (!isLoading) { setLoadingHintIndex(0); return; } const id = setInterval(() => { setLoadingHintIndex((prev) => (prev + 1) % loadingHints.length); }, 3500); return () => clearInterval(id); }, [isLoading]); const scoreSummary = (() => { if (!plan || !plan.steps || plan.steps.length === 0) return null; let totalCorrect = 0; let totalQuestions = 0; const perStep = plan.steps.map((step, index) => { const key = step.id || String(index); const result = quizResults[key]; if (result) { totalCorrect += result.correct || 0; totalQuestions += result.total || 0; } return { title: step.title, correct: result?.correct || 0, total: result?.total || 0, }; }); if (totalQuestions === 0) { return null; } return { totalCorrect, totalQuestions, perStep }; })(); const handleNextOrComplete = () => { if (!plan?.steps || currentStepIndex >= plan.steps.length - 1) { setMode("summary"); setCurrentStepIndex(-1); setQuizMode(false); setQuizQuestionIndex(0); setQuizSelectedIndex(null); setQuizFeedback(null); setCourseCompleted(true); } else { setCurrentStepIndex((idx) => idx + 1); setQuizMode(false); setQuizQuestionIndex(0); setQuizSelectedIndex(null); setQuizFeedback(null); } setShowStepScore(false); setReviewMode(false); }; return (
{/* Save your logo image as "logo.png" in this folder */} Krit.club logo

Welcome to Kritclub Learning coach

A playful space where ideas light up, learning feels magical, and every session turns curiosity into clarity.

{mode === "idle" && (

Start your learning journey

Enter any concept you're curious about, and we'll turn it into a{" "} short, guided lesson with clear topics, audio explanations, and quick quizzes so you can check your understanding as you go.

Here's how your mini-lesson will work:

  • We'll pick a few focused topics so you don't feel overwhelmed.
  • For each topic, you can read the explanation or{" "} tap play to listen like a mini audio lesson.
  • At the end of every topic, a short quiz will help you see what you've really understood.
  • You'll see your progress as you move from Topic 1 all the way to the end of your 15-minute journey.

When you're ready, type what you want to learn and{" "} create your learning plan.

setTopic(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter" && !isLoading) { e.preventDefault(); handleGeneratePlan(); } }} />
{isLoading && (

Creating your personalised 15-minute learning path {topic.trim() ? ` for “${topic.trim()}”` : ""}…

)} {error &&
{error}
} {/* No secondary “plan ready” screen here anymore; we jump straight to overview. */}
)} {plan && mode === "slides" && (

{plan.topic || topic}

🕒 Plan time {plan.totalDurationMinutes} min ⏱ {formatSeconds(secondsElapsed)}
)} {plan && mode === "overview" && (

{plan.topic || topic}

)} {plan && mode === "overview" && (
{planReady && (
✨ Your learning plan is ready!
Review the topics below, then click{" "} Start learning to move through each topic, listen to audio explanations, and take short quizzes.
)}

Learning plan for {plan.topic || topic}

{renderTextWithBold(plan.overview)}

{plan.totalDurationMinutes && (

Total time:{" "} {plan.totalDurationMinutes} min

)}

Here are the topics we'll cover in this quick session:

{plan.steps && plan.steps.length > 0 && (
    {plan.steps.map((step, index) => (
  • {index + 1} {step.funIcon ? `${step.funIcon} ` : ""} {step.title}
    {step.summary && (

    {step.summary}

    )}

    ~{step.durationMinutes} min • Key idea: {step.keyIdea}

  • ))}
)}
)} {plan && mode === "slides" && selectedStep && (
Topic {currentStepIndex + 1} of {totalSteps}
{selectedStep.funIcon ? `${selectedStep.funIcon} ` : ""} {selectedStep.title} ~{selectedStep.durationMinutes} min
{!quizMode && !showStepScore && !reviewMode && ( <>
{selectedStep.content && (

{renderTextWithBold(selectedStep.content)}

)} {selectedStep.codeExample && selectedStep.codeExample.trim() && (
                          {selectedStep.codeExample}
                        
)}

Try this: {selectedStep.activityPrompt}

{questions.length > 0 && !completedQuizzes[stepKey] && (
)} {questions.length > 0 && completedQuizzes[stepKey] && (

✅ Quiz completed for this lesson.

)} )} {quizMode && currentQuestion && (

Question {quizQuestionIndex + 1} of {questions.length} {currentQuestion.question}

{currentQuestion.options.map((opt, idx) => { const isSelected = quizSelectedIndex === idx; let cls = "kc-quiz-option"; if (quizFeedback) { if (idx === currentQuestion.correctOptionIndex) { cls += " kc-quiz-option--correct"; } else if (isSelected) { cls += " kc-quiz-option--incorrect"; } } else if (isSelected) { cls += " kc-quiz-option--selected"; } return ( ); })}
{quizFeedback && ( <>

{quizFeedback.correct ? "Correct! " : "Not quite. "} {quizFeedback.explanation}

{quizFeedback.correct ? ( <> Awesome! You nailed this one. ) : ( <> That's okay — use the explanation above and you'll crack the next one. )}
{quizFeedback.correct && ( )} )}
)} {!quizMode && reviewMode && questions.length > 0 && (

Review quiz for Lesson {currentStepIndex + 1}

Here's a quick look back at the questions for{" "} {selectedStep.title}. Answers are locked in, so this is just for review.

{(() => { const q = questions[Math.min(quizQuestionIndex, questions.length - 1)]; const answersForStep = stepKey ? quizAnswers[stepKey] || {} : {}; const chosenIndex = answersForStep[quizQuestionIndex]; if (!q) return null; return (

Question {quizQuestionIndex + 1} of {questions.length} {q.question}

{q.options.map((opt, idx) => { let cls = "kc-quiz-option"; if (idx === q.correctOptionIndex) { cls += " kc-quiz-option--correct"; } else if ( chosenIndex !== undefined && idx === chosenIndex && idx !== q.correctOptionIndex ) { cls += " kc-quiz-option--incorrect"; } else if (idx === chosenIndex) { cls += " kc-quiz-option--selected"; } return (
{String.fromCharCode(65 + idx)}. {opt}
); })}
{q.explanation && (

{q.explanation}

)}
); })()}
)} {!quizMode && showStepScore && stepResult && (

Lesson {currentStepIndex + 1} quiz summary

You've just finished the check-in for{" "} {selectedStep.title}.

Your score for this lesson

{stepResult.correct} / {stepResult.total} {" "} questions answered correctly.

{stepResult.correct === 0 ? ( <> 😔 This quiz didn't go as planned, and{" "} that's okay. Use the explanations and Review quiz option to see what clicked and what didn't—this is exactly how deep understanding is built. ) : stepResult.correct === stepResult.total ? ( <> ✨ Brilliant! You aced this lesson's quiz. If it feels clear, you're ready to move on—or do a quick review if you'd like. ) : ( <> 👏 Nice work! You've built a solid understanding of this lesson. You can review the quiz for clarity, then flow into the next topic when you're ready. )}

)}
{currentStepIndex > 0 && ( )} {!hasQuiz && ( )}
)} {plan && mode === "summary" && courseCompleted && scoreSummary && (
Krit.club logo

Kritclub Learning Coach

Certificate of Quick Learning

This certifies that Kritclub Explorer has completed the mini-course on {plan.topic || topic} and stayed curious for a focused{" "} 15-minute learning journey.

Overall score:{" "} {scoreSummary.totalCorrect} / {scoreSummary.totalQuestions}

Topic-wise score

    {scoreSummary.perStep.map((row, idx) => (
  • {row.title} {row.correct} / {row.total}
  • ))}
krit.club — Learning Coach
)}
); }; const root = ReactDOM.createRoot(document.getElementById("root")); root.render();