Skip to content

Commit 31d8c39

Browse files
source /home/dave/GPTeasers/backend/.venv/bin/activate
Merge branch 'main' of github.com:DJSaunders1997/GPTeasers
2 parents fbff0a8 + f23e3a0 commit 31d8c39

8 files changed

Lines changed: 696 additions & 126 deletions

File tree

frontend/index.html

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,18 @@ <h1>GPTeasers 🧠💡</h1>
7979
/>
8080
</div>
8181

82+
<div class="input-group">
83+
<label for="quizNumQuestions">Number of Questions:</label>
84+
<p>How many questions should this quiz have?</p>
85+
<input
86+
type="number"
87+
id="quizNumQuestions"
88+
min="1"
89+
max="10"
90+
value="5"
91+
/>
92+
</div>
93+
8294
<div class="input-group">
8395
<label for="quizModel">Select Model Provider:</label>
8496
<p>Select the AI model to generate your quiz.</p>
@@ -109,18 +121,32 @@ <h1>GPTeasers 🧠💡</h1>
109121

110122
<div id="quiz-container" style="display: none;">
111123
<h1 id="quizTitle">Title</h1>
124+
<div id="quizMeta" class="quiz-meta" aria-live="polite">
125+
<span id="quizProgress">Question 1 of 10</span>
126+
<span id="quizScore">Score: 0</span>
127+
</div>
112128
<h2 id="question-text">Question</h2>
113129

130+
<div id="quizFeedback" class="quiz-feedback" role="status" aria-live="polite" style="display: none;"></div>
131+
114132
<button id="option-A">A</button>
115133
<button id="option-B">B</button>
116134
<button id="option-C">C</button>
135+
<button id="nextQuestionButton" style="display: none;">Next Question →</button>
136+
<button id="newQuizButton" style="display: none;">New Quiz</button>
117137
</div>
118138

119139
<div id="questionsContainer">
120140
<!-- Questions will be dynamically added here -->
121141
</div>
122142

123143
</section>
144+
145+
<section id="quizHistory" aria-label="Quiz history" style="display: none;">
146+
<h2>Your Quiz History</h2>
147+
<p id="historyEmptyMessage">No quiz attempts yet.</p>
148+
<ul id="quizHistoryList"></ul>
149+
</section>
124150

125151
<footer>Website by David Saunders.</footer>
126152
</main>

frontend/scripts/app.js

Lines changed: 131 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -8,45 +8,52 @@ import Quiz from "./quiz.js";
88

99
class App {
1010
constructor() {
11-
const numQuestions = 10;
1211
// Initialise app elements as JS objects.
13-
this.quiz = new Quiz(numQuestions);
14-
this.controller = new Controller(this.quiz);
12+
this.quiz = null; // created on demand per quiz run
13+
this.controller = new Controller();
1514
this.ui = new UI();
15+
this.quizHistory = this.loadQuizHistory();
16+
this.currentTopic = "";
17+
this.currentDifficulty = "";
18+
this.currentModel = "";
19+
20+
// Render any existing history on load
21+
this.ui.renderHistory(this.quizHistory);
1622

1723
// Initialize mobile menu toggle
1824
this.initMobileMenu();
1925

2026
// Load supported models dynamically
2127
this.loadSupportedModels();
2228

23-
// Initialise button event listeners.
24-
// The arrow function implicitly binds the method to the current instance of this class.
25-
document
26-
.querySelector("#fetchQuizButton")
27-
.addEventListener("click", () => this.fetchQuizData());
28-
document
29-
.querySelector("#fetchQuizButton")
30-
.addEventListener("click", () => this.fetchAIImage());
31-
// Answer buttons aren't visible when the page first loads
32-
document
33-
.querySelector("#option-A")
34-
.addEventListener("click", () => this.checkAnswer("A"));
35-
document
36-
.querySelector("#option-B")
37-
.addEventListener("click", () => this.checkAnswer("B"));
38-
document
39-
.querySelector("#option-C")
40-
.addEventListener("click", () => this.checkAnswer("C"));
41-
42-
// If Enter key is pressed then simulate button press
43-
document
44-
.getElementById("quizTopic")
45-
.addEventListener("keydown", function (event) {
46-
// Check if the pressed key was the Enter key
47-
if (event.key === "Enter") {
48-
event.preventDefault(); // Prevent any default action
49-
document.querySelector("#fetchQuizButton").click(); // Simulate a click on the button
29+
// Initialise button event listeners
30+
this.bindButtonEvents();
31+
this.bindEnterKeyToQuizButton();
32+
}
33+
34+
/**
35+
* Bind event listeners to quiz control buttons.
36+
* @private
37+
*/
38+
bindButtonEvents() {
39+
this.ui.elements.fetchButton.addEventListener("click", () => this.fetchQuizData());
40+
this.ui.elements.fetchButton.addEventListener("click", () => this.fetchAIImage());
41+
this.ui.elements.buttonA.addEventListener("click", () => this.checkAnswer("A"));
42+
this.ui.elements.buttonB.addEventListener("click", () => this.checkAnswer("B"));
43+
this.ui.elements.buttonC.addEventListener("click", () => this.checkAnswer("C"));
44+
this.ui.elements.nextQuestionButton.addEventListener("click", () => this.nextQuestion());
45+
this.ui.elements.newQuizButton.addEventListener("click", () => location.reload());
46+
}
47+
48+
/**
49+
* Bind Enter key on quiz topic input to trigger quiz generation.
50+
* @private
51+
*/
52+
bindEnterKeyToQuizButton() {
53+
this.ui.elements.topicInput.addEventListener("keydown", (event) => {
54+
if (event.key === "Enter") {
55+
event.preventDefault();
56+
this.ui.elements.fetchButton.click();
5057
}
5158
});
5259
}
@@ -55,8 +62,8 @@ class App {
5562
* Initialize mobile menu toggle functionality
5663
*/
5764
initMobileMenu() {
58-
const navbarToggle = document.getElementById('navbar-toggle');
59-
const navbarLinks = document.getElementById('navbar-links');
65+
const navbarToggle = this.ui.elements.navbarToggle;
66+
const navbarLinks = this.ui.elements.navbarLinks;
6067

6168
if (navbarToggle && navbarLinks) {
6269
navbarToggle.addEventListener('click', () => {
@@ -89,6 +96,17 @@ class App {
8996
const topic = this.ui.getTopic();
9097
const difficulty = this.ui.getDifficulty();
9198
const model = this.ui.getModel();
99+
const numQuestions = this.ui.getNumQuestions();
100+
101+
// Persist current quiz metadata for history logging
102+
this.currentTopic = topic;
103+
this.currentDifficulty = difficulty;
104+
this.currentModel = model;
105+
106+
// Create fresh quiz state with requested number of questions
107+
this.quiz = new Quiz(numQuestions);
108+
this.controller.quiz = this.quiz;
109+
this.controller.numQuestions = numQuestions;
92110

93111
// Check if topic is empty or contains only whitespace
94112
if (!topic.trim()) {
@@ -128,7 +146,7 @@ class App {
128146

129147
async fetchAIImage() {
130148
// Use the topic as a prompt to image
131-
const prompt = document.getElementById("quizTopic").value;
149+
const prompt = this.ui.elements.topicInput.value;
132150

133151
// Check if prompt is empty or contains only whitespace
134152
if (!prompt.trim()) {
@@ -159,12 +177,39 @@ class App {
159177
const question = this.quiz.getCurrentQuestion();
160178
// Update UI elements
161179
this.ui.displayCurrentQuestion(question);
180+
this.ui.updateProgress(this.quiz.currentIndex + 1, this.quiz.numQuestions, this.quiz.score);
162181
}
163182

164183
// Calls quiz check answer method
165184
// and displays the next question
185+
/**
186+
* Handles answer selection, renders inline feedback, and advances quiz flow.
187+
* @param {"A"|"B"|"C"} answer - Selected option key.
188+
*/
166189
checkAnswer(answer) {
167-
this.quiz.checkAnswer(answer);
190+
const result = this.quiz.checkAnswer(answer);
191+
192+
if (!result) {
193+
return;
194+
}
195+
196+
if (result.isFinished) {
197+
this.saveQuizResult(result);
198+
this.ui.showFinalScore(result);
199+
this.ui.updateProgress(this.quiz.numQuestions, this.quiz.numQuestions, this.quiz.score);
200+
return;
201+
}
202+
203+
this.ui.hideAnswerButtons();
204+
this.ui.showAnswerFeedback(result);
205+
this.ui.showNextQuestionButton();
206+
}
207+
208+
nextQuestion() {
209+
this.ui.hideFeedback();
210+
this.ui.hideNextQuestionButton();
211+
this.ui.hideNewQuizButton();
212+
this.ui.showAnswerButtons();
168213
this.showQuestion();
169214
}
170215

@@ -191,6 +236,58 @@ class App {
191236
// The dropdown will keep its existing hardcoded options if this fails
192237
}
193238
}
239+
240+
/**
241+
* Retrieve quiz history from localStorage.
242+
* @returns {Array} Stored quiz attempts.
243+
*/
244+
loadQuizHistory() {
245+
try {
246+
const raw = localStorage.getItem("gptQuizHistory");
247+
return raw ? JSON.parse(raw) : [];
248+
} catch (error) {
249+
console.warn("Failed to load quiz history from localStorage", error);
250+
return [];
251+
}
252+
}
253+
254+
/**
255+
* Append the current quiz result to localStorage history.
256+
* @param {Object} result - Final quiz result payload.
257+
*/
258+
saveQuizResult(result) {
259+
const entry = {
260+
topic: this.currentTopic,
261+
difficulty: this.currentDifficulty,
262+
model: this.currentModel,
263+
score: result.score,
264+
totalQuestions: result.totalQuestions,
265+
finishedAt: new Date().toISOString(),
266+
};
267+
268+
try {
269+
const history = this.loadQuizHistory();
270+
history.push(entry);
271+
localStorage.setItem("gptQuizHistory", JSON.stringify(history));
272+
this.quizHistory = history;
273+
this.ui.renderHistory(history);
274+
} catch (error) {
275+
console.warn("Failed to save quiz history to localStorage", error);
276+
}
277+
}
278+
279+
/**
280+
* Clear quiz history from localStorage and UI.
281+
*/
282+
clearQuizHistory() {
283+
try {
284+
localStorage.removeItem("gptQuizHistory");
285+
this.quizHistory = [];
286+
this.ui.renderHistory([]);
287+
} catch (error) {
288+
console.warn("Failed to clear quiz history", error);
289+
}
290+
}
194291
}
195292

196293
const app = new App();

frontend/scripts/config.js

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/**
2+
* Configuration constants for DOM selectors and CSS classes.
3+
* Centralizes all hardcoded strings for easier maintenance.
4+
*/
5+
6+
export const HTML_ELEMENT_IDS = {
7+
// Quiz generation
8+
fetchQuizButton: "#fetchQuizButton",
9+
quizTopic: "#quizTopic",
10+
quizNumQuestions: "#quizNumQuestions",
11+
quizDifficulty: "#quizDifficulty",
12+
quizModel: "#quizModel",
13+
14+
// Quiz interaction
15+
optionA: "#option-A",
16+
optionB: "#option-B",
17+
optionC: "#option-C",
18+
nextQuestionButton: "#nextQuestionButton",
19+
newQuizButton: "#newQuizButton",
20+
21+
// History
22+
historySection: "#quizHistory",
23+
historyList: "#quizHistoryList",
24+
historyEmptyMessage: "#historyEmptyMessage",
25+
26+
// UI containers
27+
inputContainer: "#inputContainer",
28+
intro: "#intro",
29+
quizContainer: "#quiz-container",
30+
quizSection: "#quizSection",
31+
32+
// Quiz display elements
33+
quizTitle: "#quizTitle",
34+
quizProgress: "#quizProgress",
35+
quizScore: "#quizScore",
36+
questionText: "#question-text",
37+
quizFeedback: "#quizFeedback",
38+
AIImage: "#AIImage",
39+
40+
// Loading elements
41+
loadingMessage: "#loadingMessage",
42+
loadingBarContainer: "#loadingBarContainer",
43+
loadingBar: "#loadingBar",
44+
45+
// Navigation
46+
navbarToggle: "#navbar-toggle",
47+
navbarLinks: "#navbar-links",
48+
};
49+
50+
export const CSS_CLASSES = {
51+
quizMeta: "quiz-meta",
52+
quizFeedback: "quiz-feedback",
53+
feedbackCorrect: "feedback-correct",
54+
feedbackWrong: "feedback-wrong",
55+
feedbackFinal: "feedback-final",
56+
feedbackChoices: "feedback-choices",
57+
choiceCorrect: "choice-correct",
58+
choiceNeutral: "choice-neutral",
59+
navbarActive: "active",
60+
};
61+
62+
/**
63+
* Safely retrieve a DOM element by its selector.
64+
* @param {string} selector - ID selector (e.g., "#elementId")
65+
* @returns {HTMLElement|null} The element or null if not found
66+
*/
67+
export function getElement(selector) {
68+
// Remove # if present and use getElementById for performance
69+
const id = selector.startsWith("#") ? selector.slice(1) : selector;
70+
return document.getElementById(id);
71+
}

frontend/scripts/controller.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ class Controller {
99
* @constructor
1010
* @param {Quiz} quiz - The quiz object to be initialized.
1111
*/
12-
constructor(quiz) {
12+
constructor(quiz = null, defaultNumQuestions = 5) {
1313
this.eventSource = null;
1414
this.messageCount = 0;
1515
// Determine if we are running locally based on the hostname.
@@ -24,8 +24,8 @@ class Controller {
2424
this.baseURLQuiz = `${this.baseURL}/GenerateQuiz`;
2525
this.baseURLImage = `${this.baseURL}/GenerateImage`;
2626
this.baseURLModels = `${this.baseURL}/SupportedModels`;
27-
this.quiz = quiz; // this will be initialized as a quiz object
28-
this.numQuestions = this.quiz.numQuestions;
27+
this.quiz = quiz; // may be set later by App
28+
this.numQuestions = defaultNumQuestions;
2929
}
3030

3131
/**

0 commit comments

Comments
 (0)