7 Commits

Author SHA1 Message Date
d12b908014 Merge branch 'develop' into fix/frontpage_predict 2025-11-12 20:54:27 +13:00
2799718951 fix: open and close search modal (#192)
Modal now opens on badge click.
Modal now closes on random click around

Reviewed-on: enviPath/enviPy#192
Co-authored-by: Tobias O <tobias.olenyi@envipath.com>
Co-committed-by: Tobias O <tobias.olenyi@envipath.com>
2025-11-12 20:53:52 +13:00
25c10bbf77 refactor: add tooltip 2025-11-12 16:24:35 +13:00
c1ca734e81 refactor: change input toggle style 2025-11-12 16:22:02 +13:00
e5d2bc596b refactor: move Advanced to right 2025-11-12 16:01:31 +13:00
305fdc41fb [Fix] Replace datetime.now() with Djangos timezone.now() to get rid of NaiveTimestamp warning (#191)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#191
2025-11-12 11:04:00 +13:00
9deca8867e [Feature] Possibility to Retrain Models (#190)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#190
2025-11-12 10:28:35 +13:00
8 changed files with 705 additions and 702 deletions

View File

@ -1,15 +1,15 @@
import csv
import io
import logging
from datetime import datetime
from typing import Any, Callable, List, Optional
from uuid import uuid4
from celery import shared_task
from celery.utils.functional import LRUCache
from django.utils import timezone
from epdb.logic import SPathway
from epdb.models import EPModel, JobLog, Node, Package, Pathway, Rule, Setting, User, Edge
from epdb.models import Edge, EPModel, JobLog, Node, Package, Pathway, Rule, Setting, User
logger = logging.getLogger(__name__)
ML_CACHE = LRUCache(3) # Cache the three most recent ML models to reduce load times.
@ -29,7 +29,7 @@ def dispatch_eager(user: "User", job: Callable, *args, **kwargs):
log.task_id = uuid4()
log.job_name = job.__name__
log.status = "SUCCESS"
log.done_at = datetime.now()
log.done_at = timezone.now()
log.task_result = str(x) if x else None
log.save()

View File

@ -955,6 +955,12 @@ def package_model(request, package_uuid, model_uuid):
]
dispatch(current_user, evaluate_model, current_model.pk, multigen, eval_package_ids)
return redirect(current_model.url)
elif hidden == "retrain":
from .tasks import dispatch, retrain
dispatch(current_user, retrain, current_model.pk)
return redirect(current_model.url)
else:
return HttpResponseBadRequest()

View File

@ -178,6 +178,23 @@
}
});
// Open search modal function
function openSearchModal() {
const searchModal = document.getElementById("search_modal");
if (searchModal) {
searchModal.showModal();
}
}
// Click handler for search badge
const searchTrigger = document.getElementById("search-trigger");
if (searchTrigger) {
searchTrigger.addEventListener("click", function (event) {
event.preventDefault();
openSearchModal();
});
}
// Global keyboard shortcut for search (Cmd+K on Mac, Ctrl+K on Windows/Linux)
document.addEventListener("keydown", function (event) {
// Check if user is typing in an input field
@ -198,7 +215,7 @@
if (isCorrectModifier && event.key === "k") {
event.preventDefault();
search_modal.showModal();
openSearchModal();
}
});
</script>

View File

@ -57,7 +57,7 @@
<div class="navbar-end">
{% if not public_mode %}
<a href="/search" role="button">
<a id="search-trigger" role="button" class="cursor-pointer">
<div
class="flex items-center badge badge-dash space-x-1 bg-base-200 text-base-content/50 p-2 m-1"
>

View File

@ -2,74 +2,86 @@
{% load static %}
{% block main_content %}
<!-- Hero Section with Logo and Search -->
<section class="hero h-fit max-w-5xl w-full shadow-none mx-auto relative">
<section class="hero relative mx-auto h-fit w-full max-w-5xl shadow-none">
<div
class="hero min-h-[calc(100vh*0.4)] bg-gradient-to-br from-primary-800 to-primary-600"
class="hero from-primary-800 to-primary-600 min-h-[calc(100vh*0.4)] bg-gradient-to-br"
style="background-image: url('{% static "/images/hero.png" %}'); background-size: cover; background-position: center;"
>
<div class="hero-overlay"></div>
<!-- Predict Pathway text over the image -->
<div class="absolute bottom-40 left-1/8 -translate-x-8 z-10">
<h2 class="text-3xl text-base-100 text-shadow-lg text-left">
<div class="absolute bottom-40 left-1/8 z-10 -translate-x-8">
<h2 class="text-base-100 text-left text-3xl text-shadow-lg">
Predict Your Pathway
</h2>
</div>
</div>
</section>
<div class="shadow-md max-w-5xl mx-auto bg-base-200">
<div class="bg-base-200 mx-auto max-w-5xl shadow-md">
<!-- Predict Pathway Section -->
<div
class="flex-col lg:flex-row-reverse w-full mx-auto -mt-32 relative z-20 mb-10 "
class="relative z-20 mx-auto -mt-32 mb-10 w-full flex-col lg:flex-row-reverse"
>
<div
class="card bg-base-100 shrink-0 shadow-xl w-3/4 mx-auto transition-all duration-300 ease-in-out"
class="card bg-base-100 mx-auto w-3/4 shrink-0 shadow-xl transition-all duration-300 ease-in-out"
>
<div class="card-body">
<!-- Input Mode Toggle - Fixed position outside fieldset -->
<div class="flex flex-row justify-start items-center h-fit ml-8 my-4">
<div class="flex items-center gap-2">
<!-- <span class="text-sm text-neutral-500">Input Mode:</span> -->
<label class="toggle text-base-content toggle-md">
<div class="my-4 ml-8 flex h-fit flex-row items-center justify-start">
<div class="flex items-center gap-1">
<label class="swap btn btn-ghost btn-sm p-1" title="Input Mode">
<input type="checkbox" />
<svg
aria-label="smiles mode"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
class="size-5"
>
<g
stroke-linejoin="round"
stroke-linecap="round"
stroke-width="2"
fill="currentColor"
stroke="none"
<span class="swap-on flex items-center gap-1">
<div
class="bg-neutral/50 text-neutral-content flex items-center justify-center rounded-full p-1"
>
<path
fill-rule="evenodd"
d="M8 2.75A.75.75 0 0 1 8.75 2h7.5a.75.75 0 0 1 0 1.5h-3.215l-4.483 13h2.698a.75.75 0 0 1 0 1.5h-7.5a.75.75 0 0 1 0-1.5h3.215l4.483-13H8.75A.75.75 0 0 1 8 2.75Z"
clip-rule="evenodd"
/>
</g>
</svg>
<svg
aria-label="draw mode"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
stroke="none"
class="size-5"
>
<path
d="m2.695 14.762-1.262 3.155a.5.5 0 0 0 .65.65l3.155-1.262a4 4 0 0 0 1.343-.886L17.5 5.501a2.121 2.121 0 0 0-3-3L3.58 13.419a4 4 0 0 0-.885 1.343Z"
/>
</svg>
<svg
aria-label="smiles mode"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
class="size-5"
>
<g
stroke-linejoin="round"
stroke-linecap="round"
stroke-width="2"
fill="currentColor"
stroke="none"
>
<path
fill-rule="evenodd"
d="M8 2.75A.75.75 0 0 1 8.75 2h7.5a.75.75 0 0 1 0 1.5h-3.215l-4.483 13h2.698a.75.75 0 0 1 0 1.5h-7.5a.75.75 0 0 1 0-1.5h3.215l4.483-13H8.75A.75.75 0 0 1 8 2.75Z"
clip-rule="evenodd"
/>
</g>
</svg>
</div>
<span class="ext-xs">SMILES</span>
</span>
<span class="swap-off flex items-center gap-1">
<div
class="bg-neutral/50 text-neutral-content flex items-center justify-center rounded-full p-1"
>
<svg
aria-label="draw mode"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
stroke="none"
class="size-5"
>
<path
d="m2.695 14.762-1.262 3.155a.5.5 0 0 0 .65.65l3.155-1.262a4 4 0 0 0 1.343-.886L17.5 5.501a2.121 2.121 0 0 0-3-3L3.58 13.419a4 4 0 0 0-.885 1.343Z"
/>
</svg>
</div>
<span class="text-base/50 text-xs">Draw</span>
</span>
</label>
</div>
</div>
<fieldset
class="fieldset transition-all duration-300 ease-in-out overflow-hidden"
class="fieldset overflow-hidden transition-all duration-300 ease-in-out"
>
<form
id="index-form"
@ -79,29 +91,29 @@
{% csrf_token %}
<div
id="text-input-container"
class="transition-all duration-300 ease-in-out opacity-100 transform scale-100"
class="scale-100 transform opacity-100 transition-all duration-300 ease-in-out"
>
<div class="join w-full mx-auto">
<div class="join mx-auto w-full">
<input
type="text"
id="index-form-text-input"
placeholder="canonical SMILES string"
class="input grow input-md join-item"
class="input input-md join-item grow"
/>
<button class="btn btn-neutral join-item">Predict!</button>
</div>
<div class="label relative w-full mt-1">
<div class="label relative mt-1 w-full">
<div class="flex gap-2">
<a
href="#"
class="example-link cursor-pointer hover:text-primary"
class="example-link hover:text-primary cursor-pointer"
data-smiles="CN1C=NC2=C1C(=O)N(C(=O)N2C)C"
title="load example"
>Caffeine</a
>
<a
href="#"
class="example-link cursor-pointer hover:text-primary"
class="example-link hover:text-primary cursor-pointer"
data-smiles="CC(C)CC1=CC=C(C=C1)C(C)C(=O)O"
title="load example"
>Ibuprofen</a
@ -114,7 +126,7 @@
</div>
<div
id="ketcher-container"
class="hidden w-full transition-all duration-300 ease-in-out opacity-0 transform scale-95"
class="hidden w-full scale-95 transform opacity-0 transition-all duration-300 ease-in-out"
>
<iframe
id="index-ketcher"
@ -124,11 +136,13 @@
class="rounded-lg"
></iframe>
<button
class="btn btn-lg bg-primary-950 text-primary-50 join-item w-full mt-2"
class="btn btn-lg bg-primary-950 text-primary-50 join-item mt-2 w-full"
>
Predict!
</button>
<a class="label mx-auto w-full mt-1" href="#">Advanced</a>
<div class="mt-1 flex w-full justify-end">
<a class="label justify-end" href="/predict">Advanced</a>
</div>
</div>
<input
type="hidden"
@ -150,18 +164,18 @@
</div>
<!-- Community News Section -->
<section class="py-16 bg-base-200 z-10 mx-8">
<div class="max-w-7xl mx-auto px-4">
<h2 class="h2 font-bold text-left mb-8">Community Updates</h2>
<section class="bg-base-200 z-10 mx-8 py-16">
<div class="mx-auto max-w-7xl px-4">
<h2 class="h2 mb-8 text-left font-bold">Community Updates</h2>
<div id="community-news-container" class="flex gap-4 justify-center">
<div id="community-news-container" class="flex justify-center gap-4">
<!-- News cards will be populated here -->
<div id="loading" class="flex justify-center w-full">
<div id="loading" class="flex w-full justify-center">
<span class="loading loading-spinner loading-lg"></span>
</div>
</div>
<div class="text-right mt-6">
<div class="mt-6 text-right">
<a
href="https://community.envipath.org/c/announcements/10"
target="_blank"
@ -177,18 +191,18 @@
</section>
<!-- Mission Statement Section -->
<section class="py-16 from-base-200 to-base-100 bg-gradient-to-b">
<section class="from-base-200 to-base-100 bg-gradient-to-b py-16">
<div class="mx-auto px-8 md:px-12">
<div class="flex flex-row gap-4">
<div class="w-1/3">
<img
src="{% static "/images/ep-rule-artwork.png" %}"
alt="rule-based iterative tree greneration"
class="w-full h-full object-contain"
class="h-full w-full object-contain"
/>
</div>
<div class="space-y-4 text-left w-2/3 mr-8">
<h2 class="h2 font-bold mb-8">About enviPath</h2>
<div class="mr-8 w-2/3 space-y-4 text-left">
<h2 class="h2 mb-8 font-bold">About enviPath</h2>
<p class="">
enviPath is a database and prediction system for the microbial
biotransformation of organic environmental contaminants. The
@ -201,7 +215,7 @@
products. Explore our tools and contribute to advancing
environmental biotransformation research.
</p>
<div class="flex flex-row gap-4 float-right">
<div class="float-right flex flex-row gap-4">
<a href="/about" class="btn btn-ghost-neutral">Read More</a>
<a href="/about" class="btn btn-neutral">Publications</a>
</div>
@ -211,7 +225,7 @@
</section>
<!-- Partners Section -->
<section class="py-14 sm:py-12 bg-base-100">
<section class="bg-base-100 py-14 sm:py-12">
<div class="mx-auto px-6 lg:px-8">
<div class="divider">
<h2 class="text-center text-lg/8 font-semibold">Backed by Science</h2>
@ -222,12 +236,12 @@
<img
src="{% static "/images/uoa-logo-small.png" %}"
alt="The University of Auckland"
class=" max-h-20 w-full object-contain lg:col-span-1"
class="max-h-20 w-full object-contain lg:col-span-1"
/>
<img
src="{% static "/images/logo-eawag.svg" %}"
alt="Eawag"
class=" max-h-12 w-full object-contain lg:col-span-1"
class="max-h-12 w-full object-contain lg:col-span-1"
/>
<img
src="{% static "/images/uzh-logo.svg" %}"

View File

@ -1,43 +1,55 @@
<div class="modal fade" tabindex="-1" id="retrain_model_modal" role="dialog" aria-labelledby="retrain_model_modal"
aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">
<span aria-hidden="true">&times;</span>
<span class="sr-only">Close</span>
</button>
<h4 class="modal-title">Retrain Model</h4>
</div>
<div class="modal-body">
<form id="retrain_model_form" accept-charset="UTF-8" action="{{ meta.current_package.url }}/model"
data-remote="true" method="post">
<div class="jumbotron">
To reflect changes in the rule or data packages, you can use the "Retrain" button,
to let the model reflect the changes without creating a new model.
While the model is retraining, it will be unavailable for prediction.
</div>
{% csrf_token %}
<input type="hidden" name="action" value="retrain">
</form>
</div>
<div class="modal-footer">
<a id="retrain_model_form_submit" class="btn btn-primary" href="#">Retrain</a>
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
</div>
</div>
<div
class="modal fade"
tabindex="-1"
id="retrain_model_modal"
role="dialog"
aria-labelledby="retrain_model_modal"
aria-hidden="true"
>
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">
<span aria-hidden="true">&times;</span>
<span class="sr-only">Close</span>
</button>
<h4 class="modal-title">Retrain Model</h4>
</div>
<div class="modal-body">
<form
id="retrain_model_form"
accept-charset="UTF-8"
action="{{ meta.current_object.url }}"
data-remote="true"
method="post"
>
<div class="jumbotron">
To reflect changes in the rule or data packages, you can use the
"Retrain" button, to let the model reflect the changes without
creating a new model. While the model is retraining, it will be
unavailable for prediction.
</div>
{% csrf_token %}
<input type="hidden" name="hidden" value="retrain" />
</form>
</div>
<div class="modal-footer">
<a id="retrain_model_form_submit" class="btn btn-primary" href="#"
>Retrain</a
>
<button type="button" class="btn btn-default" data-dismiss="modal">
Cancel
</button>
</div>
</div>
</div>
</div>
<script>
$(function () {
$('#retrain_model_form_submit').on('click', function (e) {
e.preventDefault();
$('#retrain_model_form').submit();
});
$(function () {
$("#retrain_model_form_submit").on("click", function (e) {
e.preventDefault();
$("#retrain_model_form").submit();
});
});
</script>

File diff suppressed because it is too large Load Diff

View File

@ -1,197 +0,0 @@
{% extends "framework.html" %}
{% load static %}
{% block content %}
<div id=searchContent>
<div id="packSelector">
<label>Select Packages</label><br>
<select id="selPackages" name="selPackages" data-actions-box='true' class="selPackages" multiple
data-width='100%'>
{% if unreviewed_objects %}
<option disabled>Reviewed Packages</option>
{% endif %}
{% for obj in reviewed_objects %}
<option value="{{ obj.url }}" selected>{{ obj.name|safe }}</option>
{% endfor %}
{% if unreviewed_objects %}
<option disabled>Unreviewed Packages</option>
{% endif %}
{% for obj in unreviewed_objects %}
<option value="{{ obj.url }}">{{ obj.name|safe }}</option>
{% endfor %}
</select>
</div>
<p></p>
<div>
<label>Search Term</label><br>
<div class="input-group" id="index-form-bar">
<input type="text" class="form-control" id='searchbar' placeholder="Benfuracarb">
<div class="input-group-btn">
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown"
id="mode-button"
aria-haspopup="true" aria-expanded="false">Text <span class="caret"></span></button>
<ul class="dropdown-menu">
<li class="dropdown-header">Text</li>
<li><a id="dropdown-predict-text-text">Text</a></li>
<li class="dropdown-header">SMILES</li>
<li><a id="dropdown-search-smiles-default" data-toggle="tooltip">Default</a></li>
<li><a id="dropdown-search-smiles-canonical">Canonical</a></li>
<li><a id="dropdown-search-smiles-exact">Exact</a></li>
<li class="dropdown-header">InChI</li>
<li><a id="dropdown-search-inchi-inchikey">InChIKey</a></li>
</ul>
<button class="btn" style="background-color:#222222;color:#9d9d9d" type="button" id="search-button">
Go!
</button>
</div>
</div>
<p></p>
<div id="results"></div>
<p></p>
<div id="loading"></div>
</div>
</div>
<script>
function modeDropdownClicked() {
var suffix = ' <span class="caret"></span>';
var dropdownVal = $(this).text();
$('#mode-button').html(dropdownVal + suffix);
}
function handleSearchResponse(id, data) {
content = `
<div class='panel-group' id='search-accordion'>
<div class='panel panel-default'>
<div class='panel-heading' id='headingPanel' style='font-size:2rem;height: 46px'>
Results
</div>
<div id='descDiv'></div>
</div>`;
function makeContent(objs) {
links = "";
for (idx in objs) {
obj = objs[idx];
links += `<a class='list-group-item' href='${obj.url}'>${obj.name}</a>`
}
return links;
}
allEmpty = true;
for (key in data) {
if (key === 'searchterm') {
continue;
}
if (data[key].length < 1) {
continue;
}
allEmpty = false;
content += `
<div class='panel panel-default panel-heading list-group-item' style='background-color:silver'>
<h4 class='panel-title'>
<a id='${key}_link' data-toggle='collapse' data-parent='#search-accordion' href='#${key}_panel'>
${key}
</a>
</h4>
</div>
<div id='${key}_panel' class='panel-collapse collapse in'>
<div class='panel-body list-group-item'>
${makeContent(data[key])}
</div>
</div>
`;
}
if (allEmpty) {
$('#' + id).append('<div class="alert alert-danger" role="alert"><p>' + "No results..." + '</p></div>');
} else {
$('#' + id).append(content);
}
}
function search(e) {
e.preventDefault();
query = $("#searchbar").val()
if (!query) {
// Nothing to search...
console.log("Search phrase empty, won't do search")
return;
}
var selPacks = [];
$("#selPackages :selected").each(function () {
var id = this.value;
selPacks.push(id);
});
if (selPacks.length < 1) {
console.log("No package selected, won't do search")
return;
}
var mode = $('#mode-button').text().trim().toLowerCase();
var par = {};
par['packages'] = selPacks;
par['search'] = query;
par['mode'] = mode;
console.log(par);
var queryString = $.param(par, true);
makeLoadingGif("#loading", "{% static '/images/wait.gif' %}");
$("#results").empty();
$.getJSON("{{ SERVER_BASE }}/search?" + queryString, function (result) {
handleSearchResponse("results", result);
$("#loading").empty();
}).fail(function (d) {
$("#loading").empty();
console.log(d.responseText);
handleError(JSON.parse(d.responseText));
});
}
$(function () {
tooltips = {
'dropdown-predict-text-text': 'The inserted pattern will be searched on all enviPath object names and descriptions',
'dropdown-search-smiles-default': 'Search by SMILES, stereochemistry and charge are ignored',
'dropdown-search-smiles-canonical': 'Search by SMILES, stereochemistry is ignored but charge is preserved',
'dropdown-search-smiles-exact': 'Search by SMILES, exact match for stereochemistry and charge',
'dropdown-search-inchi-inchikey': 'Search by InChIKey',
}
Object.keys(tooltips).forEach(key => {
$('#' + key).tooltip({
placement: "top",
title: tooltips[key]
});
$('#' + key).on('click', modeDropdownClicked);
});
$("#selPackages").selectpicker();
$("#search-button").on("click", search);
$("#searchbar").on("keydown", function (e) {
if (e.key === "Enter") {
e.preventDefault();
search(e);
}
});
});
{% if search_result %}
$('#searchbar').val('{{ search_result.searchterm }}')
handleSearchResponse("results", {{ search_result|safe }});
{% endif %}
</script>
{% endblock content %}