使いどころがやっと分かった！GitHub Copilotとのアプリ開発奮闘記

私は普段ChatGPT Plusを使っています。その便利さにはいつも感心するばかり。そんな中、機会があったのでGitHub Copilotも試してみました。

GitHub Copilotと共に簡単なクイズアプリを作ってみた結果、やっとその使いどころを理解できました。"名は体を表す"と言いますが、GitHub Copilotはまさに「副操縦士」の役割を果たしてくれました。当初はアプリをすべて作ってもらうつもりでしたが、基本的な機能を動かすまでにはかなり苦労しました。しかし、一度軌道に乗れば、GitHub Copilotのサポートを受けてスムーズに進められるようになりました。

制作したクイズアプリはこちらです。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/6760/9a43c472-4f4c-9848-da32-2bb12311612a.png" alt="Streamlit Quiz App" width="400" />

https://github.com/takatama/streamlit-quiz-app

今回はGitHub Copilot Chatをメインで使いました。GitHub Copilotからの提案をソースコードにコピー＆ペーストし、その実行結果をChatでフィードバックするというプロセスを繰り返し、徐々にアプリを形にしていきました。この記事ではその奮闘の様子を紹介していきます。

GitHub Copilotを使ってみて、その得意な点と工夫が必要な点がはっきりと分かりました。得意なのは機能追加とコードの解説です。動くソフトウェアがそこにあれば、それをベースに正確に回答してくれます。

ただし、Copilotはあくまで「副操縦士」。開発環境を構築しアプリを起動できるようにするブートストラッピング（Bootstrapping）は、操縦士である私たちプログラマーの仕事です。主導権は常にプログラマーにあることがよく分かりました。このプロジェクトを通じて、GitHub Copilotは素晴らしいツールであると同時に、プログラマーの判断と創造性はますます重要になるな、と学ぶことができました。

ちなみに、GitHub Copilotにまとめてもった仕様をChatGPT-4にそのまま入力したところ、かなりいい感じのソースコードを出力してくれました。実際に動かしてみると不具合がありますが、その精度の高さに正直驚きました...。

https://chat.openai.com/share/ab7f69b9-fa0b-49ac-853b-1992c71e6508

GitHub Copilot Chatで同じようにアプリを作らせようとしても「Sorry, but I can only assist with programming related questions.」と断られてしまいます。ここにもCopilotらしさを感じました。

<details><summary>クイズアプリの仕様</summary>

GitHub Copilot Chatへの指示:

```
このクイズアプリの仕様を箇条書きでまとめて
```

回答:

このPythonスクリプトは、Streamlitを使用してクイズアプリケーションを作成しています。主な関数の簡単な説明は以下の通りです：

- JSONファイルからクイズの問題をロードします。問題は、問題文と選択肢、正解のインデックスを含むオブジェクトのリストとして表現されます。

- Streamlitのセッション状態を使用して、クイズの状態（現在の問題のインデックス、スコア、次の問題を表示するかどうか、メッセージ、ユーザーの回答）を管理します。

- 各問題は、問題文と選択肢を表示し、ユーザーからの入力を受け付けるためのラジオボタンを使用して表示されます。

- ユーザーが回答を送信すると、その回答が正しいかどうかを確認します。回答が正しい場合、スコアを増やし、正しいメッセージを表示します。回答が間違っていた場合、正解を表示します。

- ユーザーが次の問題に進むか、結果を表示するかを選択できます。これは、現在の問題が最後の問題かどうかによって決まります。

- クイズが終了したら、最終スコアとユーザーのすべての回答（問題、ユーザーの回答、その正誤、正解）を表示します。

- ユーザーは「Retry」ボタンをクリックしてクイズを再開することができます。これは、セッション状態をクリアし、クイズを最初から再開します。

</details>

# GitHub Copilotとクイズアプリ作り

ここからは、GitHub Copilotとの奮闘記を記述していきます。今回は6ステップでクイズアプリを作ってみました。使っているIDEはVisual Studio Codeです。

## Step1 お題の相談

GitHub Copilot Chatに指示して、デモアプリの候補を出してもらいます。

```
GitHub Copilotを使ってデモアプリを作りたいです。候補を10個出して
```
<details><summary>回答</summary>

### GitHub Copilot

もちろんです、以下にいくつかのデモアプリのアイデアを提案します。これらのアイデアは、さまざまな技術とプログラミング概念をカバーしています。

- ToDoリストアプリ: ユーザーがタスクを追加、削除、完了とマークできる基本的なアプリ。
天気予報アプリ: OpenWeatherMapなどのAPIを使用して、ユーザーが指定した場所の天気を表示するアプリ。
- ブログアプリ: ユーザーが記事を作成、編集、削除できるシンプルなブログアプリ。
- チャットアプリ: ユーザーがリアルタイムでメッセージを送受信できるアプリ。
- レシピ検索アプリ: ユーザーが料理のレシピを検索できるアプリ。
- ポートフォリオアプリ: ユーザーが自分の作品を展示できるアプリ。
- カレンダーアプリ: ユーザーがイベントをスケジュールし、リマインダーを設定できるアプリ。
- 電卓アプリ: 基本的な算術演算を実行できるシンプルな電卓アプリ。
- 音楽プレーヤーアプリ: ユーザーが音楽を再生、一時停止、スキップできるアプリ。
- クイズアプリ: ユーザーがクイズに答え、スコアを追跡できるアプリ。

これらのアイデアは、あなたが学びたい特定の技術や概念に基づいてカスタマイズできます。

</details>

## Step2 アプリのひな形を作る

使う技術は、Pythonの知識だけでインタラクティブなWebアプリを作れる[Streamlit](https://streamlit.io/)にしようと私が決めました。

GitHub Copilot Chatはメッセージによる指示だけでなく、コマンドを使った指示もできます。

https://code.visualstudio.com/docs/editor/github-copilot#_agents-and-slash-commands

> @workspace /explain: Explain step-by-step how the selected code works.
@workspace /fix: Propose a fix for the bugs in the selected code.
@workspace /new: Create a new project based on a natural language description.
@workspace /newNotebook: Create a new Jupyter Notebook based on your description.
@workspace /tests: Generate unit tests for the selected code.
@vscode /api: Questions about VS Code extension development.
@terminal: Explain how to do something in the integrated terminal.

ひな形となるファイルを作りたかったので、`@workspace /new`を選びました。

```console
@workspace /new Streamlitでクイズアプリ
```

すると、GitHub Copilotがすぐに準備してくれます。問題は`questions.json`で定義して、`app.py`で表示します。`README.md`まで作ってくれます。ただ、`requirements.txt`で定義されたモジュールはバージョンが古いので注意が必要です。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/6760/24594139-3577-08d6-c53c-9e044794b6b7.png" alt="GitHub Copilot workspace new command" width="400">

<details>
<summary>生成されたファイルの詳細</summary>

```json:data/questions.json
[
    {
        "question": "What is the capital of France?",
        "options": ["Paris", "London", "Berlin", "Madrid"],
        "answer": "Paris"
    },
    {
        "question": "What is the capital of England?",
        "options": ["Paris", "London", "Berlin", "Madrid"],
        "answer": "London"
    },
    {
        "question": "What is the capital of Germany?",
        "options": ["Paris", "London", "Berlin", "Madrid"],
        "answer": "Berlin"
    },
    {
        "question": "What is the capital of Spain?",
        "options": ["Paris", "London", "Berlin", "Madrid"],
        "answer": "Madrid"
    }
]
```

```python:app.py
import streamlit as st
import json

# Load questions
with open('data/questions.json', 'r') as f:
    questions = json.load(f)

# Quiz logic
def quiz(questions):
    score = 0
    for question in questions:
        st.write(question['question'])
        answer = st.selectbox("Choose one:", question['options'])
        if st.button('Submit'):
            if answer == question['answer']:
                score += 1
                st.write('Correct!')
            else:
                st.write('Incorrect.')
    st.write('Your final score is:', score)

# Run quiz
quiz(questions)
```
</details>

## Step3 開発環境の整備（自力）

次のステップへ進む前に、開発環境を整備しておくと良いでしょう。ここだけはGitHub Copilotを使わずに、自分で準備しました。

### venv

開発環境をきれいに保つため、venvを使った仮想環境を構築しておきます。

```console
python3 -m venv .venv
source .venv/bin/activate
```

### .gitignore

ファイルの変更履歴はGitで管理しておきましょう。`.gitignore`は作ってくれなかったので、次のURLから作成しておきます。

https://github.com/github/gitignore/blob/main/Python.gitignore

`.gitignore`に`.venv`も記述しておきます。ターミナルで`git init`を実行して、バージョン管理をはじます。

### pip install

GitHub Copilotが生成してくれた`requirements.txt`は、古いバージョンのStreamlitを使っています。そこで、最新版のStreamlitを手動でインストールします。少し時間がかかります。

```console
pip install streamlit
```

## Step4 ひな形を動かす

最もつまづきやすいステップです。

`/new`は便利なのですが、生成されたファイルが一発で動くことはほとんどありません。AIにとっても環境構築は難しいようです。

Streamlitのインストールが完了したら、アプリを起動します。

```console
streamlit run app.py
```

残念ながら画面にはエラーが表示されています。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/6760/8db6ad7e-3d5b-caae-9beb-00e74c1d828b.png" alt="GitHub Copilot Streamlit Quiz App Error" width="400">

ここから先は、GitHub Copilot Chatに状況を入力して、修正案を引き出していきます。今回は6回のやり取りが必要でした。Chatへの入力を示していきます。

### 1回目（エラーをそのまま入力）

```
raise DuplicateWidgetID( streamlit.errors.DuplicateWidgetID: There are multiple identical st.selectbox widgets with the same generated key.

When a widget is created, it's assigned an internal key based on its structure. Multiple widgets with an identical structure will result in the same internal key, which causes this error.

To fix this error, please pass a unique key argument to st.selectbox.
```
<details><summary>回答</summary>

#### GitHub Copilot

このエラーは、同じ構造を持つ複数のst.selectboxウィジェットが同じ内部キーを持つために発生します。これを解決するには、各st.selectboxに一意のkey引数を渡す必要があります。

以下のように、各質問のインデックスをキーとして使用することで修正できます。

```python
def quiz(questions):
    score = 0
    for i, question in enumerate(questions):
        st.write(question['question'])
        answer = st.selectbox("Choose one:", question['options'], key=i)
        if st.button('Submit', key=f"button{i}"):
            if answer == question['answer']:
                score += 1
```

この修正により、各`st.selectbox`と`st.button`は一意のキーを持つようになり、エラーは解消されます。

</details>

### 2回目（仕様変更の依頼1）

```
すべての問題が表示されてしまいます。
問題は一つだけ表示して、Submitボタンを押すと、
回答と、次の問題へ進むためのボタンを表示してください。
```

<details><summary>スクリーンショット</summary>
![スクリーンショット 2024-01-20 14.59.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/6760/ecb8a31e-784b-9e7c-ca09-2bbf2c896f42.png)
</details>


<details><summary>回答</summary>

#### GitHub Copilot Chat

Streamlitはステートレスなフレームワークで、各操作が新しいページロードを引き起こします。そのため、問題を一つずつ表示するためには、現在の問題のインデックスを追跡する必要があります。これは、StreamlitのSession Stateを使用して実現できます。

以下に、そのような変更を行ったコードを示します。

```python
import streamlit as st

# Quiz logic
def quiz(questions):
    if 'quiz_state' not in st.session_state:
        st.session_state.quiz_state = 0  # Initialize quiz_state in session_state
        st.session_state.score = 0  # Initialize score in session_state

    if st.session_state.quiz_state < len(questions):
        question = questions[st.session_state.quiz_state]
        st.write(question['question'])
        answer = st.selectbox("Choose one:", question['options'], key='selectbox')
        if st.button('Submit', key='button'):
            if answer == question['answer']:
                st.session_state.score += 1
                st.write('Correct!')
            else:
                st.write('Incorrect.')
            st.session_state.quiz_state += 1  # Move to next question
    else:
        st.write('Your final score is:', st.session_state.score)

# Run quiz
```

このコードでは、st.session_stateを使用して現在の問題のインデックス(quiz_state)とスコア(score)を追跡します。Submitボタンが押されると、quiz_stateがインクリメントされ、次の問題が表示されます。全ての問題が終わったら、最終スコアが表示されます。
</details>

### 3回目（仕様変更の依頼2）

直りきっていないので、同じことをもう一度伝えます。

```
Submitボタンを押すと、
回答と、次の問題へ進むためのボタンを表示してください。
```

<details><summary>回答</summary>

#### GitHub Copilot

あなたが要求した機能を実装するために、私たちはst.buttonを使用して"Next Question"ボタンを追加し、それが押されたときに次の問題に進むようにします。また、"Submit"ボタンが押されたときには回答の正誤だけを表示し、次の問題に進むためのボタンは表示しません。

以下に修正したコードを示します。

```python
import streamlit as st

# Quiz logic
def quiz(questions):
    if 'quiz_state' not in st.session_state:
        st.session_state.quiz_state = 0  # Initialize quiz_state in session_state
        st.session_state.score = 0  # Initialize score in session_state
        st.session_state.show_next = False  # Initialize show_next in session_state

    if st.session_state.quiz_state < len(questions):
        question = questions[st.session_state.quiz_state]
        st.write(question['question'])
        answer = st.selectbox("Choose one:", question['options'], key='selectbox')

        if st.session_state.show_next:
            if st.button('Next Question', key='next_button'):
                st.session_state.quiz_state += 1  # Move to next question
                st.session_state.show_next = False  # Reset show_next flag
        else:
            if st.button('Submit', key='submit_button'):
                if answer == question['answer']:
                    st.session_state.score += 1
                    st.write('Correct!')
                else:
                    st.write('Incorrect.')
                st.session_state.show_next = True  # Set show_next flag to True
    else:
        st.write('Your final score is:', st.session_state.score)

# Run quiz
```

このコードでは、st.session_state.show_nextフラグを使用して、"Next Question"ボタンを表示するかどうかを制御します。"Submit"ボタンが押されると、このフラグがTrueに設定され、"Next Question"ボタンが表示されます。"Next Question"ボタンが押されると、次の問題に進み、フラグがリセットされます。
</details>

### 4回目（不具合の報告）

```
Next Questionが出てきません
```

<details><summary>回答</summary>

#### GitHub Copilot

"Next Question"ボタンが表示されない問題は、Streamlitのステート管理の仕組みによるものです。Streamlitはステートレスなフレームワークで、各操作が新しいページロードを引き起こします。そのため、ボタンが押されたときにページがリロードされ、新しい状態が反映されます。

"Next Question"ボタンが表示されない問題を解決するためには、"Submit"ボタンが押されたときにst.session_state.show_nextをTrueに設定し、その後すぐにページをリロードする必要があります。これにより、"Next Question"ボタンが表示されるようになります。

以下に修正したコードを示します。

```python
import streamlit as st

# Quiz logic
def quiz(questions):
    if 'quiz_state' not in st.session_state:
        st.session_state.quiz_state = 0  # Initialize quiz_state in session_state
        st.session_state.score = 0  # Initialize score in session_state
        st.session_state.show_next = False  # Initialize show_next in session_state

    if st.session_state.quiz_state < len(questions):
        question = questions[st.session_state.quiz_state]
        st.write(question['question'])
        answer = st.selectbox("Choose one:", question['options'], key='selectbox')

        if st.session_state.show_next:
            if st.button('Next Question', key='next_button'):
                st.session_state.quiz_state += 1  # Move to next question
                st.session_state.show_next = False  # Reset show_next flag
        else:
            if st.button('Submit', key='submit_button'):
                if answer == question['answer']:
                    st.session_state.score += 1
                    st.write('Correct!')
                else:
                    st.write('Incorrect.')
                st.session_state.show_next = True  # Set show_next flag to True
                st.experimental_rerun()  # Rerun the script to update the state
    else:
        st.write('Your final score is:', st.session_state.score)

# Run quiz
```

このコードでは、"Submit"ボタンが押されたときにst.experimental_rerun()を呼び出してスクリプトを再実行します。これにより、"Next Question"ボタンが表示されるようになります。

</details>

### 5回目（不具合の報告2）

```
Submitを押すと、正解が画面に一瞬表示されたあと、消えてしまい、Next Questionが表示されます。
```

<details><summary>回答</summary>

#### GitHub Copilot

Streamlitはステートレスなフレームワークで、各操作が新しいページロードを引き起こします。そのため、st.experimental_rerun()を呼び出すと、ページがリロードされ、以前の出力がクリアされます。

この問題を解決するためには、正解または不正解のメッセージをst.session_stateに保存し、ページがリロードされたときにそれを表示するようにすることができます。

以下に修正したコードを示します。

```python
import streamlit as st

# Quiz logic
def quiz(questions):
    if 'quiz_state' not in st.session_state:
        st.session_state.quiz_state = 0  # Initialize quiz_state in session_state
        st.session_state.score = 0  # Initialize score in session_state
        st.session_state.show_next = False  # Initialize show_next in session_state
        st.session_state.message = ""  # Initialize message in session_state

    if st.session_state.quiz_state < len(questions):
        question = questions[st.session_state.quiz_state]
        st.write(question['question'])
        answer = st.selectbox("Choose one:", question['options'], key='selectbox')

        if st.session_state.show_next:
            st.write(st.session_state.message)  # Display the message
            if st.button('Next Question', key='next_button'):
                st.session_state.quiz_state += 1  # Move to next question
                st.session_state.show_next = False  # Reset show_next flag
                st.session_state.message = ""  # Reset the message
        else:
            if st.button('Submit', key='submit_button'):
                if answer == question['answer']:
                    st.session_state.score += 1
                    st.session_state.message = 'Correct!'  # Set the message
                else:
                    st.session_state.message = 'Incorrect.'  # Set the message
                st.session_state.show_next = True  # Set show_next flag to True
                st.experimental_rerun()  # Rerun the script to update the state
    else:
        st.write('Your final score is:', st.session_state.score)

# Run quiz
```

このコードでは、st.session_state.messageを使用して正解または不正解のメッセージを保存します。"Submit"ボタンが押されると、このメッセージが設定され、ページがリロードされたときに表示されます。"Next Question"ボタンが押されると、メッセージはリセットされます。

</details>

### 6回目（仕様変更の依頼3）

```
Next Questionを2回押さないと、次の問題が表示されません。1回で済ませたいです。
```

<details><summary>回答</summary>

#### GitHub Copilot

"Next Question"ボタンを押すとすぐに次の問題が表示されるようにするためには、ボタンが押されたときにスクリプトを再実行する必要があります。これにより、st.session_state.quiz_stateが更新され、次の問題が表示されます。

以下に修正したコードを示します。

```python
import streamlit as st

# Quiz logic
def quiz(questions):
    if 'quiz_state' not in st.session_state:
        st.session_state.quiz_state = 0  # Initialize quiz_state in session_state
        st.session_state.score = 0  # Initialize score in session_state
        st.session_state.show_next = False  # Initialize show_next in session_state
        st.session_state.message = ""  # Initialize message in session_state

    if st.session_state.quiz_state < len(questions):
        question = questions[st.session_state.quiz_state]
        st.write(question['question'])
        answer = st.selectbox("Choose one:", question['options'], key='selectbox')

        if st.session_state.show_next:
            st.write(st.session_state.message)  # Display the message
            if st.button('Next Question', key='next_button'):
                st.session_state.quiz_state += 1  # Move to next question
                st.session_state.show_next = False  # Reset show_next flag
                st.session_state.message = ""  # Reset the message
                st.experimental_rerun()  # Rerun the script to update the state
        else:
            if st.button('Submit', key='submit_button'):
                if answer == question['answer']:
                    st.session_state.score += 1
                    st.session_state.message = 'Correct!'  # Set the message
                else:
                    st.session_state.message = 'Incorrect.'  # Set the message
                st.session_state.show_next = True  # Set show_next flag to True
                st.experimental_rerun()  # Rerun the script to update the state
    else:
        st.write('Your final score is:', st.session_state.score)

# Run quiz
quiz(questions)
```

このコードでは、"Next Question"ボタンが押されたときにst.experimental_rerun()を呼び出してスクリプトを再実行します。これにより、"Next Question"ボタンを一度押すだけで次の問題が表示されます。

</details>

6回のやり取りを繰り返し、何とか動くようになりました！

## Step5 リファクタリング

ここからは、もっと便利になるよう仕様を追加していきます。

その前にリファクタリングをして、ソースコードを修正しやすくしておきます。

```console
app.pyでリファクタリングできるところはある？
```

<details><summary>回答</summary>

### GitHub Copilot

現在のコードはすでにきれいに整理されていますが、いくつかの改善点があります。以下に提案するリファクタリングを示します。

関数の分割: quiz関数は現在、複数の責任を持っています。これを小さな関数に分割することで、コードの可読性と再利用性を向上させることができます。

定数の使用: questions.jsonのようなファイルパスは、コードの上部で定数として定義すると良いでしょう。これにより、将来的にパスが変更された場合に、一箇所で変更するだけで済みます。

以下にリファクタリングしたコードを示します。

```python
import streamlit as st
import json

# Constants
QUESTIONS_FILE = 'data/questions.json'

# Load questions
def load_questions(file_path):
    with open(file_path, 'r') as f:
        return json.load(f)

# Initialize session state
def initialize_state():
    if 'quiz_state' not in st.session_state:
        st.session_state.quiz_state = 0
        st.session_state.score = 0
        st.session_state.show_next = False
        st.session_state.message = ""

# Display question and options
def display_question(question):
    st.write(question['question'])
    return st.selectbox("Choose one:", question['options'], key='selectbox')

# Check answer and update state
def check_answer(answer, question):
    if st.button('Submit', key='submit_button'):
        if answer == question['answer']:
            st.session_state.score += 1
            st.session_state.message = 'Correct!'
        else:
            st.session_state.message = 'Incorrect.'
        st.session_state.show_next = True
        st.experimental_rerun()

# Move to next question
def next_question():
    if st.button('Next Question', key='next_button'):
        st.session_state.quiz_state += 1
        st.session_state.show_next = False
        st.session_state.message = ""
        st.experimental_rerun()

# Quiz logic
def quiz(questions):
    initialize_state()

    if st.session_state.quiz_state < len(questions):
        question = questions[st.session_state.quiz_state]
        answer = display_question(question)

        if st.session_state.show_next:
            st.write(st.session_state.message)
            next_question()
        else:
            check_answer(answer, question)
    else:
        st.write('Your final score is:', st.session_state.score)

# Run quiz
questions = load_questions(QUESTIONS_FILE)
quiz(questions)
```
このリファクタリングにより、各関数が一つの責任を持つようになり、コードの可読性と再利用性が向上します。

</details>

## Step6 仕様追加

リファクタリングしたコードに、仕様を追加していきます。

### ラジオボックスで回答

```
回答はselectboxではなくradioを使いたい
```
<details><summary>回答</summary>

#### GitHub Copilot

Streamlitでは、st.radio関数を使用してラジオボタンを作成することができます。この関数は、選択肢のリストと一意のキーを引数に取り、選択された選択肢を返します。

以下に、st.selectboxをst.radioに置き換えたコードを示します。

```python
import streamlit as st

# Display question and options
def display_question(question):
    st.write(question['question'])
    return st.radio("Choose one:", question['options'], key='radio')

# The rest of your code...
```

この変更により、ユーザーは選択肢をラジオボタンから選択できるようになります。
</details>

### 不正解のときに正解を表示

```
Incorrectのメッセージと一緒に、正解も表示して
```
<details><summary>回答</summary>

#### GitHub Copilot

選択した回答が不正解だった場合に、正解も表示するようにコードを修正することができます。以下に修正したコードを示します。

```python
# Check answer and update state
def check_answer(answer, question):
    if st.button('Submit', key='submit_button'):
        if answer == question['answer']:
            st.session_state.score += 1
            st.session_state.message = 'Correct!'
        else:
            st.session_state.message = f'Incorrect. The correct answer is {question["answer"]}.'
        st.session_state.show_next = True
        st.experimental_rerun()
```
このコードでは、st.session_state.messageに正解を含むメッセージを設定します。これにより、ユーザーが不正解を選んだ場合、正解が表示されます。
</details>

### 最後に問題と回答をすべて表示

```
final scoreと一緒に、問題と、ユーザーの回答と、それが正解だったかどうかと、答えをすべて表示して
```
<details><summary>回答</summary>

#### GitHub Copilot

ユーザーの回答とその正誤、そして正解を保存するために、新しいセッションステート変数を追加します。そして、クイズが終了したときに、これらの情報をすべて表示します。

以下に修正したコードを示します。

```python
import streamlit as st

# Initialize session state
def initialize_state():
    if 'quiz_state' not in st.session_state:
        st.session_state.quiz_state = 0
        st.session_state.score = 0
        st.session_state.show_next = False
        st.session_state.message = ""
        st.session_state.user_answers = []  # Initialize user_answers in session_state

# Check answer and update state
def check_answer(answer, question):
    if st.button('Submit', key='submit_button'):
        correct = answer == question['answer']
        if correct:
            st.session_state.score += 1
            st.session_state.message = 'Correct!'
        else:
            st.session_state.message = f'Incorrect. The correct answer is {question["answer"]}.'
        st.session_state.show_next = True
        st.session_state.user_answers.append((question['question'], answer, correct, question['answer']))  # Save user's answer and its correctness
        st.experimental_rerun()

# Display final score and answers
def display_final_score_and_answers():
    st.write('Your final score is:', st.session_state.score)
    for question, user_answer, correct, correct_answer in st.session_state.user_answers:
        st.write(f"Question: {question}")
        st.write(f"Your answer: {user_answer} ({'Correct' if correct else 'Incorrect'})")
        st.write(f"Correct answer: {correct_answer}")
        st.write("---")

# Quiz logic
def quiz(questions):
    initialize_state()

    if st.session_state.quiz_state < len(questions):
        question = questions[st.session_state.quiz_state]
        answer = display_question(question)

        if st.session_state.show_next:
            st.write(st.session_state.message)
            next_question()
        else:
            check_answer(answer, question)
    else:
        display_final_score_and_answers()

# Run quiz
questions = load_questions(QUESTIONS_FILE)
quiz(questions)
```

このコードでは、st.session_state.user_answersを使用してユーザーの回答とその正誤を保存します。そして、クイズが終了したときに、display_final_score_and_answers関数を使用してこれらの情報をすべて表示します。

</details>

### クイズデータの形式を変更

```
questions.jsonのanswerをanswerIndexに変更
```
<details><summary>回答</summary>

#### GitHub Copilot

answerフィールドをanswerIndexに変更すると、各質問の正解は選択肢のインデックスとして表されます。これにより、選択肢が変更された場合でも、正解のインデックスは変わらないため、データの管理が容易になります。

ただし、この変更を行うと、アプリケーションのコードもそれに合わせて変更する必要があります。具体的には、正解をチェックする部分と、正解を表示する部分を更新する必要があります。

以下に修正したコードを示します。

```python
# Check answer and update state
def check_answer(answer, question):
    if st.button('Submit', key='submit_button'):
        correct = answer == question['options'][question['answerIndex']]
        if correct:
            st.session_state.score += 1
            st.session_state.message = 'Correct!'
        else:
            st.session_state.message = f'Incorrect. The correct answer is {question["options"][question["answerIndex"]]}.'
        st.session_state.show_next = True
        st.session_state.user_answers.append((question['question'], answer, correct, question['options'][question['answerIndex']]))  # Save user's answer and its correctness
        st.experimental_rerun()
```
</details>

### すべての問題に回答したら結果を表示ボタンにする

```
最後の問題に対してSubmitされたら、Next QuestionではなくShow Resultsボタンを表示したい
```
<details><summary>details</summary>

#### GitHub Copilot

その要件を満たすためには、現在が最後の問題かどうかを判断し、それに基づいてボタンのラベルを変更する必要があります。以下に修正したコードを示します。

```python
# Move to next question or show results
def next_question_or_show_results(questions):
    button_label = 'Show Results' if st.session_state.quiz_state == len(questions) - 1 else 'Next Question'
    if st.button(button_label, key='next_button'):
        st.session_state.quiz_state += 1
        st.session_state.show_next = False
        st.session_state.message = ""
        st.experimental_rerun()
```

このコードでは、button_labelを設定する際に、現在が最後の問題かどうかをチェックします。最後の問題であれば、ボタンのラベルは'Show Results'になり、そうでなければ'Next Question'になります。

</details>

### もう一度やるボタンを最後に表示

```
final scoreの下に、もう一度やるボタンを置く
```
<details><summary>回答</summary>

#### GitHub Copilot

"もう一度やる"ボタンを追加するためには、Streamlitのst.button関数を使用してボタンを作成し、そのボタンがクリックされたときにセッションステートをリセットする必要があります。以下に修正したコードを示します。

```python
# Display final score and answers
def display_final_score_and_answers():
    st.write('Your final score is:', st.session_state.score)
    for question, user_answer, correct, correct_answer in st.session_state.user_answers:
        st.write(f"Question: {question}")
        st.write(f"Your answer: {user_answer} ({'Correct' if correct else 'Incorrect'})")
        st.write(f"Correct answer: {correct_answer}")
        st.write("---")

    if st.button("もう一度やる"):
        initialize_state()
        st.experimental_rerun()
```
このコードでは、"もう一度やる"ボタンがクリックされたときに、initialize_state関数を呼び出してセッションステートをリセットし、st.experimental_rerun関数を呼び出してアプリケーションを再実行します。これにより、クイズが最初から再開されます。

#### 不具合報告

```
もう一度やるボタンを押したら、強制的にセッションをクリア
```

#### GitHub Copilot

"もう一度やる"ボタンが押されたときにセッションステートを完全にクリアするには、st.session_state.clear()を使用します。これにより、すべてのセッションステート変数が削除され、新たなセッションが開始されます。

以下に修正したコードを示します。

```python
# Display final score and answers
def display_final_score_and_answers():
    st.write('Your final score is:', st.session_state.score)
    for question, user_answer, correct, correct_answer in st.session_state.user_answers:
        st.write(f"Question: {question}")
        st.write(f"Your answer: {user_answer} ({'Correct' if correct else 'Incorrect'})")
        st.write(f"Correct answer: {correct_answer}")
        st.write("---")

    if st.button("もう一度やる"):
        st.session_state.clear()  # Clear session state
        st.experimental_rerun()
```
このコードでは、"もう一度やる"ボタンがクリックされたときに、st.session_state.clear()関数を呼び出してセッションステートをクリアし、st.experimental_rerun関数を呼び出してアプリケーションを再実行します。これにより、クイズが最初から再開されます。

</details>