1 Commits

Author SHA1 Message Date
95d16690ef added retrain code 2025-11-11 20:38:13 +01:00
6 changed files with 665 additions and 650 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 Edge, EPModel, JobLog, Node, Package, Pathway, Rule, Setting, User from epdb.models import EPModel, JobLog, Node, Package, Pathway, Rule, Setting, User, Edge
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 = timezone.now() log.done_at = datetime.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

@ -178,23 +178,6 @@
} }
}); });
// 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
@ -215,7 +198,7 @@
if (isCorrectModifier && event.key === "k") { if (isCorrectModifier && event.key === "k") {
event.preventDefault(); event.preventDefault();
openSearchModal(); search_modal.showModal();
} }
}); });
</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 id="search-trigger" role="button" class="cursor-pointer"> <a href="/search" role="button">
<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,38 +2,36 @@
{% 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 relative mx-auto h-fit w-full max-w-5xl shadow-none"> <section class="hero h-fit max-w-5xl w-full shadow-none mx-auto relative">
<div <div
class="hero from-primary-800 to-primary-600 min-h-[calc(100vh*0.4)] bg-gradient-to-br" class="hero min-h-[calc(100vh*0.4)] bg-gradient-to-br from-primary-800 to-primary-600"
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 z-10 -translate-x-8"> <div class="absolute bottom-40 left-1/8 -translate-x-8 z-10">
<h2 class="text-base-100 text-left text-3xl text-shadow-lg"> <h2 class="text-3xl text-base-100 text-shadow-lg text-left">
Predict Your Pathway Predict Your Pathway
</h2> </h2>
</div> </div>
</div> </div>
</section> </section>
<div class="bg-base-200 mx-auto max-w-5xl shadow-md"> <div class="shadow-md max-w-5xl mx-auto bg-base-200">
<!-- Predict Pathway Section --> <!-- Predict Pathway Section -->
<div <div
class="relative z-20 mx-auto -mt-32 mb-10 w-full flex-col lg:flex-row-reverse" class="flex-col lg:flex-row-reverse w-full mx-auto -mt-32 relative z-20 mb-10 "
> >
<div <div
class="card bg-base-100 mx-auto w-3/4 shrink-0 shadow-xl transition-all duration-300 ease-in-out" class="card bg-base-100 shrink-0 shadow-xl w-3/4 mx-auto transition-all duration-300 ease-in-out"
> >
<div class="card-body"> <div class="card-body">
<div class="my-4 ml-8 flex h-fit flex-row items-center justify-start"> <!-- Input Mode Toggle - Fixed position outside fieldset -->
<div class="flex items-center gap-1"> <div class="flex flex-row justify-start items-center h-fit ml-8 my-4">
<label class="swap btn btn-ghost btn-sm p-1" title="Input Mode"> <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">
<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"
@ -54,13 +52,6 @@
/> />
</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"
@ -73,15 +64,12 @@
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 overflow-hidden transition-all duration-300 ease-in-out" class="fieldset transition-all duration-300 ease-in-out overflow-hidden"
> >
<form <form
id="index-form" id="index-form"
@ -91,29 +79,29 @@
{% csrf_token %} {% csrf_token %}
<div <div
id="text-input-container" id="text-input-container"
class="scale-100 transform opacity-100 transition-all duration-300 ease-in-out" class="transition-all duration-300 ease-in-out opacity-100 transform scale-100"
> >
<div class="join mx-auto w-full"> <div class="join w-full mx-auto">
<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 input-md join-item grow" class="input grow input-md join-item"
/> />
<button class="btn btn-neutral join-item">Predict!</button> <button class="btn btn-neutral join-item">Predict!</button>
</div> </div>
<div class="label relative mt-1 w-full"> <div class="label relative w-full mt-1">
<div class="flex gap-2"> <div class="flex gap-2">
<a <a
href="#" href="#"
class="example-link hover:text-primary cursor-pointer" class="example-link cursor-pointer hover:text-primary"
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 hover:text-primary cursor-pointer" class="example-link cursor-pointer hover:text-primary"
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
@ -126,7 +114,7 @@
</div> </div>
<div <div
id="ketcher-container" id="ketcher-container"
class="hidden w-full scale-95 transform opacity-0 transition-all duration-300 ease-in-out" class="hidden w-full transition-all duration-300 ease-in-out opacity-0 transform scale-95"
> >
<iframe <iframe
id="index-ketcher" id="index-ketcher"
@ -136,13 +124,11 @@
class="rounded-lg" class="rounded-lg"
></iframe> ></iframe>
<button <button
class="btn btn-lg bg-primary-950 text-primary-50 join-item mt-2 w-full" class="btn btn-lg bg-primary-950 text-primary-50 join-item w-full mt-2"
> >
Predict! Predict!
</button> </button>
<div class="mt-1 flex w-full justify-end"> <a class="label mx-auto w-full mt-1" href="#">Advanced</a>
<a class="label justify-end" href="/predict">Advanced</a>
</div>
</div> </div>
<input <input
type="hidden" type="hidden"
@ -164,18 +150,18 @@
</div> </div>
<!-- Community News Section --> <!-- Community News Section -->
<section class="bg-base-200 z-10 mx-8 py-16"> <section class="py-16 bg-base-200 z-10 mx-8">
<div class="mx-auto max-w-7xl px-4"> <div class="max-w-7xl mx-auto px-4">
<h2 class="h2 mb-8 text-left font-bold">Community Updates</h2> <h2 class="h2 font-bold text-left mb-8">Community Updates</h2>
<div id="community-news-container" class="flex justify-center gap-4"> <div id="community-news-container" class="flex gap-4 justify-center">
<!-- News cards will be populated here --> <!-- News cards will be populated here -->
<div id="loading" class="flex w-full justify-center"> <div id="loading" class="flex justify-center w-full">
<span class="loading loading-spinner loading-lg"></span> <span class="loading loading-spinner loading-lg"></span>
</div> </div>
</div> </div>
<div class="mt-6 text-right"> <div class="text-right mt-6">
<a <a
href="https://community.envipath.org/c/announcements/10" href="https://community.envipath.org/c/announcements/10"
target="_blank" target="_blank"
@ -191,18 +177,18 @@
</section> </section>
<!-- Mission Statement Section --> <!-- Mission Statement Section -->
<section class="from-base-200 to-base-100 bg-gradient-to-b py-16"> <section class="py-16 from-base-200 to-base-100 bg-gradient-to-b">
<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="h-full w-full object-contain" class="w-full h-full object-contain"
/> />
</div> </div>
<div class="mr-8 w-2/3 space-y-4 text-left"> <div class="space-y-4 text-left w-2/3 mr-8">
<h2 class="h2 mb-8 font-bold">About enviPath</h2> <h2 class="h2 font-bold mb-8">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
@ -215,7 +201,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="float-right flex flex-row gap-4"> <div class="flex flex-row gap-4 float-right">
<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>
@ -225,7 +211,7 @@
</section> </section>
<!-- Partners Section --> <!-- Partners Section -->
<section class="bg-base-100 py-14 sm:py-12"> <section class="py-14 sm:py-12 bg-base-100">
<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>
@ -236,12 +222,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,184 +1,105 @@
{% 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 h-full w-lvw p-1 sm:h-8/12 sm:w-[85vw] sm:max-w-5xl"> <div class="modal-box w-lvw sm:w-[85vw] sm:max-w-5xl h-full sm:h-8/12 p-1" >
<!-- Search Input and Mode Selector --> <!-- Search Input and Mode Selector -->
<div class="form-control mb-4 w-full shrink-0"> <div class="form-control mb-4 flex-shrink-0 w-full">
<div class="join m-0 w-full items-center p-3"> <div class="join w-full m-0 p-3 items-center">
<label class="input join-item input-ghost grow"> <label class="input join-item input-ghost grow">
<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-search-icon lucide-search"><path d="m21 21-4.34-4.34"/><circle cx="11" cy="11" r="8"/></svg>
xmlns="http://www.w3.org/2000/svg" <input type="text" autofocus
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" class="grow" aria-label="Search" />
aria-label="Search"
/>
</label> </label>
<!-- Mode Dropdown --> <!-- Mode Dropdown -->
<div> <div>
<button <button type="button"
type="button"
tabindex="0" tabindex="0"
id="modal_mode_button" id="modal_mode_button"
popovertarget="search_dropdown_menu" popovertarget="search_dropdown_menu" style="anchor-name:--anchor-1"
style="anchor-name:--1" class="btn join-item btn-ghost">
class="btn join-item btn-ghost"
>
Text Text
<svg <svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
class="ml-1 h-4 w-4" <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
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 <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">
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 <a id="modal_dropdown_text"
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 <a id="modal_dropdown_smiles_default"
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 <a id="modal_dropdown_smiles_canonical"
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 <a id="modal_dropdown_smiles_exact"
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 <a id="modal_dropdown_inchikey"
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 <button type="button" id="modal_search_button" class="btn btn-xs btn-ghost join-item">
type="button" <kbd class="kbd kbd-sm p-1 text-base-content/50">
id="modal_search_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>
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 shrink-0"> <div class="form-control mb-4 flex-shrink-0">
<!-- Pills Container --> <!-- Pills Container -->
<div <div id="modal_package_pills_container"
id="modal_package_pills_container" class="flex flex-wrap gap-2 p-3 border-2 border-dashed border-base-300 rounded-lg m-3 min-h-[3rem] items-center">
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 <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"
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 <a class="package-option flex justify-between items-center"
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 <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">
xmlns="http://www.w3.org/2000/svg" <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
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>
@ -186,25 +107,12 @@
<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 <a class="package-option flex justify-between items-center"
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 <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">
xmlns="http://www.w3.org/2000/svg" <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
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>
@ -213,25 +121,12 @@
<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 <a class="package-option flex justify-between items-center"
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 <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">
xmlns="http://www.w3.org/2000/svg" <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
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>
@ -241,12 +136,12 @@
</div> </div>
<!-- Loading Indicator --> <!-- Loading Indicator -->
<div id="search_loading" class="hidden shrink-0 justify-center py-8"> <div id="search_loading" class="hidden justify-center py-8 flex-shrink-0">
<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="min-h-0 flex-1 overflow-y-auto p-2"></div> <div id="search_results" class="flex-1 overflow-y-auto min-h-0 p-2"></div>
</div> </div>
<!-- Backdrop to close --> <!-- Backdrop to close -->
@ -256,7 +151,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
@ -265,7 +160,7 @@
elements: { elements: {
pillsContainer: null, pillsContainer: null,
packageDropdown: null, packageDropdown: null,
packageOptions: null, packageOptions: null
}, },
init() { init() {
@ -276,48 +171,39 @@
}, },
cacheElements() { cacheElements() {
this.elements.pillsContainer = document.getElementById( this.elements.pillsContainer = document.getElementById('modal_package_pills_container');
"modal_package_pills_container", this.elements.packageDropdown = document.getElementById('package_dropdown_menu');
); 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 = const existingPills = this.elements.pillsContainer.querySelectorAll('.badge');
this.elements.pillsContainer.querySelectorAll(".badge"); existingPills.forEach(pill => {
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 = const menuItems = this.elements.packageDropdown.querySelectorAll('li');
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 ( if (item.classList.contains('menu-title') &&
item.classList.contains("menu-title") && item.textContent.trim() === 'Unreviewed Packages') {
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
}); });
} }
} }
@ -326,8 +212,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;
@ -337,12 +223,9 @@
}); });
// 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 ( if (e.target.classList.contains('package-remove-btn') || e.target.closest('.package-remove-btn')) {
e.target.classList.contains("package-remove-btn") || const pill = e.target.closest('.badge');
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);
@ -352,9 +235,7 @@
}, },
togglePackageSelection(packageUrl, packageName) { togglePackageSelection(packageUrl, packageName) {
const index = this.selectedPackages.findIndex( const index = this.selectedPackages.findIndex(pkg => pkg.url === packageUrl);
(pkg) => pkg.url === packageUrl,
);
if (index !== -1) { if (index !== -1) {
// Remove from selection // Remove from selection
@ -368,9 +249,7 @@
}, },
removePackage(packageUrl) { removePackage(packageUrl) {
const index = this.selectedPackages.findIndex( const index = this.selectedPackages.findIndex(pkg => pkg.url === packageUrl);
(pkg) => pkg.url === packageUrl,
);
if (index !== -1) { if (index !== -1) {
this.selectedPackages.splice(index, 1); this.selectedPackages.splice(index, 1);
this.render(); this.render();
@ -385,18 +264,16 @@
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( const existingButton = this.elements.pillsContainer.querySelector('#modal_package_add_button');
"#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);
}); });
@ -408,12 +285,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>
@ -424,8 +301,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;
@ -449,36 +326,35 @@
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( const isSelected = this.selectedPackages.some(pkg => pkg.url === packageUrl);
(pkg) => pkg.url === packageUrl, const checkmark = option.querySelector('.package-checkmark');
);
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(() => {
@ -490,54 +366,33 @@
observer.observe(modal, { attributes: true }); observer.observe(modal, { attributes: true });
// Close modal when clicking outside (on the backdrop)
// According to DaisyUI docs: https://daisyui.com/components/modal/
// The backdrop form with method="dialog" should handle closing automatically when its button is clicked.
// We also handle clicks directly on the dialog element (backdrop area) or the backdrop form.
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 // Clear results when modal closes
modal.addEventListener("close", function () { modal.addEventListener('close', function() {
resultsDiv.innerHTML = ""; resultsDiv.innerHTML = '';
loadingDiv.classList.add("hidden"); loadingDiv.classList.add('hidden');
searchbar.value = ""; 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 = modeButton.innerHTML = text + ` <svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
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();
} }
}); });
@ -548,29 +403,23 @@
// 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 = [ const categoryOrder = ['Compounds', 'Compound Structures', 'Rules', 'Reactions', 'Pathways'];
"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;
} }
@ -615,14 +464,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">
@ -637,33 +486,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">
@ -676,11 +525,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>

197
templates/search.html Normal file
View File

@ -0,0 +1,197 @@
{% 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 %}