// 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); return (
go("dashboard")} textScale={textScale} setTextScale={setTextScale} companyName={project && project.companyName} onHelp={() => setHelp(true)} /> setHelp(false)} onStartTutorial={() => setTuto(true)} /> setTuto(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 }) { const showCtx = screen !== "login"; const { t } = useLang(); const [session] = useG4M(() => G4M.me(), null, []); 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. */} {initiales && {initiales}}
)}
); } /* ---------- Step rail (orientation) ---------- */ function StepRail({ current, go, refStage }) { const { t } = useLang(); return ( ); } ReactDOM.createRoot(document.getElementById("root")).render();