Skip to content

Commit

Permalink
Merge pull request #432 from gyorilab/llm_ui
Browse files Browse the repository at this point in the history
Improve ODE extraction UI
  • Loading branch information
bgyori authored Feb 5, 2025
2 parents ca70f67 + 51e1fa7 commit 189a8f4
Show file tree
Hide file tree
Showing 4 changed files with 119 additions and 10 deletions.
1 change: 1 addition & 0 deletions mira/sources/sympy_ode/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
2. Have the openai Python package installed (pip install openai).
3. Run with `python -m mira.sources.sympy_ode.app`. Optionally, pass in `debug`
as an argument to run in debug mode (will reload the server on changes).
4. Go to http://localhost:<port>/llm in your browser to use the LLM UI.
"""
import os
from flask import Flask
Expand Down
32 changes: 26 additions & 6 deletions mira/sources/sympy_ode/llm_ui.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
import logging

from flask import Blueprint, render_template, request
from sympy import latex

from mira.modeling import Model
from mira.modeling.ode import OdeModel
from mira.modeling.amr.petrinet import AMRPetriNetModel

from .llm_util import execute_template_model_from_sympy_odes, image_to_odes_str
from .llm_util import (
execute_template_model_from_sympy_odes,
image_to_odes_str,
CodeExecutionError,
)
from .proxies import openai_client


Expand All @@ -15,6 +21,9 @@
llm_ui_blueprint.template_folder = "templates"


logger = logging.getLogger(__name__)


@llm_ui_blueprint.route("/", methods=["GET", "POST"])
def upload_image():
result_text = None
Expand Down Expand Up @@ -45,11 +54,22 @@ def upload_image():

# User submitted a result_text for processing
elif result_text:
template_model = execute_template_model_from_sympy_odes(
result_text,
attempt_grounding=True,
client=openai_client
)
try:
template_model = execute_template_model_from_sympy_odes(
result_text,
attempt_grounding=True,
client=openai_client
)
except CodeExecutionError as e:
# If there is an error executing the code, return the error message
# and the result_text so that the user can see the error and correct
# any mistakes in the input
logger.exception(e)
return render_template(
"index.html",
result_text=result_text,
error=str(e)
)
# Get the OdeModel
om = OdeModel(model=Model(template_model=template_model), initialized=False)
ode_system = om.get_interpretable_kinetics()
Expand Down
10 changes: 9 additions & 1 deletion mira/sources/sympy_ode/llm_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@
pattern = re.compile(ode_pattern, re.DOTALL)


class CodeExecutionError(Exception):
"""An error raised when there is an error executing the code"""


def image_file_to_odes_str(
image_path: str,
client: OpenAIClient,
Expand Down Expand Up @@ -199,7 +203,11 @@ def execute_template_model_from_sympy_odes(
odes: List[sympy.Eq] = None
# Execute the code and expose the `odes` variable to the local scope
local_dict = locals()
exec(ode_str, globals(), local_dict)
try:
exec(ode_str, globals(), local_dict)
except Exception as e:
# Raise a CodeExecutionError to handle the error in the UI
raise CodeExecutionError(f"Error while executing the code: {e}")
# `odes` should now be defined in the local scope
odes = local_dict.get("odes")
assert odes is not None, "The code should define a variable called `odes`"
Expand Down
86 changes: 83 additions & 3 deletions mira/sources/sympy_ode/templates/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,14 @@ <h1 class="text-center">Equation image to MIRA model</h1>
<div class="card mt-4">
<div class="card-body">
<form method="post" enctype="multipart/form-data">
<div class="mb-3">
<label for="file" class="form-label">Select an Image File</label>
<input class="form-control" type="file" id="file" name="file" accept="image/*" required>
<div id="drop-zone" tabindex="0" class="mb-3 border border-primary p-4 text-center" style="cursor: pointer;">
<p id="drop-zone-message">
Click here to focus, then drag & drop, click to select, or press <strong>Ctrl+V</strong> to paste an image.
</p>
<!-- Preview image element: initially hidden -->
<img id="preview-image" src="" alt="Preview" style="max-width: 100%; display: none; margin-top: 10px;">
<!-- Hidden file input -->
<input class="d-none" type="file" id="file" name="file" accept="image/*">
</div>
<button type="submit" class="btn btn-primary">Upload and Convert</button>
</form>
Expand Down Expand Up @@ -178,6 +183,81 @@ <h5>Output Model</h5>
{% endif %}
</div>


<script>
document.addEventListener("DOMContentLoaded", function() {
const dropZone = document.getElementById("drop-zone");
const fileInput = document.getElementById("file");
const previewImage = document.getElementById("preview-image");
const messageEl = document.getElementById("drop-zone-message");

// Utility: Show preview for a given file
function showPreview(file) {
const url = URL.createObjectURL(file);
previewImage.src = url;
previewImage.style.display = "block";
messageEl.textContent = "Image loaded. You can submit the form or paste/drop another image.";
}

// When the drop zone is clicked, trigger the file dialog and focus the zone.
dropZone.addEventListener("click", function() {
fileInput.click();
dropZone.focus(); // ensure the drop zone has focus for paste events
});

// Optional: add visual feedback when the drop zone is focused.
dropZone.addEventListener("focus", function() {
dropZone.classList.add("border-success");
});
dropZone.addEventListener("blur", function() {
dropZone.classList.remove("border-success");
});

// Handle drag & drop events.
dropZone.addEventListener("dragover", function(e) {
e.preventDefault();
dropZone.classList.add("bg-light");
});
dropZone.addEventListener("dragleave", function(e) {
e.preventDefault();
dropZone.classList.remove("bg-light");
});
dropZone.addEventListener("drop", function(e) {
e.preventDefault();
dropZone.classList.remove("bg-light");
if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
fileInput.files = e.dataTransfer.files;
showPreview(e.dataTransfer.files[0]);
}
});

// Handle paste events on the drop zone.
dropZone.addEventListener("paste", function(e) {
const clipboardItems = e.clipboardData.items;
for (let i = 0; i < clipboardItems.length; i++) {
const item = clipboardItems[i];
if (item.type.indexOf("image") !== -1) {
const blob = item.getAsFile();
const dataTransfer = new DataTransfer();
dataTransfer.items.add(blob);
fileInput.files = dataTransfer.files;
showPreview(blob);
// Only process the first image.
break;
}
}
});

// Also handle changes from the file dialog.
fileInput.addEventListener("change", function(e) {
if (fileInput.files && fileInput.files.length > 0) {
showPreview(fileInput.files[0]);
}
});
});
</script>


<!-- Bootstrap JS -->
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
</body>
Expand Down

0 comments on commit 189a8f4

Please sign in to comment.