louiecerv commited on
Commit
821850b
·
1 Parent(s): e9f3ce9

sync to remote

Browse files
Files changed (10) hide show
  1. .gitignore +171 -0
  2. Exam_Maker.py +214 -0
  3. LICENSE +21 -0
  4. pages/1_About.py +45 -0
  5. pages/2_Text_prompt.py +208 -0
  6. pages/3_Multimodal.py +639 -0
  7. pages/4_Settings.py +43 -0
  8. pdfutils.py +397 -0
  9. requirements.txt +5 -0
  10. users.db +0 -0
.gitignore ADDED
@@ -0,0 +1,171 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py,cover
50
+ .hypothesis/
51
+ .pytest_cache/
52
+ cover/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ .pybuilder/
76
+ target/
77
+
78
+ # Jupyter Notebook
79
+ .ipynb_checkpoints
80
+
81
+ # IPython
82
+ profile_default/
83
+ ipython_config.py
84
+
85
+ # pyenv
86
+ # For a library or package, you might want to ignore these files since the code is
87
+ # intended to run in multiple environments; otherwise, check them in:
88
+ # .python-version
89
+
90
+ # pipenv
91
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
93
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
94
+ # install all needed dependencies.
95
+ #Pipfile.lock
96
+
97
+ # UV
98
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
99
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
100
+ # commonly ignored for libraries.
101
+ #uv.lock
102
+
103
+ # poetry
104
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
105
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
106
+ # commonly ignored for libraries.
107
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
108
+ #poetry.lock
109
+
110
+ # pdm
111
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
112
+ #pdm.lock
113
+ # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
114
+ # in version control.
115
+ # https://pdm.fming.dev/latest/usage/project/#working-with-version-control
116
+ .pdm.toml
117
+ .pdm-python
118
+ .pdm-build/
119
+
120
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
121
+ __pypackages__/
122
+
123
+ # Celery stuff
124
+ celerybeat-schedule
125
+ celerybeat.pid
126
+
127
+ # SageMath parsed files
128
+ *.sage.py
129
+
130
+ # Environments
131
+ .env
132
+ .venv
133
+ env/
134
+ venv/
135
+ ENV/
136
+ env.bak/
137
+ venv.bak/
138
+
139
+ # Spyder project settings
140
+ .spyderproject
141
+ .spyproject
142
+
143
+ # Rope project settings
144
+ .ropeproject
145
+
146
+ # mkdocs documentation
147
+ /site
148
+
149
+ # mypy
150
+ .mypy_cache/
151
+ .dmypy.json
152
+ dmypy.json
153
+
154
+ # Pyre type checker
155
+ .pyre/
156
+
157
+ # pytype static type analyzer
158
+ .pytype/
159
+
160
+ # Cython debug symbols
161
+ cython_debug/
162
+
163
+ # PyCharm
164
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
165
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
166
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
167
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
168
+ #.idea/
169
+
170
+ # PyPI configuration file
171
+ .pypirc
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()
LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Louie F. Cervantes
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
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,208 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ from pdfutils import generate_quiz_content, create_pdf, create_json, generate_metadata, merge_json_strings, generate_text, clean_markdown
18
+
19
+ MODEL_ID = "gemini-2.0-flash-exp"
20
+ api_key = os.getenv("GEMINI_API_KEY")
21
+ model_id = MODEL_ID
22
+ genai.configure(api_key=api_key)
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 show_text_prompt():
52
+ st.subheader("Text Prompt")
53
+
54
+ username = st.session_state["username"]
55
+ st.write(f"Welcome, {username}! This page allows you to generate questions based on user inputs.")
56
+
57
+ # Display username and logout button on every page
58
+ st.sidebar.write(f"Current user: {st.session_state['username']}")
59
+
60
+ # User inputs
61
+ # Course selection
62
+ course = st.text_input("Enter Course",
63
+ "e.g.,Bachelor of Secondary Education")
64
+
65
+ # Year level selection
66
+ year_level = st.selectbox("Select Year Level",
67
+ ["1st Year",
68
+ "2nd Year",
69
+ "3rd Year",
70
+ "4th Year"])
71
+
72
+ # Subject selection
73
+ subject = st.text_input("Enter Subject",
74
+ "e.g.,The Teaching Profession, Facilitating Learner-Centered Teaching")
75
+
76
+ # Topic selection
77
+ topic = st.text_input("Enter Topic",
78
+ "e.g., Teacher as a professional, Introduction to Learner-Centered Teaching")
79
+
80
+ # Question type selection
81
+ question_type = st.selectbox("Select Question Type",
82
+ ["Multiple Choice",
83
+ "True or False",
84
+ "Short Response",
85
+ "Essay Type"])
86
+
87
+ difficulty = st.selectbox("Select Difficulty",["easy","average","hard"])
88
+
89
+ #number of questions to generate
90
+ if question_type != "Essay Type":
91
+ num_questions = st.selectbox("Number of Questions to Generate",
92
+ [10, 20, 30, 40, 50])
93
+ else:
94
+ num_questions = st.selectbox("Number of Questions to Generate",
95
+ [1, 2, 3, 4, 5])
96
+
97
+ # Combine user inputs into a prompt
98
+ prompt = f"""Refer to the uploaded document. Generate a {question_type} question for a {year_level} {course} student
99
+ in {subject} on the topic of {topic} with a {difficulty} difficulty level.
100
+ The questions should require higher order thinking skills.
101
+ """
102
+
103
+ if question_type == "Multiple Choice":
104
+ prompt += """Provide 4 choices. Provide the correct answer in the format 'Answer: A'.
105
+ Use the following JSON format for each question:
106
+ [{
107
+ "question": "Your question here?",
108
+ "options": ["Option A", "Option B", "Option C", "Option D"],
109
+ "correct_answer": "full text of the correct answer"
110
+ }, ... more questions]
111
+ Ensure that the response only contains the JSON array of questions and nothing else.
112
+ """
113
+ elif question_type == "True or False":
114
+ prompt += """Indicate whether the statement is true or false. Keep the statement brief and concise.
115
+ Use the following JSON format for each question:
116
+ [{
117
+ "statement": "Your statement here",
118
+ "options": ["True", "False"],
119
+ "correct_answer": True"
120
+ }, ... more questions]
121
+ Ensure that the response only contains the JSON array of questions and nothing else.
122
+ """
123
+ elif question_type == "Short Response":
124
+ prompt += """Create question that require a word or short phrase as answer. Use the following JSON format for each question:
125
+ [{
126
+ "question": "Your question here?",
127
+ "correct_answer": A word or phrase"
128
+ }, ... more questions]
129
+ Ensure that the response only contains the JSON array of questions and nothing else.
130
+ """
131
+ elif question_type == "Essay Type":
132
+ prompt += """Create questions that require a short essay between 300 to 500 words.
133
+ Provide a detailed answer. Use the following JSON format for each question:
134
+ [{
135
+ "question": "Your question here?",
136
+ "correct_answer": The essay answer goes here."
137
+ }, ... more questions]
138
+ Ensure that the response only contains the JSON array of questions and nothing else.
139
+ """
140
+
141
+ if not question_type == "Essay Type":
142
+ prompt += f"Generate 10 questions. Do not repeat questions you have already given in previous prompts. Exclude markdown tags in the response."
143
+ else:
144
+ prompt += f" Generate {num_questions} questions. Do not repeat questions you have already given in previous prompts. Exclude markdown tags in the response"
145
+
146
+ full_quiz = ""
147
+
148
+ # Send button
149
+ if st.button("Generate Questions"):
150
+
151
+ if question_type == "Essay Type":
152
+ #prompt once
153
+ with st.spinner('Generating questions...'):
154
+ full_quiz = clean_markdown(generate_text(prompt))
155
+
156
+ else:
157
+ if num_questions == 10:
158
+
159
+ #prompt once
160
+ with st.spinner('Generating questions...'):
161
+ full_quiz = clean_markdown(generate_text(prompt))
162
+ else:
163
+ #prompt multiple times
164
+ times = num_questions//10
165
+ for i in range(times):
166
+ with st.spinner('Generating questions...'):
167
+ response = generate_text(prompt)
168
+
169
+ if i==0:
170
+ full_quiz = clean_markdown(response)
171
+ else:
172
+ full_quiz = merge_json_strings(full_quiz, response)
173
+
174
+ metadata = generate_metadata(subject, topic, num_questions, question_type)
175
+
176
+ try:
177
+ # Attempt to load the string as JSON to validate it
178
+ content = json.loads(full_quiz)
179
+ except json.JSONDecodeError:
180
+ st.error("Error: Invalid JSON string for quiz content.")
181
+ st.stop()
182
+
183
+ json_string = create_json(metadata, content)
184
+
185
+ quiz_markdown = generate_quiz_content(json_string)
186
+ st.markdown(quiz_markdown)
187
+
188
+ pdf_path = create_pdf(json_string)
189
+
190
+ if pdf_path:
191
+ """Click the button to download the generated PDF."""
192
+ try:
193
+ with open(pdf_path, "rb") as f:
194
+ st.download_button("Download PDF", f, file_name=os.path.basename(pdf_path))
195
+ except Exception as e:
196
+ st.error(f"Error handling file download: {e}")
197
+ else:
198
+ st.error("Failed to generate the PDF. Please try again.")
199
+
200
+ #record the prompt for monitoring
201
+ save_user_prompt(username, datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "Multimodal")
202
+
203
+ if st.session_state["authenticated"]:
204
+ show_text_prompt()
205
+
206
+ else:
207
+ if not st.session_state["is_starting"]:
208
+ 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.")
pdfutils.py ADDED
@@ -0,0 +1,397 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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.lib.styles import getSampleStyleSheet
7
+ import streamlit as st
8
+ import tempfile
9
+ import os
10
+ from reportlab.pdfgen import canvas
11
+ from reportlab.lib.pagesizes import A4, letter
12
+
13
+ ENABLE_STREAM = False
14
+
15
+ def merge_json_strings(json_str1, json_str2):
16
+ """
17
+ Merges two JSON strings into one, handling potential markdown tags.
18
+
19
+ Args:
20
+ json_str1: The first JSON string, potentially with markdown tags.
21
+ json_str2: The second JSON string, potentially with markdown tags.
22
+
23
+ Returns:
24
+ A cleaned JSON string representing the merged JSON objects.
25
+ """
26
+
27
+ # Clean the JSON strings by removing markdown tags
28
+ cleaned_json_str1 = clean_markdown(json_str1)
29
+ cleaned_json_str2 = clean_markdown(json_str2)
30
+
31
+ try:
32
+ # Parse the cleaned JSON strings into Python dictionaries
33
+ data1 = json.loads(cleaned_json_str1)
34
+ data2 = json.loads(cleaned_json_str2)
35
+
36
+ # Merge the dictionaries
37
+ merged_data = _merge_dicts(data1, data2)
38
+
39
+ # Convert the merged dictionary back into a JSON string
40
+ return json.dumps(merged_data, indent=2)
41
+ except json.JSONDecodeError as e:
42
+ return f"Error decoding JSON: {e}"
43
+
44
+ def clean_markdown(text):
45
+ """
46
+ Removes markdown tags from a string if they exist.
47
+ Otherwise, returns the original string unchanged.
48
+
49
+ Args:
50
+ text: The input string.
51
+
52
+ Returns:
53
+ The string with markdown tags removed, or the original string
54
+ if no markdown tags were found.
55
+ """
56
+ try:
57
+ # Check if the string contains markdown
58
+ if re.match(r"^```json\s*", text) and re.search(r"\s*```$", text):
59
+ # Remove leading ```json
60
+ text = re.sub(r"^```json\s*", "", text)
61
+ # Remove trailing ```
62
+ text = re.sub(r"\s*```$", "", text)
63
+ return text
64
+ except Exception as e:
65
+ # Log the error
66
+ st.error(f"Error cleaning markdown: {e}")
67
+ return None
68
+
69
+ def _merge_dicts(data1, data2):
70
+ """
71
+ Recursively merges two data structures.
72
+
73
+ Handles merging of dictionaries and lists.
74
+ For dictionaries, if a key exists in both and both values are dictionaries
75
+ or lists, they are merged recursively. Otherwise, the value from data2 is used.
76
+ For lists, the lists are concatenated.
77
+
78
+ Args:
79
+ data1: The first data structure (dictionary or list).
80
+ data2: The second data structure (dictionary or list).
81
+
82
+ Returns:
83
+ The merged data structure.
84
+
85
+ Raises:
86
+ ValueError: If the data types are not supported for merging.
87
+ """
88
+ if isinstance(data1, dict) and isinstance(data2, dict):
89
+ for key, value in data2.items():
90
+ if key in data1 and isinstance(data1[key], (dict, list)) and isinstance(value, type(data1[key])):
91
+ _merge_dicts(data1[key], value)
92
+ else:
93
+ data1[key] = value
94
+ return data1
95
+ elif isinstance(data1, list) and isinstance(data2, list):
96
+ return data1 + data2
97
+ else:
98
+ raise ValueError("Unsupported data types for merging")
99
+
100
+ def create_json(metadata, content):
101
+ """
102
+ Creates a JSON string combining metadata and content.
103
+
104
+ Args:
105
+ metadata: A dictionary containing metadata information.
106
+ content: A dictionary containing the quiz content.
107
+
108
+ Returns:
109
+ A string representing the combined JSON data.
110
+ """
111
+
112
+ # Create metadata with timestamp
113
+ metadata = {
114
+ "subject": metadata.get("subject", ""),
115
+ "topic": metadata.get("topic", ""),
116
+ "num_questions": metadata.get("num_questions", 0),
117
+ "exam_type": metadata.get("exam_type", ""),
118
+ "timestamp": datetime.datetime.now().isoformat()
119
+ }
120
+
121
+ # Combine metadata and content
122
+ combined_data = {"metadata": metadata, "content": content}
123
+
124
+ # Convert to JSON string
125
+ json_string = json.dumps(combined_data, indent=4)
126
+
127
+ return json_string
128
+
129
+ def create_pdf(data):
130
+ """
131
+ Creates a PDF file with text wrapping for quiz content, supporting multiple question types.
132
+ """
133
+ try:
134
+ # Load the JSON data
135
+ data = json.loads(data)
136
+
137
+ if 'metadata' not in data or 'content' not in data:
138
+ st.error("Error: Invalid data format. Missing 'metadata' or 'content' keys.")
139
+ return None
140
+
141
+ metadata = data['metadata']
142
+ content = data['content']
143
+
144
+ # Validate metadata
145
+ required_metadata_keys = ['subject', 'topic', 'exam_type', 'num_questions']
146
+ if not all(key in metadata for key in required_metadata_keys):
147
+ st.error("Error: Invalid metadata format. Missing required keys.")
148
+ return None
149
+
150
+ # Create a unique filename with timestamp
151
+ timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
152
+ pdf_filename = f"quiz_output_{timestamp}.pdf"
153
+ temp_dir = tempfile.gettempdir()
154
+ pdf_path = os.path.join(temp_dir, pdf_filename)
155
+
156
+ c = canvas.Canvas(pdf_path, pagesize=A4)
157
+ c.setFont("Helvetica", 10)
158
+
159
+ styles = getSampleStyleSheet()
160
+ text_style = styles['Normal']
161
+
162
+ # Starting position
163
+ margin_left = 50
164
+ y_position = 750
165
+ line_height = 12 # Adjusted for tighter spacing
166
+ frame_width = 500
167
+ first_page = True
168
+
169
+ def wrap_text_draw(text, x, y):
170
+ """
171
+ Wraps and draws text using ReportLab's Paragraph for automatic line breaks.
172
+ """
173
+ p = Paragraph(text, text_style)
174
+ width, height = p.wrap(frame_width, y)
175
+ p.drawOn(c, x, y - height)
176
+ return height
177
+
178
+ # Print metadata once on the first page
179
+ if first_page:
180
+ for key, label in [("subject", "Subject"), ("topic", "Topic"),
181
+ ("exam_type", "Type"), ("num_questions", "Number of Questions")]:
182
+ c.drawString(margin_left, y_position, f"{label}: {metadata[key]}")
183
+ y_position -= line_height
184
+ y_position -= line_height
185
+ first_page = False
186
+
187
+ # Render questions and options
188
+ for idx, q in enumerate(content):
189
+ if not isinstance(q, dict):
190
+ st.error(f"Error: Invalid question format at index {idx}. Skipping...")
191
+ continue
192
+
193
+ question_text = f"{idx + 1}. {q.get('question', q.get('statement', ''))}"
194
+ height = wrap_text_draw(question_text, margin_left, y_position)
195
+ y_position -= (height + line_height)
196
+
197
+ if y_position < 50:
198
+ c.showPage()
199
+ c.setFont("Helvetica", 10)
200
+ y_position = 750
201
+
202
+ # Handle specific exam types
203
+ exam_type = metadata['exam_type']
204
+
205
+ if exam_type == "Multiple Choice":
206
+ for option_idx, option in enumerate(q['options'], ord('a')):
207
+ option_text = f"{chr(option_idx)}) {option}"
208
+ height = wrap_text_draw(option_text, margin_left + 20, y_position)
209
+ y_position -= (height + line_height)
210
+
211
+ if y_position < 50:
212
+ c.showPage()
213
+ c.setFont("Helvetica", 10)
214
+ y_position = 750
215
+
216
+ # Print correct answer
217
+ correct_answer_text = f"Correct Answer: {q['correct_answer']}"
218
+ height = wrap_text_draw(correct_answer_text, margin_left + 20, y_position)
219
+ y_position -= (height + line_height)
220
+
221
+ elif exam_type == "True or False":
222
+ for option in q['options']:
223
+ height = wrap_text_draw(option, margin_left + 20, y_position)
224
+ y_position -= (height + line_height)
225
+
226
+ if y_position < 50:
227
+ c.showPage()
228
+ c.setFont("Helvetica", 10)
229
+ y_position = 750
230
+
231
+ correct_answer_text = f"Correct Answer: {q['correct_answer']}"
232
+ height = wrap_text_draw(correct_answer_text, margin_left + 20, y_position)
233
+ y_position -= (height + line_height)
234
+
235
+ elif exam_type in ["Short Response", "Essay Type"]:
236
+ answer_text = f"Correct Answer: {q['correct_answer']}"
237
+ height = wrap_text_draw(answer_text, margin_left + 20, y_position)
238
+ y_position -= (height + line_height)
239
+
240
+ if y_position < 50:
241
+ c.showPage()
242
+ c.setFont("Helvetica", 10)
243
+ y_position = 750
244
+
245
+ # Add a footer
246
+ notice = "This exam was generated by the WVSU Exam Maker (c) 2025 West Visayas State University"
247
+ c.drawString(margin_left, y_position, notice)
248
+
249
+ c.save()
250
+ return pdf_path
251
+
252
+ except Exception as e:
253
+ st.error(f"Error creating PDF: {e}")
254
+ return None
255
+
256
+ def generate_quiz_content(data):
257
+ """
258
+ Separates the metadata and content from a JSON string containing exam data.
259
+ Creates a markdown formatted text that contains the exam metadata and
260
+ enumerates the questions, options and answers nicely formatted for readability.
261
+
262
+ Args:
263
+ data: A JSON string containing the exam data.
264
+
265
+ Returns:
266
+ A markdown formatted string.
267
+ """
268
+ data = json.loads(data)
269
+ metadata = data["metadata"]
270
+ content = data["content"]
271
+ exam_type = metadata["exam_type"]
272
+ if exam_type == "Multiple Choice":
273
+ md_text = f"""# {metadata['subject']} - {metadata['topic']}
274
+
275
+ **Exam Type:** {metadata['exam_type']}
276
+ **Number of Questions:** {metadata['num_questions']}
277
+ **Timestamp:** {metadata['timestamp']}
278
+
279
+ ---
280
+
281
+ """
282
+ for i, q in enumerate(content):
283
+ md_text += f"""Question {i+1}:
284
+ {q['question']}
285
+
286
+ """
287
+ for j, option in enumerate(q['options'], ord('a')):
288
+ md_text += f"""{chr(j)}. {option}
289
+
290
+ """
291
+ md_text += f"""**Correct Answer:** {q['correct_answer']}
292
+
293
+ ---
294
+
295
+ """
296
+ md_text += """This exam was generated by the WVSU Exam Maker
297
+ (c) 2025 West Visayas State University
298
+ """
299
+
300
+ elif exam_type == "True or False":
301
+ md_text = f"""# {metadata['subject']} - {metadata['topic']}
302
+
303
+ **Exam Type:** {metadata['exam_type']}
304
+ **Number of Questions:** {metadata['num_questions']}
305
+ **Timestamp:** {metadata['timestamp']}
306
+
307
+ ---
308
+
309
+ """
310
+
311
+ for i, q in enumerate(content):
312
+ md_text += f"""Statement {i+1}:
313
+
314
+ {q['statement']}
315
+
316
+ """
317
+ for j, option in enumerate(q['options'], ord('a')):
318
+ md_text += f"""{option}
319
+ """
320
+
321
+ md_text += f"""**Correct Answer:** {q['correct_answer']}
322
+
323
+ ---
324
+ """
325
+ md_text += """This exam was generated by the WVSU Exam Maker
326
+ (c) 2025 West Visayas State University"""
327
+
328
+ elif exam_type == "Short Response" or exam_type == "Essay Type":
329
+ md_text = f"""# {metadata['subject']} - {metadata['topic']}
330
+
331
+ **Exam Type:** {metadata['exam_type']}
332
+ **Number of Questions:** {metadata['num_questions']}
333
+ **Timestamp:** {metadata['timestamp']}
334
+
335
+ ---
336
+
337
+ """
338
+
339
+ for i, q in enumerate(content):
340
+ md_text += f"""Question {i+1}:
341
+
342
+ {q['question']}
343
+
344
+ """
345
+ md_text += f"""**Correct Answer:** {q['correct_answer']}
346
+
347
+ ---
348
+ """
349
+ md_text += """This exam was generated by the WVSU Exam Maker
350
+ (c) 2025 West Visayas State University"""
351
+
352
+ return md_text
353
+
354
+ def generate_metadata(subject, topic, num_questions, exam_type):
355
+ """Generates quiz metadata as a dictionary combining num_questions,
356
+ exam_type, and timestamp.
357
+
358
+ Args:
359
+ num_questions: The number of questions in the exam (int).
360
+ exam_type: The type of exam (str).
361
+
362
+ Returns:
363
+ A dictionary containing the quiz metadata.
364
+ """
365
+
366
+ # Format the timestamp
367
+ timestamp = datetime.datetime.now()
368
+ formatted_timestamp = timestamp.strftime("%Y-%m-%d %H:%M:%S")
369
+
370
+ metadata = {
371
+ "subject": subject,
372
+ "topic": topic,
373
+ "num_questions": num_questions,
374
+ "exam_type": exam_type,
375
+ "timestamp": formatted_timestamp
376
+ }
377
+
378
+ return metadata
379
+
380
+ def generate_text(prompt):
381
+ """Generates text based on the prompt."""
382
+ try:
383
+
384
+ # Send a text prompt to Gemini API
385
+ chat = st.session_state.chat
386
+ response = chat.send_message(
387
+ [
388
+ prompt
389
+ ],
390
+ stream=ENABLE_STREAM
391
+ )
392
+
393
+ return response.text
394
+
395
+ except Exception as e:
396
+ st.error(f"An error occurred while generating text: {e}")
397
+ 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