forked from enviPath/enviPy
This will hack the ketcher submission to work again (see #207). The problem seems to be that the iframe loads slower than the script tag so the reference is not available on page load. Registering from within the code to poll until ketcher is ready is a bit messy. Tracked the introduced dept in #212. Reviewed-on: enviPath/enviPy#213 Co-authored-by: Tobias O <tobias.olenyi@envipath.com> Co-committed-by: Tobias O <tobias.olenyi@envipath.com>
506 lines
19 KiB
HTML
506 lines
19 KiB
HTML
{% extends "framework_modern.html" %}
|
|
{% load static %}
|
|
{% block main_content %}
|
|
<!-- Hero Section with Logo and Search -->
|
|
<section class="hero relative mx-auto h-fit w-full max-w-5xl shadow-none">
|
|
<div
|
|
class="hero from-primary-800 to-primary-600 min-h-[calc(100vh*0.4)] bg-gradient-to-br"
|
|
style="background-image: url('{% static "/images/hero.png" %}'); background-size: cover; background-position: center;"
|
|
>
|
|
<div class="hero-overlay"></div>
|
|
<!-- Predict Pathway text over the image -->
|
|
<div class="absolute bottom-40 left-1/8 z-10 -translate-x-8">
|
|
<h2 class="text-base-100 text-left text-3xl text-shadow-lg">
|
|
Predict Your Pathway
|
|
</h2>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<div class="bg-base-200 mx-auto max-w-5xl shadow-md">
|
|
<!-- Predict Pathway Section -->
|
|
<div
|
|
class="relative z-20 mx-auto -mt-32 mb-10 w-full flex-col lg:flex-row-reverse"
|
|
>
|
|
<div
|
|
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="my-4 ml-8 flex h-fit flex-row items-center justify-start">
|
|
<div class="flex items-center gap-1">
|
|
<label class="swap btn btn-ghost btn-sm p-1" title="Input Mode">
|
|
<input type="checkbox" />
|
|
<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
|
|
aria-label="smiles mode"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
viewBox="0 0 20 20"
|
|
class="size-5"
|
|
>
|
|
<g
|
|
stroke-linejoin="round"
|
|
stroke-linecap="round"
|
|
stroke-width="2"
|
|
fill="currentColor"
|
|
stroke="none"
|
|
>
|
|
<path
|
|
fill-rule="evenodd"
|
|
d="M8 2.75A.75.75 0 0 1 8.75 2h7.5a.75.75 0 0 1 0 1.5h-3.215l-4.483 13h2.698a.75.75 0 0 1 0 1.5h-7.5a.75.75 0 0 1 0-1.5h3.215l4.483-13H8.75A.75.75 0 0 1 8 2.75Z"
|
|
clip-rule="evenodd"
|
|
/>
|
|
</g>
|
|
</svg>
|
|
</div>
|
|
<span class="ext-xs">SMILES</span>
|
|
</span>
|
|
<span class="swap-off flex items-center gap-1">
|
|
<div
|
|
class="bg-neutral/50 text-neutral-content flex items-center justify-center rounded-full p-1"
|
|
>
|
|
<svg
|
|
aria-label="draw mode"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
viewBox="0 0 20 20"
|
|
fill="currentColor"
|
|
stroke="none"
|
|
class="size-5"
|
|
>
|
|
<path
|
|
d="m2.695 14.762-1.262 3.155a.5.5 0 0 0 .65.65l3.155-1.262a4 4 0 0 0 1.343-.886L17.5 5.501a2.121 2.121 0 0 0-3-3L3.58 13.419a4 4 0 0 0-.885 1.343Z"
|
|
/>
|
|
</svg>
|
|
</div>
|
|
<span class="text-base/50 text-xs">Draw</span>
|
|
</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<fieldset
|
|
class="fieldset overflow-hidden transition-all duration-300 ease-in-out"
|
|
>
|
|
<form
|
|
id="index-form"
|
|
action="{{ meta.current_package.url }}/pathway"
|
|
method="POST"
|
|
>
|
|
{% csrf_token %}
|
|
<div
|
|
id="text-input-container"
|
|
class="scale-100 transform opacity-100 transition-all duration-300 ease-in-out"
|
|
>
|
|
<div class="join mx-auto w-full">
|
|
<input
|
|
type="text"
|
|
id="index-form-text-input"
|
|
placeholder="canonical SMILES string"
|
|
class="input input-md join-item grow"
|
|
/>
|
|
<button class="btn btn-neutral join-item">Predict!</button>
|
|
</div>
|
|
<div class="label relative mt-1 w-full">
|
|
<div class="flex gap-2">
|
|
<a
|
|
href="#"
|
|
class="example-link hover:text-primary cursor-pointer"
|
|
data-smiles="CN1C=NC2=C1C(=O)N(C(=O)N2C)C"
|
|
title="load example"
|
|
>Caffeine</a
|
|
>
|
|
<a
|
|
href="#"
|
|
class="example-link hover:text-primary cursor-pointer"
|
|
data-smiles="CC(C)CC1=CC=C(C=C1)C(C)C(=O)O"
|
|
title="load example"
|
|
>Ibuprofen</a
|
|
>
|
|
</div>
|
|
<a
|
|
class="absolute top-0 left-[calc(100%-5.4rem)]"
|
|
href="/predict"
|
|
>Advanced</a
|
|
>
|
|
</div>
|
|
</div>
|
|
<div
|
|
id="ketcher-container"
|
|
class="hidden w-full scale-95 transform opacity-0 transition-all duration-300 ease-in-out"
|
|
>
|
|
<iframe
|
|
id="index-ketcher"
|
|
src="{% static '/js/ketcher2/ketcher.html' %}"
|
|
width="100%"
|
|
height="400"
|
|
class="rounded-lg"
|
|
></iframe>
|
|
<button
|
|
class="btn btn-lg bg-primary-950 text-primary-50 join-item mt-2 w-full"
|
|
>
|
|
Predict!
|
|
</button>
|
|
<div class="mt-1 flex w-full justify-end">
|
|
<a class="label justify-end" href="/predict">Advanced</a>
|
|
</div>
|
|
</div>
|
|
<input
|
|
type="hidden"
|
|
id="index-form-smiles"
|
|
name="smiles"
|
|
value="smiles"
|
|
/>
|
|
<input
|
|
type="hidden"
|
|
id="index-form-predict"
|
|
name="predict"
|
|
value="predict"
|
|
/>
|
|
<input type="hidden" id="current-action" value="predict" />
|
|
</form>
|
|
</fieldset>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Community News Section -->
|
|
<section class="bg-base-200 z-10 mx-8 py-16">
|
|
<div class="mx-auto max-w-7xl px-4">
|
|
<h2 class="h2 mb-8 text-left font-bold">Community Updates</h2>
|
|
|
|
<div id="community-news-container" class="flex justify-center gap-4">
|
|
<!-- News cards will be populated here -->
|
|
<div id="loading" class="flex w-full justify-center">
|
|
<span class="loading loading-spinner loading-lg"></span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mt-6 text-right">
|
|
<a
|
|
href="https://community.envipath.org/c/announcements/10"
|
|
target="_blank"
|
|
class="btn btn-ghost btn-sm"
|
|
>
|
|
Read More Announcements
|
|
</a>
|
|
</div>
|
|
|
|
<!-- Discourse API integration -->
|
|
<script src="{% static 'js/discourse-api.js' %}"></script>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Mission Statement Section -->
|
|
<section class="from-base-200 to-base-100 bg-gradient-to-b py-16">
|
|
<div class="mx-auto px-8 md:px-12">
|
|
<div class="flex flex-row gap-4">
|
|
<div class="w-1/3">
|
|
<img
|
|
src="{% static "/images/ep-rule-artwork.png" %}"
|
|
alt="rule-based iterative tree greneration"
|
|
class="h-full w-full object-contain"
|
|
/>
|
|
</div>
|
|
<div class="mr-8 w-2/3 space-y-4 text-left">
|
|
<h2 class="h2 mb-8 font-bold">About enviPath</h2>
|
|
<p class="">
|
|
enviPath is a database and prediction system for the microbial
|
|
biotransformation of organic environmental contaminants. The
|
|
database provides the possibility to store and view experimentally
|
|
observed biotransformation pathways.
|
|
</p>
|
|
<p class="">
|
|
The pathway prediction system provides different relative
|
|
reasoning models to predict likely biotransformation pathways and
|
|
products. Explore our tools and contribute to advancing
|
|
environmental biotransformation research.
|
|
</p>
|
|
<div class="float-right flex flex-row gap-4">
|
|
<a href="/about" class="btn btn-ghost-neutral">Read More</a>
|
|
<a href="/about" class="btn btn-neutral">Publications</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Partners Section -->
|
|
<section class="bg-base-100 py-14 sm:py-12">
|
|
<div class="mx-auto px-6 lg:px-8">
|
|
<div class="divider">
|
|
<h2 class="text-center text-lg/8 font-semibold">Backed by Science</h2>
|
|
</div>
|
|
<div
|
|
class="mx-auto mt-10 grid max-w-lg grid-cols-4 items-center gap-x-8 gap-y-10 sm:max-w-xl sm:grid-cols-6 sm:gap-x-10 lg:mx-0 lg:max-w-none lg:grid-cols-3"
|
|
>
|
|
<img
|
|
src="{% static "/images/uoa-logo-small.png" %}"
|
|
alt="The University of Auckland"
|
|
class="max-h-20 w-full object-contain lg:col-span-1"
|
|
/>
|
|
<img
|
|
src="{% static "/images/logo-eawag.svg" %}"
|
|
alt="Eawag"
|
|
class="max-h-12 w-full object-contain lg:col-span-1"
|
|
/>
|
|
<img
|
|
src="{% static "/images/uzh-logo.svg" %}"
|
|
alt="University of Zurich"
|
|
class="2 max-h-16 w-full object-contain lg:col-span-1"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
|
|
<script language="javascript">
|
|
var currentPackage = "{{ meta.current_package.url }}";
|
|
|
|
// Helper function to safely get Ketcher instance from iframe
|
|
function getKetcherInstance(iframeId) {
|
|
const ketcherFrame = document.getElementById(iframeId);
|
|
if (!ketcherFrame) {
|
|
console.error("Ketcher iframe not found:", iframeId);
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
if (
|
|
"contentWindow" in ketcherFrame &&
|
|
ketcherFrame.contentWindow.ketcher
|
|
) {
|
|
return ketcherFrame.contentWindow.ketcher;
|
|
}
|
|
} catch (err) {
|
|
console.error(
|
|
"Cannot access Ketcher iframe - possible CORS issue:",
|
|
err,
|
|
);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
// Discourse API integration is now handled by discourse-api.js
|
|
|
|
// Function to render Discourse topics into cards
|
|
function renderDiscourseTopics(topics) {
|
|
const container = document.getElementById("community-news-container");
|
|
if (!container) return;
|
|
|
|
// Clear container
|
|
container.innerHTML = "";
|
|
|
|
// Create cards for each topic
|
|
topics.forEach((topic) => {
|
|
const card = createDiscourseCard(topic);
|
|
container.insertAdjacentHTML("beforeend", card);
|
|
});
|
|
}
|
|
|
|
// Function to create HTML card for a topic
|
|
function createDiscourseCard(topic) {
|
|
const date = new Date(topic.created_at).toLocaleDateString();
|
|
|
|
return `
|
|
<div class="card bg-white shadow-xs hover:shadow-lg transition-shadow duration-300 h-64 w-75 flex-shrink-0">
|
|
<div class="card-body flex flex-col h-full">
|
|
<h3 class="card-title leading-tight font-normal tracking-tight h-12 mb-2 line-clamp-2 text-ellipsis wrap-break-word overflow-hidden">
|
|
<a href="${topic.url}" target="_blank" class="hover:text-primary">
|
|
${topic.title}
|
|
</a>
|
|
</h3>
|
|
<div class="text-sm line-clamp-4 break-words" >
|
|
${topic.excerpt}
|
|
</div>
|
|
|
|
<div class="flex flex-row items-center justify-between">
|
|
<div class="flex items-center gap-2">
|
|
<div class="avatar tooltip tooltip-right" data-tip="${topic.author}">
|
|
<div class="w-8 rounded-full">
|
|
<img src="${topic.author_avatar}" alt="${topic.author}" />
|
|
</div>
|
|
</div>
|
|
<span class="text-xs text-gray-500">${date}</span>
|
|
</div>
|
|
<a href="${topic.url}" target="_blank" class="btn btn-ghost text-neutral-500 rounded-full p-2">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M12 7v14"/>
|
|
<path d="M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z"/>
|
|
</svg>
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// Make render function globally available
|
|
window.renderDiscourseTopics = renderDiscourseTopics;
|
|
|
|
// Toggle functionality with smooth animations
|
|
function toggleInputMode() {
|
|
const toggle = $('input[type="checkbox"]');
|
|
const textContainer = $("#text-input-container");
|
|
const ketcherContainer = $("#ketcher-container");
|
|
const formCard = $(".card");
|
|
const fieldset = $(".fieldset");
|
|
|
|
if (toggle.is(":checked")) {
|
|
// Draw mode - show Ketcher, hide text input
|
|
textContainer.addClass("opacity-0 transform scale-95");
|
|
textContainer.removeClass("opacity-100 transform scale-100");
|
|
|
|
// Adjust fieldset padding for Ketcher mode - reduce padding and make more compact
|
|
fieldset.removeClass("p-8");
|
|
fieldset.addClass("p-4");
|
|
|
|
// Wait for fade out to complete, then hide and show new content
|
|
setTimeout(() => {
|
|
textContainer.addClass("hidden");
|
|
ketcherContainer.removeClass("hidden opacity-0 transform scale-95");
|
|
ketcherContainer.addClass("opacity-100 transform scale-100");
|
|
|
|
// Force re-evaluation of iframe size
|
|
const iframe = document.getElementById("index-ketcher");
|
|
if (iframe) {
|
|
iframe.style.height = "400px";
|
|
}
|
|
}, 300);
|
|
} else {
|
|
// SMILES mode - show text input, hide Ketcher
|
|
ketcherContainer.addClass("opacity-0 transform scale-95");
|
|
ketcherContainer.removeClass("opacity-100 transform scale-100");
|
|
|
|
// Restore fieldset padding for text input mode
|
|
fieldset.removeClass("p-4");
|
|
fieldset.addClass("p-8");
|
|
|
|
// Wait for fade out to complete, then hide and show new content
|
|
setTimeout(() => {
|
|
ketcherContainer.addClass("hidden");
|
|
textContainer.removeClass("hidden opacity-0 transform scale-95");
|
|
textContainer.addClass("opacity-100 transform scale-100");
|
|
}, 300);
|
|
|
|
// Transfer SMILES from Ketcher to text input if available
|
|
const ketcher = getKetcherInstance("index-ketcher");
|
|
if (ketcher && ketcher.getSmiles) {
|
|
try {
|
|
const smiles = ketcher.getSmiles();
|
|
if (smiles && smiles.trim() !== "") {
|
|
$("#index-form-text-input").val(smiles);
|
|
}
|
|
} catch (err) {
|
|
console.error("Failed to sync Ketcher to text input:", err);
|
|
// Non-critical error, just log it
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Ketcher integration
|
|
function indexKetcherToTextInput() {
|
|
$("#index-form-smiles").val(this.ketcher.getSmiles());
|
|
}
|
|
|
|
$(function () {
|
|
// Initialize fieldset with proper padding
|
|
$(".fieldset").addClass("p-8");
|
|
|
|
// Toggle event listener
|
|
$('input[type="checkbox"]').on("change", toggleInputMode);
|
|
|
|
// Ketcher iframe load handler
|
|
$("#index-ketcher").on("load", function () {
|
|
const checkKetcherReady = () => {
|
|
const win = this.contentWindow;
|
|
if (win.ketcher && "editor" in win.ketcher) {
|
|
window.indexKetcher = win.ketcher;
|
|
win.ketcher.editor.event.change.handlers.push({
|
|
once: false,
|
|
priority: 0,
|
|
f: indexKetcherToTextInput,
|
|
ketcher: win.ketcher,
|
|
});
|
|
} else {
|
|
setTimeout(checkKetcherReady, 100);
|
|
}
|
|
};
|
|
checkKetcherReady();
|
|
});
|
|
|
|
// Handle example link clicks
|
|
$(".example-link").on("click", function (e) {
|
|
e.preventDefault();
|
|
const smiles = $(this).data("smiles");
|
|
const title = $(this).attr("title");
|
|
|
|
// Check if we're in Ketcher mode or text input mode
|
|
if ($('input[type="checkbox"]').is(":checked")) {
|
|
// In Ketcher mode - set the SMILES in Ketcher
|
|
if (window.indexKetcher && window.indexKetcher.setMolecule) {
|
|
window.indexKetcher.setMolecule(smiles);
|
|
}
|
|
} else {
|
|
// In text input mode - set the SMILES in the text input
|
|
$("#index-form-text-input").val(smiles);
|
|
}
|
|
|
|
// Show a brief feedback
|
|
const originalText = $(this).text();
|
|
$(this).text(`loaded!`);
|
|
setTimeout(() => {
|
|
$(this).text(originalText);
|
|
}, 1000);
|
|
});
|
|
|
|
// Handle form submission on Enter
|
|
$("#index-form").on("submit", function (e) {
|
|
e.preventDefault();
|
|
|
|
var textSmiles = "";
|
|
|
|
// Check if we're in Ketcher mode and extract SMILES
|
|
if ($('input[type="checkbox"]').is(":checked")) {
|
|
// Use the robust getter function
|
|
const ketcher = getKetcherInstance("index-ketcher");
|
|
if (ketcher && ketcher.getSmiles) {
|
|
try {
|
|
textSmiles = ketcher.getSmiles().trim();
|
|
} catch (err) {
|
|
console.error("Failed to get SMILES from Ketcher:", err);
|
|
alert(
|
|
"Unable to extract structure from the drawing editor. Please try again or switch to SMILES input mode.",
|
|
);
|
|
return;
|
|
}
|
|
} else {
|
|
console.warn("Ketcher not available, possibly still loading");
|
|
alert(
|
|
"The drawing editor is still loading. Please wait a moment and try again.",
|
|
);
|
|
return;
|
|
}
|
|
} else {
|
|
textSmiles = $("#index-form-text-input").val().trim();
|
|
}
|
|
|
|
if (textSmiles === "") {
|
|
alert("Please enter a SMILES string or draw a structure.");
|
|
return;
|
|
}
|
|
|
|
$("#index-form-smiles").val(textSmiles);
|
|
$("#index-form").attr("action", currentPackage + "/pathway");
|
|
$("#index-form").attr("method", "POST");
|
|
this.submit();
|
|
});
|
|
|
|
// Discourse topics are now loaded automatically by discourse-api.js
|
|
});
|
|
</script>
|
|
{% endblock main_content %}
|