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 |