3 Commits

Author SHA1 Message Date
eb6c5ade29 fix: open and close search modal
Modal now opens on badge click.
Modal now closes on random click around
2025-11-12 15:37:35 +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
7 changed files with 624 additions and 635 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,105 +1,184 @@
{% load static %}
<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 -->
<div class="form-control mb-4 flex-shrink-0 w-full">
<div class="join w-full m-0 p-3 items-center">
<div class="form-control mb-4 w-full shrink-0">
<div class="join m-0 w-full items-center p-3">
<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>
<input type="text" autofocus
<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>
<input
type="text"
autofocus
id="modal_searchbar"
placeholder="Benfuracarb"
class="grow" aria-label="Search" />
class="grow"
aria-label="Search"
/>
</label>
<!-- Mode Dropdown -->
<div>
<button type="button"
<button
type="button"
tabindex="0"
id="modal_mode_button"
popovertarget="search_dropdown_menu" style="anchor-name:--anchor-1"
class="btn join-item btn-ghost">
popovertarget="search_dropdown_menu"
style="anchor-name:--1"
class="btn join-item btn-ghost"
>
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"/>
<svg
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>
</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>
<a id="modal_dropdown_text"
<a
id="modal_dropdown_text"
class="tooltip tooltip-left"
data-tip="Search on object names and descriptions">
data-tip="Search on object names and descriptions"
>
Text
</a>
</li>
<li class="menu-title">SMILES</li>
<li>
<a id="modal_dropdown_smiles_default"
<a
id="modal_dropdown_smiles_default"
class="tooltip tooltip-left"
data-tip="Ignores stereochemistry and charge">
data-tip="Ignores stereochemistry and charge"
>
Default
</a>
</li>
<li>
<a id="modal_dropdown_smiles_canonical"
<a
id="modal_dropdown_smiles_canonical"
class="tooltip tooltip-left"
data-tip="Ignores stereochemistry, preserves charge">
data-tip="Ignores stereochemistry, preserves charge"
>
Canonical
</a>
</li>
<li>
<a id="modal_dropdown_smiles_exact"
<a
id="modal_dropdown_smiles_exact"
class="tooltip tooltip-left"
data-tip="Exact match for stereochemistry and charge">
data-tip="Exact match for stereochemistry and charge"
>
Exact
</a>
</li>
<li class="menu-title">InChI</li>
<li>
<a id="modal_dropdown_inchikey"
<a
id="modal_dropdown_inchikey"
class="tooltip tooltip-left"
data-tip="Search by InChIKey">
data-tip="Search by InChIKey"
>
InChIKey
</a>
</li>
</ul>
</div>
<button type="button" id="modal_search_button" class="btn btn-xs btn-ghost join-item">
<kbd class="kbd kbd-sm p-1 text-base-content/50">
<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>
<button
type="button"
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>
</button>
</div>
</div>
<!-- Package Selector with Pills -->
<div class="form-control mb-4 flex-shrink-0">
<div class="form-control mb-4 shrink-0">
<!-- Pills Container -->
<div 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">
<div
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 -->
</div>
<!-- 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
id="package_dropdown_menu"
style="position-anchor:--anchor-packages">
style="position-anchor:--anchor-packages"
>
{% if unreviewed_packages %}
<li class="menu-title">Reviewed Packages</li>
{% for obj in reviewed_packages %}
<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-name="{{ obj.name }}">
data-package-name="{{ obj.name }}"
>
<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">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
<svg
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>
</a>
</li>
@ -107,12 +186,25 @@
<li class="menu-title">Unreviewed Packages</li>
{% for obj in unreviewed_packages %}
<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-name="{{ obj.name }}">
data-package-name="{{ obj.name }}"
>
<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">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
<svg
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>
</a>
</li>
@ -121,12 +213,25 @@
<li class="menu-title">Reviewed Packages</li>
{% for obj in reviewed_packages %}
<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-name="{{ obj.name }}">
data-package-name="{{ obj.name }}"
>
<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">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
<svg
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>
</a>
</li>
@ -136,12 +241,12 @@
</div>
<!-- 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>
</div>
<!-- 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>
<!-- Backdrop to close -->
@ -151,7 +256,7 @@
</dialog>
<script>
(function() {
(function () {
// Package Selector Module - Data-driven multiselect package selection
const PackageSelector = {
// Single source of truth: array of selected packages
@ -160,7 +265,7 @@
elements: {
pillsContainer: null,
packageDropdown: null,
packageOptions: null
packageOptions: null,
},
init() {
@ -171,39 +276,48 @@
},
cacheElements() {
this.elements.pillsContainer = document.getElementById('modal_package_pills_container');
this.elements.packageDropdown = document.getElementById('package_dropdown_menu');
this.elements.packageOptions = document.querySelectorAll('.package-option');
this.elements.pillsContainer = document.getElementById(
"modal_package_pills_container",
);
this.elements.packageDropdown = document.getElementById(
"package_dropdown_menu",
);
this.elements.packageOptions =
document.querySelectorAll(".package-option");
},
loadInitialSelection() {
// Load pre-selected packages from server-rendered pills
const existingPills = this.elements.pillsContainer.querySelectorAll('.badge');
existingPills.forEach(pill => {
const existingPills =
this.elements.pillsContainer.querySelectorAll(".badge");
existingPills.forEach((pill) => {
this.selectedPackages.push({
url: pill.dataset.packageUrl,
name: pill.dataset.packageName
name: pill.dataset.packageName,
});
});
// If no pills found, select all reviewed packages by default
if (this.selectedPackages.length === 0) {
// 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) {
// Check if this is the "Unreviewed Packages" menu title
if (item.classList.contains('menu-title') &&
item.textContent.trim() === 'Unreviewed Packages') {
if (
item.classList.contains("menu-title") &&
item.textContent.trim() === "Unreviewed Packages"
) {
break; // Stop processing after this point
}
// Check for package options (only reviewed packages reach here)
const packageOption = item.querySelector('.package-option');
const packageOption = item.querySelector(".package-option");
if (packageOption) {
this.selectedPackages.push({
url: packageOption.dataset.packageUrl,
name: packageOption.dataset.packageName
name: packageOption.dataset.packageName,
});
}
}
@ -212,8 +326,8 @@
attachEventListeners() {
// Toggle package selection on dropdown item click
this.elements.packageOptions.forEach(option => {
option.addEventListener('click', (e) => {
this.elements.packageOptions.forEach((option) => {
option.addEventListener("click", (e) => {
e.preventDefault();
e.stopPropagation(); // Prevent dropdown from closing
const packageUrl = option.dataset.packageUrl;
@ -223,9 +337,12 @@
});
// Remove package when X is clicked (using event delegation)
this.elements.pillsContainer.addEventListener('click', (e) => {
if (e.target.classList.contains('package-remove-btn') || e.target.closest('.package-remove-btn')) {
const pill = e.target.closest('.badge');
this.elements.pillsContainer.addEventListener("click", (e) => {
if (
e.target.classList.contains("package-remove-btn") ||
e.target.closest(".package-remove-btn")
) {
const pill = e.target.closest(".badge");
if (pill) {
const packageUrl = pill.dataset.packageUrl;
this.removePackage(packageUrl);
@ -235,7 +352,9 @@
},
togglePackageSelection(packageUrl, packageName) {
const index = this.selectedPackages.findIndex(pkg => pkg.url === packageUrl);
const index = this.selectedPackages.findIndex(
(pkg) => pkg.url === packageUrl,
);
if (index !== -1) {
// Remove from selection
@ -249,7 +368,9 @@
},
removePackage(packageUrl) {
const index = this.selectedPackages.findIndex(pkg => pkg.url === packageUrl);
const index = this.selectedPackages.findIndex(
(pkg) => pkg.url === packageUrl,
);
if (index !== -1) {
this.selectedPackages.splice(index, 1);
this.render();
@ -264,16 +385,18 @@
renderPills() {
// Clear existing pills and button (except placeholder)
const pills = this.elements.pillsContainer.querySelectorAll('.badge');
pills.forEach(pill => pill.remove());
const pills = this.elements.pillsContainer.querySelectorAll(".badge");
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) {
existingButton.remove();
}
// Create pills from data
this.selectedPackages.forEach(pkg => {
this.selectedPackages.forEach((pkg) => {
const pill = this.createPillElement(pkg.url, pkg.name);
this.elements.pillsContainer.appendChild(pill);
});
@ -285,12 +408,12 @@
return;
}
const button = document.createElement('button');
button.type = 'button';
button.id = 'modal_package_add_button';
button.setAttribute('popovertarget', 'package_dropdown_menu');
button.style.cssText = 'anchor-name:--anchor-packages';
button.className = 'btn btn-sm btn-ghost gap-2 text-base-content/50';
const button = document.createElement("button");
button.type = "button";
button.id = "modal_package_add_button";
button.setAttribute("popovertarget", "package_dropdown_menu");
button.style.cssText = "anchor-name:--anchor-packages";
button.className = "btn btn-sm btn-ghost gap-2 text-base-content/50";
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>
@ -301,8 +424,8 @@
},
createPillElement(packageUrl, packageName) {
const pill = document.createElement('span');
pill.className = 'badge badge-outline gap-2 max-w-xs';
const pill = document.createElement("span");
pill.className = "badge badge-outline gap-2 max-w-xs";
pill.dataset.packageUrl = packageUrl;
pill.dataset.packageName = packageName;
@ -326,35 +449,36 @@
renderCheckmarks() {
// Update all checkmarks based on selected packages
this.elements.packageOptions.forEach(option => {
this.elements.packageOptions.forEach((option) => {
const packageUrl = option.dataset.packageUrl;
const isSelected = this.selectedPackages.some(pkg => pkg.url === packageUrl);
const checkmark = option.querySelector('.package-checkmark');
const isSelected = this.selectedPackages.some(
(pkg) => pkg.url === packageUrl,
);
const checkmark = option.querySelector(".package-checkmark");
if (checkmark) {
checkmark.classList.toggle('hidden', !isSelected);
checkmark.classList.toggle("hidden", !isSelected);
}
});
},
getSelectedPackages() {
return this.selectedPackages.map(pkg => pkg.url);
}
return this.selectedPackages.map((pkg) => pkg.url);
},
};
// Modal and Search Management
const modal = document.getElementById('search_modal');
const searchbar = document.getElementById('modal_searchbar');
const searchButton = document.getElementById('modal_search_button');
const modeButton = document.getElementById('modal_mode_button');
const resultsDiv = document.getElementById('search_results');
const loadingDiv = document.getElementById('search_loading');
const modal = document.getElementById("search_modal");
const searchbar = document.getElementById("modal_searchbar");
const searchButton = document.getElementById("modal_search_button");
const modeButton = document.getElementById("modal_mode_button");
const resultsDiv = document.getElementById("search_results");
const loadingDiv = document.getElementById("search_loading");
// MutationObserver to detect when modal opens
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.attributeName === 'open' && modal.open) {
if (mutation.attributeName === "open" && modal.open) {
PackageSelector.render();
// Delay focus to allow CSS transitions to complete (modal has 0.3s transition)
setTimeout(() => {
@ -366,33 +490,54 @@
observer.observe(modal, { attributes: true });
// Clear results when modal closes
modal.addEventListener('close', function() {
resultsDiv.innerHTML = '';
loadingDiv.classList.add('hidden');
searchbar.value = '';
// 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
modal.addEventListener("close", function () {
resultsDiv.innerHTML = "";
loadingDiv.classList.add("hidden");
searchbar.value = "";
});
// Mode dropdown handlers
const dropdownMenu = document.getElementById('search_dropdown_menu');
const dropdownMenu = document.getElementById("search_dropdown_menu");
const modeButtons = [
{ id: 'modal_dropdown_text', text: 'Text' },
{ id: 'modal_dropdown_smiles_default', text: 'Default' },
{ id: 'modal_dropdown_smiles_canonical', text: 'Canonical' },
{ id: 'modal_dropdown_smiles_exact', text: 'Exact' },
{ id: 'modal_dropdown_inchikey', text: 'InChIKey' }
{ id: "modal_dropdown_text", text: "Text" },
{ id: "modal_dropdown_smiles_default", text: "Default" },
{ id: "modal_dropdown_smiles_canonical", text: "Canonical" },
{ id: "modal_dropdown_smiles_exact", text: "Exact" },
{ id: "modal_dropdown_inchikey", text: "InChIKey" },
];
modeButtons.forEach(({ id, text }) => {
document.getElementById(id).addEventListener('click', function(e) {
document.getElementById(id).addEventListener("click", function (e) {
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"/>
</svg>`;
// Close dropdown using popover API
if (dropdownMenu && typeof dropdownMenu.hidePopover === 'function') {
if (dropdownMenu && typeof dropdownMenu.hidePopover === "function") {
dropdownMenu.hidePopover();
}
});
@ -403,23 +548,29 @@
// Search Response Handler
function handleSearchResponse(data) {
resultsDiv.innerHTML = '';
resultsDiv.innerHTML = "";
function makeContent(objs) {
let links = '';
objs.forEach(obj => {
let links = "";
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>`;
});
return links;
}
let allEmpty = true;
let content = '';
let content = "";
// 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) {
return;
}
@ -464,14 +615,14 @@
const query = searchbar.value.trim();
if (!query) {
console.log('Search phrase empty, won\'t do search');
console.log("Search phrase empty, won't do search");
return;
}
const selPacks = PackageSelector.getSelectedPackages();
if (selPacks.length < 1) {
console.log('No package selected, won\'t do search');
console.log("No package selected, won't do search");
resultsDiv.innerHTML = `
<div class="alert alert-info">
<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 params = new URLSearchParams();
selPacks.forEach(pack => params.append('packages', pack));
params.append('search', query);
params.append('mode', mode);
selPacks.forEach((pack) => params.append("packages", pack));
params.append("search", query);
params.append("mode", mode);
// Show loading
loadingDiv.classList.remove('hidden');
resultsDiv.innerHTML = '';
loadingDiv.classList.remove("hidden");
resultsDiv.innerHTML = "";
fetch(`{{ SERVER_BASE }}/search?${params.toString()}`, {
method: 'GET',
method: "GET",
headers: {
'Accept': 'application/json'
}
Accept: "application/json",
},
})
.then(response => {
.then((response) => {
if (!response.ok) {
throw new Error('Search request failed');
throw new Error("Search request failed");
}
return response.json();
})
.then(result => {
loadingDiv.classList.add('hidden');
.then((result) => {
loadingDiv.classList.add("hidden");
handleSearchResponse(result);
})
.catch(error => {
loadingDiv.classList.add('hidden');
console.error('Search error:', error);
.catch((error) => {
loadingDiv.classList.add("hidden");
console.error("Search error:", error);
resultsDiv.innerHTML = `
<div class="alert alert-error">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@ -525,11 +676,11 @@
}
// Event listeners for search
searchButton.addEventListener('click', performSearch);
searchbar.addEventListener('keydown', function(e) {
if (e.key === 'Enter') {
searchButton.addEventListener("click", performSearch);
searchbar.addEventListener("keydown", function (e) {
if (e.key === "Enter") {
performSearch(e);
}
});
})();
})();
</script>

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