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'} A diff --git a/src/lib/constants/messages.ts b/src/lib/constants/messages.ts index 68e4e451..65f44126 100644 --- a/src/lib/constants/messages.ts +++ b/src/lib/constants/messages.ts @@ -8,7 +8,8 @@ export const PROGRESS_MESSAGES = { INSTALLING_PATHSIM: 'Installing PathSim...', INSTALLING_PATHSIM_CHEM: 'Installing PathSim-Chem...', STARTING_WORKER: 'Starting worker...', - STARTING_SIMULATION: 'Starting simulation...' + STARTING_SIMULATION: 'Starting simulation...', + CHECKING_FLASK_INITIALIZATION: 'Checking Flask web server intialization status...' } as const; export const STATUS_MESSAGES = { diff --git a/src/lib/pyodide/backend/flask/backend.ts b/src/lib/pyodide/backend/flask/backend.ts new file mode 100644 index 00000000..b80bfdfb --- /dev/null +++ b/src/lib/pyodide/backend/flask/backend.ts @@ -0,0 +1,622 @@ +/** + * Flask Backend Implementation + * + * Runs Python code via API rqeuests to a Flask web server + * Supports streaming with code injection between generator steps. + * The pyodide backend has a general handler function which handles the responses that a Web Worker provides + * however, we directly handle data given the fact that we don't instantiate a worker and use a Flask web server + */ + +import { STATUS_MESSAGES } from "$lib/constants/messages"; +import { TIMEOUTS } from "$lib/constants/python"; +import { getFlaskBackendUrl } from "$lib/utils/flaskRoutes"; +import { backendState } from "../state"; +import type { + Backend, + BackendState, + REPLErrorResponse, + REPLResponse, +} from "../types"; + +interface PendingRequest { + resolve: (value: string | undefined) => void; + reject: (error: Error) => void; + timeoutId: ReturnType; +} + +interface StreamState { + id: string | null; + onData: ((data: unknown) => void) | null; + onDone: (() => void) | null; + onError: ((error: Error) => void) | null; +} + +interface BackendRequest { + type: + | "exec" + | "eval" + | "evaluate" + | "stream-start" + | "stream-exec" + | "stream-stop"; + id?: string; + code?: null | string; + expr?: null | string; +} + +export class FlaskBackend implements Backend { + streamingCodeQueue: string[] = []; + streamingActive: boolean = false; + + private streamState: StreamState = { + id: null, + onData: null, + onDone: null, + onError: null, + }; + private pendingRequests = new Map(); + private messageId = 0; + private isInitializing = false + + // Output callbacks + private stdoutCallback: ((value: string) => void) | null = null; + private stderrCallback: ((value: string) => void) | null = null; + + // ------------------------------------------------------------------------- + // Lifecycle + // ------------------------------------------------------------------------- + + async init(): Promise { + const state = this.getState(); + if (state.initialized || state.loading) return; + + let id = this.generateId() + + this.isInitializing = true + + let data:any = await fetch(getFlaskBackendUrl()+"/initialize", { + credentials: "include" + }).then(res => res.json()) + + if(data.success) { + this.handleResponse({ + type: "stdout", + value: "Your backend preference has been set to a Flask web server, initialization has already occured", + }); + + this.handleResponse({ type: "ready" }); + } else { + let error = data.error + this.handleError({ + type: "error", + error: error + }) + this.isInitializing = false + backendState.update((s) => ({ + ...s, + loading: false, + error: error instanceof Error ? error.message : String(error) + })); + throw error; + } + } + + terminate(): void {} + + // ------------------------------------------------------------------------- + // State + // ------------------------------------------------------------------------- + + getState(): BackendState { + return backendState.get(); + } + + subscribe(callback: (state: BackendState) => void): () => void { + return backendState.subscribe(callback); + } + + isReady(): boolean { + return this.getState().initialized; + } + + isLoading(): boolean { + return this.getState().loading; + } + + getError(): string | null { + return this.getState().error; + } + + // ------------------------------------------------------------------------- + // Execution + // ------------------------------------------------------------------------- + + async exec( + code: string, + timeout: number = TIMEOUTS.SIMULATION, + ): Promise { + const id = this.generateId(); + + return new Promise((resolve, reject) => { + const timeoutId = setTimeout(() => { + if (this.pendingRequests.has(id)) { + this.pendingRequests.delete(id); + reject(new Error("Execution timeout")); + } + }, timeout); + + this.pendingRequests.set(id, { + resolve: () => resolve(), + reject, + timeoutId, + }); + + this.handleRequest({ type: "exec", id, code }); + }); + } + + async evaluate( + expr: string, + timeout: number = TIMEOUTS.SIMULATION, + ): Promise { + const id = this.generateId(); + + return new Promise((resolve, reject) => { + const timeoutId = setTimeout(() => { + if (this.pendingRequests.has(id)) { + this.pendingRequests.delete(id); + reject(new Error("Evaluation timeout")); + } + }, timeout); + + this.pendingRequests.set(id, { + resolve: (value) => { + if (value === undefined) { + reject(new Error("No value returned from eval")); + return; + } + try { + resolve(JSON.parse(value) as T); + } catch { + reject( + new Error(`Failed to parse eval result: ${value}`), + ); + } + }, + reject, + timeoutId, + }); + + this.handleRequest({ type: "eval", id, expr }); + }); + } + + // ------------------------------------------------------------------------- + // Streaming + // ------------------------------------------------------------------------- + + startStreaming( + expr: string, + onData: (data: T) => void, + onDone: () => void, + onError: (error: Error) => void, + ): void { + if (!this.isReady()) { + onError(new Error("Backend not initialized")); + return; + } + + // Stop any existing stream + if (this.streamState.id) { + this.stopStreaming(); + } + + const id = this.generateId(); + this.streamState = { + id, + onData: onData as (data: unknown) => void, + onDone, + onError, + }; + + this.handleRequest({ type: "stream-start", id, expr }); + } + + stopStreaming(): void { + if (!this.streamState.id) return; + + this.handleRequest({ type: "stream-stop" }); + } + + execDuringStreaming(code: string): void { + if (!this.streamState.id) { + console.warn("Cannot exec during streaming: no active stream"); + return; + } + this.handleRequest({ type: "stream-exec", code }); + } + + isStreaming(): boolean { + return this.streamState.id != null; + } + + private async runStreamingLoop(id: string, expr: string): Promise { + try { + this.streamingActive = true; + + this.streamingCodeQueue.length = 0; + + // Unlike the Pyodide version, we only need a while loop to go through commands that are queued up, + // It isn't necessary that we maintain a while loop while streaming as we are reading it with the fetch API + + while (this.streamingCodeQueue.length > 0) { + const code = this.streamingCodeQueue.shift()!; + try { + // Simply sending requests to the Flask api to execute code, + // we only really care if errors are produced so we don't handle any data response. + // There is also no need for streaming data here.... + + let data = await fetch( + getFlaskBackendUrl() + "/execute-code", + { + credentials: "include", + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ code: code }), + }, + ).then((res) => res.json()); + if (data.success && data.output) { + this.handleResponse({ + type: "stdout", + value: data.output, + }); + } else { + throw Error(data.error); + } + } catch (error) { + const errorMsg = + error instanceof Error ? error.message : String(error); + this.handleResponse({ + type: "stderr", + value: `Stream exec error: ${errorMsg}`, + }); + } + } + + let response = await fetch(getFlaskBackendUrl() + "/streamData", { + credentials: "include" + }); + + if (response.body) { + for await (const chunk of response.body) { + let resArray: string[] = []; + + const decoder = new TextDecoder("utf-8"); + let jsonString = decoder.decode(chunk); + + const responseSubstr = "success"; + + /* Sometimes within the same chunk I'll receive more than one API response + * such that it appears liked jsonString = "{"success":...,"data":...}{"success":...,"data":...}" + * so we need to take the time to break it up into its individual requests + * */ + + let doneBreakingUp = false; + + while ( + jsonString.indexOf(responseSubstr) && + !doneBreakingUp + ) { + let firstIndex = jsonString.indexOf(responseSubstr); + + let remainingString = jsonString.slice( + firstIndex + responseSubstr.length, + jsonString.length, + ); + let secondIndex = + remainingString.indexOf(responseSubstr); + + if (secondIndex == -1 || firstIndex == -1) { + resArray.push(jsonString); + doneBreakingUp = true; + } else { + let request = jsonString.slice( + firstIndex - 2, + firstIndex + + responseSubstr.length + + secondIndex - + 2, + ); + resArray.push(request); + jsonString = jsonString.slice( + firstIndex + + responseSubstr.length + + secondIndex - + 2, + jsonString.length, + ); + } + } + + let dataChunks = resArray.flatMap((res) => { + let parsedResponse = JSON.parse(res); + if ( + parsedResponse.success && + !parsedResponse.result.done && + parsedResponse.result.result + ) { + return parsedResponse; + } else { + return []; + } + }); + + for (let dataChunk of dataChunks) { + console.log("Streaming data: ", dataChunk.result) + this.handleResponse({ + type: "stream-data", + id, + value: JSON.stringify(dataChunk.result) as string, + }); + } + } + } + } catch (error) { + return this.runTracebackWithFlask(id, error); + } finally { + this.streamingActive = false; + this.handleResponse({ type: "stream-done", id }); + } + } + + // ------------------------------------------------------------------------- + // Output Callbacks + // ------------------------------------------------------------------------- + + onStdout(callback: (value: string) => void): void { + this.stdoutCallback = callback; + } + + onStderr(callback: (value: string) => void): void { + this.stderrCallback = callback; + } + + // ------------------------------------------------------------------------- + // Private Functions + // ------------------------------------------------------------------------- + private handleError(response: REPLErrorResponse): void { + const { id, error, traceback } = response; + const errorMsg = traceback + ? `${error}\n${traceback}` + : error || "Unknown error"; + + // Handle pending request errors + if (id && this.pendingRequests.has(id)) { + const pending = this.pendingRequests.get(id)!; + clearTimeout(pending.timeoutId); + pending.reject(new Error(errorMsg)); + this.pendingRequests.delete(id); + } + + // Handle streaming errors + if (id === this.streamState.id && this.streamState.onError) { + this.streamState.onError(new Error(errorMsg)); + this.streamState = { + id: null, + onData: null, + onDone: null, + onError: null, + }; + } + + backendState.update((s) => ({ ...s, error: error || "Unknown error" })); + } + + private async runTracebackWithFlask( + id: string, + error: unknown, + ): Promise { + const errorMsg = error instanceof Error ? error.message : String(error); + let traceback: string | undefined; + + try { + traceback = ( + await fetch(getFlaskBackendUrl() + "/traceback", { + credentials: "include" + }) + .then((res) => res.json()) + .then((res) => res.json) + ).traceback as string; + } catch (error) { + // Ignore as in the Pyodide framework + } + + this.handleResponse({ type: "error", id, error: errorMsg, traceback }); + } + + private handleResponse(response: REPLResponse): void { + switch (response.type) { + case "ready": + backendState.update((s) => ({ + ...s, + initialized: true, + loading: false, + progress: STATUS_MESSAGES.READY, + })); + this.isInitializing = false + break; + + case "progress": + backendState.update((s) => ({ + ...s, + progress: response.value || "", + })); + break; + + case "ok": + if (response.id && this.pendingRequests.has(response.id)) { + const pending = this.pendingRequests.get(response.id)!; + clearTimeout(pending.timeoutId); + pending.resolve(undefined); + this.pendingRequests.delete(response.id); + } + break; + + case "value": + if (response.id && this.pendingRequests.has(response.id)) { + const pending = this.pendingRequests.get(response.id)!; + clearTimeout(pending.timeoutId); + pending.resolve(response.value); + this.pendingRequests.delete(response.id); + } + break; + + case "error": + this.handleError(response); + break; + + case "stdout": + if (response.value && this.stdoutCallback) { + this.stdoutCallback(response.value); + } + break; + + case "stderr": + if (response.value && this.stderrCallback) { + this.stderrCallback(response.value); + } + break; + + case "stream-data": + if ( + response.id === this.streamState.id && + this.streamState.onData && + response.value + ) { + try { + this.streamState.onData(JSON.parse(response.value)); + } catch { + // Ignore parse errors + } + } + break; + + case "stream-done": + if ( + response.id === this.streamState.id && + this.streamState.onDone + ) { + this.streamState.onDone(); + this.streamState = { + id: null, + onData: null, + onDone: null, + onError: null, + }; + } + break; + } + } + + private generateId(): string { + return `flask_repl_${++this.messageId}`; + } + + private async handleRequest(request: BackendRequest) { + let { type, id, code, expr } = request; + id = "id" in request ? (request.id as string) : ""; + + switch (type) { + case "exec": + try { + let data = await fetch( + getFlaskBackendUrl() + "/execute-code", + { + credentials: "include", + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ namespace: localStorage.getItem("namespace"), code: code }), + }, + ).then((res) => res.json()); + if (data.success && !data.error) { + localStorage.setItem("namespace", data.namespace) + console.log("The data given from /execute-code", data) + if (data.output) { + this.handleResponse({ + type: "stdout", + value: data.output, + }); + } + this.handleResponse({ type: "ok", id }); + } else { + throw Error(data.error); + } + } catch (error) { + // This traceback may actually be unnecessary in all Flaskc ases, + // unless there is someway that the error output doesn't fully capture all the relevant information + return this.runTracebackWithFlask(id, error); + } + break; + + case "eval": + try { + let data = await fetch( + getFlaskBackendUrl() + "/evaluate-expression", + { + credentials: "include", + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ expr: expr }), + }, + ).then((res) => res.json()); + if (data.success && !data.error) { + if (data.output) { + this.handleResponse({ + type: "stdout", + value: data.output, + }); + } + this.handleResponse({ + type: "value", + id, + value: JSON.stringify(data.result) as string, + }); + } else { + throw Error(data.error); + } + } catch (error) { + return this.runTracebackWithFlask(id, error); + } + break; + + case "stream-start": + if (!id || typeof expr !== "string") { + throw new Error( + "Invalid stream-start request: missing id or expr", + ); + } + // Don't await - let it run autonomously + this.runStreamingLoop(id, expr); + break; + + case "stream-exec": + if (typeof code === "string" && this.isStreaming()) { + // Queue code to be executed between generator steps + this.streamingCodeQueue.push(code); + } + break; + + case "stream-stop": + this.streamingActive = false; + break; + default: + throw new Error(`Unknown message type: ${type}`); + } + } +} diff --git a/src/lib/pyodide/backend/index.ts b/src/lib/pyodide/backend/index.ts index aea26ee5..6c9ec766 100644 --- a/src/lib/pyodide/backend/index.ts +++ b/src/lib/pyodide/backend/index.ts @@ -34,6 +34,7 @@ export { PyodideBackend } from './pyodide/backend'; import { getBackend } from './registry'; import { backendState } from './state'; import { consoleStore } from '$lib/stores/console'; +import type { BackendPreference } from '$lib/types'; // Alias for backward compatibility export const replState = { diff --git a/src/lib/pyodide/backend/pyodide/backend.ts b/src/lib/pyodide/backend/pyodide/backend.ts index 93d4d1b0..f88466ce 100644 --- a/src/lib/pyodide/backend/pyodide/backend.ts +++ b/src/lib/pyodide/backend/pyodide/backend.ts @@ -3,7 +3,6 @@ * Implements the Backend interface using Pyodide in a Web Worker */ -import { get } from 'svelte/store'; import type { Backend, BackendState, REPLRequest, REPLResponse, REPLErrorResponse } from '../types'; import { backendState } from '../state'; import { TIMEOUTS } from '$lib/constants/python'; diff --git a/src/lib/pyodide/backend/pyodide/worker.ts b/src/lib/pyodide/backend/pyodide/worker.ts index 81f727ad..f52ac356 100644 --- a/src/lib/pyodide/backend/pyodide/worker.ts +++ b/src/lib/pyodide/backend/pyodide/worker.ts @@ -12,7 +12,7 @@ import { import { PROGRESS_MESSAGES, ERROR_MESSAGES } from '$lib/constants/messages'; import type { REPLRequest, REPLResponse } from '../types'; -import type { PyodideInterface } from 'https://cdn.jsdelivr.net/pyodide/v0.26.2/full/pyodide.mjs'; +import type { PyodideInterface } from "https://cdn.jsdelivr.net/pyodide/v0.26.2/full/pyodide.mjs"; let pyodide: PyodideInterface | null = null; let isInitialized = false; @@ -23,35 +23,36 @@ const streamingCodeQueue: string[] = []; * Send a response to the main thread */ function send(response: REPLResponse): void { - postMessage(response); + postMessage(response); } /** - * Initialize Pyodide and install packages + * Initialize Pyodide and install packages (Needs FLASK Rework) */ async function initialize(): Promise { - if (isInitialized) { - send({ type: 'ready' }); - return; - } - - send({ type: 'progress', value: PROGRESS_MESSAGES.LOADING_PYODIDE }); - - const { loadPyodide } = await import( - /* @vite-ignore */ - PYODIDE_CDN_URL - ); - - pyodide = await loadPyodide(); - if (!pyodide) throw new Error(ERROR_MESSAGES.FAILED_TO_LOAD_PYODIDE); - - // Capture stdout/stderr - pyodide.setStdout({ - batched: (msg: string) => send({ type: 'stdout', value: msg }) - }); - pyodide.setStderr({ - batched: (msg: string) => send({ type: 'stderr', value: msg }) - }); + // Default to pyodide use + if (isInitialized) { + send({ type: "ready" }); + return; + } + + send({ type: "progress", value: PROGRESS_MESSAGES.LOADING_PYODIDE }); + + const { loadPyodide } = await import( + /* @vite-ignore */ + PYODIDE_CDN_URL + ); + + pyodide = await loadPyodide(); + if (!pyodide) throw new Error(ERROR_MESSAGES.FAILED_TO_LOAD_PYODIDE); + + // Capture stdout/stderr + pyodide.setStdout({ + batched: (msg: string) => send({ type: "stdout", value: msg }), + }); + pyodide.setStderr({ + batched: (msg: string) => send({ type: "stderr", value: msg }), + }); send({ type: 'progress', value: PROGRESS_MESSAGES.INSTALLING_DEPS }); await pyodide.loadPackage([...PYODIDE_PRELOAD]); @@ -84,202 +85,214 @@ print(f"${pkg.import} {${pkg.import}.__version__} loaded successfully") } } - // Import numpy as np and gc globally - await pyodide.runPythonAsync(`import numpy as np`); - await pyodide.runPythonAsync(`import gc`); + // Import numpy as np and gc globally + await pyodide.runPythonAsync(`import numpy as np`); + await pyodide.runPythonAsync(`import gc`); - // Capture clean state for later cleanup - await pyodide.runPythonAsync(`_clean_globals = set(globals().keys())`); + // Capture clean state for later cleanup + await pyodide.runPythonAsync(`_clean_globals = set(globals().keys())`); + console.log("Pyodide Globals are: ", pyodide.globals) + console.log("Trying to access numpy, ", pyodide.globals.get("numpy")) - isInitialized = true; - send({ type: 'ready' }); + isInitialized = true; + send({ type: "ready" }); } /** - * Execute Python code (no return value) + * Execute Python code (no return value) (Needs FLASK Rework) */ async function execCode(id: string, code: string): Promise { - if (!pyodide) throw new Error(ERROR_MESSAGES.WORKER_NOT_INITIALIZED); - - try { - await pyodide.runPythonAsync(code); - send({ type: 'ok', id }); - } catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error); - // Try to get traceback - let traceback: string | undefined; - try { - traceback = (await pyodide.runPythonAsync(` + // Default to pyodide use + if (!pyodide) { + throw new Error(ERROR_MESSAGES.WORKER_NOT_INITIALIZED); + } + try { + await pyodide.runPythonAsync(code); + send({ type: "ok", id }); + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + // Try to get traceback + let traceback: string | undefined; + try { + traceback = (await pyodide.runPythonAsync(` import traceback traceback.format_exc() `)) as string; - } catch { - // Ignore traceback extraction errors - } - send({ type: 'error', id, error: errorMsg, traceback }); - } + } catch { + // Ignore traceback extraction errors + } + send({ type: "error", id, error: errorMsg, traceback }); + } + } /** * Evaluate Python expression and return JSON result - * Note: _to_json helper is injected via REPL_SETUP_CODE + * Note: _to_json helper is injected via REPL_SETUP_CODE (Needs FLASK Rework) */ async function evalExpr(id: string, expr: string): Promise { - if (!pyodide) throw new Error(ERROR_MESSAGES.WORKER_NOT_INITIALIZED); + // Default to pyodide use + if (!pyodide) { + throw new Error(ERROR_MESSAGES.WORKER_NOT_INITIALIZED); + } - try { - const result = await pyodide.runPythonAsync(` + try { + const result = await pyodide.runPythonAsync(` _eval_result = ${expr} json.dumps(_eval_result, default=_to_json if '_to_json' in dir() else str) `); - - send({ type: 'value', id, value: result as string }); - } catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error); - let traceback: string | undefined; - try { - traceback = (await pyodide.runPythonAsync(` + send({ type: "value", id, value: result as string }); + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + let traceback: string | undefined; + try { + traceback = (await pyodide.runPythonAsync(` import traceback traceback.format_exc() `)) as string; - } catch { - // Ignore - } - send({ type: 'error', id, error: errorMsg, traceback }); - } + } catch { + // Ignore + } + send({ type: "error", id, error: errorMsg, traceback }); + } } + /** * Run streaming loop - steps generator continuously and posts results - * Runs autonomously until done or stopped + * Runs autonomously until done or stopped (Needs FLASK Rework) */ async function runStreamingLoop(id: string, expr: string): Promise { - if (!pyodide) throw new Error(ERROR_MESSAGES.WORKER_NOT_INITIALIZED); - - streamingActive = true; - // Clear any stale code from previous runs - streamingCodeQueue.length = 0; - - try { - while (streamingActive) { - // Execute any queued code first (for runtime parameter changes, events, etc.) - // Errors in queued code are reported but don't stop the simulation - while (streamingCodeQueue.length > 0) { - const code = streamingCodeQueue.shift()!; - try { - await pyodide.runPythonAsync(code); - } catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error); - send({ type: 'stderr', value: `Stream exec error: ${errorMsg}` }); - } - } - - // Step the generator - const result = await pyodide.runPythonAsync(` + if (!pyodide) { + throw new Error(ERROR_MESSAGES.WORKER_NOT_INITIALIZED); + } + + streamingActive = true; + // Clear any stale code from previous runs + streamingCodeQueue.length = 0; + + try { + while (streamingActive) { + // Execute any queued code first (for runtime parameter changes, events, etc.) + // Errors in queued code are reported but don't stop the simulation + while (streamingCodeQueue.length > 0) { + const code = streamingCodeQueue.shift()!; + try { + await pyodide.runPythonAsync(code); + } catch (error) { + const errorMsg = + error instanceof Error ? error.message : String(error); + send({ type: "stderr", value: `Stream exec error: ${errorMsg}` }); + } + } + + // Step the generator + const result = await pyodide.runPythonAsync(` _eval_result = ${expr} json.dumps(_eval_result, default=_to_json if '_to_json' in dir() else str) `); - // Parse result - const parsed = JSON.parse(result as string); - - // Check if stopped during Python execution - still send final data - if (!streamingActive) { - if (!parsed.done && parsed.result) { - send({ type: 'stream-data', id, value: result as string }); - } - break; - } - - // Check if simulation completed - if (parsed.done) { - break; - } - - // Send result and continue - send({ type: 'stream-data', id, value: result as string }); - } - } catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error); - let traceback: string | undefined; - try { - traceback = (await pyodide.runPythonAsync(` + // Parse result + const parsed = JSON.parse(result as string); + + // Check if stopped during Python execution - still send final data + if (!streamingActive) { + if (!parsed.done && parsed.result) { + send({ type: "stream-data", id, value: result as string }); + } + break; + } + + // Check if simulation completed + if (parsed.done) { + break; + } + + send({ type: "stream-data", id, value: result as string }); + } + } catch (error) { + // Traceback general error catching.... + const errorMsg = error instanceof Error ? error.message : String(error); + let traceback: string | undefined; + try { + traceback = (await pyodide.runPythonAsync(` import traceback traceback.format_exc() `)) as string; - } catch { - // Ignore - } - send({ type: 'error', id, error: errorMsg, traceback }); - } finally { - streamingActive = false; - // Always send done when loop ends (whether completed, stopped, or error) - send({ type: 'stream-done', id }); - } + } catch { + // Ignore + } + send({ type: "error", id, error: errorMsg, traceback }); + } finally { + streamingActive = false; + // Always send done when loop ends (whether completed, stopped, or error) + send({ type: "stream-done", id }); + } } /** * Stop streaming loop */ function stopStreaming(): void { - streamingActive = false; + streamingActive = false; } // Handle messages from main thread self.onmessage = async (event: MessageEvent) => { - const { type } = event.data; - // Extract fields based on request type - const id = 'id' in event.data ? event.data.id : undefined; - const code = 'code' in event.data ? event.data.code : undefined; - const expr = 'expr' in event.data ? event.data.expr : undefined; - - try { - switch (type) { - case 'init': - await initialize(); - break; - - case 'exec': - if (!id || typeof code !== 'string') { - throw new Error('Invalid exec request: missing id or code'); - } - await execCode(id, code); - break; - - case 'eval': - if (!id || typeof expr !== 'string') { - throw new Error('Invalid eval request: missing id or expr'); - } - await evalExpr(id, expr); - break; - - case 'stream-start': - if (!id || typeof expr !== 'string') { - throw new Error('Invalid stream-start request: missing id or expr'); - } - // Don't await - let it run autonomously - runStreamingLoop(id, expr); - break; - - case 'stream-stop': - stopStreaming(); - break; - - case 'stream-exec': - if (typeof code === 'string' && streamingActive) { - // Queue code to be executed between generator steps - streamingCodeQueue.push(code); - } - break; - - default: - throw new Error(`Unknown message type: ${type}`); - } - } catch (error) { - send({ - type: 'error', - id, - error: error instanceof Error ? error.message : String(error) - }); - } + const { type } = event.data; + // Extract fields based on request type + const id = "id" in event.data ? event.data.id : undefined; + const code = "code" in event.data ? event.data.code : undefined; + const expr = "expr" in event.data ? event.data.expr : undefined; + + + try { + switch (type) { + case "init": + await initialize(); + break; + + case "exec": + if (!id || typeof code !== "string") { + throw new Error("Invalid exec request: missing id or code"); + } + await execCode(id, code); + break; + + case "eval": + if (!id || typeof expr !== "string") { + throw new Error("Invalid eval request: missing id or expr"); + } + await evalExpr(id, expr); + break; + + case "stream-start": + if (!id || typeof expr !== "string") { + throw new Error("Invalid stream-start request: missing id or expr"); + } + // Don't await - let it run autonomously + runStreamingLoop(id, expr); + break; + + case "stream-stop": + stopStreaming(); + break; + + case "stream-exec": + if (typeof code === "string" && streamingActive) { + // Queue code to be executed between generator steps + streamingCodeQueue.push(code); + } + break; + + default: + throw new Error(`Unknown message type: ${type}`); + } + } catch (error) { + send({ + type: "error", + id, + error: error instanceof Error ? error.message : String(error), + }); + } }; diff --git a/src/lib/pyodide/backend/registry.ts b/src/lib/pyodide/backend/registry.ts index e1eaaddc..9e40a26f 100644 --- a/src/lib/pyodide/backend/registry.ts +++ b/src/lib/pyodide/backend/registry.ts @@ -5,19 +5,27 @@ import type { Backend } from './types'; import { PyodideBackend } from './pyodide/backend'; +import type { BackendPreference } from '$lib/types'; +import { backendPreferenceStore } from '$lib/stores'; +import { FlaskBackend } from './flask/backend'; +import { page } from '$app/state'; export type BackendType = 'pyodide' | 'local' | 'remote'; let currentBackend: Backend | null = null; -let currentBackendType: BackendType | null = null; +let currentBackendType: BackendPreference | null = null; /** * Get the current backend, creating a Pyodide backend if none exists */ export function getBackend(): Backend { + let currentBackendPreference = page.url.searchParams.get('backend') ?? "pyodide" if (!currentBackend) { - currentBackend = createBackend('pyodide'); - currentBackendType = 'pyodide'; + if(currentBackendPreference == null) currentBackendPreference = "pyodide" + currentBackend = createBackend(currentBackendPreference as BackendPreference); + } + if(getBackendType() !== currentBackendPreference) { + switchBackend(currentBackendPreference as BackendPreference) } return currentBackend; } @@ -25,13 +33,13 @@ export function getBackend(): Backend { /** * Create a backend by type */ -export function createBackend(type: BackendType): Backend { +export function createBackend(type: null | BackendPreference): Backend { switch (type) { case 'pyodide': + case null: return new PyodideBackend(); - case 'local': - case 'remote': - throw new Error(`Backend type '${type}' not yet implemented`); + case 'flask': + return new FlaskBackend() default: throw new Error(`Unknown backend type: ${type}`); } @@ -41,7 +49,7 @@ export function createBackend(type: BackendType): Backend { * Switch to a different backend * Terminates the current backend before creating the new one */ -export function switchBackend(type: BackendType): Backend { +export function switchBackend(type: BackendPreference | null): Backend { if (currentBackend) { currentBackend.terminate(); } @@ -53,7 +61,7 @@ export function switchBackend(type: BackendType): Backend { /** * Get the current backend type */ -export function getBackendType(): BackendType | null { +export function getBackendType(): BackendPreference | null { return currentBackendType; } diff --git a/src/lib/pyodide/bridge.ts b/src/lib/pyodide/bridge.ts index e9defab4..678e68d6 100644 --- a/src/lib/pyodide/bridge.ts +++ b/src/lib/pyodide/bridge.ts @@ -45,6 +45,7 @@ import { STREAMING_STEP_EXPR, STREAMING_STOP_CODE } from './pythonHelpers'; +import type { BackendPreference } from '$lib/types'; // Result types export interface SimulationResult { @@ -528,7 +529,6 @@ export async function validateGraph( // Get validation result const result = await evaluate(VALIDATION_RESULT_EXPR, TIMEOUTS.VALIDATION); - // Clean up await exec(CLEANUP_TEMP_CODE); @@ -580,6 +580,7 @@ export async function stopSimulation(): Promise { if 'sim' in dir() and sim is not None: sim.stop() `, TIMEOUTS.VALIDATION); + consoleStore.info("Stopped simulation (streaming)...") } catch { // Ignore errors - simulation might not exist yet } diff --git a/src/lib/pyodide/pathsimRunner.ts b/src/lib/pyodide/pathsimRunner.ts index b78c1976..339e2681 100644 --- a/src/lib/pyodide/pathsimRunner.ts +++ b/src/lib/pyodide/pathsimRunner.ts @@ -25,6 +25,8 @@ import { generateListDefinition, sanitizeName } from './codeBuilder'; +import type { BackendPreference } from '$lib/types'; +import { backendPreferenceStore } from '$lib/stores'; // Re-export sanitizeName for external use export { sanitizeName } from './codeBuilder'; diff --git a/src/lib/pyodide/pythonHelpers.ts b/src/lib/pyodide/pythonHelpers.ts index 6e2a9842..180a43d4 100644 --- a/src/lib/pyodide/pythonHelpers.ts +++ b/src/lib/pyodide/pythonHelpers.ts @@ -49,7 +49,9 @@ def _extract_scope_data(blocks, node_id_map, incremental=False): if block_name == 'Scope': try: data = block.read(incremental=incremental) - if data is not None: + if type(data) is tuple and data[0] is None and data[1] is None: + pass + elif data is not None: time_arr, signals = data labels = list(block.labels) if hasattr(block, 'labels') and block.labels else [] scope_data[block_id] = { @@ -58,6 +60,8 @@ def _extract_scope_data(blocks, node_id_map, incremental=False): 'labels': labels } except Exception as e: + print("Data: ", data) + print("Block ID: ", block_id) print(f"Error reading Scope: {e}") elif block_name == 'Subsystem': if hasattr(block, 'blocks'): diff --git a/src/lib/stores/backendPreference.ts b/src/lib/stores/backendPreference.ts new file mode 100644 index 00000000..573f85cd --- /dev/null +++ b/src/lib/stores/backendPreference.ts @@ -0,0 +1,63 @@ +/** + * Backend Preference store + * Manages whether the client prefers to run pathview via Pyodide local WebAssembly or with API calls to a Flask backend + * with localStorage persistence + */ + +import { writable } from 'svelte/store'; +import { browser } from '$app/environment'; +import { switchBackend } from '$lib/pyodide'; + +export type BackendPreference = 'pyodide' | 'flask'; + +// Get initial backend preference from localStorage or system preference +function getIntialBackendPreference(): BackendPreference { + if (!browser || typeof window == "undefined") return 'flask'; + + const stored = localStorage.getItem('pathview-backend-preference'); + if (stored === 'pyodide' || stored === 'flask') { + return stored; + } + + return 'pyodide'; +} + +// Create the backend preference store +const backendPreference = writable(getIntialBackendPreference()); + +// Apply backend preference to document and persist +backendPreference.subscribe((value) => { + if (!browser || typeof window == "undefined") return; + + // Persist to localStorage + localStorage.setItem('pathview-backend-preference', value); +}); + +// Theme store with actions +export const backendPreferenceStore = { + subscribe: backendPreference.subscribe, + + /** + * Toggle between a preference for flask or pyodide + */ + toggle(): void { + backendPreference.update((current) => (current === 'pyodide' ? 'flask' : 'pyodide')); + switchBackend(this.get()) + }, + + /** + * Set specific preference + */ + set(newPreference: BackendPreference): void { + backendPreference.set(newPreference); + }, + + /** + * Get current preference value + */ + get(): BackendPreference { + let current: BackendPreference = 'pyodide'; + backendPreference.subscribe((value) => (current = value))(); + return current; + } +}; diff --git a/src/lib/stores/index.ts b/src/lib/stores/index.ts index 54eb9348..4455d2e5 100644 --- a/src/lib/stores/index.ts +++ b/src/lib/stores/index.ts @@ -7,6 +7,7 @@ export { graphStore } from './graph'; export { eventStore } from './events'; export { settingsStore } from './settings'; export { themeStore } from './theme'; +export { backendPreferenceStore } from './backendPreference' export { consoleStore } from './console'; export { codeContextStore } from './codeContext'; diff --git a/src/lib/stores/theme.ts b/src/lib/stores/theme.ts index 095a2ca8..21c2b989 100644 --- a/src/lib/stores/theme.ts +++ b/src/lib/stores/theme.ts @@ -10,8 +10,8 @@ export type Theme = 'light' | 'dark'; // Get initial theme from localStorage or system preference function getInitialTheme(): Theme { - if (!browser) return 'dark'; - + if (!browser || typeof window == "undefined") return 'dark'; + const stored = localStorage.getItem('pathview-theme'); if (stored === 'light' || stored === 'dark') { return stored; @@ -30,7 +30,7 @@ const theme = writable(getInitialTheme()); // Apply theme to document and persist theme.subscribe((value) => { - if (!browser) return; + if (!browser || typeof window == "undefined") return; // Set data-theme attribute on document document.documentElement.setAttribute('data-theme', value); @@ -65,4 +65,4 @@ export const themeStore = { theme.subscribe((value) => (current = value))(); return current; } -}; +}; \ No newline at end of file diff --git a/src/lib/types/index.ts b/src/lib/types/index.ts index d5f3386a..cd2025ad 100644 --- a/src/lib/types/index.ts +++ b/src/lib/types/index.ts @@ -83,3 +83,6 @@ export type { ModelContent } from './component'; export { COMPONENT_EXTENSIONS, COMPONENT_MIME_TYPES, ALL_COMPONENT_EXTENSIONS } from './component'; + +// Backend Preference type +export type BackendPreference = 'pyodide' | 'flask'; diff --git a/src/lib/types/simulation.ts b/src/lib/types/simulation.ts index 8ba121d7..1d134618 100644 --- a/src/lib/types/simulation.ts +++ b/src/lib/types/simulation.ts @@ -57,8 +57,7 @@ export interface SimulationSettings { ftol: string; // Fixed-point iteration tolerance (Python expression) dt_min: string; // Minimum timestep for adaptive solvers (Python expression) dt_max: string; // Maximum timestep for adaptive solvers (Python expression) - ghostTraces: number; // Number of previous runs to show as ghost traces (0-6) - plotResults: boolean; // UI: auto-open plot panel + ghostTraces: number; // Number of previous runs to show as ghost traces } /** Worker message types */ @@ -91,4 +90,4 @@ export interface SimulationState { error: string | null; result: SimulationResult | null; resultHistory: SimulationResult[]; -} +} \ No newline at end of file diff --git a/src/lib/utils/flaskRoutes.ts b/src/lib/utils/flaskRoutes.ts new file mode 100644 index 00000000..656ef6c6 --- /dev/null +++ b/src/lib/utils/flaskRoutes.ts @@ -0,0 +1,6 @@ + + +export function getFlaskBackendUrl() { + // Right now it is this local address, but when we set up the server I'll make it that permanent address + return "http://localhost:8000" +} \ No newline at end of file diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 022c3a6b..665531c1 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,8 +1,13 @@