6 Commits

Author SHA1 Message Date
2e95837116 Merge branch 'develop' into feat/package_pred 2025-11-12 21:42:11 +13:00
debbef8158 [Enhancement] Cleanup Landing Page Form (#194)
I changed the toggle style to be more self evident.
Do you think this is enough, or should I add an (ugly) label?

![image.png](/attachments/0e4ce043-7544-4852-9db9-460517b36d64)

Co-authored-by: jebus <lorsbach@envipath.com>
Reviewed-on: enviPath/enviPy#194
Co-authored-by: Tobias O <tobias.olenyi@envipath.com>
Co-committed-by: Tobias O <tobias.olenyi@envipath.com>
2025-11-12 21:42:02 +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
d9e4660fd4 feat: add per-package pathway prediction 2025-11-12 18:05:44 +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
11 changed files with 740 additions and 707 deletions

View File

@ -1,15 +1,15 @@
import csv import csv
import io import io
import logging import logging
from datetime import datetime
from typing import Any, Callable, List, Optional from typing import Any, Callable, List, Optional
from uuid import uuid4 from uuid import uuid4
from celery import shared_task from celery import shared_task
from celery.utils.functional import LRUCache from celery.utils.functional import LRUCache
from django.utils import timezone
from epdb.logic import SPathway 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__) logger = logging.getLogger(__name__)
ML_CACHE = LRUCache(3) # Cache the three most recent ML models to reduce load times. 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.task_id = uuid4()
log.job_name = job.__name__ log.job_name = job.__name__
log.status = "SUCCESS" log.status = "SUCCESS"
log.done_at = datetime.now() log.done_at = timezone.now()
log.task_result = str(x) if x else None log.task_result = str(x) if x else None
log.save() log.save()

View File

@ -142,6 +142,11 @@ urlpatterns = [
v.package_pathway, v.package_pathway,
name="package pathway detail", name="package pathway detail",
), ),
re_path(
rf"^package/(?P<package_uuid>{UUID})/predict$",
v.package_predict_pathway,
name="package predict pathway",
),
# Pathway Nodes # Pathway Nodes
re_path( re_path(
rf"^package/(?P<package_uuid>{UUID})/pathway/(?P<pathway_uuid>{UUID})/node$", rf"^package/(?P<package_uuid>{UUID})/pathway/(?P<pathway_uuid>{UUID})/node$",

View File

@ -374,6 +374,22 @@ def predict_pathway(request):
return render(request, "predict_pathway.html", context) return render(request, "predict_pathway.html", context)
@package_permission_required()
def package_predict_pathway(request, package_uuid):
"""Package-specific predict pathway view."""
if request.method != "GET":
return HttpResponseNotAllowed(["GET"])
current_user = _anonymous_or_real(request)
current_package = PackageManager.get_package_by_id(current_user, package_uuid)
context = get_base_context(request)
context["title"] = f"enviPath - {current_package.name} - Predict Pathway"
context["meta"]["current_package"] = current_package
return render(request, "predict_pathway.html", context)
def packages(request): def packages(request):
current_user = _anonymous_or_real(request) current_user = _anonymous_or_real(request)
@ -955,6 +971,12 @@ def package_model(request, package_uuid, model_uuid):
] ]
dispatch(current_user, evaluate_model, current_model.pk, multigen, eval_package_ids) 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) return redirect(current_model.url)
else: else:
return HttpResponseBadRequest() return HttpResponseBadRequest()

View File

@ -1,6 +1,9 @@
{% if meta.can_edit %} {% if meta.can_edit %}
<li> <li>
<a href="{{ meta.server_url }}/predict"> <a
<span class="glyphicon glyphicon-plus"></span> New Pathway</a> href="{% if meta.current_package %}{{ meta.current_package.url }}/predict{% else %}{{ meta.server_url }}/predict{% endif %}"
>
<span class="glyphicon glyphicon-plus"></span> New Pathway</a
>
</li> </li>
{% endif %} {% endif %}

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) // Global keyboard shortcut for search (Cmd+K on Mac, Ctrl+K on Windows/Linux)
document.addEventListener("keydown", function (event) { document.addEventListener("keydown", function (event) {
// Check if user is typing in an input field // Check if user is typing in an input field
@ -198,7 +215,7 @@
if (isCorrectModifier && event.key === "k") { if (isCorrectModifier && event.key === "k") {
event.preventDefault(); event.preventDefault();
search_modal.showModal(); openSearchModal();
} }
}); });
</script> </script>

View File

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

View File

@ -2,36 +2,38 @@
{% load static %} {% load static %}
{% block main_content %} {% block main_content %}
<!-- Hero Section with Logo and Search --> <!-- 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 <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;" style="background-image: url('{% static "/images/hero.png" %}'); background-size: cover; background-position: center;"
> >
<div class="hero-overlay"></div> <div class="hero-overlay"></div>
<!-- Predict Pathway text over the image --> <!-- Predict Pathway text over the image -->
<div class="absolute bottom-40 left-1/8 -translate-x-8 z-10"> <div class="absolute bottom-40 left-1/8 z-10 -translate-x-8">
<h2 class="text-3xl text-base-100 text-shadow-lg text-left"> <h2 class="text-base-100 text-left text-3xl text-shadow-lg">
Predict Your Pathway Predict Your Pathway
</h2> </h2>
</div> </div>
</div> </div>
</section> </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 --> <!-- Predict Pathway Section -->
<div <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 <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"> <div class="card-body">
<!-- Input Mode Toggle - Fixed position outside fieldset --> <div class="my-4 ml-8 flex h-fit flex-row items-center justify-start">
<div class="flex flex-row justify-start items-center h-fit ml-8 my-4"> <div class="flex items-center gap-1">
<div class="flex items-center gap-2"> <label class="swap btn btn-ghost btn-sm p-1" title="Input Mode">
<!-- <span class="text-sm text-neutral-500">Input Mode:</span> -->
<label class="toggle text-base-content toggle-md">
<input type="checkbox" /> <input type="checkbox" />
<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"
>
<svg <svg
aria-label="smiles mode" aria-label="smiles mode"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@ -52,6 +54,13 @@
/> />
</g> </g>
</svg> </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 <svg
aria-label="draw mode" aria-label="draw mode"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@ -64,12 +73,15 @@
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" 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>
</div>
<span class="text-base/50 text-xs">Draw</span>
</span>
</label> </label>
</div> </div>
</div> </div>
<fieldset <fieldset
class="fieldset transition-all duration-300 ease-in-out overflow-hidden" class="fieldset overflow-hidden transition-all duration-300 ease-in-out"
> >
<form <form
id="index-form" id="index-form"
@ -79,29 +91,29 @@
{% csrf_token %} {% csrf_token %}
<div <div
id="text-input-container" 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 <input
type="text" type="text"
id="index-form-text-input" id="index-form-text-input"
placeholder="canonical SMILES string" 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> <button class="btn btn-neutral join-item">Predict!</button>
</div> </div>
<div class="label relative w-full mt-1"> <div class="label relative mt-1 w-full">
<div class="flex gap-2"> <div class="flex gap-2">
<a <a
href="#" 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" data-smiles="CN1C=NC2=C1C(=O)N(C(=O)N2C)C"
title="load example" title="load example"
>Caffeine</a >Caffeine</a
> >
<a <a
href="#" 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" data-smiles="CC(C)CC1=CC=C(C=C1)C(C)C(=O)O"
title="load example" title="load example"
>Ibuprofen</a >Ibuprofen</a
@ -114,7 +126,7 @@
</div> </div>
<div <div
id="ketcher-container" 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 <iframe
id="index-ketcher" id="index-ketcher"
@ -124,11 +136,13 @@
class="rounded-lg" class="rounded-lg"
></iframe> ></iframe>
<button <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! Predict!
</button> </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> </div>
<input <input
type="hidden" type="hidden"
@ -150,18 +164,18 @@
</div> </div>
<!-- Community News Section --> <!-- Community News Section -->
<section class="py-16 bg-base-200 z-10 mx-8"> <section class="bg-base-200 z-10 mx-8 py-16">
<div class="max-w-7xl mx-auto px-4"> <div class="mx-auto max-w-7xl px-4">
<h2 class="h2 font-bold text-left mb-8">Community Updates</h2> <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 --> <!-- 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> <span class="loading loading-spinner loading-lg"></span>
</div> </div>
</div> </div>
<div class="text-right mt-6"> <div class="mt-6 text-right">
<a <a
href="https://community.envipath.org/c/announcements/10" href="https://community.envipath.org/c/announcements/10"
target="_blank" target="_blank"
@ -177,18 +191,18 @@
</section> </section>
<!-- Mission Statement 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="mx-auto px-8 md:px-12">
<div class="flex flex-row gap-4"> <div class="flex flex-row gap-4">
<div class="w-1/3"> <div class="w-1/3">
<img <img
src="{% static "/images/ep-rule-artwork.png" %}" src="{% static "/images/ep-rule-artwork.png" %}"
alt="rule-based iterative tree greneration" alt="rule-based iterative tree greneration"
class="w-full h-full object-contain" class="h-full w-full object-contain"
/> />
</div> </div>
<div class="space-y-4 text-left w-2/3 mr-8"> <div class="mr-8 w-2/3 space-y-4 text-left">
<h2 class="h2 font-bold mb-8">About enviPath</h2> <h2 class="h2 mb-8 font-bold">About enviPath</h2>
<p class=""> <p class="">
enviPath is a database and prediction system for the microbial enviPath is a database and prediction system for the microbial
biotransformation of organic environmental contaminants. The biotransformation of organic environmental contaminants. The
@ -201,7 +215,7 @@
products. Explore our tools and contribute to advancing products. Explore our tools and contribute to advancing
environmental biotransformation research. environmental biotransformation research.
</p> </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-ghost-neutral">Read More</a>
<a href="/about" class="btn btn-neutral">Publications</a> <a href="/about" class="btn btn-neutral">Publications</a>
</div> </div>
@ -211,7 +225,7 @@
</section> </section>
<!-- Partners 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="mx-auto px-6 lg:px-8">
<div class="divider"> <div class="divider">
<h2 class="text-center text-lg/8 font-semibold">Backed by Science</h2> <h2 class="text-center text-lg/8 font-semibold">Backed by Science</h2>
@ -222,12 +236,12 @@
<img <img
src="{% static "/images/uoa-logo-small.png" %}" src="{% static "/images/uoa-logo-small.png" %}"
alt="The University of Auckland" 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 <img
src="{% static "/images/logo-eawag.svg" %}" src="{% static "/images/logo-eawag.svg" %}"
alt="Eawag" 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 <img
src="{% static "/images/uzh-logo.svg" %}" src="{% static "/images/uzh-logo.svg" %}"

View File

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

View File

@ -1,105 +1,184 @@
{% load static %} {% load static %}
<dialog id="search_modal" class="modal @max-sm:modal-top justify-center"> <dialog id="search_modal" class="modal @max-sm:modal-top justify-center">
<div class="modal-box w-lvw sm:w-[85vw] sm:max-w-5xl h-full sm:h-8/12 p-1" > <div class="modal-box h-full w-lvw p-1 sm:h-8/12 sm:w-[85vw] sm:max-w-5xl">
<!-- Search Input and Mode Selector --> <!-- Search Input and Mode Selector -->
<div class="form-control mb-4 flex-shrink-0 w-full"> <div class="form-control mb-4 w-full shrink-0">
<div class="join w-full m-0 p-3 items-center"> <div class="join m-0 w-full items-center p-3">
<label class="input join-item input-ghost grow"> <label class="input join-item input-ghost grow">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-search-icon lucide-search"><path d="m21 21-4.34-4.34"/><circle cx="11" cy="11" r="8"/></svg> <svg
<input type="text" autofocus xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-search-icon lucide-search"
>
<path d="m21 21-4.34-4.34" />
<circle cx="11" cy="11" r="8" />
</svg>
<input
type="text"
autofocus
id="modal_searchbar" id="modal_searchbar"
placeholder="Benfuracarb" placeholder="Benfuracarb"
class="grow" aria-label="Search" /> class="grow"
aria-label="Search"
/>
</label> </label>
<!-- Mode Dropdown --> <!-- Mode Dropdown -->
<div> <div>
<button type="button" <button
type="button"
tabindex="0" tabindex="0"
id="modal_mode_button" id="modal_mode_button"
popovertarget="search_dropdown_menu" style="anchor-name:--anchor-1" popovertarget="search_dropdown_menu"
class="btn join-item btn-ghost"> style="anchor-name:--1"
class="btn join-item btn-ghost"
>
Text Text
<svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/> class="ml-1 h-4 w-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M19 9l-7 7-7-7"
/>
</svg> </svg>
</button> </button>
<ul tabindex="0"" class="dropdown dropdown-end menu bg-base-200 rounded-box z-[100] w-64 p-2 shadow-lg" popover id="search_dropdown_menu" style="position-anchor:--anchor-1"> <ul
tabindex="0"
class="dropdown dropdown-end menu bg-base-200 rounded-box w-64 p-2 shadow-lg"
popover
id="search_dropdown_menu"
style="position-anchor:--anchor-2"
>
<li class="menu-title">Text</li> <li class="menu-title">Text</li>
<li> <li>
<a id="modal_dropdown_text" <a
id="modal_dropdown_text"
class="tooltip tooltip-left" class="tooltip tooltip-left"
data-tip="Search on object names and descriptions"> data-tip="Search on object names and descriptions"
>
Text Text
</a> </a>
</li> </li>
<li class="menu-title">SMILES</li> <li class="menu-title">SMILES</li>
<li> <li>
<a id="modal_dropdown_smiles_default" <a
id="modal_dropdown_smiles_default"
class="tooltip tooltip-left" class="tooltip tooltip-left"
data-tip="Ignores stereochemistry and charge"> data-tip="Ignores stereochemistry and charge"
>
Default Default
</a> </a>
</li> </li>
<li> <li>
<a id="modal_dropdown_smiles_canonical" <a
id="modal_dropdown_smiles_canonical"
class="tooltip tooltip-left" class="tooltip tooltip-left"
data-tip="Ignores stereochemistry, preserves charge"> data-tip="Ignores stereochemistry, preserves charge"
>
Canonical Canonical
</a> </a>
</li> </li>
<li> <li>
<a id="modal_dropdown_smiles_exact" <a
id="modal_dropdown_smiles_exact"
class="tooltip tooltip-left" class="tooltip tooltip-left"
data-tip="Exact match for stereochemistry and charge"> data-tip="Exact match for stereochemistry and charge"
>
Exact Exact
</a> </a>
</li> </li>
<li class="menu-title">InChI</li> <li class="menu-title">InChI</li>
<li> <li>
<a id="modal_dropdown_inchikey" <a
id="modal_dropdown_inchikey"
class="tooltip tooltip-left" class="tooltip tooltip-left"
data-tip="Search by InChIKey"> data-tip="Search by InChIKey"
>
InChIKey InChIKey
</a> </a>
</li> </li>
</ul> </ul>
</div> </div>
<button type="button" id="modal_search_button" class="btn btn-xs btn-ghost join-item"> <button
<kbd class="kbd kbd-sm p-1 text-base-content/50"> type="button"
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-corner-down-left-icon lucide-corner-down-left"><path d="M20 4v7a4 4 0 0 1-4 4H4"/><path d="m9 10-5 5 5 5"/></svg> id="modal_search_button"
class="btn btn-xs btn-ghost join-item"
>
<kbd class="kbd kbd-sm text-base-content/50 p-1">
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-corner-down-left-icon lucide-corner-down-left"
>
<path d="M20 4v7a4 4 0 0 1-4 4H4" />
<path d="m9 10-5 5 5 5" />
</svg>
</kbd> </kbd>
</button> </button>
</div> </div>
</div> </div>
<!-- Package Selector with Pills --> <!-- Package Selector with Pills -->
<div class="form-control mb-4 flex-shrink-0"> <div class="form-control mb-4 shrink-0">
<!-- Pills Container --> <!-- Pills Container -->
<div id="modal_package_pills_container" <div
class="flex flex-wrap gap-2 p-3 border-2 border-dashed border-base-300 rounded-lg m-3 min-h-[3rem] items-center"> id="modal_package_pills_container"
class="border-base-300 m-3 flex min-h-12 flex-wrap items-center gap-2 rounded-lg border-2 border-dashed p-3"
>
<!-- Pills will be added here dynamically --> <!-- Pills will be added here dynamically -->
</div> </div>
<!-- Package Dropdown Menu --> <!-- Package Dropdown Menu -->
<ul class="dropdown dropdown-end menu bg-base-200 rounded-box z-[100] w-80 max-h-96 overflow-y-auto p-2 shadow-lg" <ul
class="dropdown dropdown-center menu bg-base-200 rounded-box max-h-96 w-80 overflow-y-auto p-2 shadow-lg"
popover popover
id="package_dropdown_menu" id="package_dropdown_menu"
style="position-anchor:--anchor-packages"> style="position-anchor:--anchor-packages"
>
{% if unreviewed_packages %} {% if unreviewed_packages %}
<li class="menu-title">Reviewed Packages</li> <li class="menu-title">Reviewed Packages</li>
{% for obj in reviewed_packages %} {% for obj in reviewed_packages %}
<li> <li>
<a class="package-option flex justify-between items-center" <a
class="package-option flex items-center justify-between"
data-package-url="{{ obj.url }}" data-package-url="{{ obj.url }}"
data-package-name="{{ obj.name }}"> data-package-name="{{ obj.name }}"
>
<span>{{ obj.name }}</span> <span>{{ obj.name }}</span>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 package-checkmark hidden" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/> xmlns="http://www.w3.org/2000/svg"
class="package-checkmark hidden h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M5 13l4 4L19 7"
/>
</svg> </svg>
</a> </a>
</li> </li>
@ -107,12 +186,25 @@
<li class="menu-title">Unreviewed Packages</li> <li class="menu-title">Unreviewed Packages</li>
{% for obj in unreviewed_packages %} {% for obj in unreviewed_packages %}
<li> <li>
<a class="package-option flex justify-between items-center" <a
class="package-option flex items-center justify-between"
data-package-url="{{ obj.url }}" data-package-url="{{ obj.url }}"
data-package-name="{{ obj.name }}"> data-package-name="{{ obj.name }}"
>
<span>{{ obj.name }}</span> <span>{{ obj.name }}</span>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 package-checkmark hidden" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/> xmlns="http://www.w3.org/2000/svg"
class="package-checkmark hidden h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M5 13l4 4L19 7"
/>
</svg> </svg>
</a> </a>
</li> </li>
@ -121,12 +213,25 @@
<li class="menu-title">Reviewed Packages</li> <li class="menu-title">Reviewed Packages</li>
{% for obj in reviewed_packages %} {% for obj in reviewed_packages %}
<li> <li>
<a class="package-option flex justify-between items-center" <a
class="package-option flex items-center justify-between"
data-package-url="{{ obj.url }}" data-package-url="{{ obj.url }}"
data-package-name="{{ obj.name }}"> data-package-name="{{ obj.name }}"
>
<span>{{ obj.name }}</span> <span>{{ obj.name }}</span>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 package-checkmark hidden" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/> xmlns="http://www.w3.org/2000/svg"
class="package-checkmark hidden h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M5 13l4 4L19 7"
/>
</svg> </svg>
</a> </a>
</li> </li>
@ -136,12 +241,12 @@
</div> </div>
<!-- Loading Indicator --> <!-- Loading Indicator -->
<div id="search_loading" class="hidden justify-center py-8 flex-shrink-0"> <div id="search_loading" class="hidden shrink-0 justify-center py-8">
<span class="loading loading-spinner loading-lg text-primary"></span> <span class="loading loading-spinner loading-lg text-primary"></span>
</div> </div>
<!-- Results Container - scrollable --> <!-- Results Container - scrollable -->
<div id="search_results" class="flex-1 overflow-y-auto min-h-0 p-2"></div> <div id="search_results" class="min-h-0 flex-1 overflow-y-auto p-2"></div>
</div> </div>
<!-- Backdrop to close --> <!-- Backdrop to close -->
@ -151,7 +256,7 @@
</dialog> </dialog>
<script> <script>
(function() { (function () {
// Package Selector Module - Data-driven multiselect package selection // Package Selector Module - Data-driven multiselect package selection
const PackageSelector = { const PackageSelector = {
// Single source of truth: array of selected packages // Single source of truth: array of selected packages
@ -160,7 +265,7 @@
elements: { elements: {
pillsContainer: null, pillsContainer: null,
packageDropdown: null, packageDropdown: null,
packageOptions: null packageOptions: null,
}, },
init() { init() {
@ -171,39 +276,48 @@
}, },
cacheElements() { cacheElements() {
this.elements.pillsContainer = document.getElementById('modal_package_pills_container'); this.elements.pillsContainer = document.getElementById(
this.elements.packageDropdown = document.getElementById('package_dropdown_menu'); "modal_package_pills_container",
this.elements.packageOptions = document.querySelectorAll('.package-option'); );
this.elements.packageDropdown = document.getElementById(
"package_dropdown_menu",
);
this.elements.packageOptions =
document.querySelectorAll(".package-option");
}, },
loadInitialSelection() { loadInitialSelection() {
// Load pre-selected packages from server-rendered pills // Load pre-selected packages from server-rendered pills
const existingPills = this.elements.pillsContainer.querySelectorAll('.badge'); const existingPills =
existingPills.forEach(pill => { this.elements.pillsContainer.querySelectorAll(".badge");
existingPills.forEach((pill) => {
this.selectedPackages.push({ this.selectedPackages.push({
url: pill.dataset.packageUrl, url: pill.dataset.packageUrl,
name: pill.dataset.packageName name: pill.dataset.packageName,
}); });
}); });
// If no pills found, select all reviewed packages by default // If no pills found, select all reviewed packages by default
if (this.selectedPackages.length === 0) { if (this.selectedPackages.length === 0) {
// Iterate through all menu items and collect reviewed packages // Iterate through all menu items and collect reviewed packages
const menuItems = this.elements.packageDropdown.querySelectorAll('li'); const menuItems =
this.elements.packageDropdown.querySelectorAll("li");
for (const item of menuItems) { for (const item of menuItems) {
// Check if this is the "Unreviewed Packages" menu title // Check if this is the "Unreviewed Packages" menu title
if (item.classList.contains('menu-title') && if (
item.textContent.trim() === 'Unreviewed Packages') { item.classList.contains("menu-title") &&
item.textContent.trim() === "Unreviewed Packages"
) {
break; // Stop processing after this point break; // Stop processing after this point
} }
// Check for package options (only reviewed packages reach here) // Check for package options (only reviewed packages reach here)
const packageOption = item.querySelector('.package-option'); const packageOption = item.querySelector(".package-option");
if (packageOption) { if (packageOption) {
this.selectedPackages.push({ this.selectedPackages.push({
url: packageOption.dataset.packageUrl, url: packageOption.dataset.packageUrl,
name: packageOption.dataset.packageName name: packageOption.dataset.packageName,
}); });
} }
} }
@ -212,8 +326,8 @@
attachEventListeners() { attachEventListeners() {
// Toggle package selection on dropdown item click // Toggle package selection on dropdown item click
this.elements.packageOptions.forEach(option => { this.elements.packageOptions.forEach((option) => {
option.addEventListener('click', (e) => { option.addEventListener("click", (e) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); // Prevent dropdown from closing e.stopPropagation(); // Prevent dropdown from closing
const packageUrl = option.dataset.packageUrl; const packageUrl = option.dataset.packageUrl;
@ -223,9 +337,12 @@
}); });
// Remove package when X is clicked (using event delegation) // Remove package when X is clicked (using event delegation)
this.elements.pillsContainer.addEventListener('click', (e) => { this.elements.pillsContainer.addEventListener("click", (e) => {
if (e.target.classList.contains('package-remove-btn') || e.target.closest('.package-remove-btn')) { if (
const pill = e.target.closest('.badge'); e.target.classList.contains("package-remove-btn") ||
e.target.closest(".package-remove-btn")
) {
const pill = e.target.closest(".badge");
if (pill) { if (pill) {
const packageUrl = pill.dataset.packageUrl; const packageUrl = pill.dataset.packageUrl;
this.removePackage(packageUrl); this.removePackage(packageUrl);
@ -235,7 +352,9 @@
}, },
togglePackageSelection(packageUrl, packageName) { togglePackageSelection(packageUrl, packageName) {
const index = this.selectedPackages.findIndex(pkg => pkg.url === packageUrl); const index = this.selectedPackages.findIndex(
(pkg) => pkg.url === packageUrl,
);
if (index !== -1) { if (index !== -1) {
// Remove from selection // Remove from selection
@ -249,7 +368,9 @@
}, },
removePackage(packageUrl) { removePackage(packageUrl) {
const index = this.selectedPackages.findIndex(pkg => pkg.url === packageUrl); const index = this.selectedPackages.findIndex(
(pkg) => pkg.url === packageUrl,
);
if (index !== -1) { if (index !== -1) {
this.selectedPackages.splice(index, 1); this.selectedPackages.splice(index, 1);
this.render(); this.render();
@ -264,16 +385,18 @@
renderPills() { renderPills() {
// Clear existing pills and button (except placeholder) // Clear existing pills and button (except placeholder)
const pills = this.elements.pillsContainer.querySelectorAll('.badge'); const pills = this.elements.pillsContainer.querySelectorAll(".badge");
pills.forEach(pill => pill.remove()); pills.forEach((pill) => pill.remove());
const existingButton = this.elements.pillsContainer.querySelector('#modal_package_add_button'); const existingButton = this.elements.pillsContainer.querySelector(
"#modal_package_add_button",
);
if (existingButton) { if (existingButton) {
existingButton.remove(); existingButton.remove();
} }
// Create pills from data // Create pills from data
this.selectedPackages.forEach(pkg => { this.selectedPackages.forEach((pkg) => {
const pill = this.createPillElement(pkg.url, pkg.name); const pill = this.createPillElement(pkg.url, pkg.name);
this.elements.pillsContainer.appendChild(pill); this.elements.pillsContainer.appendChild(pill);
}); });
@ -285,12 +408,12 @@
return; return;
} }
const button = document.createElement('button'); const button = document.createElement("button");
button.type = 'button'; button.type = "button";
button.id = 'modal_package_add_button'; button.id = "modal_package_add_button";
button.setAttribute('popovertarget', 'package_dropdown_menu'); button.setAttribute("popovertarget", "package_dropdown_menu");
button.style.cssText = 'anchor-name:--anchor-packages'; button.style.cssText = "anchor-name:--anchor-packages";
button.className = 'btn btn-sm btn-ghost gap-2 text-base-content/50'; button.className = "btn btn-sm btn-ghost gap-2 text-base-content/50";
button.innerHTML = ` button.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-plus-icon lucide-plus"><path d="M5 12h14"/><path d="M12 5v14"/></svg> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-plus-icon lucide-plus"><path d="M5 12h14"/><path d="M12 5v14"/></svg>
@ -301,8 +424,8 @@
}, },
createPillElement(packageUrl, packageName) { createPillElement(packageUrl, packageName) {
const pill = document.createElement('span'); const pill = document.createElement("span");
pill.className = 'badge badge-outline gap-2 max-w-xs'; pill.className = "badge badge-outline gap-2 max-w-xs";
pill.dataset.packageUrl = packageUrl; pill.dataset.packageUrl = packageUrl;
pill.dataset.packageName = packageName; pill.dataset.packageName = packageName;
@ -326,35 +449,36 @@
renderCheckmarks() { renderCheckmarks() {
// Update all checkmarks based on selected packages // Update all checkmarks based on selected packages
this.elements.packageOptions.forEach(option => { this.elements.packageOptions.forEach((option) => {
const packageUrl = option.dataset.packageUrl; const packageUrl = option.dataset.packageUrl;
const isSelected = this.selectedPackages.some(pkg => pkg.url === packageUrl); const isSelected = this.selectedPackages.some(
const checkmark = option.querySelector('.package-checkmark'); (pkg) => pkg.url === packageUrl,
);
const checkmark = option.querySelector(".package-checkmark");
if (checkmark) { if (checkmark) {
checkmark.classList.toggle('hidden', !isSelected); checkmark.classList.toggle("hidden", !isSelected);
} }
}); });
}, },
getSelectedPackages() { getSelectedPackages() {
return this.selectedPackages.map(pkg => pkg.url); return this.selectedPackages.map((pkg) => pkg.url);
} },
}; };
// Modal and Search Management // Modal and Search Management
const modal = document.getElementById('search_modal'); const modal = document.getElementById("search_modal");
const searchbar = document.getElementById('modal_searchbar'); const searchbar = document.getElementById("modal_searchbar");
const searchButton = document.getElementById('modal_search_button'); const searchButton = document.getElementById("modal_search_button");
const modeButton = document.getElementById('modal_mode_button'); const modeButton = document.getElementById("modal_mode_button");
const resultsDiv = document.getElementById('search_results'); const resultsDiv = document.getElementById("search_results");
const loadingDiv = document.getElementById('search_loading'); const loadingDiv = document.getElementById("search_loading");
// MutationObserver to detect when modal opens // MutationObserver to detect when modal opens
const observer = new MutationObserver((mutations) => { const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => { mutations.forEach((mutation) => {
if (mutation.attributeName === 'open' && modal.open) { if (mutation.attributeName === "open" && modal.open) {
PackageSelector.render(); PackageSelector.render();
// Delay focus to allow CSS transitions to complete (modal has 0.3s transition) // Delay focus to allow CSS transitions to complete (modal has 0.3s transition)
setTimeout(() => { setTimeout(() => {
@ -366,33 +490,54 @@
observer.observe(modal, { attributes: true }); observer.observe(modal, { attributes: true });
// Clear results when modal closes // Close modal when clicking outside (on the backdrop)
modal.addEventListener('close', function() { // According to DaisyUI docs: https://daisyui.com/components/modal/
resultsDiv.innerHTML = ''; // The backdrop form with method="dialog" should handle closing automatically when its button is clicked.
loadingDiv.classList.add('hidden'); // We also handle clicks directly on the dialog element (backdrop area) or the backdrop form.
searchbar.value = ''; modal.addEventListener("click", function (event) {
const backdrop = modal.querySelector(".modal-backdrop");
const modalBox = modal.querySelector(".modal-box");
// Close if clicking directly on the dialog element (backdrop area)
// or on the backdrop form (but ensure we're not clicking on modal-box content)
if (
event.target === modal ||
(backdrop &&
(event.target === backdrop || backdrop.contains(event.target)) &&
!modalBox.contains(event.target))
) {
modal.close();
}
}); });
// Clear results when modal closes
modal.addEventListener("close", function () {
resultsDiv.innerHTML = "";
loadingDiv.classList.add("hidden");
searchbar.value = "";
});
// Mode dropdown handlers // Mode dropdown handlers
const dropdownMenu = document.getElementById('search_dropdown_menu'); const dropdownMenu = document.getElementById("search_dropdown_menu");
const modeButtons = [ const modeButtons = [
{ id: 'modal_dropdown_text', text: 'Text' }, { id: "modal_dropdown_text", text: "Text" },
{ id: 'modal_dropdown_smiles_default', text: 'Default' }, { id: "modal_dropdown_smiles_default", text: "Default" },
{ id: 'modal_dropdown_smiles_canonical', text: 'Canonical' }, { id: "modal_dropdown_smiles_canonical", text: "Canonical" },
{ id: 'modal_dropdown_smiles_exact', text: 'Exact' }, { id: "modal_dropdown_smiles_exact", text: "Exact" },
{ id: 'modal_dropdown_inchikey', text: 'InChIKey' } { id: "modal_dropdown_inchikey", text: "InChIKey" },
]; ];
modeButtons.forEach(({ id, text }) => { modeButtons.forEach(({ id, text }) => {
document.getElementById(id).addEventListener('click', function(e) { document.getElementById(id).addEventListener("click", function (e) {
e.preventDefault(); e.preventDefault();
modeButton.innerHTML = text + ` <svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"> modeButton.innerHTML =
text +
` <svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
</svg>`; </svg>`;
// Close dropdown using popover API // Close dropdown using popover API
if (dropdownMenu && typeof dropdownMenu.hidePopover === 'function') { if (dropdownMenu && typeof dropdownMenu.hidePopover === "function") {
dropdownMenu.hidePopover(); dropdownMenu.hidePopover();
} }
}); });
@ -403,23 +548,29 @@
// Search Response Handler // Search Response Handler
function handleSearchResponse(data) { function handleSearchResponse(data) {
resultsDiv.innerHTML = ''; resultsDiv.innerHTML = "";
function makeContent(objs) { function makeContent(objs) {
let links = ''; let links = "";
objs.forEach(obj => { objs.forEach((obj) => {
links += `<a href="${obj.url}" class="block px-4 py-2 hover:bg-base-300 rounded-lg transition-colors">${obj.name}</a>`; links += `<a href="${obj.url}" class="block px-4 py-2 hover:bg-base-300 rounded-lg transition-colors">${obj.name}</a>`;
}); });
return links; return links;
} }
let allEmpty = true; let allEmpty = true;
let content = ''; let content = "";
// Category order for better UX // Category order for better UX
const categoryOrder = ['Compounds', 'Compound Structures', 'Rules', 'Reactions', 'Pathways']; const categoryOrder = [
"Compounds",
"Compound Structures",
"Rules",
"Reactions",
"Pathways",
];
categoryOrder.forEach(key => { categoryOrder.forEach((key) => {
if (!data[key] || data[key].length < 1) { if (!data[key] || data[key].length < 1) {
return; return;
} }
@ -464,14 +615,14 @@
const query = searchbar.value.trim(); const query = searchbar.value.trim();
if (!query) { if (!query) {
console.log('Search phrase empty, won\'t do search'); console.log("Search phrase empty, won't do search");
return; return;
} }
const selPacks = PackageSelector.getSelectedPackages(); const selPacks = PackageSelector.getSelectedPackages();
if (selPacks.length < 1) { if (selPacks.length < 1) {
console.log('No package selected, won\'t do search'); console.log("No package selected, won't do search");
resultsDiv.innerHTML = ` resultsDiv.innerHTML = `
<div class="alert alert-info"> <div class="alert alert-info">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@ -486,33 +637,33 @@
const mode = modeButton.textContent.trim().toLowerCase(); const mode = modeButton.textContent.trim().toLowerCase();
const params = new URLSearchParams(); const params = new URLSearchParams();
selPacks.forEach(pack => params.append('packages', pack)); selPacks.forEach((pack) => params.append("packages", pack));
params.append('search', query); params.append("search", query);
params.append('mode', mode); params.append("mode", mode);
// Show loading // Show loading
loadingDiv.classList.remove('hidden'); loadingDiv.classList.remove("hidden");
resultsDiv.innerHTML = ''; resultsDiv.innerHTML = "";
fetch(`{{ SERVER_BASE }}/search?${params.toString()}`, { fetch(`{{ SERVER_BASE }}/search?${params.toString()}`, {
method: 'GET', method: "GET",
headers: { headers: {
'Accept': 'application/json' Accept: "application/json",
} },
}) })
.then(response => { .then((response) => {
if (!response.ok) { if (!response.ok) {
throw new Error('Search request failed'); throw new Error("Search request failed");
} }
return response.json(); return response.json();
}) })
.then(result => { .then((result) => {
loadingDiv.classList.add('hidden'); loadingDiv.classList.add("hidden");
handleSearchResponse(result); handleSearchResponse(result);
}) })
.catch(error => { .catch((error) => {
loadingDiv.classList.add('hidden'); loadingDiv.classList.add("hidden");
console.error('Search error:', error); console.error("Search error:", error);
resultsDiv.innerHTML = ` resultsDiv.innerHTML = `
<div class="alert alert-error"> <div class="alert alert-error">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@ -525,11 +676,11 @@
} }
// Event listeners for search // Event listeners for search
searchButton.addEventListener('click', performSearch); searchButton.addEventListener("click", performSearch);
searchbar.addEventListener('keydown', function(e) { searchbar.addEventListener("keydown", function (e) {
if (e.key === 'Enter') { if (e.key === "Enter") {
performSearch(e); performSearch(e);
} }
}); });
})(); })();
</script> </script>

View File

@ -2,7 +2,13 @@
{% load static %} {% load static %}
{% block content %} {% block content %}
<div class="mx-auto w-full p-8"> <div class="mx-auto w-full p-8">
<h1 class="h1 mb-4 text-3xl font-bold">Predict a Pathway</h1> <h1 class="h1 mb-4 text-3xl font-bold">
Predict a Pathway
<span class="text-base-content/50 text-xs"
>in <strong>{{ meta.current_package.name|safe }}</strong>
</span>
</h1>
<form <form
id="predict_form" id="predict_form"

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 %}