[Fix] UI Fixes (#266)

Rather than have a bunch of pull-requests that @jebus will have to merge shall we collect some of the fixes for the UI issues I found in here.

- [x] #259
- [x] #260
- [x] #261
- [x] #262
- [x] #263
- [x] #264
- [x] #265

Co-authored-by: Tobias O <tobias.olenyi@envipath.com>
Reviewed-on: enviPath/enviPy#266
Co-authored-by: Liam Brydon <lbry121@aucklanduni.ac.nz>
Co-committed-by: Liam Brydon <lbry121@aucklanduni.ac.nz>
This commit is contained in:
2025-12-15 21:28:43 +13:00
committed by jebus
parent 8adb93012a
commit 4bf20e62ef
15 changed files with 279 additions and 170 deletions

View File

@ -34,23 +34,3 @@
} }
@import "./daisyui-theme.css"; @import "./daisyui-theme.css";
/* Loading Spinner - Benzene Ring */
.benzene-spinner {
display: flex;
justify-content: center;
align-items: center;
padding: 2rem;
}
.benzene-spinner svg {
width: 48px;
height: 48px;
animation: spin 3s linear infinite;
}
@keyframes spin {
100% {
transform: rotate(360deg);
}
}

View File

@ -44,6 +44,7 @@ document.addEventListener('alpine:init', () => {
this.isLoading = true; this.isLoading = true;
this.error = null; this.error = null;
this.$dispatch('loading-start');
try { try {
const url = new URL(this.endpoint, window.location.origin); const url = new URL(this.endpoint, window.location.origin);
@ -74,6 +75,7 @@ document.addEventListener('alpine:init', () => {
this.error = `Unable to load ${this.endpoint}. Please try again.`; this.error = `Unable to load ${this.endpoint}. Please try again.`;
} finally { } finally {
this.isLoading = false; this.isLoading = false;
this.$dispatch('loading-end');
} }
}, },

View File

@ -0,0 +1,88 @@
/**
* Pathway Viewer Alpine.js Component
*
* Provides reactive status management and polling for pathway predictions.
* Handles status updates, change detection, and update notices.
*/
document.addEventListener('alpine:init', () => {
/**
* Pathway Viewer Component
*
* Usage:
* <div x-data="pathwayViewer({
* status: 'running',
* modified: '2024-01-01T00:00:00Z',
* statusUrl: '/pathway/123?status=true'
* })" x-init="init()">
* ...
* </div>
*/
Alpine.data('pathwayViewer', (config) => ({
status: config.status,
modified: config.modified,
statusUrl: config.statusUrl,
showUpdateNotice: false,
updateMessage: '',
pollInterval: null,
get statusTooltip() {
const tooltips = {
'completed': 'Pathway prediction complete.',
'failed': 'Pathway prediction failed.',
'running': 'Pathway prediction running.'
};
return tooltips[this.status] || '';
},
init() {
if (this.status === 'running') {
this.startPolling();
}
},
startPolling() {
this.pollInterval = setInterval(() => this.checkStatus(), 5000);
},
async checkStatus() {
try {
const response = await fetch(this.statusUrl);
const data = await response.json();
if (data.modified > this.modified) {
this.showUpdateNotice = true;
this.updateMessage = this.getUpdateMessage(data.status);
}
if (data.status !== 'running') {
this.status = data.status;
if (this.pollInterval) {
clearInterval(this.pollInterval);
this.pollInterval = null;
}
}
} catch (err) {
console.error('Polling error:', err);
}
},
getUpdateMessage(status) {
let msg = 'Prediction ';
if (status === 'running') {
msg += 'is still running. But the Pathway was updated.';
} else if (status === 'completed') {
msg += 'is completed. Reload the page to see the updated Pathway.';
} else if (status === 'failed') {
msg += 'failed. Reload the page to see the current shape.';
}
return msg;
},
reloadPage() {
location.reload();
}
}));
});

View File

@ -2,7 +2,12 @@
{# Variables: empty_text (string), show_review_badge (bool), always_show_badge (bool) #} {# Variables: empty_text (string), show_review_badge (bool), always_show_badge (bool) #}
{# Loading state #} {# Loading state #}
<div x-show="isLoading">{% include "components/loading-spinner.html" %}</div> <div
x-show="isLoading"
class="mx-auto flex h-32 w-32 items-center justify-center"
>
{% include "components/loading-spinner.html" %}
</div>
{# Error state #} {# Error state #}
<div <div

View File

@ -45,43 +45,36 @@
class="mt-6 w-full" class="mt-6 w-full"
x-data="{ x-data="{
activeTab: 'reviewed', activeTab: 'reviewed',
reviewedCount: 0, reviewedCount: null,
unreviewedCount: 0, unreviewedCount: null,
reviewedLoaded: false, get bothLoaded() { return this.reviewedCount !== null && this.unreviewedCount !== null },
unreviewedLoaded: false, get isEmpty() { return this.bothLoaded && this.reviewedCount === 0 && this.unreviewedCount === 0 },
updateTabSelection() { updateTabSelection() {
// Only auto-select unreviewed tab if both have loaded and there are no reviewed items if (this.bothLoaded && this.reviewedCount === 0 && this.unreviewedCount > 0) {
if (this.reviewedLoaded && this.unreviewedLoaded && this.reviewedCount === 0 && this.unreviewedCount > 0) {
this.activeTab = 'unreviewed'; this.activeTab = 'unreviewed';
} }
} }
}" }"
> >
{# No items found message #} {# No items found message - only show after both tabs have loaded #}
<div <div x-show="isEmpty" class="text-base-content/70 py-8 text-center">
x-show="reviewedCount === 0 && unreviewedCount === 0"
class="text-base-content/70 py-8 text-center"
>
<p>No items found.</p> <p>No items found.</p>
</div> </div>
{# Tabs Navigation #} {# Tabs Navigation #}
<div <div role="tablist" class="tabs tabs-border" x-show="!isEmpty">
role="tablist"
class="tabs tabs-border"
x-show="reviewedCount > 0 || unreviewedCount > 0"
>
<button <button
role="tab" role="tab"
class="tab" class="tab"
:class="{ 'tab-active': activeTab === 'reviewed' }" :class="{ 'tab-active': activeTab === 'reviewed' }"
@click="activeTab = 'reviewed'" @click="activeTab = 'reviewed'"
x-show="reviewedCount > 0" x-show="reviewedCount === null || reviewedCount > 0"
> >
Reviewed Reviewed
<span <span
class="badge badge-xs badge-dash badge-info mb-2 ml-2" class="badge badge-xs badge-dash badge-info mb-2 ml-2"
x-text="reviewedCount" :class="{ 'animate-pulse': reviewedCount === null }"
x-text="reviewedCount ?? '…'"
></span> ></span>
</button> </button>
<button <button
@ -89,12 +82,13 @@
class="tab" class="tab"
:class="{ 'tab-active': activeTab === 'unreviewed' }" :class="{ 'tab-active': activeTab === 'unreviewed' }"
@click="activeTab = 'unreviewed'" @click="activeTab = 'unreviewed'"
x-show="unreviewedCount > 0" x-show="unreviewedCount === null || unreviewedCount > 0"
> >
Unreviewed Unreviewed
<span <span
class="badge badge-xs badge-dash badge-info mb-2 ml-2" class="badge badge-xs badge-dash badge-info mb-2 ml-2"
x-text="unreviewedCount" :class="{ 'animate-pulse': unreviewedCount === null }"
x-text="unreviewedCount ?? '…'"
></span> ></span>
</button> </button>
</div> </div>
@ -102,14 +96,14 @@
{# Reviewed Tab Content #} {# Reviewed Tab Content #}
<div <div
class="mt-6" class="mt-6"
x-show="activeTab === 'reviewed' && (reviewedCount > 0 || unreviewedCount > 0)" x-show="activeTab === 'reviewed' && !isEmpty"
x-data="remotePaginatedList({ x-data="remotePaginatedList({
endpoint: '{{ api_endpoint }}?review_status=true', endpoint: '{{ api_endpoint }}?review_status=true',
instanceId: '{{ entity_type }}_reviewed', instanceId: '{{ entity_type }}_reviewed',
isReviewed: true, isReviewed: true,
perPage: {{ per_page|default:50 }} perPage: {{ per_page|default:50 }}
})" })"
@items-loaded="reviewedCount = totalItems; reviewedLoaded = true; updateTabSelection()" @items-loaded="reviewedCount = totalItems; updateTabSelection()"
> >
{% include "collections/_paginated_list_partial.html" with empty_text="reviewed "|add:list_title|default:"items" show_review_badge=True always_show_badge=True %} {% include "collections/_paginated_list_partial.html" with empty_text="reviewed "|add:list_title|default:"items" show_review_badge=True always_show_badge=True %}
</div> </div>
@ -117,14 +111,14 @@
{# Unreviewed Tab Content #} {# Unreviewed Tab Content #}
<div <div
class="mt-6" class="mt-6"
x-show="activeTab === 'unreviewed' && (reviewedCount > 0 || unreviewedCount > 0)" x-show="activeTab === 'unreviewed' && !isEmpty"
x-data="remotePaginatedList({ x-data="remotePaginatedList({
endpoint: '{{ api_endpoint }}?review_status=false', endpoint: '{{ api_endpoint }}?review_status=false',
instanceId: '{{ entity_type }}_unreviewed', instanceId: '{{ entity_type }}_unreviewed',
isReviewed: false, isReviewed: false,
perPage: {{ per_page|default:50 }} perPage: {{ per_page|default:50 }}
})" })"
@items-loaded="unreviewedCount = totalItems; unreviewedLoaded = true; updateTabSelection()" @items-loaded="unreviewedCount = totalItems; updateTabSelection()"
> >
{% include "collections/_paginated_list_partial.html" with empty_text="unreviewed "|add:list_title|default:"items" %} {% include "collections/_paginated_list_partial.html" with empty_text="unreviewed "|add:list_title|default:"items" %}
</div> </div>

View File

@ -6,9 +6,9 @@
<h6 class="footer-title">Services</h6> <h6 class="footer-title">Services</h6>
<a class="link link-hover" href="/predict">Predict</a> <a class="link link-hover" href="/predict">Predict</a>
<a class="link link-hover" href="/package">Packages</a> <a class="link link-hover" href="/package">Packages</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 <a
href="https://wiki.envipath.org/" href="https://wiki.envipath.org/"
target="_blank" target="_blank"

View File

@ -1,5 +1,22 @@
<div class="benzene-spinner"> <style>
<svg viewBox="0 0 1000 1000" xmlns="http://www.w3.org/2000/svg"> @keyframes spin-slow {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.spinner-slow svg {
animation: spin-slow 3s linear infinite;
}
</style>
<div class="spinner-slow flex h-full w-full items-center justify-center">
<svg
viewBox="0 0 1000 1000"
xmlns="http://www.w3.org/2000/svg"
class="h-full w-full"
>
<path <path
class="hexagon" class="hexagon"
d="m 758.78924,684.71562 0.65313,-363.85 33.725,0.066 -0.65313,363.85001 z M 201.52187,362.53368 512.50834,173.66181 530.01077,202.48506 219.03091,391.35694 z M 510.83924,841.63056 199.3448,653.59653 216.77465,624.72049 528.2691,812.76111 z M 500,975 85.905556,742.30278 l 0,-474.94722 L 500,24.999998 914.09445,257.64444 l 0,475.00001 z M 124.90833,722.45834 500,936.15556 880.26389,713.69722 l 0,-436.15555 L 500,63.949998 124.90833,286.40833 z" d="m 758.78924,684.71562 0.65313,-363.85 33.725,0.066 -0.65313,363.85001 z M 201.52187,362.53368 512.50834,173.66181 530.01077,202.48506 219.03091,391.35694 z M 510.83924,841.63056 199.3448,653.59653 216.77465,624.72049 528.2691,812.76111 z M 500,975 85.905556,742.30278 l 0,-474.94722 L 500,24.999998 914.09445,257.64444 l 0,475.00001 z M 124.90833,722.45834 500,936.15556 880.26389,713.69722 l 0,-436.15555 L 500,63.949998 124.90833,286.40833 z"

View File

@ -118,7 +118,7 @@
</div> </div>
</a> </a>
{% endif %} {% endif %}
{% if meta.user.username == 'anonymous' or public_mode %} {% if meta.user.username == 'anonymous' %}
<a href="{% url 'login' %}" id="loginButton" class="p-2">Login</a> <a href="{% url 'login' %}" id="loginButton" class="p-2">Login</a>
{% else %} {% else %}
<div class="dropdown dropdown-end"> <div class="dropdown dropdown-end">

View File

@ -29,6 +29,7 @@
<script src="{% static 'js/alpine/index.js' %}"></script> <script src="{% static 'js/alpine/index.js' %}"></script>
<script src="{% static 'js/alpine/search.js' %}"></script> <script src="{% static 'js/alpine/search.js' %}"></script>
<script src="{% static 'js/alpine/pagination.js' %}"></script> <script src="{% static 'js/alpine/pagination.js' %}"></script>
<script src="{% static 'js/alpine/pathway.js' %}"></script>
{# Font Awesome #} {# Font Awesome #}
<link <link

View File

@ -16,12 +16,12 @@
> >
<div class="modal-box max-w-3xl"> <div class="modal-box max-w-3xl">
<!-- Header --> <!-- Header -->
<h3 class="font-bold text-lg">New Scenario</h3> <h3 class="text-lg font-bold">New Scenario</h3>
<!-- Close button (X) --> <!-- Close button (X) -->
<form method="dialog"> <form method="dialog">
<button <button
class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2" class="btn btn-sm btn-circle btn-ghost absolute top-2 right-2"
:disabled="isSubmitting" :disabled="isSubmitting"
> >
@ -114,20 +114,37 @@
</div> </div>
<div class="form-control mb-3"> <div class="form-control mb-3">
<label class="label" for="scenario-type"> <label class="label">
<span class="label-text">Scenario Type</span> <span class="label-text">Scenario Type</span>
</label> </label>
<select <div role="tablist" class="tabs tabs-border">
<button
type="button"
role="tab"
class="tab"
:class="{ 'tab-active': scenarioType === 'empty' }"
@click="scenarioType = 'empty'"
>
Empty Scenario
</button>
{% for k, v in scenario_types.items %}
<button
type="button"
role="tab"
class="tab"
:class="{ 'tab-active': scenarioType === '{{ v.name }}' }"
@click="scenarioType = '{{ v.name }}'"
>
{{ k }}
</button>
{% endfor %}
</div>
<input
type="hidden"
id="scenario-type" id="scenario-type"
name="scenario-type" name="scenario-type"
class="select select-bordered w-full"
x-model="scenarioType" x-model="scenarioType"
> />
<option value="empty" selected>Empty Scenario</option>
{% for k, v in scenario_types.items %}
<option value="{{ v.name }}">{{ k }}</option>
{% endfor %}
</select>
</div> </div>
{% for type in scenario_types.values %} {% for type in scenario_types.values %}

View File

@ -18,7 +18,11 @@
this.isLoading = true; this.isLoading = true;
try { try {
const response = await fetch('{% url "package scenario list" meta.current_package.uuid %}'); const response = await fetch('{% url "package scenario list" meta.current_package.uuid %}', {
headers: {
'Accept': 'application/json'
}
});
const data = await response.json(); const data = await response.json();
this.scenarios = data; this.scenarios = data;
this.loaded = true; this.loaded = true;
@ -47,7 +51,13 @@
} }
}" }"
@close="reset()" @close="reset()"
x-init="$watch('$el.open', value => { if (value) loadScenarios(); })" x-init="
new MutationObserver(() => {
if ($el.hasAttribute('open')) {
loadScenarios();
}
}).observe($el, { attributes: true });
"
> >
<div class="modal-box max-w-4xl"> <div class="modal-box max-w-4xl">
<!-- Header --> <!-- Header -->
@ -102,7 +112,8 @@
</select> </select>
<label class="label"> <label class="label">
<span class="label-text-alt" <span class="label-text-alt"
>Hold Ctrl/Cmd to select multiple scenarios</span >Hold Ctrl/Cmd to select multiple scenarios. Ctrl/Cmd + click one
item to deselect it</span
> >
</label> </label>
</div> </div>

View File

@ -136,7 +136,11 @@
</button> </button>
</div> </div>
</div> </div>
<div id="predictLoading" class="mt-2"></div> <div id="predictLoading" class="mt-2 flex hidden justify-center">
<div class="h-8 w-8">
{% include "components/loading-spinner.html" %}
</div>
</div>
<div id="predictResultTable" class="mt-4"></div> <div id="predictResultTable" class="mt-4"></div>
</div> </div>
</div> </div>
@ -167,7 +171,11 @@
</button> </button>
</div> </div>
</div> </div>
<div id="appDomainLoading" class="mt-2"></div> <div id="appDomainLoading" class="mt-2 flex hidden justify-center">
<div class="h-8 w-8">
{% include "components/loading-spinner.html" %}
</div>
</div>
<div id="appDomainAssessmentResultTable" class="mt-4"></div> <div id="appDomainAssessmentResultTable" class="mt-4"></div>
</div> </div>
</div> </div>
@ -397,7 +405,8 @@
return; return;
} }
makeLoadingGif("#predictLoading", "{% static '/images/wait.gif' %}"); const loadingEl = document.getElementById("predictLoading");
if (loadingEl) loadingEl.classList.remove("hidden");
const params = new URLSearchParams({ const params = new URLSearchParams({
smiles: smiles, smiles: smiles,
@ -418,12 +427,12 @@
}) })
.then(data => { .then(data => {
const loadingEl = document.getElementById("predictLoading"); const loadingEl = document.getElementById("predictLoading");
if (loadingEl) loadingEl.innerHTML = ""; if (loadingEl) loadingEl.classList.add("hidden");
handlePredictionResponse(data); handlePredictionResponse(data);
}) })
.catch(error => { .catch(error => {
const loadingEl = document.getElementById("predictLoading"); const loadingEl = document.getElementById("predictLoading");
if (loadingEl) loadingEl.innerHTML = ""; if (loadingEl) loadingEl.classList.add("hidden");
const resultTable = document.getElementById("predictResultTable"); const resultTable = document.getElementById("predictResultTable");
if (resultTable) { if (resultTable) {
resultTable.classList.add("alert", "alert-error"); resultTable.classList.add("alert", "alert-error");
@ -453,7 +462,8 @@
return; return;
} }
makeLoadingGif("#appDomainLoading", "{% static '/images/wait.gif' %}"); const loadingEl = document.getElementById("appDomainLoading");
if (loadingEl) loadingEl.classList.remove("hidden");
const params = new URLSearchParams({ const params = new URLSearchParams({
smiles: smiles, smiles: smiles,
@ -474,7 +484,7 @@
}) })
.then(data => { .then(data => {
const loadingEl = document.getElementById("appDomainLoading"); const loadingEl = document.getElementById("appDomainLoading");
if (loadingEl) loadingEl.innerHTML = ""; if (loadingEl) loadingEl.classList.add("hidden");
if (typeof handleAssessmentResponse === 'function') { if (typeof handleAssessmentResponse === 'function') {
handleAssessmentResponse("{% url 'depict' %}", data); handleAssessmentResponse("{% url 'depict' %}", data);
} }
@ -482,7 +492,7 @@
}) })
.catch(error => { .catch(error => {
const loadingEl = document.getElementById("appDomainLoading"); const loadingEl = document.getElementById("appDomainLoading");
if (loadingEl) loadingEl.innerHTML = ""; if (loadingEl) loadingEl.classList.add("hidden");
const resultTable = document.getElementById("appDomainAssessmentResultTable"); const resultTable = document.getElementById("appDomainAssessmentResultTable");
if (resultTable) { if (resultTable) {
resultTable.classList.add("alert", "alert-error"); resultTable.classList.add("alert", "alert-error");

View File

@ -211,11 +211,21 @@
</div> </div>
</div> </div>
</div> </div>
<div id="vizdiv"> <div
{% if pathway.completed %} id="vizdiv"
<div class="tooltip tooltip-bottom absolute top-4 right-4 z-10"> x-data="pathwayViewer({
<div class="tooltip-content">Pathway prediction complete.</div> status: '{{ pathway.status }}',
<div id="status" class="flex items-center"> modified: '{{ pathway.modified|date:"Y-m-d H:i:s" }}',
statusUrl: '{{ pathway.url }}?status=true'
})"
x-init="init()"
>
<!-- Status Display -->
<div class="tooltip tooltip-left absolute top-4 right-4 z-10">
<div class="tooltip-content" x-text="statusTooltip"></div>
<div id="status" class="flex items-center">
<!-- Completed icon -->
<template x-if="status === 'completed'">
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
width="16" width="16"
@ -230,12 +240,9 @@
> >
<path d="M20 6 9 17l-5-5" /> <path d="M20 6 9 17l-5-5" />
</svg> </svg>
</div> </template>
</div> <!-- Failed icon -->
{% elif pathway.failed %} <template x-if="status === 'failed'">
<div class="tooltip tooltip-bottom absolute top-4 right-4 z-10">
<div class="tooltip-content">Pathway prediction failed.</div>
<div id="status" class="flex items-center">
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
width="16" width="16"
@ -251,19 +258,28 @@
<path d="M18 6 6 18" /> <path d="M18 6 6 18" />
<path d="M6 6l12 12" /> <path d="M6 6l12 12" />
</svg> </svg>
</template>
<!-- Loading spinner -->
<div
x-show="status === 'running'"
style="width: 20px; height: 20px;"
>
{% include "components/loading-spinner.html" %}
</div> </div>
</div> </div>
{% else %} </div>
<div class="tooltip tooltip-bottom absolute top-4 right-4 z-10">
<div class="tooltip-content">Pathway prediction running.</div> <!-- Update Notice -->
<div id="status" class="flex items-center"> <div
<div x-show="showUpdateNotice"
id="status-loading-spinner" x-cloak
style="width: 20px; height: 20px;" class="alert alert-info absolute right-4 bottom-4 left-4 z-10"
></div> >
</div> <span x-html="updateMessage"></span>
</div> <button @click="reloadPage()" class="btn btn-primary btn-sm mt-2">
{% endif %} Reload page
</button>
</div>
<svg id="pwsvg"> <svg id="pwsvg">
<defs> <defs>
<marker <marker
@ -463,60 +479,6 @@
var pathway = {{ pathway.d3_json | safe }}; var pathway = {{ pathway.d3_json | safe }};
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
// Initialize loading spinner if pathway is running
if (pathway.status === 'running') {
const spinnerContainer = document.getElementById('status-loading-spinner');
if (spinnerContainer) {
showLoadingSpinner(spinnerContainer);
}
}
// If prediction is still running, regularly check status
if (pathway.status === 'running') {
let last_modified = pathway.modified;
let pollInterval = setInterval(async () => {
try {
const response = await fetch("{{ pathway.url }}?status=true", {});
const data = await response.json();
if (data.modified > last_modified) {
var msg = 'Prediction ';
var btn = '<button type="button" onclick="location.reload()" class="btn btn-primary btn-sm mt-2" id="reloadBtn">Reload page</button>';
if (data.status === "running") {
msg += 'is still running. But the Pathway was updated.<br>' + btn;
} else if (data.status === "completed") {
msg += 'is completed. Reload the page to see the updated Pathway.<br>' + btn;
} else if (data.status === "failed") {
msg += 'failed. Reload the page to see the current shape.<br>' + btn;
}
showStatusPopover(msg);
}
if (data.status === "completed" || data.status === "failed") {
const statusBtn = document.getElementById('status');
const tooltipContent = statusBtn.parentElement.querySelector('.tooltip-content');
const spinner = statusBtn.querySelector('#status-loading-spinner');
if (spinner) spinner.remove();
if (data.status === "completed") {
statusBtn.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-check"><path d="M20 6 9 17l-5-5"/></svg>`;
tooltipContent.textContent = 'Pathway prediction complete.';
} else {
statusBtn.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-x"><path d="M18 6 6 18"/><path d="M6 6l12 12"/></svg>`;
tooltipContent.textContent = 'Pathway prediction failed.';
}
clearInterval(pollInterval);
}
} catch (err) {
console.error("Polling error:", err);
}
}, 5000);
}
draw(pathway, 'vizdiv'); draw(pathway, 'vizdiv');
// Transform references in description // Transform references in description

View File

@ -49,7 +49,7 @@
<a <a
href="https://community.envipath.org/" href="https://community.envipath.org/"
target="_blank" target="_blank"
class="btn btn-secondary" class="btn btn-neutral"
>Visit Forums</a >Visit Forums</a
> >
</div> </div>
@ -81,7 +81,7 @@
<a <a
href="https://wiki.envipath.org/" href="https://wiki.envipath.org/"
target="_blank" target="_blank"
class="btn btn-accent" class="btn btn-neutral"
>Read Docs</a >Read Docs</a
> >
</div> </div>

View File

@ -61,7 +61,7 @@ class HTMLGenerator:
else: else:
clz_name = additional_information.__class__.__name__ clz_name = additional_information.__class__.__name__
widget = f"<h4>{clz_name}</h4>" widget = f'<h4 class="h4 font-semibold mt-2 mb-1">{clz_name}</h4>'
if hasattr(additional_information, "uuid"): if hasattr(additional_information, "uuid"):
uuid = additional_information.uuid uuid = additional_information.uuid
@ -89,15 +89,21 @@ class HTMLGenerator:
) )
if is_interval_float: if is_interval_float:
label_text_start = " ".join([x.capitalize() for x in name.split("_")]) + " Start"
label_text_end = " ".join([x.capitalize() for x in name.split("_")]) + " End"
widget += f""" widget += f"""
<div class="form-group row"> <div class="grid grid-cols-2 gap-4 mb-4">
<div class="col-md-6"> <div class="form-control">
<label for="{full_name}__start">{" ".join([x.capitalize() for x in name.split("_")])} Start</label> <label class="label" for="{full_name}__start">
<input type="number" class="form-control" id="{full_name}__start" name="{full_name}__start" value="{value.start if value else ""}"> <span class="label-text">{label_text_start}</span>
</label>
<input type="number" class="input input-bordered w-full" id="{full_name}__start" name="{full_name}__start" value="{value.start if value else ""}">
</div> </div>
<div class="col-md-6"> <div class="form-control">
<label for="{full_name}__end">{" ".join([x.capitalize() for x in name.split("_")])} End</label> <label class="label" for="{full_name}__end">
<input type="number" class="form-control" id="{full_name}__end" name="{full_name}__end" value="{value.end if value else ""}"> <span class="label-text">{label_text_end}</span>
</label>
<input type="number" class="input input-bordered w-full" id="{full_name}__end" name="{full_name}__end" value="{value.end if value else ""}">
</div> </div>
</div> </div>
""" """
@ -106,11 +112,14 @@ class HTMLGenerator:
for e in field_type: for e in field_type:
options += f'<option value="{e.value}" {"selected" if e == value else ""}>{html.escape(e.name)}</option>' options += f'<option value="{e.value}" {"selected" if e == value else ""}>{html.escape(e.name)}</option>'
label_text = " ".join([x.capitalize() for x in name.split("_")])
widget += f""" widget += f"""
<div class="form-group"> <div class="form-control mb-4">
<label for="{full_name}">{" ".join([x.capitalize() for x in name.split("_")])}</label> <label class="label" for="{full_name}">
<select class="form-control" id="{full_name}" name="{full_name}"> <span class="label-text">{label_text}</span>
<option value="" disabled selected>Select {" ".join([x.capitalize() for x in name.split("_")])}</option> </label>
<select class="select select-bordered w-full" id="{full_name}" name="{full_name}">
<option value="" disabled selected>Select {label_text}</option>
{options} {options}
</select> </select>
</div> </div>
@ -126,15 +135,28 @@ class HTMLGenerator:
raise ValueError(f"Could not parse field type {field_type} for {name}") raise ValueError(f"Could not parse field type {field_type} for {name}")
value_to_use = value if value and field_type is not bool else "" value_to_use = value if value and field_type is not bool else ""
label_text = " ".join([x.capitalize() for x in name.split("_")])
widget += f""" if field_type is bool:
<div class="form-group"> widget += f"""
<label for="{full_name}">{" ".join([x.capitalize() for x in name.split("_")])}</label> <div class="form-control mb-4">
<input type="{input_type}" class="form-control" id="{full_name}" name="{full_name}" value="{value_to_use}" {"checked" if value and field_type is bool else ""}> <label class="label cursor-pointer">
</div> <span class="label-text">{label_text}</span>
""" <input type="checkbox" class="checkbox" id="{full_name}" name="{full_name}" {"checked" if value else ""}>
</label>
</div>
"""
else:
widget += f"""
<div class="form-control mb-4">
<label class="label" for="{full_name}">
<span class="label-text">{label_text}</span>
</label>
<input type="{input_type}" class="input input-bordered w-full" id="{full_name}" name="{full_name}" value="{value_to_use}">
</div>
"""
return widget + "<hr>" return widget
@staticmethod @staticmethod
def build_models(params) -> Dict[str, List["EnviPyModel"]]: def build_models(params) -> Dict[str, List["EnviPyModel"]]: