refactor: decouple Bilan styles and migrate assets to optimized build output
This commit is contained in:
parent
bbe7f2ccee
commit
b8c3647224
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
4
dist/index.html
vendored
4
dist/index.html
vendored
@ -4,8 +4,8 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Bilans de compétences</title>
|
||||
<script type="module" crossorigin src="/assets/index-CPsAkw2K.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-hUIr4MzD.css">
|
||||
<script type="module" crossorigin src="/assets/index-C9dFjS6F.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-CMDmPM3c.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@ -1023,6 +1023,83 @@
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
/* ── Q&A recommandations ── */
|
||||
.ai-questions-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.ai-questions-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.ai-questions-header label {
|
||||
margin: 0;
|
||||
font-size: 12.5px;
|
||||
font-weight: 600;
|
||||
color: var(--text-slate-600);
|
||||
}
|
||||
|
||||
.btn-ask-ai {
|
||||
flex-shrink: 0;
|
||||
height: 30px;
|
||||
padding: 0 12px;
|
||||
border-radius: var(--radius-md);
|
||||
border: 1px solid #a5b4fc;
|
||||
background: #eef2ff;
|
||||
color: #4338ca;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: var(--transition);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.btn-ask-ai:hover:not(:disabled) {
|
||||
background: #e0e7ff;
|
||||
border-color: #818cf8;
|
||||
}
|
||||
|
||||
.btn-ask-ai:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.ai-questions-box {
|
||||
background: #f0f4ff;
|
||||
border: 1px solid #c7d2fe;
|
||||
border-radius: var(--radius-md);
|
||||
padding: 10px 14px;
|
||||
font-size: 13px;
|
||||
color: #334155;
|
||||
white-space: pre-line;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.ai-answers-textarea {
|
||||
width: 100%;
|
||||
min-height: 80px;
|
||||
padding: 10px 14px;
|
||||
border-radius: var(--radius-md);
|
||||
border: 1px solid var(--border-slate-300);
|
||||
background: #ffffff;
|
||||
font-size: 13.5px;
|
||||
font-family: var(--font-sans);
|
||||
outline: none;
|
||||
transition: var(--transition);
|
||||
box-sizing: border-box;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.ai-answers-textarea:focus {
|
||||
border-color: #818cf8;
|
||||
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.app-topbar {
|
||||
height: auto;
|
||||
|
||||
@ -320,6 +320,9 @@ const Bilan = () => {
|
||||
const [aiTone, setAiTone] = useState("neutre");
|
||||
const [aiPrepend, setAiPrepend] = useState(false);
|
||||
const [mistralModel, setMistralModel] = useState("mistral-small-latest");
|
||||
const [aiQuestions, setAiQuestions] = useState("");
|
||||
const [aiAnswers, setAiAnswers] = useState("");
|
||||
const [isQuestionsLoading, setIsQuestionsLoading] = useState(false);
|
||||
const [observation, setObservation] = useState("");
|
||||
const [toasts, setToasts] = useState([]);
|
||||
const [isAiLoading, setIsAiLoading] = useState(false);
|
||||
@ -563,6 +566,82 @@ const Bilan = () => {
|
||||
};
|
||||
}, [currentCp, firstName, items, lastName]);
|
||||
|
||||
const buildBucketText = useCallback((payload) => {
|
||||
const fmt = (label, arr) =>
|
||||
arr.length ? `${label} :\n${arr.map((c) => `- ${c}`).join("\n")}` : "";
|
||||
return [
|
||||
fmt("Performance", payload.performance),
|
||||
fmt("Compétence", payload.competence),
|
||||
fmt("Connaissance", payload.connaissance),
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("\n\n");
|
||||
}, []);
|
||||
|
||||
const generateAIQuestions = useCallback(async () => {
|
||||
if (!MISTRAL_API_KEY) {
|
||||
showStatus("Clé Mistral introuvable.", "error");
|
||||
return;
|
||||
}
|
||||
const payload = buildPayloadForAI();
|
||||
if (!payload.validatedCount) {
|
||||
showStatus("Aucune compétence validée à analyser.", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsQuestionsLoading(true);
|
||||
showStatus("Génération des questions…", "info", true);
|
||||
|
||||
const userMessage = [
|
||||
`CP évalué : ${payload.cp} — ${payload.cpTitle}`,
|
||||
payload.apprenant ? `Apprenant : ${payload.apprenant}` : "",
|
||||
"",
|
||||
"Compétences validées :",
|
||||
buildBucketText(payload),
|
||||
aiNotes.trim() ? `\nNotes du formateur : ${aiNotes.trim()}` : "",
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("\n");
|
||||
|
||||
try {
|
||||
const response = await fetch("https://api.mistral.ai/v1/chat/completions", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${MISTRAL_API_KEY}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: mistralModel,
|
||||
temperature: 0.5,
|
||||
messages: [
|
||||
{
|
||||
role: "system",
|
||||
content: `Tu es ingénieur pédagogique expert du Titre Professionnel CDUI (niveau 6). À partir des compétences validées et des notes du formateur, pose exactement 3 questions ciblées et courtes pour affiner les recommandations pédagogiques du bilan ECF.
|
||||
Les questions doivent aider à cerner : les difficultés rencontrées, les points forts à valoriser, et les axes de progression prioritaires.
|
||||
Format : 3 questions numérotées (1. 2. 3.), directes, sans introduction ni commentaire.`,
|
||||
},
|
||||
{ role: "user", content: userMessage },
|
||||
],
|
||||
}),
|
||||
});
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
throw new Error(`HTTP ${response.status} — ${text}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
const questions = data?.choices?.[0]?.message?.content?.trim() || "";
|
||||
if (!questions) throw new Error("Réponse vide.");
|
||||
setAiQuestions(questions);
|
||||
setAiAnswers("");
|
||||
showStatus("Questions générées — répondez ci-dessous puis lancez la synthèse.", "success");
|
||||
} catch (error) {
|
||||
showStatus(`Erreur : ${error.message || error}`, "error");
|
||||
} finally {
|
||||
dismissProgressToasts();
|
||||
setIsQuestionsLoading(false);
|
||||
}
|
||||
}, [aiNotes, buildBucketText, buildPayloadForAI, dismissProgressToasts, mistralModel, showStatus]);
|
||||
|
||||
const generateAISummaryMistral = useCallback(async () => {
|
||||
if (!MISTRAL_API_KEY) {
|
||||
showStatus("Clé Mistral introuvable. Vérifie ton fichier .env.local (VITE_MISTRAL_API_KEY).", "error");
|
||||
@ -576,22 +655,22 @@ const Bilan = () => {
|
||||
}
|
||||
|
||||
const notesFormateur = aiNotes.trim();
|
||||
const questionsCtx = aiQuestions.trim();
|
||||
const answersCtx = aiAnswers.trim();
|
||||
|
||||
setIsAiLoading(true);
|
||||
showStatus("Génération de la synthèse ECF via Mistral…", "info", true);
|
||||
|
||||
const formatBucket = (label, items) =>
|
||||
items.length ? `${label} :\n${items.map((c) => `- ${c}`).join("\n")}` : "";
|
||||
|
||||
const userMessage = [
|
||||
`CP évalué : ${payload.cp} — ${payload.cpTitle}`,
|
||||
payload.apprenant ? `Apprenant : ${payload.apprenant}` : "",
|
||||
"",
|
||||
"Compétences validées (Acquis / Maîtrisé) :",
|
||||
formatBucket("Performance", payload.performance),
|
||||
formatBucket("Compétence", payload.competence),
|
||||
formatBucket("Connaissance", payload.connaissance),
|
||||
buildBucketText(payload),
|
||||
notesFormateur ? `\nNotes du formateur : ${notesFormateur}` : "",
|
||||
questionsCtx && answersCtx
|
||||
? `\nQuestions posées au formateur :\n${questionsCtx}\n\nRéponses du formateur :\n${answersCtx}`
|
||||
: "",
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("\n");
|
||||
@ -609,13 +688,14 @@ const Bilan = () => {
|
||||
messages: [
|
||||
{
|
||||
role: "system",
|
||||
content: `Tu es ingénieur pédagogique expert du Titre Professionnel CDUI (Concepteur Développeur d'Interfaces Utilisateur, niveau 6). Tu rédiges des synthèses pour les bilans ECF (Évaluation en Cours de Formation), documents officiels du Ministère du Travail.
|
||||
content: `Tu es ingénieur pédagogique expert du Titre Professionnel CDUI (Concepteur Développeur d'Interfaces Utilisateur, niveau 6). Tu rédiges des bilans ECF (Évaluation en Cours de Formation), documents officiels du Ministère du Travail.
|
||||
|
||||
À partir des compétences validées (Acquis et Maîtrisé) organisées par Performance, Compétence et Connaissance, rédige une synthèse ECF en 4 à 5 lignes maximum.
|
||||
- Réponds aux attendus officiels du CP évalué.
|
||||
- Formule un jugement pédagogique global : ne liste pas les critères un à un.
|
||||
- Sois synthétique et précis, sans fioritures ni répétitions.
|
||||
- Adopte un registre professionnel et factuel, adapté à un document officiel.`,
|
||||
À partir des compétences validées, des notes du formateur et — si disponibles — des réponses du formateur à tes questions, produis un texte structuré en deux blocs distincts :
|
||||
|
||||
1. **Synthèse** (3 à 4 lignes) : jugement pédagogique global sur le CP évalué, factuel et professionnel. Ne liste pas les critères un à un.
|
||||
2. **Recommandations** (2 à 3 lignes) : axes de progression prioritaires, ancrés dans les réponses du formateur si elles sont présentes.
|
||||
|
||||
Adopte un registre adapté à un document officiel. Sois synthétique et précis.`,
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
@ -673,7 +753,10 @@ const Bilan = () => {
|
||||
setIsAiLoading(false);
|
||||
}
|
||||
}, [
|
||||
aiAnswers,
|
||||
aiNotes,
|
||||
aiQuestions,
|
||||
buildBucketText,
|
||||
buildPayloadForAI,
|
||||
dismissProgressToasts,
|
||||
insertAISummaryIntoObservation,
|
||||
@ -1106,6 +1189,31 @@ const Bilan = () => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="input-group ai-questions-group">
|
||||
<div className="ai-questions-header">
|
||||
<label>Recommandations — questions IA</label>
|
||||
<button
|
||||
type="button"
|
||||
className="btn-ask-ai"
|
||||
onClick={generateAIQuestions}
|
||||
disabled={isQuestionsLoading || isAiLoading}
|
||||
>
|
||||
{isQuestionsLoading ? "…" : "💬 Générer les questions"}
|
||||
</button>
|
||||
</div>
|
||||
{aiQuestions && (
|
||||
<>
|
||||
<div className="ai-questions-box">{aiQuestions}</div>
|
||||
<textarea
|
||||
className="ai-answers-textarea"
|
||||
placeholder="Vos réponses aux questions ci-dessus…"
|
||||
value={aiAnswers}
|
||||
onChange={(event) => setAiAnswers(event.target.value)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="row-options">
|
||||
<div className="input-group">
|
||||
<label htmlFor="aiTone">Tonalité de l'IA</label>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user