From 0946fe262d66f2d8331cd74d6552846f72b34f98 Mon Sep 17 00:00:00 2001 From: calvinchai Date: Wed, 7 Feb 2024 22:04:09 +0000 Subject: [PATCH 1/8] update files --- .dockerignore | 1 + dev.py | 0 webui/pages/scratch.py | 56 ------------------------------------------ 3 files changed, 1 insertion(+), 56 deletions(-) delete mode 100644 dev.py delete mode 100644 webui/pages/scratch.py diff --git a/.dockerignore b/.dockerignore index 40b3b0a..2c93340 100644 --- a/.dockerignore +++ b/.dockerignore @@ -17,6 +17,7 @@ scratch/ # Ignore dynamic pages webui/pages/1_*.py webui/pages/2_*.py +webui/pages/scratch.py docs/ .git/ diff --git a/dev.py b/dev.py deleted file mode 100644 index e69de29..0000000 diff --git a/webui/pages/scratch.py b/webui/pages/scratch.py deleted file mode 100644 index 19a75cf..0000000 --- a/webui/pages/scratch.py +++ /dev/null @@ -1,56 +0,0 @@ -from dataclasses import dataclass - -import streamlit as st -from streamlit import session_state - -from webui.functions.session import init_session - -init_session() -SettingManager = session_state.SettingManager -AddonManager = session_state.AddonManager - - -@dataclass -class MyDataClass: - attribute1: str - attribute2: int - # add more attributes as needed - - -def add_object_callback(new_attr1, new_attr2): - new_object = MyDataClass(new_attr1, new_attr2) - st.session_state.my_objects.append(new_object) - - -def remove_object_callback(index): - def remove(): - st.session_state.my_objects.pop(index) - - return remove - - -def edit_object_callback(index): - def edit(): - st.session_state.my_objects[index] = MyDataClass(new_attr1, new_attr2) - - return edit - - -if "my_objects" not in st.session_state: - st.session_state.my_objects = [] -with st.container(border=True): - # Display existing objects - for i, obj in enumerate(st.session_state.my_objects): - with st.container(border=True): - cols = st.columns(2) - new_attr1 = cols[0].text_input(f"Attribute 1 for object {i}", obj.attribute1, key=f"attr1_{i}") - new_attr2 = cols[1].number_input(f"Attribute 2 for object {i}", obj.attribute2, key=f"attr2_{i}") - st.button(f"Remove", on_click=remove_object_callback(i), key=f"remove_{i}") - - # Interface to add a new object - with st.container(border=False): - cols = st.columns(2) - new_attr1 = cols[0].text_input("Attribute 1", key="new_attr1") - new_attr2 = cols[1].number_input("Attribute 2", key="new_attr2") - cols[0].button("Add", on_click=add_object_callback, args=(new_attr1, new_attr2), - key=f"add {st.session_state.my_objects.__len__()}") From f4c93636853742a6557af121776e7086afb22913 Mon Sep 17 00:00:00 2001 From: calvinchai Date: Wed, 7 Feb 2024 22:06:38 +0000 Subject: [PATCH 2/8] update structure --- .gitignore | 1 + main.py | 12 ------------ scratch.py | 7 ------- setup.py | 0 test_autograder.py | 3 --- 5 files changed, 1 insertion(+), 22 deletions(-) delete mode 100644 scratch.py delete mode 100644 setup.py delete mode 100644 test_autograder.py diff --git a/.gitignore b/.gitignore index 9b2fd81..6a1c07a 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,5 @@ scratch/ # Ignore dynamic pages webui/pages/1_*.py webui/pages/2_*.py +webui/pages/scratch.py diff --git a/main.py b/main.py index a2664b6..ff9c152 100644 --- a/main.py +++ b/main.py @@ -4,12 +4,6 @@ from logging import handlers -# GUI will only be started for development purposes -def start_gui(): - from gui.app import start_app - start_app() - - def setup(): pass @@ -37,12 +31,6 @@ def main(): elif args.autograder: from magi.components.grader import grade_submission grade_submission() - elif args.mock: - pass - else: - start_gui() - from magi.managers import SettingManager - SettingManager.save_settings() if __name__ == '__main__': diff --git a/scratch.py b/scratch.py deleted file mode 100644 index 625dc2d..0000000 --- a/scratch.py +++ /dev/null @@ -1,7 +0,0 @@ -from magi.components.render import render_templates -from scratch.jinja.config import Config - -context = Config().__dict__ -render_templates('scratch/jinja/template', context, 'scratch/jinja/output') - - diff --git a/setup.py b/setup.py deleted file mode 100644 index e69de29..0000000 diff --git a/test_autograder.py b/test_autograder.py deleted file mode 100644 index 75ddae6..0000000 --- a/test_autograder.py +++ /dev/null @@ -1,3 +0,0 @@ -# we need to create the directory if we are not in the container environment - -# after setting things up for the user, we will run the python from that directory as a subprocess, in this case, we dont need to worry about path From 18759567e3d63e6d1f57bf7098e23ae5acc1933d Mon Sep 17 00:00:00 2001 From: calvinchai Date: Wed, 7 Feb 2024 22:12:13 +0000 Subject: [PATCH 3/8] update gitignore --- .gitignore | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitignore b/.gitignore index 6a1c07a..116dbd3 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,6 @@ **.pyc -venv/ output/ logs/ settings/ From 5899d17b91fa628fc455bab1a990ca972219f541 Mon Sep 17 00:00:00 2001 From: calvinchai Date: Wed, 7 Feb 2024 22:16:17 +0000 Subject: [PATCH 4/8] update setup --- main.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/main.py b/main.py index ff9c152..9aba41f 100644 --- a/main.py +++ b/main.py @@ -1,11 +1,16 @@ import argparse import logging +from pathlib import Path import time from logging import handlers def setup(): - pass + required_dirs = ['logs', 'workdir', 'settings', 'modules', 'plugins'] + app_path = Path(__file__).resolve().parent + for d in required_dirs: + if not app_path.joinpath(d).exists(): + app_path.joinpath(d).mkdir() def main(): From e102b5acf1c953da59ea103cecad600815a94dc9 Mon Sep 17 00:00:00 2001 From: calvinchai Date: Wed, 7 Feb 2024 22:36:11 +0000 Subject: [PATCH 5/8] update logging logic --- .streamlit/config.toml | 4 ++++ Dockerfile | 4 +++- magi/_private/base_settings.py | 4 ++-- main.py | 29 +++++++++++++++-------------- 4 files changed, 24 insertions(+), 17 deletions(-) diff --git a/.streamlit/config.toml b/.streamlit/config.toml index 43f6fd1..8f336d8 100644 --- a/.streamlit/config.toml +++ b/.streamlit/config.toml @@ -1,3 +1,6 @@ +[logger] + +level = "debug" [runner] fastReruns = false @@ -6,3 +9,4 @@ magicEnabled = false [server] enableCORS = false +folderWatchBlacklist = ['/home/kcc/MAGI/logs'] diff --git a/Dockerfile b/Dockerfile index 5781e8a..bbd606e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,6 +12,8 @@ RUN apt install python3-all python3-pip -y COPY static/GRADESCOPE_TEMPLATE/source/setup.sh /setup.sh RUN /bin/bash -c "apt-get install -y build-essential libgtest-dev cmake && cd /usr/src/gtest && cmake CMakeLists.txt && make" RUN pip install -r requirements.txt -RUN chmod +x /app/run.sh +RUN python3 main.py --setup + +RUN chmod +x /app/run.sh ENTRYPOINT ["/app/run.sh"] diff --git a/magi/_private/base_settings.py b/magi/_private/base_settings.py index 5984b39..4cf0411 100644 --- a/magi/_private/base_settings.py +++ b/magi/_private/base_settings.py @@ -6,8 +6,8 @@ class BaseSettings: project_name: str = "" project_description: str = field(default_factory=str, metadata={"text_area": True}) - strict_file_checking: bool = field(default=True, metadata={"help": "When enabled, student not submitting all required files will not be graded."}) - allow_all_file: bool = field(default=False, metadata={"help": "Allow all files to be uploaded, when disabled, only files in the submission_files list will be allowed."}) + strict_file_checking: bool = field(default=False, metadata={"help": "When enabled, student not submitting all required files will not be graded."}) + allow_all_file: bool = field(default=True, metadata={"help": "Allow all files to be uploaded, when disabled, only files in the submission_files list will be allowed."}) submission_files: List[str] = field(default_factory=list) output_dir: str = field(default_factory=lambda: "output", metadata={"excluded_from_ui": True}) enabled_module: str = field(default_factory=str, metadata={"excluded_from_ui": True}) diff --git a/main.py b/main.py index 9aba41f..ebe1670 100644 --- a/main.py +++ b/main.py @@ -1,9 +1,5 @@ import argparse -import logging from pathlib import Path -import time -from logging import handlers - def setup(): required_dirs = ['logs', 'workdir', 'settings', 'modules', 'plugins'] @@ -14,13 +10,7 @@ def setup(): def main(): - log_format = '%(asctime)s - %(name)s - %(levelname)s: %(message)s - %(pathname)s[line:%(lineno)d]' - logging.basicConfig(format=log_format, level=logging.DEBUG) - # TODO: log file path should be configured - th = handlers.TimedRotatingFileHandler(filename=f"logs/log-{time.strftime('%m%d%H%M')}.txt", encoding='utf-8') - formatter = logging.Formatter(log_format) - th.setFormatter(formatter) - logging.getLogger().addHandler(th) + parser = argparse.ArgumentParser() parser.add_argument("-s", "--setup", action="store_true") @@ -31,9 +21,20 @@ def main(): if args.setup: setup() - elif args.test: - pass - elif args.autograder: + return + import logging + from logging import handlers + import time + + log_format = '%(asctime)s - %(name)s - %(levelname)s: %(message)s - %(pathname)s[line:%(lineno)d]' + logging.basicConfig(format=log_format, level=logging.DEBUG) + # TODO: log file path should be configured + th = handlers.TimedRotatingFileHandler(filename=f"logs/log-{time.strftime('%m%d%H%M')}.txt", encoding='utf-8') + formatter = logging.Formatter(log_format) + th.setFormatter(formatter) + logging.getLogger().addHandler(th) + + if args.autograder: from magi.components.grader import grade_submission grade_submission() From c99bef632a62092e2b4eaee7390b95ea6e5417e1 Mon Sep 17 00:00:00 2001 From: calvinchai Date: Wed, 7 Feb 2024 22:42:49 +0000 Subject: [PATCH 6/8] optimize dockerfile order --- Dockerfile | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/Dockerfile b/Dockerfile index bbd606e..6e64f36 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,18 +1,16 @@ FROM ubuntu:22.04 LABEL authors="calvinchai" -COPY ./ /app - WORKDIR /app - -VOLUME /app/settings - +COPY ./requirements.txt /app/requirements.txt RUN apt update RUN apt install python3-all python3-pip -y -COPY static/GRADESCOPE_TEMPLATE/source/setup.sh /setup.sh RUN /bin/bash -c "apt-get install -y build-essential libgtest-dev cmake && cd /usr/src/gtest && cmake CMakeLists.txt && make" RUN pip install -r requirements.txt +COPY ./ /app +VOLUME /app/settings + RUN python3 main.py --setup RUN chmod +x /app/run.sh From b3128ade73b66a4e1d3bb7be958540d6d8700668 Mon Sep 17 00:00:00 2001 From: calvinchai Date: Wed, 7 Feb 2024 23:24:07 +0000 Subject: [PATCH 7/8] fix bug that TestManager is not reset between grading --- .streamlit/config.toml | 2 +- magi/components/grader.py | 3 ++- magi/managers/test_manager.py | 11 +++++++- mock/autograder/results/results.json | 40 ++++++---------------------- webui/functions/session.py | 6 +++-- webui/pages/8_Preview.py | 27 +++++++++++++++---- 6 files changed, 47 insertions(+), 42 deletions(-) diff --git a/.streamlit/config.toml b/.streamlit/config.toml index 8f336d8..73b9a38 100644 --- a/.streamlit/config.toml +++ b/.streamlit/config.toml @@ -9,4 +9,4 @@ magicEnabled = false [server] enableCORS = false -folderWatchBlacklist = ['/home/kcc/MAGI/logs'] +folderWatchBlacklist = ['logs','.git'] diff --git a/magi/components/grader.py b/magi/components/grader.py index af86840..075a4f0 100644 --- a/magi/components/grader.py +++ b/magi/components/grader.py @@ -64,7 +64,8 @@ def grade_submission(): from magi.managers import SettingManager from magi.managers.info_manager import Directories from magi.managers import AddonManager, TestManager - + TestManager.reset() + submission_files = SettingManager.BaseSettings.submission_files submission_dir = Directories.SUBMISSION_DIR os.makedirs(Directories.WORK_DIR, exist_ok=True) diff --git a/magi/managers/test_manager.py b/magi/managers/test_manager.py index e80b406..cf3bef2 100644 --- a/magi/managers/test_manager.py +++ b/magi/managers/test_manager.py @@ -12,7 +12,16 @@ test_cases_by_name = {} anonymous_counter: int = 0 - +def reset(): + global score, execution_time, output, extra_data, test_cases, test_cases_by_name, anonymous_counter + score = 0 + execution_time = 0 + output = "" + extra_data = {} + test_cases = [] + test_cases_by_name = {} + anonymous_counter = 0 + def output_global_message(msg: str): global output output += "\n" + msg diff --git a/mock/autograder/results/results.json b/mock/autograder/results/results.json index 8bd01f4..85c1f13 100644 --- a/mock/autograder/results/results.json +++ b/mock/autograder/results/results.json @@ -4,60 +4,36 @@ "stdout_visibility": "hidden", "extra_data": {}, "tests": [ - { - "score": 10, - "max_score": 10, - "visibility": "visible", - "output": "Successfully connected", - "name": "Test case 0", - "status": "" - }, - { - "score": 10, - "max_score": 10, - "visibility": "visible", - "output": "Test Case: 612 + 504 ...... SUCCESS", - "name": "Test case 1", - "status": "" - }, - { - "score": 10, - "max_score": 10, - "visibility": "visible", - "output": "Reached end of tests", - "name": "Test case 2", - "status": "" - }, { "score": 30, "max_score": 30, "visibility": "visible", "output": "Successfully connected", - "name": "Test case 3", + "name": "Test case 0", "status": "" }, { "score": 3, "max_score": 3, "visibility": "visible", - "output": "Test Case: 205 + 339 ...... SUCCESS", - "name": "Test case 4", + "output": "Test Case: 500 + 846 ...... SUCCESS", + "name": "Test case 1", "status": "" }, { "score": 3, "max_score": 3, "visibility": "visible", - "output": "Test Case: 744 - 322 ...... SUCCESS", - "name": "Test case 5", + "output": "Test Case: 106 + 574 ...... SUCCESS", + "name": "Test case 2", "status": "" }, { "score": 3, "max_score": 3, "visibility": "visible", - "output": "Test Case: 567 - 842 ...... SUCCESS", - "name": "Test case 6", + "output": "Test Case: 356 * 463 ...... SUCCESS", + "name": "Test case 3", "status": "" }, { @@ -65,7 +41,7 @@ "max_score": 30, "visibility": "visible", "output": "Reached end of tests", - "name": "Test case 7", + "name": "Test case 4", "status": "" } ], diff --git a/webui/functions/session.py b/webui/functions/session.py index e142db3..5533436 100644 --- a/webui/functions/session.py +++ b/webui/functions/session.py @@ -8,7 +8,7 @@ -def init_session(): +def init_session(preview_page=False): st.set_page_config(layout="wide") if "update_addon_lock" not in session_state: session_state.update_addon_lock = threading.Lock() @@ -25,7 +25,9 @@ def init_session(): session_state["InfoManager"] = InfoManager # print(session_state["AddonManager"]._name_to_modules["ClientServerSocket"].loaded) - + if not preview_page: + session_state["output_generated"] = False + update_pages() session_state["SettingManager"].save_settings() st.markdown( diff --git a/webui/pages/8_Preview.py b/webui/pages/8_Preview.py index 5e348c6..93b0013 100644 --- a/webui/pages/8_Preview.py +++ b/webui/pages/8_Preview.py @@ -5,13 +5,26 @@ from streamlit_file_browser import st_file_browser from webui.functions.session import init_session import json +import time -init_session() +init_session(preview_page=True) SettingManager = session_state.SettingManager AddonManager = session_state.AddonManager InfoManager = session_state.InfoManager st.write("# Preview") + +if not session_state.get("output_generated", False): + with st.spinner('Wait for generation...'): + from magi.components import generator + + generator.generate_output("output") + time.sleep(2) + +session_state["output_generated"] = True +st.write("## Test Solution") +uploaded_file = st.file_uploader("Choose a file", type="zip") + grading_progress_region = st.empty() result_region = st.empty() @@ -68,11 +81,15 @@ def grade_zip_file(zip_file_path: str | os.PathLike[str]): show_test_results() +if uploaded_file is not None: + bytes_data = uploaded_file.getvalue() + if uploaded_file.file_id+"tested" not in st.session_state: + st.session_state[uploaded_file.file_id+"tested"] = True + with open("output/submission.zip", "wb") as f: + f.write(bytes_data) + grade_zip_file("output/submission.zip") + -with st.spinner('Wait for generation...'): - from magi.components import generator - - generator.generate_output("output") st.write("## Download Autograder") From 09c41b72268307828e0144087a22c33805ed4893 Mon Sep 17 00:00:00 2001 From: calvinchai Date: Wed, 7 Feb 2024 23:43:23 +0000 Subject: [PATCH 8/8] refactor --- webui/pages/8_Preview.py | 206 ++++++++++++++++++++++----------------- 1 file changed, 114 insertions(+), 92 deletions(-) diff --git a/webui/pages/8_Preview.py b/webui/pages/8_Preview.py index 93b0013..8b8d49f 100644 --- a/webui/pages/8_Preview.py +++ b/webui/pages/8_Preview.py @@ -5,7 +5,9 @@ from streamlit_file_browser import st_file_browser from webui.functions.session import init_session import json -import time +import time + +from magi.components.grader import grade_zip_submission init_session(preview_page=True) SettingManager = session_state.SettingManager @@ -29,114 +31,134 @@ result_region = st.empty() -def display_test_results(data): - # Load data from JSON string if not already a dictionary - if isinstance(data, str): - data = json.loads(data) +def show_test_results(): + with open(InfoManager.Directories.RESULT_JSON_PATH) as f: + data = json.load(f) + display_test_results(data) - # Calculate total score and total max score - total_score = sum(test['score'] for test in data['tests']) - total_max_score = sum(test['max_score'] for test in data['tests']) + +def display_test_results(data): with result_region.container(): st.write("## Test Results") - # Display total score and total max score + total_score, total_max_score = calculate_scores(data) st.write(f"### Total Achieved Score: {total_score}/{total_max_score}") - with st.expander("Test Cases Details"): - # Display each test case - for test in data['tests']: - # Determine color based on score - color = "green" if test['score'] == test['max_score'] else "red" + display_test_cases(data) - # Display test case details with color - with st.container(border=True): - st.markdown(f"**{test['name']}** ({test['visibility']})") - st.markdown( - f"{test['score']}/{test['max_score']}", unsafe_allow_html=True) - # Check if output should be shown based on visibility +def calculate_scores(data): + total_score = sum(test['score'] for test in data['tests']) + total_max_score = sum(test['max_score'] for test in data['tests']) + return total_score, total_max_score - if test.get('status'): - st.write(f"Status: {test['status']}") - if test.get('output'): - st.text_area("Output:", test['output'], height=100,key=test['name']) - # Optionally display the main output if needed - if data.get('output'): - with st.expander("Main Output"): - st.write("Main Output:") - st.code(data['output']) +def display_test_cases(data): + with st.expander("Test Cases Details"): + for test in data['tests']: + display_test_case(test) + if 'output' in data: + with st.expander("Main Output"): + st.code(data['output']) -def show_test_results(): - with open(InfoManager.Directories.RESULT_JSON_PATH) as f: - data = json.load(f) - display_test_results(data) +def display_test_case(test): + color = "green" if test['score'] == test['max_score'] else "red" + st.markdown(f"**{test['name']}** ({test['visibility']})") + st.markdown( + f"{test['score']}/{test['max_score']}", unsafe_allow_html=True) + if test.get('status'): + st.write(f"Status: {test['status']}") + if test.get('output'): + st.text_area("Output:", test['output'], height=100, key=test['name']) def grade_zip_file(zip_file_path: str | os.PathLike[str]): with grading_progress_region.container(): with st.spinner('Grading...'): - from magi.components.grader import grade_zip_submission grade_zip_submission(zip_file_path) - show_test_results() -if uploaded_file is not None: - bytes_data = uploaded_file.getvalue() - if uploaded_file.file_id+"tested" not in st.session_state: - st.session_state[uploaded_file.file_id+"tested"] = True - with open("output/submission.zip", "wb") as f: - f.write(bytes_data) - grade_zip_file("output/submission.zip") - - - - -st.write("## Download Autograder") - -if os.path.exists("output/autograder.zip"): - - st.download_button("Download autograder.zip", open( - "output/autograder.zip", mode="rb"), "autograder.zip", type="primary") - - with st.container(border=True): - st.write("### Autograder Contents") - st_file_browser("output/source", key="autograder.zip") - -else: - st.warning("Autograder not found") - - -st.write("## Download Solution") -if os.path.exists("output/solution.zip"): - st.download_button("Download solution.zip", open( - "output/solution.zip", mode="rb"), "solution.zip", type="primary") - if st.button("Test autograder with generated solution"): - grade_zip_file("output/solution.zip") - with st.container(border=True): - st.write("### Solution Contents") - st_file_browser("output/solution", key="solution.zip") - # st.button("Test autograder with generated solution") +def check_uploaded_file(uploaded_file): + if uploaded_file is not None: + bytes_data = uploaded_file.getvalue() + if uploaded_file.file_id+"tested" not in st.session_state: + st.session_state[uploaded_file.file_id+"tested"] = True + with open("output/submission.zip", "wb") as f: + f.write(bytes_data) + grade_zip_file("output/submission.zip") + + +def download_section(title, file_path, download_label, content_label=None, key=None, extra_action=None, actual_folder=None): + """Display a section for downloading files and showing their contents if available. + + Args: + title (str): The title of the section. + file_path (str): The path to the file to be downloaded. + download_label (str): The label for the download button. + content_label (str, optional): The label for the content section. Defaults to None. + key (str, optional): The key for the file browser or content display. Defaults to None. + extra_action (function, optional): An optional function to execute after the download button. Defaults to None. + """ + st.write(f"## {title}") + if not os.path.exists(file_path): + st.warning(f"{title} not found.") + return + with open(file_path, "rb") as file: + st.download_button(download_label, file, file_path.split('/')[-1], type="primary") + + with st.container(): + if content_label: + st.write(f"### {content_label}") + if 'zip' in file_path: # For zip files, show file browser + with st.container(border=True): + st_file_browser(os.path.join(os.path.dirname(file_path),actual_folder) if actual_folder else file_path[:-4], key=key) + else: # For other file types, potentially show content directly + if extra_action: + extra_action(file_path) + + +def show_code_editor(file_path): + """Display a code editor with the contents of the given file. + + Args: + file_path (str): The path to the file to be displayed in the code editor. + """ + with open(file_path, "r") as file: + code_editor.code_editor(file.read(), lang="markdown") + +# Download sections +download_section( + title="Download Autograder", + file_path="output/autograder.zip", + download_label="Download autograder.zip", + content_label="Autograder Contents", + key="autograder.zip", + actual_folder="source" +) + +download_section( + title="Download Solution", + file_path="output/solution.zip", + download_label="Download solution.zip", + content_label="Solution Contents", + key="solution.zip", + extra_action=lambda _: grade_zip_file("output/solution.zip") if st.button("Test autograder with generated solution") else None +) + +download_section( + title="Download Documentation", + file_path="output/misc/documentation.md", + download_label="Download documentation.md", + extra_action=show_code_editor +) + +download_section( + title="Download Misc Files", + file_path="output/misc.zip", + download_label="Download misc.zip", + content_label="Misc Contents", + key="misc.zip" +) + +check_uploaded_file(uploaded_file) -else: - st.warning("No solution was generated") - -st.write("## Download Documentation") -if os.path.exists("output/misc/documentation.md"): - st.download_button("Download documentation.md", open( - "output/misc/documentation.md", mode="rb"), "documentation.md", type="primary") - code_editor.code_editor( - open("output/misc/documentation.md").read(), lang="markdown") -else: - st.warning("No documentation was generated") - -st.write("## Download Solution") -if os.path.exists("output/misc.zip"): - st.download_button("Download misc.zip", open( - "output/misc.zip", mode="rb"), "misc.zip", type="primary") - with st.container(border=True): - st.write("### Misc Contents") - st_file_browser("output/misc", key="misc.zip") -else: - st.warning("No additional file was generated")