diff --git a/static/css/input.css b/static/css/input.css index 2407b62e..98ad1678 100644 --- a/static/css/input.css +++ b/static/css/input.css @@ -34,23 +34,3 @@ } @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); - } -} diff --git a/static/js/alpine/pagination.js b/static/js/alpine/pagination.js index e87687b6..aaa5d9ca 100644 --- a/static/js/alpine/pagination.js +++ b/static/js/alpine/pagination.js @@ -44,6 +44,7 @@ document.addEventListener('alpine:init', () => { this.isLoading = true; this.error = null; + this.$dispatch('loading-start'); try { 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.`; } finally { this.isLoading = false; + this.$dispatch('loading-end'); } }, diff --git a/static/js/alpine/pathway.js b/static/js/alpine/pathway.js new file mode 100644 index 00000000..99f65074 --- /dev/null +++ b/static/js/alpine/pathway.js @@ -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: + *
+ * ... + *
+ */ + 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(); + } + })); +}); diff --git a/templates/collections/_paginated_list_partial.html b/templates/collections/_paginated_list_partial.html index 44d7158b..74ce39f4 100644 --- a/templates/collections/_paginated_list_partial.html +++ b/templates/collections/_paginated_list_partial.html @@ -2,7 +2,12 @@ {# Variables: empty_text (string), show_review_badge (bool), always_show_badge (bool) #} {# Loading state #} -
{% include "components/loading-spinner.html" %}
+
+ {% include "components/loading-spinner.html" %} +
{# Error state #}
- {# No items found message #} -
+ {# No items found message - only show after both tabs have loaded #} +

No items found.

{# Tabs Navigation #} -
+
@@ -102,14 +96,14 @@ {# Reviewed Tab Content #}
{% include "collections/_paginated_list_partial.html" with empty_text="reviewed "|add:list_title|default:"items" show_review_badge=True always_show_badge=True %}
@@ -117,14 +111,14 @@ {# Unreviewed Tab Content #}
{% include "collections/_paginated_list_partial.html" with empty_text="unreviewed "|add:list_title|default:"items" %}
diff --git a/templates/components/footer.html b/templates/components/footer.html index a8cd8611..59499beb 100644 --- a/templates/components/footer.html +++ b/templates/components/footer.html @@ -6,9 +6,9 @@ Predict Packages - {% if user.is_authenticated %} - Your Collections - {% endif %} + {# {% if user.is_authenticated %}#} + {# Your Collections#} + {# {% endif %}#} - + +
+ {% endif %} - {% if meta.user.username == 'anonymous' or public_mode %} + {% if meta.user.username == 'anonymous' %} Login {% else %}
-
+
@@ -167,7 +171,11 @@
-
+
@@ -397,7 +405,8 @@ return; } - makeLoadingGif("#predictLoading", "{% static '/images/wait.gif' %}"); + const loadingEl = document.getElementById("predictLoading"); + if (loadingEl) loadingEl.classList.remove("hidden"); const params = new URLSearchParams({ smiles: smiles, @@ -418,12 +427,12 @@ }) .then(data => { const loadingEl = document.getElementById("predictLoading"); - if (loadingEl) loadingEl.innerHTML = ""; + if (loadingEl) loadingEl.classList.add("hidden"); handlePredictionResponse(data); }) .catch(error => { const loadingEl = document.getElementById("predictLoading"); - if (loadingEl) loadingEl.innerHTML = ""; + if (loadingEl) loadingEl.classList.add("hidden"); const resultTable = document.getElementById("predictResultTable"); if (resultTable) { resultTable.classList.add("alert", "alert-error"); @@ -453,7 +462,8 @@ return; } - makeLoadingGif("#appDomainLoading", "{% static '/images/wait.gif' %}"); + const loadingEl = document.getElementById("appDomainLoading"); + if (loadingEl) loadingEl.classList.remove("hidden"); const params = new URLSearchParams({ smiles: smiles, @@ -474,7 +484,7 @@ }) .then(data => { const loadingEl = document.getElementById("appDomainLoading"); - if (loadingEl) loadingEl.innerHTML = ""; + if (loadingEl) loadingEl.classList.add("hidden"); if (typeof handleAssessmentResponse === 'function') { handleAssessmentResponse("{% url 'depict' %}", data); } @@ -482,7 +492,7 @@ }) .catch(error => { const loadingEl = document.getElementById("appDomainLoading"); - if (loadingEl) loadingEl.innerHTML = ""; + if (loadingEl) loadingEl.classList.add("hidden"); const resultTable = document.getElementById("appDomainAssessmentResultTable"); if (resultTable) { resultTable.classList.add("alert", "alert-error"); diff --git a/templates/objects/pathway.html b/templates/objects/pathway.html index f48e2ed7..0e996b30 100644 --- a/templates/objects/pathway.html +++ b/templates/objects/pathway.html @@ -211,11 +211,21 @@ -
- {% if pathway.completed %} -
-
Pathway prediction complete.
-
+
+ +
+
+
+ + + + + +
+ {% include "components/loading-spinner.html" %}
- {% else %} -
-
Pathway prediction running.
-
-
-
-
- {% endif %} +
+ + +
+ + +
{ - try { - const response = await fetch("{{ pathway.url }}?status=true", {}); - const data = await response.json(); - - if (data.modified > last_modified) { - var msg = 'Prediction '; - var btn = ''; - - if (data.status === "running") { - msg += 'is still running. But the Pathway was updated.
' + btn; - } else if (data.status === "completed") { - msg += 'is completed. Reload the page to see the updated Pathway.
' + btn; - } else if (data.status === "failed") { - msg += 'failed. Reload the page to see the current shape.
' + 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 = ``; - tooltipContent.textContent = 'Pathway prediction complete.'; - } else { - statusBtn.innerHTML = ``; - tooltipContent.textContent = 'Pathway prediction failed.'; - } - clearInterval(pollInterval); - } - - } catch (err) { - console.error("Polling error:", err); - } - }, 5000); - } - draw(pathway, 'vizdiv'); // Transform references in description diff --git a/templates/static/contact.html b/templates/static/contact.html index 6b0ecde0..cb38034b 100644 --- a/templates/static/contact.html +++ b/templates/static/contact.html @@ -49,7 +49,7 @@ Visit Forums
@@ -81,7 +81,7 @@ Read Docs
diff --git a/utilities/misc.py b/utilities/misc.py index 5b4da153..fddf61dd 100644 --- a/utilities/misc.py +++ b/utilities/misc.py @@ -61,7 +61,7 @@ class HTMLGenerator: else: clz_name = additional_information.__class__.__name__ - widget = f"

{clz_name}

" + widget = f'

{clz_name}

' if hasattr(additional_information, "uuid"): uuid = additional_information.uuid @@ -89,15 +89,21 @@ class HTMLGenerator: ) 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""" -
-
- - +
+
+ +
-
- - +
+ +
""" @@ -106,11 +112,14 @@ class HTMLGenerator: for e in field_type: options += f'' + label_text = " ".join([x.capitalize() for x in name.split("_")]) widget += f""" -
- - + {options}
@@ -126,15 +135,28 @@ class HTMLGenerator: 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 "" + label_text = " ".join([x.capitalize() for x in name.split("_")]) - widget += f""" -
- - -
- """ + if field_type is bool: + widget += f""" +
+ +
+ """ + else: + widget += f""" +
+ + +
+ """ - return widget + "
" + return widget @staticmethod def build_models(params) -> Dict[str, List["EnviPyModel"]]: