5 Commits

Author SHA1 Message Date
db286d14ec Merge branch 'develop' into fix/missing_nav_links 2025-11-12 22:01:46 +13:00
d20a705011 [Feature] Add per-package pathway prediction (#195)
## Major Changes

- Introduces a new view for per-package predictions

Co-authored-by: jebus <lorsbach@envipath.com>
Reviewed-on: enviPath/enviPy#195
Co-authored-by: Tobias O <tobias.olenyi@envipath.com>
Co-committed-by: Tobias O <tobias.olenyi@envipath.com>
2025-11-12 22:01:34 +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
999dc15189 fix: add/update missing nav links 2025-11-12 18:15:33 +13:00
10 changed files with 751 additions and 708 deletions

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)

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

@ -1,17 +1,20 @@
{% load static %} {% load static %}
<div class="lg:max-w-5xl mt-10 mx-auto bg-base-300 text-base-content"> <div class="bg-base-300 text-base-content mx-auto mt-10 lg:max-w-5xl">
<footer class="footer sm:footer-horizontal p-10"> <footer class="footer sm:footer-horizontal p-10">
{% if not public_mode %} {% if not public_mode %}
<nav> <nav>
<h6 class="footer-title">Services</h6> <h6 class="footer-title">Services</h6>
<a class="link link-hover" href="/">Predict</a> <a class="link link-hover" href="/predict">Predict</a>
<a class="link link-hover" href="/search">Search</a> <a class="link link-hover" href="/package">Packages</a>
<a class="link link-hover" href="/package">Browse</a>
{% if user.is_authenticated %} {% if user.is_authenticated %}
<a class="link link-hover" href="/model">Your Collections</a> <a class="link link-hover" href="/model">Your Collections</a>
{% endif %} {% endif %}
<a href="https://wiki.envipath.org/" target="_blank" class="link link-hover">Documentation</a> <a
href="https://wiki.envipath.org/"
target="_blank"
class="link link-hover"
>Documentation</a
>
</nav> </nav>
{% endif %} {% endif %}
<nav> <nav>
@ -29,39 +32,61 @@
<a class="link link-hover" href="/cite">Cite enviPath</a> <a class="link link-hover" href="/cite">Cite enviPath</a>
</nav> </nav>
</footer> </footer>
<footer class="footer border-neutral-300 border-t-2 px-10 py-4"> <footer class="footer border-t-2 border-neutral-300 px-10 py-4">
<div class="flex flex-row justify-between w-full items-start"> <div class="flex w-full flex-row items-start justify-between">
<aside class="grid-flow-col items-center"> <aside class="grid-flow-col items-center">
<svg class="fill-neutral-content flex-shrink-0 h-14 m-2" viewbox="0 0 65 65" > <svg
class="fill-neutral-content m-2 h-14 flex-shrink-0"
viewbox="0 0 65 65"
>
<use <use
href="{% static "/images/logo-square.svg" %}#ep-logo-square" href="{% static "/images/logo-square.svg" %}#ep-logo-square"
> ></use>
</use>
</svg> </svg>
enviPath Ltd. enviPath Ltd.
<br /> <br />
Biodegredation prediction since 2015. Biodegredation prediction since 2015.
</p>
</aside> </aside>
<aside class="text-sm text-base-200 mt-2"><span class="text-xs tracking-tight">Version</span> <span class="text-base font-bold">{{ meta.version }}</span></aside> <aside class="text-base-200 mt-2 text-sm">
<span class="text-xs tracking-tight">Version</span>
<span class="text-base font-bold">{{ meta.version }}</span>
</aside>
</div> </div>
<nav class="md:place-self-center md:justify-self-end"> <nav class="md:place-self-center md:justify-self-end">
<div class="grid grid-flow-col gap-4"> <div class="grid grid-flow-col gap-4">
<a href="https://www.youtube.com/@envipath7231" target="_blank"> <a href="https://www.youtube.com/@envipath7231" target="_blank">
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" class="w-6 h-6 fill-current"> <svg
role="img"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6 fill-current"
>
<title>YouTube</title> <title>YouTube</title>
<path d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z"/> <path
d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z"
/>
</svg> </svg>
</a> </a>
<a href="https://community.envipath.org/" target="_blank"> <a href="https://community.envipath.org/" target="_blank">
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" class="w-6 h-6 fill-current"> <svg
role="img"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6 fill-current"
>
<title>Discourse</title> <title>Discourse</title>
<path d="M12.103 0C18.666 0 24 5.485 24 11.997c0 6.51-5.33 11.99-11.9 11.99L0 24V11.79C0 5.28 5.532 0 12.103 0zm.116 4.563c-2.593-.003-4.996 1.352-6.337 3.57-1.33 2.208-1.387 4.957-.148 7.22L4.4 19.61l4.794-1.074c2.745 1.225 5.965.676 8.136-1.39 2.17-2.054 2.86-5.228 1.737-7.997-1.135-2.778-3.84-4.59-6.84-4.585h-.008z"/> <path
d="M12.103 0C18.666 0 24 5.485 24 11.997c0 6.51-5.33 11.99-11.9 11.99L0 24V11.79C0 5.28 5.532 0 12.103 0zm.116 4.563c-2.593-.003-4.996 1.352-6.337 3.57-1.33 2.208-1.387 4.957-.148 7.22L4.4 19.61l4.794-1.074c2.745 1.225 5.965.676 8.136-1.39 2.17-2.054 2.86-5.228 1.737-7.997-1.135-2.778-3.84-4.59-6.84-4.585h-.008z"
/>
</svg> </svg>
</a> </a>
<a href="https://www.linkedin.com/company/envipath/" target="_blank"> <a href="https://www.linkedin.com/company/envipath/" target="_blank">
<img src="{% static "/images/linkedin.png" %}" alt="LinkedIn" class="w-6 h-6"> <img
src="{% static "/images/linkedin.png" %}"
alt="LinkedIn"
class="h-6 w-6"
/>
</a> </a>
</div> </div>
</nav> </nav>

View File

@ -1,9 +1,9 @@
{% load static %} {% load static %}
{# Modern DaisyUI Navbar #} {# Modern DaisyUI Navbar #}
<div class="navbar bg-neutral-50 text-neutral-950 shadow-lg x-50"> <div class="navbar x-50 bg-neutral-50 text-neutral-950 shadow-lg">
<div class="navbar-start"> <div class="navbar-start">
<a href="{{ meta.server_url }}" class="btn btn-ghost normal-case text-xl"> <a href="{{ meta.server_url }}" class="btn btn-ghost text-xl normal-case">
<svg class="h-8 fill-base-content" viewBox="0 0 104 26" role="img"> <svg class="fill-base-content h-8" viewBox="0 0 104 26" role="img">
<use href="{% static "/images/logo-name.svg" %}#ep-logo-name" /> <use href="{% static "/images/logo-name.svg" %}#ep-logo-name" />
</svg> </svg>
</a> </a>
@ -26,6 +26,9 @@
tabindex="-1" tabindex="-1"
class="dropdown-content menu bg-base-100 rounded-box z-1 w-52 p-2 shadow-sm" class="dropdown-content menu bg-base-100 rounded-box z-1 w-52 p-2 shadow-sm"
> >
<li>
<a href="{{ meta.server_url }}/Package" id="packageLink">Package</a>
</li>
<li> <li>
<a href="{{ meta.server_url }}/pathway" id="pathwayLink">Pathway</a> <a href="{{ meta.server_url }}/pathway" id="pathwayLink">Pathway</a>
</li> </li>
@ -57,9 +60,9 @@
<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="badge badge-dash bg-base-200 text-base-content/50 m-1 flex items-center space-x-1 p-2"
> >
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@ -87,7 +90,7 @@
<div <div
tabindex="0" tabindex="0"
role="button" role="button"
class="btn btn-ghost m-1 btn-circle" class="btn btn-ghost btn-circle m-1"
id="loggedInButton" id="loggedInButton"
> >
<svg <svg

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>

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 -->
@ -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,9 +676,9 @@
} }
// 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);
} }
}); });

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