feat: replace inline status with floating toast notifications
This commit is contained in:
+451
-211
File diff suppressed because it is too large
Load Diff
+97
-59
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user