Frontpage update (#179)

This PR introduces an overhaul for the front page and login features while keeping the rest of the application intact.

## Major Changes

- TailwindCSS + DaisyUI Integration: Add  modern CSS framework for component-based utility styling
- Build System: Added pnpm for CSS building; can be extended for updated frontend builds in the future
- Navbar + Footer: Redesigned and includable; old version retained for unstyled elements
- Optimized Assets: Added minified and CSS-stylable logos

## New Features

- Static Pages: Added comprehensive mockups of static pages (legal, privacy policy, terms of use, contact, etc.). **Note:** These have to be fixed before a public release, as their content is largely unreviewed and incorrect. Probably best to do in a separate PR that only contains updates to these.
- Discourse API: Implement minimal features based on RestAPI for controllable results.

## Current bugs
- [x] The static pages include the default navbar and footer on the login page. This will likely not work, as users need to access it before logging in; no good workaround so far (problem with Django templating system).
- [ ] The front page predict link is currently non-functional; the redesigned page is almost ready but better done in a separate PR as it also touches Django code.
- [x] Visual bug with the news cards. Still intend to fix for this PR

Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#179
Reviewed-by: jebus <lorsbach@envipath.com>
Co-authored-by: Tobias O <tobias.olenyi@envipath.com>
Co-committed-by: Tobias O <tobias.olenyi@envipath.com>
This commit is contained in:
2025-11-12 01:09:39 +13:00
committed by jebus
parent 34589efbde
commit ddf1fd3515
47 changed files with 4271 additions and 999 deletions

View File

@ -1,163 +1,264 @@
{% extends "framework.html" %}
{% load static %}
{% load envipytags %}
{% block content %}
{% block action_modals %}
{% include "modals/objects/edit_model_modal.html" %}
{% include "modals/objects/evaluate_model_modal.html" %}
{% include "modals/objects/retrain_model_modal.html" %}
{% include "modals/objects/generic_delete_modal.html" %}
{% endblock action_modals %}
{% block action_modals %}
{% include "modals/objects/edit_model_modal.html" %}
{% include "modals/objects/evaluate_model_modal.html" %}
{% include "modals/objects/retrain_model_modal.html" %}
{% include "modals/objects/generic_delete_modal.html" %}
{% endblock action_modals %}
<!-- Include required libs -->
<script src="https://d3js.org/d3.v5.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/c3@0.7.20/c3.min.js"></script>
<link href="https://cdn.jsdelivr.net/npm/c3@0.7.20/c3.min.css" rel="stylesheet">
<!-- Include required libs -->
<script src="https://d3js.org/d3.v5.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/c3@0.7.20/c3.min.js"></script>
<link
href="https://cdn.jsdelivr.net/npm/c3@0.7.20/c3.min.css"
rel="stylesheet"
/>
<div class="panel-group" id="model-detail">
<div class="panel panel-default">
<div class="panel-heading" id="headingPanel" style="font-size:2rem;height: 46px">
{{ model.name|safe }}
<div id="actionsButton"
style="float: right;font-weight: normal;font-size: medium;position: relative; top: 50%; transform: translateY(-50%);z-index:100;display: none;"
class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button"
aria-haspopup="true" aria-expanded="false"><span
class="glyphicon glyphicon-wrench"></span> Actions <span class="caret"></span><span
style="padding-right:1em"></span></a>
<ul id="actionsList" class="dropdown-menu">
{% block actions %}
{% include "actions/objects/model.html" %}
{% endblock %}
</ul>
</div>
<div class="panel-group" id="model-detail">
<div class="panel panel-default">
<div
class="panel-heading"
id="headingPanel"
style="font-size:2rem;height: 46px"
>
{{ model.name|safe }}
<div
id="actionsButton"
style="float: right;font-weight: normal;font-size: medium;position: relative; top: 50%; transform: translateY(-50%);z-index:100;display: none;"
class="dropdown"
>
<a
href="#"
class="dropdown-toggle"
data-toggle="dropdown"
role="button"
aria-haspopup="true"
aria-expanded="false"
><span class="glyphicon glyphicon-wrench"></span> Actions
<span class="caret"></span><span style="padding-right:1em"></span
></a>
<ul id="actionsList" class="dropdown-menu">
{% block actions %}
{% include "actions/objects/model.html" %}
{% endblock %}
</ul>
</div>
</div>
<div class="panel-body">
<p>{{ model.description|safe }}</p>
</div>
{% if model|classname == 'MLRelativeReasoning' or model|classname == 'RuleBasedRelativeReasoning' %}
<!-- Rule Packages -->
<div
class="panel panel-default panel-heading list-group-item"
style="background-color:silver"
>
<h4 class="panel-title">
<a
id="rule-package-link"
data-toggle="collapse"
data-parent="#model-detail"
href="#rule-package"
>Rule Packages</a
>
</h4>
</div>
<div id="rule-package" class="panel-collapse collapse in">
<div class="panel-body list-group-item">
{% for p in model.rule_packages.all %}
<a class="list-group-item" href="{{ p.url }}"
>{{ p.name|safe }}</a
>
{% endfor %}
</div>
</div>
<!-- Reaction Packages -->
<div
class="panel panel-default panel-heading list-group-item"
style="background-color:silver"
>
<h4 class="panel-title">
<a
id="reaction-package-link"
data-toggle="collapse"
data-parent="#model-detail"
href="#reaction-package"
>Rule Packages</a
>
</h4>
</div>
<div id="reaction-package" class="panel-collapse collapse in">
<div class="panel-body list-group-item">
{% for p in model.data_packages.all %}
<a class="list-group-item" href="{{ p.url }}"
>{{ p.name|safe }}</a
>
{% endfor %}
</div>
</div>
{% if model.eval_packages.all|length > 0 %}
<!-- Eval Packages -->
<div
class="panel panel-default panel-heading list-group-item"
style="background-color:silver"
>
<h4 class="panel-title">
<a
id="eval-package-link"
data-toggle="collapse"
data-parent="#model-detail"
href="#eval-package"
>Rule Packages</a
>
</h4>
</div>
<div id="eval-package" class="panel-collapse collapse in">
<div class="panel-body list-group-item">
{% for p in model.eval_packages.all %}
<a class="list-group-item" href="{{ p.url }}"
>{{ p.name|safe }}</a
>
{% endfor %}
</div>
<div class="panel-body">
<p> {{ model.description|safe }} </p>
</div>
{% endif %}
<!-- Model Status -->
<div
class="panel panel-default panel-heading list-group-item"
style="background-color:silver"
>
<h4 class="panel-title">
<a
id="model-status-link"
data-toggle="collapse"
data-parent="#model-detail"
href="#model-status"
>Model Status</a
>
</h4>
</div>
<div id="model-status" class="panel-collapse collapse in">
<div class="panel-body list-group-item">{{ model.status }}</div>
</div>
{% endif %}
{% if model.ready_for_prediction %}
<!-- Predict Panel -->
<div
class="panel panel-default panel-heading list-group-item"
style="background-color:silver"
>
<h4 class="panel-title">
<a
id="predict-smiles-link"
data-toggle="collapse"
data-parent="#model-detail"
href="#predict-smiles"
>Predict</a
>
</h4>
</div>
<div id="predict-smiles" class="panel-collapse collapse in">
<div class="panel-body list-group-item">
<div class="input-group">
<input
id="smiles-to-predict"
type="text"
class="form-control"
placeholder="CCN(CC)C(=O)C1=CC(=CC=C1)C"
/>
<span class="input-group-btn">
<button
class="btn btn-default"
type="submit"
id="predict-button"
>
Predict!
</button>
</span>
</div>
{% if model|classname == 'MLRelativeReasoning' or model|classname == 'RuleBasedRelativeReasoning'%}
<!-- Rule Packages -->
<div class="panel panel-default panel-heading list-group-item" style="background-color:silver">
<h4 class="panel-title">
<a id="rule-package-link" data-toggle="collapse" data-parent="#model-detail"
href="#rule-package">Rule Packages</a>
</h4>
</div>
<div id="rule-package" class="panel-collapse collapse in">
<div class="panel-body list-group-item">
{% for p in model.rule_packages.all %}
<a class="list-group-item" href="{{ p.url }}">{{ p.name|safe }}</a>
{% endfor %}
</div>
</div>
<!-- Reaction Packages -->
<div class="panel panel-default panel-heading list-group-item" style="background-color:silver">
<h4 class="panel-title">
<a id="reaction-package-link" data-toggle="collapse" data-parent="#model-detail"
href="#reaction-package">Rule Packages</a>
</h4>
</div>
<div id="reaction-package" class="panel-collapse collapse in">
<div class="panel-body list-group-item">
{% for p in model.data_packages.all %}
<a class="list-group-item" href="{{ p.url }}">{{ p.name|safe }}</a>
{% endfor %}
</div>
</div>
{% if model.eval_packages.all|length > 0 %}
<!-- Eval Packages -->
<div class="panel panel-default panel-heading list-group-item" style="background-color:silver">
<h4 class="panel-title">
<a id="eval-package-link" data-toggle="collapse" data-parent="#model-detail"
href="#eval-package">Rule Packages</a>
</h4>
</div>
<div id="eval-package" class="panel-collapse collapse in">
<div class="panel-body list-group-item">
{% for p in model.eval_packages.all %}
<a class="list-group-item" href="{{ p.url }}">{{ p.name|safe }}</a>
{% endfor %}
</div>
</div>
{% endif %}
<!-- Model Status -->
<div class="panel panel-default panel-heading list-group-item" style="background-color:silver">
<h4 class="panel-title">
<a id="model-status-link" data-toggle="collapse" data-parent="#model-detail"
href="#model-status">Model Status</a>
</h4>
</div>
<div id="model-status" class="panel-collapse collapse in">
<div class="panel-body list-group-item">
{{ model.status }}
</div>
</div>
{% endif %}
{% if model.ready_for_prediction %}
<!-- Predict Panel -->
<div class="panel panel-default panel-heading list-group-item" style="background-color:silver">
<h4 class="panel-title">
<a id="predict-smiles-link" data-toggle="collapse" data-parent="#model-detail"
href="#predict-smiles">Predict</a>
</h4>
</div>
<div id="predict-smiles" class="panel-collapse collapse in">
<div class="panel-body list-group-item">
<div class="input-group">
<input id="smiles-to-predict" type="text" class="form-control"
placeholder="CCN(CC)C(=O)C1=CC(=CC=C1)C">
<span class="input-group-btn">
<button class="btn btn-default" type="submit" id="predict-button">Predict!</button>
</span>
</div>
<div id="predictLoading"></div>
<div id="predictResultTable"></div>
</div>
</div>
<!-- End Predict Panel -->
{% endif %}
<div id="predictLoading"></div>
<div id="predictResultTable"></div>
</div>
</div>
<!-- End Predict Panel -->
{% endif %}
{% if model.ready_for_prediction and model.app_domain %}
<!-- App Domain -->
<div class="panel panel-default panel-heading list-group-item" style="background-color:silver">
<h4 class="panel-title">
<a id="app-domain-assessment-link" data-toggle="collapse" data-parent="#model-detail"
href="#app-domain-assessment">Applicability Domain Assessment</a>
</h4>
</div>
<div id="app-domain-assessment" class="panel-collapse collapse in">
<div class="panel-body list-group-item">
<div class="input-group">
<input id="smiles-to-assess" type="text" class="form-control" placeholder="CCN(CC)C(=O)C1=CC(=CC=C1)C">
<span class="input-group-btn">
<button class="btn btn-default" type="submit" id="assess-button">Assess!</button>
</span>
</div>
<div id="appDomainLoading"></div>
<div id="appDomainAssessmentResultTable"></div>
</div>
</div>
<!-- End App Domain -->
{% endif %}
{% if model.ready_for_prediction and model.app_domain %}
<!-- App Domain -->
<div
class="panel panel-default panel-heading list-group-item"
style="background-color:silver"
>
<h4 class="panel-title">
<a
id="app-domain-assessment-link"
data-toggle="collapse"
data-parent="#model-detail"
href="#app-domain-assessment"
>Applicability Domain Assessment</a
>
</h4>
</div>
<div id="app-domain-assessment" class="panel-collapse collapse in">
<div class="panel-body list-group-item">
<div class="input-group">
<input
id="smiles-to-assess"
type="text"
class="form-control"
placeholder="CCN(CC)C(=O)C1=CC(=CC=C1)C"
/>
<span class="input-group-btn">
<button
class="btn btn-default"
type="submit"
id="assess-button"
>
Assess!
</button>
</span>
</div>
<div id="appDomainLoading"></div>
<div id="appDomainAssessmentResultTable"></div>
</div>
</div>
<!-- End App Domain -->
{% endif %}
{% if model.model_status == 'FINISHED' %}
<!-- Single Gen Curve Panel -->
<div class="panel panel-default panel-heading list-group-item" style="background-color:silver">
<h4 class="panel-title">
<a id="sg-curve-link" data-toggle="collapse" data-parent="#model-detail"
href="#sg-curve">Precision Recall Curve</a>
</h4>
</div>
<div id="sg-curve" class="panel-collapse collapse in">
<div class="panel-body list-group-item">
<!-- Center container contents -->
<div class="container" style="display: flex;justify-content: center;">
<div id="sg-curve-plotdiv" class="chart">
<div id="sg-chart"></div>
</div>
</div>
</div>
</div>
{% if model.model_status == 'FINISHED' %}
<!-- Single Gen Curve Panel -->
<div
class="panel panel-default panel-heading list-group-item"
style="background-color:silver"
>
<h4 class="panel-title">
<a
id="sg-curve-link"
data-toggle="collapse"
data-parent="#model-detail"
href="#sg-curve"
>Precision Recall Curve</a
>
</h4>
</div>
<div id="sg-curve" class="panel-collapse collapse in">
<div class="panel-body list-group-item">
<!-- Center container contents -->
<div
class="container"
style="display: flex;justify-content: center;"
>
<div id="sg-curve-plotdiv" class="chart">
<div id="sg-chart"></div>
</div>
</div>
</div>
</div>
{# prettier-ignore-start #}
<script>
$(function () {
if (!($('#sg-chart').length > 0)) {
@ -264,138 +365,162 @@
});
});
</script>
<!-- End Single Gen Curve Panel -->
{% endif %}
</div>
{# prettier-ignore-end #}
<!-- End Single Gen Curve Panel -->
{% endif %}
</div>
</div>
<script>
<script>
function handlePredictionResponse(data) {
res = "<table class='table table-striped'>";
res += "<thead>";
res += "<th scope='col'>#</th>";
function handlePredictionResponse(data) {
res = "<table class='table table-striped'>"
res += "<thead>"
res += "<th scope='col'>#</th>"
columns = ["products", "image", "probability", "btrule"];
columns = ['products', 'image', 'probability', 'btrule']
for (col in columns) {
res += "<th scope='col'>" + columns[col] + "</th>";
}
for (col in columns) {
res += "<th scope='col'>" + columns[col] + "</th>"
res += "</thead>";
res += "<tbody>";
var cnt = 1;
for (transformation in data) {
res += "<tr>";
res += "<th scope='row'>" + cnt + "</th>";
res +=
"<th scope='row'>" +
data[transformation]["products"][0].join(", ") +
"</th>";
res +=
"<th scope='row'>" +
"<img width='400' src='{% url 'depict' %}?smiles=" +
encodeURIComponent(data[transformation]["products"][0].join(".")) +
"'></th>";
res +=
"<th scope='row'>" +
data[transformation]["probability"].toFixed(3) +
"</th>";
if (data[transformation]["btrule"] != null) {
res +=
"<th scope='row'>" +
"<a href='" +
data[transformation]["btrule"]["url"] +
"'>" +
data[transformation]["btrule"]["name"] +
"</a>" +
"</th>";
} else {
res += "<th scope='row'>N/A</th>";
}
res += "</tr>";
cnt += 1;
}
res += "</tbody>";
res += "</table>";
$("#predictResultTable").append(res);
}
function clear(divid) {
$("#" + divid).removeClass("alert alert-danger");
$("#" + divid).empty();
}
if ($("#predict-button").length > 0) {
$("#predict-button").on("click", function (e) {
e.preventDefault();
clear("predictResultTable");
data = {
smiles: $("#smiles-to-predict").val(),
classify: "ILikeCats!",
};
if (data["smiles"].trim() === "") {
$("#predictResultTable").addClass("alert alert-danger");
$("#predictResultTable").append(
"Please enter a SMILES string to predict!",
);
return;
}
makeLoadingGif("#predictLoading", "{% static '/images/wait.gif' %}");
$.ajax({
type: "get",
data: data,
url: "",
success: function (data, textStatus) {
try {
$("#predictLoading").empty();
handlePredictionResponse(data);
} catch (error) {
$("#predictLoading").empty();
$("#predictResultTable").addClass("alert alert-danger");
$("#predictResultTable").append(
"Error while processing response :/",
);
}
},
error: function (jqXHR, textStatus, errorThrown, x) {
$("#predictLoading").empty();
$("#predictResultTable").addClass("alert alert-danger");
$("#predictResultTable").append(jqXHR.responseJSON.error);
},
});
});
}
res += "</thead>"
res += "<tbody>"
var cnt = 1;
for (transformation in data) {
res += "<tr>"
res += "<th scope='row'>" + cnt + "</th>"
res += "<th scope='row'>" + data[transformation]['products'][0].join(', ') + "</th>"
res += "<th scope='row'>" + "<img width='400' src='{% url 'depict' %}?smiles=" + encodeURIComponent(data[transformation]['products'][0].join('.')) + "'></th>"
res += "<th scope='row'>" + data[transformation]['probability'].toFixed(3) + "</th>"
if (data[transformation]['btrule'] != null) {
res += "<th scope='row'>" + "<a href='" + data[transformation]['btrule']['url'] + "'>" + data[transformation]['btrule']['name'] + "</a>" + "</th>"
} else {
res += "<th scope='row'>N/A</th>"
}
res += "</tr>"
cnt += 1;
if ($("#assess-button").length > 0) {
$("#assess-button").on("click", function (e) {
e.preventDefault();
clear("appDomainAssessmentResultTable");
data = {
smiles: $("#smiles-to-assess").val(),
"app-domain-assessment": "ILikeCats!",
};
if (data["smiles"].trim() === "") {
$("#appDomainAssessmentResultTable").addClass("alert alert-danger");
$("#appDomainAssessmentResultTable").append(
"Please enter a SMILES string to predict!",
);
return;
}
makeLoadingGif("#appDomainLoading", "{% static '/images/wait.gif' %}");
$.ajax({
type: "get",
data: data,
url: "",
success: function (data, textStatus) {
try {
$("#appDomainLoading").empty();
handleAssessmentResponse("{% url 'depict' %}", data);
console.log(data);
} catch (error) {
$("#appDomainLoading").empty();
$("#appDomainAssessmentResultTable").addClass(
"alert alert-danger",
);
$("#appDomainAssessmentResultTable").append(
"Error while processing response :/",
);
}
res += "</tbody>"
res += "</table>"
$("#predictResultTable").append(res);
}
function clear(divid) {
$("#" + divid).removeClass("alert alert-danger");
$("#" + divid).empty();
}
if ($('#predict-button').length > 0) {
$("#predict-button").on("click", function (e) {
e.preventDefault();
clear("predictResultTable");
data = {
"smiles": $("#smiles-to-predict").val(),
"classify": "ILikeCats!"
}
if (data["smiles"].trim() === "") {
$("#predictResultTable").addClass("alert alert-danger");
$("#predictResultTable").append("Please enter a SMILES string to predict!");
return;
}
makeLoadingGif("#predictLoading", "{% static '/images/wait.gif' %}");
$.ajax({
type: 'get',
data: data,
url: '',
success: function (data, textStatus) {
try {
$("#predictLoading").empty();
handlePredictionResponse(data);
} catch (error) {
$("#predictLoading").empty();
$("#predictResultTable").addClass("alert alert-danger");
$("#predictResultTable").append("Error while processing response :/");
}
},
error: function (jqXHR, textStatus, errorThrown, x) {
$("#predictLoading").empty();
$("#predictResultTable").addClass("alert alert-danger");
$("#predictResultTable").append(jqXHR.responseJSON.error);
},
});
});
}
if ($('#assess-button').length > 0) {
$("#assess-button").on("click", function (e) {
e.preventDefault();
clear("appDomainAssessmentResultTable");
data = {
"smiles": $("#smiles-to-assess").val(),
"app-domain-assessment": "ILikeCats!"
}
if (data["smiles"].trim() === "") {
$("#appDomainAssessmentResultTable").addClass("alert alert-danger");
$("#appDomainAssessmentResultTable").append("Please enter a SMILES string to predict!");
return;
}
makeLoadingGif("#appDomainLoading", "{% static '/images/wait.gif' %}");
$.ajax({
type: 'get',
data: data,
url: '',
success: function (data, textStatus) {
try {
$("#appDomainLoading").empty();
handleAssessmentResponse("{% url 'depict' %}", data);
console.log(data);
} catch (error) {
$("#appDomainLoading").empty();
$("#appDomainAssessmentResultTable").addClass("alert alert-danger");
$("#appDomainAssessmentResultTable").append("Error while processing response :/");
}
},
error: function (jqXHR, textStatus, errorThrown) {
$("#appDomainLoading").empty();
$("#appDomainAssessmentResultTable").addClass("alert alert-danger");
$("#appDomainAssessmentResultTable").append(jqXHR.responseJSON.error);
}
});
});
}
</script>
},
error: function (jqXHR, textStatus, errorThrown) {
$("#appDomainLoading").empty();
$("#appDomainAssessmentResultTable").addClass("alert alert-danger");
$("#appDomainAssessmentResultTable").append(
jqXHR.responseJSON.error,
);
},
});
});
}
</script>
{% endblock content %}