// questions.jsx — Écran « À confirmer » (étape 5/6) + 3 variantes tweakables. // Le moteur a proposé une valeur pour chaque champ ; l'utilisateur valide ou ajuste. // // CÂBLAGE (2026-08-07, passe « enlever le static ») : les champs viennent du // RUN réel, point — le jeu de champs de démonstration est parti avec data.jsx. // Deux portes y mènent : `missingFields[]` (questions) ou `reviewFields[]` // (révision : tout est rempli, personne n'a confirmé). La confirmation // distingue ce qui a été EXPLICITEMENT validé — c'est cette liste qui porte // l'imputabilité et qui seule nourrit la mémoire d'entreprise. const { useState: useStateQ } = React; /** missingFields (GraphQL) → la forme attendue par les 3 variantes. */ function fieldsFromRun(run) { const WHY = { not_in_context: I18N.t("Cette information ne figure ni dans votre profil ni dans votre dossier."), low_confidence: I18N.t("L'assistant a une proposition, mais préfère votre confirmation."), requires_user_judgment: I18N.t("Cette réponse demande votre jugement et vous engage."), }; return (run && run.missingFields ? run.missingFields : []).map((m) => ({ id: m.fieldName, label: m.question || m.fieldName, explication: WHY[m.whyMissing] || I18N.t("À vérifier avant l'envoi de votre {demande}."), proposition: m.suggestedValue || "", valeur: m.suggestedValue || "", mono: ["number", "date"].includes(m.fieldType), choices: m.choices || null, })); } /** reviewFields (phase awaiting_review) → la même forme, avec l'origine. * Le moteur a tout rempli : ce n'est plus « répondre », c'est RELIRE. */ function fieldsFromReview(run) { const ORIGIN = { context: I18N.t("Repris de votre profil d'entreprise."), memory: I18N.t("Réponse que vous aviez déjà validée sur un autre {formulaire}."), validated: I18N.t("Déjà validé par vous."), inferred: I18N.t("Rédigé par l'IA à partir de votre dossier. À relire."), }; return (run && run.reviewFields ? run.reviewFields : []).map((f) => ({ id: f.fieldName, label: f.label || f.fieldName, explication: ORIGIN[f.origin] || ORIGIN.inferred, proposition: f.value == null ? "" : String(f.value), valeur: f.value == null ? "" : String(f.value), mono: ["number", "date"].includes(f.fieldType), origin: f.origin, })); } function ConfirmScreen({ variant, onNext, onBack, runId }) { const { t } = useLang(); const [run] = useRunPolling(runId); // Deux portes mènent ici : des questions (awaiting_user) ou une révision // finale (awaiting_review — le moteur a tout rempli, personne n'a confirmé). const review = !!(runId && run && run.status === "awaiting_review"); const live = !!(runId && run); const fields = review ? fieldsFromReview(run) : fieldsFromRun(run); const [done, setDone] = useStateQ({}); // id -> true const [vals, setVals] = useStateQ({}); const [sending, setSending] = useStateQ(false); const [err, setErr] = useStateQ(null); // Les valeurs se (re)posent quand la source change — un run qui arrive après // le premier rendu ne doit pas laisser l'écran sur les mocks. React.useEffect(() => { const o = {}; fields.forEach((f) => (o[f.id] = f.proposition || f.valeur)); setVals(o); }, [live, fields.length]); // Aucun champ en suspens (run absent, ou moteur passé directement à done) : // on le dit, on ne fabrique pas des questions pour occuper l'écran. if (!fields.length) { return (

{live ? t("Le moteur n'a laissé aucune question en suspens pour ce {formulaire}.") : t("Téléversez d'abord un {formulaire} à l'étape précédente : c'est lui qui lance le travail.")}

{t("Retour")} {live ? {t("Voir le résultat")} : }
); } const nbDone = fields.filter((f) => done[f.id]).length; const allDone = nbDone === fields.length && fields.length > 0; const confirmField = (id) => setDone((d) => ({ ...d, [id]: true })); const setVal = (id, v) => setVals((x) => ({ ...x, [id]: v })); const submit = async () => { setSending(true); setErr(null); try { if (review) { // Révision : les corrections repartent comme réponses, et la liste des // champs cochés porte l'imputabilité. Rien n'est généré sans ce geste. await G4M.confirmReview({ runId, corrections: vals, validated: fields.filter((f) => done[f.id]).map((f) => f.id), }); onNext(); return; } // `validated` = ce que la personne a explicitement confirmé. Les autres // réponses partent quand même (le formulaire doit être complet) mais // marquées comme non validées côté moteur. await G4M.provideAnswers({ runId, answers: vals, validated: fields.filter((f) => done[f.id]).map((f) => f.id), }); onNext(); } catch (e) { setErr(e.message); } finally { setSending(false); } }; const shared = { fields, done, vals, confirmField, setVal }; return (

{review ? <>{t("Le moteur a rempli ")}{t("{n} champs", { n: fields.length })}{t(" sans avoir besoin de vous poser de question. Relisez-les : ce {formulaire} sera déposé en votre nom, et c'est ")}{t("votre confirmation qui l'engage")}{t(". Corrigez ce que vous voulez.")} : <>{t("L'assistant a ")}{t("déjà proposé une réponse")}{t(" pour chacun de ces {n} champs, à partir de votre fiche projet et de vos documents. Vérifiez simplement que c'est juste et corrigez au besoin.", { n: fields.length })}}

{/* compteur de progression */}
{nbDone} / {fields.length} {t(nbDone > 1 ? "confirmés" : "confirmé")}
{err &&
{t("Envoi impossible :")} {err}
} {variant === "conversational" && } {variant === "queue" && } {(variant === "grouped" || !variant) && }
{t("Retour")} {sending ? t("Envoi…") : allDone ? t("Terminer ma {demande}") : t("Confirmez les {n} champs restants", { n: fields.length - nbDone })}
); } /* ---------- Variante A : formulaire groupé (tout d'un coup) ---------- */ function GroupedVariant({ fields, done, vals, confirmField, setVal }) { return (
{fields.map((f) => { const ok = done[f.id]; return (
{f.label}{f.aide && {f.aide}} {I18N.t("Proposé par l'assistant")}

{f.explication}

setVal(f.id, e.target.value)} aria-label={f.label} /> {ok ? {}}>{I18N.t("Confirmé")} : confirmField(f.id)}>{I18N.t("C'est exact")}}
); })}
); } /* ---------- Variante B : conversationnel (une question à la fois) ---------- */ function ConfVariant({ fields, done, vals, confirmField, setVal }) { const [idx, setIdx] = useStateQ(0); const [edit, setEdit] = useStateQ(false); const f = fields[Math.min(idx, fields.length - 1)]; if (!f) return null; const ok = done[f.id]; const last = idx === fields.length - 1; const advance = () => { setEdit(false); if (!last) setIdx(idx + 1); }; return (
{I18N.t("Question {i} sur {n}", { i: idx + 1, n: fields.length })}
{I18N.t("Assistant Grant4me")}
{f.label}{f.aide && {f.aide}}
{f.explication}
{I18N.t("Ma proposition")}
{edit ?
setVal(f.id, e.target.value)} aria-label={f.label} />
:
{vals[f.id]}
}
{!ok && !edit && { confirmField(f.id); advance(); }}>{I18N.t("Oui, c'est exact")}} {!ok && !edit && setEdit(true)}>{I18N.t("Ajuster")}} {edit && { confirmField(f.id); advance(); }}>{I18N.t("Enregistrer et continuer")}} {ok && !edit &&
{I18N.t("Confirmé")}
} {ok && !last && setIdx(idx + 1)}>{I18N.t("Suivant")}}
{/* fil des réponses confirmées */}
{fields.map((ff, i) => (
); } /* ---------- Variante C : file de cartes (une carte à régler à la fois) ---------- */ function QueueVariant({ fields, done, vals, confirmField, setVal }) { const [idx, setIdx] = useStateQ(0); const f = fields[Math.min(idx, fields.length - 1)]; if (!f) return null; const ok = done[f.id]; const last = idx === fields.length - 1; const next = () => { if (!last) setIdx(idx + 1); }; return (
{I18N.t("Carte {i} sur {n}", { i: idx + 1, n: fields.length })} {fields.map((ff, i) => ( ))}
{!last &&
} {idx < fields.length - 2 &&
}
{f.label}{f.aide && {f.aide}}

{f.explication}

{I18N.t("Proposé par l'assistant")}
setVal(f.id, e.target.value)} aria-label={f.label} style={{ background: ok ? "var(--green-bg)" : "#fff" }} />
idx > 0 && setIdx(idx - 1)} disabled={idx === 0}>{I18N.t("Précédent")} {!ok ? { confirmField(f.id); next(); }}>{I18N.t(last ? "C'est exact, terminer" : "C'est exact, carte suivante")} : {I18N.t(last ? "Toutes confirmées" : "Carte suivante")}}
); } Object.assign(window, { ConfirmScreen });