|
|
|
@ -64,12 +64,17 @@ body, .bg-light { |
|
|
|
[class*="bg-"] { |
|
|
|
[class*="bg-"] { |
|
|
|
background-color: inherit !important; |
|
|
|
background-color: inherit !important; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
#credit-restant { |
|
|
|
|
|
|
|
background-color: #ffe082 !important; |
|
|
|
|
|
|
|
color: #23272b !important; |
|
|
|
|
|
|
|
border: 1px solid #444c56 !important; |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
</style> |
|
|
|
</style> |
|
|
|
</head> |
|
|
|
</head> |
|
|
|
<body class="bg-light"> |
|
|
|
<body class="bg-light"> |
|
|
|
<nav class="navbar navbar-expand navbar-dark bg-dark"> |
|
|
|
<nav class="navbar navbar-expand navbar-dark bg-dark"> |
|
|
|
<a class="navbar-brand ps-3" href="#">IAProf</a> |
|
|
|
<a class="navbar-brand ps-3" href="/generation/">IAProf</a> |
|
|
|
<ul class="navbar-nav ms-auto me-3"> |
|
|
|
<ul class="navbar-nav ms-auto me-3"> |
|
|
|
<li class="nav-item"> |
|
|
|
<li class="nav-item"> |
|
|
|
<a class="nav-link" href="/logout/">Se déconnecter</a> |
|
|
|
<a class="nav-link" href="/logout/">Se déconnecter</a> |
|
|
|
@ -85,14 +90,29 @@ body, .bg-light { |
|
|
|
<h2 class="m-0 font-weight-bold text-primary">Génération des appréciations</h2> |
|
|
|
<h2 class="m-0 font-weight-bold text-primary">Génération des appréciations</h2> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
<div class="card-body"> |
|
|
|
<div class="card-body"> |
|
|
|
<div class="mb-3"> |
|
|
|
<div class="mb-3 d-flex justify-content-between align-items-center"> |
|
|
|
<label for="modele-select" class="form-label">Choisir un modèle</label> |
|
|
|
<div> |
|
|
|
<select id="modele-select" class="form-select"> |
|
|
|
<label for="modele-select" class="form-label">Choisir un modèle</label> |
|
|
|
<option value="ft:gpt-4o-2024-08-06:personal:app-gen-gangneux2:AYJecsON">Modèle Gangneux</option> |
|
|
|
<select id="modele-select" class="form-select"> |
|
|
|
<option value="gpt-4.1-mini">GPT-4.1 mini</option> |
|
|
|
{% for modele in modeles %} |
|
|
|
</select> |
|
|
|
<option value="{{ modele.code }}" {% if modele.code == 'gpt-4.1-mini' %}selected{% endif %}>{{ modele.nom }}</option> |
|
|
|
|
|
|
|
{% endfor %} |
|
|
|
|
|
|
|
{% if not modeles or not modeles|dictsort:'code'|length %} |
|
|
|
|
|
|
|
<option value="gpt-4.1-mini" selected>gpt-4.1-mini</option> |
|
|
|
|
|
|
|
{% endif %} |
|
|
|
|
|
|
|
</select> |
|
|
|
|
|
|
|
</div> |
|
|
|
|
|
|
|
<div> |
|
|
|
|
|
|
|
<span class="badge bg-info text-dark" id="credit-restant">Crédits restants : {{ credit }}</span> |
|
|
|
|
|
|
|
</div> |
|
|
|
|
|
|
|
</div> |
|
|
|
|
|
|
|
<div class="mb-3 d-flex gap-2"> |
|
|
|
|
|
|
|
<button id="generation-toggle" class="btn btn-primary mb-3">Lancer la génération</button> |
|
|
|
|
|
|
|
<button id="export-pdf" class="btn btn-secondary mb-3" type="button">Exporter en PDF</button> |
|
|
|
|
|
|
|
</div> |
|
|
|
|
|
|
|
<div id="credit-alert" class="alert alert-danger d-none" role="alert"> |
|
|
|
|
|
|
|
Vous n'avez plus de crédits pour générer des appréciations. |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
<button id="generation-toggle" class="btn btn-primary mb-3">Lancer la génération</button> |
|
|
|
|
|
|
|
<table class="table table-bordered" id="appreciations-table"> |
|
|
|
<table class="table table-bordered" id="appreciations-table"> |
|
|
|
<thead> |
|
|
|
<thead> |
|
|
|
<tr> |
|
|
|
<tr> |
|
|
|
@ -116,78 +136,98 @@ body, .bg-light { |
|
|
|
</tbody> |
|
|
|
</tbody> |
|
|
|
</table> |
|
|
|
</table> |
|
|
|
<script> |
|
|
|
<script> |
|
|
|
let stopGeneration = false; |
|
|
|
let stopGeneration = false; |
|
|
|
let enCours = false; |
|
|
|
let enCours = false; |
|
|
|
|
|
|
|
|
|
|
|
function getRowsSansAppreciation() { |
|
|
|
function getRowsSansAppreciation() { |
|
|
|
return Array.from(document.querySelectorAll('#appreciations-table tbody tr')).filter(row => !row.querySelector('.appreciation').textContent.trim()); |
|
|
|
return Array.from(document.querySelectorAll('#appreciations-table tbody tr')).filter(row => !row.querySelector('.appreciation').textContent.trim()); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', function() { |
|
|
|
document.addEventListener('DOMContentLoaded', function() { |
|
|
|
const btnToggle = document.getElementById('generation-toggle'); |
|
|
|
const btnToggle = document.getElementById('generation-toggle'); |
|
|
|
const selectModele = document.getElementById('modele-select'); |
|
|
|
const selectModele = document.getElementById('modele-select'); |
|
|
|
let rows = getRowsSansAppreciation(); |
|
|
|
const creditRestant = document.getElementById('credit-restant'); |
|
|
|
|
|
|
|
const creditAlert = document.getElementById('credit-alert'); |
|
|
|
|
|
|
|
let rows = getRowsSansAppreciation(); |
|
|
|
|
|
|
|
|
|
|
|
function setButtonState(state) { |
|
|
|
function setButtonState(state) { |
|
|
|
if (state === 'lancer') { |
|
|
|
if (state === 'lancer') { |
|
|
|
btnToggle.textContent = 'Lancer la génération'; |
|
|
|
btnToggle.textContent = 'Lancer la génération'; |
|
|
|
btnToggle.className = 'btn btn-primary mb-3'; |
|
|
|
btnToggle.className = 'btn btn-primary mb-3'; |
|
|
|
} else if (state === 'arreter') { |
|
|
|
} else if (state === 'arreter') { |
|
|
|
btnToggle.textContent = 'Arrêter la génération'; |
|
|
|
btnToggle.textContent = 'Arrêter la génération'; |
|
|
|
btnToggle.className = 'btn btn-danger mb-3'; |
|
|
|
btnToggle.className = 'btn btn-danger mb-3'; |
|
|
|
} else if (state === 'continuer') { |
|
|
|
} else if (state === 'continuer') { |
|
|
|
btnToggle.textContent = 'Continuer la génération'; |
|
|
|
btnToggle.textContent = 'Continuer la génération'; |
|
|
|
btnToggle.className = 'btn btn-success mb-3'; |
|
|
|
btnToggle.className = 'btn btn-success mb-3'; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function traiterLignes() { |
|
|
|
function traiterLignes() { |
|
|
|
rows = getRowsSansAppreciation(); |
|
|
|
rows = getRowsSansAppreciation(); |
|
|
|
if (rows.length === 0 || stopGeneration) { |
|
|
|
if (rows.length === 0 || stopGeneration) { |
|
|
|
enCours = false; |
|
|
|
enCours = false; |
|
|
|
if (stopGeneration) setButtonState('continuer'); |
|
|
|
if (stopGeneration) setButtonState('continuer'); |
|
|
|
else setButtonState('lancer'); |
|
|
|
else setButtonState('lancer'); |
|
|
|
return; |
|
|
|
return; |
|
|
|
} |
|
|
|
} |
|
|
|
enCours = true; |
|
|
|
enCours = true; |
|
|
|
setButtonState('arreter'); |
|
|
|
setButtonState('arreter'); |
|
|
|
const row = rows[0]; // Toujours traiter la première ligne sans appréciation |
|
|
|
const row = rows[0]; |
|
|
|
const eleve = row.querySelector('.eleve').textContent; |
|
|
|
const eleve = row.querySelector('.eleve').textContent; |
|
|
|
const appreciationCell = row.querySelector('.appreciation'); |
|
|
|
const appreciationCell = row.querySelector('.appreciation'); |
|
|
|
appreciationCell.innerHTML = '<span class="spinner-border spinner-border-sm"></span> Génération...'; |
|
|
|
appreciationCell.innerHTML = '<span class="spinner-border spinner-border-sm"></span> Génération...'; |
|
|
|
fetch("{% url 'generer_appreciation_ajax' %}", { |
|
|
|
fetch("{% url 'generer_appreciation_ajax' %}", { |
|
|
|
method: 'POST', |
|
|
|
method: 'POST', |
|
|
|
headers: { |
|
|
|
headers: { |
|
|
|
'Content-Type': 'application/json', |
|
|
|
'Content-Type': 'application/json', |
|
|
|
'X-CSRFToken': '{{ csrf_token }}' |
|
|
|
'X-CSRFToken': '{{ csrf_token }}' |
|
|
|
}, |
|
|
|
}, |
|
|
|
body: JSON.stringify({eleve: eleve, modele: selectModele.value}) |
|
|
|
body: JSON.stringify({eleve: eleve, modele: selectModele.value}) |
|
|
|
}) |
|
|
|
}) |
|
|
|
.then(response => response.json()) |
|
|
|
.then(response => response.json()) |
|
|
|
.then(data => { |
|
|
|
.then(data => { |
|
|
|
appreciationCell.textContent = data.appreciation; |
|
|
|
appreciationCell.textContent = data.appreciation; |
|
|
|
traiterLignes(); |
|
|
|
if (typeof data.credit !== 'undefined') { |
|
|
|
}) |
|
|
|
creditRestant.textContent = 'Crédits restants : ' + data.credit; |
|
|
|
.catch(() => { |
|
|
|
} |
|
|
|
appreciationCell.textContent = 'Erreur lors de la génération'; |
|
|
|
if (data.stop) { |
|
|
|
traiterLignes(); |
|
|
|
stopGeneration = true; |
|
|
|
}); |
|
|
|
setButtonState('lancer'); |
|
|
|
} |
|
|
|
if (data.credit === 0) { |
|
|
|
|
|
|
|
creditAlert.classList.remove('d-none'); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
return; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
traiterLignes(); |
|
|
|
|
|
|
|
}) |
|
|
|
|
|
|
|
.catch(() => { |
|
|
|
|
|
|
|
appreciationCell.textContent = 'Erreur lors de la génération'; |
|
|
|
|
|
|
|
traiterLignes(); |
|
|
|
|
|
|
|
}); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
btnToggle.addEventListener('click', function() { |
|
|
|
btnToggle.addEventListener('click', function() { |
|
|
|
if (!enCours && (btnToggle.textContent === 'Lancer la génération' || btnToggle.textContent === 'Continuer la génération')) { |
|
|
|
const credit = parseInt(creditRestant.textContent.replace(/\D/g, '')); |
|
|
|
stopGeneration = false; |
|
|
|
if (credit === 0) { |
|
|
|
setButtonState('arreter'); |
|
|
|
creditAlert.classList.remove('d-none'); |
|
|
|
traiterLignes(); |
|
|
|
return; |
|
|
|
} else if (enCours && btnToggle.textContent === 'Arrêter la génération') { |
|
|
|
} else { |
|
|
|
stopGeneration = true; |
|
|
|
creditAlert.classList.add('d-none'); |
|
|
|
setButtonState('continuer'); |
|
|
|
} |
|
|
|
} |
|
|
|
if (!enCours && (btnToggle.textContent === 'Lancer la génération' || btnToggle.textContent === 'Continuer la génération')) { |
|
|
|
}); |
|
|
|
stopGeneration = false; |
|
|
|
|
|
|
|
setButtonState('arreter'); |
|
|
|
|
|
|
|
traiterLignes(); |
|
|
|
|
|
|
|
} else if (enCours && btnToggle.textContent === 'Arrêter la génération') { |
|
|
|
|
|
|
|
stopGeneration = true; |
|
|
|
|
|
|
|
setButtonState('continuer'); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
setButtonState('lancer'); |
|
|
|
setButtonState('lancer'); |
|
|
|
}); |
|
|
|
}); |
|
|
|
</script> |
|
|
|
</script> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
@ -195,5 +235,28 @@ body, .bg-light { |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script> |
|
|
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script> |
|
|
|
|
|
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script> |
|
|
|
|
|
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf-autotable/3.8.2/jspdf.plugin.autotable.min.js"></script> |
|
|
|
|
|
|
|
<script> |
|
|
|
|
|
|
|
function exportTableToPDF() { |
|
|
|
|
|
|
|
const doc = new window.jspdf.jsPDF(); |
|
|
|
|
|
|
|
doc.text('Tableau des appréciations', 14, 14); |
|
|
|
|
|
|
|
const table = document.getElementById('appreciations-table'); |
|
|
|
|
|
|
|
const rows = Array.from(table.querySelectorAll('tbody tr')) |
|
|
|
|
|
|
|
.map(tr => [ |
|
|
|
|
|
|
|
tr.querySelector('.eleve')?.textContent.trim() || '', |
|
|
|
|
|
|
|
tr.querySelector('.appreciation')?.textContent.trim() || '' |
|
|
|
|
|
|
|
]); |
|
|
|
|
|
|
|
doc.autoTable({ |
|
|
|
|
|
|
|
head: [['Élève', 'Appréciation']], |
|
|
|
|
|
|
|
body: rows, |
|
|
|
|
|
|
|
startY: 20, |
|
|
|
|
|
|
|
styles: { fontSize: 10, cellWidth: 'wrap' }, |
|
|
|
|
|
|
|
headStyles: { fillColor: [55, 90, 127] } |
|
|
|
|
|
|
|
}); |
|
|
|
|
|
|
|
doc.save('appreciations.pdf'); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
document.getElementById('export-pdf').addEventListener('click', exportTableToPDF); |
|
|
|
|
|
|
|
</script> |
|
|
|
</body> |
|
|
|
</body> |
|
|
|
</html> |
|
|
|
</html> |
|
|
|
|