File size: 17,411 Bytes
7461958
 
1830afe
7461958
 
1804d1f
7461958
a263c13
 
ad2cf05
 
738bef1
7461958
3bc5a71
1830afe
dc5787b
ad2cf05
7461958
1830afe
 
dc5787b
1830afe
 
dc5787b
1830afe
 
 
ad2cf05
 
 
 
 
 
1830afe
 
 
 
 
 
ad2cf05
1830afe
ad2cf05
 
 
 
738bef1
ad2cf05
3bc5a71
ad2cf05
 
 
 
 
 
 
 
 
 
3bc5a71
56f04a7
3bc5a71
 
f7df0a6
 
3bc5a71
ad2cf05
 
 
 
 
1830afe
ad2cf05
 
 
 
 
 
 
 
1830afe
 
 
 
 
 
 
 
 
 
 
 
ad2cf05
 
 
 
 
 
 
 
 
 
eccc273
ff40b9b
ad2cf05
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17b329c
ad2cf05
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3a94602
ad2cf05
 
 
 
 
 
 
 
 
 
 
 
 
 
2428246
3bc5a71
ad2cf05
3bc5a71
b948b56
1804d1f
 
8c624e7
1804d1f
 
3bc5a71
4210a3c
21c7278
4210a3c
21c7278
 
 
 
 
 
 
 
3bc5a71
21c7278
 
3bc5a71
ad2cf05
 
 
f7df0a6
 
 
 
 
 
 
3bc5a71
 
 
 
 
 
f7df0a6
b02478c
 
b55bbb9
f7df0a6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56f04a7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f7df0a6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b55bbb9
 
56f04a7
f7df0a6
 
 
3bc5a71
 
ad2cf05
 
 
 
 
 
 
f7df0a6
 
3bc5a71
7461958
ad2cf05
 
 
7461958
ad2cf05
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
import streamlit as st
import google.generativeai as genai
import openai
import os
import re  # For improved parsing
from streamlit_extras.metric_cards import style_metric_cards

st.set_page_config(initial_sidebar_state="collapsed")

# --- Login Page ---
def login_page():
    # st.title("API Selection")


    api_options = ["Gemini API", "OpenAI API"]
    selected_api = st.selectbox("### Select API:", api_options)
    api_key = st.text_input(f"Enter your {selected_api} Key:", type="password")

    if selected_api == "Gemini API":
        model_options = ["gemini-pro"] # Add more Gemini models if available
        selected_model = st.selectbox("### Select Gemini Model:", model_options, index=0) # Default to first model
    elif selected_api == "OpenAI API":
        model_options = ["gpt-3.5-turbo", "gpt-4"] # Add more OpenAI models as needed
        selected_model = st.selectbox("### Select OpenAI Model:", model_options, index=0) # Default to first model
    else:
        selected_model = None # Should not happen, but for safety

    if st.button("Login"):
        if not api_key:
            st.error("Please enter your API key.")
        else:
            st.session_state.api_key = api_key
            st.session_state.selected_api = selected_api
            if selected_api == "Gemini API":
                st.session_state.gemini_model = selected_model
                st.session_state.openai_model = None # Clear OpenAI model if Gemini is selected
            elif selected_api == "OpenAI API":
                st.session_state.openai_model = selected_model
                st.session_state.gemini_model = None # Clear Gemini model if OpenAI is selected
            st.session_state.logged_in = True
            st.success(f"Logged in with {selected_api} using model: {selected_model}!")
            st.rerun() # Rerun to show the quiz app

# --- Quiz Application ---
def quiz_app():
    st.title("QuizLM 🧠")

    # Initialize session state for quiz data and progress
    if 'quiz_data' not in st.session_state:
        st.session_state.quiz_data = None
    if 'current_question_index' not in st.session_state:
        st.session_state.current_question_index = 0
    if 'user_answers' not in st.session_state:
        st.session_state.user_answers = []
    if 'quiz_completed' not in st.session_state:
        st.session_state.quiz_completed = False
    if 'score' not in st.session_state:
        st.session_state.score = 0
    if 'quiz_started' not in st.session_state:
        st.session_state.quiz_started = False
    if 'quiz_topic' not in st.session_state:
        st.session_state.quiz_topic = "" # Initialize quiz topic
    if 'topic_from_sidebar' not in st.session_state:
        st.session_state.topic_from_sidebar = False # Flag to track if topic is from sidebar


    # API Configuration based on selection - Moved inside quiz_app after login
    if st.session_state.selected_api == "Gemini API":
        try:
            genai.configure(api_key=st.session_state.api_key)
            model = genai.GenerativeModel(st.session_state.gemini_model)
        except Exception as e:
            st.error(f"Error configuring Gemini API: {e}")
            st.error("Please check your API key and network connection.")
            st.session_state.logged_in = False # Force logout if API config fails
            del st.session_state.api_key
            del st.session_state.selected_api
            st.rerun() # Go back to login page
            return # Exit quiz_app
    elif st.session_state.selected_api == "OpenAI API":
        try:
            openai.api_key = st.session_state.api_key
            model_name = st.session_state.openai_model # Model name already selected in login
        except Exception as e:
            st.error(f"Error configuring OpenAI API: {e}")
            st.error("Please check your API key and network connection.")
            st.session_state.logged_in = False # Force logout if API config fails
            del st.session_state.api_key
            del st.session_state.selected_api
            st.rerun() # Go back to login page
            return # Exit quiz_app


    def parse_quiz_content(quiz_content):
        """Parses the quiz content and answer key into a structured format, handling title."""
        questions = []
        answer_key_dict = {}

        quiz_parts = quiz_content.split("Answer Key:")
        if len(quiz_parts) != 2:
            st.warning("Could not reliably separate quiz questions and answer key. Parsing might be imperfect.")
            return None, None

        question_section = quiz_parts[0].strip()
        answer_key_section = quiz_parts[1].strip()

        question_lines = question_section.strip().split('\n')
        current_question = None
        for line in question_lines:
            line = line.strip()
            if not line:
                continue # Skip empty lines

            if re.match(r'^\d+\.\s', line): # Start of a new question
                if current_question: # Save previous question if exists
                    questions.append(current_question)
                current_question = {'options': {}} # Start new question
                current_question['question'] = re.sub(r'\*\*|\*', '', line).strip() # Question text, remove bold
            elif re.match(r'^[A-D]\)\s', line) or re.match(r'^[A-D]\.\s', line): # Option line (handle both ')' and '.' after letter)
                if current_question:
                    option_letter = line[0]
                    option_text = line[2:].strip() # Remove "A) " or "A. "
                    current_question['options'][option_letter] = option_text
            elif re.match(r'^\*\*.*?\*\*$', line) or re.match(r'^\*.*?\*$', line) : # Check for title again, just in case after split. Remove bold title if found in lines.
                pass # Ignore title lines within question section if any.

        if current_question: # Append the last question
            questions.append(current_question)


        # --- Answer Key Parsing (No change needed) ---
        answer_lines = answer_key_section.strip().split('\n')
        for line in answer_lines:
            line = line.strip()
            match = re.match(r'(\d+)\.\s*([A-D])', line)
            if match:
                question_num = int(match.group(1)) - 1
                correct_answer = match.group(2)
                answer_key_dict[question_num] = correct_answer

        # Basic validation
        if not questions or not answer_key_dict:
            st.error("Error parsing quiz content. Please try again or check the generated format.")
            return None, None
        if len(questions) != len(answer_key_dict):
            st.warning(f"Number of questions parsed ({len(questions)}) does not match number of answers in answer key ({len(answer_key_dict)}). Parsing might be incomplete.")


        # Combine parsed questions and answer key into quiz_data
        quiz_data_list = []
        for i, q_data in enumerate(questions):
            correct_answer = answer_key_dict.get(i)
            if correct_answer:
                quiz_data_list.append({
                    'question': q_data['question'],
                    'options': q_data['options'],
                    'correct_answer': correct_answer
                })
            else:
                st.warning(f"Could not find correct answer for question {i+1} in the answer key.")
                return None, None

        return quiz_data_list, answer_key_dict


    def display_question():
        """Displays the current question and options for interactive quiz."""
        if st.session_state.quiz_data is None:
            st.error("Quiz data not loaded yet. Generate a quiz first.")
            return

        if st.session_state.current_question_index < len(st.session_state.quiz_data):
            question_data = st.session_state.quiz_data[st.session_state.current_question_index]
            question_number = st.session_state.current_question_index + 1
            st.subheader(f"{question_data['question']}")

            options_list = [f"{key}. {value}" for key, value in question_data['options'].items()]
            user_choice = st.radio("Choose an answer:", options_list, key=f"q_{question_number}") # Unique key for radio buttons

            if st.button("Submit Answer", key=f"submit_q_{question_number}"): # Unique key for submit button
                selected_option_letter = user_choice.split('.')[0] # Extract A, B, C, or D
                st.session_state.user_answers.append(selected_option_letter)

                if st.session_state.current_question_index < len(st.session_state.quiz_data) - 1:
                    st.session_state.current_question_index += 1
                else:
                    st.session_state.quiz_completed = True # Mark quiz as completed after last question
                st.rerun()


    def display_results():
        """Displays the quiz results after completion."""
        if st.session_state.quiz_completed:
            st.balloons()
            st.markdown("### Quiz Results")
            correct_count = 0
            for i in range(len(st.session_state.quiz_data)):
                user_answer = st.session_state.user_answers[i] if i < len(st.session_state.user_answers) else None # Handle if user didn't answer all
                correct_answer = st.session_state.quiz_data[i]['correct_answer']
                if user_answer == correct_answer:
                    correct_count += 1
                    result_text = f"**Question {i+1}:** ✅ Correct! (Your answer: {user_answer}, Correct answer: {correct_answer})"
                    color = "green"
                else:
                    result_text = f"**Question {i+1}:** ❌ Incorrect. (Your answer: {user_answer if user_answer else 'Not answered'}, Correct answer: {correct_answer})"
                    color = "red"
                st.markdown(f"<font color='{color}'>{result_text}</font>", unsafe_allow_html=True)

            st.divider()

            percentage_correct = (correct_count / len(st.session_state.quiz_data)) * 100

            if percentage_correct > 75:
                border_color = "#9AD8E1"
                delta_text_color = "normal"
            else:
                border_color = "#E098B7"
                delta_text_color = "inverse"


            st.write(
                """
                <style>
                [data-testid="stMetricDelta"] svg {
                    display: none;
                }
                </style>
                """,
                unsafe_allow_html=True,
            )

            st.metric(label=f"**Final Score**", value=f"{correct_count} out of {len(st.session_state.quiz_data)}", delta=f"{percentage_correct:.0f}%", delta_color=delta_text_color)
            style_metric_cards(border_left_color=border_color, background_color="DEFAULT_BACKGROUND_COLOR")

            st.session_state.score = correct_count # Store score in session state


    # Sidebar topic input - Always visible
    sidebar_topic = st.sidebar.text_input("New Quiz Topic", value=st.session_state.get('quiz_topic', ''))
    if st.sidebar.button("Generate New Quiz"): # Button in sidebar to generate quiz, always resets
        st.session_state.quiz_started = True
        st.session_state.quiz_topic = sidebar_topic
        st.session_state.topic_from_sidebar = True # Flag to indicate topic from sidebar
        # Reset quiz state for new quiz
        st.session_state.quiz_data = None
        st.session_state.current_question_index = 0
        st.session_state.user_answers = []
        st.session_state.quiz_completed = False
        st.session_state.score = 0


    # Initial topic input in main area - Shown only if quiz not started
    if not st.session_state.quiz_started: # Modified condition here
        initial_topic = st.text_input("Enter the topic for your quiz:", value=st.session_state.get('quiz_topic', '') ) # Removed condition causing issue
        if st.button("Generate Quiz"): # Button to generate quiz from initial input
            if initial_topic:
                st.session_state.quiz_started = True
                st.session_state.quiz_topic = initial_topic
                st.session_state.topic_from_sidebar = False # Reset sidebar topic flag
                # Reset quiz state - although should be already reset if coming from sidebar, or first time
                st.session_state.quiz_data = None
                st.session_state.current_question_index = 0
                st.session_state.user_answers = []
                st.session_state.quiz_completed = False
                st.session_state.score = 0
            else:
                st.warning("Please enter a topic to generate a quiz.")


    # Quiz Generation Logic - Moved outside input blocks, runs after button presses
    topic_to_generate = None
    if st.session_state.quiz_started and st.session_state.quiz_data is None: # Generate only if quiz started and no data yet
        topic_to_generate = st.session_state.quiz_topic

    if topic_to_generate:
        with st.spinner(f"Generating quiz on '{topic_to_generate}'..."):
            try:
                prompt = f"""
                    Generate a multiple-choice quiz on the topic of "{topic_to_generate}".
                    The quiz should have 5 questions.

                    **Formatting Instructions:**

                    1. **Question Numbering:**  Start each question with a number followed by a period (e.g., "1.").
                    2. **Question Text:**  Immediately after the question number, write the question text.
                    3. **Options:**
                        * List four options for each question, labeled A, B, C, and D.
                        * Use the format: "A) Option text", "B) Option text", etc. (letter followed by a parenthesis, then a space, then the option text).
                        * Place each option on a new line directly below the question.
                    4. **Answer Key:**
                        * After all questions, create a separate section titled "**Answer Key:**".
                        * In the Answer Key, list the correct answer for each question in the format: "1. Correct option letter", "2. Correct option letter", etc. (question number, period, space, correct option letter - A, B, C, or D).

                    **Example of Desired Format:**

                    **Quiz Title (Optional, but good to have)**

                    1. Question 1 text?
                    A) Option A
                    B) Option B
                    C) Option C
                    D) Option D

                    2. Question 2 text?
                    A) Option A
                    B) Option B
                    C) Option C
                    D) Option D

                    ... (and so on for 5 questions) ...

                    **Answer Key:**
                    1. C
                    2. A
                    3. B
                    4. D
                    5. A

                    Please generate the quiz in this exact format.
                    """
                if st.session_state.selected_api == "Gemini API":
                    response = model.generate_content(prompt)
                    quiz_content = response.text
                elif st.session_state.selected_api == "OpenAI API":
                    response = openai.ChatCompletion.create(
                        model=st.session_state.openai_model,
                        messages=[{"role": "user", "content": prompt}]
                    )
                    quiz_content = response.choices[0].message.content


                parsed_quiz_data, answer_key = parse_quiz_content(quiz_content)


                if quiz_content: # Check if quiz_content was generated successfully (outer if)
                    if parsed_quiz_data: # Check if parsing was successful (inner if)
                        st.session_state.quiz_data = parsed_quiz_data
                        st.success(f"Quiz on '{topic_to_generate}' generated successfully! Let's begin.")
                    else: # else associated with inner if parsed_quiz_data
                        st.error("Failed to parse quiz content. Please try generating again.")
                        st.session_state.quiz_data = None
                else: # else associated with outer if quiz_content
                    st.error("Failed to generate quiz content. Please try again or check your API key.")

            except Exception as e:
                st.error(f"An error occurred: {e}")
                st.error("Please check your API key and network connection. If the problem persists, try a different topic or try again later.")
        st.rerun() # Rerun to update the quiz display based on new topic/generation


    # Quiz Display Logic
    if st.session_state.quiz_data:
        if not st.session_state.quiz_completed:
            display_question()
        else:
            display_results()
    elif not st.session_state.quiz_started and not st.session_state.quiz_data and not st.session_state.quiz_completed:
        st.info("Enter a topic in the main area and click 'Generate Quiz' to start, or use the sidebar to start a new quiz anytime.")


# --- Main App Flow Control ---
if 'logged_in' not in st.session_state:
    st.session_state.logged_in = False

if not st.session_state.logged_in:
    login_page()
else:
    quiz_app()