import { useState, useRef, useCallback } from "react";
import {
LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend,
BarChart, Bar, ResponsiveContainer, AreaChart, Area
} from "recharts";
/* ══════════ DONNÉES KPI ══════════ */
// Latence par endpoint (3 calls : search, competences, match)
const kpiLatency = [
{ t:"Lun", search:1.2, competences:1.8, match:2.9 },
{ t:"Mar", search:1.4, competences:2.1, match:3.2 },
{ t:"Mer", search:1.1, competences:1.6, match:2.6 },
{ t:"Jeu", search:1.8, competences:2.4, match:3.7 },
{ t:"Ven", search:1.3, competences:1.9, match:2.8 },
{ t:"Sam", search:1.0, competences:1.5, match:2.3 },
{ t:"Dim", search:0.9, competences:1.3, match:2.0 },
];
// Requêtes/jour par endpoint
const kpiEndpoints = [
{ t:"Lun", search:85, extract:42, competences:38, match:31 },
{ t:"Mar", search:91, extract:48, competences:44, match:37 },
{ t:"Mer", search:78, extract:38, competences:35, match:28 },
{ t:"Jeu", search:105, extract:61, competences:57, match:49 },
{ t:"Ven", search:96, extract:54, competences:50, match:42 },
{ t:"Sam", search:45, extract:22, competences:19, match:14 },
{ t:"Dim", search:38, extract:18, competences:15, match:10 },
];
// Tokens par type d'appel LLM (3 call types, coûts réels)
const kpiLLM = [
{ t:"00h", search:320, comp:0, match:0, cost:0.001 },
{ t:"04h", search:110, comp:0, match:0, cost:0.000 },
{ t:"08h", search:980, comp:760, match:1240, cost:0.009 },
{ t:"12h", search:1640,comp:1280, match:2080, cost:0.019 },
{ t:"16h", search:1820,comp:1420, match:2310, cost:0.022 },
{ t:"20h", search:1240,comp:970, match:1580, cost:0.015 },
{ t:"23h", search:480, comp:360, match:580, cost:0.006 },
];
// Usage hebdo par persona + adoption match
const kpiUsage = [
{ t:"S1", sophie:42, karim:18, match:18 },
{ t:"S2", sophie:55, karim:27, match:29 },
{ t:"S3", sophie:61, karim:35, match:38 },
{ t:"S4", sophie:78, karim:44, match:51 },
{ t:"S5", sophie:90, karim:52, match:64 },
{ t:"S6", sophie:105,karim:63, match:79 },
];
// Taux d'adoption match (% users who use match after competences)
const kpiMatchAdoption = [
{ t:"S1", taux:30, retention:45 },
{ t:"S2", taux:38, retention:52 },
{ t:"S3", taux:47, retention:61 },
{ t:"S4", taux:56, retention:69 },
{ t:"S5", taux:64, retention:75 },
{ t:"S6", taux:71, retention:82 },
];
// Scores match distribution hebdo
const kpiMatchScores = [
{ t:"S1", excellent:18, bon:31, partiel:35, faible:16, score_moy:62 },
{ t:"S2", excellent:22, bon:35, partiel:31, faible:12, score_moy:66 },
{ t:"S3", excellent:28, bon:38, partiel:26, faible:8, score_moy:71 },
{ t:"S4", excellent:33, bon:40, partiel:22, faible:5, score_moy:75 },
{ t:"S5", excellent:38, bon:42, partiel:16, faible:4, score_moy:79 },
{ t:"S6", excellent:44, bon:41, partiel:12, faible:3, score_moy:83 },
];
// Métriques métier : précision, satisfaction, match_rate
const kpiMetier = [
{ t:"S1", precision:71, satisfaction:68, export:55, match_score:62 },
{ t:"S2", precision:74, satisfaction:72, export:61, match_score:66 },
{ t:"S3", precision:79, satisfaction:75, export:67, match_score:71 },
{ t:"S4", precision:83, satisfaction:80, export:74, match_score:75 },
{ t:"S5", precision:87, satisfaction:84, export:79, match_score:79 },
{ t:"S6", precision:91, satisfaction:89, export:85, match_score:83 },
];
// Compétences détectées / validées / matchées
const kpiCompetences = [
{ t:"S1", detected:3.1, validated:2.4, matched:1.8 },
{ t:"S2", detected:3.4, validated:2.8, matched:2.2 },
{ t:"S3", detected:4.0, validated:3.3, matched:2.7 },
{ t:"S4", detected:4.2, validated:3.6, matched:3.1 },
{ t:"S5", detected:4.6, validated:4.0, matched:3.5 },
{ t:"S6", detected:5.0, validated:4.4, matched:3.9 },
];
/* ══════════ PERSONAS ══════════ */
const PERSONAS = {
sophie: {
id: "sophie", name: "Sophie", age: 38, icon: "👩💼",
role: "Conseillère France Travail — Caen",
color: "#059669", colorSoft: "#ECFDF5", colorMid: "#A7F3D0",
defaultMode: "keywords",
tagline: "Code ROME en 30 sec → PPAE sans ressaisie",
context: "Open-space bruyant. Écran 19\". PPAE + Emploi Store ouverts. 23 mails non lus. Demandeur en face. 35 min max par entretien.",
goal: "Identifier en 2 clics les codes ROME + NAF correspondant au profil oral du demandeur, pour orienter et remplir le PPAE sans friction.",
skills: ["Écoute active","Reformulation métier","Connaissance ROME","Orientation emploi","Saisie PPAE","Gestion du temps"],
pains: ["847 résultats non triés → signal noyé dans le bruit","Codes NAF hybrides (ex: BTP + logistique)","Temps de saisie > temps d'échange humain"],
success: "Elle tourne l'écran vers le demandeur, clique Copier, colle dans le PPAE. Zéro ressaisie.",
rgaa: ["Contraste AA minimum","Navigation clavier","Lecteur d'écran"],
quickActions: ["Copier ROME","Copier NAF","Envoyer PPAE"],
placeholder: "ex: agent logistique polyvalent, infirmière coordinatrice…",
tip: "💡 Dites simplement le métier décrit par le demandeur — le micro est activable",
},
karim: {
id: "karim", name: "Karim", age: 44, icon: "🎯",
role: "Chasseur de Tête indépendant — Paris/TGV",
color: "#2563EB", colorSoft: "#EFF6FF", colorMid: "#BFDBFE",
defaultMode: "annonce",
tagline: "Annonce brute → ROME + NAF + Compétences en 12 sec",
context: "MacBook en mobilité. ATS Bullhorn ouvert. Annonce client reçue à 18h avec fautes et acronymes. Brief client à 9h le lendemain.",
goal: "Extraire en 10 secondes les codes ROME + NAF d'une annonce brute pour lancer immédiatement le sourcing LinkedIn Recruiter.",
skills: ["Lecture annonce","Sourcing LinkedIn","Qualification profil","Connaissance secteurs","Négociation","Gestion mandats"],
pains: ["Annonce non normalisée → perte de temps de qualification","Secteur mentionné sans code NAF (ex: 'industrie métallurgique')","Plusieurs ROME possibles → hésitation sur le sourcing"],
success: "Il copie le code ROME, ouvre LinkedIn Recruiter, colle dans le filtre, lance la recherche. 12 secondes chrono.",
rgaa: ["Copier en 1 clic","Résultat lisible mobile","Export rapide"],
quickActions: ["Copier ROME","Copier NAF","Ouvrir LinkedIn","Exporter fiche"],
placeholder: "Collez l'annonce brute complète — même avec fautes et acronymes internes…",
tip: "💡 Collez l'annonce telle quelle, l'IA normalise et extrait ROME, NAF et compétences",
},
};
/* ══════════ SECTEURS ══════════ */
const SECTORS = [
{id:"all",label:"Tous secteurs",icon:"🌐"},
{id:"IT",label:"IT & Digital",icon:"💻"},
{id:"Santé",label:"Santé & Social",icon:"🏥"},
{id:"BTP",label:"BTP & Industrie",icon:"🏗"},
{id:"Commerce",label:"Commerce & Vente",icon:"🛒"},
{id:"RH",label:"RH & Formation",icon:"👥"},
{id:"Finance",label:"Finance & Gestion",icon:"📊"},
{id:"Transport",label:"Transport & Logistique",icon:"🚛"},
];
const LEVELS = [
{id:"all",label:"Tous niveaux"},{id:"CAP",label:"CAP/BEP"},
{id:"BAC",label:"Bac"},{id:"BAC+2",label:"Bac+2"},
{id:"BAC+3",label:"Bac+3/4"},{id:"BAC+5",label:"Bac+5+"},
];
/* ══════════ ATOMS ══════════ */
const Spinner = () => (
);
const Skel = ({w="100%",h=14,r=6,mb=0}) => (
);
const BdgType = ({type}) => {
const m = {
exact:{bg:"#DCFCE7",c:"#15803D",br:"#86EFAC",l:"Exact"},
proche:{bg:"#DBEAFE",c:"#1D4ED8",br:"#93C5FD",l:"Proche"},
"associé":{bg:"#FEF3C7",c:"#B45309",br:"#FCD34D",l:"Associé"},
naf:{bg:"#EDE9FE",c:"#6D28D9",br:"#C4B5FD",l:"NAF"},
clé:{bg:"#DCFCE7",c:"#166534",br:"#86EFAC",l:"Clé métier"},
transverse:{bg:"#F3E8FF",c:"#7E22CE",br:"#D8B4FE",l:"Transverse"},
}[type]||{bg:"#F1F5F9",c:"#475569",br:"#CBD5E1",l:type};
return {m.l} ;
};
function CopyBtn({text,label="Copier",onCopy,accent="#2563EB"}){
const[ok,setOk]=useState(false);
return {navigator.clipboard.writeText(text);setOk(true);onCopy?.();setTimeout(()=>setOk(false),1800);}} style={{display:"inline-flex",alignItems:"center",gap:4,padding:"5px 12px",borderRadius:6,fontSize:11,fontWeight:600,border:`1px solid ${ok?"#A7F3D0":"#E2E8F0"}`,background:ok?"#ECFDF5":"#F8FAFC",color:ok?"#059669":"#6B7280",cursor:"pointer",transition:"all .2s",whiteSpace:"nowrap",flexShrink:0}}>
{ok?"✓":"⎘"} {ok?"Copié":label}
;
}
function exportCSV(rows, filename="export") {
const csv = rows.map(r=>r.map(v=>`"${String(v).replace(/"/g,'""')}"`).join(";")).join("\n");
const a = document.createElement("a");
a.href = "data:text/csv;charset=utf-8,\uFEFF"+encodeURIComponent(csv);
a.download = `${filename}_${Date.now()}.csv`;
a.click();
}
/* ══════════ DOSSIER LIGHT ATOMS ══════════ */
const DTag = ({label,color}) => (
{label}
);
const DCard = ({children,style={}}) => (
{children}
);
const DSec = ({title,children}) => (
);
const DStep = ({n,label,detail,tags}) => (
{n}
{label}
{detail&&
{detail}
}
{tags&&
{tags.map((t,i)=>)}
}
);
const KPIBox = ({label,value,unit,delta,color}) => (
{label}
{value}{unit}
{delta!=null&&
0?"#059669":"#E11D48",fontSize:11,marginTop:2}}>{delta>0?"▲":"▼"} {Math.abs(delta)}% vs S-1
}
);
const TT = {contentStyle:{background:"#FFFFFF",border:"1px solid #E2E8F0",color:"#0F172A",fontSize:11,borderRadius:8,boxShadow:"0 4px 12px rgba(0,0,0,.1)"}};
/* ══════════ MATCH ANNONCES PAR COMPÉTENCES ══════════ */
function MatchAnnonces({competences, codes, persona}) {
const pc = PERSONAS[persona];
const[annonce,setAnnonce]=useState("");
const[loading,setLoading]=useState(false);
const[result,setResult]=useState(null);
const[error,setError]=useState("");
const[open,setOpen]=useState(false);
const fileRef=useRef();
const allCompetences = competences
? Object.values(competences).flat()
: [];
const match = useCallback(async() => {
if(!annonce.trim()||!allCompetences.length) return;
setLoading(true); setResult(null); setError("");
try {
const res = await fetch("/api/claude", {
method:"POST", headers:{"Content-Type":"application/json"},
body: JSON.stringify({model:"claude-sonnet-4-20250514",max_tokens:1200,
messages:[{role:"user",content:
`Expert RH et référentiel ROME.
Compétences du profil : ${allCompetences.join(", ")}
Codes ROME du profil : ${codes.join(", ")}
Annonce d'emploi :
${annonce.slice(0,3000)}
Analyse le niveau de correspondance. Retourne UNIQUEMENT JSON valide sans markdown :
{"score":85,"niveau":"Excellent|Bon|Partiel|Faible","titre_poste":"...","competences_matchees":["..."],"competences_manquantes":["..."],"competences_bonus":["compétences dans l'annonce non requises mais utiles"],"rome_annonce":"XXXXX","recommandation":"2-3 phrases d'analyse pour le recruteur/conseiller","points_forts":["2-3 atouts du profil pour ce poste"],"points_vigilance":["1-2 points à vérifier en entretien"]}`
}]})
});
const data = await res.json();
const raw = data.content?.[0]?.text||"";
setResult(JSON.parse(raw.replace(/```json|```/g,"").trim()));
} catch{ setError("Erreur d'analyse. Réessayez."); }
finally{ setLoading(false); }
},[annonce, allCompetences, codes]);
const scoreColor = (s) => s>=80?"#059669":s>=60?"#D97706":s>=40?"#EA580C":"#E11D48";
const scoreLabel = (s) => s>=80?"Excellent":s>=60?"Bon":s>=40?"Partiel":"Faible";
const scoreBg = (s) => s>=80?"#ECFDF5":s>=60?"#FFFBEB":s>=40?"#FFF7ED":"#FFF1F2";
if(!allCompetences.length) return null;
return (
setOpen(o=>!o)} style={{width:"100%",display:"flex",alignItems:"center",justifyContent:"space-between",padding:"14px 20px",background:"none",border:"none",cursor:"pointer",textAlign:"left",transition:"background .15s"}} onMouseEnter={e=>e.currentTarget.style.background="#F8FAFC"} onMouseLeave={e=>e.currentTarget.style.background="none"}>
🎯
Matcher une annonce
Score de correspondance profil ↔ offre d'emploi · {allCompetences.length} compétences disponibles
⌄
{open&&(
{/* Compétences disponibles */}
Profil — {allCompetences.length} compétences à matcher
{allCompetences.slice(0,20).map((c,i)=>(
{c}
))}
{allCompetences.length>20&&+{allCompetences.length-20} autres }
{/* Input annonce */}
fileRef.current?.click()} style={{display:"flex",alignItems:"center",gap:5,padding:"8px 14px",borderRadius:8,border:"1px solid #E2E8F0",background:"#F8FAFC",color:"#6B7280",fontSize:12,fontWeight:600,cursor:"pointer"}}>📎 Fichier
{const f=e.target.files?.[0];if(!f)return;const r=new FileReader();r.onload=ev=>setAnnonce(ev.target.result);r.readAsText(f)}}/>
{loading?<> Calcul du score…>:<>⚡ Calculer le score de match>}
{error&&
⚠ {error}
}
{loading&&(
)}
{result&&(
{/* Score hero */}
{scoreLabel(result.score)}
{result.titre_poste&&{result.titre_poste} }
{result.rome_annonce&&{result.rome_annonce} }
{/* Score bar */}
{result.recommandation}
{/* 3 colonnes : matchées / manquantes / bonus */}
✓ Matchées ({result.competences_matchees?.length||0})
{(result.competences_matchees||[]).map((c,i)=>
● {c}
)}
⚠ Manquantes ({result.competences_manquantes?.length||0})
{(result.competences_manquantes||[]).map((c,i)=>
○ {c}
)}
+ Bonus ({result.competences_bonus?.length||0})
{(result.competences_bonus||[]).map((c,i)=>
+ {c}
)}
{/* Points forts / vigilance */}
💪 Points forts
{(result.points_forts||[]).map((p,i)=>
✓ {p}
)}
🔍 Points de vigilance
{(result.points_vigilance||[]).map((p,i)=>
! {p}
)}
)}
)}
);
}
/* ══════════ COMPETENCES SECTION ══════════ */
function CompetencesBlock({codes, persona}) {
const[loading,setLoading]=useState(false);
const[competences,setCompetences]=useState(null);
const[open,setOpen]=useState(false);
const pc = PERSONAS[persona];
const fetchCompetences = useCallback(async() => {
if(!codes?.length) return;
setLoading(true); setOpen(true);
try {
const res = await fetch("/api/claude", {
method:"POST",
headers:{"Content-Type":"application/json"},
body: JSON.stringify({model:"claude-sonnet-4-20250514",max_tokens:800,messages:[{role:"user",content:
`Expert ROME France Travail. Pour les codes ROME suivants : ${codes.join(", ")}\nRetourne UNIQUEMENT JSON valide sans markdown :\n{"cles_metier":["..."],"transverses":["..."],"numeriques":["..."],"savoir_etre":["..."],"certifications":["..."]}\ncles_metier: 5-7 compétences techniques cœur de métier. transverses: 3-4 compétences transversales. numeriques: 2-3 outils/logiciels. savoir_etre: 3-4 compétences comportementales. certifications: 2-3 certifications ou habilitations utiles.`
}]})
});
const data = await res.json();
const raw = data.content?.[0]?.text||"";
setCompetences(JSON.parse(raw.replace(/```json|```/g,"").trim()));
} catch{ setCompetences(null); }
finally{ setLoading(false); }
},[codes]);
const categories = competences ? [
{key:"cles_metier",label:"Cœur de métier",color:pc.color,bg:pc.colorSoft,type:"clé"},
{key:"transverses",label:"Transversales",color:"#7C3AED",bg:"#F5F3FF",type:"transverse"},
{key:"numeriques",label:"Outils & digital",color:"#0891B2",bg:"#ECFEFF",type:"proche"},
{key:"savoir_etre",label:"Savoir-être",color:"#D97706",bg:"#FFFBEB",type:"associé"},
{key:"certifications",label:"Certifications",color:"#6B7280",bg:"#F1F5F9",type:"exact"},
] : [];
return (
open?setOpen(false):fetchCompetences()} style={{width:"100%",display:"flex",alignItems:"center",justifyContent:"space-between",padding:"14px 20px",background:"none",border:"none",cursor:"pointer",textAlign:"left",transition:"background .15s"}} onMouseEnter={e=>e.currentTarget.style.background="#F8FAFC"} onMouseLeave={e=>e.currentTarget.style.background="none"}>
🧩
Compétences associées
Référentiel ROME · cœur métier, transversales, certifications
{loading&& }
⌄
{open&&!loading&&competences&&(
{categories.map(cat=>(competences[cat.key]||[]).length>0&&(
{cat.label}
{(competences[cat.key]||[]).map((c,i)=>(
navigator.clipboard.writeText(c)}>
{c}
⎘
))}
))}
exportCSV([["Catégorie","Compétence"],...Object.entries(competences).flatMap(([k,vs])=>vs.map(v=>[k,v]))],"competences")} style={{display:"flex",alignItems:"center",gap:4,padding:"5px 12px",borderRadius:6,border:"1px solid #E2E8F0",background:"#F8FAFC",color:"#6B7280",fontSize:11,fontWeight:600,cursor:"pointer"}}>⬇ CSV
{/* Match annonce sur base compétences */}
)}
);
}
/* ══════════ TOOL MODULE ══════════ */
function ToolModule({persona}) {
const pc = PERSONAS[persona];
const[mode,setMode]=useState(pc.defaultMode);
const[input,setInput]=useState("");
const[annonceText,setAnnonceText]=useState("");
const[sector,setSector]=useState("all");
const[level,setLevel]=useState("all");
const[typeFilter,setTypeFilter]=useState("all");
const[sortBy,setSortBy]=useState("pertinence");
const[loading,setLoading]=useState(false);
const[results,setResults]=useState(null);
const[compared,setCompared]=useState([]);
const[listening,setListening]=useState(false);
const[error,setError]=useState("");
const fileRef=useRef();
const hasContent = mode==="keywords" ? input.trim().length>1 : annonceText.trim().length>20;
const getQ = () => mode==="keywords" ? input.trim() : annonceText.trim();
const analyze = useCallback(async() => {
if(!hasContent) return;
setLoading(true); setResults(null); setError("");
const sh = sector!=="all"?`Secteur prioritaire : ${sector}. `:"";
const lh = level!=="all"?`Niveau de qualification cible : ${level}. `:"";
const prompt = mode==="keywords"
? `Expert ROME France Travail. ${sh}${lh}Persona : ${pc.name} (${pc.role}).\nMots-clés : "${getQ()}"\nRetourne UNIQUEMENT JSON valide sans markdown :\n[{"code":"XXXXX","intitule":"titre officiel exact","type":"exact|proche|associé","description":"1-2 phrases","domaine":"secteur","grandDomaine":"lettre + libellé","niveauAcces":"ex: Bac+2"}]\n5 à 7 codes ROME pertinents triés.`
: `Expert RH ROME NAF. ${sh}Persona : ${pc.name} (${pc.role}).\nAnalyse cette offre. Retourne UNIQUEMENT JSON valide sans markdown :\n{"rome_principal":{"code":"XXXXX","intitule":"...","description":"...","domaine":"..."},"rome_alternatifs":[{"code":"XXXXX","intitule":"...","type":"proche|associé","domaine":"..."}],"naf":{"code":"XX.XXX","intitule":"...","secteur":"..."},"synthese":"2-3 phrases","mots_cles":["5-7 mots-clés"],"niveau":"junior|confirmé|senior|expert","contrat":"CDI|CDD|Stage|Alternance|Freelance|Non précisé","teletravail":"Oui|Partiel|Non|Non précisé","localisation":"ville si précisée ou vide"}\n\nOffre :\n${getQ().slice(0,4000)}`;
try {
const res = await fetch("/api/claude", {
method:"POST", headers:{"Content-Type":"application/json"},
body: JSON.stringify({model:"claude-sonnet-4-20250514",max_tokens:1400,messages:[{role:"user",content:prompt}]})
});
const data = await res.json();
const raw = data.content?.[0]?.text||"";
setResults(JSON.parse(raw.replace(/```json|```/g,"").trim()));
} catch{ setError("Erreur d'analyse. Vérifiez votre connexion."); }
finally{ setLoading(false); }
},[mode,input,annonceText,sector,level,persona]);
const startListen = () => {
const SR = window.SpeechRecognition||window.webkitSpeechRecognition;
if(!SR) return;
const r = new SR(); r.lang="fr-FR";
r.onresult=e=>setInput(e.results[0][0].transcript);
r.onend=()=>setListening(false);
r.start(); setListening(true);
};
const filtered = (() => {
if(!results||!Array.isArray(results)) return results;
let r=[...results];
if(typeFilter!=="all") r=r.filter(x=>x.type===typeFilter);
if(sortBy==="alpha") r=[...r].sort((a,b)=>a.intitule.localeCompare(b.intitule));
if(sortBy==="type") r=[...r].sort((a,b)=>["exact","proche","associé"].indexOf(a.type)-["exact","proche","associé"].indexOf(b.type));
return r;
})();
const codesForCompetences = results
? Array.isArray(results) ? results.map(r=>r.code) : [results.rome_principal?.code,...(results.rome_alternatifs||[]).map(r=>r.code)].filter(Boolean)
: [];
const lvlC={junior:{bg:"#EDE9FE",c:"#6D28D9"},confirmé:{bg:"#DBEAFE",c:"#1D4ED8"},senior:{bg:"#FEF3C7",c:"#B45309"},expert:{bg:"#FEE2E2",c:"#B91C1C"}};
const ctrC={CDI:{bg:"#DCFCE7",c:"#15803D"},CDD:{bg:"#FEF3C7",c:"#B45309"},Stage:{bg:"#EDE9FE",c:"#6D28D9"},Alternance:{bg:"#DBEAFE",c:"#1D4ED8"},Freelance:{bg:"#FEE2E2",c:"#B91C1C"}};
return (
{/* Persona context banner */}
{pc.icon}
{pc.name} — {pc.role}
{pc.tagline}
{pc.tip}
{/* Mode toggle */}
{[{id:"keywords",icon:"⌖",label:"Mots-clés → ROME"},{id:"annonce",icon:"⊡",label:"Annonce → ROME + NAF + Compétences"}].map(m=>(
{setMode(m.id);setResults(null);setError("")}}
style={{flex:1,padding:"10px 14px",borderRadius:10,border:`1.5px solid ${mode===m.id?pc.color:"#E2E8F0"}`,background:mode===m.id?pc.colorSoft:"#FFF",color:mode===m.id?pc.color:"#6B7280",fontWeight:mode===m.id?700:500,fontSize:13,cursor:"pointer",transition:"all .2s",display:"flex",alignItems:"center",justifyContent:"center",gap:7,boxShadow:mode===m.id?`0 2px 8px ${pc.color}25`:"0 1px 2px rgba(0,0,0,.05)"}}>
{m.icon} {m.label}
))}
{/* Filtres secteur */}
{SECTORS.map(s=>(
setSector(s.id)} style={{display:"inline-flex",alignItems:"center",gap:5,padding:"5px 13px",borderRadius:20,fontSize:12,fontWeight:sector===s.id?700:400,border:`1.5px solid ${sector===s.id?pc.color:"#E2E8F0"}`,background:sector===s.id?pc.colorSoft:"#FFF",color:sector===s.id?pc.color:"#6B7280",cursor:"pointer",whiteSpace:"nowrap",transition:"all .15s"}}>
{s.icon} {s.label}
))}
{/* Filtres niveau */}
Niveau :
{LEVELS.map(l=>(
setLevel(l.id)} style={{padding:"4px 11px",borderRadius:16,fontSize:11.5,fontWeight:level===l.id?700:400,border:`1px solid ${level===l.id?"#0891B2":"#E2E8F0"}`,background:level===l.id?"#ECFEFF":"#FFF",color:level===l.id?"#0891B2":"#9CA3AF",cursor:"pointer",transition:"all .15s"}}>
{l.label}
))}
{/* Search box */}
{mode==="keywords" ? (
) : (
<>
{/* Comparateur */}
{compared.length>0&&(
⚖ Comparateur — {compared.length} code{compared.length>1?"s":""}
c.code).join(", ")} label="Tout copier"/>
{compared.map((item,i)=>(
{item.code}
{item.intitule}
setCompared(p=>p.filter((_,idx)=>idx!==i))} style={{background:"none",border:"none",cursor:"pointer",fontSize:13,color:"#9CA3AF",padding:0}}>×
))}
)}
{error&&
⚠ {error}
}
{/* Skeletons */}
{loading&&
}
{/* Résultats Keywords */}
{!loading&&results&&Array.isArray(results)&&(
{filtered?.length||0}
code{(filtered?.length||0)>1?"s":""} ROME identifié{(filtered?.length||0)>1?"s":""}
{["exact","proche","associé"].map(t=>{const n=results.filter(r=>r.type===t).length;return n>0&&{n} {t} })}
setTypeFilter(e.target.value)} style={{padding:"5px 10px",borderRadius:7,border:"1px solid #E2E8F0",background:"#FFF",fontSize:12,color:"#374151",outline:"none",cursor:"pointer"}}>
Tous types
Exact
Proche
Associé
setSortBy(e.target.value)} style={{padding:"5px 10px",borderRadius:7,border:"1px solid #E2E8F0",background:"#FFF",fontSize:12,color:"#374151",outline:"none",cursor:"pointer"}}>
Pertinence
Alphabétique
Par type
exportCSV([["Code ROME","Intitulé","Type","Domaine","Niveau",...(results.map(r=>r.code))],[...results.map(r=>[r.code,r.intitule,r.type||"",r.domaine||"",r.niveauAcces||""])].flat()],"rome_export")} style={{display:"flex",alignItems:"center",gap:4,padding:"5px 11px",borderRadius:7,border:"1px solid #E2E8F0",background:"#F8FAFC",color:"#6B7280",fontSize:12,fontWeight:600,cursor:"pointer"}}>⬇ CSV
r.code).join(", ")} label="Tout copier"/>
{(filtered||[]).map((r,i)=>(
{e.currentTarget.style.borderColor=pc.color;e.currentTarget.style.boxShadow=`0 4px 20px ${pc.color}15`}}
onMouseLeave={e=>{e.currentTarget.style.borderColor="#E2E8F0";e.currentTarget.style.boxShadow="0 1px 2px rgba(0,0,0,.05)"}}>
{i+1}
{r.code}
{r.domaine&&{r.domaine} }
{r.niveauAcces&&{r.niveauAcces} }
{r.grandDomaine&&{r.grandDomaine} }
{r.intitule}
{r.description}
compared.find(c=>c.code===r.code)?setCompared(p=>p.filter(c=>c.code!==r.code)):(compared.length<4&&setCompared(p=>[...p,r]))} style={{display:"flex",alignItems:"center",gap:4,padding:"4px 11px",borderRadius:6,fontSize:11,fontWeight:600,border:`1px solid ${compared.find(c=>c.code===r.code)?"#C7D2FE":"#E2E8F0"}`,background:compared.find(c=>c.code===r.code)?"#EEF2FF":"#F8FAFC",color:compared.find(c=>c.code===r.code)?"#4F46E5":"#6B7280",cursor:"pointer",opacity:(!compared.find(c=>c.code===r.code)&&compared.length>=4)?.4:1}}>
{compared.find(c=>c.code===r.code)?"✓ Ajouté":"⚖ Comparer"}
))}
{/* Compétences pour mots-clés */}
)}
{/* Résultats Annonce */}
{!loading&&results&&!Array.isArray(results)&&(
{/* Hero ROME */}
ROME Principal
{results.niveau&&lvlC[results.niveau]&&{results.niveau} }
{results.contrat&&results.contrat!=="Non précisé"&&ctrC[results.contrat]&&{results.contrat} }
{results.teletravail&&results.teletravail!=="Non précisé"&&🏠 {results.teletravail} }
{results.localisation&&📍 {results.localisation} }
r.code)].join(", ")} label="Tous ROME"/>
exportCSV([["Type","Code","Intitulé"],["Principal",results.rome_principal.code,results.rome_principal.intitule],...(results.rome_alternatifs||[]).map(r=>["Alternatif",r.code,r.intitule]),["NAF",results.naf.code,results.naf.intitule]],"extraction_annonce")} style={{display:"flex",alignItems:"center",gap:4,padding:"5px 11px",borderRadius:6,border:"1px solid #E2E8F0",background:"#F8FAFC",color:"#6B7280",fontSize:11,fontWeight:600,cursor:"pointer"}}>⬇ CSV
{results.rome_principal.code}
{results.rome_principal.intitule}
{results.rome_principal.description&&
{results.rome_principal.description}
}
{/* Alternatifs */}
{results.rome_alternatifs?.length>0&&(
ROME Alternatifs
{results.rome_alternatifs.map((r,i)=>(
{r.code}
{r.intitule}
{r.domaine&&{r.domaine} }
))}
)}
{/* NAF + Synthèse */}
Code NAF / APE
{results.naf.code} {results.naf.intitule}
{results.naf.secteur&&
{results.naf.secteur}
}
Synthèse
{results.synthese}
{/* Mots-clés */}
{results.mots_cles?.length>0&&(
Mots-clés détectés
{results.mots_cles.map((k,i)=>{k} )}
)}
{/* Compétences enrichies */}
)}
);
}
/* ══════════ DOSSIER MODULE ══════════ */
function DossierModule() {
const[dtab,setDtab]=useState(0);
const DNAV=["👤 Personas","🗺 Parcours UX","🏗 Architecture","📈 KPIs CEO"];
return (
{/* Dossier header — accent band */}
ROME·NAF MATCHER
— Dossier de conception UX · v2.0
Personas · Parcours UX · Architecture · KPIs CEO
{DNAV.map((n,i)=>(
setDtab(i)} style={{background:"none",border:"none",cursor:"pointer",padding:"10px 18px",fontSize:12,fontFamily:"Syne,sans-serif",fontWeight:700,letterSpacing:"0.04em",color:dtab===i?"#FFFFFF":"#64748B",borderBottom:dtab===i?"2px solid #38BDF8":"2px solid transparent",transition:"all .2s"}}>
{n}
))}
{/* ── PERSONAS ── */}
{dtab===0&&(
{Object.values(PERSONAS).map(p=>(
{p.icon}
{p.name}, {p.age} ans
{p.role}
{p.context}
{p.goal}
{p.skills.map(s=>)}
{p.pains.map((f,i)=>✕ {f}
)}
Critère de succès
"{p.success}"
))}
{/* Persona secondaire Mehdi */}
🧍
Mehdi, 29 ans — Demandeur d'emploi (persona secondaire)
Cuisine, laptop, 3 onglets Indeed. Ne maîtrise pas le vocabulaire ROME officiel.
Frustration
Il dit "manutentionnaire" ou "cariste" — l'outil traduit en ROME sans jargon imposé.
Compétences
{["Langage naturel","Saisie vocale","Autonomie numérique"].map(c=> )}
Succès
Screenshot du résultat, rappelle Sophie avec le code en main.
)}
{/* ── PARCOURS UX ── */}
{dtab===1&&(
LOI DE MILLER · 7±2
Chaque écran affiche max 5 résultats ROME visibles sans scroll. Codes NAF : 3 maximum en premier plan. Pagination pour le reste.
LOI DE HICK · Réduire les choix
2 modules distincts dès l'accueil. Zéro configuration. Le résultat principal est mis en avant — les alternatifs sont secondaires.
UX HONEYCOMB (Morville) — Application au ROME·NAF Matcher
{[{dim:"Utile",desc:"Matching ROME/NAF réel, pertinent, opérationnel",c:"#059669"},{dim:"Utilisable",desc:"3 clics max pour un résultat. Interface épurée.",c:"#2563EB"},{dim:"Désirable",desc:"Design sobre et pro. Pas de surcharge visuelle.",c:"#D97706"},{dim:"Trouvable",desc:"Recherche vocale + texte + upload visibles au 1er regard",c:"#7C3AED"},{dim:"Accessible",desc:"RGAA AA : contraste, clavier, screen reader",c:"#EA580C"},{dim:"Crédible",desc:"Référentiel France Travail officiel. Source citée.",c:"#E11D48"},{dim:"Valeur",desc:"Gain de temps mesurable pour Sophie (PPAE) et Karim (sourcing)",c:"#059669"}].map(h=>(
))}
👩💼 Parcours Sophie — Mots-clés → ROME + NAF
🎯 Parcours Karim — Annonce → ROME + NAF + Compétences
)}
{/* ── ARCHITECTURE ── */}
{dtab===2&&(
🔌 API ENDPOINTS
{[
{m:"GET",ep:"/rome/{code_name}",desc:"Recherche ROME par nom métier (mots-clés)"},
{m:"GET",ep:"/rome/select/{code}_{id}",desc:"Récupère un code ROME précis par ID"},
{m:"POST",ep:"/rome/extract",desc:"Annonce brute → ROME + NAF extraits par LLM"},
{m:"GET",ep:"/naf/{code}",desc:"Détails d'un code NAF"},
{m:"POST",ep:"/rome/competences",desc:"Compétences associées à un code ROME"},
{m:"GET",ep:"/rome/nearby/{code}",desc:"Codes ROME proches (voisinage sémantique)"},
{m:"GET",ep:"/analytics/kpi",desc:"Dashboard KPI infrastructure et métier"},
{m:"POST",ep:"/rome/match",desc:"Score de match compétences ↔ annonce"},
].map(api=>(
))}
🤖 STACK LLM
{[
{label:"Modèle principal",val:"Claude Sonnet (Anthropic)",c:"#EA580C"},
{label:"Fallback",val:"Mistral-7B-Instruct (local)",c:"#7C3AED"},
{label:"Hébergement",val:"HuggingFace Spaces (prod)",c:"#2563EB"},
{label:"Température",val:"0.1 (précision > créativité)",c:"#D97706"},
{label:"Max tokens",val:"1400 tokens / requête",c:"#059669"},
{label:"Prompt strategy",val:"System prompt strict JSON + few-shot",c:"#059669"},
].map(s=>(
{s.label}
{s.val}
))}
🐳 DOCKER COMPOSE
services:
frontend: react:18 · port 3000
api: fastapi · port 8000
llm: claude/mistral · port 11434
db: postgres:15 · port 5432
cache: redis:7 · port 6379
nginx: reverse-proxy · port 80
)}
{/* ── KPIs CEO ── */}
{dtab===3&&(
{/* ── KPI Infrastructure ── */}
LATENCE PAR ENDPOINT (secondes)
REQUÊTES/JOUR PAR ENDPOINT
{/* ── KPI LLM ── */}
TOKENS PAR TYPE D'APPEL & COÛT ESTIMÉ / HEURE
{/* ── KPI Business ── */}
SESSIONS PAR PERSONA + USAGE MATCH
TAUX ADOPTION MATCH & RÉTENTION (%)
{/* ── KPI Match ── */}
DISTRIBUTION DES SCORES MATCH (%)
ÉVOLUTION SCORE MATCH MOYEN /100
{/* ── KPI Métier ── */}
PRÉCISION ROME / SATISFACTION / EXPORT / SCORE MATCH (%)
COMPÉTENCES DÉTECTÉES / VALIDÉES / MATCHÉES / SESSION
)}
);
}
/* ══════════ APP ROOT ══════════ */
export default function App() {
const[persona,setPersona]=useState("sophie");
const[mainTab,setMainTab]=useState("tool"); // tool | dossier
const pc = PERSONAS[persona];
return (
{/* ── HEADER ── */}
{/* ── MAIN ── */}
{mainTab==="tool"&&}
{mainTab==="dossier"&& }
ROME NAF Pro v4 · Claude (Anthropic) · Référentiel France Travail · RGAA AA · Données stockées localement
);
}