// Floating Deva chat, onboarding + open chat. Persists chosen role. const ROLE_PROFILES = { pilote: { emoji: '🏡', label: "Porteur de lieu", impactName: "Pilote d'impact", short: 'Porteur de lieu', accent: '#018262', tagline: "Vous portez un lieu (tiers-lieu, Ă©colieu, ferme, association
) et cherchez Ă  le faire rayonner.", section: 'roles', nextCue: 'DĂ©couvrez votre profil et les outils EVAD →' }, batisseur: { emoji: '🌿', label: "Citoyen", impactName: "BĂątisseur d'impact", short: 'Citoyen', accent: '#c8732a', tagline: "Vous voulez agir sur des quĂȘtes concrĂštes et gagner des graines Ă  utiliser dans le rĂ©seau.", section: 'roles', nextCue: 'Voyez les quĂȘtes qui vous attendent →' }, semeur: { emoji: 'đŸŒŸ', label: "Financeur", impactName: "Semeur d'impact", short: 'Financeur', accent: '#3a6e8c', tagline: "Vous soutenez des projets durables (fondation, financeur, collectivitĂ©, investisseur).", section: 'roles', nextCue: 'DĂ©couvrez comment financer contre des preuves →' }, }; const LS_KEY = 'evad.deva.persona'; const LS_DISMISSED = 'evad.deva.onboarding.dismissed'; const LS_FEEDBACK = 'evad.deva.feedback'; const LS_USAGE = 'evad.deva.usage'; // ─────────── Supabase : retours & questions Deva ─────────── // MĂȘmes identifiants que Hero.jsx (la clĂ© "anon" est PUBLIQUE par conception ; // la sĂ©curitĂ© vient des rĂšgles RLS « insert seul » cĂŽtĂ© Supabase). // → Voir INSTALLATION-deva-supabase.md pour crĂ©er les 2 tables (deva_feedback, deva_questions). const SUPABASE_URL = 'https://lmhhrccmgebztioesmik.supabase.co'; const SUPABASE_ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImxtaGhyY2NtZ2VienRpb2VzbWlrIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjUzMjIyOTgsImV4cCI6MjA4MDg5ODI5OH0.epfoBIsZJHLqj96dYE7AvImK_EgjMW9PFtvLk4VwlDc'; const SB_CONFIGURED = !SUPABASE_URL.includes('VOTRE-PROJET') && !SUPABASE_ANON_KEY.includes('COLLEZ'); const curPage = () => (typeof window !== 'undefined' ? window.location.pathname : ''); // Enregistrement « best-effort » : on n'attend pas la rĂ©ponse et on n'interrompt // JAMAIS l'expĂ©rience visiteur si Supabase est indisponible. function devaLog(table, rows) { if (!SB_CONFIGURED || !rows || (Array.isArray(rows) && rows.length === 0)) return; try { fetch(SUPABASE_URL + '/rest/v1/' + table, { method: 'POST', headers: { 'apikey': SUPABASE_ANON_KEY, 'Authorization': 'Bearer ' + SUPABASE_ANON_KEY, 'Content-Type': 'application/json', 'Prefer': 'return=minimal', }, body: JSON.stringify(rows), }).catch(() => {}); } catch (e) { /* silencieux */ } } // Empreinte estimĂ©e d'UNE question Ă  l'IA (ordre de grandeur, modĂšle frugal type // Mistral + rĂ©ponses courtes). AffichĂ© comme estimation, pour sensibiliser. const ECO_PER_MSG = { wh: 1.5, water: 18, co2: 0.9 }; // Wh, mL d'eau, g CO₂e const fmtWater = (ml) => ml >= 1000 ? (ml / 1000).toFixed(ml >= 10000 ? 1 : 2).replace('.', ',') + ' L' : Math.round(ml) + ' mL'; const fmtCo2 = (g) => g >= 1000 ? (g / 1000).toFixed(2).replace('.', ',') + ' kg' : (g < 10 ? g.toFixed(1).replace('.', ',') : Math.round(g)) + ' g'; const fmtWh = (wh) => wh >= 1000 ? (wh / 1000).toFixed(2).replace('.', ',') + ' kWh' : (wh < 10 ? wh.toFixed(1).replace('.', ',') : Math.round(wh)) + ' Wh'; // Compensation : Deva propose des gestes concrets ancrĂ©s dans l'Ă©cosystĂšme EVAD. const OFFSET_THRESHOLD = 10; // questions avant la suggestion spontanĂ©e const DON_URL = 'https://www.helloasso.com/associations/evad-connect/formulaires/1'; const MEMBER_URL = 'https://www.helloasso.com/associations/evad-connect/adhesions/devenir-membre-2026'; const OFFSET_TEXT = "🌿 On a bien Ă©changĂ© ! Si vous souhaitez Ă©quilibrer l'empreinte de notre conversation, voici quelques gestes concrets. Et cĂŽtĂ© sobriĂ©tĂ©, des questions prĂ©cises suffisent souvent đŸŒ±"; const OFFSET_CTAS = [ { label: '🌳 Soutenir un lieu pilote', href: DON_URL }, { label: 'đŸŒ± Devenir membre & agir', href: MEMBER_URL }, { label: "☀ D'autres façons d'agir", section: 'agir' }, ]; // Endpoint du backend Deva en production : deva.php sur evad.org (Hostinger → Mistral). // En aperçu/local, cet appel Ă©choue → repli automatique sur le pont de prototype. const DEVA_ENDPOINT = 'https://evad.org/deva.php'; // Prompt systĂšme utilisĂ© UNIQUEMENT par le repli d'aperçu. // En production, c'est deva.php qui construit le prompt + injecte la documentation. function buildSystemPrompt(persona) { return `Tu es Deva, l'esprit rĂ©gĂ©nĂ©ratif et compagnon IA de l'Ă©cosystĂšme EVAD (ÉcosystĂšme Vivant Autonome & DĂ©centralisĂ©). EVAD relie trois rĂŽles : Pilotes d'impact (porteurs de lieux durables), BĂątisseurs d'impact (citoyens qui agissent via des quĂȘtes) et Semeurs d'impact (financeurs). Le mouvement s'appuie sur quatre piliers : Solarpunk (vision), Économie rĂ©gĂ©nĂ©rative (boussole), Écocratie (gouvernance) et Gamification (engagement avec quĂȘtes, preuves, graines et score REGEN). EVAD est portĂ© par l'association EVAD Connect. ${persona ? `Le visiteur est ${ROLE_PROFILES[persona].label}. Adapte tes rĂ©ponses Ă  ce profil.` : ''} RĂ©ponds en français, avec chaleur et concision (2-4 phrases max), au vouvoiement. Pas d'emojis sauf 🌿 occasionnel. Si la question dĂ©passe EVAD, redirige gentiment.`; } // Appelle d'abord le backend rĂ©el (production) ; en cas d'absence (aperçu), // repli transparent sur le pont de prototype window.claude.complete. async function devaComplete(history, persona) { // 1) Backend Mistral sur evad.org try { const res = await fetch(DEVA_ENDPOINT, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ persona: persona || null, messages: history.map(m => ({ role: m.role, text: m.text })), }), }); if (res.ok) { const data = await res.json(); if (data && data.reply) return String(data.reply).trim(); } } catch (e) { /* deva.php absent (aperçu) → repli ci-dessous */ } // 2) Repli aperçu (sandbox) if (window.claude && typeof window.claude.complete === 'function') { const conv = history .filter(m => m.role !== 'system') .map(h => (h.role === 'user' ? 'Visiteur: ' : 'Deva: ') + h.text) .join('\n'); const reply = await window.claude.complete({ messages: [ { role: 'user', content: buildSystemPrompt(persona) + '\n\n--- Conversation ---\n' + conv + '\nDeva:' }, ], }); return String(reply).trim(); } throw new Error('Aucun backend disponible'); } // Rendu lĂ©ger : convertit le Markdown **gras** renvoyĂ© par l'IA en vrai gras. function renderRich(text) { return String(text).split(/(\*\*[^*]+\*\*)/g).map((p, i) => { const m = /^\*\*([^*]+)\*\*$/.exec(p); return m ? {m[1]} : {p}; }); } const FEEDBACK_PROMPTS = [ "Qu'est-ce qui vous a manquĂ© sur cette page ?", "Si vous pouviez changer une chose, ce serait quoi ?", "Une derniĂšre idĂ©e folle pour faire grandir EVAD ?", ]; const DevaChat = ({ role, setRole, onPersonaChange }) => { // ───── persisted persona ───── const [persona, setPersona] = React.useState(() => { try { const v = localStorage.getItem(LS_KEY); return (v && v !== 'null') ? v : null; } catch { return null; } }); // ───── widget state ───── const [open, setOpen] = React.useState(false); const [teaser, setTeaser] = React.useState(false); const [unread, setUnread] = React.useState(true); const [stage, setStage] = React.useState(persona ? 'chat' : 'chat'); // onboard-ask | onboard-confirm | chat | feedback const [feedbackStep, setFeedbackStep] = React.useState(0); const [feedbackAnswers, setFeedbackAnswers] = React.useState([]); const [draft, setDraft] = React.useState(''); const [busy, setBusy] = React.useState(false); // Empreinte : repart de zĂ©ro Ă  chaque ouverture de page (pas de cumul entre visites). const [queryCount, setQueryCount] = React.useState(0); const [ecoOpen, setEcoOpen] = React.useState(false); const offsetFiredRef = React.useRef(false); const scrollRef = React.useRef(null); // Bulle d'accroche : apparaĂźt Ă  chaque chargement de page (chat fermĂ©). React.useEffect(() => { const t = setTimeout(() => setTeaser(true), 1100); return () => clearTimeout(t); }, []); const openChat = () => { setOpen(true); setTeaser(false); setUnread(false); }; const dismissTeaser = (e) => { if (e) { e.stopPropagation(); } setTeaser(false); }; const [messages, setMessages] = React.useState(() => { if (persona) { const p = ROLE_PROFILES[persona]; return [ { role: 'deva', text: `Ravi de vous revoir 🌿 Vous ĂȘtes ${p.label}. Une question sur l'Ă©cosystĂšme ?` }, ]; } return [ { role: 'deva', text: "Bonjour ! Je suis Deva, l'esprit rĂ©gĂ©nĂ©ratif d'EVAD. Explorez le site librement, je reste lĂ  pour rĂ©pondre Ă  vos questions." }, ]; }); // ───── scroll messages on update ───── React.useEffect(() => { if (open) { setUnread(false); setTimeout(() => { if (scrollRef.current) scrollRef.current.scrollTop = scrollRef.current.scrollHeight; }, 50); } }, [open, messages.length, busy, stage]); // ───── propose compensation (manual + automatic after threshold) ───── const proposeOffset = () => { setEcoOpen(false); setStage(s => (s === 'onboard-ask' ? 'chat' : s)); setMessages(m => { // Ă©vite un doublon si la proposition est dĂ©jĂ  le dernier message Deva const last = m[m.length - 1]; if (last && last.role === 'deva' && last.text === OFFSET_TEXT) return m; return [...m, { role: 'deva', text: OFFSET_TEXT, ctas: OFFSET_CTAS }]; }); }; React.useEffect(() => { if (queryCount < OFFSET_THRESHOLD || offsetFiredRef.current || busy) return; offsetFiredRef.current = true; setMessages(m => [...m, { role: 'deva', text: OFFSET_TEXT, ctas: OFFSET_CTAS }]); }, [queryCount, busy]); // ───── pick a role ───── const pickRole = (id) => { const p = ROLE_PROFILES[id]; setPersona(id); try { localStorage.setItem(LS_KEY, id); } catch {} if (typeof setRole === 'function') setRole(id); if (typeof onPersonaChange === 'function') onPersonaChange(id); setMessages(m => [ ...m, { role: 'user', text: `${p.emoji} Je suis ${p.short}` }, { role: 'deva', text: `Parfait. ${p.tagline}\n\nJ'ai adaptĂ© la page pour vous : votre boucle REGEN et vos outils EVAD sont dĂ©jĂ  personnalisĂ©s.`, ctas: [ { label: 'Voir ma boucle REGEN', section: 'cycle' }, { label: 'Voir les outils EVAD', section: 'ecosystem' }, ] }, ]); setStage('onboard-confirm'); }; const skipOnboarding = () => { try { localStorage.setItem(LS_DISMISSED, '1'); } catch {} setStage('chat'); setMessages(m => [...m, { role: 'deva', text: "Pas de souci. Posez-moi n'importe quelle question sur EVAD." }]); }; // ───── feedback flow ───── const startFeedback = () => { setStage('feedback'); setFeedbackStep(0); setFeedbackAnswers([]); setMessages(m => [ ...m, { role: 'deva', text: "đŸŒ± Aidez-nous Ă  faire pousser ce site. Je vais vous poser 3 petites questions, rĂ©pondez librement (ou tapez «passer» pour sauter).\n\n1ïžâƒŁ " + FEEDBACK_PROMPTS[0] }, ]); }; const submitFeedback = (answer) => { const newAnswers = [...feedbackAnswers, { q: FEEDBACK_PROMPTS[feedbackStep], a: answer }]; setFeedbackAnswers(newAnswers); setMessages(m => [...m, { role: 'user', text: answer }]); const nextStep = feedbackStep + 1; if (nextStep < FEEDBACK_PROMPTS.length) { setFeedbackStep(nextStep); const numberEmoji = ['1ïžâƒŁ', '2ïžâƒŁ', '3ïžâƒŁ'][nextStep] || (nextStep + 1); setMessages(m => [...m, { role: 'deva', text: `${numberEmoji} ${FEEDBACK_PROMPTS[nextStep]}` }]); } else { // persist locally try { const existing = JSON.parse(localStorage.getItem(LS_FEEDBACK) || '[]'); existing.push({ ts: new Date().toISOString(), persona, page: typeof window !== 'undefined' ? window.location.pathname : '', answers: newAnswers, }); localStorage.setItem(LS_FEEDBACK, JSON.stringify(existing)); } catch {} // Envoi centralisĂ© vers Supabase (table deva_feedback) : 1 ligne par rĂ©ponse, // en ignorant les « passer » / rĂ©ponses vides. devaLog('deva_feedback', newAnswers .filter(a => a.a && a.a.trim() && a.a.trim().toLowerCase() !== 'passer') .map(a => ({ persona, page: curPage(), question: a.q, answer: a.a.trim() }))); setMessages(m => [ ...m, { role: 'deva', text: "🌿 Merci, vos graines sont plantĂ©es. Chaque retour nourrit la prochaine version d'EVAD. Vous pouvez continuer la conversation ou refermer la fenĂȘtre." }, ]); setStage('chat'); setFeedbackStep(0); } setDraft(''); }; // ───── free chat ───── const ask = async (text) => { if (!text.trim() || busy) return; const userMsg = { role: 'user', text: text.trim() }; setMessages(m => [...m, userMsg]); setDraft(''); setQueryCount(c => c + 1); setBusy(true); try { const reply = await devaComplete([...messages, userMsg], persona); setMessages(m => [...m, { role: 'deva', text: reply }]); // Journalise la question + la rĂ©ponse (table deva_questions) pour repĂ©rer // les sujets cherchĂ©s et combler les manques de documentation-evad.txt. devaLog('deva_questions', [{ persona, page: curPage(), question: userMsg.text, answer: reply }]); } catch (e) { setMessages(m => [...m, { role: 'deva', text: "Je n'ai pas pu rĂ©pondre, rĂ©essayez dans un instant." }]); } finally { setBusy(false); } }; const suggestedAfter = persona ? [ persona === 'pilote' ? 'Comment rĂ©fĂ©rencer mon lieu ?' : persona === 'batisseur' ? 'Quelles quĂȘtes me correspondent ?' : 'Comment financer un projet ?', 'Comment fonctionne le score REGEN ?', 'Comment fonctionnent les graines ?', ] : []; const resetPersona = () => { try { localStorage.removeItem(LS_KEY); localStorage.removeItem(LS_DISMISSED); } catch {} setPersona(null); if (typeof onPersonaChange === 'function') onPersonaChange(null); setStage('onboard-ask'); setMessages([{ role: 'deva', text: "On recommence : qui ĂȘtes-vous ?" }]); }; return ( <> {/* ─── Floating launcher ─── */} {/* ─── Bulle d'accroche (avatar fermĂ©) ─── */} {!open && teaser && (
{ if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); openChat(); } }} aria-label="Ouvrir la conversation avec Deva" style={{ position: 'fixed', bottom: 112, left: 22, zIndex: 81, width: 252, maxWidth: 'calc(100vw - 44px)', background: 'linear-gradient(160deg, #0d2b22 0%, #013b2d 100%)', border: '1px solid rgba(126,201,176,.2)', color: '#e8f7f3', borderRadius: 18, borderBottomLeftRadius: 6, padding: '14px 16px 14px 15px', boxShadow: '0 18px 40px rgba(13,43,34,.32), 0 4px 12px rgba(1,130,98,.2)', cursor: 'pointer', fontFamily: "'Satoshi', sans-serif", animation: 'devateaser .4s cubic-bezier(.2,1.1,.4,1) both', transformOrigin: 'bottom left', }}> {/* Queue de bulle pointant vers l'avatar */}
)} {/* Persona badge removed, access reset via chat header */} {/* ─── Panel ─── */} {open && (
{/* Header */}
Deva
Deva
En ligne · IA frugale
{/* ─── Eco footprint counter ─── */}
{ecoOpen && (
{[ { ic: '⚡', val: fmtWh(queryCount * ECO_PER_MSG.wh), lbl: 'Énergie' }, { ic: '💧', val: fmtWater(queryCount * ECO_PER_MSG.water), lbl: 'Eau' }, { ic: 'đŸŒ«ïž', val: fmtCo2(queryCount * ECO_PER_MSG.co2), lbl: 'CO₂e' }, ].map((s, i) => (
{s.val} {s.lbl}
))}

Chaque question Ă  une IA a un coĂ»t rĂ©el en Ă©nergie, en eau et en carbone. Deva s'appuie sur un modĂšle frugal et des rĂ©ponses courtes pour le limiter. PrivilĂ©giez des questions prĂ©cises 🌿

Estimation indicative Ă  titre de sensibilisation : ~{ECO_PER_MSG.wh} Wh, {ECO_PER_MSG.water} mL d'eau et {ECO_PER_MSG.co2} g CO₂e par question (ordre de grandeur, modĂšle d'IA frugal). Ne reflĂšte pas une mesure rĂ©elle.

)}
{/* Messages */}
{messages.map((m, i) => (
{renderRich(m.text)}
{m.ctas && m.ctas.length > 0 && (
{m.ctas.map((cta, j) => ( ))}
)}
))} {busy && (
)} {/* Onboarding chips */} {false && stage === 'onboard-ask' && !busy && (
{Object.entries(ROLE_PROFILES).map(([id, p]) => ( ))}
)} {/* Post-onboarding suggested questions */} {stage !== 'onboard-ask' && messages.length <= 4 && !busy && suggestedAfter.length > 0 && (
Suggestions
{suggestedAfter.map(q => ( ))} {persona && ( )}
)}
{/* ─── Footer action (above input) ─── */} {stage !== 'feedback' && stage !== 'onboard-ask' && (
)} {/* Input */}
{ e.preventDefault(); const text = draft.trim(); if (!text) return; if (stage === 'feedback') { submitFeedback(text); } else { // Pendant le choix de profil, on laisse les cartes visibles // (on ne bascule pas en 'chat') tout en rĂ©pondant aux questions. if (stage !== 'onboard-ask') setStage('chat'); ask(text); } }} style={{ padding: 10, borderTop: '1px solid rgba(126,201,176,.16)', background: 'rgba(13,43,34,.4)', display: 'flex', gap: 8, alignItems: 'center', }}> setDraft(e.target.value)} placeholder={stage === 'onboard-ask' ? 'Choisissez un profil ou posez une question
' : stage === 'feedback' ? 'Votre rĂ©ponse
 (ou «passer»)' : 'Demandez Ă  Deva
'} disabled={busy} style={{ flex: 1, background: 'rgba(13,43,34,.5)', border: '1px solid rgba(126,201,176,.2)', borderRadius: 12, padding: '11px 14px', color: '#e8f7f3', fontFamily: "'Satoshi', sans-serif", fontSize: 14, outline: 'none', transition: 'border-color .15s, box-shadow .15s', opacity: 1, }} onFocus={e => { e.currentTarget.style.borderColor = 'rgba(126,201,176,.5)'; e.currentTarget.style.boxShadow = '0 0 0 3px rgba(126,201,176,.12)'; }} onBlur={e => { e.currentTarget.style.borderColor = 'rgba(126,201,176,.2)'; e.currentTarget.style.boxShadow = 'none'; }} />
)} ); }; window.DevaChat = DevaChat;