diff --git a/.gitignore b/.gitignore
index 91d8dfa3..13ed74f9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -38,3 +38,4 @@ tmpclaude-*
__pycache__/
+src/example.py
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index 8e444964..5f937228 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -12,6 +12,7 @@
"@codemirror/theme-one-dark": "^6.0.0",
"@xyflow/svelte": "^1.5.0",
"codemirror": "^6.0.0",
+ "dotenv": "^17.2.3",
"katex": "^0.16.0",
"pathfinding": "^0.4.18",
"plotly.js-dist-min": "^2.35.0",
@@ -1811,6 +1812,18 @@
"integrity": "sha512-jDwizj+IlEZBunHcOuuFVBnIMPAEHvTsJj0BcIp94xYguLRVBcXO853px/MyIJvbVzWdsGvrRweIUWJw8hBP7A==",
"license": "MIT"
},
+ "node_modules/dotenv": {
+ "version": "17.2.3",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz",
+ "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://dotenvx.com"
+ }
+ },
"node_modules/esbuild": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz",
diff --git a/package.json b/package.json
index 9d626406..9fd16148 100644
--- a/package.json
+++ b/package.json
@@ -17,7 +17,9 @@
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "eslint .",
- "format": "prettier --write ."
+ "format": "prettier --write .",
+ "start:backend": "python src/backend.py",
+ "start:both": "concurrently \"npm run dev\" \"npm run start:backend\""
},
"devDependencies": {
"@sveltejs/adapter-static": "^3.0.0",
@@ -40,6 +42,7 @@
"@codemirror/theme-one-dark": "^6.0.0",
"@xyflow/svelte": "^1.5.0",
"codemirror": "^6.0.0",
+ "dotenv": "^17.2.3",
"katex": "^0.16.0",
"pathfinding": "^0.4.18",
"plotly.js-dist-min": "^2.35.0",
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 00000000..9b3a6938
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,8 @@
+# Web application dependencies (not needed for core package)
+Flask>=3.1.1
+flask-cors>=6.0.1
+
+numpy
+pathsim
+pathsim_chem
+dotenv
\ No newline at end of file
diff --git a/src/backend.py b/src/backend.py
new file mode 100644
index 00000000..acbbb617
--- /dev/null
+++ b/src/backend.py
@@ -0,0 +1,338 @@
+import os
+import json
+import traceback
+import requests as req
+from flask import Flask, request, jsonify, Response, stream_with_context, session
+from flask_session import Session
+from flask_cors import CORS
+
+from cachelib import FileSystemCache
+
+from dotenv import load_dotenv
+import pickle
+import types
+import uuid
+
+import io
+from contextlib import redirect_stdout, redirect_stderr
+
+# Initialization Code
+
+import ast
+import numpy as np
+import gc
+import pathsim, pathsim_chem
+
+print(f"PathSim {pathsim.__version__} loaded successfully")
+
+STREAMING_STEP_EXPR = "_step_streaming_gen()"
+
+_clean_globals = set(globals().keys())
+
+'''
+The Flask web server would not be initialized simultaneously with the SvelteKit website since the latter is statically generated,
+rather there would be some type of deployment of this application such that it could receive requests from
+"https://view.pathsim.org" (which I think is already encapsualted by the "*" in the CORS.resources.options parameter)
+'''
+
+load_dotenv()
+
+server_namespaces = {}
+
+app = Flask(__name__, static_folder="../static", static_url_path="")
+
+# app.secret_key = os.getenv("SECRET_KEY")
+# app.config["SECRET_KEY"] = os.getenv("SECRET_KEY")
+
+# app.config["SESSION_PERMANENT"] = False
+# app.config["SESSION_TYPE"] = 'filesystem'
+app.config.update(
+ SECRET_KEY=os.getenv("SECRET_KEY"), # Required for session
+ SESSION_COOKIE_SAMESITE='None',
+ SESSION_COOKIE_SECURE=True,
+ SESSION_COOKIE_HTTPONLY=True,
+)
+# app.config["SESSION_SERIALIZATION_FORMAT"] = 'json'
+# app.config["SESSION_CACHELIB"] = FileSystemCache(threshold=500, cache_dir="/sessions"),
+
+# Session(app)f
+
+if os.getenv("FLASK_ENV") == "production":
+ CORS(app,
+ resources={
+ r"/*": {
+ "origins": ["*"],
+ "methods": ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
+ "allow_headers": ["Content-Type", "Authorization"]
+ }
+ }, supports_credentials=True)
+else:
+ print("We are not in production...")
+ CORS(
+ app,
+ resources={
+ r"/*": {""
+ "origins": ["http://localhost:5173", "http://localhost:3000"]
+ }
+ },
+ supports_credentials=True
+ )
+
+@app.route("/initialize", methods=["GET"])
+def initalize():
+ session_id = None
+
+ if "id" in session:
+ session_id = session["id"]
+ app.logger.info("We already have a session ID it is...")
+ app.logger.info(session_id)
+ else:
+ app.logger.info("Making a session id...")
+ session_id = str(os.urandom(12))
+ app.logger.info("Made the id: ")
+ app.logger.info(session_id)
+ session["id"] = session_id
+
+ server_namespaces[session_id] = {}
+
+ try:
+
+ return jsonify({
+ "success": True,
+ "id": session_id
+ })
+ except Exception as e:
+ return jsonify({
+ "success": False,
+ "error": e
+ }), 400
+
+@app.route("/idCheck", methods=["GET"])
+def idCheck():
+ session_id = None
+ if "id" in session:
+ session_id = session["id"]
+
+ return jsonify({ "success": True, "id": session_id })
+
+@app.route("/namespaceCheck", methods=["GET"])
+def namespaceCheck():
+ namespace = {}
+ session_id = None
+ if "id" in session:
+ app.logger.info("The id exists...")
+ session_id = session["id"]
+ if session_id in server_namespaces:
+ app.logger.info("Found an associated namespace...")
+ namespace = server_namespaces[session["id"]]
+ keys = ""
+ if isinstance(namespace, dict):
+ for k in server_namespaces.keys():
+ keys += k + ", "
+
+ return jsonify({ "success": True, "namespace_keys": keys})
+# Execute Python route copied from the previous repository
+@app.route("/execute-code", methods=["POST"])
+def execute_code():
+ """Execute Python code and returns nothing."""
+
+ try:
+ data = request.json
+ code = data.get("code", "")
+
+ if not code.strip():
+ return jsonify({"success": False, "error": "No code provided"}), 400
+
+ # Capture stdout and stderr
+ stdout_capture = io.StringIO()
+ stderr_capture = io.StringIO()
+
+ user_namespace = {}
+ app.logger.info("Session: ", session)
+ app.logger.info("Session ID: ", session["id"])
+ if "id" in session:
+ user_namespace = server_namespaces[session["id"]]
+
+ try:
+ with redirect_stdout(stdout_capture), redirect_stderr(stderr_capture):
+ exec(code, user_namespace)
+
+ app.logger.info("User Namespace: ", user_namespace)
+
+ if "id" in session:
+ server_namespaces[session["id"]] = user_namespace
+
+ # Capture any output
+ output = stdout_capture.getvalue()
+ error_output = stderr_capture.getvalue()
+
+ if error_output:
+ return jsonify({"success": False, "error": error_output})
+
+ return jsonify(
+ {
+ "success": True,
+ "output": output,
+ }
+ )
+
+ except SyntaxError as e:
+ return jsonify({"success": False, "error": f"Syntax Error: {str(e)}"}), 400
+ except Exception as e:
+ return jsonify({"success": False, "error": f"Runtime Error: {str(e)}"}), 400
+
+ except Exception as e:
+ return jsonify({"success": False, "error": f"Server error: {str(e)}"}), 500
+
+@app.route("/evaluate-expression", methods=["POST"])
+def evaluate_expression():
+ "Evaluates Python expression and returns result"
+ try:
+ data = request.json
+ expr = data.get("expr")
+
+ if not expr.strip():
+ return jsonify({"success": False, "error": "No Python expression provided"}), 400
+
+ stdout_capture = io.StringIO()
+ stderr_capture = io.StringIO()
+
+ user_namespace = {}
+ if "id" in session:
+ user_namespace = server_namespaces[session["id"]]
+
+ try:
+ result = ""
+ with redirect_stdout(stdout_capture), redirect_stderr(stderr_capture):
+ result = eval(expr, user_namespace)
+
+ app.logger.info("User Namespace: ", user_namespace)
+
+ if "id" in session:
+ server_namespaces[session["id"]] = user_namespace
+
+ # Capture any output
+ output = stdout_capture.getvalue()
+ error_output = stderr_capture.getvalue()
+
+ if error_output:
+ return jsonify({"success": False, "error": error_output})
+
+ return jsonify(
+ {
+ "success": True,
+ "result": result,
+ "output": output
+ }
+ )
+
+ except SyntaxError as e:
+ return jsonify({"success": False, "error": f"Syntax Error: {str(e)}"}), 400
+ except Exception as e:
+ return jsonify({"success": False, "error": f"Runtime Error: {str(e)}"}), 400
+ except Exception as e:
+ return jsonify({"success": False, "error": f"Server error: {str(e)}"}), 500
+
+
+@app.route("/traceback", methods=["GET"])
+def check_traceback():
+ try:
+ traceback_text = traceback.format_exc()
+ return jsonify({"success": True, "traceback": traceback_text})
+ except Exception as e:
+ return jsonify({"success": False, "error": f"Server-side error: {e}"})
+
+@app.route("/streamData", methods=["POST", "GET"])
+def stream_data():
+ def generate(expr):
+
+ # Capture stdout and stderror
+ stdout_capture = io.StringIO()
+ stderr_capture = io.StringIO()
+
+ user_namespace = {}
+ if "id" in session:
+ user_namespace = server_namespaces[session["id"]]
+
+ isDone = False
+
+ while not isDone:
+
+ result = " "
+ with redirect_stdout(stdout_capture), redirect_stderr(stderr_capture):
+ result = eval(expr, user_namespace)
+
+ app.logger.info("User Namespace: ", user_namespace)
+
+ if "id" in session:
+ server_namespaces[session["id"]] = user_namespace
+
+ # Capture any output
+ output = stdout_capture.getvalue()
+ error_output = stderr_capture.getvalue()
+
+ if error_output:
+ return jsonify({"success": False, "error": error_output})
+
+ # Directly responding with a Flask Response object (as jsonify(...) does) doesn't work
+ # so we need to use the json.dumps(...) function to return a string so that it can pass into
+ # stream_with_context(...)
+
+ yield json.dumps(
+ {
+ "success": True,
+ "result": result,
+ "output": output
+ }
+ )
+
+ if result["done"]:
+ isDone = True
+
+ try:
+ method = request.method
+
+ expr = STREAMING_STEP_EXPR
+
+ if method == "POST":
+ data = request.json
+ expr = data.get("expr")
+
+ try:
+ return Response(stream_with_context(generate(expr)), content_type='application/json')
+
+ except SyntaxError as e:
+ return jsonify({"success": False, "error": f"Syntax Error: {str(e)}"}), 400
+ except Exception as e:
+ return jsonify({"success": False, "error": f"Runtime Error: {str(e)}"}), 400
+
+
+ except Exception as e:
+ return jsonify({"success": False, "error": f"Server error: {str(e)}"}), 500
+
+
+# Global error handler to ensure all errors return JSON
+@app.errorhandler(Exception)
+def handle_exception(e):
+ """Global exception handler to ensure JSON responses."""
+ import traceback
+ from werkzeug.exceptions import HTTPException
+
+ error_details = traceback.format_exc()
+ print(f"Unhandled exception: {error_details}")
+
+ # For HTTP exceptions, return a cleaner response
+ if isinstance(e, HTTPException):
+ return jsonify(
+ {"success": False, "error": f"{e.name}: {e.description}"}
+ ), e.code
+
+ # For all other exceptions, return a generic JSON error
+ return jsonify({"success": False, "error": f"Internal server error: {str(e)}"}), 500
+
+
+if __name__ == "__main__":
+ port = int(os.getenv("PORT", 8000))
+ print("Hello there...our port is: ", port)
+ print("Application Configuration: ", app.config)
+ app.run(host="0.0.0.0", port=port, debug=os.getenv("FLASK_ENV") != "production")
\ No newline at end of file
diff --git a/src/lib/components/dialogs/KeyboardShortcutsDialog.svelte b/src/lib/components/dialogs/KeyboardShortcutsDialog.svelte
index be3eb955..29fb13e9 100644
--- a/src/lib/components/dialogs/KeyboardShortcutsDialog.svelte
+++ b/src/lib/components/dialogs/KeyboardShortcutsDialog.svelte
@@ -58,7 +58,7 @@
{ keys: ['H'], description: 'Go to root' },
{ keys: ['+'], description: 'Zoom in' },
{ keys: ['-'], description: 'Zoom out' },
- { keys: ['T'], description: 'Theme' }
+ { keys: ['T'], description: 'Theme' },
]
},
{
@@ -70,7 +70,7 @@
{ keys: ['E'], description: 'Editor' },
{ keys: ['V'], description: 'Results' },
{ keys: ['C'], description: 'Console' },
- { keys: ['P'], description: 'Pin plots' }
+ { keys: ['P'], description: 'Pin plots' },
]
},
{
@@ -78,7 +78,8 @@
items: [
{ keys: ['Ctrl', 'Enter'], description: 'Simulate' },
{ keys: ['Shift', 'Enter'], description: 'Continue' },
- { keys: ['?'], description: 'Shortcuts' }
+ { keys: ['?'], description: 'Shortcuts' },
+ { keys: ['Q'], description: 'Toggle Backend Preference'},
]
}
];
diff --git a/src/lib/components/icons/Icon.svelte b/src/lib/components/icons/Icon.svelte
index b8cbc042..08280c0c 100644
--- a/src/lib/components/icons/Icon.svelte
+++ b/src/lib/components/icons/Icon.svelte
@@ -415,6 +415,15 @@
+{:else if name === "server"}
+
+{:else if name == "laptop"}
+
{:else if name === 'font-size-increase'}