Feature: Support both Free-Text and Multiple-Choice question types

This commit is contained in:
kobold-ai
2026-09-21 11:35:39 +02:00
parent c3a21369ef
commit 58959ca881
13 changed files with 448 additions and 47 deletions
+82
View File
@@ -428,6 +428,88 @@ body {
box-shadow: 0 0 12px rgba(201, 168, 106, 0.4);
}
/* Multiple Choice Styles */
.choice-group {
width: 100%;
margin-top: 10px;
}
.choice-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 14px;
width: 100%;
max-width: 650px;
margin: 0 auto;
}
.choice-btn {
background: rgba(255, 255, 255, 0.05);
border: 2px solid var(--border-gold);
border-radius: 10px;
padding: 14px 18px;
color: var(--text-main);
font-size: 1.05rem;
font-family: inherit;
cursor: pointer;
text-align: left;
display: flex;
align-items: center;
gap: 12px;
transition: all 0.25s ease;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}
.choice-btn:hover:not(:disabled) {
background: rgba(201, 168, 106, 0.18);
border-color: var(--gold);
box-shadow: 0 0 15px rgba(201, 168, 106, 0.4);
transform: translateY(-2px);
}
.choice-btn:disabled {
cursor: not-allowed;
opacity: 0.7;
}
.choice-letter {
display: inline-flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
background: rgba(201, 168, 106, 0.2);
border: 1px solid var(--gold);
border-radius: 50%;
color: var(--gold-bright);
font-weight: bold;
font-size: 0.9rem;
flex-shrink: 0;
}
.choice-text {
flex: 1;
}
.choice-selected {
border-color: var(--gold-bright);
background: rgba(201, 168, 106, 0.3);
}
.choice-correct {
background: rgba(46, 204, 113, 0.25) !important;
border-color: #2ecc71 !important;
color: #55efc4 !important;
box-shadow: 0 0 15px rgba(46, 204, 113, 0.6) !important;
}
.choice-wrong {
background: rgba(231, 76, 60, 0.25) !important;
border-color: #e74c3c !important;
color: #ff7675 !important;
box-shadow: 0 0 15px rgba(231, 76, 60, 0.5) !important;
}
.hint-box {
margin-top: 20px;
padding: 15px;
+14 -2
View File
@@ -7,8 +7,9 @@ function showTab(tabId) {
targetTab.classList.add("active");
}
// Mark active button
event.currentTarget.classList.add("active");
if (event && event.currentTarget) {
event.currentTarget.classList.add("active");
}
}
function toggleEdit(id) {
@@ -18,6 +19,17 @@ function toggleEdit(id) {
}
}
function toggleAdminNewType(type) {
const mcGroup = document.getElementById("adminMcOptions");
if (mcGroup) {
if (type === "multiple_choice") {
mcGroup.classList.remove("hidden");
} else {
mcGroup.classList.add("hidden");
}
}
}
async function approveQuestion(id) {
if (!confirm(`Möchtest du Frage #${id} wirklich freischalten und für alle Spieler aktivieren?`)) {
return;
+64 -14
View File
@@ -103,16 +103,42 @@ function renderQuestion(qData) {
document.getElementById("attemptBadge").innerText = `Versuch ${attemptsUsed + 1} von ${maxAttempts}`;
document.getElementById("frageText").innerText = qData.question;
const textGroup = document.getElementById("textAnswerGroup");
const choiceGroup = document.getElementById("choiceAnswerGroup");
const choiceContainer = document.getElementById("choiceOptionsContainer");
const input = document.getElementById("answerInput");
input.value = "";
input.disabled = false;
input.focus();
document.getElementById("checkBtn").disabled = false;
document.getElementById("hintBtn").disabled = false;
document.getElementById("hintBox").classList.add("hidden");
document.getElementById("feedbackBox").innerHTML = "";
if (qData.question_type === "multiple_choice" && qData.options && qData.options.length > 0) {
textGroup.classList.add("hidden");
choiceGroup.classList.remove("hidden");
choiceContainer.innerHTML = "";
const letters = ["A", "B", "C", "D", "E", "F"];
qData.options.forEach((opt, idx) => {
const btn = document.createElement("button");
btn.className = "choice-btn";
btn.type = "button";
btn.innerHTML = `<span class="choice-letter">${letters[idx] || "•"}</span> <span class="choice-text">${opt}</span>`;
btn.onclick = () => submitAnswer(opt, btn);
choiceContainer.appendChild(btn);
});
} else {
choiceGroup.classList.add("hidden");
textGroup.classList.remove("hidden");
input.value = "";
input.disabled = false;
document.getElementById("checkBtn").disabled = false;
input.focus();
}
const hintBtn = document.getElementById("hintBtn");
if (hintBtn) hintBtn.disabled = false;
const hintBtnMC = document.getElementById("hintBtnMC");
if (hintBtnMC) hintBtnMC.disabled = false;
updateProgress();
}
@@ -121,17 +147,28 @@ function updateProgress() {
document.getElementById("progressBar").style.width = percent + "%";
}
async function checkAnswer() {
function checkAnswer() {
const input = document.getElementById("answerInput");
const answer = input.value.trim();
if (!answer) {
input.focus();
return;
}
submitAnswer(answer);
}
async function submitAnswer(answer, clickedBtn = null) {
const checkBtn = document.getElementById("checkBtn");
checkBtn.disabled = true;
const input = document.getElementById("answerInput");
if (checkBtn) checkBtn.disabled = true;
if (input) input.disabled = true;
// Disable all choice buttons
const choiceBtns = document.querySelectorAll(".choice-btn");
choiceBtns.forEach(b => b.disabled = true);
if (clickedBtn) {
clickedBtn.classList.add("choice-selected");
}
try {
const res = await fetch("/api/game/check", {
@@ -157,9 +194,12 @@ async function checkAnswer() {
if (result.correct) {
// Success!
playSound('magic');
if (clickedBtn) {
clickedBtn.classList.remove("choice-selected");
clickedBtn.classList.add("choice-correct");
}
frageBox.classList.add("sparkle-effect");
feedbackBox.innerHTML = `<span class="feedback-correct">✨ Richtig! 1 Hauspunkt für dein Haus! ✨</span>`;
input.disabled = true;
setTimeout(() => {
frageBox.classList.remove("sparkle-effect");
@@ -175,6 +215,10 @@ async function checkAnswer() {
} else {
// Incorrect
playSound('wrong');
if (clickedBtn) {
clickedBtn.classList.remove("choice-selected");
clickedBtn.classList.add("choice-wrong");
}
frageBox.classList.add("shake-effect");
setTimeout(() => frageBox.classList.remove("shake-effect"), 500);
@@ -182,12 +226,11 @@ async function checkAnswer() {
document.getElementById("attemptBadge").innerText = `Versuch ${Math.min(attemptsUsed + 1, maxAttempts)} von ${maxAttempts}`;
if (result.attempts_left === 0) {
// Exhausted all 3 attempts
// Exhausted all attempts
feedbackBox.innerHTML = `
<div class="feedback-wrong">Leider nicht richtig (3 Fehlversuche).</div>
<div class="feedback-revealed">Richtige Lösung: <strong>${result.revealed_answer}</strong></div>
`;
input.disabled = true;
setTimeout(() => {
if (result.is_finished) {
@@ -202,14 +245,21 @@ async function checkAnswer() {
} else {
// Still attempts remaining
feedbackBox.innerHTML = `<span class="feedback-wrong">Leider falsch! Noch ${result.attempts_left} Versuch(e).</span>`;
checkBtn.disabled = false;
input.select();
if (checkBtn) checkBtn.disabled = false;
if (input) {
input.disabled = false;
input.select();
}
// Re-enable choice buttons
choiceBtns.forEach(b => b.disabled = false);
}
}
} catch (err) {
alert("Netzwerkfehler: " + err.message);
checkBtn.disabled = false;
if (checkBtn) checkBtn.disabled = false;
if (input) input.disabled = false;
choiceBtns.forEach(b => b.disabled = false);
}
}
+41 -6
View File
@@ -1,3 +1,19 @@
function toggleVorschlagType(type) {
const mcGroup = document.getElementById("mcOptionsGroup");
const antwortLabel = document.getElementById("antwortLabel");
const antwortHelp = document.getElementById("antwortHelp");
if (type === "multiple_choice") {
mcGroup.classList.remove("hidden");
antwortLabel.innerText = "Richtige Antwort (die korrekte Option):";
antwortHelp.innerText = "Diese Antwort wird zusammen mit den falschen Optionen zufällig für den Spieler angeordnet.";
} else {
mcGroup.classList.add("hidden");
antwortLabel.innerText = "Richtige Antwort:";
antwortHelp.innerText = "Unser System gleicht Tippfehler und Groß-/Kleinschreibung automatisch ab.";
}
}
async function submitSuggestion(event) {
event.preventDefault();
@@ -5,6 +21,7 @@ async function submitSuggestion(event) {
const frage = document.getElementById("frage").value.trim();
const antwort = document.getElementById("antwort").value.trim();
const tipp = document.getElementById("tipp").value.trim();
const qType = document.getElementById("q_type").value;
const submitBtn = document.getElementById("submitBtn");
const resultBox = document.getElementById("resultMessage");
@@ -13,19 +30,36 @@ async function submitSuggestion(event) {
return;
}
let falscheAntworten = [];
if (qType === "multiple_choice") {
const w1 = document.getElementById("wrong1").value.trim();
const w2 = document.getElementById("wrong2").value.trim();
const w3 = document.getElementById("wrong3").value.trim();
falscheAntworten = [w1, w2, w3].filter(x => x.length > 0);
if (falscheAntworten.length === 0) {
alert("Bitte gib bei Multiple-Choice mindestens eine falsche Antwortmöglichkeit an.");
return;
}
}
submitBtn.disabled = true;
submitBtn.innerText = "✨ Vorschlag wird übertragen...";
try {
const payload = {
name: name || "Gast",
frage: frage,
antwort: antwort,
tipp: tipp || null,
question_type: qType,
falsche_antworten: falscheAntworten
};
const res = await fetch("/api/suggestions", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
name: name || "Gast",
frage: frage,
antwort: antwort,
tipp: tipp || null
})
body: JSON.stringify(payload)
});
const data = await res.json();
@@ -39,6 +73,7 @@ async function submitSuggestion(event) {
`;
resultBox.classList.remove("hidden");
document.getElementById("suggestionForm").reset();
toggleVorschlagType("text");
} else {
resultBox.innerHTML = `
<div class="alert alert-danger">