louiecerv commited on
Commit
3a35e19
·
2 Parent(s): 39c9ccf b39f888

Merge branch 'main' of https://huggingface.co/spaces/wvsuaidev/exam_maker_v1 into mylocal

Browse files
.gitattributes ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
Exam_Maker.py ADDED
@@ -0,0 +1,214 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import sqlite3
3
+ from passlib.hash import bcrypt
4
+ import pandas as pd
5
+ import re
6
+ import warnings
7
+ warnings.filterwarnings("ignore", message="module 'bcrypt' has no attribute '__about__'")
8
+ if "is_starting" not in st.session_state:
9
+ st.session_state["is_starting"] = True
10
+
11
+ if "authenticated" not in st.session_state:
12
+ st.session_state["authenticated"] = False
13
+
14
+ #from pages.About import show_about
15
+ #from pages.Text_prompt import show_text_prompt
16
+ #from pages.Multimodal import show_multimodal
17
+ #from pages.Settings import show_settings
18
+
19
+ if "authenticated" not in st.session_state:
20
+ st.session_state["authenticated"] = False
21
+
22
+ def create_usertable():
23
+ conn = sqlite3.connect('users.db')
24
+ c = conn.cursor()
25
+ c.execute('CREATE TABLE IF NOT EXISTS userstable(username TEXT, password BLOB)')
26
+ c.execute('CREATE TABLE IF NOT EXISTS system_instructions(username TEXT PRIMARY KEY, instruction TEXT)')
27
+ c.execute('CREATE TABLE IF NOT EXISTS user_prompts(id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT, prompt_time TEXT, prompt_type TEXT)')
28
+ conn.commit()
29
+ conn.close()
30
+
31
+ def add_userdata(username, password):
32
+ conn = sqlite3.connect('users.db')
33
+ c = conn.cursor()
34
+ c.execute('INSERT INTO userstable(username, password) VALUES (?,?)', (username, password))
35
+ conn.commit()
36
+ conn.close()
37
+
38
+ def login_user(username, password):
39
+ conn = sqlite3.connect('users.db')
40
+ c = conn.cursor()
41
+ c.execute('SELECT password FROM userstable WHERE username =?', (username,))
42
+ stored_hash = c.fetchone()
43
+ conn.close()
44
+
45
+ if stored_hash:
46
+ stored_hash = stored_hash[0]
47
+ return check_hashes(password, stored_hash)
48
+ else:
49
+ return False
50
+
51
+ def view_all_users():
52
+ conn = sqlite3.connect('users.db')
53
+ c = conn.cursor()
54
+ c.execute('SELECT * FROM userstable')
55
+ data = c.fetchall()
56
+ conn.close()
57
+ return data
58
+
59
+ # --- Hashing ---
60
+ def make_hashes(password):
61
+ return bcrypt.hash(password)
62
+
63
+ def check_hashes(password, hashed_text):
64
+ return bcrypt.verify(password, hashed_text)
65
+
66
+ # --- Authentication ---
67
+ def authenticate(username, password):
68
+ return login_user(username, password)
69
+
70
+ def logout():
71
+ del st.session_state["authenticated"]
72
+ del st.session_state["username"]
73
+ del st.session_state["page"]
74
+
75
+ # --- Initialize session state ---
76
+ if "authenticated" not in st.session_state:
77
+ st.session_state["authenticated"] = False
78
+ if "username" not in st.session_state:
79
+ st.session_state["username"] = None
80
+ if "page" not in st.session_state:
81
+ st.session_state["page"] = "login"
82
+
83
+ # --- Login page ---
84
+ def login_page():
85
+ st.title("WVSU Exam Maker")
86
+ st.subheader("User Login")
87
+ username = st.text_input("User Name")
88
+ password = st.text_input("Password", type='password')
89
+ if st.button("Login"):
90
+ result = authenticate(username.lower(), password)
91
+ if result:
92
+ st.session_state["authenticated"] = True
93
+ st.session_state["username"] = username
94
+ st.success("Logged In as {}".format(username))
95
+ st.session_state["page"] = "main"
96
+ st.session_state["is_starting"] = False
97
+ st.rerun()
98
+ else:
99
+ st.warning("Incorrect Username/Password")
100
+
101
+ st.write("Don't have an account? Click Signup.")
102
+ # --- Signup button ---
103
+ if st.button("Signup"):
104
+ st.session_state["page"] = "signup"
105
+ st.rerun()
106
+
107
+ # --- Signup page ---
108
+ def signup_page():
109
+ st.subheader("Create New Account")
110
+ new_user = st.text_input("Username")
111
+ new_password = st.text_input("Password", type='password')
112
+
113
+ # Display password requirements
114
+ st.write("Password Requirements:")
115
+ st.write("* Minimum length: 8 characters")
116
+ st.write("* Mix of uppercase and lowercase letters")
117
+ st.write("* At least one number")
118
+ st.write("* At least one special character")
119
+
120
+ # Validate password strength
121
+ col1, col2 = st.columns([1, 1])
122
+ if col1.button("Signup"):
123
+ password_strength = validate_password(new_password)
124
+ if password_strength:
125
+ # Check if username already exists
126
+ conn = sqlite3.connect('users.db')
127
+ c = conn.cursor()
128
+ c.execute('SELECT * FROM userstable WHERE username=?', (new_user,))
129
+ existing_user = c.fetchone()
130
+ conn.close()
131
+
132
+ if existing_user:
133
+ st.error("Username already exists. Please choose a different username.")
134
+ else:
135
+ hashed_new_password = make_hashes(new_password.encode("utf-8"))
136
+ add_userdata(new_user, hashed_new_password)
137
+ st.success("You have successfully created a valid Account")
138
+ st.info("Go to Login Menu to login")
139
+ st.session_state["page"] = "login"
140
+ st.rerun()
141
+ else:
142
+ st.error("Password does not meet the requirements.")
143
+ if col2.button("Cancel"):
144
+ st.session_state["page"] = "login"
145
+ st.rerun()
146
+
147
+ # --- Validate password strength ---
148
+ def validate_password(password):
149
+ # Define password requirements
150
+ min_length = 8
151
+ has_uppercase = re.search(r"[A-Z]", password)
152
+ has_lowercase = re.search(r"[a-z]", password)
153
+ has_number = re.search(r"\d", password)
154
+ has_symbol = re.search(r"[!@#$%^&*()_+=-{};:'<>,./?]", password)
155
+
156
+ # Check if password meets all requirements
157
+ if (len(password) >= min_length and
158
+ has_uppercase and
159
+ has_lowercase and
160
+ has_number and
161
+ has_symbol):
162
+ return True
163
+ else:
164
+ return False
165
+
166
+ # --- Manage users page ---
167
+ def manage_users_page():
168
+ st.subheader("User Management")
169
+ user_result = view_all_users()
170
+ clean_db = pd.DataFrame(user_result, columns=["Username", "Password"])
171
+ st.dataframe(clean_db)
172
+
173
+ # --- Main app ---
174
+ def main():
175
+ create_usertable()
176
+
177
+ if st.session_state["page"] == "login":
178
+ login_page()
179
+ elif st.session_state["page"] == "signup":
180
+ signup_page()
181
+ else:
182
+
183
+ msg = """
184
+ # Welcome to the WVSU Exam Maker!
185
+
186
+ We are excited to introduce you to the WVSU Exam Maker, a cutting-edge tool designed to assist faculty members of West Visayas State University in creating comprehensive exams with ease.
187
+
188
+ ### Empowering Teachers, Enhancing Education
189
+
190
+ With the WVSU Exam Maker, you can generate high-quality exam questions in various formats, saving you time and effort. Our innovative app leverages the latest AI technology from Google Gemini 2 to help you create exams that are both effective and engaging.
191
+
192
+ ### Explore the Possibilities
193
+
194
+ • **Streamline exam creation**: Generate questions in multiple formats, including Multiple Choice, True or False, Short Response, and Essay.
195
+ • **Enhance exam accuracy**: Review and refine AI-generated questions to ensure accuracy and relevance.
196
+ • **Simplify exam preparation**: Use our intuitive interface to define exam requirements and upload reference materials.
197
+
198
+ ### Get Started Today!
199
+
200
+ We invite you to explore the WVSU Exam Maker and discover how it can support your teaching and assessment needs.
201
+
202
+ Thank you for using the WVSU Exam Maker!
203
+ """
204
+ st.markdown(msg)
205
+
206
+ # Display username and logout button on every page
207
+ st.sidebar.write(f"Welcome, {st.session_state['username']}")
208
+ if st.sidebar.button("Logout"):
209
+ logout()
210
+ st.rerun()
211
+
212
+
213
+ if __name__ == "__main__":
214
+ main()
README.md ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: WVSU Exam Maker
3
+ emoji: 📝
4
+ colorFrom: purple
5
+ colorTo: purple
6
+ sdk: streamlit
7
+ sdk_version: 1.41.1
8
+ app_file: Exam_Maker.py
9
+ pinned: false
10
+ license: apache-2.0
11
+ short_description: App that uses user authentication
12
+ ---
13
+
14
+ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
15
+
16
+ # WVSU Exam Maker
17
+ =====================
18
+
19
+ A cutting-edge tool designed to assist faculty members of West Visayas State University in creating comprehensive exams with ease.
20
+
21
+ ## Table of Contents
22
+ -----------------
23
+
24
+ * [Features](#features)
25
+ * [Getting Started](#getting-started)
26
+ * [Important Note](#important-note)
27
+ * [Development Team](#development-team)
28
+
29
+ ## Features
30
+ --------
31
+
32
+ * Generate high-quality exam questions in various formats, including:
33
+ + Multiple Choice
34
+ + True or False
35
+ + Short Response
36
+ + Essay
37
+ * Leverage the latest AI technology from Google Gemini 2 to create effective and engaging exams
38
+ * Streamline exam creation and simplify exam preparation
39
+
40
+ ## Getting Started
41
+ ---------------
42
+
43
+ 1. Define exam requirements using our intuitive interface
44
+ 2. Upload reference materials, such as lecture notes and tables of specifications
45
+ 3. Generate exam questions in various formats
46
+
47
+ ## Important Note
48
+ --------------
49
+
50
+ While the WVSU Exam Maker utilizes advanced AI technology, it is essential to review the output carefully, as AI can make mistakes. User supervision is necessary to ensure accuracy.
51
+
52
+ ## Development Team
53
+ ------------------
54
+
55
+ The WVSU Exam Maker was developed by the AI Research Team of the Management Information System Office.
pages/1_About.py ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+
3
+ def show_about():
4
+ about = """
5
+ ### WVSU Exam Maker: A Faculty Guide
6
+
7
+ The WVSU Exam Maker is a cutting-edge tool designed to assist faculty members of West Visayas State University in creating comprehensive exams with ease. Leveraging the latest AI technology from Google Gemini 2, this innovative app helps teachers generate questions in various formats, including:
8
+
9
+
10
+ * **Multiple Choice**: Assess students' knowledge with objective, structured questions.
11
+ * **True or False**: Evaluate students' understanding with concise, binary questions.
12
+ * **Short Response**: Encourage students to provide brief, written answers.
13
+ * **Essay**: Foster critical thinking and in-depth writing with longer, more open-ended questions.
14
+
15
+
16
+ ## Key Features
17
+
18
+ ### Text Prompt Page
19
+ Define exam requirements with precision using various input options.
20
+
21
+
22
+ ### Multimodal Prompt Page
23
+ Upload reference documents (PDF or image) to generate questions, including:
24
+
25
+
26
+ * Lecture materials
27
+ * Tables of specifications
28
+ * Rubrics
29
+ * Other relevant inputs
30
+
31
+
32
+ ## Important Note
33
+ While the WVSU Exam Maker utilizes advanced AI technology, it is essential to review the output carefully, as AI can make mistakes. User supervision is necessary to ensure accuracy.
34
+
35
+ ## Development Team
36
+ The WVSU Exam Maker was developed by the AI Research Team of the Management Information System Office.
37
+ """
38
+ # Add your About page content here
39
+ st.markdown(about)
40
+
41
+ if st.session_state["authenticated"]:
42
+ show_about()
43
+ else:
44
+ if not st.session_state["is_starting"]:
45
+ st.write("You are not authenticated. Please log in to access this page.")
pages/2_Text_prompt.py ADDED
@@ -0,0 +1,594 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import sqlite3
3
+ import time
4
+ import datetime
5
+ from PIL import Image
6
+ import google.generativeai as genai
7
+ import os
8
+ from reportlab.pdfgen import canvas
9
+ from reportlab.lib.pagesizes import A4, letter
10
+ from io import BytesIO
11
+ import tempfile
12
+ import json
13
+ import re
14
+ from reportlab.platypus import Paragraph, Frame, Spacer
15
+ from reportlab.lib.styles import getSampleStyleSheet
16
+ import shutil
17
+
18
+ MODEL_ID = "gemini-2.0-flash-exp"
19
+ api_key = os.getenv("GEMINI_API_KEY")
20
+ model_id = MODEL_ID
21
+ genai.configure(api_key=api_key)
22
+ enable_stream = False
23
+
24
+ if "model" not in st.session_state:
25
+ st.session_state.model = genai.GenerativeModel(MODEL_ID)
26
+
27
+ if "chat" not in st.session_state:
28
+ st.session_state.chat = st.session_state.model.start_chat()
29
+
30
+ def get_system_instruction(username):
31
+ """ Retrieves the system instruction for the user from the database. """
32
+ conn = sqlite3.connect('users.db')
33
+ c = conn.cursor()
34
+ c.execute('SELECT instruction FROM system_instructions WHERE username=?', (username,))
35
+ instruction = c.fetchone()
36
+ conn.close()
37
+ if instruction:
38
+ return instruction[0]
39
+ else:
40
+ return "Default system instruction."
41
+
42
+ def save_user_prompt(username, prompt_time, prompt_type):
43
+ """ Saves the user prompt to the database for monitoring purposes. """
44
+
45
+ conn = sqlite3.connect('users.db')
46
+ c = conn.cursor()
47
+ c.execute('INSERT INTO user_prompts(username, prompt_time, prompt_type) VALUES (?,?,?)', (username, prompt_time, prompt_type))
48
+ conn.commit()
49
+ conn.close()
50
+
51
+ def merge_json_strings(json_str1, json_str2):
52
+ """
53
+ Merges two JSON strings into one, handling potential markdown tags.
54
+
55
+ Args:
56
+ json_str1: The first JSON string, potentially with markdown tags.
57
+ json_str2: The second JSON string, potentially with markdown tags.
58
+
59
+ Returns:
60
+ A cleaned JSON string representing the merged JSON objects.
61
+ """
62
+
63
+ # Clean the JSON strings by removing markdown tags
64
+ cleaned_json_str1 = _clean_markdown(json_str1)
65
+ cleaned_json_str2 = _clean_markdown(json_str2)
66
+
67
+ try:
68
+ # Parse the cleaned JSON strings into Python dictionaries
69
+ data1 = json.loads(cleaned_json_str1)
70
+ data2 = json.loads(cleaned_json_str2)
71
+
72
+ # Merge the dictionaries
73
+ merged_data = _merge_dicts(data1, data2)
74
+
75
+ # Convert the merged dictionary back into a JSON string
76
+ return json.dumps(merged_data, indent=2)
77
+ except json.JSONDecodeError as e:
78
+ return f"Error decoding JSON: {e}"
79
+
80
+
81
+ def _clean_markdown(text):
82
+ """
83
+ Removes markdown tags from a string if they exist.
84
+ Otherwise, returns the original string unchanged.
85
+
86
+ Args:
87
+ text: The input string.
88
+
89
+ Returns:
90
+ The string with markdown tags removed, or the original string
91
+ if no markdown tags were found.
92
+ """
93
+ try:
94
+ # Check if the string contains markdown
95
+ if re.match(r"^```json\s*", text) and re.search(r"\s*```$", text):
96
+ # Remove leading ```json
97
+ text = re.sub(r"^```json\s*", "", text)
98
+ # Remove trailing ```
99
+ text = re.sub(r"\s*```$", "", text)
100
+ return text
101
+ except Exception as e:
102
+ # Log the error
103
+ st.error(f"Error cleaning markdown: {e}")
104
+ return None
105
+
106
+ def _merge_dicts(data1, data2):
107
+ """
108
+ Recursively merges two data structures.
109
+
110
+ Handles merging of dictionaries and lists.
111
+ For dictionaries, if a key exists in both and both values are dictionaries
112
+ or lists, they are merged recursively. Otherwise, the value from data2 is used.
113
+ For lists, the lists are concatenated.
114
+
115
+ Args:
116
+ data1: The first data structure (dictionary or list).
117
+ data2: The second data structure (dictionary or list).
118
+
119
+ Returns:
120
+ The merged data structure.
121
+
122
+ Raises:
123
+ ValueError: If the data types are not supported for merging.
124
+ """
125
+ if isinstance(data1, dict) and isinstance(data2, dict):
126
+ for key, value in data2.items():
127
+ if key in data1 and isinstance(data1[key], (dict, list)) and isinstance(value, type(data1[key])):
128
+ _merge_dicts(data1[key], value)
129
+ else:
130
+ data1[key] = value
131
+ return data1
132
+ elif isinstance(data1, list) and isinstance(data2, list):
133
+ return data1 + data2
134
+ else:
135
+ raise ValueError("Unsupported data types for merging")
136
+
137
+ def create_json(metadata, content):
138
+ """
139
+ Creates a JSON string combining metadata and content.
140
+
141
+ Args:
142
+ metadata: A dictionary containing metadata information.
143
+ content: A dictionary containing the quiz content.
144
+
145
+ Returns:
146
+ A string representing the combined JSON data.
147
+ """
148
+
149
+ # Create metadata with timestamp
150
+ metadata = {
151
+ "subject": metadata.get("subject", ""),
152
+ "topic": metadata.get("topic", ""),
153
+ "num_questions": metadata.get("num_questions", 0),
154
+ "exam_type": metadata.get("exam_type", ""),
155
+ "timestamp": datetime.datetime.now().isoformat()
156
+ }
157
+
158
+ # Combine metadata and content
159
+ combined_data = {"metadata": metadata, "content": content}
160
+
161
+ # Convert to JSON string
162
+ json_string = json.dumps(combined_data, indent=4)
163
+
164
+ return json_string
165
+
166
+ def create_pdf(data):
167
+ """
168
+ Creates a PDF file with text wrapping for quiz content, supporting multiple question types.
169
+ """
170
+ try:
171
+ # Load the JSON data
172
+ data = json.loads(data)
173
+
174
+ if 'metadata' not in data or 'content' not in data:
175
+ st.error("Error: Invalid data format. Missing 'metadata' or 'content' keys.")
176
+ return None
177
+
178
+ metadata = data['metadata']
179
+ content = data['content']
180
+
181
+ # Validate metadata
182
+ required_metadata_keys = ['subject', 'topic', 'exam_type', 'num_questions']
183
+ if not all(key in metadata for key in required_metadata_keys):
184
+ st.error("Error: Invalid metadata format. Missing required keys.")
185
+ return None
186
+
187
+ # Create a unique filename with timestamp
188
+ timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
189
+ pdf_filename = f"quiz_output_{timestamp}.pdf"
190
+ temp_dir = tempfile.gettempdir()
191
+ pdf_path = os.path.join(temp_dir, pdf_filename)
192
+
193
+ c = canvas.Canvas(pdf_path, pagesize=A4)
194
+ c.setFont("Helvetica", 10)
195
+
196
+ styles = getSampleStyleSheet()
197
+ text_style = styles['Normal']
198
+
199
+ # Starting position
200
+ margin_left = 50
201
+ y_position = 750
202
+ line_height = 12 # Adjusted for tighter spacing
203
+ frame_width = 500
204
+ first_page = True
205
+
206
+ def wrap_text_draw(text, x, y):
207
+ """
208
+ Wraps and draws text using ReportLab's Paragraph for automatic line breaks.
209
+ """
210
+ p = Paragraph(text, text_style)
211
+ width, height = p.wrap(frame_width, y)
212
+ p.drawOn(c, x, y - height)
213
+ return height
214
+
215
+ # Print metadata once on the first page
216
+ if first_page:
217
+ for key, label in [("subject", "Subject"), ("topic", "Topic"),
218
+ ("exam_type", "Type"), ("num_questions", "Number of Questions")]:
219
+ c.drawString(margin_left, y_position, f"{label}: {metadata[key]}")
220
+ y_position -= line_height
221
+ y_position -= line_height
222
+ first_page = False
223
+
224
+ # Render questions and options
225
+ for idx, q in enumerate(content):
226
+ if not isinstance(q, dict):
227
+ st.error(f"Error: Invalid question format at index {idx}. Skipping...")
228
+ continue
229
+
230
+ question_text = f"{idx + 1}. {q.get('question', q.get('statement', ''))}"
231
+ height = wrap_text_draw(question_text, margin_left, y_position)
232
+ y_position -= (height + line_height)
233
+
234
+ if y_position < 50:
235
+ c.showPage()
236
+ c.setFont("Helvetica", 10)
237
+ y_position = 750
238
+
239
+ # Handle specific exam types
240
+ exam_type = metadata['exam_type']
241
+
242
+ if exam_type == "Multiple Choice":
243
+ for option_idx, option in enumerate(q['options'], ord('a')):
244
+ option_text = f"{chr(option_idx)}) {option}"
245
+ height = wrap_text_draw(option_text, margin_left + 20, y_position)
246
+ y_position -= (height + line_height)
247
+
248
+ if y_position < 50:
249
+ c.showPage()
250
+ c.setFont("Helvetica", 10)
251
+ y_position = 750
252
+
253
+ # Print correct answer
254
+ correct_answer_text = f"Correct Answer: {q['correct_answer']}"
255
+ height = wrap_text_draw(correct_answer_text, margin_left + 20, y_position)
256
+ y_position -= (height + line_height)
257
+
258
+ elif exam_type == "True or False":
259
+ for option in q['options']:
260
+ height = wrap_text_draw(option, margin_left + 20, y_position)
261
+ y_position -= (height + line_height)
262
+
263
+ if y_position < 50:
264
+ c.showPage()
265
+ c.setFont("Helvetica", 10)
266
+ y_position = 750
267
+
268
+ correct_answer_text = f"Correct Answer: {q['correct_answer']}"
269
+ height = wrap_text_draw(correct_answer_text, margin_left + 20, y_position)
270
+ y_position -= (height + line_height)
271
+
272
+ elif exam_type in ["Short Response", "Essay Type"]:
273
+ answer_text = f"Correct Answer: {q['correct_answer']}"
274
+ height = wrap_text_draw(answer_text, margin_left + 20, y_position)
275
+ y_position -= (height + line_height)
276
+
277
+ if y_position < 50:
278
+ c.showPage()
279
+ c.setFont("Helvetica", 10)
280
+ y_position = 750
281
+
282
+ # Add a footer
283
+ notice = "This exam was generated by the WVSU Exam Maker (c) 2025 West Visayas State University"
284
+ c.drawString(margin_left, y_position, notice)
285
+
286
+ c.save()
287
+ return pdf_path
288
+
289
+ except Exception as e:
290
+ st.error(f"Error creating PDF: {e}")
291
+ return None
292
+
293
+
294
+ def generate_quiz_content(data):
295
+ """
296
+ Separates the metadata and content from a JSON string containing exam data.
297
+ Creates a markdown formatted text that contains the exam metadata and
298
+ enumerates the questions, options and answers nicely formatted for readability.
299
+
300
+ Args:
301
+ data: A JSON string containing the exam data.
302
+
303
+ Returns:
304
+ A markdown formatted string.
305
+ """
306
+ data = json.loads(data)
307
+ metadata = data["metadata"]
308
+ content = data["content"]
309
+ exam_type = metadata["exam_type"]
310
+ if exam_type == "Multiple Choice":
311
+ md_text = f"""# {metadata['subject']} - {metadata['topic']}
312
+
313
+ **Exam Type:** {metadata['exam_type']}
314
+ **Number of Questions:** {metadata['num_questions']}
315
+ **Timestamp:** {metadata['timestamp']}
316
+
317
+ ---
318
+
319
+ """
320
+ for i, q in enumerate(content):
321
+ md_text += f"""Question {i+1}:
322
+ {q['question']}
323
+
324
+ """
325
+ for j, option in enumerate(q['options'], ord('a')):
326
+ md_text += f"""{chr(j)}. {option}
327
+
328
+ """
329
+ md_text += f"""**Correct Answer:** {q['correct_answer']}
330
+
331
+ ---
332
+
333
+ """
334
+ md_text += """This exam was generated by the WVSU Exam Maker
335
+ (c) 2025 West Visayas State University
336
+ """
337
+
338
+ elif exam_type == "True or False":
339
+ md_text = f"""# {metadata['subject']} - {metadata['topic']}
340
+
341
+ **Exam Type:** {metadata['exam_type']}
342
+ **Number of Questions:** {metadata['num_questions']}
343
+ **Timestamp:** {metadata['timestamp']}
344
+
345
+ ---
346
+
347
+ """
348
+
349
+ for i, q in enumerate(content):
350
+ md_text += f"""Statement {i+1}:
351
+
352
+ {q['statement']}
353
+
354
+ """
355
+ for j, option in enumerate(q['options'], ord('a')):
356
+ md_text += f"""{option}
357
+ """
358
+
359
+ md_text += f"""**Correct Answer:** {q['correct_answer']}
360
+
361
+ ---
362
+ """
363
+ md_text += """This exam was generated by the WVSU Exam Maker
364
+ (c) 2025 West Visayas State University"""
365
+
366
+ elif exam_type == "Short Response" or exam_type == "Essay Type":
367
+ md_text = f"""# {metadata['subject']} - {metadata['topic']}
368
+
369
+ **Exam Type:** {metadata['exam_type']}
370
+ **Number of Questions:** {metadata['num_questions']}
371
+ **Timestamp:** {metadata['timestamp']}
372
+
373
+ ---
374
+
375
+ """
376
+
377
+ for i, q in enumerate(content):
378
+ md_text += f"""Question {i+1}:
379
+
380
+ {q['question']}
381
+
382
+ """
383
+ md_text += f"""**Correct Answer:** {q['correct_answer']}
384
+
385
+ ---
386
+ """
387
+ md_text += """This exam was generated by the WVSU Exam Maker
388
+ (c) 2025 West Visayas State University"""
389
+
390
+ return md_text
391
+
392
+ def generate_metadata(subject, topic, num_questions, exam_type):
393
+ """Generates quiz metadata as a dictionary combining num_questions,
394
+ exam_type, and timestamp.
395
+
396
+ Args:
397
+ num_questions: The number of questions in the exam (int).
398
+ exam_type: The type of exam (str).
399
+
400
+ Returns:
401
+ A dictionary containing the quiz metadata.
402
+ """
403
+
404
+ # Format the timestamp
405
+ timestamp = datetime.datetime.now()
406
+ formatted_timestamp = timestamp.strftime("%Y-%m-%d %H:%M:%S")
407
+
408
+ metadata = {
409
+ "subject": subject,
410
+ "topic": topic,
411
+ "num_questions": num_questions,
412
+ "exam_type": exam_type,
413
+ "timestamp": formatted_timestamp
414
+ }
415
+
416
+ return metadata
417
+
418
+ def generate_text(prompt):
419
+ """Generates text based on the prompt."""
420
+ try:
421
+
422
+ # Send a text prompt to Gemini API
423
+ chat = st.session_state.chat
424
+ response = chat.send_message(
425
+ [
426
+ prompt
427
+ ],
428
+ stream=enable_stream
429
+ )
430
+
431
+ return response.text
432
+
433
+ except Exception as e:
434
+ st.error(f"An error occurred while generating text: {e}")
435
+ return None
436
+
437
+ def show_text_prompt():
438
+ st.subheader("Text Prompt")
439
+
440
+ username = st.session_state["username"]
441
+ st.write(f"Welcome, {username}! This page allows you to generate questions based on user inputs.")
442
+
443
+ # Display username and logout button on every page
444
+ st.sidebar.write(f"Current user: {st.session_state['username']}")
445
+
446
+ # User inputs
447
+ # Course selection
448
+ course = st.text_input("Enter Course",
449
+ "e.g.,Bachelor of Secondary Education")
450
+
451
+ # Year level selection
452
+ year_level = st.selectbox("Select Year Level",
453
+ ["1st Year",
454
+ "2nd Year",
455
+ "3rd Year",
456
+ "4th Year"])
457
+
458
+ # Subject selection
459
+ subject = st.text_input("Enter Subject",
460
+ "e.g.,The Teaching Profession, Facilitating Learner-Centered Teaching")
461
+
462
+ # Topic selection
463
+ topic = st.text_input("Enter Topic",
464
+ "e.g., Teacher as a professional, Introduction to Learner-Centered Teaching")
465
+
466
+ # Question type selection
467
+ question_type = st.selectbox("Select Question Type",
468
+ ["Multiple Choice",
469
+ "True or False",
470
+ "Short Response",
471
+ "Essay Type"])
472
+
473
+ difficulty = st.selectbox("Select Difficulty",["easy","average","hard"])
474
+
475
+ #number of questions to generate
476
+ if question_type != "Essay Type":
477
+ num_questions = st.selectbox("Number of Questions to Generate",
478
+ [10, 20, 30, 40, 50])
479
+ else:
480
+ num_questions = st.selectbox("Number of Questions to Generate",
481
+ [1, 2, 3, 4, 5])
482
+
483
+ # Combine user inputs into a prompt
484
+ prompt = f"""Refer to the uploaded document. Generate a {question_type} question for a {year_level} {course} student
485
+ in {subject} on the topic of {topic} with a {difficulty} difficulty level.
486
+ The questions should require higher order thinking skills.
487
+ """
488
+
489
+ if question_type == "Multiple Choice":
490
+ prompt += """Provide 4 choices. Provide the correct answer in the format 'Answer: A'.
491
+ Use the following JSON format for each question:
492
+ [{
493
+ "question": "Your question here?",
494
+ "options": ["Option A", "Option B", "Option C", "Option D"],
495
+ "correct_answer": "full text of the correct answer"
496
+ }, ... more questions]
497
+ Ensure that the response only contains the JSON array of questions and nothing else.
498
+ """
499
+ elif question_type == "True or False":
500
+ prompt += """Indicate whether the statement is true or false. Keep the statement brief and concise.
501
+ Use the following JSON format for each question:
502
+ [{
503
+ "statement": "Your statement here",
504
+ "options": ["True", "False"],
505
+ "correct_answer": True"
506
+ }, ... more questions]
507
+ Ensure that the response only contains the JSON array of questions and nothing else.
508
+ """
509
+ elif question_type == "Short Response":
510
+ prompt += """Create question that require a word or short phrase as answer. Use the following JSON format for each question:
511
+ [{
512
+ "question": "Your question here?",
513
+ "correct_answer": A word or phrase"
514
+ }, ... more questions]
515
+ Ensure that the response only contains the JSON array of questions and nothing else.
516
+ """
517
+ elif question_type == "Essay Type":
518
+ prompt += """Create questions that require a short essay between 300 to 500 words.
519
+ Provide a detailed answer. Use the following JSON format for each question:
520
+ [{
521
+ "question": "Your question here?",
522
+ "correct_answer": The essay answer goes here."
523
+ }, ... more questions]
524
+ Ensure that the response only contains the JSON array of questions and nothing else.
525
+ """
526
+
527
+ if not question_type == "Essay Type":
528
+ prompt += f"Generate 10 questions. Do not repeat questions you have already given in previous prompts. Exclude markdown tags in the response."
529
+ else:
530
+ prompt += f" Generate {num_questions} questions. Do not repeat questions you have already given in previous prompts. Exclude markdown tags in the response"
531
+
532
+ full_quiz = ""
533
+
534
+ # Send button
535
+ if st.button("Generate Questions"):
536
+
537
+ if question_type == "Essay Type":
538
+ #prompt once
539
+ with st.spinner('Generating questions...'):
540
+ full_quiz = _clean_markdown(generate_text(prompt))
541
+
542
+ else:
543
+ if num_questions == 10:
544
+
545
+ #prompt once
546
+ with st.spinner('Generating questions...'):
547
+ full_quiz = _clean_markdown(generate_text(prompt))
548
+ else:
549
+ #prompt multiple times
550
+ times = num_questions//10
551
+ for i in range(times):
552
+ with st.spinner('Generating questions...'):
553
+ response = generate_text(prompt)
554
+
555
+ if i==0:
556
+ full_quiz = _clean_markdown(response)
557
+ else:
558
+ full_quiz = merge_json_strings(full_quiz, response)
559
+
560
+ metadata = generate_metadata(subject, topic, num_questions, question_type)
561
+
562
+ try:
563
+ # Attempt to load the string as JSON to validate it
564
+ content = json.loads(full_quiz)
565
+ except json.JSONDecodeError:
566
+ st.error("Error: Invalid JSON string for quiz content.")
567
+ st.stop()
568
+
569
+ json_string = create_json(metadata, content)
570
+
571
+ quiz_markdown = generate_quiz_content(json_string)
572
+ st.markdown(quiz_markdown)
573
+
574
+ pdf_path = create_pdf(json_string)
575
+
576
+ if pdf_path:
577
+ """Click the button to download the generated PDF."""
578
+ try:
579
+ with open(pdf_path, "rb") as f:
580
+ st.download_button("Download PDF", f, file_name=os.path.basename(pdf_path))
581
+ except Exception as e:
582
+ st.error(f"Error handling file download: {e}")
583
+ else:
584
+ st.error("Failed to generate the PDF. Please try again.")
585
+
586
+ #record the prompt for monitoring
587
+ save_user_prompt(username, datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "Multimodal")
588
+
589
+ if st.session_state["authenticated"]:
590
+ show_text_prompt()
591
+
592
+ else:
593
+ if not st.session_state["is_starting"]:
594
+ st.write("You are not authenticated. Please log in to access this page.")
pages/3_Multimodal.py ADDED
@@ -0,0 +1,639 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import sqlite3
3
+ import time
4
+ import datetime
5
+ from PIL import Image
6
+ import google.generativeai as genai
7
+ import os
8
+ from reportlab.pdfgen import canvas
9
+ from reportlab.lib.pagesizes import A4, letter
10
+ from io import BytesIO
11
+ import tempfile
12
+ import json
13
+ import re
14
+ from reportlab.platypus import Paragraph, Frame, Spacer
15
+ from reportlab.lib.styles import getSampleStyleSheet
16
+ import shutil
17
+
18
+ MODEL_ID = "gemini-2.0-flash-exp"
19
+ api_key = os.getenv("GEMINI_API_KEY")
20
+ model_id = MODEL_ID
21
+ genai.configure(api_key=api_key)
22
+ enable_stream = False
23
+
24
+ if "model" not in st.session_state:
25
+ st.session_state.model = genai.GenerativeModel(MODEL_ID)
26
+
27
+ if "chat" not in st.session_state:
28
+ st.session_state.chat = st.session_state.model.start_chat()
29
+
30
+ if "is_new_file" not in st.session_state:
31
+ st.session_state.is_new_file = True
32
+
33
+ def get_system_instruction(username):
34
+ """ Retrieves the system instruction for the user from the database. """
35
+ conn = sqlite3.connect('users.db')
36
+ c = conn.cursor()
37
+ c.execute('SELECT instruction FROM system_instructions WHERE username=?', (username,))
38
+ instruction = c.fetchone()
39
+ conn.close()
40
+ if instruction:
41
+ return instruction[0]
42
+ else:
43
+ return "Default system instruction."
44
+
45
+ def save_user_prompt(username, prompt_time, prompt_type):
46
+ """ Saves the user prompt to the database for monitoring purposes. """
47
+
48
+ conn = sqlite3.connect('users.db')
49
+ c = conn.cursor()
50
+ c.execute('INSERT INTO user_prompts(username, prompt_time, prompt_type) VALUES (?,?,?)', (username, prompt_time, prompt_type))
51
+ conn.commit()
52
+ conn.close()
53
+
54
+ def merge_json_strings(json_str1, json_str2):
55
+ """
56
+ Merges two JSON strings into one, handling potential markdown tags.
57
+
58
+ Args:
59
+ json_str1: The first JSON string, potentially with markdown tags.
60
+ json_str2: The second JSON string, potentially with markdown tags.
61
+
62
+ Returns:
63
+ A cleaned JSON string representing the merged JSON objects.
64
+ """
65
+
66
+ # Clean the JSON strings by removing markdown tags
67
+ cleaned_json_str1 = _clean_markdown(json_str1)
68
+ cleaned_json_str2 = _clean_markdown(json_str2)
69
+
70
+ try:
71
+ # Parse the cleaned JSON strings into Python dictionaries
72
+ data1 = json.loads(cleaned_json_str1)
73
+ data2 = json.loads(cleaned_json_str2)
74
+
75
+ # Merge the dictionaries
76
+ merged_data = _merge_dicts(data1, data2)
77
+
78
+ # Convert the merged dictionary back into a JSON string
79
+ return json.dumps(merged_data, indent=2)
80
+ except json.JSONDecodeError as e:
81
+ return f"Error decoding JSON: {e}"
82
+
83
+
84
+ def _clean_markdown(text):
85
+ """
86
+ Removes markdown tags from a string if they exist.
87
+ Otherwise, returns the original string unchanged.
88
+
89
+ Args:
90
+ text: The input string.
91
+
92
+ Returns:
93
+ The string with markdown tags removed, or the original string
94
+ if no markdown tags were found.
95
+ """
96
+ try:
97
+ # Check if the string contains markdown
98
+ if re.match(r"^```json\s*", text) and re.search(r"\s*```$", text):
99
+ # Remove leading ```json
100
+ text = re.sub(r"^```json\s*", "", text)
101
+ # Remove trailing ```
102
+ text = re.sub(r"\s*```$", "", text)
103
+ return text
104
+ except Exception as e:
105
+ # Log the error
106
+ st.error(f"Error cleaning markdown: {e}")
107
+ return None
108
+
109
+ def _merge_dicts(data1, data2):
110
+ """
111
+ Recursively merges two data structures.
112
+
113
+ Handles merging of dictionaries and lists.
114
+ For dictionaries, if a key exists in both and both values are dictionaries
115
+ or lists, they are merged recursively. Otherwise, the value from data2 is used.
116
+ For lists, the lists are concatenated.
117
+
118
+ Args:
119
+ data1: The first data structure (dictionary or list).
120
+ data2: The second data structure (dictionary or list).
121
+
122
+ Returns:
123
+ The merged data structure.
124
+
125
+ Raises:
126
+ ValueError: If the data types are not supported for merging.
127
+ """
128
+ if isinstance(data1, dict) and isinstance(data2, dict):
129
+ for key, value in data2.items():
130
+ if key in data1 and isinstance(data1[key], (dict, list)) and isinstance(value, type(data1[key])):
131
+ _merge_dicts(data1[key], value)
132
+ else:
133
+ data1[key] = value
134
+ return data1
135
+ elif isinstance(data1, list) and isinstance(data2, list):
136
+ return data1 + data2
137
+ else:
138
+ raise ValueError("Unsupported data types for merging")
139
+
140
+ def create_json(metadata, content):
141
+ """
142
+ Creates a JSON string combining metadata and content.
143
+
144
+ Args:
145
+ metadata: A dictionary containing metadata information.
146
+ content: A dictionary containing the quiz content.
147
+
148
+ Returns:
149
+ A string representing the combined JSON data.
150
+ """
151
+
152
+ # Create metadata with timestamp
153
+ metadata = {
154
+ "subject": metadata.get("subject", ""),
155
+ "topic": metadata.get("topic", ""),
156
+ "num_questions": metadata.get("num_questions", 0),
157
+ "exam_type": metadata.get("exam_type", ""),
158
+ "timestamp": datetime.datetime.now().isoformat()
159
+ }
160
+
161
+ # Combine metadata and content
162
+ combined_data = {"metadata": metadata, "content": content}
163
+
164
+ # Convert to JSON string
165
+ json_string = json.dumps(combined_data, indent=4)
166
+
167
+ return json_string
168
+
169
+ def create_pdf(data):
170
+ """
171
+ Creates a PDF file with text wrapping for quiz content, supporting multiple question types.
172
+ """
173
+ try:
174
+ # Load the JSON data
175
+ data = json.loads(data)
176
+
177
+ if 'metadata' not in data or 'content' not in data:
178
+ st.error("Error: Invalid data format. Missing 'metadata' or 'content' keys.")
179
+ return None
180
+
181
+ metadata = data['metadata']
182
+ content = data['content']
183
+
184
+ # Validate metadata
185
+ required_metadata_keys = ['subject', 'topic', 'exam_type', 'num_questions']
186
+ if not all(key in metadata for key in required_metadata_keys):
187
+ st.error("Error: Invalid metadata format. Missing required keys.")
188
+ return None
189
+
190
+ # Create a unique filename with timestamp
191
+ timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
192
+ pdf_filename = f"quiz_output_{timestamp}.pdf"
193
+ temp_dir = tempfile.gettempdir()
194
+ pdf_path = os.path.join(temp_dir, pdf_filename)
195
+
196
+ c = canvas.Canvas(pdf_path, pagesize=A4)
197
+ c.setFont("Helvetica", 10)
198
+
199
+ styles = getSampleStyleSheet()
200
+ text_style = styles['Normal']
201
+
202
+ # Starting position
203
+ margin_left = 50
204
+ y_position = 750
205
+ line_height = 12 # Adjusted for tighter spacing
206
+ frame_width = 500
207
+ first_page = True
208
+
209
+ def wrap_text_draw(text, x, y):
210
+ """
211
+ Wraps and draws text using ReportLab's Paragraph for automatic line breaks.
212
+ """
213
+ p = Paragraph(text, text_style)
214
+ width, height = p.wrap(frame_width, y)
215
+ p.drawOn(c, x, y - height)
216
+ return height
217
+
218
+ # Print metadata once on the first page
219
+ if first_page:
220
+ for key, label in [("subject", "Subject"), ("topic", "Topic"),
221
+ ("exam_type", "Type"), ("num_questions", "Number of Questions")]:
222
+ c.drawString(margin_left, y_position, f"{label}: {metadata[key]}")
223
+ y_position -= line_height
224
+ y_position -= line_height
225
+ first_page = False
226
+
227
+ # Render questions and options
228
+ for idx, q in enumerate(content):
229
+ if not isinstance(q, dict):
230
+ st.error(f"Error: Invalid question format at index {idx}. Skipping...")
231
+ continue
232
+
233
+ question_text = f"{idx + 1}. {q.get('question', q.get('statement', ''))}"
234
+ height = wrap_text_draw(question_text, margin_left, y_position)
235
+ y_position -= (height + line_height)
236
+
237
+ if y_position < 50:
238
+ c.showPage()
239
+ c.setFont("Helvetica", 10)
240
+ y_position = 750
241
+
242
+ # Handle specific exam types
243
+ exam_type = metadata['exam_type']
244
+
245
+ if exam_type == "Multiple Choice":
246
+ for option_idx, option in enumerate(q['options'], ord('a')):
247
+ option_text = f"{chr(option_idx)}) {option}"
248
+ height = wrap_text_draw(option_text, margin_left + 20, y_position)
249
+ y_position -= (height + line_height)
250
+
251
+ if y_position < 50:
252
+ c.showPage()
253
+ c.setFont("Helvetica", 10)
254
+ y_position = 750
255
+
256
+ # Print correct answer
257
+ correct_answer_text = f"Correct Answer: {q['correct_answer']}"
258
+ height = wrap_text_draw(correct_answer_text, margin_left + 20, y_position)
259
+ y_position -= (height + line_height)
260
+
261
+ elif exam_type == "True or False":
262
+ for option in q['options']:
263
+ height = wrap_text_draw(option, margin_left + 20, y_position)
264
+ y_position -= (height + line_height)
265
+
266
+ if y_position < 50:
267
+ c.showPage()
268
+ c.setFont("Helvetica", 10)
269
+ y_position = 750
270
+
271
+ correct_answer_text = f"Correct Answer: {q['correct_answer']}"
272
+ height = wrap_text_draw(correct_answer_text, margin_left + 20, y_position)
273
+ y_position -= (height + line_height)
274
+
275
+ elif exam_type in ["Short Response", "Essay Type"]:
276
+ answer_text = f"Correct Answer: {q['correct_answer']}"
277
+ height = wrap_text_draw(answer_text, margin_left + 20, y_position)
278
+ y_position -= (height + line_height)
279
+
280
+ if y_position < 50:
281
+ c.showPage()
282
+ c.setFont("Helvetica", 10)
283
+ y_position = 750
284
+
285
+ # Add a footer
286
+ notice = "This exam was generated by the WVSU Exam Maker (c) 2025 West Visayas State University"
287
+ c.drawString(margin_left, y_position, notice)
288
+
289
+ c.save()
290
+ return pdf_path
291
+
292
+ except Exception as e:
293
+ st.error(f"Error creating PDF: {e}")
294
+ return None
295
+
296
+ def generate_quiz_content(data):
297
+ """
298
+ Separates the metadata and content from a JSON string containing exam data.
299
+ Creates a markdown formatted text that contains the exam metadata and
300
+ enumerates the questions, options and answers nicely formatted for readability.
301
+
302
+ Args:
303
+ data: A JSON string containing the exam data.
304
+
305
+ Returns:
306
+ A markdown formatted string.
307
+ """
308
+ data = json.loads(data)
309
+ metadata = data["metadata"]
310
+ content = data["content"]
311
+ exam_type = metadata["exam_type"]
312
+ if exam_type == "Multiple Choice":
313
+ md_text = f"""# {metadata['subject']} - {metadata['topic']}
314
+
315
+ **Exam Type:** {metadata['exam_type']}
316
+ **Number of Questions:** {metadata['num_questions']}
317
+ **Timestamp:** {metadata['timestamp']}
318
+
319
+ ---
320
+
321
+ """
322
+ for i, q in enumerate(content):
323
+ md_text += f"""Question {i+1}:
324
+ {q['question']}
325
+
326
+ """
327
+ for j, option in enumerate(q['options'], ord('a')):
328
+ md_text += f"""{chr(j)}. {option}
329
+
330
+ """
331
+ md_text += f"""**Correct Answer:** {q['correct_answer']}
332
+
333
+ ---
334
+
335
+ """
336
+ md_text += """This exam was generated by the WVSU Exam Maker
337
+ (c) 2025 West Visayas State University
338
+ """
339
+
340
+ elif exam_type == "True or False":
341
+ md_text = f"""# {metadata['subject']} - {metadata['topic']}
342
+
343
+ **Exam Type:** {metadata['exam_type']}
344
+ **Number of Questions:** {metadata['num_questions']}
345
+ **Timestamp:** {metadata['timestamp']}
346
+
347
+ ---
348
+
349
+ """
350
+
351
+ for i, q in enumerate(content):
352
+ md_text += f"""Statement {i+1}:
353
+
354
+ {q['statement']}
355
+
356
+ """
357
+ for j, option in enumerate(q['options'], ord('a')):
358
+ md_text += f"""{option}
359
+ """
360
+
361
+ md_text += f"""**Correct Answer:** {q['correct_answer']}
362
+
363
+ ---
364
+ """
365
+ md_text += """This exam was generated by the WVSU Exam Maker
366
+ (c) 2025 West Visayas State University"""
367
+
368
+ elif exam_type == "Short Response" or exam_type == "Essay Type":
369
+ md_text = f"""# {metadata['subject']} - {metadata['topic']}
370
+
371
+ **Exam Type:** {metadata['exam_type']}
372
+ **Number of Questions:** {metadata['num_questions']}
373
+ **Timestamp:** {metadata['timestamp']}
374
+
375
+ ---
376
+
377
+ """
378
+
379
+ for i, q in enumerate(content):
380
+ md_text += f"""Question {i+1}:
381
+
382
+ {q['question']}
383
+
384
+ """
385
+ md_text += f"""**Correct Answer:** {q['correct_answer']}
386
+
387
+ ---
388
+ """
389
+ md_text += """This exam was generated by the WVSU Exam Maker
390
+ (c) 2025 West Visayas State University"""
391
+
392
+ return md_text
393
+
394
+ def generate_metadata(subject, topic, num_questions, exam_type):
395
+ """Generates quiz metadata as a dictionary combining num_questions,
396
+ exam_type, and timestamp.
397
+
398
+ Args:
399
+ num_questions: The number of questions in the exam (int).
400
+ exam_type: The type of exam (str).
401
+
402
+ Returns:
403
+ A dictionary containing the quiz metadata.
404
+ """
405
+
406
+ # Format the timestamp
407
+ timestamp = datetime.datetime.now()
408
+ formatted_timestamp = timestamp.strftime("%Y-%m-%d %H:%M:%S")
409
+
410
+ metadata = {
411
+ "subject": subject,
412
+ "topic": topic,
413
+ "num_questions": num_questions,
414
+ "exam_type": exam_type,
415
+ "timestamp": formatted_timestamp
416
+ }
417
+
418
+ return metadata
419
+
420
+ def generate_text(uploaded_file, mime_type, prompt):
421
+ """Generates text based on the uploaded file and prompt."""
422
+ try:
423
+ if st.session_state.is_new_file:
424
+ # Upload the file with the correct MIME type
425
+ file_data = genai.upload_file(uploaded_file, mime_type=mime_type)
426
+
427
+ # Send file and prompt to Gemini API
428
+ chat = st.session_state.chat
429
+ response = chat.send_message(
430
+ [
431
+ prompt,
432
+ file_data
433
+ ],
434
+ stream=enable_stream
435
+ )
436
+ st.session_state.is_new_file = False
437
+ else:
438
+ # continue chat without sending the file again
439
+ # Send a text prompt to Gemini API
440
+ chat = st.session_state.chat
441
+ response = chat.send_message(
442
+ [
443
+ prompt
444
+ ],
445
+ stream=enable_stream
446
+ )
447
+
448
+ return response.text
449
+
450
+ except Exception as e:
451
+ st.error(f"An error occurred while generating text: {e}")
452
+ return None
453
+
454
+ def show_multimodal():
455
+ st.subheader("Multimodal")
456
+ username = st.session_state["username"]
457
+ st.write(f"Welcome, {username}! This page allows you to generate questions based on an image or PDF file.")
458
+
459
+ # Display username and logout button on every page
460
+ st.sidebar.write(f"Current user: {st.session_state['username']}")
461
+
462
+ # we dont use the system instruction for now
463
+ #system_instruction = get_system_instruction(username)
464
+
465
+ # File uploader with allowed types
466
+ uploaded_file = st.file_uploader("Choose an image or PDF...", type=["jpg", "jpeg", "png", "pdf"])
467
+
468
+ if uploaded_file is not None:
469
+ # Determine file type
470
+ file_type = uploaded_file.type
471
+ if file_type.startswith('image'):
472
+ # Display the uploaded image
473
+ image = Image.open(uploaded_file)
474
+ st.image(image, caption="Uploaded Image.", use_container_width=True)
475
+ mime_type = "image/jpeg" # Use a consistent MIME type for images
476
+ # Display a message for PDF upload
477
+ st.write("Image file was uploaded. Questions will be generated based on its contents.")
478
+ elif file_type == 'application/pdf':
479
+ # Display a message for PDF upload
480
+ st.write("PDF file uploaded. Questions will be generated based on its contents.")
481
+ mime_type = "application/pdf"
482
+ else:
483
+ st.error("Unsupported file type. Please upload an image or PDF.")
484
+ st.stop()
485
+
486
+ # User inputs
487
+ # Course selection
488
+ course = st.text_input("Enter Course",
489
+ "e.g.,Bachelor of Secondary Education")
490
+
491
+ # Year level selection
492
+ year_level = st.selectbox("Select Year Level",
493
+ ["1st Year",
494
+ "2nd Year",
495
+ "3rd Year",
496
+ "4th Year"])
497
+
498
+ # Subject selection
499
+ subject = st.text_input("Enter Subject",
500
+ "e.g.,The Teaching Profession, Facilitating Learner-Centered Teaching")
501
+
502
+ # Topic selection
503
+ topic = st.text_input("Enter Topic",
504
+ "e.g., Teacher as a professional, Introduction to Learner-Centered Teaching")
505
+
506
+ # Question type selection
507
+ question_type = st.selectbox("Select Question Type",
508
+ ["Multiple Choice",
509
+ "True or False",
510
+ "Short Response",
511
+ "Essay Type"])
512
+
513
+ difficulty = st.selectbox("Select Difficulty",["easy","average","hard"])
514
+
515
+ #number of questions to generate
516
+ if question_type != "Essay Type":
517
+ num_questions = st.selectbox("Number of Questions to Generate",
518
+ [10, 20, 30, 40, 50])
519
+ else:
520
+ num_questions = st.selectbox("Number of Questions to Generate",
521
+ [1, 2, 3, 4, 5])
522
+
523
+ # Combine user inputs into a prompt
524
+ prompt = f"""Refer to the uploaded document. Generate a {question_type} question for a {year_level} {course} student
525
+ in {subject} on the topic of {topic} with a {difficulty} difficulty level.
526
+ The questions should require higher order thinking skills.
527
+ """
528
+
529
+ if question_type == "Multiple Choice":
530
+ prompt += """Provide 4 choices. Provide the correct answer in the format 'Answer: A'.
531
+ Use the following JSON format for each question:
532
+ [{
533
+ "question": "Your question here?",
534
+ "options": ["Option A", "Option B", "Option C", "Option D"],
535
+ "correct_answer": "full text of the correct answer"
536
+ }, ... more questions]
537
+ Ensure that the response only contains the JSON array of questions and nothing else.
538
+ """
539
+ elif question_type == "True or False":
540
+ prompt += """Indicate whether the statement is true or false. Keep the statement brief and concise.
541
+ Use the following JSON format for each question:
542
+ [{
543
+ "statement": "Your statement here",
544
+ "options": ["True", "False"],
545
+ "correct_answer": True"
546
+ }, ... more questions]
547
+ Ensure that the response only contains the JSON array of questions and nothing else.
548
+ """
549
+ elif question_type == "Short Response":
550
+ prompt += """Create question that require a word or short phrase as answer. Use the following JSON format for each question:
551
+ [{
552
+ "question": "Your question here?",
553
+ "correct_answer": A word or phrase"
554
+ }, ... more questions]
555
+ Ensure that the response only contains the JSON array of questions and nothing else.
556
+ """
557
+ elif question_type == "Essay Type":
558
+ prompt += """Create questions that require a short essay between 300 to 500 words.
559
+ Provide a detailed answer. Use the following JSON format for each question:
560
+ [{
561
+ "question": "Your question here?",
562
+ "correct_answer": The essay answer goes here."
563
+ }, ... more questions]
564
+ Ensure that the response only contains the JSON array of questions and nothing else.
565
+ """
566
+
567
+ if not question_type == "Essay Type":
568
+ prompt += f"Generate 10 questions. Do not repeat questions you have already given in previous prompts. Exclude markdown tags in the response."
569
+ else:
570
+ prompt += f" Generate {num_questions} questions. Do not repeat questions you have already given in previous prompts. Exclude markdown tags in the response"
571
+
572
+ full_quiz = ""
573
+
574
+ # Send button
575
+ if st.button("Generate Questions"):
576
+
577
+
578
+ if not uploaded_file:
579
+
580
+ st.warning("Please upload an image or PDF and enter a prompt.")
581
+ st.stop()
582
+ else:
583
+ if question_type == "Essay Type":
584
+ #prompt once
585
+ with st.spinner('Generating questions...'):
586
+ full_quiz = _clean_markdown(generate_text(uploaded_file, mime_type, prompt))
587
+
588
+ else:
589
+ if num_questions == 10:
590
+
591
+ #prompt once
592
+ with st.spinner('Generating questions...'):
593
+ full_quiz = _clean_markdown(generate_text(uploaded_file, mime_type, prompt))
594
+ else:
595
+ #prompt multiple times
596
+ times = num_questions//10
597
+ for i in range(times):
598
+ with st.spinner('Generating questions...'):
599
+ response = generate_text(uploaded_file, mime_type, prompt)
600
+
601
+ if i==0:
602
+ full_quiz = _clean_markdown(response)
603
+ else:
604
+ full_quiz = merge_json_strings(full_quiz, response)
605
+
606
+ metadata = generate_metadata(subject, topic, num_questions, question_type)
607
+
608
+ try:
609
+ # Attempt to load the string as JSON to validate it
610
+ content = json.loads(full_quiz)
611
+ except json.JSONDecodeError:
612
+ st.error("Error: Invalid JSON string for quiz content.")
613
+ st.stop()
614
+
615
+ json_string = create_json(metadata, content)
616
+
617
+ quiz_markdown = generate_quiz_content(json_string)
618
+ st.markdown(quiz_markdown)
619
+
620
+ pdf_path = create_pdf(json_string)
621
+
622
+ if pdf_path:
623
+ """Click the button to download the generated PDF."""
624
+ try:
625
+ with open(pdf_path, "rb") as f:
626
+ st.download_button("Download PDF", f, file_name=os.path.basename(pdf_path))
627
+ except Exception as e:
628
+ st.error(f"Error handling file download: {e}")
629
+ else:
630
+ st.error("Failed to generate the PDF. Please try again.")
631
+
632
+ #record the prompt for monitoring
633
+ save_user_prompt(username, datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "Multimodal")
634
+
635
+ if st.session_state["authenticated"]:
636
+ show_multimodal()
637
+ else:
638
+ if not st.session_state["is_starting"]:
639
+ st.write("You are not authenticated. Please log in to access this page.")
pages/4_Settings.py ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import sqlite3
3
+
4
+ def get_system_instruction(username):
5
+ conn = sqlite3.connect('users.db')
6
+ c = conn.cursor()
7
+ c.execute('SELECT instruction FROM system_instructions WHERE username=?', (username,))
8
+ instruction = c.fetchone()
9
+ conn.close()
10
+ if instruction:
11
+ return instruction[0]
12
+ else:
13
+ return "Default system instruction."
14
+
15
+ def save_system_instruction(username, instruction):
16
+ conn = sqlite3.connect('users.db')
17
+ c = conn.cursor()
18
+ c.execute('SELECT * FROM system_instructions WHERE username=?', (username,))
19
+ existing_instruction = c.fetchone()
20
+ if existing_instruction:
21
+ c.execute('UPDATE system_instructions SET instruction=? WHERE username=?', (instruction, username))
22
+ else:
23
+ c.execute('INSERT INTO system_instructions(username, instruction) VALUES (?,?)', (username, instruction))
24
+ conn.commit()
25
+ conn.close()
26
+
27
+ def show_settings():
28
+ st.subheader("Settings")
29
+ username = st.session_state["username"]
30
+ system_instruction = get_system_instruction(username)
31
+ st.write("System Instruction:")
32
+ instruction = st.text_area("", value=system_instruction, height=200)
33
+ if st.button("Save Changes"):
34
+ save_system_instruction(username, instruction)
35
+ st.success("System instruction saved successfully.")
36
+
37
+ st.write("Note: System instruction is not used in this version of the app.")
38
+
39
+ if st.session_state["authenticated"]:
40
+ show_settings()
41
+ else:
42
+ if not st.session_state["is_starting"]:
43
+ st.write("You are not authenticated. Please log in to access this page.")
pages/__pycache__/About.cpython-313.pyc ADDED
Binary file (740 Bytes). View file
 
pages/__pycache__/Multimodal.cpython-313.pyc ADDED
Binary file (22.8 kB). View file
 
pages/__pycache__/Settings.cpython-313.pyc ADDED
Binary file (2.61 kB). View file
 
pages/__pycache__/Text_prompt.cpython-313.pyc ADDED
Binary file (2.56 kB). View file
 
pdfutils.py ADDED
@@ -0,0 +1,249 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import re
3
+ from reportlab.platypus import Paragraph, Frame, Spacer
4
+ from reportlab.lib.styles import getSampleStyleSheet
5
+ import datetime
6
+ from reportlab.platypus import Paragraph, Frame, Spacer
7
+ from reportlab.lib.styles import getSampleStyleSheet
8
+
9
+
10
+ def merge_json_strings(json_str1, json_str2):
11
+ """
12
+ Merges two JSON strings into one, handling potential markdown tags.
13
+
14
+ Args:
15
+ json_str1: The first JSON string, potentially with markdown tags.
16
+ json_str2: The second JSON string, potentially with markdown tags.
17
+
18
+ Returns:
19
+ A cleaned JSON string representing the merged JSON objects.
20
+ """
21
+
22
+ # Clean the JSON strings by removing markdown tags
23
+ cleaned_json_str1 = _clean_markdown(json_str1)
24
+ cleaned_json_str2 = _clean_markdown(json_str2)
25
+
26
+ try:
27
+ # Parse the cleaned JSON strings into Python dictionaries
28
+ data1 = json.loads(cleaned_json_str1)
29
+ data2 = json.loads(cleaned_json_str2)
30
+
31
+ # Merge the dictionaries
32
+ merged_data = _merge_dicts(data1, data2)
33
+
34
+ # Convert the merged dictionary back into a JSON string
35
+ return json.dumps(merged_data, indent=2)
36
+ except json.JSONDecodeError as e:
37
+ return f"Error decoding JSON: {e}"
38
+
39
+ def _clean_markdown(text):
40
+ """
41
+ Removes markdown tags from a string if they exist.
42
+ Otherwise, returns the original string unchanged.
43
+
44
+ Args:
45
+ text: The input string.
46
+
47
+ Returns:
48
+ The string with markdown tags removed, or the original string
49
+ if no markdown tags were found.
50
+ """
51
+ try:
52
+ # Check if the string contains markdown
53
+ if re.match(r"^```json\s*", text) and re.search(r"\s*```$", text):
54
+ # Remove leading ```json
55
+ text = re.sub(r"^```json\s*", "", text)
56
+ # Remove trailing ```
57
+ text = re.sub(r"\s*```$", "", text)
58
+ return text
59
+ except Exception as e:
60
+ # Log the error
61
+ st.error(f"Error cleaning markdown: {e}")
62
+ return None
63
+
64
+ def _merge_dicts(data1, data2):
65
+ """
66
+ Recursively merges two data structures.
67
+
68
+ Handles merging of dictionaries and lists.
69
+ For dictionaries, if a key exists in both and both values are dictionaries
70
+ or lists, they are merged recursively. Otherwise, the value from data2 is used.
71
+ For lists, the lists are concatenated.
72
+
73
+ Args:
74
+ data1: The first data structure (dictionary or list).
75
+ data2: The second data structure (dictionary or list).
76
+
77
+ Returns:
78
+ The merged data structure.
79
+
80
+ Raises:
81
+ ValueError: If the data types are not supported for merging.
82
+ """
83
+ if isinstance(data1, dict) and isinstance(data2, dict):
84
+ for key, value in data2.items():
85
+ if key in data1 and isinstance(data1[key], (dict, list)) and isinstance(value, type(data1[key])):
86
+ _merge_dicts(data1[key], value)
87
+ else:
88
+ data1[key] = value
89
+ return data1
90
+ elif isinstance(data1, list) and isinstance(data2, list):
91
+ return data1 + data2
92
+ else:
93
+ raise ValueError("Unsupported data types for merging")
94
+
95
+ def create_json(metadata, content):
96
+ """
97
+ Creates a JSON string combining metadata and content.
98
+
99
+ Args:
100
+ metadata: A dictionary containing metadata information.
101
+ content: A dictionary containing the quiz content.
102
+
103
+ Returns:
104
+ A string representing the combined JSON data.
105
+ """
106
+
107
+ # Create metadata with timestamp
108
+ metadata = {
109
+ "subject": metadata.get("subject", ""),
110
+ "topic": metadata.get("topic", ""),
111
+ "num_questions": metadata.get("num_questions", 0),
112
+ "exam_type": metadata.get("exam_type", ""),
113
+ "timestamp": datetime.datetime.now().isoformat()
114
+ }
115
+
116
+ # Combine metadata and content
117
+ combined_data = {"metadata": metadata, "content": content}
118
+
119
+ # Convert to JSON string
120
+ json_string = json.dumps(combined_data, indent=4)
121
+
122
+ return json_string
123
+
124
+ def create_pdf(data):
125
+ """
126
+ Creates a PDF file with text wrapping for quiz content, supporting multiple question types.
127
+ """
128
+ try:
129
+ # Load the JSON data
130
+ data = json.loads(data)
131
+
132
+ if 'metadata' not in data or 'content' not in data:
133
+ st.error("Error: Invalid data format. Missing 'metadata' or 'content' keys.")
134
+ return None
135
+
136
+ metadata = data['metadata']
137
+ content = data['content']
138
+
139
+ # Validate metadata
140
+ required_metadata_keys = ['subject', 'topic', 'exam_type', 'num_questions']
141
+ if not all(key in metadata for key in required_metadata_keys):
142
+ st.error("Error: Invalid metadata format. Missing required keys.")
143
+ return None
144
+
145
+ # Create a unique filename with timestamp
146
+ timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
147
+ pdf_filename = f"quiz_output_{timestamp}.pdf"
148
+ temp_dir = tempfile.gettempdir()
149
+ pdf_path = os.path.join(temp_dir, pdf_filename)
150
+
151
+ c = canvas.Canvas(pdf_path, pagesize=A4)
152
+ c.setFont("Helvetica", 10)
153
+
154
+ styles = getSampleStyleSheet()
155
+ text_style = styles['Normal']
156
+
157
+ # Starting position
158
+ margin_left = 50
159
+ y_position = 750
160
+ line_height = 12 # Adjusted for tighter spacing
161
+ frame_width = 500
162
+ first_page = True
163
+
164
+ def wrap_text_draw(text, x, y):
165
+ """
166
+ Wraps and draws text using ReportLab's Paragraph for automatic line breaks.
167
+ """
168
+ p = Paragraph(text, text_style)
169
+ width, height = p.wrap(frame_width, y)
170
+ p.drawOn(c, x, y - height)
171
+ return height
172
+
173
+ # Print metadata once on the first page
174
+ if first_page:
175
+ for key, label in [("subject", "Subject"), ("topic", "Topic"),
176
+ ("exam_type", "Type"), ("num_questions", "Number of Questions")]:
177
+ c.drawString(margin_left, y_position, f"{label}: {metadata[key]}")
178
+ y_position -= line_height
179
+ y_position -= line_height
180
+ first_page = False
181
+
182
+ # Render questions and options
183
+ for idx, q in enumerate(content):
184
+ if not isinstance(q, dict):
185
+ st.error(f"Error: Invalid question format at index {idx}. Skipping...")
186
+ continue
187
+
188
+ question_text = f"{idx + 1}. {q.get('question', q.get('statement', ''))}"
189
+ height = wrap_text_draw(question_text, margin_left, y_position)
190
+ y_position -= (height + line_height)
191
+
192
+ if y_position < 50:
193
+ c.showPage()
194
+ c.setFont("Helvetica", 10)
195
+ y_position = 750
196
+
197
+ # Handle specific exam types
198
+ exam_type = metadata['exam_type']
199
+
200
+ if exam_type == "Multiple Choice":
201
+ for option_idx, option in enumerate(q['options'], ord('a')):
202
+ option_text = f"{chr(option_idx)}) {option}"
203
+ height = wrap_text_draw(option_text, margin_left + 20, y_position)
204
+ y_position -= (height + line_height)
205
+
206
+ if y_position < 50:
207
+ c.showPage()
208
+ c.setFont("Helvetica", 10)
209
+ y_position = 750
210
+
211
+ # Print correct answer
212
+ correct_answer_text = f"Correct Answer: {q['correct_answer']}"
213
+ height = wrap_text_draw(correct_answer_text, margin_left + 20, y_position)
214
+ y_position -= (height + line_height)
215
+
216
+ elif exam_type == "True or False":
217
+ for option in q['options']:
218
+ height = wrap_text_draw(option, margin_left + 20, y_position)
219
+ y_position -= (height + line_height)
220
+
221
+ if y_position < 50:
222
+ c.showPage()
223
+ c.setFont("Helvetica", 10)
224
+ y_position = 750
225
+
226
+ correct_answer_text = f"Correct Answer: {q['correct_answer']}"
227
+ height = wrap_text_draw(correct_answer_text, margin_left + 20, y_position)
228
+ y_position -= (height + line_height)
229
+
230
+ elif exam_type in ["Short Response", "Essay Type"]:
231
+ answer_text = f"Correct Answer: {q['correct_answer']}"
232
+ height = wrap_text_draw(answer_text, margin_left + 20, y_position)
233
+ y_position -= (height + line_height)
234
+
235
+ if y_position < 50:
236
+ c.showPage()
237
+ c.setFont("Helvetica", 10)
238
+ y_position = 750
239
+
240
+ # Add a footer
241
+ notice = "This exam was generated by the WVSU Exam Maker (c) 2025 West Visayas State University"
242
+ c.drawString(margin_left, y_position, notice)
243
+
244
+ c.save()
245
+ return pdf_path
246
+
247
+ except Exception as e:
248
+ st.error(f"Error creating PDF: {e}")
249
+ return None
requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ streamlit
2
+ passlib
3
+ bcrypt==3.2.0
4
+ google-generativeai
5
+ reportlab
users.db ADDED
Binary file (24.6 kB). View file