Spaces:
Sleeping
Sleeping
import json | |
import re | |
from reportlab.platypus import Paragraph, Frame, Spacer | |
from reportlab.lib.styles import getSampleStyleSheet | |
import datetime | |
from reportlab.lib.styles import getSampleStyleSheet | |
import streamlit as st | |
import tempfile | |
import os | |
from reportlab.pdfgen import canvas | |
from reportlab.lib.pagesizes import A4, letter | |
ENABLE_STREAM = False | |
def merge_json_strings(json_str1, json_str2): | |
""" | |
Merges two JSON strings into one, handling potential markdown tags. | |
Args: | |
json_str1: The first JSON string, potentially with markdown tags. | |
json_str2: The second JSON string, potentially with markdown tags. | |
Returns: | |
A cleaned JSON string representing the merged JSON objects. | |
""" | |
# Clean the JSON strings by removing markdown tags | |
cleaned_json_str1 = clean_markdown(json_str1) | |
cleaned_json_str2 = clean_markdown(json_str2) | |
try: | |
# Parse the cleaned JSON strings into Python dictionaries | |
data1 = json.loads(cleaned_json_str1) | |
data2 = json.loads(cleaned_json_str2) | |
# Merge the dictionaries | |
merged_data = _merge_dicts(data1, data2) | |
# Convert the merged dictionary back into a JSON string | |
return json.dumps(merged_data, indent=2) | |
except json.JSONDecodeError as e: | |
return f"Error decoding JSON: {e}" | |
def clean_markdown(text): | |
""" | |
Removes markdown tags from a string if they exist. | |
Otherwise, returns the original string unchanged. | |
Args: | |
text: The input string. | |
Returns: | |
The string with markdown tags removed, or the original string | |
if no markdown tags were found. | |
""" | |
try: | |
# Check if the string contains markdown | |
if re.match(r"^```json\s*", text) and re.search(r"\s*```$", text): | |
# Remove leading ```json | |
text = re.sub(r"^```json\s*", "", text) | |
# Remove trailing ``` | |
text = re.sub(r"\s*```$", "", text) | |
return text | |
except Exception as e: | |
# Log the error | |
st.error(f"Error cleaning markdown: {e}") | |
return None | |
def _merge_dicts(data1, data2): | |
""" | |
Recursively merges two data structures. | |
Handles merging of dictionaries and lists. | |
For dictionaries, if a key exists in both and both values are dictionaries | |
or lists, they are merged recursively. Otherwise, the value from data2 is used. | |
For lists, the lists are concatenated. | |
Args: | |
data1: The first data structure (dictionary or list). | |
data2: The second data structure (dictionary or list). | |
Returns: | |
The merged data structure. | |
Raises: | |
ValueError: If the data types are not supported for merging. | |
""" | |
if isinstance(data1, dict) and isinstance(data2, dict): | |
for key, value in data2.items(): | |
if key in data1 and isinstance(data1[key], (dict, list)) and isinstance(value, type(data1[key])): | |
_merge_dicts(data1[key], value) | |
else: | |
data1[key] = value | |
return data1 | |
elif isinstance(data1, list) and isinstance(data2, list): | |
return data1 + data2 | |
else: | |
raise ValueError("Unsupported data types for merging") | |
def create_json(metadata, content): | |
""" | |
Creates a JSON string combining metadata and content. | |
Args: | |
metadata: A dictionary containing metadata information. | |
content: A dictionary containing the quiz content. | |
Returns: | |
A string representing the combined JSON data. | |
""" | |
# Create metadata with timestamp | |
metadata = { | |
"subject": metadata.get("subject", ""), | |
"topic": metadata.get("topic", ""), | |
"num_questions": metadata.get("num_questions", 0), | |
"exam_type": metadata.get("exam_type", ""), | |
"timestamp": datetime.datetime.now().isoformat() | |
} | |
# Combine metadata and content | |
combined_data = {"metadata": metadata, "content": content} | |
# Convert to JSON string | |
json_string = json.dumps(combined_data, indent=4) | |
return json_string | |
def create_pdf(data): | |
""" | |
Creates a PDF file with text wrapping for quiz content, supporting multiple question types. | |
""" | |
try: | |
# Load the JSON data | |
data = json.loads(data) | |
if 'metadata' not in data or 'content' not in data: | |
st.error("Error: Invalid data format. Missing 'metadata' or 'content' keys.") | |
return None | |
metadata = data['metadata'] | |
content = data['content'] | |
# Validate metadata | |
required_metadata_keys = ['subject', 'topic', 'exam_type', 'num_questions'] | |
if not all(key in metadata for key in required_metadata_keys): | |
st.error("Error: Invalid metadata format. Missing required keys.") | |
return None | |
# Create a unique filename with timestamp | |
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") | |
pdf_filename = f"quiz_output_{timestamp}.pdf" | |
temp_dir = tempfile.gettempdir() | |
pdf_path = os.path.join(temp_dir, pdf_filename) | |
c = canvas.Canvas(pdf_path, pagesize=A4) | |
c.setFont("Helvetica", 10) | |
styles = getSampleStyleSheet() | |
text_style = styles['Normal'] | |
# Starting position | |
margin_left = 50 | |
y_position = 750 | |
line_height = 12 # Adjusted for tighter spacing | |
frame_width = 500 | |
first_page = True | |
def wrap_text_draw(text, x, y): | |
""" | |
Wraps and draws text using ReportLab's Paragraph for automatic line breaks. | |
""" | |
p = Paragraph(text, text_style) | |
width, height = p.wrap(frame_width, y) | |
p.drawOn(c, x, y - height) | |
return height | |
# Print metadata once on the first page | |
if first_page: | |
for key, label in [("subject", "Subject"), ("topic", "Topic"), | |
("exam_type", "Type"), ("num_questions", "Number of Questions")]: | |
c.drawString(margin_left, y_position, f"{label}: {metadata[key]}") | |
y_position -= line_height | |
y_position -= line_height | |
first_page = False | |
# Render questions and options | |
for idx, q in enumerate(content): | |
if not isinstance(q, dict): | |
st.error(f"Error: Invalid question format at index {idx}. Skipping...") | |
continue | |
question_text = f"{idx + 1}. {q.get('question', q.get('statement', ''))}" | |
height = wrap_text_draw(question_text, margin_left, y_position) | |
y_position -= (height + line_height) | |
if y_position < 50: | |
c.showPage() | |
c.setFont("Helvetica", 10) | |
y_position = 750 | |
# Handle specific exam types | |
exam_type = metadata['exam_type'] | |
if exam_type == "Multiple Choice": | |
for option_idx, option in enumerate(q['options'], ord('a')): | |
option_text = f"{chr(option_idx)}) {option}" | |
height = wrap_text_draw(option_text, margin_left + 20, y_position) | |
y_position -= (height + line_height) | |
if y_position < 50: | |
c.showPage() | |
c.setFont("Helvetica", 10) | |
y_position = 750 | |
# Print correct answer | |
correct_answer_text = f"Correct Answer: {q['correct_answer']}" | |
height = wrap_text_draw(correct_answer_text, margin_left + 20, y_position) | |
y_position -= (height + line_height) | |
elif exam_type == "True or False": | |
for option in q['options']: | |
height = wrap_text_draw(option, margin_left + 20, y_position) | |
y_position -= (height + line_height) | |
if y_position < 50: | |
c.showPage() | |
c.setFont("Helvetica", 10) | |
y_position = 750 | |
correct_answer_text = f"Correct Answer: {q['correct_answer']}" | |
height = wrap_text_draw(correct_answer_text, margin_left + 20, y_position) | |
y_position -= (height + line_height) | |
elif exam_type in ["Short Response", "Essay Type"]: | |
answer_text = f"Correct Answer: {q['correct_answer']}" | |
height = wrap_text_draw(answer_text, margin_left + 20, y_position) | |
y_position -= (height + line_height) | |
if y_position < 50: | |
c.showPage() | |
c.setFont("Helvetica", 10) | |
y_position = 750 | |
# Add a footer | |
notice = "This exam was generated by the WVSU Exam Maker (c) 2025 West Visayas State University" | |
c.drawString(margin_left, y_position, notice) | |
c.save() | |
return pdf_path | |
except Exception as e: | |
st.error(f"Error creating PDF: {e}") | |
return None | |
def generate_quiz_content(data): | |
""" | |
Separates the metadata and content from a JSON string containing exam data. | |
Creates a markdown formatted text that contains the exam metadata and | |
enumerates the questions, options and answers nicely formatted for readability. | |
Args: | |
data: A JSON string containing the exam data. | |
Returns: | |
A markdown formatted string. | |
""" | |
data = json.loads(data) | |
metadata = data["metadata"] | |
content = data["content"] | |
exam_type = metadata["exam_type"] | |
if exam_type == "Multiple Choice": | |
md_text = f"""# {metadata['subject']} - {metadata['topic']} | |
**Exam Type:** {metadata['exam_type']} | |
**Number of Questions:** {metadata['num_questions']} | |
**Timestamp:** {metadata['timestamp']} | |
--- | |
""" | |
for i, q in enumerate(content): | |
md_text += f"""Question {i+1}: | |
{q['question']} | |
""" | |
for j, option in enumerate(q['options'], ord('a')): | |
md_text += f"""{chr(j)}. {option} | |
""" | |
md_text += f"""**Correct Answer:** {q['correct_answer']} | |
--- | |
""" | |
md_text += """This exam was generated by the WVSU Exam Maker | |
(c) 2025 West Visayas State University | |
""" | |
elif exam_type == "True or False": | |
md_text = f"""# {metadata['subject']} - {metadata['topic']} | |
**Exam Type:** {metadata['exam_type']} | |
**Number of Questions:** {metadata['num_questions']} | |
**Timestamp:** {metadata['timestamp']} | |
--- | |
""" | |
for i, q in enumerate(content): | |
md_text += f"""Statement {i+1}: | |
{q['statement']} | |
""" | |
for j, option in enumerate(q['options'], ord('a')): | |
md_text += f"""{option} | |
""" | |
md_text += f"""**Correct Answer:** {q['correct_answer']} | |
--- | |
""" | |
md_text += """This exam was generated by the WVSU Exam Maker | |
(c) 2025 West Visayas State University""" | |
elif exam_type == "Short Response" or exam_type == "Essay Type": | |
md_text = f"""# {metadata['subject']} - {metadata['topic']} | |
**Exam Type:** {metadata['exam_type']} | |
**Number of Questions:** {metadata['num_questions']} | |
**Timestamp:** {metadata['timestamp']} | |
--- | |
""" | |
for i, q in enumerate(content): | |
md_text += f"""Question {i+1}: | |
{q['question']} | |
""" | |
md_text += f"""**Correct Answer:** {q['correct_answer']} | |
--- | |
""" | |
md_text += """This exam was generated by the WVSU Exam Maker | |
(c) 2025 West Visayas State University""" | |
return md_text | |
def generate_metadata(subject, topic, num_questions, exam_type): | |
"""Generates quiz metadata as a dictionary combining num_questions, | |
exam_type, and timestamp. | |
Args: | |
num_questions: The number of questions in the exam (int). | |
exam_type: The type of exam (str). | |
Returns: | |
A dictionary containing the quiz metadata. | |
""" | |
# Format the timestamp | |
timestamp = datetime.datetime.now() | |
formatted_timestamp = timestamp.strftime("%Y-%m-%d %H:%M:%S") | |
metadata = { | |
"subject": subject, | |
"topic": topic, | |
"num_questions": num_questions, | |
"exam_type": exam_type, | |
"timestamp": formatted_timestamp | |
} | |
return metadata | |
def generate_text(prompt): | |
"""Generates text based on the prompt.""" | |
try: | |
# Send a text prompt to Gemini API | |
chat = st.session_state.chat | |
response = chat.send_message( | |
[ | |
prompt | |
], | |
stream=ENABLE_STREAM | |
) | |
return response.text | |
except Exception as e: | |
st.error(f"An error occurred while generating text: {e}") | |
return None |