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
+68 -18
View File
@@ -1,6 +1,7 @@
import os
import uuid
import json
import random
import datetime
from fastapi import FastAPI, Request, Depends, HTTPException, status, Form
from fastapi.responses import HTMLResponse, RedirectResponse, JSONResponse
@@ -28,6 +29,27 @@ BASE_DIR = os.path.dirname(os.path.abspath(__file__))
app.mount("/static", StaticFiles(directory=os.path.join(BASE_DIR, "static")), name="static")
templates = Jinja2Templates(directory=os.path.join(BASE_DIR, "templates"))
def format_question_for_client(q: Question, number: int, total: int) -> dict:
options_list = None
if q.question_type == "multiple_choice" and q.options:
try:
parsed = json.loads(q.options)
if isinstance(parsed, list):
options_list = list(parsed)
random.shuffle(options_list)
except Exception:
options_list = [opt.strip() for opt in q.options.split(",") if opt.strip()]
random.shuffle(options_list)
return {
"question_number": number,
"total_questions": total,
"category": q.category,
"question": q.question,
"question_type": q.question_type or "text",
"options": options_list
}
@app.on_event("startup")
def startup_event():
seed_db()
@@ -65,16 +87,14 @@ async def start_game(db: Session = Depends(get_db)):
db.commit()
first_q = questions[0]
return {
res = format_question_for_client(first_q, 1, len(question_ids))
res.update({
"session_id": session_id,
"question_number": 1,
"total_questions": len(question_ids),
"category": first_q.category,
"question": first_q.question,
"score": 0,
"attempts_used": 0,
"max_attempts": 3
}
})
return res
@app.post("/api/game/check")
async def check_answer(payload: GameCheckRequest, db: Session = Depends(get_db)):
@@ -118,12 +138,7 @@ async def check_answer(payload: GameCheckRequest, db: Session = Depends(get_db))
if not is_finished:
next_q = db.query(Question).filter(Question.id == question_ids[session.current_question_idx]).first()
if next_q:
next_q_data = {
"question_number": session.current_question_idx + 1,
"total_questions": len(question_ids),
"category": next_q.category,
"question": next_q.question
}
next_q_data = format_question_for_client(next_q, session.current_question_idx + 1, len(question_ids))
return {
"correct": True,
@@ -155,12 +170,7 @@ async def check_answer(payload: GameCheckRequest, db: Session = Depends(get_db))
if not is_finished:
next_q = db.query(Question).filter(Question.id == question_ids[session.current_question_idx]).first()
if next_q:
next_q_data = {
"question_number": session.current_question_idx + 1,
"total_questions": len(question_ids),
"category": next_q.category,
"question": next_q.question
}
next_q_data = format_question_for_client(next_q, session.current_question_idx + 1, len(question_ids))
return {
"correct": False,
@@ -218,11 +228,23 @@ async def submit_suggestion(payload: SuggestionCreateRequest, db: Session = Depe
if not payload.frage.strip() or not payload.antwort.strip():
raise HTTPException(status_code=400, detail="Frage und Antwort dürfen nicht leer sein.")
options_json = None
q_type = payload.question_type or "text"
if q_type == "multiple_choice":
opts = [payload.antwort.strip()]
if payload.falsche_antworten:
for opt in payload.falsche_antworten:
if opt and opt.strip():
opts.append(opt.strip())
options_json = json.dumps(opts, ensure_ascii=False)
new_suggestion = Question(
category="community",
question=payload.frage.strip(),
canonical_answer=payload.antwort.strip(),
hint=payload.tipp.strip() if payload.tipp else None,
question_type=q_type,
options=options_json,
status="needs_review", # User Question Need Test / Review
submitted_by=payload.name.strip() if payload.name else "Gast",
sort_order=999
@@ -304,17 +326,31 @@ async def admin_add_question(
canonical_answer: str = Form(...),
hint: str = Form(None),
category: str = Form("harry_potter"),
question_type: str = Form("text"),
wrong_option_1: str = Form(None),
wrong_option_2: str = Form(None),
wrong_option_3: str = Form(None),
sort_order: int = Form(0),
db: Session = Depends(get_db)
):
if not request.session.get("admin_user"):
return RedirectResponse(url="/admin/login", status_code=303)
options_json = None
if question_type == "multiple_choice":
opts = [canonical_answer.strip()]
for w in [wrong_option_1, wrong_option_2, wrong_option_3]:
if w and w.strip():
opts.append(w.strip())
options_json = json.dumps(opts, ensure_ascii=False)
new_q = Question(
category=category.strip(),
question=question.strip(),
canonical_answer=canonical_answer.strip(),
hint=hint.strip() if hint else None,
question_type=question_type.strip(),
options=options_json,
status="active",
sort_order=sort_order,
submitted_by=f"Admin ({request.session['admin_user']})"
@@ -365,6 +401,8 @@ async def admin_edit_question(
question: str = Form(...),
canonical_answer: str = Form(...),
hint: str = Form(None),
question_type: str = Form("text"),
options_raw: str = Form(None),
status_val: str = Form("active"),
sort_order: int = Form(0),
db: Session = Depends(get_db)
@@ -379,6 +417,18 @@ async def admin_edit_question(
q.question = question.strip()
q.canonical_answer = canonical_answer.strip()
q.hint = hint.strip() if hint else None
q.question_type = question_type.strip()
if options_raw and options_raw.strip():
val = options_raw.strip()
if val.startswith("[") and val.endswith("]"):
q.options = val
else:
items = [x.strip() for x in val.split(",") if x.strip()]
q.options = json.dumps(items, ensure_ascii=False)
else:
q.options = None
q.status = status_val.strip()
q.sort_order = sort_order
db.commit()