feat: replace inline status with floating toast notifications

This commit is contained in:
sebvtl728
2026-06-21 16:45:38 +02:00
parent 37261ba30d
commit 126c44db78
2 changed files with 604 additions and 326 deletions
+451 -211
View File
File diff suppressed because it is too large Load Diff
+97 -59
View File
@@ -321,9 +321,10 @@ const Bilan = () => {
const [openaiKey, setOpenaiKey] = useState(""); const [openaiKey, setOpenaiKey] = useState("");
const [openaiModel, setOpenaiModel] = useState("gpt-4o-mini"); const [openaiModel, setOpenaiModel] = useState("gpt-4o-mini");
const [observation, setObservation] = useState(""); const [observation, setObservation] = useState("");
const [status, setStatus] = useState(null); const [toasts, setToasts] = useState([]);
const [isAiLoading, setIsAiLoading] = useState(false); const [isAiLoading, setIsAiLoading] = useState(false);
const [dragOverBucket, setDragOverBucket] = useState(null); const [dragOverBucket, setDragOverBucket] = useState(null);
const [showApiConfig, setShowApiConfig] = useState(false);
const draggingIdRef = useRef(null); const draggingIdRef = useRef(null);
useEffect(() => { useEffect(() => {
@@ -343,22 +344,18 @@ const Bilan = () => {
); );
const showStatus = useCallback((message, type = "info", withProgress = false) => { const showStatus = useCallback((message, type = "info", withProgress = false) => {
setStatus({ const id = Date.now();
id: Date.now(), setToasts((prev) => [...prev, { id, message, type, withProgress }]);
message, if (!withProgress) {
type, setTimeout(() => {
withProgress, setToasts((prev) => prev.filter((t) => t.id !== id));
}); }, type === "info" ? 3000 : 5000);
}
}, []); }, []);
useEffect(() => { const dismissProgressToasts = useCallback(() => {
if (!status || status.withProgress) return; setToasts((prev) => prev.filter((t) => !t.withProgress));
const timeout = setTimeout( }, []);
() => setStatus(null),
status.type === "info" ? 3000 : 5000
);
return () => clearTimeout(timeout);
}, [status]);
const updateItem = useCallback((id, updater) => { const updateItem = useCallback((id, updater) => {
setItems((prev) => setItems((prev) =>
@@ -683,12 +680,13 @@ ${notesFormateur}`,
} catch (error) { } catch (error) {
showStatus(`Erreur OpenAI : ${error.message || error}`, "error"); showStatus(`Erreur OpenAI : ${error.message || error}`, "error");
} finally { } finally {
setStatus((prev) => (prev ? { ...prev, withProgress: false } : prev)); dismissProgressToasts();
setIsAiLoading(false); setIsAiLoading(false);
} }
}, [ }, [
aiNotes, aiNotes,
buildPayloadForAI, buildPayloadForAI,
dismissProgressToasts,
insertAISummaryIntoObservation, insertAISummaryIntoObservation,
observation, observation,
openaiKey, openaiKey,
@@ -909,9 +907,9 @@ ${notesFormateur}`,
} catch (error) { } catch (error) {
showStatus(`Erreur PDF : ${error.message || error}`, "error"); showStatus(`Erreur PDF : ${error.message || error}`, "error");
} finally { } finally {
setStatus((prev) => (prev ? { ...prev, withProgress: false } : prev)); dismissProgressToasts();
} }
}, [currentCp, firstName, lastName, observation, showStatus]); }, [currentCp, dismissProgressToasts, firstName, lastName, observation, showStatus]);
const currentTitle = titles[currentCp] || { h1: "", h2: "" }; const currentTitle = titles[currentCp] || { h1: "", h2: "" };
@@ -1057,41 +1055,61 @@ ${notesFormateur}`,
})} })}
</div> </div>
<div className="ai"> <div className="evaluation-summary-section">
<strong>Résumé</strong> <h3>Synthèse & Observations</h3>
<div className="row">
<div> <div className="summary-grid">
<label htmlFor="aiNotes">Notes formateur</label> {/* Colonne gauche : Saisie et Actions */}
<div className="summary-inputs-panel">
<div className="input-group">
<label htmlFor="aiNotes">Notes du formateur</label>
<textarea <textarea
id="aiNotes" id="aiNotes"
placeholder="Saisissez des commentaires sur le comportement, le travail, etc."
value={aiNotes} value={aiNotes}
onChange={(event) => setAiNotes(event.target.value)} onChange={(event) => setAiNotes(event.target.value)}
/> />
</div> </div>
<div>
<label htmlFor="aiTone">Tonalité</label> <div className="row-options">
<div className="input-group">
<label htmlFor="aiTone">Tonalité de l'IA</label>
<select <select
id="aiTone" id="aiTone"
value={aiTone} value={aiTone}
onChange={(event) => setAiTone(event.target.value)} onChange={(event) => setAiTone(event.target.value)}
> >
<option value="neutre">Neutre</option> <option value="neutre">Neutre 😐</option>
<option value="valorisant">Valorisant</option> <option value="valorisant">Valorisant 🌟</option>
<option value="exigeant">Exigeant</option> <option value="exigeant">Exigeant 🎯</option>
</select> </select>
<label className="checkbox-inline"> </div>
<div className="input-group-checkbox">
<label className="checkbox-label">
<input <input
id="aiPrepend" id="aiPrepend"
type="checkbox" type="checkbox"
checked={aiPrepend} checked={aiPrepend}
onChange={(event) => setAiPrepend(event.target.checked)} onChange={(event) => setAiPrepend(event.target.checked)}
/>{" "} />
Insérer en tête <span>Insérer en tête du bilan</span>
</label> </label>
</div> </div>
</div> </div>
<div className="row">
<div> <div className="api-config-section">
<button
type="button"
className="api-toggle-btn"
onClick={() => setShowApiConfig(!showApiConfig)}
>
{showApiConfig ? "▲ Masquer les paramètres API" : "⚙️ Configuration API OpenAI (Optionnelle)"}
</button>
{showApiConfig && (
<div className="api-fields-grid">
<div className="input-group">
<label htmlFor="openaiKey">Clé API OpenAI</label> <label htmlFor="openaiKey">Clé API OpenAI</label>
<input <input
id="openaiKey" id="openaiKey"
@@ -1101,7 +1119,7 @@ ${notesFormateur}`,
onChange={(event) => setOpenaiKey(event.target.value)} onChange={(event) => setOpenaiKey(event.target.value)}
/> />
</div> </div>
<div> <div className="input-group">
<label htmlFor="openaiModel">Modèle</label> <label htmlFor="openaiModel">Modèle</label>
<select <select
id="openaiModel" id="openaiModel"
@@ -1116,55 +1134,75 @@ ${notesFormateur}`,
</select> </select>
</div> </div>
</div> </div>
<div className="controls ai-actions"> )}
<button type="button" onClick={handleGenerateObservation}> </div>
Résumé...
<div className="action-buttons-group">
<button
type="button"
className="btn-secondary"
onClick={handleGenerateObservation}
>
📝 Générer le Bilan
</button> </button>
<button <button
type="button" type="button"
className="btn-primary"
onClick={generateAISummaryOpenAI} onClick={generateAISummaryOpenAI}
disabled={isAiLoading} disabled={isAiLoading}
> >
Générer l'observation ✨ Résumer avec l'IA
</button> </button>
{isAiLoading && (
<div className="ai-loader" aria-live="polite">
<span className="spinner" />
<span>Génération en cours</span>
</div>
)}
{anySelected && ( {anySelected && (
<button type="button" onClick={downloadPDF}> <button
Exporter PDF type="button"
className="btn-success"
onClick={downloadPDF}
>
📄 Télécharger le PDF
</button> </button>
)} )}
</div> </div>
</div>
{status && ( {isAiLoading && (
<div className={`status ${status.type}`}> <div className="ai-status-loader">
{status.withProgress ? ( <span className="spinner" />
<> <span>Génération du résumé en cours</span>
<div>{status.message}</div>
<div className="progress">
<div className="progress-bar" />
</div> </div>
</>
) : (
status.message
)} )}
</div> </div>
)}
{/* Colonne droite : Aperçu / Résultat en direct */}
<div className="summary-output-panel">
<label htmlFor="observation">Aperçu du bilan final (Markdown)</label>
<textarea <textarea
id="observation" id="observation"
className="observation-textarea" className="observation-textarea"
placeholder="Observation générée ici…" placeholder="Le bilan d'évaluation généré apparaîtra ici. Vous pouvez également le modifier manuellement."
value={observation} value={observation}
onChange={(event) => setObservation(event.target.value)} onChange={(event) => setObservation(event.target.value)}
/> />
</div> </div>
</div> </div>
</div>
</div>
<div className="toast-container">
{toasts.map((toast) => (
<div key={toast.id} className={`toast toast-${toast.type}`}>
{toast.withProgress ? (
<>
<span>{toast.message}</span>
<div className="toast-progress">
<div className="toast-progress-bar" />
</div>
</>
) : (
<span>{toast.message}</span>
)}
</div>
))}
</div>
</div>
); );
}; };