flask_whisper / templates /history.html
Michael Natanael
Add prediction history page (only for authenticated users)
0c67b3e
{% extends 'base.html' %}
{% block head %}
<title>Indonesian Lyrics Classification By Age Group - Prediction</title>
{% endblock %}
{% block body %}
{% include 'navbar.html' %}
<main class="flex flex-row justify-center items-center min-h-screen">
<div class="container max-w-xl w-full bg-white rounded-2xl text-center space-y-6">
<div class="flex items-center justify-between p-4 bg-gray-100">
<h5 class="text-md font-bold text-gray-900 md:text-md uppercase">Prediction History</h5>
</div>
<table id="export-table" class="">
<thead>
<tr>
<th class="text-gray-900 text-center">Action</th>
<th>
<span class="flex items-center text-gray-900">
Lyric
<svg class="w-4 h-4 ms-1" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24"
height="24" fill="none" viewBox="0 0 24 24">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
stroke-width="2" d="m8 15 4 4 4-4m0-6-4-4-4 4" />
</svg>
</span>
</th>
<th>
<span class="flex items-center text-gray-900">
Predicted Age Group
<svg class="w-4 h-4 ms-1" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24"
height="24" fill="none" viewBox="0 0 24 24">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
stroke-width="2" d="m8 15 4 4 4-4m0-6-4-4-4 4" />
</svg>
</span>
</th>
<th>
<span class="flex items-center text-gray-900">
Processing Time (s)
<svg class="w-4 h-4 ms-1" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24"
height="24" fill="none" viewBox="0 0 24 24">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
stroke-width="2" d="m8 15 4 4 4-4m0-6-4-4-4 4" />
</svg>
</span>
</th>
<th data-type="date" data-format="YYYY-MM-DD HH:mm:ss">
<span class="flex items-center text-gray-900">
Created Date
<svg class="w-4 h-4 ms-1" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24"
height="24" fill="none" viewBox="0 0 24 24">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
stroke-width="2" d="m8 15 4 4 4-4m0-6-4-4-4 4" />
</svg>
</span>
</th>
</tr>
</thead>
<tbody>
{% for item in data_history %}
<tr class="hover:bg-gray-50 cursor-pointer">
<td class="text-center">
<!-- Modal toggle -->
<button data-modal-target="select-modal-{{ item.id }}" data-modal-toggle="select-modal-{{ item.id }}"
class="text-blue-700 hover:text-white border border-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-2 py-0.5 text-center"
type="button">
Detail
</button>
<!-- Main modal -->
<div id="select-modal-{{ item.id }}" data-modal-backdrop="static" tabindex="-1" aria-hidden="true"
class="hidden overflow-y-auto overflow-x-hidden fixed top-0 right-0 left-0 z-50 justify-center items-center w-full md:inset-0 h-[calc(100%-1rem)] max-h-full">
<div class="relative w-full max-w-7xl max-h-full">
<!-- Modal content -->
<div class="relative bg-white rounded-lg shadow-sm">
<!-- Modal header -->
<div
class="flex items-center justify-between p-4 md:p-5 border-b rounded-t border-gray-200">
<h3 class="text-lg font-semibold text-gray-900">
{{ item.created_date.strftime('%A, %d %B %Y (%I:%M %p)') }}
</h3>
<button type="button"
class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm h-8 w-8 ms-auto inline-flex justify-center items-center"
data-modal-toggle="select-modal-{{ item.id }}">
<svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg"
fill="none" viewBox="0 0 14 14">
<path stroke="currentColor" stroke-linecap="round"
stroke-linejoin="round" stroke-width="2"
d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6" />
</svg>
<span class="sr-only">Close modal</span>
</button>
</div>
<!-- Modal body -->
<div class="p-4 md:p-5">
<!-- Lyrics -->
<div class="mb-3">
<h1 class="text-lg font-semibold text-gray-800 sm:text-xl">🎵 Lyrics</h1>
<p class="text-center px-2 py-2 text-1xl sm:text-lg text-gray-800">{{ item.lyric }}</p>
</div>
<hr class="mb-3 border-t border-gray-200" />
<!-- Predicted Age Group -->
<div class="mt-2 mb-3">
<h2 class="text-lg font-semibold text-gray-800">Predicted Age Group</h2>
<span class="text-center text-lg font-extrabold text-slate-900 sm:text-2xl">
{{ item.predicted_label.upper() }}
</span>
</div>
<hr class="border-t border-gray-200" />
<!-- Class Probabilities -->
<div class="mt-2 mb-3 flex flex-col text-center items-center">
<h3 class="text-lg font-semibold text-gray-800 mb-2">📊 Class Probabilities
</h3>
<div class="space-y-3">
<div class="w-full">
{% for label, prob in item.probabilities %}
<div
class="flex gap-20 justify-between items-center px-4 py-0.5 text-gray-800 {{ 'font-medium bg-green-500' if label == item.predicted_label else '' }}">
<div
class="capitalize text-1xl sm:text-lg {{ 'font-medium' if label == item.predicted_label else '' }}">
{{ label.capitalize() }}
</div>
<div
class="text-1xl sm:text-lg {{ 'font-medium' if label == item.predicted_label else '' }}">
{{ prob }}
</div>
</div>
{% endfor %}
</div>
</div>
</div>
<hr class="border-t border-gray-200" />
<!-- Processing Time -->
{% if item.processing_time %}
<div class="text-sm mt-2 mb-3 text-gray-500">
⏱️ Total Processing Time: <strong>{{ "%.2f"|format(item.processing_time) }}
seconds</strong>
</div>
{% endif %}
</div>
<!-- Modal footer -->
<div class="flex items-center p-4 md:p-5 border-t border-gray-200 rounded-b">
<button data-modal-hide="select-modal-{{ item.id }}" type="button"
class="inline-flex w-full justify-center text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center">Close</button>
</div>
</div>
</div>
</div>
</td>
<td class="font-medium text-gray-900 whitespace-nowrap">{{ item.lyric | truncate(60) }}</td>
<td class="text-gray-900">{{ item.predicted_label | capitalize }}</td>
<td class="text-right !pr-[5rem] text-gray-900">{{ "%.2f"|format(item.processing_time) }} seconds
</td>
<td class="text-gray-900">{{ item.created_date.strftime('%Y-%m-%d %H:%M:%S') }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</main>
<footer class="flex flex-row justify-center items-center">
<div class="mb-3 text-gray-500">By Michael Natanael</div>
</footer>
{% endblock %}
{% block script %}
<script>
if (document.getElementById("export-table") && typeof simpleDatatables.DataTable !== 'undefined') {
const exportCustomCSV = function (dataTable, userOptions = {}) {
// A modified CSV export that includes a row of minuses at the start and end.
const clonedUserOptions = {
...userOptions
}
clonedUserOptions.download = false
const csv = simpleDatatables.exportCSV(dataTable, clonedUserOptions)
// If CSV didn't work, exit.
if (!csv) {
return false
}
const defaults = {
download: true,
lineDelimiter: "\n",
columnDelimiter: ";"
}
const options = {
...defaults,
...clonedUserOptions
}
const separatorRow = Array(dataTable.data.headings.filter((_heading, index) => !dataTable.columns.settings[index]?.hidden).length)
.fill("+")
.join("+"); // Use "+" as the delimiter
const str = separatorRow + options.lineDelimiter + csv + options.lineDelimiter + separatorRow;
if (userOptions.download) {
// Create a link to trigger the download
const link = document.createElement("a");
link.href = encodeURI("data:text/csv;charset=utf-8," + str);
link.download = (options.filename || "Prediction History") + ".txt";
// Append the link
document.body.appendChild(link);
// Trigger the download
link.click();
// Remove the link
document.body.removeChild(link);
}
return str
}
const table = new simpleDatatables.DataTable("#export-table", {
template: (options, dom) => "<div class='" + options.classes.top + "'>" +
"<div class='flex flex-col sm:flex-row sm:items-center space-y-4 sm:space-y-0 sm:space-x-3 rtl:space-x-reverse w-full sm:w-auto'>" +
(options.paging && options.perPageSelect ?
"<div class='" + options.classes.dropdown + "'>" +
"<label>" +
"<select class='" + options.classes.selector + "'></select> " + options.labels.perPage +
"</label>" +
"</div>" : ""
) + "<button id='exportDropdownButton' type='button' class='flex w-full items-center justify-center rounded-lg border border-gray-200 bg-white px-3 py-2 text-sm font-medium text-gray-900 hover:bg-gray-100 hover:text-primary-700 focus:z-10 focus:outline-none focus:ring-4 focus:ring-gray-100 sm:w-auto'>" +
"Export as" +
"<svg class='-me-0.5 ms-1.5 h-4 w-4' aria-hidden='true' xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='none' viewBox='0 0 24 24'>" +
"<path stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m19 9-7 7-7-7' />" +
"</svg>" +
"</button>" +
"<div id='exportDropdown' class='z-10 hidden w-52 divide-y divide-gray-100 rounded-lg bg-white shadow-sm data-popper-placement='bottom'>" +
"<ul class='p-2 text-left text-sm font-medium text-gray-500 aria-labelledby='exportDropdownButton'>" +
"<li>" +
"<button id='export-csv' class='group inline-flex w-full items-center rounded-md px-3 py-2 text-sm text-gray-500 hover:bg-gray-100 hover:text-gray-900'>" +
"<svg class='me-1.5 h-4 w-4 text-gray-400 group-hover:text-gray-900 aria-hidden='true' xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='currentColor' viewBox='0 0 24 24'>" +
"<path fill-rule='evenodd' d='M9 2.221V7H4.221a2 2 0 0 1 .365-.5L8.5 2.586A2 2 0 0 1 9 2.22ZM11 2v5a2 2 0 0 1-2 2H4a2 2 0 0 0-2 2v7a2 2 0 0 0 2 2 2 2 0 0 0 2 2h12a2 2 0 0 0 2-2 2 2 0 0 0 2-2v-7a2 2 0 0 0-2-2V4a2 2 0 0 0-2-2h-7Zm1.018 8.828a2.34 2.34 0 0 0-2.373 2.13v.008a2.32 2.32 0 0 0 2.06 2.497l.535.059a.993.993 0 0 0 .136.006.272.272 0 0 1 .263.367l-.008.02a.377.377 0 0 1-.018.044.49.49 0 0 1-.078.02 1.689 1.689 0 0 1-.297.021h-1.13a1 1 0 1 0 0 2h1.13c.417 0 .892-.05 1.324-.279.47-.248.78-.648.953-1.134a2.272 2.272 0 0 0-2.115-3.06l-.478-.052a.32.32 0 0 1-.285-.341.34.34 0 0 1 .344-.306l.94.02a1 1 0 1 0 .043-2l-.943-.02h-.003Zm7.933 1.482a1 1 0 1 0-1.902-.62l-.57 1.747-.522-1.726a1 1 0 0 0-1.914.578l1.443 4.773a1 1 0 0 0 1.908.021l1.557-4.773Zm-13.762.88a.647.647 0 0 1 .458-.19h1.018a1 1 0 1 0 0-2H6.647A2.647 2.647 0 0 0 4 13.647v1.706A2.647 2.647 0 0 0 6.647 18h1.018a1 1 0 1 0 0-2H6.647A.647.647 0 0 1 6 15.353v-1.706c0-.172.068-.336.19-.457Z' clip-rule='evenodd'/>" +
"</svg>" +
"<span>Export CSV</span>" +
"</button>" +
"</li>" +
"<li>" +
"<button id='export-json' class='group inline-flex w-full items-center rounded-md px-3 py-2 text-sm text-gray-500 hover:bg-gray-100 hover:text-gray-900'>" +
"<svg class='me-1.5 h-4 w-4 text-gray-400 group-hover:text-gray-900 aria-hidden='true' xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='currentColor' viewBox='0 0 24 24'>" +
"<path fill-rule='evenodd' d='M9 2.221V7H4.221a2 2 0 0 1 .365-.5L8.5 2.586A2 2 0 0 1 9 2.22ZM11 2v5a2 2 0 0 1-2 2H4v11a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2h-7Zm-.293 9.293a1 1 0 0 1 0 1.414L9.414 14l1.293 1.293a1 1 0 0 1-1.414 1.414l-2-2a1 1 0 0 1 0-1.414l2-2a1 1 0 0 1 1.414 0Zm2.586 1.414a1 1 0 0 1 1.414-1.414l2 2a1 1 0 0 1 0 1.414l-2 2a1 1 0 0 1-1.414-1.414L14.586 14l-1.293-1.293Z' clip-rule='evenodd'/>" +
"</svg>" +
"<span>Export JSON</span>" +
"</button>" +
"</li>" +
"<li>" +
"<button id='export-txt' class='group inline-flex w-full items-center rounded-md px-3 py-2 text-sm text-gray-500 hover:bg-gray-100 hover:text-gray-900'>" +
"<svg class='me-1.5 h-4 w-4 text-gray-400 group-hover:text-gray-900 aria-hidden='true' xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='currentColor' viewBox='0 0 24 24'>" +
"<path fill-rule='evenodd' d='M9 2.221V7H4.221a2 2 0 0 1 .365-.5L8.5 2.586A2 2 0 0 1 9 2.22ZM11 2v5a2 2 0 0 1-2 2H4v11a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2h-7ZM8 16a1 1 0 0 1 1-1h6a1 1 0 1 1 0 2H9a1 1 0 0 1-1-1Zm1-5a1 1 0 1 0 0 2h6a1 1 0 1 0 0-2H9Z' clip-rule='evenodd'/>" +
"</svg>" +
"<span>Export TXT</span>" +
"</button>" +
"</li>" +
"<li>" +
"<button id='export-sql' class='group inline-flex w-full items-center rounded-md px-3 py-2 text-sm text-gray-500 hover:bg-gray-100 hover:text-gray-900'>" +
"<svg class='me-1.5 h-4 w-4 text-gray-400 group-hover:text-gray-900 aria-hidden='true' xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='currentColor' viewBox='0 0 24 24'>" +
"<path d='M12 7.205c4.418 0 8-1.165 8-2.602C20 3.165 16.418 2 12 2S4 3.165 4 4.603c0 1.437 3.582 2.602 8 2.602ZM12 22c4.963 0 8-1.686 8-2.603v-4.404c-.052.032-.112.06-.165.09a7.75 7.75 0 0 1-.745.387c-.193.088-.394.173-.6.253-.063.024-.124.05-.189.073a18.934 18.934 0 0 1-6.3.998c-2.135.027-4.26-.31-6.3-.998-.065-.024-.126-.05-.189-.073a10.143 10.143 0 0 1-.852-.373 7.75 7.75 0 0 1-.493-.267c-.053-.03-.113-.058-.165-.09v4.404C4 20.315 7.037 22 12 22Zm7.09-13.928a9.91 9.91 0 0 1-.6.253c-.063.025-.124.05-.189.074a18.935 18.935 0 0 1-6.3.998c-2.135.027-4.26-.31-6.3-.998-.065-.024-.126-.05-.189-.074a10.163 10.163 0 0 1-.852-.372 7.816 7.816 0 0 1-.493-.268c-.055-.03-.115-.058-.167-.09V12c0 .917 3.037 2.603 8 2.603s8-1.686 8-2.603V7.596c-.052.031-.112.059-.165.09a7.816 7.816 0 0 1-.745.386Z'/>" +
"</svg>" +
"<span>Export SQL</span>" +
"</button>" +
"</li>" +
"</ul>" +
"</div>" + "</div>" +
(options.searchable ?
"<div class='" + options.classes.search + "'>" +
"<input class='" + options.classes.input + "' placeholder='" + options.labels.placeholder + "' type='search' title='" + options.labels.searchTitle + "'" + (dom.id ? " aria-controls='" + dom.id + "'" : "") + ">" +
"</div>" : ""
) +
"</div>" +
"<div class='" + options.classes.container + "'" + (options.scrollY.length ? " style='height: " + options.scrollY + "; overflow-Y: auto;'" : "") + "></div>" +
"<div class='" + options.classes.bottom + "'>" +
(options.paging ?
"<div class='" + options.classes.info + "'></div>" : ""
) +
"<nav class='" + options.classes.pagination + "'></nav>" +
"</div>"
})
const $exportButton = document.getElementById("exportDropdownButton");
const $exportDropdownEl = document.getElementById("exportDropdown");
const dropdown = new Dropdown($exportDropdownEl, $exportButton);
console.log(dropdown)
document.getElementById("export-csv").addEventListener("click", () => {
simpleDatatables.exportCSV(table, {
download: true,
lineDelimiter: "\n",
columnDelimiter: ";"
})
})
document.getElementById("export-sql").addEventListener("click", () => {
simpleDatatables.exportSQL(table, {
download: true,
tableName: "export_table"
})
})
document.getElementById("export-txt").addEventListener("click", () => {
simpleDatatables.exportTXT(table, {
download: true
})
})
document.getElementById("export-json").addEventListener("click", () => {
simpleDatatables.exportJSON(table, {
download: true,
space: 3
})
})
}
</script>
{% endblock %}