const { useState, useEffect, useRef, useMemo, useCallback } = React; // ── CONFIG ────────────────────────────────────────────────────────────────── // Paste your project values (Dashboard → Settings → API). The anon key is // designed to be public; the LLM key lives only in the edge function. const SUPABASE_URL = "https://gxiwgcrmuudutcvmrlui.supabase.co"; // e.g. https://abcd1234.supabase.co const SUPABASE_ANON_KEY = "sb_publishable_yW4wEfjVBrY8gUCtuN4Glw_Yq0vNcMI"; const sb = supabase.createClient(SUPABASE_URL, SUPABASE_ANON_KEY); // ── CONSTANTS ─────────────────────────────────────────────────────────────── const DOMAINS = [ { id:"logical_reasoning", label:"Logical Reasoning", color:"#2563EB", blurb:"Deduction, inference, argument structure" }, { id:"mathematical", label:"Mathematical Thinking", color:"#7C3AED", blurb:"Quantitative reasoning and abstraction" }, { id:"scientific_reasoning",label:"Scientific Reasoning", color:"#0891B2", blurb:"Hypotheses, evidence, causal thinking" }, { id:"verbal_linguistic", label:"Verbal & Linguistic", color:"#D97706", blurb:"Language precision and comprehension" }, { id:"systems_thinking", label:"Systems Thinking", color:"#16A34A", blurb:"Feedback loops, interdependence, dynamics" }, { id:"metacognition", label:"Metacognition", color:"#DC2626", blurb:"Thinking about your own thinking" }, ]; const SUBJECT_COLORS = ["#0E6E66","#5D55C4","#A66B00","#1C8A56","#BC4430","#2563EB"]; // ── LOGO — iris vortex mark ───────────────────────────────────────────────── const Logo = ({ size=26, color="var(--brand)" }) => ( // Aperture mark — nested arcs narrowing to a focal point, reading as a lens // bringing thought into focus. Deliberately asymmetric (open gap, upward // gnomon) so it never resolves into a rotational/cross figure. ); // ── IRIS RING — signature mastery indicator ───────────────────────────────── const Ring = ({ value=0, size=30, stroke=3.5, color="var(--brand)", track="var(--line)", label }) => { const r=(size-stroke)/2, c=2*Math.PI*r, off=c*(1-Math.max(0,Math.min(100,value))/100); return (
{label!==undefined&&
{label}
}
); }; // ── P — pure profile math ─────────────────────────────────────────────────── class P { static defDomain(){ return { score:50, sessions:0, history:[], traj:"forming" }; } static emptyState(name){ const brain={}; DOMAINS.forEach(d=>{ brain[d.id]=P.defDomain(); }); return { name:name||"", brain, subjects:[], modality:{dominant:"logical_sequential",weights:{visual:.25,logical_sequential:.25,narrative:.25,kinesthetic:.25},confidence:.1}, accuracy:[], sessions:0, totalEx:0, questionTypePerf:{}, feedbackSignal:[] }; } static fromRows(userRow, pfRow, subjects){ const s=P.emptyState(userRow?.name||""); if(pfRow){ DOMAINS.forEach(d=>{ if(pfRow.brain_scores?.[d.id]) s.brain[d.id]={...P.defDomain(),...pfRow.brain_scores[d.id]}; }); s.modality=pfRow.modality||s.modality; s.accuracy=pfRow.accuracy_history||[]; s.sessions=pfRow.session_count||0; s.totalEx=pfRow.total_exercises||0; s.questionTypePerf=pfRow.question_type_perf||{}; s.feedbackSignal=pfRow.feedback_signal||[]; } s.subjects=subjects; return s; } static brainScore(p){ const v=DOMAINS.map(d=>p.brain[d.id]?.score||50); return Math.round(v.reduce((a,b)=>a+b,0)/v.length); } static recentAcc(p){ const a=(p.accuracy||[]).slice(-12); return a.length?Math.round(a.reduce((x,y)=>x+y,0)/a.length*100):null; } static traj(history){ const h=(history||[]).slice(-5); if(h.length<3) return "forming"; const k=h.filter(x=>x.correct).length/h.length; return k>=0.7?"improving":k<=0.35?"declining":"stable"; } static updateBrain(p, domainId, correct){ const d={...(p.brain[domainId]||P.defDomain())}; d.score=Math.max(0,Math.min(100,d.score+(correct?3.5:-2))); d.sessions=(d.sessions||0)+1; d.history=[...(d.history||[]),{correct,t:Date.now()}].slice(-30); d.traj=P.traj(d.history); p.brain={...p.brain,[domainId]:d}; p.accuracy=[...(p.accuracy||[]),correct?1:0].slice(-50); p.totalEx=(p.totalEx||0)+1; } static updateTopic(p, subject, topicId, correct){ const tp={...(subject.tp[topicId]||{mastery:0,correct:0,total:0,skipped:false})}; tp.total++; if(correct) tp.correct++; tp.mastery=Math.max(0,Math.min(100,Math.round(tp.mastery+(correct?(tp.mastery<50?14:9):-6)))); tp.last=Date.now(); subject.tp={...subject.tp,[topicId]:tp}; p.accuracy=[...(p.accuracy||[]),correct?1:0].slice(-50); p.totalEx=(p.totalEx||0)+1; return tp; } static recordQType(p, type, correct){ const qt={...(p.questionTypePerf||{})}; const r={...(qt[type]||{correct:0,total:0})}; r.total++; if(correct) r.correct++; qt[type]=r; p.questionTypePerf=qt; } static recordFeedback(p, difficulty){ p.feedbackSignal=[...(p.feedbackSignal||[]),difficulty].slice(-20); } static orderedTopics(sub){ return [...(sub.curriculum?.topics||[])].sort((a,b)=>(a.section_index-b.section_index)||(a.position-b.position)); } static topicStatus(sub, topicId){ const ord=P.orderedTopics(sub); const i=ord.findIndex(t=>t.id===topicId); if(i<0) return "locked"; const tp=sub.tp[topicId]; if(tp?.skipped||(tp?.mastery||0)>=70) return "complete"; if((tp?.mastery||0)>0||tp?.total>0) return "in-progress"; if(i===0) return "available"; const prev=sub.tp[ord[i-1].id]; return (prev?.skipped||(prev?.mastery||0)>=70)?"available":"locked"; } static calibratedDifficulty(p, sub, topic){ const mastery=sub?.tp?.[topic?.id]?.mastery||0; const acc=P.recentAcc(p); const brain=P.brainScore(p); let tier=mastery<35?0:mastery<70?1:2; if(acc!==null&&acc>=80&&tier<2) tier++; if(brain>=70&&tier<2&&mastery>15) tier++; if(acc!==null&&acc<45&&tier>0) tier--; return ["beginner","intermediate","advanced"][Math.max(0,Math.min(2,tier))]; } static subjectContext(sub){ if(!sub) return ""; const lines=[`Active Subject: ${sub.name} (level: ${sub.level||"unspecified"})`]; const ord=P.orderedTopics(sub); if(ord.length) lines.push(`Topic Mastery: {${ord.map(t=>`${t.name}: ${sub.tp[t.id]?.mastery||0}`).join(", ")}}`); return lines.join("\n"); } // Highest section that has built topics; progressive building threshold = 75. static sectionStatus(sub){ const topics=sub.curriculum?.topics||[]; const built=[...new Set(topics.map(t=>t.section_index))].sort((a,b)=>a-b); const current=built.length?built[built.length-1]:-1; const currentTopics=topics.filter(t=>t.section_index===current); const cleared=currentTopics.length>0&¤tTopics.every(t=>(sub.tp[t.id]?.mastery||0)>=75||sub.tp[t.id]?.skipped); const nextIndex=current+1; const hasNext=nextIndex<(sub.curriculum?.skeleton?.sections||[]).length; return { current, cleared, nextIndex, hasNext }; } } // ── DB — all persistence lives in Supabase ────────────────────────────────── const DB = { userId:null, async loadAll(user){ DB.userId=user.id; const [{data:u},{data:pf},{data:subs}] = await Promise.all([ sb.from("users").select("*").eq("id",user.id).single(), sb.from("portfolios").select("*").eq("user_id",user.id).single(), sb.from("subjects").select("*").eq("user_id",user.id).neq("status","archived").order("created_at"), ]); const subjects=[]; if(subs?.length){ const subIds=subs.map(s=>s.id); const {data:currs}=await sb.from("curricula").select("*").in("subject_id",subIds).eq("is_active",true); const currIds=(currs||[]).map(c=>c.id); const {data:topics}=currIds.length?await sb.from("topics").select("*").in("curriculum_id",currIds).order("section_index").order("position"):{data:[]}; const {data:prog}=await sb.from("topic_progress").select("*").eq("user_id",user.id); const progBy={}; (prog||[]).forEach(r=>{progBy[r.topic_id]={mastery:r.mastery,correct:r.correct,total:r.total_exercises,skipped:r.skipped,last:r.last_practiced};}); subs.forEach(s=>{ const c=(currs||[]).find(c=>c.subject_id===s.id); const tps=(topics||[]).filter(t=>t.curriculum_id===c?.id); const tp={}; tps.forEach(t=>{ if(progBy[t.id]) tp[t.id]=progBy[t.id]; }); subjects.push({ id:s.id, name:s.name, color:s.color, status:s.status, level:s.level, timeConstraint:s.time_constraint, exerciseFormats:s.exercise_formats||["mcq"], curriculum:c?{id:c.id,versionNumber:c.version_number,skeleton:c.skeleton||{},topics:tps}:null, tp }); }); } return P.fromRows(u,pf,subjects); }, saveName(name){ return sb.from("users").update({name}).eq("id",DB.userId); }, savePortfolio(p){ return sb.from("portfolios").update({ brain_scores:p.brain, modality:p.modality, accuracy_history:p.accuracy, question_type_perf:p.questionTypePerf, feedback_signal:p.feedbackSignal, session_count:p.sessions, total_exercises:p.totalEx, updated_at:new Date().toISOString(), }).eq("user_id",DB.userId); }, event(event_type,payload={}){ return sb.from("events").insert({user_id:DB.userId,event_type,payload}); }, async logExercise(row){ const {data}=await sb.from("exercises").insert({user_id:DB.userId,...row}).select("id").single(); return data?.id||null; }, setExerciseFeedback(id,user_feedback){ if(!id) return; return sb.from("exercises").update({user_feedback}).eq("id",id); }, upsertProgress(topicId,tp){ return sb.from("topic_progress").upsert({ user_id:DB.userId, topic_id:topicId, mastery:tp.mastery, correct:tp.correct, total_exercises:tp.total, skipped:!!tp.skipped, last_practiced:new Date().toISOString() }); }, async createSubject(skeleton,color,sectionZeroTopics){ const {data:s,error:e1}=await sb.from("subjects").insert({ user_id:DB.userId, name:skeleton.name, color, level:skeleton.level, time_constraint:skeleton.timeConstraint, exercise_formats:skeleton.exerciseFormats||["mcq"] }).select("*").single(); if(e1) throw e1; const {data:c,error:e2}=await sb.from("curricula").insert({ subject_id:s.id, version_number:1, skeleton:{description:skeleton.description,level:skeleton.level,timeConstraint:skeleton.timeConstraint, exerciseFormats:skeleton.exerciseFormats,sections:skeleton.sections}, is_active:true }).select("*").single(); if(e2) throw e2; const topics=await DB.insertTopics(c.id,0,sectionZeroTopics); return { id:s.id, name:s.name, color:s.color, status:s.status, level:s.level, timeConstraint:s.time_constraint, exerciseFormats:s.exercise_formats, curriculum:{id:c.id,versionNumber:1,skeleton:c.skeleton,topics}, tp:{} }; }, async insertTopics(curriculumId,sectionIndex,list){ const rows=list.map((t,i)=>({ curriculum_id:curriculumId, section_index:sectionIndex, position:i, name:t.name, description:t.description||"", priority:t.priority||"medium", type:t.type||"concept", is_built:true })); const {data,error}=await sb.from("topics").insert(rows).select("*"); if(error) throw error; return data||[]; }, // Versioned surgical edit: snapshot old, new curricula row, re-parent kept // topics (progress preserved), insert "new" topics, deactivate old version. async applyEdit(sub, edited, changeType){ const old=sub.curriculum; const snapshot=old.topics.map(t=>({id:t.id,name:t.name,section_index:t.section_index,position:t.position,priority:t.priority,type:t.type})); await sb.from("curricula").update({is_active:false,skeleton:{...old.skeleton,archived_topics:snapshot}}).eq("id",old.id); const newSkeleton={...old.skeleton,sections:edited.sections||old.skeleton.sections}; const {data:c,error}=await sb.from("curricula").insert({ subject_id:sub.id, version_number:(old.versionNumber||1)+1, skeleton:newSkeleton, is_active:true }).select("*").single(); if(error) throw error; const kept=[], created=[]; for(let i=0;io.id===t.id)) kept.push({...t,position:i}); else created.push({...t,position:i}); } for(const t of kept){ await sb.from("topics").update({ curriculum_id:c.id, section_index:t.section_index??0, position:t.position, name:t.name, description:t.description||"", priority:t.priority||"medium", type:t.type||"concept" }).eq("id",t.id); } let newRows=[]; if(created.length){ const rows=created.map(t=>({ curriculum_id:c.id, section_index:t.section_index??0, position:t.position, name:t.name, description:t.description||"", priority:t.priority||"medium", type:t.type||"concept", is_built:true })); const {data}=await sb.from("topics").insert(rows).select("*"); newRows=data||[]; } const {data:topics}=await sb.from("topics").select("*").eq("curriculum_id",c.id).order("section_index").order("position"); DB.event("curriculum_edited",{subject_id:sub.id,change_type:changeType}); return {id:c.id,versionNumber:c.version_number,skeleton:c.skeleton,topics:topics||[]}; }, archiveSubject(id){ return sb.from("subjects").update({status:"archived"}).eq("id",id); }, }; // ── AI — every call goes through the edge function ───────────────────────── async function callTask(task,payload={}){ const {data:{session}}=await sb.auth.getSession(); if(!session) throw new Error("Not signed in"); const r=await fetch(`${SUPABASE_URL}/functions/v1/llm`,{ method:"POST", headers:{ "Content-Type":"application/json", "Authorization":`Bearer ${session.access_token}`, "apikey":SUPABASE_ANON_KEY }, body:JSON.stringify({task,payload}) }); const d=await r.json(); if(!r.ok||d.error) throw new Error(d.error||`Error ${r.status}`); return d.result; } const parseTag=(text,tag)=>{ const m=text.match(new RegExp(`<${tag}>([\\s\\S]*?)`)); return m?m[1]:null; }; const stripTags=(text)=>text.replace(/[\s\S]*?<\/skeleton>/g,"").replace(/[\s\S]*?<\/curriculum>/g,"").replace(/[\s\S]*?<\/change>/g,"").trim(); // ── SHARED UI ─────────────────────────────────────────────────────────────── const Loading = ({label="Thinking…"}) => (
{label}
); const Toast = ({msg}) => msg?
{msg}
:null; // Difficulty feedback widget — shown after every answered exercise. const FeedbackPills = ({ onPick, picked }) => (
How did that feel? {[["too_easy","Too easy"],["right","Just right"],["too_hard","Too hard"]].map(([v,l])=>( ))}
); // ── LOGIN ─────────────────────────────────────────────────────────────────── const Login = () => { const [email,setEmail]=useState(""); const [sent,setSent]=useState(false); const [err,setErr]=useState(null); const [busy,setBusy]=useState(false); const send=async()=>{ if(!email.includes("@")||busy) return; setBusy(true); setErr(null); const {error}=await sb.auth.signInWithOtp({email,options:{emailRedirectTo:window.location.origin+window.location.pathname}}); setBusy(false); if(error) setErr(error.message); else setSent(true); }; return (

Cognitif

Train your mind. Learn anything. Your curriculum adapts to you.

{sent?(
Check your inbox — we sent a sign-in link to {email}. Open it in this browser.
):(
setEmail(e.target.value)} onKeyDown={e=>e.key==="Enter"&&send()}/>
Passwordless — we email you a magic link.
{err&&
{err}
}
)}
); }; const NamePrompt = ({ onDone }) => { const [name,setName]=useState(""); const save=async()=>{ if(!name.trim()) return; await DB.saveName(name.trim()); onDone(name.trim()); }; return (

What should we call you?

Used in your profile and nowhere else.

setName(e.target.value)} onKeyDown={e=>e.key==="Enter"&&save()}/>
); }; // ── EXERCISE SESSION (shared by subject practice and brain training) ─────── const Session = ({ state, setState, kind, subject, topic, domain, count=5, testOut=false, onDone }) => { const [i,setI]=useState(0); const [ex,setEx]=useState(null); const [exId,setExId]=useState(null); const [loading,setLoading]=useState(true); const [err,setErr]=useState(null); const [sel,setSel]=useState(null); const [submitted,setSubmitted]=useState(false); const [thesis,setThesis]=useState(""); const [evalR,setEvalR]=useState(null); const [grading,setGrading]=useState(false); const [fb,setFb]=useState(null); const [correctCount,setCorrectCount]=useState(0); const startRef=useRef(Date.now()); const total=testOut?3:count; const load=useCallback(async()=>{ setLoading(true); setErr(null); setEx(null); setSel(null); setSubmitted(false); setThesis(""); setEvalR(null); setFb(null); try{ let result; if(kind==="brain"){ const score=state.brain[domain.id]?.score||50; const acc=P.recentAcc(state); let diff=score<40?"beginner":score<65?"intermediate":"advanced"; if(acc!==null&&acc>=80&&diff!=="advanced") diff=diff==="beginner"?"intermediate":"advanced"; result=await callTask("brain",{ domainLabel:domain.label, difficulty:diff, exType:["curriculum","pattern","training"][Math.floor(Math.random()*3)] }); }else{ const wantThesis=!testOut&&subject.exerciseFormats?.includes("thesis")&&["essay","argument","analysis"].includes(topic.type); result=await callTask("exercise",{ format:wantThesis?"thesis":"mcq", subjectName:subject.name, topicName:topic.name, topicDescription:topic.description, difficulty:testOut?"advanced":P.calibratedDifficulty(state,subject,topic), testOut, subjectContext:P.subjectContext(subject) }); } // Final safety net: never show an MCQ whose correct answer isn't a real // option. If the server returns a malformed one, retry once. const mcqOk=(r)=>r&&r.type==="thesis"||(Array.isArray(r?.options)&&r.options.length>=2&&Number.isInteger(r.correct)&&r.correct>=0&&r.correcttypeof o==="string"&&o.trim())); if(!mcqOk(result)){ const retry=kind==="brain" ? await callTask("brain",{ domainLabel:domain.label, difficulty:"intermediate", exType:"pattern" }) : await callTask("exercise",{ format:"mcq", subjectName:subject.name, topicName:topic.name, topicDescription:topic.description, difficulty:P.calibratedDifficulty(state,subject,topic), testOut, subjectContext:P.subjectContext(subject) }); if(mcqOk(retry)) result=retry; else throw new Error("Question generator returned an invalid item. Tap retry."); } setEx(result); startRef.current=Date.now(); DB.event("exercise_generated",{topic_id:topic?.id||null,brain_domain:domain?.id||null}); }catch(e){ setErr(e.message); } setLoading(false); },[i]); useEffect(()=>{ load(); },[load]); const persist=async(correct,responseText,score)=>{ const np={...state}; if(kind==="brain"){ P.updateBrain(np,domain.id,correct); } else{ const sub=np.subjects.find(s=>s.id===subject.id); const tp=P.updateTopic(np,sub,topic.id,correct); P.recordQType(np,ex.type||"mcq",correct); DB.upsertProgress(topic.id,tp); } setState(np); DB.savePortfolio(np); const id=await DB.logExercise({ topic_id:topic?.id||null, brain_domain:domain?.id||null, prompt:{q:ex.prompt,type:ex.type||"mcq",difficulty:ex.difficulty}, user_response:responseText, correct, score:score??null, response_time:Date.now()-startRef.current }); setExId(id); DB.event("exercise_answered",{exercise_id:id,correct,time:Date.now()-startRef.current}); if(correct) setCorrectCount(c=>c+1); }; const submitMCQ=()=>{ if(sel===null||submitted) return; setSubmitted(true); persist(sel===ex.correct,"ABCD"[sel]); }; const submitThesis=async()=>{ if(thesis.trim().length<30||grading) return; setGrading(true); try{ const r=await callTask("thesis_eval",{ subjectName:subject.name, prompt:ex.prompt, rubric:ex.rubric, modelAnswer:ex.model_answer, response:thesis, subjectContext:P.subjectContext(subject) }); setEvalR(r); setSubmitted(true); const ok=r.earned_point||r.score>=3; const np={...state}; const sub=np.subjects.find(s=>s.id===subject.id); const tp=P.updateTopic(np,sub,topic.id,ok); P.recordQType(np,"thesis",ok); DB.upsertProgress(topic.id,tp); setState(np); DB.savePortfolio(np); const id=await DB.logExercise({ topic_id:topic.id, prompt:{q:ex.prompt,type:"thesis"}, user_response:thesis, correct:ok, score:r.score, response_time:Date.now()-startRef.current }); setExId(id); DB.event("exercise_answered",{exercise_id:id,correct:ok,time:Date.now()-startRef.current}); if(ok) setCorrectCount(c=>c+1); }catch(e){ setErr(e.message); } setGrading(false); }; const pickFeedback=(v)=>{ setFb(v); DB.setExerciseFeedback(exId,{difficulty:v}); const np={...state}; P.recordFeedback(np,v); setState(np); DB.savePortfolio(np); }; const next=()=>{ if(i+1>=total){ onDone(correctCount,total); } else setI(i+1); }; const title=kind==="brain"?domain.label:(testOut?`Test out · ${topic.name}`:topic.name); return (
{title}
{Array.from({length:total}).map((_,k)=>(
))}
{loading&&} {err&&
Couldn't generate — {err}
} {ex&&!loading&&ex.type==="thesis"&&(
{ex.prompt}