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 ; } 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}) => (

{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 (
{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 */}