第4回

クイズアプリ

選択式のクイズアプリです。
挑戦したいクイズのカテゴリなども変更できます。

app.py


import os
import json
import sys

from flask import Flask, render_template, request, redirect, url_for, session
from openai import OpenAI

# UTF-8対策
sys.stdout.reconfigure(encoding="utf-8")

app = Flask(__name__)
app.secret_key = "quiz-secret-key"

# OpenAI APIキー
OPENAI_API_KEY = ""

# ランキング
ranking_data = []


@app.route("/", methods=["GET", "POST"])
def index():

    if request.method == "POST":

        category = request.form.get("category")
        difficulty = request.form.get("difficulty")
        question_count = request.form.get("question_count")

        client = OpenAI(api_key=OPENAI_API_KEY)

        prompt = f"""
以下の条件で4択クイズをJSON形式で作成してください。

カテゴリ: {category}
難易度: {difficulty}
問題数: {question_count}

重要ルール:
- 必ず4択問題にしてください
- choices に4つの選択肢を入れてください
- answer は choices の中の1つと完全一致
- 問題文はシンプルにしてください
- 解説も作成してください
- JSONのみ返してください

以下のJSON形式のみで返してください。

{{
  "questions": [
    {{
      "question": "問題文",
      "choices": [
        "選択肢1",
        "選択肢2",
        "選択肢3",
        "選択肢4"
      ],
      "answer": "正解",
      "explanation": "解説"
    }}
  ]
}}
"""

        try:

            response = client.chat.completions.create(
                model="gpt-4.1-mini",
                messages=[
                    {
                        "role": "system",
                        "content": "あなたはクイズ作成AIです。必ずJSONのみ返してください。"
                    },
                    {
                        "role": "user",
                        "content": prompt
                    }
                ],
                temperature=0.7
            )

            content = response.choices[0].message.content

            # UTF-8対策
            content = content.encode("utf-8").decode("utf-8")

            quiz_data = json.loads(content)

            session["quiz_data"] = quiz_data

            return redirect(url_for("quiz"))

        except Exception as e:

            return render_template(
                "index.html",
                error=str(e)
            )

    return render_template("index.html")


@app.route("/quiz", methods=["GET", "POST"])
def quiz():

    quiz_data = session.get("quiz_data")

    if not quiz_data:
        return redirect(url_for("index"))

    if request.method == "POST":

        questions = quiz_data["questions"]

        score = 0
        results = []

        for i, q in enumerate(questions):

            user_answer = request.form.get(f"question_{i}", "")

            correct_answer = q["answer"]

            is_correct = user_answer == correct_answer

            if is_correct:
                score += 1

            results.append({
                "question": q["question"],
                "user_answer": user_answer,
                "correct_answer": correct_answer,
                "is_correct": is_correct,
                "explanation": q.get("explanation", "解説なし")
            })

        # ランキング保存
        ranking_data.append(score)

        ranking_data.sort(reverse=True)

        ranking_top = ranking_data[:10]

        return render_template(
            "result.html",
            score=score,
            total=len(questions),
            results=results,
            ranking=ranking_top
        )

    return render_template(
        "quiz.html",
        questions=quiz_data["questions"]
    )


if __name__ == "__main__":
    port = int(os.environ.get("PORT", 3000))
    app.run(host="0.0.0.0", port=port)

index.html


<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>AIクイズアプリ</title>

    <link
        rel="stylesheet"
        href="{{ url_for('static', filename='style.css') }}"
    >
</head>
<body>

<div class="container">

    <h1>AIクイズアプリ</h1>

    {% if error %}
        <div class="error">
            {{ error }}
        </div>
    {% endif %}

    <form method="POST">

        <label>カテゴリ</label>

        <select name="category">

            <option value="歴史">歴史</option>
            <option value="科学">科学</option>
            <option value="IT">IT</option>
            <option value="地理">地理</option>
            <option value="スポーツ">スポーツ</option>
            <option value="アニメ">アニメ</option>
            <option value="ゲーム">ゲーム</option>
            <option value="映画">映画</option>
            <option value="音楽">音楽</option>
            <option value="英語">英語</option>
            <option value="数学">数学</option>
            <option value="雑学">雑学</option>

        </select>

        <label>難易度</label>

        <select name="difficulty">

            <option value="初級">初級</option>
            <option value="中級">中級</option>
            <option value="上級">上級</option>

        </select>

        <label>問題数</label>

        <select name="question_count">

            <option value="3">3</option>
            <option value="5">5</option>
            <option value="10">10</option>

        </select>

        <button type="submit">
            クイズ生成
        </button>

    </form>

</div>

</body>
</html>

quiz.html


<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>クイズ</title>

    <link
        rel="stylesheet"
        href="{{ url_for('static', filename='style.css') }}"
    >
</head>
<body>

<div class="container">

    <h1>クイズ</h1>

    <div class="warning">
        制限時間内に回答してください
    </div>

    <div class="timer">
        残り時間: <span id="time">60</span> 秒
    </div>

    <form method="POST">

        {% for q in questions %}

            {% set question_index = loop.index0 %}

            <div class="question-box">

                <h3>
                    Q{{ loop.index }}. {{ q.question }}
                </h3>

                {% for choice in q.choices %}

                    <label class="choice">

                        <input
                            type="radio"
                            name="question_{{ question_index }}"
                            value="{{ choice }}"
                            required
                        >

                        {{ choice }}

                    </label>

                {% endfor %}

            </div>

        {% endfor %}

        <button type="submit">
            回答する
        </button>

    </form>

</div>

<script>

let time = 60;

const timer = setInterval(() => {

    time--;

    document.getElementById("time").innerText = time;

    if (time <= 0) {

        clearInterval(timer);

        alert("時間切れです");

        document.forms[0].submit();
    }

}, 1000);

</script>

</body>
</html>

result.html


<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>結果</title>

    <link
        rel="stylesheet"
        href="{{ url_for('static', filename='style.css') }}"
    >
</head>
<body>

<div class="container">

    <h1>結果</h1>

    <div class="score">
        {{ total }}問中 {{ score }}問正解
    </div>

    {% for r in results %}

        <div class="result-box">

            <h3>
                {{ loop.index }}. {{ r.question }}
            </h3>

            <p>
                あなたの回答:
                <strong>{{ r.user_answer }}</strong>
            </p>

            <p>
                正解:
                <strong>{{ r.correct_answer }}</strong>
            </p>

            {% if r.is_correct %}
                <p class="correct">
                    正解
                </p>
            {% else %}
                <p class="incorrect">
                    不正解
                </p>
            {% endif %}

            <div class="explanation">
                <strong>解説:</strong>
                {{ r.explanation }}
            </div>

        </div>

    {% endfor %}

    <a href="{{ url_for('index') }}">
        <button>
            もう一度遊ぶ
        </button>
    </a>

</div>

</body>
</html>

style.css


body {
    font-family: Arial, sans-serif;
    background: #121212;
    color: white;
    margin: 0;
    padding: 0;
    line-height: 1.7;
}

.container {
    width: 92%;
    max-width: 900px;
    margin: 40px auto;
    background: #1e1e1e;
    padding: 35px;
    border-radius: 16px;
    box-shadow: 0 0 20px rgba(0,0,0,0.5);
}

h1 {
    text-align: center;
    margin-bottom: 30px;
    font-size: 36px;
}

h2 {
    margin-top: 40px;
    text-align: center;
}

h3 {
    margin-bottom: 20px;
    font-size: 22px;
    line-height: 1.6;
}

form {
    display: flex;
    flex-direction: column;
}

label {
    margin-top: 15px;
    margin-bottom: 8px;
    font-size: 18px;
}

input,
select,
button {
    padding: 14px;
    font-size: 17px;
    border-radius: 10px;
    border: none;
}

input,
select {
    background: #2b2b2b;
    color: white;
}

button {
    margin-top: 30px;
    background: #4f8cff;
    color: white;
    cursor: pointer;
    font-weight: bold;
    transition: 0.2s;
}

button:hover {
    opacity: 0.9;
    transform: scale(1.01);
}

.question-box,
.result-box {
    margin-top: 30px;
    padding: 25px;
    border: 1px solid #333;
    border-radius: 14px;
    background: #262626;
}

.choice {
    display: flex;
    align-items: center;
    gap: 12px;
    margin-top: 14px;
    padding: 14px;
    background: #333;
    border-radius: 10px;
    cursor: pointer;
    transition: 0.2s;
    font-size: 18px;
}

.choice:hover {
    background: #444;
}

.choice input[type="radio"] {
    transform: scale(1.3);
}

.correct {
    color: #00ff88;
    font-weight: bold;
    font-size: 20px;
}

.incorrect {
    color: #ff6666;
    font-weight: bold;
    font-size: 20px;
}

.score {
    font-size: 32px;
    text-align: center;
    margin-bottom: 30px;
    font-weight: bold;
}

.error {
    background: #5c1f1f;
    color: #ffb3b3;
    padding: 14px;
    border-radius: 10px;
    margin-bottom: 20px;
}

.timer {
    font-size: 28px;
    text-align: center;
    color: #ffd166;
    margin-bottom: 30px;
    font-weight: bold;
}

.explanation {
    margin-top: 15px;
    padding: 14px;
    background: #202020;
    border-left: 5px solid #4f8cff;
    border-radius: 8px;
    line-height: 1.8;
}

ol {
    padding-left: 25px;
}

li {
    margin-top: 10px;
    font-size: 18px;
}

.warning {
    background: #403000;
    color: #ffd166;
    padding: 12px;
    border-radius: 10px;
    margin-bottom: 25px;
    text-align: center;
}

@media screen and (max-width: 600px) {

    .container {
        padding: 20px;
    }

    h1 {
        font-size: 28px;
    }

    h3 {
        font-size: 19px;
    }

    .choice {
        font-size: 16px;
        padding: 12px;
    }

    button {
        font-size: 16px;
    }
}

requiements.txt


Flask==3.0.3
openai==1.35.10

振り返り・感想

普段、AIには文章での返答をメインとして質問をしますが、今回、コードの作成をしてもらい、
短時間でこれほどのクオリティのアプリを作ることができる凄さには驚きました。
やっていくと問題点も見つかりますが、その部分も質問するだけで修正したコードを貰えるので、便利です。
今はAIが発達しているので、AIの使い方を理解し、スムーズに作業を進められるようにしていきたいです。