// 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 */}
{/* Bouton fermer */}
Deva
Bonjour ! Une question sur EVAD ? Cliquez ici, je vous réponds.
)}
{/* Persona badge removed, access reset via chat header */}
{/* âââ Panel âââ */}
{open && (
{/* Header */}
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.ic}
{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) => (
{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 */}
)}
>
);
};
window.DevaChat = DevaChat;