// app.jsx — coquille, navigation, persistance, Tweaks. Montage final. const { useState: useStateA, useEffect: useEffectA } = React; const WIZARD = ["company", "reference", "upload", "run", "confirm", "result"]; const VARIANT_MAP = { "Formulaire groupé": "grouped", "Conversationnel": "conversational", "File de cartes": "queue" }; const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{ "questionsVariant": "Formulaire groupé", "accent": "#c8a24a", "density": "regular" }/*EDITMODE-END*/; const PERSIST_KEY = "grant4me.nav.v2"; function App() { const [t, setTweak] = useTweaks(TWEAK_DEFAULTS); // Le dossier courant vient du moteur. Sans backend, il reste null et tous // les écrans basculent en démonstration — le prototype reste navigable. const [projects] = useG4M(() => G4M.projects(), [], []); const [projetCree, setProjetCree] = useStateA(null); const project = projetCree || (projects && projects[0]) || null; // BUG DE PREMIER UTILISATEUR (vu au test bêta 2026-08-07) : un compte neuf // n'a AUCUN dossier, et aucun écran n'en créait — la fiche ne se // persistait pas et le téléversement restait bloqué. « Nouvelle demande » // crée donc le dossier s'il n'existe pas. const ensureProject = async () => { if (project) return project; const p = await G4M.createProject("Ma demande", null); setProjetCree(p); return p; }; const [nav, setNav] = useStateA(() => { try { return { screen: "login", refStage: "intro", ...JSON.parse(localStorage.getItem(PERSIST_KEY) || "{}") }; } catch (e) { return { screen: "login", refStage: "intro" }; } }); const [textScale, setTextScale] = useStateA(() => parseFloat(localStorage.getItem("grant4me.scale") || "1")); useEffectA(() => { try { localStorage.setItem(PERSIST_KEY, JSON.stringify(nav)); } catch (e) {} }, [nav]); useEffectA(() => { document.documentElement.style.setProperty("--scale", textScale); try { localStorage.setItem("grant4me.scale", String(textScale)); } catch (e) {} }, [textScale]); // accent dérivé d'une seule couleur useEffectA(() => { const r = document.documentElement.style; const c = t.accent; r.setProperty("--accent", c); r.setProperty("--accent-2", `color-mix(in oklab, ${c}, #2a1d05 30%)`); r.setProperty("--accent-bg", `color-mix(in oklab, ${c} 15%, var(--paper))`); r.setProperty("--accent-line", `color-mix(in oklab, ${c} 42%, var(--paper))`); }, [t.accent]); // densité → espacements useEffectA(() => { const g = t.density === "compact" ? "16px" : t.density === "comfy" ? "40px" : "clamp(20px,2.4vw,34px)"; document.documentElement.style.setProperty("--gap", g); }, [t.density]); const screen = nav.screen; const go = (screen, extra = {}) => setNav((n) => ({ ...n, screen, ...extra })); const variant = VARIANT_MAP[t.questionsVariant] || "grouped"; const isWizard = WIZARD.includes(screen); const wizIdx = WIZARD.indexOf(screen); // navigation wizard const nextWiz = () => { const i = WIZARD.indexOf(screen); if (i < WIZARD.length - 1) go(WIZARD[i + 1]); }; const prevWiz = () => { const i = WIZARD.indexOf(screen); if (i > 0) go(WIZARD[i - 1]); }; // Le vocabulaire du domaine actif (« demande de subvention » / « déclaration ») // est chargé ici, une fois : tous les écrans l'interpolent ensuite via t(). useDomainLexicon(project && project.id, screen !== "login"); // Aide (bouton « i ») et tutoriel. Le tutoriel se propose UNE fois, au // premier tableau de bord vide — ensuite il vit dans l'aide. const [help, setHelp] = useStateA(false); const [tuto, setTuto] = useStateA(false); const [billing, setBilling] = useStateA(false); return (
go("dashboard")} textScale={textScale} setTextScale={setTextScale} companyName={project && project.companyName} onHelp={() => setHelp(true)} onBilling={() => setBilling(true)} onLogout={async () => { // Le jeton est RÉVOQUÉ côté serveur (pas seulement oublié ici) : // un jeton copié avant la déconnexion ne doit plus rien ouvrir. try { await G4M.logout(); } catch (e) {} try { localStorage.removeItem(PERSIST_KEY); } catch (e) {} setNav({ screen: "login", refStage: "intro" }); }} /> setHelp(false)} onStartTutorial={() => setTuto(true)} /> setTuto(false)} /> setBilling(false)} /> {screen === "login" ? ( go("dashboard")} /> ) : (
{isWizard && } {screen === "dashboard" && { try { await ensureProject(); } catch (e) {} go("company"); }} onOpen={() => go("company")} onFirstVisit={() => { if (tutorialPending()) setTuto(true); }} />} {screen === "company" && } {screen === "upload" && ( setNav((n) => ({ ...n, runId: id, _runKey: (n._runKey || 0) + 1 }))} /> )} {screen === "reference" && ( setNav((n) => ({ ...n, refStage: s }))} /> )} {screen === "run" && go("confirm")} onBack={prevWiz} />} {screen === "confirm" && go("result")} onBack={prevWiz} />} {screen === "result" && go("dashboard")} />}
)} setTweak("questionsVariant", v)} />

Comment l'utilisateur valide les champs proposés par l'assistant.

setTweak("accent", v)} /> setTweak("density", v)} />
); } /* ---------- Top bar ---------- */ function TopBar({ screen, onHome, textScale, setTextScale, companyName, onHelp, onBilling, onLogout }) { const showCtx = screen !== "login"; const { t } = useLang(); // La barre est montée AVANT la connexion : interroger `me` une seule fois // au montage renvoyait toujours vide (aucun jeton encore). On redemande // quand le jeton apparaît ou change — sinon l'avatar reste anonyme et le // nom d'entreprise ne s'affiche jamais. const [session] = useG4M(() => G4M.me(), null, [showCtx, G4M.token]); const [menu, setMenu] = useStateA(false); // Fermer au clic ailleurs et à Échap : un menu qu'on ne peut pas refermer // est plus agaçant que pas de menu du tout. useEffectA(() => { if (!menu) return; const fermer = () => setMenu(false); const parEchap = (e) => e.key === "Escape" && setMenu(false); window.addEventListener("click", fermer); window.addEventListener("keydown", parEchap); return () => { window.removeEventListener("click", fermer); window.removeEventListener("keydown", parEchap); }; }, [menu]); const nom = companyName || (session && session.companyName) || ""; // Initiales de la personne connectée. « GL » était celles de la dirigeante // fictive du prototype — afficher les initiales de quelqu'un d'autre en haut // de chaque écran, c'est le genre de détail qu'on finit par ne plus voir. const initiales = ((session && (session.fullName || session.email)) || "") .split(/[\s.@_-]+/).filter(Boolean).slice(0, 2).map((m) => m[0].toUpperCase()).join(""); return (
{showCtx && nom && ( {nom} )} {!showCtx && (
)} {showCtx && (
{t("Texte")}
{/* Le « i » n'était branché sur rien — un bouton mort au sommet de chaque écran. Il ouvre l'aide : FAQ + tutoriel. */} e.stopPropagation()}> {menu && ( {session && (session.fullName || session.email) && {session.fullName || session.email} {session.email && {session.email}} {session.role === "admin" && {t("Administrateur")}} } )}
)}
); } /* ---------- Step rail (orientation) ---------- */ function StepRail({ current, go, refStage }) { const { t } = useLang(); return ( ); } ReactDOM.createRoot(document.getElementById("root")).render();