Spaces:
Sleeping
Sleeping
Update src/streamlit_app.py
Browse files- src/streamlit_app.py +601 -38
src/streamlit_app.py
CHANGED
@@ -1,40 +1,603 @@
|
|
1 |
-
import altair as alt
|
2 |
-
import numpy as np
|
3 |
-
import pandas as pd
|
4 |
import streamlit as st
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
5 |
|
6 |
-
""
|
7 |
-
|
8 |
-
|
9 |
-
Edit `/streamlit_app.py` to customize this app to your heart's desire :heart:.
|
10 |
-
If you have any questions, checkout our [documentation](https://docs.streamlit.io) and [community
|
11 |
-
forums](https://discuss.streamlit.io).
|
12 |
-
|
13 |
-
In the meantime, below is an example of what you can do with just a few lines of code:
|
14 |
-
"""
|
15 |
-
|
16 |
-
num_points = st.slider("Number of points in spiral", 1, 10000, 1100)
|
17 |
-
num_turns = st.slider("Number of turns in spiral", 1, 300, 31)
|
18 |
-
|
19 |
-
indices = np.linspace(0, 1, num_points)
|
20 |
-
theta = 2 * np.pi * num_turns * indices
|
21 |
-
radius = indices
|
22 |
-
|
23 |
-
x = radius * np.cos(theta)
|
24 |
-
y = radius * np.sin(theta)
|
25 |
-
|
26 |
-
df = pd.DataFrame({
|
27 |
-
"x": x,
|
28 |
-
"y": y,
|
29 |
-
"idx": indices,
|
30 |
-
"rand": np.random.randn(num_points),
|
31 |
-
})
|
32 |
-
|
33 |
-
st.altair_chart(alt.Chart(df, height=700, width=700)
|
34 |
-
.mark_point(filled=True)
|
35 |
-
.encode(
|
36 |
-
x=alt.X("x", axis=None),
|
37 |
-
y=alt.Y("y", axis=None),
|
38 |
-
color=alt.Color("idx", legend=None, scale=alt.Scale()),
|
39 |
-
size=alt.Size("rand", legend=None, scale=alt.Scale(range=[1, 150])),
|
40 |
-
))
|
|
|
|
|
|
|
|
|
1 |
import streamlit as st
|
2 |
+
from educhain import Educhain, LLMConfig
|
3 |
+
from educhain.engines import qna_engine
|
4 |
+
from langchain_openai import ChatOpenAI
|
5 |
+
import os
|
6 |
+
from dotenv import load_dotenv
|
7 |
+
import json
|
8 |
+
from datetime import datetime
|
9 |
+
import pandas as pd
|
10 |
+
import random
|
11 |
+
|
12 |
+
# Load environment variables if available
|
13 |
+
load_dotenv()
|
14 |
+
|
15 |
+
# Set page configuration at the very top of the script
|
16 |
+
st.set_page_config(page_title="Multilingual Quiz App", page_icon="🧠", layout="wide")
|
17 |
+
|
18 |
+
# Define supported languages
|
19 |
+
languages = [
|
20 |
+
"English", "Hindi", "Gujarati", "Bengali", "Tamil",
|
21 |
+
"Telugu", "Kannada", "Malayalam", "Punjabi", "Marathi",
|
22 |
+
"Urdu", "Assamese", "Odia", "Sanskrit", "Korean",
|
23 |
+
"Japanese", "Arabic", "French", "German", "Spanish",
|
24 |
+
"Portuguese", "Russian", "Chinese", "Vietnamese", "Thai",
|
25 |
+
"Indonesian", "Turkish", "Polish", "Ukrainian", "Dutch",
|
26 |
+
"Italian", "Greek", "Hebrew", "Persian", "Swedish",
|
27 |
+
"Norwegian", "Danish", "Finnish", "Czech", "Hungarian",
|
28 |
+
"Romanian", "Bulgarian", "Croatian", "Serbian", "Slovak",
|
29 |
+
"Slovenian", "Estonian", "Latvian", "Lithuanian", "Malay",
|
30 |
+
"Tagalog", "Swahili"
|
31 |
+
]
|
32 |
+
|
33 |
+
# Define question types
|
34 |
+
question_types = ["Multiple Choice", "True/False"]
|
35 |
+
|
36 |
+
# Define difficulty levels
|
37 |
+
difficulty_levels = ["Easy", "Medium", "Hard"]
|
38 |
+
|
39 |
+
# Initialize session state
|
40 |
+
if 'current_quiz' not in st.session_state:
|
41 |
+
st.session_state.current_quiz = None
|
42 |
+
if 'quiz_in_progress' not in st.session_state:
|
43 |
+
st.session_state.quiz_in_progress = False
|
44 |
+
if 'current_question' not in st.session_state:
|
45 |
+
st.session_state.current_question = 0
|
46 |
+
if 'user_answers' not in st.session_state:
|
47 |
+
st.session_state.user_answers = []
|
48 |
+
if 'user_score' not in st.session_state:
|
49 |
+
st.session_state.user_score = 0
|
50 |
+
if 'quiz_completed' not in st.session_state:
|
51 |
+
st.session_state.quiz_completed = False
|
52 |
+
if 'saved_quizzes' not in st.session_state:
|
53 |
+
st.session_state.saved_quizzes = []
|
54 |
+
if 'page' not in st.session_state:
|
55 |
+
st.session_state.page = "create" # Options: "create", "take", "history"
|
56 |
+
|
57 |
+
# --- Sidebar Navigation ---
|
58 |
+
with st.sidebar:
|
59 |
+
st.sidebar.image("https://framerusercontent.com/images/T5kFJeyNUyAYJBz4PaWuP7Bfr0.png", use_container_width=True)
|
60 |
+
st.title("Multilingual Quiz App")
|
61 |
+
|
62 |
+
# Navigation
|
63 |
+
st.subheader("📚 Navigation")
|
64 |
+
if st.button("Create Quiz", use_container_width=True):
|
65 |
+
st.session_state.page = "create"
|
66 |
+
if st.button("Saved Quizzes", use_container_width=True):
|
67 |
+
st.session_state.page = "saved"
|
68 |
+
if st.button("Quiz History", use_container_width=True):
|
69 |
+
st.session_state.page = "history"
|
70 |
+
|
71 |
+
st.header("⚙️ Configuration")
|
72 |
+
|
73 |
+
# Use environment variable if available, otherwise hide API key input
|
74 |
+
api_key = os.getenv("SUTRA_API_KEY", "")
|
75 |
+
if not api_key:
|
76 |
+
api_key = st.text_input("Sutra API Key", value="", type="password")
|
77 |
+
if not api_key:
|
78 |
+
st.warning("Please set your Sutra API Key as an environment variable or enter it above")
|
79 |
+
|
80 |
+
st.markdown("---")
|
81 |
+
st.markdown("**Powered by** [Educhain](https://github.com/satvik314/educhain)")
|
82 |
+
st.markdown("**Using** [Sutra LLM](https://docs.two.ai/) for multilingual")
|
83 |
+
st.write("❤️ Built with [Streamlit](https://streamlit.io)")
|
84 |
+
|
85 |
+
# --- Initialize Educhain with Sutra Model ---
|
86 |
+
@st.cache_resource
|
87 |
+
def initialize_educhain(api_key):
|
88 |
+
if not api_key:
|
89 |
+
return None # Return None if API key is missing
|
90 |
+
|
91 |
+
sutra_model = ChatOpenAI(
|
92 |
+
api_key=api_key,
|
93 |
+
base_url="https://api.two.ai/v2",
|
94 |
+
model="sutra-v2",
|
95 |
+
temperature=0.9
|
96 |
+
)
|
97 |
+
llm_config = LLMConfig(custom_model=sutra_model)
|
98 |
+
return Educhain(llm_config)
|
99 |
+
|
100 |
+
# --- Utility Function to Convert Questions to Quiz Format ---
|
101 |
+
def convert_to_quiz_format(questions_obj, topic, language, difficulty):
|
102 |
+
quiz = {
|
103 |
+
"title": f"{topic} Quiz ({difficulty})",
|
104 |
+
"language": language,
|
105 |
+
"difficulty": difficulty,
|
106 |
+
"topic": topic,
|
107 |
+
"created_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
108 |
+
"questions": []
|
109 |
+
}
|
110 |
+
|
111 |
+
if hasattr(questions_obj, "questions"):
|
112 |
+
for q in questions_obj.questions:
|
113 |
+
question_data = {
|
114 |
+
"question": q.question,
|
115 |
+
"answer": q.answer
|
116 |
+
}
|
117 |
+
|
118 |
+
if hasattr(q, 'options'):
|
119 |
+
question_data["type"] = "multiple_choice"
|
120 |
+
question_data["options"] = q.options
|
121 |
+
else:
|
122 |
+
question_data["type"] = "true_false"
|
123 |
+
question_data["options"] = ["True", "False"]
|
124 |
+
|
125 |
+
if hasattr(q, 'explanation') and q.explanation:
|
126 |
+
question_data["explanation"] = q.explanation
|
127 |
+
|
128 |
+
quiz["questions"].append(question_data)
|
129 |
+
|
130 |
+
return quiz
|
131 |
+
|
132 |
+
# --- Save Quiz Function ---
|
133 |
+
def save_quiz(quiz):
|
134 |
+
# Create a unique ID for the quiz
|
135 |
+
quiz_id = f"{len(st.session_state.saved_quizzes) + 1}_{datetime.now().strftime('%Y%m%d%H%M%S')}"
|
136 |
+
quiz["id"] = quiz_id
|
137 |
+
|
138 |
+
# Add to saved quizzes
|
139 |
+
st.session_state.saved_quizzes.append(quiz)
|
140 |
+
|
141 |
+
# Also save to disk (optional)
|
142 |
+
try:
|
143 |
+
# Check if file exists and load existing data
|
144 |
+
if os.path.exists("saved_quizzes.json"):
|
145 |
+
with open("saved_quizzes.json", "r") as f:
|
146 |
+
existing_quizzes = json.load(f)
|
147 |
+
else:
|
148 |
+
existing_quizzes = []
|
149 |
+
|
150 |
+
# Append new quiz
|
151 |
+
existing_quizzes.append(quiz)
|
152 |
+
|
153 |
+
# Save updated list
|
154 |
+
with open("saved_quizzes.json", "w") as f:
|
155 |
+
json.dump(existing_quizzes, f)
|
156 |
+
except Exception as e:
|
157 |
+
st.warning(f"Could not save quiz to disk: {str(e)}")
|
158 |
+
|
159 |
+
return quiz_id
|
160 |
+
|
161 |
+
# --- Start Quiz Function ---
|
162 |
+
def start_quiz(quiz):
|
163 |
+
st.session_state.current_quiz = quiz
|
164 |
+
st.session_state.quiz_in_progress = True
|
165 |
+
st.session_state.current_question = 0
|
166 |
+
st.session_state.user_answers = [None] * len(quiz["questions"])
|
167 |
+
st.session_state.user_score = 0
|
168 |
+
st.session_state.quiz_completed = False
|
169 |
+
|
170 |
+
# --- Submit Answer Function ---
|
171 |
+
def submit_answer(answer_index):
|
172 |
+
current_q = st.session_state.current_question
|
173 |
+
st.session_state.user_answers[current_q] = answer_index
|
174 |
+
|
175 |
+
# Check if answer is correct
|
176 |
+
correct_answer = st.session_state.current_quiz["questions"][current_q]["answer"]
|
177 |
+
|
178 |
+
# For multiple choice, the answer might be the option text or the option index
|
179 |
+
if st.session_state.current_quiz["questions"][current_q]["type"] == "multiple_choice":
|
180 |
+
# Try to match by index first (if answer is A, B, C, D)
|
181 |
+
if correct_answer in ["A", "B", "C", "D"]:
|
182 |
+
correct_index = ord(correct_answer) - ord("A")
|
183 |
+
if answer_index == correct_index:
|
184 |
+
st.session_state.user_score += 1
|
185 |
+
# Otherwise, match by option text
|
186 |
+
else:
|
187 |
+
options = st.session_state.current_quiz["questions"][current_q]["options"]
|
188 |
+
if answer_index < len(options) and options[answer_index] == correct_answer:
|
189 |
+
st.session_state.user_score += 1
|
190 |
+
# For true/false
|
191 |
+
else:
|
192 |
+
options = ["True", "False"]
|
193 |
+
user_answer = options[answer_index]
|
194 |
+
# Convert both answers to lowercase strings for comparison
|
195 |
+
if str(user_answer).lower() == str(correct_answer).lower():
|
196 |
+
st.session_state.user_score += 1
|
197 |
+
|
198 |
+
# Move to next question or end quiz
|
199 |
+
if current_q < len(st.session_state.current_quiz["questions"]) - 1:
|
200 |
+
st.session_state.current_question += 1
|
201 |
+
else:
|
202 |
+
st.session_state.quiz_completed = True
|
203 |
+
# Save quiz results to history
|
204 |
+
save_quiz_result()
|
205 |
+
|
206 |
+
# --- Save Quiz Result Function ---
|
207 |
+
def save_quiz_result():
|
208 |
+
result = {
|
209 |
+
"quiz_title": st.session_state.current_quiz["title"],
|
210 |
+
"language": st.session_state.current_quiz["language"],
|
211 |
+
"topic": st.session_state.current_quiz["topic"],
|
212 |
+
"difficulty": st.session_state.current_quiz["difficulty"],
|
213 |
+
"score": st.session_state.user_score,
|
214 |
+
"total": len(st.session_state.current_quiz["questions"]),
|
215 |
+
"date": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
216 |
+
}
|
217 |
+
|
218 |
+
# Initialize history if not exists
|
219 |
+
if 'quiz_history' not in st.session_state:
|
220 |
+
st.session_state.quiz_history = []
|
221 |
+
|
222 |
+
# Add to history
|
223 |
+
st.session_state.quiz_history.append(result)
|
224 |
+
|
225 |
+
# Also save to disk (optional)
|
226 |
+
try:
|
227 |
+
# Check if file exists and load existing data
|
228 |
+
if os.path.exists("quiz_history.json"):
|
229 |
+
with open("quiz_history.json", "r") as f:
|
230 |
+
existing_history = json.load(f)
|
231 |
+
else:
|
232 |
+
existing_history = []
|
233 |
+
|
234 |
+
# Append new result
|
235 |
+
existing_history.append(result)
|
236 |
+
|
237 |
+
# Save updated list
|
238 |
+
with open("quiz_history.json", "w") as f:
|
239 |
+
json.dump(existing_history, f)
|
240 |
+
except Exception as e:
|
241 |
+
st.warning(f"Could not save quiz history to disk: {str(e)}")
|
242 |
+
|
243 |
+
# --- Create Quiz Page ---
|
244 |
+
def show_create_quiz_page():
|
245 |
+
st.markdown(
|
246 |
+
f'<h1><img src="https://framerusercontent.com/images/9vH8BcjXKRcC5OrSfkohhSyDgX0.png" width="60"/> Create Multilingual Quiz</h1>',
|
247 |
+
unsafe_allow_html=True
|
248 |
+
)
|
249 |
+
|
250 |
+
# --- Initialize Educhain client if API key is provided ---
|
251 |
+
api_key = os.getenv("SUTRA_API_KEY", "")
|
252 |
+
if not api_key:
|
253 |
+
api_key = st.sidebar.text_input("Sutra API Key", value="", type="password")
|
254 |
+
|
255 |
+
if api_key:
|
256 |
+
educhain_client = initialize_educhain(api_key)
|
257 |
+
if educhain_client:
|
258 |
+
qna_engine = educhain_client.qna_engine
|
259 |
+
else:
|
260 |
+
st.error("Failed to initialize Educhain. Please check your Sutra API key.")
|
261 |
+
return
|
262 |
+
else:
|
263 |
+
st.warning("Please enter your Sutra API Key in the sidebar or set it as an environment variable to continue.")
|
264 |
+
return
|
265 |
+
|
266 |
+
# Quiz configuration
|
267 |
+
col1, col2, col3 = st.columns(3)
|
268 |
+
with col1:
|
269 |
+
selected_language = st.selectbox("Language:", languages)
|
270 |
+
with col2:
|
271 |
+
selected_question_type = st.selectbox("Question Type:", question_types)
|
272 |
+
with col3:
|
273 |
+
selected_difficulty = st.selectbox("Difficulty:", difficulty_levels)
|
274 |
+
|
275 |
+
topic = st.text_input("Quiz Topic:", "General Knowledge")
|
276 |
+
num_questions = st.slider("Number of Questions", 3, 10, 5)
|
277 |
+
|
278 |
+
custom_instructions = st.text_area(
|
279 |
+
"Custom Instructions (optional):",
|
280 |
+
placeholder=f"e.g. 'Focus on {selected_difficulty.lower()} concepts for {topic}'",
|
281 |
+
height=100
|
282 |
+
)
|
283 |
+
|
284 |
+
# Add language instruction to custom instructions
|
285 |
+
language_custom_instructions = f"Generate all questions, options, answers and explanations in {selected_language} language. Make questions {selected_difficulty.lower()} difficulty. {custom_instructions}"
|
286 |
+
|
287 |
+
# Generate quiz button
|
288 |
+
if st.button("Generate Quiz"):
|
289 |
+
with st.spinner(f"Generating {num_questions} {selected_question_type.lower()} questions in {selected_language}..."):
|
290 |
+
# Use the appropriate method based on question type
|
291 |
+
if selected_question_type == "Multiple Choice":
|
292 |
+
questions = qna_engine.generate_questions(
|
293 |
+
topic=topic,
|
294 |
+
num=num_questions,
|
295 |
+
question_type="Multiple Choice",
|
296 |
+
custom_instructions=language_custom_instructions
|
297 |
+
)
|
298 |
+
else: # True/False
|
299 |
+
questions = qna_engine.generate_questions(
|
300 |
+
topic=topic,
|
301 |
+
num=num_questions,
|
302 |
+
question_type="True/False",
|
303 |
+
custom_instructions=language_custom_instructions
|
304 |
+
)
|
305 |
+
|
306 |
+
# Convert to quiz format and save
|
307 |
+
quiz = convert_to_quiz_format(questions, topic, selected_language, selected_difficulty)
|
308 |
+
quiz_id = save_quiz(quiz)
|
309 |
+
|
310 |
+
st.success(f"Quiz generated successfully! Quiz ID: {quiz_id}")
|
311 |
+
|
312 |
+
# Preview quiz
|
313 |
+
with st.expander("Preview Quiz"):
|
314 |
+
for i, q in enumerate(quiz["questions"]):
|
315 |
+
st.subheader(f"Question {i+1}: {q['question']}")
|
316 |
+
st.write("Options:")
|
317 |
+
for j, opt in enumerate(q["options"]):
|
318 |
+
st.write(f" {chr(65 + j)}. {opt}")
|
319 |
+
st.write(f"**Correct Answer:** {q['answer']}")
|
320 |
+
if "explanation" in q and q["explanation"]:
|
321 |
+
st.write(f"**Explanation:** {q['explanation']}")
|
322 |
+
st.markdown("---")
|
323 |
+
|
324 |
+
# Button to start quiz
|
325 |
+
if st.button("Start This Quiz"):
|
326 |
+
start_quiz(quiz)
|
327 |
+
st.session_state.page = "take"
|
328 |
+
st.rerun()
|
329 |
+
|
330 |
+
# --- Saved Quizzes Page ---
|
331 |
+
def show_saved_quizzes_page():
|
332 |
+
st.markdown(
|
333 |
+
f'<h1><img src="https://framerusercontent.com/images/9vH8BcjXKRcC5OrSfkohhSyDgX0.png" width="60"/> Saved Quizzes</h1>',
|
334 |
+
unsafe_allow_html=True
|
335 |
+
)
|
336 |
+
|
337 |
+
if not st.session_state.saved_quizzes:
|
338 |
+
st.info("No saved quizzes yet. Create one first!")
|
339 |
+
return
|
340 |
+
|
341 |
+
# Create a dataframe for better display
|
342 |
+
quiz_data = []
|
343 |
+
for quiz in st.session_state.saved_quizzes:
|
344 |
+
quiz_data.append({
|
345 |
+
"ID": quiz.get("id", "Unknown"),
|
346 |
+
"Title": quiz.get("title", "Untitled"),
|
347 |
+
"Topic": quiz.get("topic", "Unknown"),
|
348 |
+
"Language": quiz.get("language", "Unknown"),
|
349 |
+
"Difficulty": quiz.get("difficulty", "Unknown"),
|
350 |
+
"Questions": len(quiz.get("questions", [])),
|
351 |
+
"Created": quiz.get("created_at", "Unknown")
|
352 |
+
})
|
353 |
+
|
354 |
+
df = pd.DataFrame(quiz_data)
|
355 |
+
st.dataframe(df, use_container_width=True)
|
356 |
+
|
357 |
+
# Select quiz to take
|
358 |
+
selected_quiz_id = st.selectbox(
|
359 |
+
"Select a quiz to take:",
|
360 |
+
options=[quiz.get("id", "Unknown") for quiz in st.session_state.saved_quizzes],
|
361 |
+
format_func=lambda x: next((q["title"] for q in st.session_state.saved_quizzes if q.get("id") == x), x)
|
362 |
+
)
|
363 |
+
|
364 |
+
# Start selected quiz
|
365 |
+
if st.button("Start Selected Quiz"):
|
366 |
+
selected_quiz = next((q for q in st.session_state.saved_quizzes if q.get("id") == selected_quiz_id), None)
|
367 |
+
if selected_quiz:
|
368 |
+
start_quiz(selected_quiz)
|
369 |
+
st.session_state.page = "take"
|
370 |
+
st.rerun()
|
371 |
+
|
372 |
+
# Option to delete a quiz
|
373 |
+
if st.button("Delete Selected Quiz"):
|
374 |
+
st.session_state.saved_quizzes = [q for q in st.session_state.saved_quizzes if q.get("id") != selected_quiz_id]
|
375 |
+
st.success("Quiz deleted successfully!")
|
376 |
+
st.rerun()
|
377 |
+
|
378 |
+
# --- Take Quiz Page ---
|
379 |
+
def show_take_quiz_page():
|
380 |
+
if not st.session_state.quiz_in_progress or not st.session_state.current_quiz:
|
381 |
+
st.warning("No quiz is currently in progress.")
|
382 |
+
if st.button("Go to Saved Quizzes"):
|
383 |
+
st.session_state.page = "saved"
|
384 |
+
st.rerun()
|
385 |
+
return
|
386 |
+
|
387 |
+
quiz = st.session_state.current_quiz
|
388 |
+
|
389 |
+
# Display quiz header
|
390 |
+
st.markdown(
|
391 |
+
f'<h1>{quiz["title"]}</h1>',
|
392 |
+
unsafe_allow_html=True
|
393 |
+
)
|
394 |
+
st.write(f"Language: {quiz['language']} | Difficulty: {quiz['difficulty']} | Topic: {quiz['topic']}")
|
395 |
+
|
396 |
+
# If quiz is completed, show results
|
397 |
+
if st.session_state.quiz_completed:
|
398 |
+
st.balloons()
|
399 |
+
st.markdown(f"## Quiz Completed!")
|
400 |
+
st.markdown(f"### Your Score: {st.session_state.user_score}/{len(quiz['questions'])}")
|
401 |
+
|
402 |
+
# Calculate percentage
|
403 |
+
percentage = (st.session_state.user_score / len(quiz['questions'])) * 100
|
404 |
+
st.progress(percentage / 100)
|
405 |
+
|
406 |
+
# Different messages based on score
|
407 |
+
if percentage >= 80:
|
408 |
+
st.success("Excellent! You've mastered this topic!")
|
409 |
+
elif percentage >= 60:
|
410 |
+
st.info("Good job! You have a solid understanding of the material.")
|
411 |
+
else:
|
412 |
+
st.warning("You might want to review this topic again.")
|
413 |
+
|
414 |
+
# Show answers and explanations
|
415 |
+
with st.expander("Review Questions and Answers"):
|
416 |
+
for i, (question, user_answer) in enumerate(zip(quiz["questions"], st.session_state.user_answers)):
|
417 |
+
correct_answer = question["answer"]
|
418 |
+
is_correct = False
|
419 |
+
|
420 |
+
# Determine if the answer was correct
|
421 |
+
if question["type"] == "multiple_choice":
|
422 |
+
if correct_answer in ["A", "B", "C", "D"]:
|
423 |
+
correct_index = ord(correct_answer) - ord("A")
|
424 |
+
is_correct = (user_answer == correct_index)
|
425 |
+
else:
|
426 |
+
is_correct = (question["options"][user_answer] == correct_answer)
|
427 |
+
else: # true/false
|
428 |
+
options = ["True", "False"]
|
429 |
+
# Convert both answers to lowercase strings for comparison
|
430 |
+
is_correct = (str(options[user_answer]).lower() == str(correct_answer).lower())
|
431 |
+
|
432 |
+
# Display question and answer
|
433 |
+
st.markdown(f"**Question {i+1}:** {question['question']}")
|
434 |
+
|
435 |
+
if question["type"] == "multiple_choice":
|
436 |
+
st.write("Options:")
|
437 |
+
for j, opt in enumerate(question["options"]):
|
438 |
+
prefix = "✅ " if (is_correct and user_answer == j) else "❌ " if (not is_correct and user_answer == j) else ""
|
439 |
+
highlight = "**" if (correct_answer in ["A", "B", "C", "D"] and j == ord(correct_answer) - ord("A")) or \
|
440 |
+
(correct_answer not in ["A", "B", "C", "D"] and opt == correct_answer) else ""
|
441 |
+
st.write(f" {prefix}{chr(65 + j)}. {highlight}{opt}{highlight}")
|
442 |
+
else: # true/false
|
443 |
+
st.write("Options:")
|
444 |
+
for j, opt in enumerate(["True", "False"]):
|
445 |
+
prefix = "✅ " if (is_correct and user_answer == j) else "❌ " if (not is_correct and user_answer == j) else ""
|
446 |
+
highlight = "**" if str(opt).lower() == str(correct_answer).lower() else ""
|
447 |
+
st.write(f" {prefix}{opt} {highlight}")
|
448 |
+
|
449 |
+
if "explanation" in question and question["explanation"]:
|
450 |
+
st.write(f"**Explanation:** {question['explanation']}")
|
451 |
+
|
452 |
+
st.markdown("---")
|
453 |
+
|
454 |
+
# Button to go back to saved quizzes
|
455 |
+
if st.button("Choose Another Quiz"):
|
456 |
+
st.session_state.page = "saved"
|
457 |
+
st.rerun()
|
458 |
+
|
459 |
+
# Button to create a new quiz
|
460 |
+
if st.button("Create New Quiz"):
|
461 |
+
st.session_state.page = "create"
|
462 |
+
st.rerun()
|
463 |
+
|
464 |
+
# If quiz is in progress, show current question
|
465 |
+
else:
|
466 |
+
current_q_index = st.session_state.current_question
|
467 |
+
total_questions = len(quiz["questions"])
|
468 |
+
current_q = quiz["questions"][current_q_index]
|
469 |
+
|
470 |
+
# Progress bar
|
471 |
+
st.progress((current_q_index) / total_questions)
|
472 |
+
st.write(f"Question {current_q_index + 1} of {total_questions}")
|
473 |
+
|
474 |
+
# Display question
|
475 |
+
st.markdown(f"## {current_q['question']}")
|
476 |
+
|
477 |
+
# Display options based on question type
|
478 |
+
if current_q["type"] == "multiple_choice":
|
479 |
+
for i, option in enumerate(current_q["options"]):
|
480 |
+
if st.button(f"{chr(65 + i)}. {option}", key=f"opt_{i}"):
|
481 |
+
submit_answer(i)
|
482 |
+
st.rerun()
|
483 |
+
else: # true/false
|
484 |
+
col1, col2 = st.columns(2)
|
485 |
+
with col1:
|
486 |
+
if st.button("True", use_container_width=True):
|
487 |
+
submit_answer(0)
|
488 |
+
st.rerun()
|
489 |
+
with col2:
|
490 |
+
if st.button("False", use_container_width=True):
|
491 |
+
submit_answer(1)
|
492 |
+
st.rerun()
|
493 |
+
|
494 |
+
# Option to skip question
|
495 |
+
if st.button("Skip Question"):
|
496 |
+
submit_answer(random.randint(0, len(current_q["options"]) - 1)) # Submit random answer
|
497 |
+
st.rerun()
|
498 |
+
|
499 |
+
# --- Quiz History Page ---
|
500 |
+
def show_history_page():
|
501 |
+
st.markdown(
|
502 |
+
f'<h1><img src="https://framerusercontent.com/images/9vH8BcjXKRcC5OrSfkohhSyDgX0.png" width="60"/> Quiz History</h1>',
|
503 |
+
unsafe_allow_html=True
|
504 |
+
)
|
505 |
+
|
506 |
+
# Initialize quiz_history if not exists
|
507 |
+
if 'quiz_history' not in st.session_state:
|
508 |
+
st.session_state.quiz_history = []
|
509 |
+
|
510 |
+
# Try to load from disk
|
511 |
+
try:
|
512 |
+
if os.path.exists("quiz_history.json"):
|
513 |
+
with open("quiz_history.json", "r") as f:
|
514 |
+
st.session_state.quiz_history = json.load(f)
|
515 |
+
except Exception:
|
516 |
+
pass
|
517 |
+
|
518 |
+
if not st.session_state.quiz_history:
|
519 |
+
st.info("No quiz history yet. Take a quiz first!")
|
520 |
+
return
|
521 |
+
|
522 |
+
# Create a dataframe for better display
|
523 |
+
history_data = []
|
524 |
+
for result in st.session_state.quiz_history:
|
525 |
+
percentage = (result["score"] / result["total"]) * 100
|
526 |
+
history_data.append({
|
527 |
+
"Date": result["date"],
|
528 |
+
"Quiz": result["quiz_title"],
|
529 |
+
"Topic": result["topic"],
|
530 |
+
"Language": result["language"],
|
531 |
+
"Difficulty": result["difficulty"],
|
532 |
+
"Score": f"{result['score']}/{result['total']} ({percentage:.1f}%)"
|
533 |
+
})
|
534 |
+
|
535 |
+
# Sort by date, newest first
|
536 |
+
history_data.sort(key=lambda x: x["Date"], reverse=True)
|
537 |
+
|
538 |
+
df = pd.DataFrame(history_data)
|
539 |
+
st.dataframe(df, use_container_width=True)
|
540 |
+
|
541 |
+
# Some analytics
|
542 |
+
if len(history_data) > 1:
|
543 |
+
st.subheader("Your Progress")
|
544 |
+
|
545 |
+
# Calculate average score by topic
|
546 |
+
topic_data = {}
|
547 |
+
for result in st.session_state.quiz_history:
|
548 |
+
topic = result["topic"]
|
549 |
+
if topic not in topic_data:
|
550 |
+
topic_data[topic] = {"total": 0, "correct": 0, "count": 0}
|
551 |
+
topic_data[topic]["correct"] += result["score"]
|
552 |
+
topic_data[topic]["total"] += result["total"]
|
553 |
+
topic_data[topic]["count"] += 1
|
554 |
+
|
555 |
+
# Create chart data
|
556 |
+
chart_data = []
|
557 |
+
for topic, data in topic_data.items():
|
558 |
+
percentage = (data["correct"] / data["total"]) * 100
|
559 |
+
chart_data.append({
|
560 |
+
"Topic": topic,
|
561 |
+
"Percentage": percentage,
|
562 |
+
"Quizzes Taken": data["count"]
|
563 |
+
})
|
564 |
+
|
565 |
+
# Display chart
|
566 |
+
if chart_data:
|
567 |
+
chart_df = pd.DataFrame(chart_data)
|
568 |
+
st.bar_chart(chart_df.set_index("Topic")["Percentage"])
|
569 |
+
|
570 |
+
# Clear history button
|
571 |
+
if st.button("Clear History"):
|
572 |
+
st.session_state.quiz_history = []
|
573 |
+
# Also delete from disk
|
574 |
+
if os.path.exists("quiz_history.json"):
|
575 |
+
os.remove("quiz_history.json")
|
576 |
+
st.success("History cleared!")
|
577 |
+
st.rerun()
|
578 |
+
|
579 |
+
# --- Main App Logic ---
|
580 |
+
def main():
|
581 |
+
# Load saved quizzes from disk on startup
|
582 |
+
if 'saved_quizzes' not in st.session_state or not st.session_state.saved_quizzes:
|
583 |
+
try:
|
584 |
+
if os.path.exists("saved_quizzes.json"):
|
585 |
+
with open("saved_quizzes.json", "r") as f:
|
586 |
+
st.session_state.saved_quizzes = json.load(f)
|
587 |
+
except Exception:
|
588 |
+
pass
|
589 |
+
|
590 |
+
# Display the appropriate page
|
591 |
+
if st.session_state.page == "create":
|
592 |
+
show_create_quiz_page()
|
593 |
+
elif st.session_state.page == "saved":
|
594 |
+
show_saved_quizzes_page()
|
595 |
+
elif st.session_state.page == "take" and st.session_state.quiz_in_progress:
|
596 |
+
show_take_quiz_page()
|
597 |
+
elif st.session_state.page == "history":
|
598 |
+
show_history_page()
|
599 |
+
else:
|
600 |
+
show_create_quiz_page()
|
601 |
|
602 |
+
if __name__ == "__main__":
|
603 |
+
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|