Spaces:
Sleeping
Sleeping
Upload 9 files
Browse files- App.jsx +1134 -0
- Dockerfile +20 -0
- README.md +34 -5
- index.html +13 -0
- main.jsx +9 -0
- main.py +69 -0
- package.json +22 -0
- requirements.txt +4 -0
- vite.config.js +15 -0
App.jsx
ADDED
|
@@ -0,0 +1,1134 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useState, useRef, useCallback } from "react";
|
| 2 |
+
import {
|
| 3 |
+
LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend,
|
| 4 |
+
BarChart, Bar, ResponsiveContainer, AreaChart, Area
|
| 5 |
+
} from "recharts";
|
| 6 |
+
|
| 7 |
+
/* ══════════ DONNÉES KPI ══════════ */
|
| 8 |
+
// Latence par endpoint (3 calls : search, competences, match)
|
| 9 |
+
const kpiLatency = [
|
| 10 |
+
{ t:"Lun", search:1.2, competences:1.8, match:2.9 },
|
| 11 |
+
{ t:"Mar", search:1.4, competences:2.1, match:3.2 },
|
| 12 |
+
{ t:"Mer", search:1.1, competences:1.6, match:2.6 },
|
| 13 |
+
{ t:"Jeu", search:1.8, competences:2.4, match:3.7 },
|
| 14 |
+
{ t:"Ven", search:1.3, competences:1.9, match:2.8 },
|
| 15 |
+
{ t:"Sam", search:1.0, competences:1.5, match:2.3 },
|
| 16 |
+
{ t:"Dim", search:0.9, competences:1.3, match:2.0 },
|
| 17 |
+
];
|
| 18 |
+
// Requêtes/jour par endpoint
|
| 19 |
+
const kpiEndpoints = [
|
| 20 |
+
{ t:"Lun", search:85, extract:42, competences:38, match:31 },
|
| 21 |
+
{ t:"Mar", search:91, extract:48, competences:44, match:37 },
|
| 22 |
+
{ t:"Mer", search:78, extract:38, competences:35, match:28 },
|
| 23 |
+
{ t:"Jeu", search:105, extract:61, competences:57, match:49 },
|
| 24 |
+
{ t:"Ven", search:96, extract:54, competences:50, match:42 },
|
| 25 |
+
{ t:"Sam", search:45, extract:22, competences:19, match:14 },
|
| 26 |
+
{ t:"Dim", search:38, extract:18, competences:15, match:10 },
|
| 27 |
+
];
|
| 28 |
+
// Tokens par type d'appel LLM (3 call types, coûts réels)
|
| 29 |
+
const kpiLLM = [
|
| 30 |
+
{ t:"00h", search:320, comp:0, match:0, cost:0.001 },
|
| 31 |
+
{ t:"04h", search:110, comp:0, match:0, cost:0.000 },
|
| 32 |
+
{ t:"08h", search:980, comp:760, match:1240, cost:0.009 },
|
| 33 |
+
{ t:"12h", search:1640,comp:1280, match:2080, cost:0.019 },
|
| 34 |
+
{ t:"16h", search:1820,comp:1420, match:2310, cost:0.022 },
|
| 35 |
+
{ t:"20h", search:1240,comp:970, match:1580, cost:0.015 },
|
| 36 |
+
{ t:"23h", search:480, comp:360, match:580, cost:0.006 },
|
| 37 |
+
];
|
| 38 |
+
// Usage hebdo par persona + adoption match
|
| 39 |
+
const kpiUsage = [
|
| 40 |
+
{ t:"S1", sophie:42, karim:18, match:18 },
|
| 41 |
+
{ t:"S2", sophie:55, karim:27, match:29 },
|
| 42 |
+
{ t:"S3", sophie:61, karim:35, match:38 },
|
| 43 |
+
{ t:"S4", sophie:78, karim:44, match:51 },
|
| 44 |
+
{ t:"S5", sophie:90, karim:52, match:64 },
|
| 45 |
+
{ t:"S6", sophie:105,karim:63, match:79 },
|
| 46 |
+
];
|
| 47 |
+
// Taux d'adoption match (% users who use match after competences)
|
| 48 |
+
const kpiMatchAdoption = [
|
| 49 |
+
{ t:"S1", taux:30, retention:45 },
|
| 50 |
+
{ t:"S2", taux:38, retention:52 },
|
| 51 |
+
{ t:"S3", taux:47, retention:61 },
|
| 52 |
+
{ t:"S4", taux:56, retention:69 },
|
| 53 |
+
{ t:"S5", taux:64, retention:75 },
|
| 54 |
+
{ t:"S6", taux:71, retention:82 },
|
| 55 |
+
];
|
| 56 |
+
// Scores match distribution hebdo
|
| 57 |
+
const kpiMatchScores = [
|
| 58 |
+
{ t:"S1", excellent:18, bon:31, partiel:35, faible:16, score_moy:62 },
|
| 59 |
+
{ t:"S2", excellent:22, bon:35, partiel:31, faible:12, score_moy:66 },
|
| 60 |
+
{ t:"S3", excellent:28, bon:38, partiel:26, faible:8, score_moy:71 },
|
| 61 |
+
{ t:"S4", excellent:33, bon:40, partiel:22, faible:5, score_moy:75 },
|
| 62 |
+
{ t:"S5", excellent:38, bon:42, partiel:16, faible:4, score_moy:79 },
|
| 63 |
+
{ t:"S6", excellent:44, bon:41, partiel:12, faible:3, score_moy:83 },
|
| 64 |
+
];
|
| 65 |
+
// Métriques métier : précision, satisfaction, match_rate
|
| 66 |
+
const kpiMetier = [
|
| 67 |
+
{ t:"S1", precision:71, satisfaction:68, export:55, match_score:62 },
|
| 68 |
+
{ t:"S2", precision:74, satisfaction:72, export:61, match_score:66 },
|
| 69 |
+
{ t:"S3", precision:79, satisfaction:75, export:67, match_score:71 },
|
| 70 |
+
{ t:"S4", precision:83, satisfaction:80, export:74, match_score:75 },
|
| 71 |
+
{ t:"S5", precision:87, satisfaction:84, export:79, match_score:79 },
|
| 72 |
+
{ t:"S6", precision:91, satisfaction:89, export:85, match_score:83 },
|
| 73 |
+
];
|
| 74 |
+
// Compétences détectées / validées / matchées
|
| 75 |
+
const kpiCompetences = [
|
| 76 |
+
{ t:"S1", detected:3.1, validated:2.4, matched:1.8 },
|
| 77 |
+
{ t:"S2", detected:3.4, validated:2.8, matched:2.2 },
|
| 78 |
+
{ t:"S3", detected:4.0, validated:3.3, matched:2.7 },
|
| 79 |
+
{ t:"S4", detected:4.2, validated:3.6, matched:3.1 },
|
| 80 |
+
{ t:"S5", detected:4.6, validated:4.0, matched:3.5 },
|
| 81 |
+
{ t:"S6", detected:5.0, validated:4.4, matched:3.9 },
|
| 82 |
+
];
|
| 83 |
+
|
| 84 |
+
/* ══════════ PERSONAS ══════════ */
|
| 85 |
+
const PERSONAS = {
|
| 86 |
+
sophie: {
|
| 87 |
+
id: "sophie", name: "Sophie", age: 38, icon: "👩💼",
|
| 88 |
+
role: "Conseillère France Travail — Caen",
|
| 89 |
+
color: "#059669", colorSoft: "#ECFDF5", colorMid: "#A7F3D0",
|
| 90 |
+
defaultMode: "keywords",
|
| 91 |
+
tagline: "Code ROME en 30 sec → PPAE sans ressaisie",
|
| 92 |
+
context: "Open-space bruyant. Écran 19\". PPAE + Emploi Store ouverts. 23 mails non lus. Demandeur en face. 35 min max par entretien.",
|
| 93 |
+
goal: "Identifier en 2 clics les codes ROME + NAF correspondant au profil oral du demandeur, pour orienter et remplir le PPAE sans friction.",
|
| 94 |
+
skills: ["Écoute active","Reformulation métier","Connaissance ROME","Orientation emploi","Saisie PPAE","Gestion du temps"],
|
| 95 |
+
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"],
|
| 96 |
+
success: "Elle tourne l'écran vers le demandeur, clique Copier, colle dans le PPAE. Zéro ressaisie.",
|
| 97 |
+
rgaa: ["Contraste AA minimum","Navigation clavier","Lecteur d'écran"],
|
| 98 |
+
quickActions: ["Copier ROME","Copier NAF","Envoyer PPAE"],
|
| 99 |
+
placeholder: "ex: agent logistique polyvalent, infirmière coordinatrice…",
|
| 100 |
+
tip: "💡 Dites simplement le métier décrit par le demandeur — le micro est activable",
|
| 101 |
+
},
|
| 102 |
+
karim: {
|
| 103 |
+
id: "karim", name: "Karim", age: 44, icon: "🎯",
|
| 104 |
+
role: "Chasseur de Tête indépendant — Paris/TGV",
|
| 105 |
+
color: "#2563EB", colorSoft: "#EFF6FF", colorMid: "#BFDBFE",
|
| 106 |
+
defaultMode: "annonce",
|
| 107 |
+
tagline: "Annonce brute → ROME + NAF + Compétences en 12 sec",
|
| 108 |
+
context: "MacBook en mobilité. ATS Bullhorn ouvert. Annonce client reçue à 18h avec fautes et acronymes. Brief client à 9h le lendemain.",
|
| 109 |
+
goal: "Extraire en 10 secondes les codes ROME + NAF d'une annonce brute pour lancer immédiatement le sourcing LinkedIn Recruiter.",
|
| 110 |
+
skills: ["Lecture annonce","Sourcing LinkedIn","Qualification profil","Connaissance secteurs","Négociation","Gestion mandats"],
|
| 111 |
+
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"],
|
| 112 |
+
success: "Il copie le code ROME, ouvre LinkedIn Recruiter, colle dans le filtre, lance la recherche. 12 secondes chrono.",
|
| 113 |
+
rgaa: ["Copier en 1 clic","Résultat lisible mobile","Export rapide"],
|
| 114 |
+
quickActions: ["Copier ROME","Copier NAF","Ouvrir LinkedIn","Exporter fiche"],
|
| 115 |
+
placeholder: "Collez l'annonce brute complète — même avec fautes et acronymes internes…",
|
| 116 |
+
tip: "💡 Collez l'annonce telle quelle, l'IA normalise et extrait ROME, NAF et compétences",
|
| 117 |
+
},
|
| 118 |
+
};
|
| 119 |
+
|
| 120 |
+
/* ══════════ SECTEURS ══════════ */
|
| 121 |
+
const SECTORS = [
|
| 122 |
+
{id:"all",label:"Tous secteurs",icon:"🌐"},
|
| 123 |
+
{id:"IT",label:"IT & Digital",icon:"💻"},
|
| 124 |
+
{id:"Santé",label:"Santé & Social",icon:"🏥"},
|
| 125 |
+
{id:"BTP",label:"BTP & Industrie",icon:"🏗"},
|
| 126 |
+
{id:"Commerce",label:"Commerce & Vente",icon:"🛒"},
|
| 127 |
+
{id:"RH",label:"RH & Formation",icon:"👥"},
|
| 128 |
+
{id:"Finance",label:"Finance & Gestion",icon:"📊"},
|
| 129 |
+
{id:"Transport",label:"Transport & Logistique",icon:"🚛"},
|
| 130 |
+
];
|
| 131 |
+
|
| 132 |
+
const LEVELS = [
|
| 133 |
+
{id:"all",label:"Tous niveaux"},{id:"CAP",label:"CAP/BEP"},
|
| 134 |
+
{id:"BAC",label:"Bac"},{id:"BAC+2",label:"Bac+2"},
|
| 135 |
+
{id:"BAC+3",label:"Bac+3/4"},{id:"BAC+5",label:"Bac+5+"},
|
| 136 |
+
];
|
| 137 |
+
|
| 138 |
+
/* ══════════ ATOMS ══════════ */
|
| 139 |
+
const Spinner = () => (
|
| 140 |
+
<div style={{width:16,height:16,border:"2px solid #E2E8F0",borderTop:"2px solid #2563EB",borderRadius:"50%",animation:"spin .7s linear infinite",flexShrink:0}}/>
|
| 141 |
+
);
|
| 142 |
+
const Skel = ({w="100%",h=14,r=6,mb=0}) => (
|
| 143 |
+
<div style={{width:w,height:h,borderRadius:r,marginBottom:mb,background:"linear-gradient(90deg,#F1F5F9 25%,#E2E8F0 50%,#F1F5F9 75%)",backgroundSize:"200% 100%",animation:"shimmer 1.4s infinite"}}/>
|
| 144 |
+
);
|
| 145 |
+
|
| 146 |
+
const BdgType = ({type}) => {
|
| 147 |
+
const m = {
|
| 148 |
+
exact:{bg:"#DCFCE7",c:"#15803D",br:"#86EFAC",l:"Exact"},
|
| 149 |
+
proche:{bg:"#DBEAFE",c:"#1D4ED8",br:"#93C5FD",l:"Proche"},
|
| 150 |
+
"associé":{bg:"#FEF3C7",c:"#B45309",br:"#FCD34D",l:"Associé"},
|
| 151 |
+
naf:{bg:"#EDE9FE",c:"#6D28D9",br:"#C4B5FD",l:"NAF"},
|
| 152 |
+
clé:{bg:"#DCFCE7",c:"#166534",br:"#86EFAC",l:"Clé métier"},
|
| 153 |
+
transverse:{bg:"#F3E8FF",c:"#7E22CE",br:"#D8B4FE",l:"Transverse"},
|
| 154 |
+
}[type]||{bg:"#F1F5F9",c:"#475569",br:"#CBD5E1",l:type};
|
| 155 |
+
return <span style={{display:"inline-flex",alignItems:"center",padding:"2px 8px",borderRadius:20,fontSize:10,fontWeight:800,letterSpacing:"0.07em",textTransform:"uppercase",background:m.bg,color:m.c,border:`1px solid ${m.br}`}}>{m.l}</span>;
|
| 156 |
+
};
|
| 157 |
+
|
| 158 |
+
function CopyBtn({text,label="Copier",onCopy,accent="#2563EB"}){
|
| 159 |
+
const[ok,setOk]=useState(false);
|
| 160 |
+
return <button onClick={()=>{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}}>
|
| 161 |
+
{ok?"✓":"⎘"} {ok?"Copié":label}
|
| 162 |
+
</button>;
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
+
function exportCSV(rows, filename="export") {
|
| 166 |
+
const csv = rows.map(r=>r.map(v=>`"${String(v).replace(/"/g,'""')}"`).join(";")).join("\n");
|
| 167 |
+
const a = document.createElement("a");
|
| 168 |
+
a.href = "data:text/csv;charset=utf-8,\uFEFF"+encodeURIComponent(csv);
|
| 169 |
+
a.download = `${filename}_${Date.now()}.csv`;
|
| 170 |
+
a.click();
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
/* ══════════ DOSSIER LIGHT ATOMS ══════════ */
|
| 174 |
+
const DTag = ({label,color}) => (
|
| 175 |
+
<span style={{background:color+"18",color,border:`1px solid ${color}44`,padding:"2px 10px",borderRadius:20,fontSize:11,fontWeight:600}}>{label}</span>
|
| 176 |
+
);
|
| 177 |
+
const DCard = ({children,style={}}) => (
|
| 178 |
+
<div style={{background:"#FFFFFF",border:"1px solid #E2E8F0",borderRadius:12,padding:20,boxShadow:"0 1px 3px rgba(0,0,0,.05)",...style}}>{children}</div>
|
| 179 |
+
);
|
| 180 |
+
const DSec = ({title,children}) => (
|
| 181 |
+
<div style={{marginBottom:24}}>
|
| 182 |
+
<div style={{display:"flex",alignItems:"center",gap:10,marginBottom:12}}>
|
| 183 |
+
<div style={{width:3,height:20,background:"#2563EB",borderRadius:2}}/>
|
| 184 |
+
<h3 style={{margin:0,fontSize:11,fontFamily:"Syne,sans-serif",color:"#2563EB",letterSpacing:"0.1em",textTransform:"uppercase"}}>{title}</h3>
|
| 185 |
+
</div>
|
| 186 |
+
{children}
|
| 187 |
+
</div>
|
| 188 |
+
);
|
| 189 |
+
const DStep = ({n,label,detail,tags}) => (
|
| 190 |
+
<div style={{display:"flex",gap:12,marginBottom:14}}>
|
| 191 |
+
<div style={{minWidth:26,height:26,borderRadius:"50%",background:"#2563EB",color:"#FFF",display:"flex",alignItems:"center",justifyContent:"center",fontWeight:800,fontSize:12,fontFamily:"Syne,sans-serif",flexShrink:0}}>{n}</div>
|
| 192 |
+
<div>
|
| 193 |
+
<div style={{color:"#0F172A",fontWeight:600,fontSize:13}}>{label}</div>
|
| 194 |
+
{detail&&<div style={{color:"#6B7280",fontSize:11,marginTop:3,lineHeight:1.6}}>{detail}</div>}
|
| 195 |
+
{tags&&<div style={{marginTop:6,display:"flex",flexWrap:"wrap",gap:5}}>{tags.map((t,i)=><DTag key={i} label={t.l} color={t.c}/>)}</div>}
|
| 196 |
+
</div>
|
| 197 |
+
</div>
|
| 198 |
+
);
|
| 199 |
+
const KPIBox = ({label,value,unit,delta,color}) => (
|
| 200 |
+
<div style={{background:color+"12",border:`1px solid ${color}33`,borderRadius:10,padding:"14px 18px",flex:1,minWidth:120,boxShadow:"0 1px 2px rgba(0,0,0,.04)"}}>
|
| 201 |
+
<div style={{color:"#9CA3AF",fontSize:10,textTransform:"uppercase",letterSpacing:"0.08em"}}>{label}</div>
|
| 202 |
+
<div style={{color,fontSize:24,fontWeight:800,fontFamily:"Syne,sans-serif",marginTop:4}}>{value}<span style={{fontSize:12,marginLeft:4}}>{unit}</span></div>
|
| 203 |
+
{delta!=null&&<div style={{color:delta>0?"#059669":"#E11D48",fontSize:11,marginTop:2}}>{delta>0?"▲":"▼"} {Math.abs(delta)}% vs S-1</div>}
|
| 204 |
+
</div>
|
| 205 |
+
);
|
| 206 |
+
const TT = {contentStyle:{background:"#FFFFFF",border:"1px solid #E2E8F0",color:"#0F172A",fontSize:11,borderRadius:8,boxShadow:"0 4px 12px rgba(0,0,0,.1)"}};
|
| 207 |
+
|
| 208 |
+
/* ══════════ MATCH ANNONCES PAR COMPÉTENCES ══════════ */
|
| 209 |
+
function MatchAnnonces({competences, codes, persona}) {
|
| 210 |
+
const pc = PERSONAS[persona];
|
| 211 |
+
const[annonce,setAnnonce]=useState("");
|
| 212 |
+
const[loading,setLoading]=useState(false);
|
| 213 |
+
const[result,setResult]=useState(null);
|
| 214 |
+
const[error,setError]=useState("");
|
| 215 |
+
const[open,setOpen]=useState(false);
|
| 216 |
+
const fileRef=useRef();
|
| 217 |
+
|
| 218 |
+
const allCompetences = competences
|
| 219 |
+
? Object.values(competences).flat()
|
| 220 |
+
: [];
|
| 221 |
+
|
| 222 |
+
const match = useCallback(async() => {
|
| 223 |
+
if(!annonce.trim()||!allCompetences.length) return;
|
| 224 |
+
setLoading(true); setResult(null); setError("");
|
| 225 |
+
try {
|
| 226 |
+
const res = await fetch("/api/claude", {
|
| 227 |
+
method:"POST", headers:{"Content-Type":"application/json"},
|
| 228 |
+
body: JSON.stringify({model:"claude-sonnet-4-20250514",max_tokens:1200,
|
| 229 |
+
messages:[{role:"user",content:
|
| 230 |
+
`Expert RH et référentiel ROME.
|
| 231 |
+
Compétences du profil : ${allCompetences.join(", ")}
|
| 232 |
+
Codes ROME du profil : ${codes.join(", ")}
|
| 233 |
+
|
| 234 |
+
Annonce d'emploi :
|
| 235 |
+
${annonce.slice(0,3000)}
|
| 236 |
+
|
| 237 |
+
Analyse le niveau de correspondance. Retourne UNIQUEMENT JSON valide sans markdown :
|
| 238 |
+
{"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"]}`
|
| 239 |
+
}]})
|
| 240 |
+
});
|
| 241 |
+
const data = await res.json();
|
| 242 |
+
const raw = data.content?.[0]?.text||"";
|
| 243 |
+
setResult(JSON.parse(raw.replace(/```json|```/g,"").trim()));
|
| 244 |
+
} catch{ setError("Erreur d'analyse. Réessayez."); }
|
| 245 |
+
finally{ setLoading(false); }
|
| 246 |
+
},[annonce, allCompetences, codes]);
|
| 247 |
+
|
| 248 |
+
const scoreColor = (s) => s>=80?"#059669":s>=60?"#D97706":s>=40?"#EA580C":"#E11D48";
|
| 249 |
+
const scoreLabel = (s) => s>=80?"Excellent":s>=60?"Bon":s>=40?"Partiel":"Faible";
|
| 250 |
+
const scoreBg = (s) => s>=80?"#ECFDF5":s>=60?"#FFFBEB":s>=40?"#FFF7ED":"#FFF1F2";
|
| 251 |
+
|
| 252 |
+
if(!allCompetences.length) return null;
|
| 253 |
+
|
| 254 |
+
return (
|
| 255 |
+
<div style={{background:"#FFF",border:"1.5px solid #E2E8F0",borderRadius:14,overflow:"hidden",boxShadow:"0 1px 2px rgba(0,0,0,.05)",marginTop:12}}>
|
| 256 |
+
<button onClick={()=>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"}>
|
| 257 |
+
<div style={{display:"flex",alignItems:"center",gap:10}}>
|
| 258 |
+
<div style={{width:32,height:32,borderRadius:9,background:"linear-gradient(135deg,#7C3AED20,#2563EB20)",display:"flex",alignItems:"center",justifyContent:"center",fontSize:16,border:"1.5px solid #C7D2FE"}}>🎯</div>
|
| 259 |
+
<div>
|
| 260 |
+
<div style={{fontSize:14,fontWeight:700,color:"#0F172A"}}>Matcher une annonce</div>
|
| 261 |
+
<div style={{fontSize:11,color:"#9CA3AF"}}>Score de correspondance profil ↔ offre d'emploi · {allCompetences.length} compétences disponibles</div>
|
| 262 |
+
</div>
|
| 263 |
+
</div>
|
| 264 |
+
<span style={{fontSize:16,color:"#9CA3AF",transition:"transform .2s",transform:open?"rotate(180deg)":"none"}}>⌄</span>
|
| 265 |
+
</button>
|
| 266 |
+
|
| 267 |
+
{open&&(
|
| 268 |
+
<div style={{padding:"0 20px 20px",animation:"fadeIn .3s ease"}}>
|
| 269 |
+
<div style={{height:1,background:"#E2E8F0",marginBottom:16}}/>
|
| 270 |
+
|
| 271 |
+
{/* Compétences disponibles */}
|
| 272 |
+
<div style={{background:"#F8FAFC",borderRadius:10,padding:"12px 14px",marginBottom:14,border:"1px solid #E2E8F0"}}>
|
| 273 |
+
<div style={{fontSize:10.5,fontWeight:700,color:"#9CA3AF",letterSpacing:"0.07em",textTransform:"uppercase",marginBottom:8}}>Profil — {allCompetences.length} compétences à matcher</div>
|
| 274 |
+
<div style={{display:"flex",flexWrap:"wrap",gap:6}}>
|
| 275 |
+
{allCompetences.slice(0,20).map((c,i)=>(
|
| 276 |
+
<span key={i} style={{padding:"3px 10px",borderRadius:20,fontSize:11,fontWeight:500,background:pc.colorSoft,color:pc.color,border:`1px solid ${pc.colorMid}`}}>{c}</span>
|
| 277 |
+
))}
|
| 278 |
+
{allCompetences.length>20&&<span style={{padding:"3px 10px",borderRadius:20,fontSize:11,color:"#9CA3AF"}}>+{allCompetences.length-20} autres</span>}
|
| 279 |
+
</div>
|
| 280 |
+
</div>
|
| 281 |
+
|
| 282 |
+
{/* Input annonce */}
|
| 283 |
+
<div style={{marginBottom:12}}>
|
| 284 |
+
<textarea value={annonce} onChange={e=>setAnnonce(e.target.value)} rows={5}
|
| 285 |
+
placeholder="Collez ici l'offre d'emploi à évaluer pour ce profil…"
|
| 286 |
+
style={{width:"100%",background:"#FFF",border:"1.5px solid #E2E8F0",borderRadius:10,padding:"12px 14px",fontSize:13,color:"#0F172A",outline:"none",resize:"vertical",lineHeight:1.7,transition:"all .2s"}}
|
| 287 |
+
onFocus={e=>{e.target.style.borderColor=pc.color;e.target.style.boxShadow=`0 0 0 3px ${pc.color}18`}}
|
| 288 |
+
onBlur={e=>{e.target.style.borderColor="#E2E8F0";e.target.style.boxShadow="none"}}/>
|
| 289 |
+
</div>
|
| 290 |
+
<div style={{display:"flex",gap:9,marginBottom:16}}>
|
| 291 |
+
<button onClick={()=>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</button>
|
| 292 |
+
<input ref={fileRef} type="file" accept=".txt,.doc,.docx,.pdf" style={{display:"none"}} onChange={e=>{const f=e.target.files?.[0];if(!f)return;const r=new FileReader();r.onload=ev=>setAnnonce(ev.target.result);r.readAsText(f)}}/>
|
| 293 |
+
<button onClick={match} disabled={loading||!annonce.trim()}
|
| 294 |
+
style={{flex:1,display:"flex",alignItems:"center",justifyContent:"center",gap:8,padding:"10px",borderRadius:9,background:loading||!annonce.trim()?"#F1F5F9":"linear-gradient(135deg,#7C3AED,#2563EB)",border:"none",color:loading||!annonce.trim()?"#9CA3AF":"#FFF",fontWeight:700,fontSize:13,cursor:loading||!annonce.trim()?"not-allowed":"pointer",boxShadow:!loading&&annonce.trim()?"0 2px 10px rgba(124,58,237,.3)":"none"}}>
|
| 295 |
+
{loading?<><Spinner/>Calcul du score…</>:<>⚡ Calculer le score de match</>}
|
| 296 |
+
</button>
|
| 297 |
+
</div>
|
| 298 |
+
|
| 299 |
+
{error&&<div style={{padding:"12px 16px",background:"#FFF1F2",border:"1px solid #FECDD3",borderRadius:9,color:"#9F1239",fontSize:12,marginBottom:12}}>⚠ {error}</div>}
|
| 300 |
+
|
| 301 |
+
{loading&&(
|
| 302 |
+
<div style={{display:"flex",flexDirection:"column",gap:10}}>
|
| 303 |
+
<div style={{background:"#F8FAFC",borderRadius:12,padding:20,border:"1px solid #E2E8F0"}}>
|
| 304 |
+
<Skel w="40%" h={40} r={8} mb={14}/><Skel w="70%" h={13} mb={8}/><Skel w="55%" h={13}/>
|
| 305 |
+
</div>
|
| 306 |
+
</div>
|
| 307 |
+
)}
|
| 308 |
+
|
| 309 |
+
{result&&(
|
| 310 |
+
<div style={{animation:"fadeIn .4s ease",display:"flex",flexDirection:"column",gap:12}}>
|
| 311 |
+
|
| 312 |
+
{/* Score hero */}
|
| 313 |
+
<div style={{background:scoreBg(result.score),border:`2px solid ${scoreColor(result.score)}33`,borderRadius:14,padding:"20px 22px",display:"flex",alignItems:"center",gap:20}}>
|
| 314 |
+
<div style={{flexShrink:0,textAlign:"center"}}>
|
| 315 |
+
<div style={{fontSize:52,fontWeight:900,color:scoreColor(result.score),lineHeight:1,fontFamily:"Syne,sans-serif"}}>{result.score}</div>
|
| 316 |
+
<div style={{fontSize:11,fontWeight:700,color:scoreColor(result.score),marginTop:2}}>/100</div>
|
| 317 |
+
</div>
|
| 318 |
+
<div style={{flex:1}}>
|
| 319 |
+
<div style={{display:"flex",alignItems:"center",gap:10,marginBottom:8,flexWrap:"wrap"}}>
|
| 320 |
+
<span style={{background:scoreColor(result.score),color:"#FFF",fontSize:12,fontWeight:800,padding:"4px 14px",borderRadius:20}}>{scoreLabel(result.score)}</span>
|
| 321 |
+
{result.titre_poste&&<span style={{fontSize:13,fontWeight:700,color:"#0F172A"}}>{result.titre_poste}</span>}
|
| 322 |
+
{result.rome_annonce&&<span style={{fontFamily:"monospace",fontSize:12,fontWeight:700,color:"#2563EB",background:"#EFF6FF",padding:"2px 9px",borderRadius:6,border:"1px solid #BFDBFE"}}>{result.rome_annonce}</span>}
|
| 323 |
+
</div>
|
| 324 |
+
{/* Score bar */}
|
| 325 |
+
<div style={{height:8,background:"rgba(0,0,0,.08)",borderRadius:4,overflow:"hidden",marginBottom:8}}>
|
| 326 |
+
<div style={{height:"100%",width:`${result.score}%`,background:`linear-gradient(90deg,${scoreColor(result.score)},${scoreColor(result.score)}CC)`,borderRadius:4,transition:"width 1.2s cubic-bezier(.4,0,.2,1)"}}/>
|
| 327 |
+
</div>
|
| 328 |
+
<p style={{fontSize:13,color:"#374151",lineHeight:1.65,margin:0}}>{result.recommandation}</p>
|
| 329 |
+
</div>
|
| 330 |
+
</div>
|
| 331 |
+
|
| 332 |
+
{/* 3 colonnes : matchées / manquantes / bonus */}
|
| 333 |
+
<div style={{display:"grid",gridTemplateColumns:"1fr 1fr 1fr",gap:10}}>
|
| 334 |
+
<div style={{background:"#ECFDF5",borderRadius:12,padding:"14px 16px",border:"1.5px solid #A7F3D0"}}>
|
| 335 |
+
<div style={{fontSize:10.5,fontWeight:800,color:"#059669",letterSpacing:"0.07em",textTransform:"uppercase",marginBottom:10}}>✓ Matchées ({result.competences_matchees?.length||0})</div>
|
| 336 |
+
<div style={{display:"flex",flexDirection:"column",gap:5}}>
|
| 337 |
+
{(result.competences_matchees||[]).map((c,i)=><div key={i} style={{display:"flex",alignItems:"center",gap:6,fontSize:12,color:"#065F46"}}><span style={{color:"#059669",flexShrink:0}}>●</span>{c}</div>)}
|
| 338 |
+
</div>
|
| 339 |
+
</div>
|
| 340 |
+
<div style={{background:"#FFF7ED",borderRadius:12,padding:"14px 16px",border:"1.5px solid #FED7AA"}}>
|
| 341 |
+
<div style={{fontSize:10.5,fontWeight:800,color:"#EA580C",letterSpacing:"0.07em",textTransform:"uppercase",marginBottom:10}}>⚠ Manquantes ({result.competences_manquantes?.length||0})</div>
|
| 342 |
+
<div style={{display:"flex",flexDirection:"column",gap:5}}>
|
| 343 |
+
{(result.competences_manquantes||[]).map((c,i)=><div key={i} style={{display:"flex",alignItems:"center",gap:6,fontSize:12,color:"#9A3412"}}><span style={{color:"#EA580C",flexShrink:0}}>○</span>{c}</div>)}
|
| 344 |
+
</div>
|
| 345 |
+
</div>
|
| 346 |
+
<div style={{background:"#EFF6FF",borderRadius:12,padding:"14px 16px",border:"1.5px solid #BFDBFE"}}>
|
| 347 |
+
<div style={{fontSize:10.5,fontWeight:800,color:"#2563EB",letterSpacing:"0.07em",textTransform:"uppercase",marginBottom:10}}>+ Bonus ({result.competences_bonus?.length||0})</div>
|
| 348 |
+
<div style={{display:"flex",flexDirection:"column",gap:5}}>
|
| 349 |
+
{(result.competences_bonus||[]).map((c,i)=><div key={i} style={{display:"flex",alignItems:"center",gap:6,fontSize:12,color:"#1E40AF"}}><span style={{color:"#2563EB",flexShrink:0}}>+</span>{c}</div>)}
|
| 350 |
+
</div>
|
| 351 |
+
</div>
|
| 352 |
+
</div>
|
| 353 |
+
|
| 354 |
+
{/* Points forts / vigilance */}
|
| 355 |
+
<div style={{display:"grid",gridTemplateColumns:"1fr 1fr",gap:10}}>
|
| 356 |
+
<div style={{background:"#F8FAFC",borderRadius:12,padding:"14px 16px",border:"1px solid #E2E8F0"}}>
|
| 357 |
+
<div style={{fontSize:10.5,fontWeight:800,color:"#374151",letterSpacing:"0.07em",textTransform:"uppercase",marginBottom:10}}>💪 Points forts</div>
|
| 358 |
+
{(result.points_forts||[]).map((p,i)=><div key={i} style={{display:"flex",gap:7,marginBottom:6,alignItems:"flex-start"}}><span style={{color:"#059669",flexShrink:0,marginTop:1}}>✓</span><span style={{fontSize:12,color:"#374151"}}>{p}</span></div>)}
|
| 359 |
+
</div>
|
| 360 |
+
<div style={{background:"#FFFBEB",borderRadius:12,padding:"14px 16px",border:"1px solid #FDE68A"}}>
|
| 361 |
+
<div style={{fontSize:10.5,fontWeight:800,color:"#92400E",letterSpacing:"0.07em",textTransform:"uppercase",marginBottom:10}}>🔍 Points de vigilance</div>
|
| 362 |
+
{(result.points_vigilance||[]).map((p,i)=><div key={i} style={{display:"flex",gap:7,marginBottom:6,alignItems:"flex-start"}}><span style={{color:"#D97706",flexShrink:0,marginTop:1}}>!</span><span style={{fontSize:12,color:"#92400E"}}>{p}</span></div>)}
|
| 363 |
+
</div>
|
| 364 |
+
</div>
|
| 365 |
+
</div>
|
| 366 |
+
)}
|
| 367 |
+
</div>
|
| 368 |
+
)}
|
| 369 |
+
</div>
|
| 370 |
+
);
|
| 371 |
+
}
|
| 372 |
+
|
| 373 |
+
/* ══════════ COMPETENCES SECTION ══════════ */
|
| 374 |
+
function CompetencesBlock({codes, persona}) {
|
| 375 |
+
const[loading,setLoading]=useState(false);
|
| 376 |
+
const[competences,setCompetences]=useState(null);
|
| 377 |
+
const[open,setOpen]=useState(false);
|
| 378 |
+
const pc = PERSONAS[persona];
|
| 379 |
+
|
| 380 |
+
const fetchCompetences = useCallback(async() => {
|
| 381 |
+
if(!codes?.length) return;
|
| 382 |
+
setLoading(true); setOpen(true);
|
| 383 |
+
try {
|
| 384 |
+
const res = await fetch("/api/claude", {
|
| 385 |
+
method:"POST",
|
| 386 |
+
headers:{"Content-Type":"application/json"},
|
| 387 |
+
body: JSON.stringify({model:"claude-sonnet-4-20250514",max_tokens:800,messages:[{role:"user",content:
|
| 388 |
+
`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.`
|
| 389 |
+
}]})
|
| 390 |
+
});
|
| 391 |
+
const data = await res.json();
|
| 392 |
+
const raw = data.content?.[0]?.text||"";
|
| 393 |
+
setCompetences(JSON.parse(raw.replace(/```json|```/g,"").trim()));
|
| 394 |
+
} catch{ setCompetences(null); }
|
| 395 |
+
finally{ setLoading(false); }
|
| 396 |
+
},[codes]);
|
| 397 |
+
|
| 398 |
+
const categories = competences ? [
|
| 399 |
+
{key:"cles_metier",label:"Cœur de métier",color:pc.color,bg:pc.colorSoft,type:"clé"},
|
| 400 |
+
{key:"transverses",label:"Transversales",color:"#7C3AED",bg:"#F5F3FF",type:"transverse"},
|
| 401 |
+
{key:"numeriques",label:"Outils & digital",color:"#0891B2",bg:"#ECFEFF",type:"proche"},
|
| 402 |
+
{key:"savoir_etre",label:"Savoir-être",color:"#D97706",bg:"#FFFBEB",type:"associé"},
|
| 403 |
+
{key:"certifications",label:"Certifications",color:"#6B7280",bg:"#F1F5F9",type:"exact"},
|
| 404 |
+
] : [];
|
| 405 |
+
|
| 406 |
+
return (
|
| 407 |
+
<div style={{background:"#FFF",border:"1.5px solid #E2E8F0",borderRadius:14,overflow:"hidden",boxShadow:"0 1px 2px rgba(0,0,0,.05)"}}>
|
| 408 |
+
<button onClick={()=>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"}>
|
| 409 |
+
<div style={{display:"flex",alignItems:"center",gap:10}}>
|
| 410 |
+
<div style={{width:32,height:32,borderRadius:9,background:pc.colorSoft,display:"flex",alignItems:"center",justifyContent:"center",fontSize:16,border:`1.5px solid ${pc.colorMid}`}}>🧩</div>
|
| 411 |
+
<div>
|
| 412 |
+
<div style={{fontSize:14,fontWeight:700,color:"#0F172A"}}>Compétences associées</div>
|
| 413 |
+
<div style={{fontSize:11,color:"#9CA3AF"}}>Référentiel ROME · cœur métier, transversales, certifications</div>
|
| 414 |
+
</div>
|
| 415 |
+
</div>
|
| 416 |
+
<div style={{display:"flex",alignItems:"center",gap:8}}>
|
| 417 |
+
{loading&&<Spinner/>}
|
| 418 |
+
<span style={{fontSize:16,color:"#9CA3AF",transition:"transform .2s",transform:open?"rotate(180deg)":"none"}}>⌄</span>
|
| 419 |
+
</div>
|
| 420 |
+
</button>
|
| 421 |
+
{open&&!loading&&competences&&(
|
| 422 |
+
<div style={{padding:"0 20px 20px",animation:"fadeIn .3s ease"}}>
|
| 423 |
+
<div style={{height:1,background:"#E2E8F0",marginBottom:16}}/>
|
| 424 |
+
<div style={{display:"flex",flexDirection:"column",gap:14}}>
|
| 425 |
+
{categories.map(cat=>(competences[cat.key]||[]).length>0&&(
|
| 426 |
+
<div key={cat.key}>
|
| 427 |
+
<div style={{fontSize:11,fontWeight:800,letterSpacing:"0.07em",textTransform:"uppercase",color:cat.color,marginBottom:8}}>{cat.label}</div>
|
| 428 |
+
<div style={{display:"flex",flexWrap:"wrap",gap:7}}>
|
| 429 |
+
{(competences[cat.key]||[]).map((c,i)=>(
|
| 430 |
+
<div key={i} style={{display:"flex",alignItems:"center",gap:6,padding:"5px 12px",borderRadius:20,background:cat.bg,border:`1.5px solid ${cat.color}33`,cursor:"pointer",transition:"all .15s"}} title="Cliquer pour copier" onClick={()=>navigator.clipboard.writeText(c)}>
|
| 431 |
+
<span style={{fontSize:12,fontWeight:600,color:cat.color}}>{c}</span>
|
| 432 |
+
<span style={{fontSize:10,color:cat.color,opacity:.6}}>⎘</span>
|
| 433 |
+
</div>
|
| 434 |
+
))}
|
| 435 |
+
</div>
|
| 436 |
+
</div>
|
| 437 |
+
))}
|
| 438 |
+
</div>
|
| 439 |
+
<div style={{marginTop:14,display:"flex",gap:8}}>
|
| 440 |
+
<CopyBtn text={Object.values(competences).flat().join(", ")} label="Tout copier"/>
|
| 441 |
+
<button onClick={()=>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</button>
|
| 442 |
+
</div>
|
| 443 |
+
{/* Match annonce sur base compétences */}
|
| 444 |
+
<MatchAnnonces competences={competences} codes={codes} persona={persona}/>
|
| 445 |
+
</div>
|
| 446 |
+
)}
|
| 447 |
+
</div>
|
| 448 |
+
);
|
| 449 |
+
}
|
| 450 |
+
|
| 451 |
+
/* ══════════ TOOL MODULE ══════════ */
|
| 452 |
+
function ToolModule({persona}) {
|
| 453 |
+
const pc = PERSONAS[persona];
|
| 454 |
+
const[mode,setMode]=useState(pc.defaultMode);
|
| 455 |
+
const[input,setInput]=useState("");
|
| 456 |
+
const[annonceText,setAnnonceText]=useState("");
|
| 457 |
+
const[sector,setSector]=useState("all");
|
| 458 |
+
const[level,setLevel]=useState("all");
|
| 459 |
+
const[typeFilter,setTypeFilter]=useState("all");
|
| 460 |
+
const[sortBy,setSortBy]=useState("pertinence");
|
| 461 |
+
const[loading,setLoading]=useState(false);
|
| 462 |
+
const[results,setResults]=useState(null);
|
| 463 |
+
const[compared,setCompared]=useState([]);
|
| 464 |
+
const[listening,setListening]=useState(false);
|
| 465 |
+
const[error,setError]=useState("");
|
| 466 |
+
const fileRef=useRef();
|
| 467 |
+
|
| 468 |
+
const hasContent = mode==="keywords" ? input.trim().length>1 : annonceText.trim().length>20;
|
| 469 |
+
const getQ = () => mode==="keywords" ? input.trim() : annonceText.trim();
|
| 470 |
+
|
| 471 |
+
const analyze = useCallback(async() => {
|
| 472 |
+
if(!hasContent) return;
|
| 473 |
+
setLoading(true); setResults(null); setError("");
|
| 474 |
+
const sh = sector!=="all"?`Secteur prioritaire : ${sector}. `:"";
|
| 475 |
+
const lh = level!=="all"?`Niveau de qualification cible : ${level}. `:"";
|
| 476 |
+
const prompt = mode==="keywords"
|
| 477 |
+
? `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.`
|
| 478 |
+
: `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)}`;
|
| 479 |
+
try {
|
| 480 |
+
const res = await fetch("/api/claude", {
|
| 481 |
+
method:"POST", headers:{"Content-Type":"application/json"},
|
| 482 |
+
body: JSON.stringify({model:"claude-sonnet-4-20250514",max_tokens:1400,messages:[{role:"user",content:prompt}]})
|
| 483 |
+
});
|
| 484 |
+
const data = await res.json();
|
| 485 |
+
const raw = data.content?.[0]?.text||"";
|
| 486 |
+
setResults(JSON.parse(raw.replace(/```json|```/g,"").trim()));
|
| 487 |
+
} catch{ setError("Erreur d'analyse. Vérifiez votre connexion."); }
|
| 488 |
+
finally{ setLoading(false); }
|
| 489 |
+
},[mode,input,annonceText,sector,level,persona]);
|
| 490 |
+
|
| 491 |
+
const startListen = () => {
|
| 492 |
+
const SR = window.SpeechRecognition||window.webkitSpeechRecognition;
|
| 493 |
+
if(!SR) return;
|
| 494 |
+
const r = new SR(); r.lang="fr-FR";
|
| 495 |
+
r.onresult=e=>setInput(e.results[0][0].transcript);
|
| 496 |
+
r.onend=()=>setListening(false);
|
| 497 |
+
r.start(); setListening(true);
|
| 498 |
+
};
|
| 499 |
+
|
| 500 |
+
const filtered = (() => {
|
| 501 |
+
if(!results||!Array.isArray(results)) return results;
|
| 502 |
+
let r=[...results];
|
| 503 |
+
if(typeFilter!=="all") r=r.filter(x=>x.type===typeFilter);
|
| 504 |
+
if(sortBy==="alpha") r=[...r].sort((a,b)=>a.intitule.localeCompare(b.intitule));
|
| 505 |
+
if(sortBy==="type") r=[...r].sort((a,b)=>["exact","proche","associé"].indexOf(a.type)-["exact","proche","associé"].indexOf(b.type));
|
| 506 |
+
return r;
|
| 507 |
+
})();
|
| 508 |
+
|
| 509 |
+
const codesForCompetences = results
|
| 510 |
+
? Array.isArray(results) ? results.map(r=>r.code) : [results.rome_principal?.code,...(results.rome_alternatifs||[]).map(r=>r.code)].filter(Boolean)
|
| 511 |
+
: [];
|
| 512 |
+
|
| 513 |
+
const lvlC={junior:{bg:"#EDE9FE",c:"#6D28D9"},confirmé:{bg:"#DBEAFE",c:"#1D4ED8"},senior:{bg:"#FEF3C7",c:"#B45309"},expert:{bg:"#FEE2E2",c:"#B91C1C"}};
|
| 514 |
+
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"}};
|
| 515 |
+
|
| 516 |
+
return (
|
| 517 |
+
<div>
|
| 518 |
+
{/* Persona context banner */}
|
| 519 |
+
<div style={{background:pc.colorSoft,border:`1.5px solid ${pc.colorMid}`,borderRadius:12,padding:"12px 18px",marginBottom:20,display:"flex",alignItems:"center",gap:14}}>
|
| 520 |
+
<span style={{fontSize:26}}>{pc.icon}</span>
|
| 521 |
+
<div style={{flex:1}}>
|
| 522 |
+
<div style={{fontWeight:800,fontSize:14,color:pc.color}}>{pc.name} — {pc.role}</div>
|
| 523 |
+
<div style={{fontSize:12,color:"#6B7280",marginTop:2}}>{pc.tagline}</div>
|
| 524 |
+
</div>
|
| 525 |
+
<div style={{fontSize:11,color:pc.color,background:"#FFF",border:`1px solid ${pc.colorMid}`,borderRadius:8,padding:"5px 12px",fontWeight:700}}>{pc.tip}</div>
|
| 526 |
+
</div>
|
| 527 |
+
|
| 528 |
+
{/* Mode toggle */}
|
| 529 |
+
<div style={{display:"flex",gap:8,marginBottom:16}}>
|
| 530 |
+
{[{id:"keywords",icon:"⌖",label:"Mots-clés → ROME"},{id:"annonce",icon:"⊡",label:"Annonce → ROME + NAF + Compétences"}].map(m=>(
|
| 531 |
+
<button key={m.id} onClick={()=>{setMode(m.id);setResults(null);setError("")}}
|
| 532 |
+
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)"}}>
|
| 533 |
+
<span style={{fontSize:16}}>{m.icon}</span>{m.label}
|
| 534 |
+
</button>
|
| 535 |
+
))}
|
| 536 |
+
</div>
|
| 537 |
+
|
| 538 |
+
{/* Filtres secteur */}
|
| 539 |
+
<div style={{marginBottom:12,overflowX:"auto",paddingBottom:4}}>
|
| 540 |
+
<div style={{display:"flex",gap:7,width:"max-content"}}>
|
| 541 |
+
{SECTORS.map(s=>(
|
| 542 |
+
<button key={s.id} onClick={()=>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"}}>
|
| 543 |
+
<span>{s.icon}</span>{s.label}
|
| 544 |
+
</button>
|
| 545 |
+
))}
|
| 546 |
+
</div>
|
| 547 |
+
</div>
|
| 548 |
+
|
| 549 |
+
{/* Filtres niveau */}
|
| 550 |
+
<div style={{display:"flex",gap:7,flexWrap:"wrap",alignItems:"center",marginBottom:20}}>
|
| 551 |
+
<span style={{fontSize:11,color:"#9CA3AF",fontWeight:700,textTransform:"uppercase",letterSpacing:"0.06em"}}>Niveau :</span>
|
| 552 |
+
{LEVELS.map(l=>(
|
| 553 |
+
<button key={l.id} onClick={()=>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"}}>
|
| 554 |
+
{l.label}
|
| 555 |
+
</button>
|
| 556 |
+
))}
|
| 557 |
+
</div>
|
| 558 |
+
|
| 559 |
+
{/* Search box */}
|
| 560 |
+
<div style={{background:"#FFF",border:"1.5px solid #E2E8F0",borderRadius:16,padding:20,boxShadow:"0 1px 3px rgba(0,0,0,.07),0 4px 12px rgba(0,0,0,.05)",marginBottom:16}}>
|
| 561 |
+
{mode==="keywords" ? (
|
| 562 |
+
<div style={{display:"flex",gap:10}}>
|
| 563 |
+
<div style={{flex:1,position:"relative"}}>
|
| 564 |
+
<span style={{position:"absolute",left:13,top:"50%",transform:"translateY(-50%)",fontSize:17,opacity:.4,pointerEvents:"none"}}>🔍</span>
|
| 565 |
+
<input value={input} onChange={e=>setInput(e.target.value)} onKeyDown={e=>e.key==="Enter"&&analyze()}
|
| 566 |
+
placeholder={pc.placeholder}
|
| 567 |
+
style={{width:"100%",background:"#FFF",border:"1.5px solid #E2E8F0",borderRadius:10,padding:"12px 48px 12px 42px",fontSize:14,color:"#0F172A",outline:"none",transition:"all .2s"}}
|
| 568 |
+
onFocus={e=>{e.target.style.borderColor=pc.color;e.target.style.boxShadow=`0 0 0 3px ${pc.color}18`}}
|
| 569 |
+
onBlur={e=>{e.target.style.borderColor="#E2E8F0";e.target.style.boxShadow="none"}}/>
|
| 570 |
+
<button onClick={startListen} style={{position:"absolute",right:13,top:"50%",transform:"translateY(-50%)",background:"none",border:"none",cursor:"pointer",fontSize:17,color:listening?"#E11D48":"#9CA3AF",animation:listening?"pulse 1s infinite":"none"}}>🎤</button>
|
| 571 |
+
</div>
|
| 572 |
+
<button onClick={analyze} disabled={loading||!hasContent}
|
| 573 |
+
style={{display:"flex",alignItems:"center",gap:8,padding:"0 22px",borderRadius:10,background:loading||!hasContent?"#F1F5F9":`linear-gradient(135deg,${pc.color},#0891B2)`,border:"none",color:loading||!hasContent?"#9CA3AF":"#FFF",fontWeight:700,fontSize:14,cursor:loading||!hasContent?"not-allowed":"pointer",transition:"all .2s",boxShadow:!loading&&hasContent?`0 2px 10px ${pc.color}35`:"none",whiteSpace:"nowrap"}}>
|
| 574 |
+
{loading?<><Spinner/>Analyse…</>:<>→ Analyser</>}
|
| 575 |
+
</button>
|
| 576 |
+
</div>
|
| 577 |
+
) : (
|
| 578 |
+
<>
|
| 579 |
+
<textarea value={annonceText} onChange={e=>setAnnonceText(e.target.value)} rows={7}
|
| 580 |
+
placeholder={pc.placeholder}
|
| 581 |
+
style={{width:"100%",background:"#FFF",border:"1.5px solid #E2E8F0",borderRadius:10,padding:"13px 16px",fontSize:13.5,color:"#0F172A",outline:"none",resize:"vertical",lineHeight:1.7,marginBottom:12,transition:"all .2s"}}
|
| 582 |
+
onFocus={e=>{e.target.style.borderColor=pc.color;e.target.style.boxShadow=`0 0 0 3px ${pc.color}18`}}
|
| 583 |
+
onBlur={e=>{e.target.style.borderColor="#E2E8F0";e.target.style.boxShadow="none"}}/>
|
| 584 |
+
<div style={{display:"flex",gap:10}}>
|
| 585 |
+
<button onClick={()=>fileRef.current?.click()} style={{display:"flex",alignItems:"center",gap:6,padding:"9px 16px",borderRadius:8,border:"1px solid #E2E8F0",background:"#F8FAFC",color:"#6B7280",fontSize:13,fontWeight:600,cursor:"pointer"}}>📎 Importer</button>
|
| 586 |
+
<input ref={fileRef} type="file" accept=".txt,.doc,.docx,.pdf" style={{display:"none"}} onChange={e=>{const f=e.target.files?.[0];if(!f)return;const r=new FileReader();r.onload=ev=>setAnnonceText(ev.target.result);r.readAsText(f)}}/>
|
| 587 |
+
<button onClick={analyze} disabled={loading||!hasContent}
|
| 588 |
+
style={{flex:1,display:"flex",alignItems:"center",justifyContent:"center",gap:8,padding:"11px",borderRadius:10,background:loading||!hasContent?"#F1F5F9":`linear-gradient(135deg,${pc.color},#0891B2)`,border:"none",color:loading||!hasContent?"#9CA3AF":"#FFF",fontWeight:700,fontSize:14,cursor:loading||!hasContent?"not-allowed":"pointer",boxShadow:!loading&&hasContent?`0 2px 10px ${pc.color}35`:"none"}}>
|
| 589 |
+
{loading?<><Spinner/>Extraction…</>:<>⟶ Extraire ROME + NAF + Compétences</>}
|
| 590 |
+
</button>
|
| 591 |
+
</div>
|
| 592 |
+
</>
|
| 593 |
+
)}
|
| 594 |
+
</div>
|
| 595 |
+
|
| 596 |
+
{/* Comparateur */}
|
| 597 |
+
{compared.length>0&&(
|
| 598 |
+
<div style={{background:"#EEF2FF",border:"1.5px solid #C7D2FE",borderRadius:14,padding:"14px 18px",marginBottom:16}}>
|
| 599 |
+
<div style={{display:"flex",justifyContent:"space-between",alignItems:"center",marginBottom:10}}>
|
| 600 |
+
<span style={{fontSize:12,fontWeight:800,color:"#4F46E5",letterSpacing:"0.06em",textTransform:"uppercase"}}>⚖ Comparateur — {compared.length} code{compared.length>1?"s":""}</span>
|
| 601 |
+
<CopyBtn text={compared.map(c=>c.code).join(", ")} label="Tout copier"/>
|
| 602 |
+
</div>
|
| 603 |
+
<div style={{display:"flex",gap:8,flexWrap:"wrap"}}>
|
| 604 |
+
{compared.map((item,i)=>(
|
| 605 |
+
<div key={i} style={{display:"flex",alignItems:"center",gap:7,padding:"7px 12px",background:"#FFF",borderRadius:9,border:"1px solid #C7D2FE",boxShadow:"0 1px 2px rgba(0,0,0,.04)"}}>
|
| 606 |
+
<span style={{fontFamily:"monospace",fontWeight:800,color:"#4F46E5",fontSize:13}}>{item.code}</span>
|
| 607 |
+
<span style={{fontSize:12,color:"#374151",maxWidth:130,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"}}>{item.intitule}</span>
|
| 608 |
+
<BdgType type={item.type}/>
|
| 609 |
+
<button onClick={()=>setCompared(p=>p.filter((_,idx)=>idx!==i))} style={{background:"none",border:"none",cursor:"pointer",fontSize:13,color:"#9CA3AF",padding:0}}>×</button>
|
| 610 |
+
</div>
|
| 611 |
+
))}
|
| 612 |
+
</div>
|
| 613 |
+
</div>
|
| 614 |
+
)}
|
| 615 |
+
|
| 616 |
+
{error&&<div style={{padding:"13px 18px",background:"#FFF1F2",border:"1px solid #FECDD3",borderRadius:10,color:"#9F1239",fontSize:13,fontWeight:500,marginBottom:16}}><span>⚠ {error}</span></div>}
|
| 617 |
+
|
| 618 |
+
{/* Skeletons */}
|
| 619 |
+
{loading&&<div style={{display:"flex",flexDirection:"column",gap:10}}>
|
| 620 |
+
{[1,2,3].map(i=><div key={i} style={{background:"#FFF",borderRadius:14,padding:20,border:"1.5px solid #E2E8F0"}}>
|
| 621 |
+
<div style={{display:"flex",gap:8,marginBottom:10}}><Skel w="70px" h={11} r={20}/><Skel w="100px" h={11} r={20}/></div>
|
| 622 |
+
<Skel w="50%" h={20} mb={8}/><Skel w="85%" h={12} mb={5}/><Skel w="65%" h={12}/>
|
| 623 |
+
</div>)}
|
| 624 |
+
</div>}
|
| 625 |
+
|
| 626 |
+
{/* Résultats Keywords */}
|
| 627 |
+
{!loading&&results&&Array.isArray(results)&&(
|
| 628 |
+
<div style={{animation:"fadeIn .35s ease"}}>
|
| 629 |
+
<div style={{display:"flex",justifyContent:"space-between",alignItems:"center",marginBottom:14,flexWrap:"wrap",gap:10}}>
|
| 630 |
+
<div style={{display:"flex",alignItems:"center",gap:10,flexWrap:"wrap"}}>
|
| 631 |
+
<span style={{background:pc.color,color:"#FFF",fontWeight:800,fontSize:13,padding:"3px 12px",borderRadius:20}}>{filtered?.length||0}</span>
|
| 632 |
+
<span style={{fontSize:13,color:"#6B7280"}}>code{(filtered?.length||0)>1?"s":""} ROME identifié{(filtered?.length||0)>1?"s":""}</span>
|
| 633 |
+
{["exact","proche","associé"].map(t=>{const n=results.filter(r=>r.type===t).length;return n>0&&<span key={t} style={{fontSize:11,fontWeight:700,padding:"2px 9px",borderRadius:20,background:t==="exact"?"#DCFCE7":t==="proche"?"#DBEAFE":"#FEF3C7",color:t==="exact"?"#15803D":t==="proche"?"#1D4ED8":"#B45309"}}>{n} {t}</span>})}
|
| 634 |
+
</div>
|
| 635 |
+
<div style={{display:"flex",gap:7,flexWrap:"wrap"}}>
|
| 636 |
+
<select value={typeFilter} onChange={e=>setTypeFilter(e.target.value)} style={{padding:"5px 10px",borderRadius:7,border:"1px solid #E2E8F0",background:"#FFF",fontSize:12,color:"#374151",outline:"none",cursor:"pointer"}}>
|
| 637 |
+
<option value="all">Tous types</option>
|
| 638 |
+
<option value="exact">Exact</option>
|
| 639 |
+
<option value="proche">Proche</option>
|
| 640 |
+
<option value="associé">Associé</option>
|
| 641 |
+
</select>
|
| 642 |
+
<select value={sortBy} onChange={e=>setSortBy(e.target.value)} style={{padding:"5px 10px",borderRadius:7,border:"1px solid #E2E8F0",background:"#FFF",fontSize:12,color:"#374151",outline:"none",cursor:"pointer"}}>
|
| 643 |
+
<option value="pertinence">Pertinence</option>
|
| 644 |
+
<option value="alpha">Alphabétique</option>
|
| 645 |
+
<option value="type">Par type</option>
|
| 646 |
+
</select>
|
| 647 |
+
<button onClick={()=>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</button>
|
| 648 |
+
<CopyBtn text={results.map(r=>r.code).join(", ")} label="Tout copier"/>
|
| 649 |
+
</div>
|
| 650 |
+
</div>
|
| 651 |
+
<div style={{display:"flex",flexDirection:"column",gap:10,marginBottom:16}}>
|
| 652 |
+
{(filtered||[]).map((r,i)=>(
|
| 653 |
+
<div key={r.code+i} style={{background:"#FFF",borderRadius:14,padding:"16px 20px",border:"1.5px solid #E2E8F0",boxShadow:"0 1px 2px rgba(0,0,0,.05)",transition:"all .2s",animation:`fadeUp .3s ease ${i*.06}s both"`,display:"flex",alignItems:"flex-start",gap:14}}
|
| 654 |
+
onMouseEnter={e=>{e.currentTarget.style.borderColor=pc.color;e.currentTarget.style.boxShadow=`0 4px 20px ${pc.color}15`}}
|
| 655 |
+
onMouseLeave={e=>{e.currentTarget.style.borderColor="#E2E8F0";e.currentTarget.style.boxShadow="0 1px 2px rgba(0,0,0,.05)"}}>
|
| 656 |
+
<div style={{flexShrink:0,width:34,height:34,borderRadius:9,background:pc.colorSoft,display:"flex",alignItems:"center",justifyContent:"center",fontWeight:900,fontSize:12,color:pc.color,fontFamily:"monospace",border:`1.5px solid ${pc.colorMid}`}}>{i+1}</div>
|
| 657 |
+
<div style={{flex:1,minWidth:0}}>
|
| 658 |
+
<div style={{display:"flex",alignItems:"center",gap:8,marginBottom:7,flexWrap:"wrap"}}>
|
| 659 |
+
<span style={{fontFamily:"monospace",fontSize:15.5,fontWeight:700,color:pc.color,letterSpacing:"0.04em"}}>{r.code}</span>
|
| 660 |
+
<BdgType type={r.type}/>
|
| 661 |
+
{r.domaine&&<span style={{fontSize:11,color:"#9CA3AF",padding:"2px 8px",background:"#F8FAFC",borderRadius:6,border:"1px solid #E2E8F0"}}>{r.domaine}</span>}
|
| 662 |
+
{r.niveauAcces&&<span style={{fontSize:11,color:"#4F46E5",padding:"2px 8px",background:"#EEF2FF",borderRadius:6,border:"1px solid #C7D2FE"}}>{r.niveauAcces}</span>}
|
| 663 |
+
{r.grandDomaine&&<span style={{fontSize:11,color:"#9CA3AF",padding:"2px 8px",background:"#F8FAFC",borderRadius:6,border:"1px solid #E2E8F0"}}>{r.grandDomaine}</span>}
|
| 664 |
+
</div>
|
| 665 |
+
<div style={{fontSize:15,fontWeight:700,color:"#0F172A",marginBottom:4,lineHeight:1.3}}>{r.intitule}</div>
|
| 666 |
+
<div style={{fontSize:13,color:"#6B7280",lineHeight:1.65}}>{r.description}</div>
|
| 667 |
+
</div>
|
| 668 |
+
<div style={{display:"flex",flexDirection:"column",gap:5,flexShrink:0}}>
|
| 669 |
+
<CopyBtn text={r.code}/>
|
| 670 |
+
<button onClick={()=>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}}>
|
| 671 |
+
{compared.find(c=>c.code===r.code)?"✓ Ajouté":"⚖ Comparer"}
|
| 672 |
+
</button>
|
| 673 |
+
</div>
|
| 674 |
+
</div>
|
| 675 |
+
))}
|
| 676 |
+
</div>
|
| 677 |
+
{/* Compétences pour mots-clés */}
|
| 678 |
+
<CompetencesBlock codes={codesForCompetences} persona={persona}/>
|
| 679 |
+
</div>
|
| 680 |
+
)}
|
| 681 |
+
|
| 682 |
+
{/* Résultats Annonce */}
|
| 683 |
+
{!loading&&results&&!Array.isArray(results)&&(
|
| 684 |
+
<div style={{animation:"fadeIn .4s ease"}}>
|
| 685 |
+
{/* Hero ROME */}
|
| 686 |
+
<div style={{background:"#FFF",borderRadius:16,padding:"22px 24px",border:`2px solid ${pc.color}`,boxShadow:`0 4px 20px ${pc.color}12`,marginBottom:12}}>
|
| 687 |
+
<div style={{display:"flex",justifyContent:"space-between",alignItems:"flex-start",flexWrap:"wrap",gap:12,marginBottom:14}}>
|
| 688 |
+
<div style={{display:"flex",gap:7,flexWrap:"wrap"}}>
|
| 689 |
+
<span style={{background:pc.color,color:"#FFF",fontSize:10,fontWeight:800,padding:"3px 9px",borderRadius:20,letterSpacing:"0.08em",textTransform:"uppercase"}}>ROME Principal</span>
|
| 690 |
+
{results.niveau&&lvlC[results.niveau]&&<span style={{...lvlC[results.niveau],fontSize:11,fontWeight:700,padding:"3px 10px",borderRadius:20,textTransform:"capitalize",background:lvlC[results.niveau].bg,color:lvlC[results.niveau].c}}>{results.niveau}</span>}
|
| 691 |
+
{results.contrat&&results.contrat!=="Non précisé"&&ctrC[results.contrat]&&<span style={{fontSize:11,fontWeight:700,padding:"3px 10px",borderRadius:20,background:ctrC[results.contrat].bg,color:ctrC[results.contrat].c}}>{results.contrat}</span>}
|
| 692 |
+
{results.teletravail&&results.teletravail!=="Non précisé"&&<span style={{fontSize:11,fontWeight:700,padding:"3px 10px",borderRadius:20,background:"#ECFDF5",color:"#059669"}}>🏠 {results.teletravail}</span>}
|
| 693 |
+
{results.localisation&&<span style={{fontSize:11,fontWeight:700,padding:"3px 10px",borderRadius:20,background:"#F8FAFC",color:"#6B7280",border:"1px solid #E2E8F0"}}>📍 {results.localisation}</span>}
|
| 694 |
+
</div>
|
| 695 |
+
<div style={{display:"flex",gap:7,flexWrap:"wrap"}}>
|
| 696 |
+
<CopyBtn text={results.rome_principal.code} label="Copier ROME" accent={pc.color}/>
|
| 697 |
+
<CopyBtn text={[results.rome_principal.code,...(results.rome_alternatifs||[]).map(r=>r.code)].join(", ")} label="Tous ROME"/>
|
| 698 |
+
<button onClick={()=>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</button>
|
| 699 |
+
</div>
|
| 700 |
+
</div>
|
| 701 |
+
<div style={{display:"flex",alignItems:"flex-start",gap:16}}>
|
| 702 |
+
<span style={{fontFamily:"monospace",fontSize:26,fontWeight:700,color:pc.color,letterSpacing:"0.04em",flexShrink:0,lineHeight:1}}>{results.rome_principal.code}</span>
|
| 703 |
+
<div>
|
| 704 |
+
<div style={{fontWeight:800,fontSize:17,color:"#0F172A",marginBottom:5,lineHeight:1.3}}>{results.rome_principal.intitule}</div>
|
| 705 |
+
{results.rome_principal.description&&<div style={{fontSize:13,color:"#6B7280",lineHeight:1.65}}>{results.rome_principal.description}</div>}
|
| 706 |
+
</div>
|
| 707 |
+
</div>
|
| 708 |
+
</div>
|
| 709 |
+
|
| 710 |
+
{/* Alternatifs */}
|
| 711 |
+
{results.rome_alternatifs?.length>0&&(
|
| 712 |
+
<div style={{background:"#FFF",borderRadius:14,padding:"16px 20px",border:"1.5px solid #E2E8F0",boxShadow:"0 1px 2px rgba(0,0,0,.05)",marginBottom:12}}>
|
| 713 |
+
<div style={{fontSize:10.5,fontWeight:800,letterSpacing:"0.09em",textTransform:"uppercase",color:"#9CA3AF",marginBottom:12,paddingBottom:8,borderBottom:"1px solid #E2E8F0"}}>ROME Alternatifs</div>
|
| 714 |
+
<div style={{display:"flex",flexDirection:"column",gap:8}}>
|
| 715 |
+
{results.rome_alternatifs.map((r,i)=>(
|
| 716 |
+
<div key={i} style={{display:"flex",alignItems:"center",gap:12,padding:"9px 13px",background:"#F8FAFC",borderRadius:9,border:"1px solid #E2E8F0"}}>
|
| 717 |
+
<span style={{fontFamily:"monospace",fontWeight:700,color:pc.color,fontSize:13,flexShrink:0}}>{r.code}</span>
|
| 718 |
+
<span style={{fontSize:13.5,color:"#374151",flex:1}}>{r.intitule}</span>
|
| 719 |
+
{r.domaine&&<span style={{fontSize:11,color:"#9CA3AF",padding:"2px 7px",background:"#FFF",borderRadius:5,border:"1px solid #E2E8F0",flexShrink:0}}>{r.domaine}</span>}
|
| 720 |
+
<BdgType type={r.type||"proche"}/>
|
| 721 |
+
<CopyBtn text={r.code}/>
|
| 722 |
+
</div>
|
| 723 |
+
))}
|
| 724 |
+
</div>
|
| 725 |
+
</div>
|
| 726 |
+
)}
|
| 727 |
+
|
| 728 |
+
{/* NAF + Synthèse */}
|
| 729 |
+
<div style={{display:"grid",gridTemplateColumns:"1fr 1fr",gap:12,marginBottom:12}}>
|
| 730 |
+
<div style={{background:"#FFF",borderRadius:14,padding:"16px 20px",border:"1.5px solid #E2E8F0",boxShadow:"0 1px 2px rgba(0,0,0,.05)"}}>
|
| 731 |
+
<div style={{fontSize:10.5,fontWeight:800,letterSpacing:"0.09em",textTransform:"uppercase",color:"#9CA3AF",marginBottom:12}}>Code NAF / APE</div>
|
| 732 |
+
<div style={{display:"flex",alignItems:"center",justifyContent:"space-between",gap:10}}>
|
| 733 |
+
<div><span style={{fontFamily:"monospace",fontSize:20,fontWeight:700,color:"#7C3AED"}}>{results.naf.code}</span><div style={{fontSize:13,color:"#374151",marginTop:4}}>{results.naf.intitule}</div>{results.naf.secteur&&<div style={{fontSize:11,color:"#9CA3AF",marginTop:2}}>{results.naf.secteur}</div>}</div>
|
| 734 |
+
<CopyBtn text={results.naf.code}/>
|
| 735 |
+
</div>
|
| 736 |
+
</div>
|
| 737 |
+
<div style={{background:"#F8FAFC",borderRadius:14,padding:"16px 20px",border:"1.5px solid #E2E8F0"}}>
|
| 738 |
+
<div style={{fontSize:10.5,fontWeight:800,letterSpacing:"0.09em",textTransform:"uppercase",color:"#9CA3AF",marginBottom:10}}>Synthèse</div>
|
| 739 |
+
<p style={{fontSize:13,color:"#374151",lineHeight:1.75,margin:0}}>{results.synthese}</p>
|
| 740 |
+
</div>
|
| 741 |
+
</div>
|
| 742 |
+
|
| 743 |
+
{/* Mots-clés */}
|
| 744 |
+
{results.mots_cles?.length>0&&(
|
| 745 |
+
<div style={{background:"#FFF",borderRadius:14,padding:"16px 20px",border:"1.5px solid #E2E8F0",boxShadow:"0 1px 2px rgba(0,0,0,.05)",marginBottom:12}}>
|
| 746 |
+
<div style={{fontSize:10.5,fontWeight:800,letterSpacing:"0.09em",textTransform:"uppercase",color:"#9CA3AF",marginBottom:12}}>Mots-clés détectés</div>
|
| 747 |
+
<div style={{display:"flex",flexWrap:"wrap",gap:8}}>
|
| 748 |
+
{results.mots_cles.map((k,i)=><span key={i} style={{padding:"4px 12px",borderRadius:20,fontSize:12,fontWeight:600,background:pc.colorSoft,color:pc.color,border:`1px solid ${pc.colorMid}`}}>{k}</span>)}
|
| 749 |
+
</div>
|
| 750 |
+
</div>
|
| 751 |
+
)}
|
| 752 |
+
|
| 753 |
+
{/* Compétences enrichies */}
|
| 754 |
+
<CompetencesBlock codes={codesForCompetences} persona={persona}/>
|
| 755 |
+
</div>
|
| 756 |
+
)}
|
| 757 |
+
</div>
|
| 758 |
+
);
|
| 759 |
+
}
|
| 760 |
+
|
| 761 |
+
/* ══════════ DOSSIER MODULE ══════════ */
|
| 762 |
+
function DossierModule() {
|
| 763 |
+
const[dtab,setDtab]=useState(0);
|
| 764 |
+
const DNAV=["👤 Personas","🗺 Parcours UX","🏗 Architecture","📈 KPIs CEO"];
|
| 765 |
+
|
| 766 |
+
return (
|
| 767 |
+
<div style={{background:"#FFFFFF",borderRadius:16,overflow:"hidden",border:"1px solid #E2E8F0",boxShadow:"0 1px 3px rgba(0,0,0,.07),0 4px 12px rgba(0,0,0,.05)"}}>
|
| 768 |
+
{/* Dossier header — accent band */}
|
| 769 |
+
<div style={{background:"linear-gradient(135deg,#0F172A 0%,#1E3A5F 100%)",padding:"20px 28px 0",borderBottom:"1px solid #E2E8F0"}}>
|
| 770 |
+
<div style={{display:"flex",alignItems:"baseline",gap:12,marginBottom:4}}>
|
| 771 |
+
<h2 style={{margin:0,fontFamily:"Syne,sans-serif",fontSize:18,color:"#FFFFFF",letterSpacing:"0.06em"}}>ROME·NAF MATCHER</h2>
|
| 772 |
+
<span style={{color:"#94A3B8",fontSize:11}}>— Dossier de conception UX · v2.0</span>
|
| 773 |
+
</div>
|
| 774 |
+
<p style={{margin:"0 0 16px",color:"#94A3B8",fontSize:12}}>Personas · Parcours UX · Architecture · KPIs CEO</p>
|
| 775 |
+
<div style={{display:"flex",gap:0}}>
|
| 776 |
+
{DNAV.map((n,i)=>(
|
| 777 |
+
<button key={i} onClick={()=>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"}}>
|
| 778 |
+
{n}
|
| 779 |
+
</button>
|
| 780 |
+
))}
|
| 781 |
+
</div>
|
| 782 |
+
</div>
|
| 783 |
+
|
| 784 |
+
<div style={{padding:"28px",background:"#F8FAFC",fontFamily:"DM Sans,sans-serif",color:"#0F172A"}}>
|
| 785 |
+
|
| 786 |
+
{/* ── PERSONAS ── */}
|
| 787 |
+
{dtab===0&&(
|
| 788 |
+
<div>
|
| 789 |
+
<div style={{display:"grid",gridTemplateColumns:"1fr 1fr",gap:18,marginBottom:18}}>
|
| 790 |
+
{Object.values(PERSONAS).map(p=>(
|
| 791 |
+
<DCard key={p.id}>
|
| 792 |
+
<div style={{display:"flex",alignItems:"center",gap:12,marginBottom:16}}>
|
| 793 |
+
<div style={{width:50,height:50,borderRadius:"50%",background:`linear-gradient(135deg,${p.color}66,${p.color})`,display:"flex",alignItems:"center",justifyContent:"center",fontSize:22}}>{p.icon}</div>
|
| 794 |
+
<div>
|
| 795 |
+
<div style={{fontFamily:"Syne,sans-serif",fontSize:16,fontWeight:700,color:p.color}}>{p.name}, {p.age} ans</div>
|
| 796 |
+
<div style={{color:"#8B9BB4",fontSize:11}}>{p.role}</div>
|
| 797 |
+
</div>
|
| 798 |
+
</div>
|
| 799 |
+
<DSec title="Contexte & environnement">
|
| 800 |
+
<p style={{color:"#374151",fontSize:12,lineHeight:1.7,margin:0}}>{p.context}</p>
|
| 801 |
+
</DSec>
|
| 802 |
+
<DSec title="Objectif principal">
|
| 803 |
+
<p style={{color:"#374151",fontSize:12,lineHeight:1.7,margin:0}}>{p.goal}</p>
|
| 804 |
+
</DSec>
|
| 805 |
+
<DSec title="Compétences mobilisées">
|
| 806 |
+
<div style={{display:"flex",flexWrap:"wrap",gap:5}}>{p.skills.map(s=><DTag key={s} label={s} color={p.color}/>)}</div>
|
| 807 |
+
</DSec>
|
| 808 |
+
<DSec title="Frustrations clés">
|
| 809 |
+
{p.pains.map((f,i)=><div key={i} style={{display:"flex",gap:7,marginBottom:5,alignItems:"flex-start"}}><span style={{color:"#E11D48",marginTop:2,flexShrink:0}}>✕</span><span style={{color:"#374151",fontSize:12}}>{f}</span></div>)}
|
| 810 |
+
</DSec>
|
| 811 |
+
<div style={{background:p.color+"15",border:`1px solid ${p.color}30`,borderRadius:8,padding:10}}>
|
| 812 |
+
<div style={{color:"#8B9BB4",fontSize:10,textTransform:"uppercase",letterSpacing:"0.07em",marginBottom:4}}>Critère de succès</div>
|
| 813 |
+
<em style={{color:p.color,fontSize:12}}>"{p.success}"</em>
|
| 814 |
+
</div>
|
| 815 |
+
</DCard>
|
| 816 |
+
))}
|
| 817 |
+
</div>
|
| 818 |
+
{/* Persona secondaire Mehdi */}
|
| 819 |
+
<DCard>
|
| 820 |
+
<div style={{display:"flex",alignItems:"center",gap:12,marginBottom:14}}>
|
| 821 |
+
<div style={{width:40,height:40,borderRadius:"50%",background:"linear-gradient(135deg,#7B2D8B,#C77DFF)",display:"flex",alignItems:"center",justifyContent:"center",fontSize:18}}>🧍</div>
|
| 822 |
+
<div>
|
| 823 |
+
<div style={{fontFamily:"Syne,sans-serif",fontSize:14,fontWeight:700,color:"#C77DFF"}}>Mehdi, 29 ans <span style={{fontSize:11,color:"#8B9BB4",fontWeight:400}}>— Demandeur d'emploi (persona secondaire)</span></div>
|
| 824 |
+
<div style={{color:"#8B9BB4",fontSize:11}}>Cuisine, laptop, 3 onglets Indeed. Ne maîtrise pas le vocabulaire ROME officiel.</div>
|
| 825 |
+
</div>
|
| 826 |
+
</div>
|
| 827 |
+
<div style={{display:"flex",gap:16,flexWrap:"wrap"}}>
|
| 828 |
+
<div style={{flex:1,minWidth:180}}><div style={{color:"#9CA3AF",fontSize:10,textTransform:"uppercase",letterSpacing:"0.07em",marginBottom:6}}>Frustration</div><p style={{color:"#374151",fontSize:12,margin:0}}>Il dit "manutentionnaire" ou "cariste" — l'outil traduit en ROME sans jargon imposé.</p></div>
|
| 829 |
+
<div style={{flex:1,minWidth:180}}><div style={{color:"#9CA3AF",fontSize:10,textTransform:"uppercase",letterSpacing:"0.07em",marginBottom:6}}>Compétences</div><div style={{display:"flex",flexWrap:"wrap",gap:5}}>{["Langage naturel","Saisie vocale","Autonomie numérique"].map(c=><DTag key={c} label={c} color="#7C3AED"/>)}</div></div>
|
| 830 |
+
<div style={{flex:1,minWidth:180}}><div style={{color:"#9CA3AF",fontSize:10,textTransform:"uppercase",letterSpacing:"0.07em",marginBottom:6}}>Succès</div><p style={{color:"#374151",fontSize:12,margin:0}}>Screenshot du résultat, rappelle Sophie avec le code en main.</p></div>
|
| 831 |
+
</div>
|
| 832 |
+
</DCard>
|
| 833 |
+
</div>
|
| 834 |
+
)}
|
| 835 |
+
|
| 836 |
+
{/* ── PARCOURS UX ── */}
|
| 837 |
+
{dtab===1&&(
|
| 838 |
+
<div>
|
| 839 |
+
<div style={{display:"grid",gridTemplateColumns:"1fr 1fr",gap:16,marginBottom:18}}>
|
| 840 |
+
<DCard><div style={{color:"#1D4ED8",fontFamily:"Syne,sans-serif",fontSize:12,fontWeight:700,marginBottom:8}}>LOI DE MILLER · 7±2</div><p style={{color:"#374151",fontSize:12,margin:0,lineHeight:1.7}}>Chaque écran affiche <strong style={{color:"#0F172A"}}>max 5 résultats ROME</strong> visibles sans scroll. Codes NAF : 3 maximum en premier plan. Pagination pour le reste.</p></DCard>
|
| 841 |
+
<DCard><div style={{color:"#1D4ED8",fontFamily:"Syne,sans-serif",fontSize:12,fontWeight:700,marginBottom:8}}>LOI DE HICK · Réduire les choix</div><p style={{color:"#374151",fontSize:12,margin:0,lineHeight:1.7}}>2 modules distincts dès l'accueil. Zéro configuration. Le résultat principal est mis en avant — les alternatifs sont secondaires.</p></DCard>
|
| 842 |
+
</div>
|
| 843 |
+
<DCard style={{marginBottom:18}}>
|
| 844 |
+
<div style={{color:"#1D4ED8",fontFamily:"Syne,sans-serif",fontSize:12,fontWeight:700,marginBottom:14}}>UX HONEYCOMB (Morville) — Application au ROME·NAF Matcher</div>
|
| 845 |
+
<div style={{display:"grid",gridTemplateColumns:"repeat(4,1fr)",gap:8}}>
|
| 846 |
+
{[{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=>(
|
| 847 |
+
<div key={h.dim} style={{background:h.c+"10",border:`1px solid ${h.c}28`,borderRadius:8,padding:10}}>
|
| 848 |
+
<div style={{color:h.c,fontFamily:"Syne,sans-serif",fontSize:11,fontWeight:700}}>{h.dim}</div>
|
| 849 |
+
<div style={{color:"#6B7280",fontSize:10,marginTop:4,lineHeight:1.5}}>{h.desc}</div>
|
| 850 |
+
</div>
|
| 851 |
+
))}
|
| 852 |
+
</div>
|
| 853 |
+
</DCard>
|
| 854 |
+
<div style={{display:"grid",gridTemplateColumns:"1fr 1fr",gap:16}}>
|
| 855 |
+
<DCard>
|
| 856 |
+
<div style={{color:"#52B788",fontFamily:"Syne,sans-serif",fontSize:14,fontWeight:700,marginBottom:14}}>👩💼 Parcours Sophie — Mots-clés → ROME + NAF</div>
|
| 857 |
+
<DStep n="1" label="Accueil — Choix du module" detail="Sophie voit 2 boutons. Elle clique sur 'Mots-clés → ROME'. 2 sec." tags={[{l:"Hick : 2 choix max",c:"#E9C46A"},{l:"< 2 sec",c:"#52B788"}]}/>
|
| 858 |
+
<DStep n="2" label="Saisie orale ou texte" detail="Microphone actif. Elle dit 'agent logistique polyvalent BTP'. Transcription temps réel." tags={[{l:"Web Speech API",c:"#2E86C1"},{l:"Feedback pulse",c:"#C77DFF"}]}/>
|
| 859 |
+
<DStep n="3" label="Validation & appel API" detail="Skeleton loader < 1.5s. Indicateur de progression visible." tags={[{l:"API /rome/search",c:"#E9C46A"},{l:"LLM Claude",c:"#F4A261"}]}/>
|
| 860 |
+
<DStep n="4" label="Résultats ROME principal" detail="1 grande carte : code + intitulé + badge 'Exact'. 3 ROME proches. 1 NAF suggéré." tags={[{l:"Miller : 5 résultats max",c:"#E9C46A"},{l:"Contraste RGAA AA",c:"#52B788"}]}/>
|
| 861 |
+
<DStep n="5" label="Copier → PPAE" detail="Bouton 'Copier le code' → clipboard. Toast 2 sec. Zéro ressaisie." tags={[{l:"1 action = 1 clic",c:"#52B788"},{l:"Toast non bloquant",c:"#2E86C1"}]}/>
|
| 862 |
+
<DStep n="6" label="Compétences associées" detail="Section dépliable : compétences ROME liées (ex: 'Conduite chariot', 'Gestion stocks')." tags={[{l:"Progressive disclosure",c:"#C77DFF"}]}/>
|
| 863 |
+
</DCard>
|
| 864 |
+
<DCard>
|
| 865 |
+
<div style={{color:"#2E86C1",fontFamily:"Syne,sans-serif",fontSize:14,fontWeight:700,marginBottom:14}}>🎯 Parcours Karim — Annonce → ROME + NAF + Compétences</div>
|
| 866 |
+
<DStep n="1" label="Choix module 2" detail="Karim clique sur 'Annonce → ROME+NAF'. 3 tabs : Texte / Fichier / URL." tags={[{l:"3 modes d'input",c:"#2E86C1"}]}/>
|
| 867 |
+
<DStep n="2" label="Coller l'annonce brute" detail="Textarea large. Il paste 400 mots avec fautes et acronymes. Aucun reformatage requis." tags={[{l:"Tolérance au bruit",c:"#E9C46A"}]}/>
|
| 868 |
+
<DStep n="3" label="Extraction automatique" detail="Appel LLM. Résultat en < 2s. Skeleton pendant l'attente." tags={[{l:"API /rome/extract",c:"#F4A261"},{l:"Timeout 8s max",c:"#E63946"}]}/>
|
| 869 |
+
<DStep n="4" label="Résultats structurés" detail="Code ROME principal + 2 alternatifs + NAF + Synthèse + 5 mots-clés extraits." tags={[{l:"Miller : max 5 KW",c:"#E9C46A"},{l:"Hiérarchie claire",c:"#2E86C1"}]}/>
|
| 870 |
+
<DStep n="5" label="Compétences extraites" detail="Section générée : compétences métier, transversales, outils, savoir-être, certifications." tags={[{l:"Compétences clés",c:"#52B788"},{l:"Transverses",c:"#C77DFF"}]}/>
|
| 871 |
+
<DStep n="6" label="Export & action" detail="'Copier ROME', 'Copier NAF', 'Exporter CSV'. Karim copie ROME → LinkedIn Recruiter." tags={[{l:"Export 1 clic",c:"#52B788"},{l:"< 12 sec total",c:"#E9C46A"}]}/>
|
| 872 |
+
</DCard>
|
| 873 |
+
</div>
|
| 874 |
+
</div>
|
| 875 |
+
)}
|
| 876 |
+
|
| 877 |
+
{/* ── ARCHITECTURE ── */}
|
| 878 |
+
{dtab===2&&(
|
| 879 |
+
<div>
|
| 880 |
+
<div style={{display:"grid",gridTemplateColumns:"1fr 1fr",gap:18,marginBottom:18}}>
|
| 881 |
+
<DCard>
|
| 882 |
+
<div style={{color:"#1D4ED8",fontFamily:"Syne,sans-serif",fontSize:12,fontWeight:700,marginBottom:14}}>🔌 API ENDPOINTS</div>
|
| 883 |
+
{[
|
| 884 |
+
{m:"GET",ep:"/rome/{code_name}",desc:"Recherche ROME par nom métier (mots-clés)"},
|
| 885 |
+
{m:"GET",ep:"/rome/select/{code}_{id}",desc:"Récupère un code ROME précis par ID"},
|
| 886 |
+
{m:"POST",ep:"/rome/extract",desc:"Annonce brute → ROME + NAF extraits par LLM"},
|
| 887 |
+
{m:"GET",ep:"/naf/{code}",desc:"Détails d'un code NAF"},
|
| 888 |
+
{m:"POST",ep:"/rome/competences",desc:"Compétences associées à un code ROME"},
|
| 889 |
+
{m:"GET",ep:"/rome/nearby/{code}",desc:"Codes ROME proches (voisinage sémantique)"},
|
| 890 |
+
{m:"GET",ep:"/analytics/kpi",desc:"Dashboard KPI infrastructure et métier"},
|
| 891 |
+
{m:"POST",ep:"/rome/match",desc:"Score de match compétences ↔ annonce"},
|
| 892 |
+
].map(api=>(
|
| 893 |
+
<div key={api.ep} style={{display:"flex",gap:9,marginBottom:10,alignItems:"flex-start"}}>
|
| 894 |
+
<span style={{background:api.m==="GET"?"#DCFCE7":"#DBEAFE",color:api.m==="GET"?"#15803D":"#1D4ED8",padding:"2px 7px",borderRadius:4,fontSize:10,fontWeight:700,fontFamily:"monospace",minWidth:38,textAlign:"center",flexShrink:0}}>{api.m}</span>
|
| 895 |
+
<div><code style={{color:"#0F172A",fontSize:11}}>{api.ep}</code><div style={{color:"#6B7280",fontSize:10,marginTop:2}}>{api.desc}</div></div>
|
| 896 |
+
</div>
|
| 897 |
+
))}
|
| 898 |
+
</DCard>
|
| 899 |
+
<div style={{display:"flex",flexDirection:"column",gap:14}}>
|
| 900 |
+
<DCard>
|
| 901 |
+
<div style={{color:"#1D4ED8",fontFamily:"Syne,sans-serif",fontSize:12,fontWeight:700,marginBottom:12}}>🤖 STACK LLM</div>
|
| 902 |
+
{[
|
| 903 |
+
{label:"Modèle principal",val:"Claude Sonnet (Anthropic)",c:"#EA580C"},
|
| 904 |
+
{label:"Fallback",val:"Mistral-7B-Instruct (local)",c:"#7C3AED"},
|
| 905 |
+
{label:"Hébergement",val:"HuggingFace Spaces (prod)",c:"#2563EB"},
|
| 906 |
+
{label:"Température",val:"0.1 (précision > créativité)",c:"#D97706"},
|
| 907 |
+
{label:"Max tokens",val:"1400 tokens / requête",c:"#059669"},
|
| 908 |
+
{label:"Prompt strategy",val:"System prompt strict JSON + few-shot",c:"#059669"},
|
| 909 |
+
].map(s=>(
|
| 910 |
+
<div key={s.label} style={{display:"flex",justifyContent:"space-between",marginBottom:8,alignItems:"center"}}>
|
| 911 |
+
<span style={{color:"#6B7280",fontSize:11}}>{s.label}</span>
|
| 912 |
+
<span style={{color:s.c,fontSize:11,fontWeight:600,textAlign:"right",maxWidth:"55%"}}>{s.val}</span>
|
| 913 |
+
</div>
|
| 914 |
+
))}
|
| 915 |
+
</DCard>
|
| 916 |
+
<DCard>
|
| 917 |
+
<div style={{color:"#1D4ED8",fontFamily:"Syne,sans-serif",fontSize:12,fontWeight:700,marginBottom:12}}>🐳 DOCKER COMPOSE</div>
|
| 918 |
+
<div style={{fontFamily:"monospace",fontSize:10,color:"#374151",lineHeight:1.9,background:"#F1F5F9",padding:12,borderRadius:6,border:"1px solid #E2E8F0"}}>
|
| 919 |
+
<span style={{color:"#059669"}}>services:</span><br/>
|
| 920 |
+
<span style={{color:"#2563EB"}}>frontend:</span> react:18 · port 3000<br/>
|
| 921 |
+
<span style={{color:"#2563EB"}}>api:</span> fastapi · port 8000<br/>
|
| 922 |
+
<span style={{color:"#2563EB"}}>llm:</span> claude/mistral · port 11434<br/>
|
| 923 |
+
<span style={{color:"#2563EB"}}>db:</span> postgres:15 · port 5432<br/>
|
| 924 |
+
<span style={{color:"#2563EB"}}>cache:</span> redis:7 · port 6379<br/>
|
| 925 |
+
<span style={{color:"#2563EB"}}>nginx:</span> reverse-proxy · port 80
|
| 926 |
+
</div>
|
| 927 |
+
</DCard>
|
| 928 |
+
</div>
|
| 929 |
+
</div>
|
| 930 |
+
</div>
|
| 931 |
+
)}
|
| 932 |
+
|
| 933 |
+
{/* ── KPIs CEO ── */}
|
| 934 |
+
{dtab===3&&(
|
| 935 |
+
<div>
|
| 936 |
+
|
| 937 |
+
{/* ── KPI Infrastructure ── */}
|
| 938 |
+
<DSec title="📡 KPI Infrastructure">
|
| 939 |
+
<div style={{display:"flex",gap:10,flexWrap:"wrap",marginBottom:14}}>
|
| 940 |
+
<KPIBox label="Latence moy. search" value="1.3" unit="s" delta={-8} color="#059669"/>
|
| 941 |
+
<KPIBox label="Latence moy. match" value="2.8" unit="s" delta={-5} color="#2563EB"/>
|
| 942 |
+
<KPIBox label="Uptime" value="99.4" unit="%" delta={0.2} color="#0891B2"/>
|
| 943 |
+
<KPIBox label="Cache hit rate" value="67" unit="%" delta={8} color="#D97706"/>
|
| 944 |
+
<KPIBox label="Req. match/jour" value="211" unit="" delta={22} color="#7C3AED"/>
|
| 945 |
+
</div>
|
| 946 |
+
<div style={{display:"grid",gridTemplateColumns:"1fr 1fr",gap:12}}>
|
| 947 |
+
<DCard>
|
| 948 |
+
<div style={{color:"#6B7280",fontSize:10,marginBottom:10,fontFamily:"Syne,sans-serif",letterSpacing:"0.08em",textTransform:"uppercase"}}>LATENCE PAR ENDPOINT (secondes)</div>
|
| 949 |
+
<ResponsiveContainer width="100%" height={160}>
|
| 950 |
+
<AreaChart data={kpiLatency}><CartesianGrid strokeDasharray="3 3" stroke="#E2E8F0"/><XAxis dataKey="t" tick={{fill:"#9CA3AF",fontSize:10}}/><YAxis tick={{fill:"#9CA3AF",fontSize:10}}/><Tooltip {...TT}/><Legend wrapperStyle={{fontSize:11}}/><Area type="monotone" dataKey="search" stroke="#059669" fill="#05996912" name="Search (s)" strokeWidth={2}/><Area type="monotone" dataKey="competences" stroke="#D97706" fill="#D9770612" name="Compétences (s)" strokeWidth={2}/><Area type="monotone" dataKey="match" stroke="#7C3AED" fill="#7C3AED12" name="Match (s)" strokeWidth={2}/></AreaChart>
|
| 951 |
+
</ResponsiveContainer>
|
| 952 |
+
</DCard>
|
| 953 |
+
<DCard>
|
| 954 |
+
<div style={{color:"#6B7280",fontSize:10,marginBottom:10,fontFamily:"Syne,sans-serif",letterSpacing:"0.08em",textTransform:"uppercase"}}>REQUÊTES/JOUR PAR ENDPOINT</div>
|
| 955 |
+
<ResponsiveContainer width="100%" height={160}>
|
| 956 |
+
<BarChart data={kpiEndpoints}><CartesianGrid strokeDasharray="3 3" stroke="#E2E8F0"/><XAxis dataKey="t" tick={{fill:"#9CA3AF",fontSize:10}}/><YAxis tick={{fill:"#9CA3AF",fontSize:10}}/><Tooltip {...TT}/><Legend wrapperStyle={{fontSize:11}}/><Bar dataKey="search" fill="#059669" name="/rome/search" radius={[3,3,0,0]} stackId="a"/><Bar dataKey="extract" fill="#2563EB" name="/rome/extract" radius={[0,0,0,0]} stackId="a"/><Bar dataKey="competences" fill="#D97706" name="/rome/competences" radius={[0,0,0,0]} stackId="a"/><Bar dataKey="match" fill="#7C3AED" name="/rome/match" radius={[3,3,0,0]} stackId="a"/></BarChart>
|
| 957 |
+
</ResponsiveContainer>
|
| 958 |
+
</DCard>
|
| 959 |
+
</div>
|
| 960 |
+
</DSec>
|
| 961 |
+
|
| 962 |
+
{/* ── KPI LLM ── */}
|
| 963 |
+
<DSec title="🤖 KPI Infrastructure LLM">
|
| 964 |
+
<div style={{display:"flex",gap:10,flexWrap:"wrap",marginBottom:14}}>
|
| 965 |
+
<KPIBox label="Tokens search moy." value="487" unit="tok" delta={-4} color="#059669"/>
|
| 966 |
+
<KPIBox label="Tokens compét. moy." value="812" unit="tok" delta={-2} color="#D97706"/>
|
| 967 |
+
<KPIBox label="Tokens match moy." value="1 320" unit="tok" delta={-6} color="#7C3AED"/>
|
| 968 |
+
<KPIBox label="Coût/jour estimé" value="0.22" unit="€" delta={12} color="#EA580C"/>
|
| 969 |
+
<KPIBox label="JSON parse OK" value="97.8" unit="%" delta={1.1} color="#2563EB"/>
|
| 970 |
+
</div>
|
| 971 |
+
<DCard>
|
| 972 |
+
<div style={{color:"#6B7280",fontSize:10,marginBottom:10,fontFamily:"Syne,sans-serif",letterSpacing:"0.08em",textTransform:"uppercase"}}>TOKENS PAR TYPE D'APPEL & COÛT ESTIMÉ / HEURE</div>
|
| 973 |
+
<ResponsiveContainer width="100%" height={170}>
|
| 974 |
+
<AreaChart data={kpiLLM}><CartesianGrid strokeDasharray="3 3" stroke="#E2E8F0"/><XAxis dataKey="t" tick={{fill:"#9CA3AF",fontSize:10}}/><YAxis yAxisId="left" tick={{fill:"#9CA3AF",fontSize:10}}/><YAxis yAxisId="right" orientation="right" tick={{fill:"#9CA3AF",fontSize:10}}/><Tooltip {...TT}/><Legend wrapperStyle={{fontSize:11}}/><Area yAxisId="left" type="monotone" dataKey="search" stroke="#059669" fill="#05996912" name="Tokens search"/><Area yAxisId="left" type="monotone" dataKey="comp" stroke="#D97706" fill="#D9770612" name="Tokens compét."/><Area yAxisId="left" type="monotone" dataKey="match" stroke="#7C3AED" fill="#7C3AED12" name="Tokens match"/><Area yAxisId="right" type="monotone" dataKey="cost" stroke="#EA580C" fill="#EA580C12" name="Coût €" strokeDasharray="4 2"/></AreaChart>
|
| 975 |
+
</ResponsiveContainer>
|
| 976 |
+
</DCard>
|
| 977 |
+
</DSec>
|
| 978 |
+
|
| 979 |
+
{/* ── KPI Business ── */}
|
| 980 |
+
<DSec title="💼 KPI Business">
|
| 981 |
+
<div style={{display:"flex",gap:10,flexWrap:"wrap",marginBottom:14}}>
|
| 982 |
+
<KPIBox label="Utilisateurs actifs" value="168" unit="/sem" delta={18} color="#D97706"/>
|
| 983 |
+
<KPIBox label="Taux adoption match" value="71" unit="%" delta={11} color="#7C3AED"/>
|
| 984 |
+
<KPIBox label="Taux export CSV" value="74" unit="%" delta={9} color="#059669"/>
|
| 985 |
+
<KPIBox label="Taux retour semaine" value="63" unit="%" delta={7} color="#2563EB"/>
|
| 986 |
+
<KPIBox label="NPS estimé" value="+41" unit="" delta={5} color="#EA580C"/>
|
| 987 |
+
</div>
|
| 988 |
+
<div style={{display:"grid",gridTemplateColumns:"1fr 1fr",gap:12}}>
|
| 989 |
+
<DCard>
|
| 990 |
+
<div style={{color:"#6B7280",fontSize:10,marginBottom:10,fontFamily:"Syne,sans-serif",letterSpacing:"0.08em",textTransform:"uppercase"}}>SESSIONS PAR PERSONA + USAGE MATCH</div>
|
| 991 |
+
<ResponsiveContainer width="100%" height={160}>
|
| 992 |
+
<BarChart data={kpiUsage}><CartesianGrid strokeDasharray="3 3" stroke="#E2E8F0"/><XAxis dataKey="t" tick={{fill:"#9CA3AF",fontSize:10}}/><YAxis tick={{fill:"#9CA3AF",fontSize:10}}/><Tooltip {...TT}/><Legend wrapperStyle={{fontSize:11}}/><Bar dataKey="sophie" fill="#059669" name="Sophie (Conseillère)" radius={[3,3,0,0]}/><Bar dataKey="karim" fill="#2563EB" name="Karim (Chasseur)" radius={[3,3,0,0]}/><Bar dataKey="match" fill="#7C3AED" name="Sessions avec Match" radius={[3,3,0,0]}/></BarChart>
|
| 993 |
+
</ResponsiveContainer>
|
| 994 |
+
</DCard>
|
| 995 |
+
<DCard>
|
| 996 |
+
<div style={{color:"#6B7280",fontSize:10,marginBottom:10,fontFamily:"Syne,sans-serif",letterSpacing:"0.08em",textTransform:"uppercase"}}>TAUX ADOPTION MATCH & RÉTENTION (%)</div>
|
| 997 |
+
<ResponsiveContainer width="100%" height={160}>
|
| 998 |
+
<LineChart data={kpiMatchAdoption}><CartesianGrid strokeDasharray="3 3" stroke="#E2E8F0"/><XAxis dataKey="t" tick={{fill:"#9CA3AF",fontSize:10}}/><YAxis domain={[0,100]} tick={{fill:"#9CA3AF",fontSize:10}}/><Tooltip {...TT}/><Legend wrapperStyle={{fontSize:11}}/><Line type="monotone" dataKey="taux" stroke="#7C3AED" strokeWidth={2.5} dot={{r:3,fill:"#7C3AED"}} name="Taux adoption Match %"/><Line type="monotone" dataKey="retention" stroke="#2563EB" strokeWidth={2.5} dot={{r:3,fill:"#2563EB"}} name="Taux rétention %"/></LineChart>
|
| 999 |
+
</ResponsiveContainer>
|
| 1000 |
+
</DCard>
|
| 1001 |
+
</div>
|
| 1002 |
+
</DSec>
|
| 1003 |
+
|
| 1004 |
+
{/* ── KPI Match ── */}
|
| 1005 |
+
<DSec title="⚡ KPI Matcher d'annonces">
|
| 1006 |
+
<div style={{display:"flex",gap:10,flexWrap:"wrap",marginBottom:14}}>
|
| 1007 |
+
<KPIBox label="Score match moyen" value="83" unit="/100" delta={4} color="#7C3AED"/>
|
| 1008 |
+
<KPIBox label="Matches Excellent+Bon" value="85" unit="%" delta={7} color="#059669"/>
|
| 1009 |
+
<KPIBox label="Compét. matchées moy." value="3.9" unit="/session" delta={12} color="#2563EB"/>
|
| 1010 |
+
<KPIBox label="Compét. manquantes moy." value="1.4" unit="/session" delta={-18} color="#EA580C"/>
|
| 1011 |
+
<KPIBox label="Durée analyse match" value="2.8" unit="s" delta={-9} color="#D97706"/>
|
| 1012 |
+
</div>
|
| 1013 |
+
<div style={{display:"grid",gridTemplateColumns:"1fr 1fr",gap:12}}>
|
| 1014 |
+
<DCard>
|
| 1015 |
+
<div style={{color:"#6B7280",fontSize:10,marginBottom:10,fontFamily:"Syne,sans-serif",letterSpacing:"0.08em",textTransform:"uppercase"}}>DISTRIBUTION DES SCORES MATCH (%)</div>
|
| 1016 |
+
<ResponsiveContainer width="100%" height={160}>
|
| 1017 |
+
<BarChart data={kpiMatchScores}><CartesianGrid strokeDasharray="3 3" stroke="#E2E8F0"/><XAxis dataKey="t" tick={{fill:"#9CA3AF",fontSize:10}}/><YAxis tick={{fill:"#9CA3AF",fontSize:10}}/><Tooltip {...TT}/><Legend wrapperStyle={{fontSize:11}}/><Bar dataKey="excellent" fill="#059669" name="Excellent (≥80)" radius={[3,3,0,0]} stackId="s"/><Bar dataKey="bon" fill="#2563EB" name="Bon (60-79)" stackId="s"/><Bar dataKey="partiel" fill="#D97706" name="Partiel (40-59)" stackId="s"/><Bar dataKey="faible" fill="#E11D48" name="Faible (<40)" radius={[0,0,3,3]} stackId="s"/></BarChart>
|
| 1018 |
+
</ResponsiveContainer>
|
| 1019 |
+
</DCard>
|
| 1020 |
+
<DCard>
|
| 1021 |
+
<div style={{color:"#6B7280",fontSize:10,marginBottom:10,fontFamily:"Syne,sans-serif",letterSpacing:"0.08em",textTransform:"uppercase"}}>ÉVOLUTION SCORE MATCH MOYEN /100</div>
|
| 1022 |
+
<ResponsiveContainer width="100%" height={160}>
|
| 1023 |
+
<AreaChart data={kpiMatchScores}><CartesianGrid strokeDasharray="3 3" stroke="#E2E8F0"/><XAxis dataKey="t" tick={{fill:"#9CA3AF",fontSize:10}}/><YAxis domain={[50,100]} tick={{fill:"#9CA3AF",fontSize:10}}/><Tooltip {...TT}/><Area type="monotone" dataKey="score_moy" stroke="#7C3AED" fill="#7C3AED18" strokeWidth={2.5} name="Score moyen /100" dot={{r:4,fill:"#7C3AED"}}/></AreaChart>
|
| 1024 |
+
</ResponsiveContainer>
|
| 1025 |
+
</DCard>
|
| 1026 |
+
</div>
|
| 1027 |
+
</DSec>
|
| 1028 |
+
|
| 1029 |
+
{/* ── KPI Métier ── */}
|
| 1030 |
+
<DSec title="🎯 KPI Métier">
|
| 1031 |
+
<div style={{display:"flex",gap:10,flexWrap:"wrap",marginBottom:14}}>
|
| 1032 |
+
<KPIBox label="Précision ROME" value="91" unit="%" delta={4} color="#059669"/>
|
| 1033 |
+
<KPIBox label="Satisfaction globale" value="89" unit="%" delta={5} color="#D97706"/>
|
| 1034 |
+
<KPIBox label="Taux copie code" value="85" unit="%" delta={6} color="#2563EB"/>
|
| 1035 |
+
<KPIBox label="Temps moyen résultat" value="1.8" unit="s" delta={-15} color="#EA580C"/>
|
| 1036 |
+
</div>
|
| 1037 |
+
<div style={{display:"grid",gridTemplateColumns:"1fr 1fr",gap:12}}>
|
| 1038 |
+
<DCard>
|
| 1039 |
+
<div style={{color:"#6B7280",fontSize:10,marginBottom:10,fontFamily:"Syne,sans-serif",letterSpacing:"0.08em",textTransform:"uppercase"}}>PRÉCISION ROME / SATISFACTION / EXPORT / SCORE MATCH (%)</div>
|
| 1040 |
+
<ResponsiveContainer width="100%" height={160}>
|
| 1041 |
+
<LineChart data={kpiMetier}><CartesianGrid strokeDasharray="3 3" stroke="#E2E8F0"/><XAxis dataKey="t" tick={{fill:"#9CA3AF",fontSize:10}}/><YAxis domain={[50,100]} tick={{fill:"#9CA3AF",fontSize:10}}/><Tooltip {...TT}/><Legend wrapperStyle={{fontSize:11}}/><Line type="monotone" dataKey="precision" stroke="#059669" strokeWidth={2} dot={false} name="Précision ROME %"/><Line type="monotone" dataKey="satisfaction" stroke="#D97706" strokeWidth={2} dot={false} name="Satisfaction %"/><Line type="monotone" dataKey="export" stroke="#2563EB" strokeWidth={2} dot={false} name="Taux export %"/><Line type="monotone" dataKey="match_score" stroke="#7C3AED" strokeWidth={2} dot={false} strokeDasharray="4 2" name="Score match moy."/></LineChart>
|
| 1042 |
+
</ResponsiveContainer>
|
| 1043 |
+
</DCard>
|
| 1044 |
+
<DCard>
|
| 1045 |
+
<div style={{color:"#6B7280",fontSize:10,marginBottom:10,fontFamily:"Syne,sans-serif",letterSpacing:"0.08em",textTransform:"uppercase"}}>COMPÉTENCES DÉTECTÉES / VALIDÉES / MATCHÉES / SESSION</div>
|
| 1046 |
+
<ResponsiveContainer width="100%" height={160}>
|
| 1047 |
+
<AreaChart data={kpiCompetences}><CartesianGrid strokeDasharray="3 3" stroke="#E2E8F0"/><XAxis dataKey="t" tick={{fill:"#9CA3AF",fontSize:10}}/><YAxis tick={{fill:"#9CA3AF",fontSize:10}}/><Tooltip {...TT}/><Legend wrapperStyle={{fontSize:11}}/><Area type="monotone" dataKey="detected" stroke="#D97706" fill="#D9770610" strokeWidth={2} name="Détectées"/><Area type="monotone" dataKey="validated" stroke="#2563EB" fill="#2563EB10" strokeWidth={2} name="Validées"/><Area type="monotone" dataKey="matched" stroke="#7C3AED" fill="#7C3AED10" strokeWidth={2} name="Matchées"/></AreaChart>
|
| 1048 |
+
</ResponsiveContainer>
|
| 1049 |
+
</DCard>
|
| 1050 |
+
</div>
|
| 1051 |
+
</DSec>
|
| 1052 |
+
|
| 1053 |
+
</div>
|
| 1054 |
+
)}
|
| 1055 |
+
</div>
|
| 1056 |
+
</div>
|
| 1057 |
+
);
|
| 1058 |
+
}
|
| 1059 |
+
|
| 1060 |
+
/* ══════════ APP ROOT ══════════ */
|
| 1061 |
+
export default function App() {
|
| 1062 |
+
const[persona,setPersona]=useState("sophie");
|
| 1063 |
+
const[mainTab,setMainTab]=useState("tool"); // tool | dossier
|
| 1064 |
+
const pc = PERSONAS[persona];
|
| 1065 |
+
|
| 1066 |
+
return (
|
| 1067 |
+
<div style={{minHeight:"100vh",background:"#F4F6F9",fontFamily:"'Outfit','DM Sans',sans-serif",color:"#0F172A"}}>
|
| 1068 |
+
<style>{`
|
| 1069 |
+
@import url('https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700;800;900&family=JetBrains+Mono:wght@600;700&family=Syne:wght@600;700;800&family=DM+Sans:wght@400;500;600&display=swap');
|
| 1070 |
+
*{box-sizing:border-box;margin:0;padding:0}
|
| 1071 |
+
@keyframes shimmer{0%{background-position:200% 0}100%{background-position:-200% 0}}
|
| 1072 |
+
@keyframes spin{to{transform:rotate(360deg)}}
|
| 1073 |
+
@keyframes fadeUp{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:none}}
|
| 1074 |
+
@keyframes fadeIn{from{opacity:0}to{opacity:1}}
|
| 1075 |
+
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}}
|
| 1076 |
+
input,textarea,button,select{font-family:inherit}
|
| 1077 |
+
::-webkit-scrollbar{width:5px;height:5px}
|
| 1078 |
+
::-webkit-scrollbar-thumb{background:#CBD5E1;border-radius:3px}
|
| 1079 |
+
`}</style>
|
| 1080 |
+
|
| 1081 |
+
{/* ── HEADER ── */}
|
| 1082 |
+
<header style={{background:"#FFF",borderBottom:"1.5px solid #E2E8F0",position:"sticky",top:0,zIndex:100,boxShadow:"0 1px 0 rgba(0,0,0,.04)"}}>
|
| 1083 |
+
<div style={{maxWidth:960,margin:"0 auto",padding:"0 24px"}}>
|
| 1084 |
+
<div style={{display:"flex",alignItems:"center",gap:14,paddingTop:14,paddingBottom:12,borderBottom:"1px solid #F8FAFC",flexWrap:"wrap",rowGap:8}}>
|
| 1085 |
+
<div style={{width:40,height:40,borderRadius:11,background:"linear-gradient(135deg,#2563EB,#0891B2)",display:"flex",alignItems:"center",justifyContent:"center",fontSize:18,boxShadow:"0 2px 8px rgba(37,99,235,.3)",flexShrink:0}}>🎯</div>
|
| 1086 |
+
<div style={{flex:1,minWidth:140}}>
|
| 1087 |
+
<div style={{fontWeight:900,fontSize:17,color:"#0F172A",letterSpacing:"-.01em",lineHeight:1.2}}>ROME NAF Pro</div>
|
| 1088 |
+
<div style={{fontSize:11,color:"#9CA3AF"}}>Décodeur IA · Référentiel France Travail · v4.0</div>
|
| 1089 |
+
</div>
|
| 1090 |
+
|
| 1091 |
+
{/* Persona switcher */}
|
| 1092 |
+
<div style={{display:"flex",gap:1,background:"#F1F5F9",borderRadius:10,padding:3,border:"1px solid #E2E8F0"}}>
|
| 1093 |
+
{Object.values(PERSONAS).map(p=>(
|
| 1094 |
+
<button key={p.id} onClick={()=>setPersona(p.id)} style={{display:"flex",alignItems:"center",gap:7,padding:"7px 14px",borderRadius:8,border:"none",background:persona===p.id?"#FFF":"none",color:persona===p.id?p.color:"#9CA3AF",fontWeight:persona===p.id?800:500,fontSize:12.5,cursor:"pointer",transition:"all .2s",boxShadow:persona===p.id?"0 1px 3px rgba(0,0,0,.08)":"none",whiteSpace:"nowrap"}}>
|
| 1095 |
+
<span style={{fontSize:16}}>{p.icon}</span>
|
| 1096 |
+
<span>{p.name}</span>
|
| 1097 |
+
<span style={{fontSize:10,opacity:.7}}>— {p.id==="sophie"?"Conseillère":"Chasseur"}</span>
|
| 1098 |
+
</button>
|
| 1099 |
+
))}
|
| 1100 |
+
</div>
|
| 1101 |
+
|
| 1102 |
+
{/* Main tab switcher */}
|
| 1103 |
+
<div style={{display:"flex",gap:6}}>
|
| 1104 |
+
{[{id:"tool",label:"🔍 Outil",},{id:"dossier",label:"📋 Dossier UX"}].map(t=>(
|
| 1105 |
+
<button key={t.id} onClick={()=>setMainTab(t.id)} style={{padding:"7px 16px",borderRadius:8,border:`1.5px solid ${mainTab===t.id?"#2563EB":"#E2E8F0"}`,background:mainTab===t.id?"#EFF6FF":"#FFF",color:mainTab===t.id?"#2563EB":"#6B7280",fontWeight:mainTab===t.id?700:500,fontSize:12,cursor:"pointer",transition:"all .2s"}}>
|
| 1106 |
+
{t.label}
|
| 1107 |
+
</button>
|
| 1108 |
+
))}
|
| 1109 |
+
</div>
|
| 1110 |
+
</div>
|
| 1111 |
+
{/* Persona indicator */}
|
| 1112 |
+
<div style={{padding:"8px 0",display:"flex",alignItems:"center",gap:10}}>
|
| 1113 |
+
<span style={{fontSize:18}}>{pc.icon}</span>
|
| 1114 |
+
<span style={{fontSize:12.5,fontWeight:600,color:pc.color}}>{pc.name}</span>
|
| 1115 |
+
<span style={{fontSize:11,color:"#9CA3AF"}}>·</span>
|
| 1116 |
+
<span style={{fontSize:11,color:"#9CA3AF"}}>{pc.role}</span>
|
| 1117 |
+
<span style={{fontSize:11,color:"#9CA3AF"}}>·</span>
|
| 1118 |
+
<span style={{fontSize:11,color:pc.color,fontWeight:600}}>{pc.tagline}</span>
|
| 1119 |
+
</div>
|
| 1120 |
+
</div>
|
| 1121 |
+
</header>
|
| 1122 |
+
|
| 1123 |
+
{/* ── MAIN ── */}
|
| 1124 |
+
<main style={{maxWidth:960,margin:"0 auto",padding:"24px 24px 80px"}}>
|
| 1125 |
+
{mainTab==="tool"&&<ToolModule persona={persona}/>}
|
| 1126 |
+
{mainTab==="dossier"&&<DossierModule/>}
|
| 1127 |
+
</main>
|
| 1128 |
+
|
| 1129 |
+
<footer style={{borderTop:"1.5px solid #E2E8F0",background:"#FFF",padding:"14px 24px",textAlign:"center"}}>
|
| 1130 |
+
<span style={{fontSize:11,color:"#CBD5E1"}}>ROME NAF Pro v4 · Claude (Anthropic) · Référentiel France Travail · <strong style={{color:"#9CA3AF"}}>RGAA AA</strong> · Données stockées localement</span>
|
| 1131 |
+
</footer>
|
| 1132 |
+
</div>
|
| 1133 |
+
);
|
| 1134 |
+
}
|
Dockerfile
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ── Étape 1 : Build du frontend React ──
|
| 2 |
+
FROM node:20-slim AS frontend
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
COPY frontend/package*.json ./
|
| 5 |
+
RUN npm install
|
| 6 |
+
COPY frontend/ .
|
| 7 |
+
RUN npm run build
|
| 8 |
+
|
| 9 |
+
# ── Étape 2 : Backend Python + fichiers statiques ──
|
| 10 |
+
FROM python:3.11-slim
|
| 11 |
+
WORKDIR /app
|
| 12 |
+
|
| 13 |
+
COPY backend/requirements.txt .
|
| 14 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 15 |
+
|
| 16 |
+
COPY backend/ .
|
| 17 |
+
COPY --from=frontend /app/dist ./static
|
| 18 |
+
|
| 19 |
+
EXPOSE 7860
|
| 20 |
+
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
|
README.md
CHANGED
|
@@ -1,10 +1,39 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: docker
|
| 7 |
pinned: false
|
|
|
|
| 8 |
---
|
| 9 |
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: ROME NAF Pro v4 Mistral
|
| 3 |
+
emoji: 🎯
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: yellow
|
| 6 |
sdk: docker
|
| 7 |
pinned: false
|
| 8 |
+
license: mit
|
| 9 |
---
|
| 10 |
|
| 11 |
+
# ROME NAF Pro v4 — Mistral AI · Référentiel France Travail
|
| 12 |
+
|
| 13 |
+
IA souveraine européenne · Mistral AI · Données stockées localement
|
| 14 |
+
|
| 15 |
+
## Configuration — Secrets HF
|
| 16 |
+
|
| 17 |
+
| Variable | Valeur |
|
| 18 |
+
|---|---|
|
| 19 |
+
| `MISTRAL_API_KEY` | Votre clé sur console.mistral.ai |
|
| 20 |
+
| `MISTRAL_MODEL` | `mistral-small-latest` (défaut) |
|
| 21 |
+
|
| 22 |
+
### Modèles disponibles
|
| 23 |
+
|
| 24 |
+
| Modèle | Vitesse | Coût | Recommandé pour |
|
| 25 |
+
|---|---|---|---|
|
| 26 |
+
| `mistral-small-latest` | ⚡ Rapide | 💚 Frugal | Production, usage courant |
|
| 27 |
+
| `mistral-medium-latest` | ◑ Moyen | 🟡 Modéré | Meilleure précision |
|
| 28 |
+
| `mistral-large-latest` | 🐢 Lent | 🔴 Élevé | Cas complexes |
|
| 29 |
+
|
| 30 |
+
## Stack
|
| 31 |
+
|
| 32 |
+
- Frontend : React + Vite + Recharts
|
| 33 |
+
- Backend : FastAPI + Python
|
| 34 |
+
- LLM : **Mistral AI** (souveraineté européenne 🇪🇺)
|
| 35 |
+
- Déploiement : Docker sur HF Spaces
|
| 36 |
+
|
| 37 |
+
---
|
| 38 |
+
|
| 39 |
+
*ROME NAF Pro v4 · Mistral AI · Référentiel France Travail · RGAA AA · #IAResponsable*
|
index.html
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html lang="fr">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8" />
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 6 |
+
<title>ROME NAF Pro v4 · Décodeur IA · France Travail</title>
|
| 7 |
+
<meta name="description" content="Identifiez codes ROME et NAF en quelques secondes grâce à l'IA — pour conseillers France Travail et chasseurs de tête." />
|
| 8 |
+
</head>
|
| 9 |
+
<body>
|
| 10 |
+
<div id="root"></div>
|
| 11 |
+
<script type="module" src="/src/main.jsx"></script>
|
| 12 |
+
</body>
|
| 13 |
+
</html>
|
main.jsx
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { StrictMode } from 'react'
|
| 2 |
+
import { createRoot } from 'react-dom/client'
|
| 3 |
+
import App from './App.jsx'
|
| 4 |
+
|
| 5 |
+
createRoot(document.getElementById('root')).render(
|
| 6 |
+
<StrictMode>
|
| 7 |
+
<App />
|
| 8 |
+
</StrictMode>,
|
| 9 |
+
)
|
main.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from fastapi import FastAPI, HTTPException
|
| 3 |
+
from fastapi.staticfiles import StaticFiles
|
| 4 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 5 |
+
from pydantic import BaseModel
|
| 6 |
+
from typing import List, Optional
|
| 7 |
+
from mistralai import Mistral
|
| 8 |
+
|
| 9 |
+
app = FastAPI(title="ROME NAF Pro API — Mistral")
|
| 10 |
+
|
| 11 |
+
app.add_middleware(
|
| 12 |
+
CORSMiddleware,
|
| 13 |
+
allow_origins=["*"],
|
| 14 |
+
allow_methods=["*"],
|
| 15 |
+
allow_headers=["*"],
|
| 16 |
+
)
|
| 17 |
+
|
| 18 |
+
api_key = os.environ.get("MISTRAL_API_KEY", "")
|
| 19 |
+
client = Mistral(api_key=api_key) if api_key else None
|
| 20 |
+
|
| 21 |
+
# Modèle utilisé — changeable via variable d'env HF
|
| 22 |
+
# Options : mistral-small-latest | mistral-medium-latest | mistral-large-latest
|
| 23 |
+
MODEL = os.environ.get("MISTRAL_MODEL", "mistral-small-latest")
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
class Message(BaseModel):
|
| 27 |
+
role: str
|
| 28 |
+
content: str
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
class ClaudeRequest(BaseModel):
|
| 32 |
+
messages: List[Message]
|
| 33 |
+
max_tokens: Optional[int] = 1200
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
@app.post("/api/claude")
|
| 37 |
+
async def call_mistral(body: ClaudeRequest):
|
| 38 |
+
"""
|
| 39 |
+
Endpoint conserve le nom /api/claude pour ne pas modifier le frontend.
|
| 40 |
+
Il appelle Mistral en coulisses.
|
| 41 |
+
"""
|
| 42 |
+
if not client:
|
| 43 |
+
raise HTTPException(
|
| 44 |
+
status_code=500,
|
| 45 |
+
detail="MISTRAL_API_KEY manquante. Configurez-la dans les Secrets du Space HF."
|
| 46 |
+
)
|
| 47 |
+
try:
|
| 48 |
+
response = client.chat.complete(
|
| 49 |
+
model=MODEL,
|
| 50 |
+
max_tokens=body.max_tokens,
|
| 51 |
+
messages=[{"role": m.role, "content": m.content} for m in body.messages]
|
| 52 |
+
)
|
| 53 |
+
text = response.choices[0].message.content
|
| 54 |
+
return {"content": [{"text": text}]}
|
| 55 |
+
except Exception as e:
|
| 56 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
@app.get("/api/health")
|
| 60 |
+
async def health():
|
| 61 |
+
return {
|
| 62 |
+
"status": "ok",
|
| 63 |
+
"provider": "Mistral AI",
|
| 64 |
+
"model": MODEL,
|
| 65 |
+
"api_key_configured": bool(api_key)
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
app.mount("/", StaticFiles(directory="static", html=True), name="static")
|
package.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "rome-naf-pro",
|
| 3 |
+
"private": true,
|
| 4 |
+
"version": "4.0.0",
|
| 5 |
+
"type": "module",
|
| 6 |
+
"scripts": {
|
| 7 |
+
"dev": "vite",
|
| 8 |
+
"build": "vite build",
|
| 9 |
+
"preview": "vite preview"
|
| 10 |
+
},
|
| 11 |
+
"dependencies": {
|
| 12 |
+
"react": "^18.3.1",
|
| 13 |
+
"react-dom": "^18.3.1",
|
| 14 |
+
"recharts": "^2.12.7"
|
| 15 |
+
},
|
| 16 |
+
"devDependencies": {
|
| 17 |
+
"@types/react": "^18.3.3",
|
| 18 |
+
"@types/react-dom": "^18.3.0",
|
| 19 |
+
"@vitejs/plugin-react": "^4.3.1",
|
| 20 |
+
"vite": "^5.3.1"
|
| 21 |
+
}
|
| 22 |
+
}
|
requirements.txt
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi==0.111.0
|
| 2 |
+
uvicorn==0.30.1
|
| 3 |
+
mistralai==1.2.5
|
| 4 |
+
python-multipart==0.0.9
|
vite.config.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { defineConfig } from 'vite'
|
| 2 |
+
import react from '@vitejs/plugin-react'
|
| 3 |
+
|
| 4 |
+
export default defineConfig({
|
| 5 |
+
plugins: [react()],
|
| 6 |
+
server: {
|
| 7 |
+
proxy: {
|
| 8 |
+
'/api': 'http://localhost:7860'
|
| 9 |
+
}
|
| 10 |
+
},
|
| 11 |
+
build: {
|
| 12 |
+
outDir: 'dist',
|
| 13 |
+
sourcemap: false
|
| 14 |
+
}
|
| 15 |
+
})
|