Spaces:
Sleeping
Sleeping
sync to remote
Browse files- .gitignore +171 -0
- Exam_Maker.py +214 -0
- LICENSE +21 -0
- pages/1_About.py +45 -0
- pages/2_Text_prompt.py +208 -0
- pages/3_Multimodal.py +639 -0
- pages/4_Settings.py +43 -0
- pdfutils.py +397 -0
- requirements.txt +5 -0
- 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
|
|