Matis Codjia commited on
Commit
1d8c2e0
·
1 Parent(s): e4d3f10

Scoring app

Browse files
CHANGELOG.md ADDED
@@ -0,0 +1,185 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Changelog - Version Production
2
+
3
+ ## Version 2.0.0 - Production Ready (2025-01-27)
4
+
5
+ ### 🎉 Nouvelles fonctionnalités majeures
6
+
7
+ #### 🔐 Sécurité et Authentification
8
+ - **Authentification par mot de passe** : Accès sécurisé via `APP_PASSWORD`
9
+ - **Système de dispatch multi-annotateurs** : Chaque annotateur ne voit que sa portion
10
+ - **Isolation des données** : Traçabilité complète par annotateur
11
+ - Module : `backend/auth.py`
12
+
13
+ #### 👥 Gestion des Annotateurs
14
+ - **Configuration flexible** : Fichier JSON ou secrets HF
15
+ - **Assignment automatique** : Filtrage par `start_idx` / `end_idx`
16
+ - **Interface de sélection** : Choix visuel de l'identifiant
17
+ - **Validation** : Vérification de la cohérence de la config
18
+ - Module : `backend/annotator_config.py`
19
+
20
+ #### ☁️ Persistance Cloud
21
+ - **Sauvegarde HuggingFace** : Stockage permanent sur dataset privé
22
+ - **Auto-restore** : Chargement automatique au démarrage
23
+ - **Format horodaté** : Fichiers JSON avec timestamp
24
+ - **Bouton explicite** : Sauvegarde manuelle à tout moment
25
+ - Module : `backend/hf_storage.py`
26
+
27
+ ### 🔧 Modifications
28
+
29
+ #### Interface Utilisateur
30
+ - Affichage de l'identité annotateur dans la sidebar
31
+ - Indicateur de portion assignée (start-end)
32
+ - Badge de statut sauvegarde cloud
33
+ - Bouton "☁️ Sauvegarder sur HF" proéminent
34
+ - Nom de fichier export personnalisé par annotateur
35
+
36
+ #### Backend
37
+ - Filtrage automatique du dataset au chargement
38
+ - Stockage du dataset complet + portion filtrée
39
+ - Métadonnées enrichies (annotator_id, filtered, etc.)
40
+ - Support secrets HF + variables d'environnement
41
+
42
+ #### Configuration
43
+ - Nouveau fichier `data/annotators.json`
44
+ - Exemple fourni : `data/annotators.json.example`
45
+ - Secrets HF : `APP_PASSWORD`, `HF_TOKEN`, `HF_DATASET_REPO`
46
+ - Fallback sur config par défaut si non configuré
47
+
48
+ ### 📚 Documentation
49
+
50
+ #### Nouveaux fichiers
51
+ - **QUICKSTART.md** : Guide de démarrage rapide (15 min)
52
+ - **CONFIGURATION.md** : Documentation complète de configuration
53
+ - **CHANGELOG.md** : Ce fichier
54
+
55
+ #### Mises à jour
56
+ - **README.md** : Vue d'ensemble production, architecture, sécurité
57
+ - **requirements.txt** : Ajout de `huggingface_hub>=0.20.0`
58
+ - **.gitignore** : Ignorer secrets et données sensibles
59
+
60
+ ### 🏗️ Architecture
61
+
62
+ #### Nouveaux modules
63
+ ```
64
+ backend/
65
+ ├── auth.py # Authentification
66
+ ├── annotator_config.py # Configuration annotateurs
67
+ └── hf_storage.py # Sauvegarde HuggingFace
68
+
69
+ data/
70
+ └── annotators.json.example # Template configuration
71
+ ```
72
+
73
+ #### Flux d'exécution
74
+ 1. **Authentification** : Vérification mot de passe
75
+ 2. **Sélection** : Choix de l'annotateur
76
+ 3. **Filtrage** : Dataset filtré automatiquement
77
+ 4. **Annotation** : Interface scoring
78
+ 5. **Persistance** : Sauvegarde locale + HF cloud
79
+
80
+ ### 🔒 Sécurité
81
+
82
+ #### Protections ajoutées
83
+ - ✅ Mot de passe obligatoire pour accès
84
+ - ✅ Tokens jamais en clair dans le code
85
+ - ✅ Dataset de stockage privé recommandé
86
+ - ✅ Isolation des annotateurs
87
+ - ✅ Traçabilité complète (qui a annoté quoi)
88
+
89
+ #### Bonnes pratiques
90
+ - Secrets via HF Spaces (pas de commit)
91
+ - Validation des configurations
92
+ - Messages d'erreur explicites
93
+ - Mode développement avec fallbacks
94
+
95
+ ### 📊 Intégration FFGen
96
+
97
+ #### Workflow complet
98
+ 1. **FFGen** : `create_annotation_study.py` → Créer subsets + gold
99
+ 2. **App** : Distribuer et collecter annotations
100
+ 3. **FFGen** : `analyze_agreement.py` → Calculer accord
101
+ 4. **FFGen** : `merge_scores.py` → Dataset final
102
+
103
+ #### Compatibilité
104
+ - Format identique aux scripts FFGen
105
+ - Support des gold items pour IAA
106
+ - Export JSONL directement utilisable
107
+ - Métadonnées compatibles
108
+
109
+ ### 🐛 Corrections
110
+
111
+ - Fix : Sauvegarde perdue après redémarrage → Sauvegarde HF
112
+ - Fix : Tous les annotateurs voient tout → Filtrage par portion
113
+ - Fix : Pas de traçabilité → annotator_id dans toutes les sauvegardes
114
+ - Fix : Accès public non sécurisé → Authentification obligatoire
115
+
116
+ ### ⚠️ Breaking Changes
117
+
118
+ #### Configuration requise
119
+ - **Avant** : Aucune config nécessaire
120
+ - **Après** : Secrets HF obligatoires en production
121
+
122
+ #### Workflow
123
+ - **Avant** : Un seul utilisateur, dataset complet
124
+ - **Après** : Multi-utilisateurs, portions filtrées
125
+
126
+ #### Migration depuis v1.0
127
+ 1. Configurer les secrets HF (voir QUICKSTART.md)
128
+ 2. Créer `data/annotators.json`
129
+ 3. Pousser la nouvelle version
130
+ 4. Informer les annotateurs du nouveau workflow
131
+
132
+ ### 📈 Métriques
133
+
134
+ #### Fichiers modifiés
135
+ - 4 fichiers modifiés
136
+ - 3 nouveaux modules backend
137
+ - 3 nouveaux fichiers documentation
138
+ - 1 fichier exemple configuration
139
+
140
+ #### Lignes de code
141
+ - ~600 lignes ajoutées (backend)
142
+ - ~200 lignes ajoutées (app.py)
143
+ - ~800 lignes documentation
144
+
145
+ ### 🚀 Déploiement
146
+
147
+ #### Prérequis
148
+ 1. Créer dataset HF privé pour stockage
149
+ 2. Créer token HF avec droits write
150
+ 3. Configurer secrets dans HF Space
151
+ 4. Créer configuration annotateurs
152
+
153
+ #### Commandes
154
+ ```bash
155
+ git add .
156
+ git commit -m "Add authentication, dispatch and HF persistence"
157
+ git push origin main
158
+ ```
159
+
160
+ Le Space rebuild automatiquement (3-5 min).
161
+
162
+ ### 📖 Ressources
163
+
164
+ - **Guide rapide** : [QUICKSTART.md](QUICKSTART.md) - 15 minutes
165
+ - **Documentation complète** : [CONFIGURATION.md](CONFIGURATION.md)
166
+ - **Exemple config** : `data/annotators.json.example`
167
+
168
+ ### 🙏 Remerciements
169
+
170
+ Cette version implémente les recommandations de sécurité et persistance pour un déploiement en production sur HuggingFace Spaces avec hébergement gratuit.
171
+
172
+ Inspiré par :
173
+ - Le système de dispatch de FFGen (`create_annotation_study.py`)
174
+ - Les bonnes pratiques HuggingFace Spaces
175
+ - Les besoins réels d'études d'annotation multi-annotateurs
176
+
177
+ ---
178
+
179
+ ## Versions précédentes
180
+
181
+ ### Version 1.0.0 - Version initiale
182
+ - Interface de scoring basique
183
+ - Sauvegarde locale uniquement
184
+ - Un seul utilisateur
185
+ - Pas d'authentification
CONFIGURATION.md ADDED
@@ -0,0 +1,343 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Configuration de l'Application d'Annotation
2
+
3
+ Cette documentation explique comment configurer l'application pour un déploiement en production sur Hugging Face Spaces avec plusieurs annotateurs.
4
+
5
+ ## Table des matières
6
+
7
+ 1. [Vue d'ensemble](#vue-densemble)
8
+ 2. [Étape 1 : Créer le Dataset de stockage](#étape-1--créer-le-dataset-de-stockage)
9
+ 3. [Étape 2 : Configurer les Secrets HF](#étape-2--configurer-les-secrets-hf)
10
+ 4. [Étape 3 : Configurer les Annotateurs](#étape-3--configurer-les-annotateurs)
11
+ 5. [Étape 4 : Pousser sur HF Spaces](#étape-4--pousser-sur-hf-spaces)
12
+ 6. [Utilisation pour les Annotateurs](#utilisation-pour-les-annotateurs)
13
+ 7. [Récupération des Annotations](#récupération-des-annotations)
14
+
15
+ ---
16
+
17
+ ## Vue d'ensemble
18
+
19
+ L'application implémente 3 systèmes de sécurité et persistance :
20
+
21
+ 1. **Authentification** : Mot de passe unique pour accéder à l'app
22
+ 2. **Dispatch** : Chaque annotateur voit uniquement sa portion du dataset
23
+ 3. **Persistance** : Sauvegarde automatique sur un Dataset HuggingFace privé
24
+
25
+ ---
26
+
27
+ ## Étape 1 : Créer le Dataset de stockage
28
+
29
+ 1. Allez sur https://huggingface.co/new-dataset
30
+
31
+ 2. Créez un nouveau dataset **PRIVÉ** :
32
+ - Nom : `ffgen-annotations-storage` (ou autre)
33
+ - Type : Dataset
34
+ - Visibilité : **Private** ⚠️ Très important !
35
+
36
+ 3. Laissez-le vide, l'app créera automatiquement la structure
37
+
38
+ ---
39
+
40
+ ## Étape 2 : Configurer les Secrets HF
41
+
42
+ ### 2.1 Créer un Token HuggingFace
43
+
44
+ 1. Allez dans vos settings HF : https://huggingface.co/settings/tokens
45
+ 2. Créez un nouveau token avec les permissions **write**
46
+ 3. Copiez le token (format : `hf_xxxxxxxxxxxx`)
47
+
48
+ ### 2.2 Ajouter les Secrets dans le Space
49
+
50
+ 1. Allez dans votre Space : `https://huggingface.co/spaces/VOTRE-USERNAME/feedbacks-scoring-app`
51
+ 2. Cliquez sur **Settings**
52
+ 3. Scrollez vers **Variables and secrets**
53
+ 4. Ajoutez les secrets suivants :
54
+
55
+ | Nom | Valeur | Description |
56
+ |-----|--------|-------------|
57
+ | `APP_PASSWORD` | `VotreMotDePasse2025!` | Mot de passe pour accéder à l'app |
58
+ | `HF_TOKEN` | `hf_xxxxxxxxxxxx` | Token avec droits d'écriture |
59
+ | `HF_DATASET_REPO` | `VOTRE-USERNAME/ffgen-annotations-storage` | Nom du dataset de stockage |
60
+
61
+ **Important** :
62
+ - Le token doit avoir les droits d'écriture
63
+ - Le dataset doit être privé pour protéger les annotations
64
+
65
+ ---
66
+
67
+ ## Étape 3 : Configurer les Annotateurs
68
+
69
+ ### Option A : Configuration automatique (recommandé)
70
+
71
+ Créez un fichier `annotators.json` dans le dossier `data/` :
72
+
73
+ ```json
74
+ {
75
+ "annotator_1": {
76
+ "name": "Alice Dupont",
77
+ "start_idx": 0,
78
+ "end_idx": 100,
79
+ "description": "Première portion du dataset"
80
+ },
81
+ "annotator_2": {
82
+ "name": "Bob Martin",
83
+ "start_idx": 100,
84
+ "end_idx": 200,
85
+ "description": "Deuxième portion"
86
+ },
87
+ "annotator_3": {
88
+ "name": "Charlie Durand",
89
+ "start_idx": 200,
90
+ "end_idx": 300,
91
+ "description": "Troisième portion"
92
+ }
93
+ }
94
+ ```
95
+
96
+ ### Option B : Configuration via Secret HF
97
+
98
+ Ajoutez un secret `ANNOTATOR_CONFIG` avec la configuration JSON en une ligne :
99
+
100
+ ```json
101
+ {"annotator_1":{"name":"Alice","start_idx":0,"end_idx":100},"annotator_2":{"name":"Bob","start_idx":100,"end_idx":200}}
102
+ ```
103
+
104
+ ### Utilisation du script de génération FFGen
105
+
106
+ Si vous avez déjà utilisé le script `create_annotation_study.py` de FFGen :
107
+
108
+ ```bash
109
+ # Depuis FFGen/3_data_processing/
110
+ python create_annotation_study.py \
111
+ matis35/code-feedback-infonce \
112
+ --num-samples 300 \
113
+ --num-subsets 3 \
114
+ --output-dir annotation_study
115
+ ```
116
+
117
+ Cela crée 3 subsets de ~100 items chacun + gold standard.
118
+
119
+ Pour l'app, configurez les annotateurs correspondants :
120
+ - annotator_1 : subset_01 (indices 0-100)
121
+ - annotator_2 : subset_02 (indices 100-200)
122
+ - annotator_3 : subset_03 (indices 200-300)
123
+
124
+ ---
125
+
126
+ ## Étape 4 : Pousser sur HF Spaces
127
+
128
+ 1. Commitez tous les changements :
129
+ ```bash
130
+ git add .
131
+ git commit -m "Add authentication, dispatch and HF persistence"
132
+ ```
133
+
134
+ 2. Poussez vers HF :
135
+ ```bash
136
+ git push origin main
137
+ ```
138
+
139
+ 3. Le Space va rebuilder automatiquement (prend 3-5 minutes)
140
+
141
+ 4. Une fois prêt, vérifiez que l'app démarre correctement
142
+
143
+ ---
144
+
145
+ ## Utilisation pour les Annotateurs
146
+
147
+ ### Workflow complet
148
+
149
+ 1. **Accès à l'application**
150
+ - URL : `https://huggingface.co/spaces/VOTRE-USERNAME/feedbacks-scoring-app`
151
+ - Entrer le mot de passe fourni
152
+
153
+ 2. **Sélection de l'identité**
154
+ - Choisir son identifiant dans la liste
155
+ - Le système affiche automatiquement la portion assignée
156
+
157
+ 3. **Chargement du dataset**
158
+ - Option 1 : Upload d'un fichier JSONL
159
+ - Option 2 : Chargement depuis HF Hub
160
+ - Le dataset est automatiquement filtré selon la portion
161
+
162
+ 4. **Annotation**
163
+ - Scorer les feedbacks (1-5)
164
+ - Ajouter des commentaires optionnels
165
+ - La progression est sauvegardée automatiquement
166
+
167
+ 5. **Sauvegarde**
168
+ - **Importante** : Cliquer sur "☁️ Sauvegarder sur HF" régulièrement
169
+ - Cette sauvegarde est permanente (survit aux redémarrages)
170
+ - La sauvegarde locale est perdue tous les 48h
171
+
172
+ 6. **Export (optionnel)**
173
+ - Télécharger le JSONL en local pour backup
174
+
175
+ ### Instructions à donner aux annotateurs
176
+
177
+ > **Bienvenue sur l'outil d'annotation FFGen !**
178
+ >
179
+ > 1. Mot de passe : `[VOTRE_MOT_DE_PASSE]`
180
+ > 2. Choisissez votre identifiant dans la liste
181
+ > 3. Chargez le dataset (demandez le lien HF ou le fichier)
182
+ > 4. Annotez les feedbacks selon les critères fournis
183
+ > 5. **Important** : Sauvegardez sur HF toutes les 30-60 minutes
184
+ > 6. Vous pouvez fermer et reprendre plus tard
185
+ >
186
+ > Questions ? Contactez [VOTRE_EMAIL]
187
+
188
+ ---
189
+
190
+ ## Récupération des Annotations
191
+
192
+ ### Méthode 1 : Via HuggingFace Hub (recommandé)
193
+
194
+ 1. Allez sur votre dataset de stockage :
195
+ `https://huggingface.co/datasets/VOTRE-USERNAME/ffgen-annotations-storage`
196
+
197
+ 2. Dans le dossier `annotations/`, vous trouverez tous les fichiers :
198
+ - `annotation_annotator_1_20250127_143022.json`
199
+ - `annotation_annotator_2_20250127_151533.json`
200
+ - etc.
201
+
202
+ 3. Téléchargez-les ou utilisez la CLI :
203
+ ```bash
204
+ huggingface-cli download \
205
+ VOTRE-USERNAME/ffgen-annotations-storage \
206
+ --repo-type dataset \
207
+ --local-dir ./annotations
208
+ ```
209
+
210
+ ### Méthode 2 : Via l'API Python
211
+
212
+ ```python
213
+ from huggingface_hub import HfApi, hf_hub_download
214
+ import json
215
+
216
+ api = HfApi(token="hf_xxxxxxxxxxxx")
217
+
218
+ # Lister tous les fichiers
219
+ files = api.list_repo_files(
220
+ repo_id="VOTRE-USERNAME/ffgen-annotations-storage",
221
+ repo_type="dataset"
222
+ )
223
+
224
+ # Télécharger et charger chaque annotation
225
+ for file in files:
226
+ if file.startswith("annotations/"):
227
+ local_path = hf_hub_download(
228
+ repo_id="VOTRE-USERNAME/ffgen-annotations-storage",
229
+ filename=file,
230
+ repo_type="dataset",
231
+ token="hf_xxxxxxxxxxxx"
232
+ )
233
+
234
+ with open(local_path, 'r') as f:
235
+ data = json.load(f)
236
+ print(f"Annotator: {data['annotator_name']}")
237
+ print(f"Scores: {len(data['scores'])}")
238
+ ```
239
+
240
+ ### Méthode 3 : Utiliser le script de merge FFGen
241
+
242
+ Si vous avez des gold items pour calculer l'accord inter-annotateurs :
243
+
244
+ ```bash
245
+ cd FFGen/3_data_processing
246
+
247
+ # Analyser l'accord
248
+ python analyze_agreement.py \
249
+ ../../annotations/annotation_*.json \
250
+ --gold-file annotation_study/matis35_code-feedback-infonce_gold_standard.json
251
+
252
+ # Merger les scores
253
+ python merge_scores.py \
254
+ ../../annotations/annotation_*.json \
255
+ -o final_annotations.jsonl
256
+ ```
257
+
258
+ ---
259
+
260
+ ## Dépannage
261
+
262
+ ### Le Space ne démarre pas
263
+ - Vérifiez que tous les secrets sont bien configurés
264
+ - Regardez les logs du Space (onglet Logs)
265
+ - Vérifiez que le `HF_TOKEN` a les droits d'écriture
266
+
267
+ ### L'authentification ne marche pas
268
+ - Vérifiez que `APP_PASSWORD` est bien dans les secrets
269
+ - Pas d'espaces avant/après le mot de passe
270
+
271
+ ### La sauvegarde HF échoue
272
+ - Vérifiez que `HF_TOKEN` a les droits d'écriture
273
+ - Vérifiez que `HF_DATASET_REPO` existe et est privé
274
+ - Regardez les messages d'erreur dans l'app
275
+
276
+ ### Un annotateur voit le dataset complet
277
+ - Vérifiez la configuration dans `annotators.json`
278
+ - Vérifiez que les indices start_idx/end_idx sont corrects
279
+ - Rechargez le dataset après modification de la config
280
+
281
+ ### Les annotations sont perdues
282
+ - Rappelez aux annotateurs de sauvegarder sur HF
283
+ - La sauvegarde locale est réinitialisée tous les 48h
284
+ - Les sauvegardes HF sont permanentes
285
+
286
+ ---
287
+
288
+ ## Architecture Technique
289
+
290
+ ```
291
+ ┌─────────────────────────────────────────────────────────┐
292
+ │ HF Space (Public) │
293
+ │ ┌────────────────────────────────────────────────────┐ │
294
+ │ │ 1. Authentification (APP_PASSWORD) │ │
295
+ │ │ ↓ │ │
296
+ │ │ 2. Sélection Annotateur │ │
297
+ │ │ ↓ │ │
298
+ │ │ 3. Filtrage Dataset (start_idx, end_idx) │ │
299
+ │ │ ↓ │ │
300
+ │ │ 4. Interface d'Annotation │ │
301
+ │ │ ↓ │ │
302
+ │ │ 5. Sauvegarde Cloud (HF_TOKEN) │ │
303
+ │ └────────────────────────────────────────────────────┘ │
304
+ └────────────────────┬────────────────────────────────────┘
305
+
306
+
307
+ ┌─────────────────────────────────────────────────────────┐
308
+ │ HF Dataset (Private): annotations-storage │
309
+ │ ┌────────────────────────────────────────────────────┐ │
310
+ │ │ annotations/ │ │
311
+ │ │ ├── annotation_annotator_1_timestamp.json │ │
312
+ │ │ ├── annotation_annotator_2_timestamp.json │ │
313
+ │ │ └── annotation_annotator_3_timestamp.json │ │
314
+ │ └────────────────────────────────────────────────────┘ │
315
+ └─────────────────────────────────────────────────────────┘
316
+ ```
317
+
318
+ ---
319
+
320
+ ## Sécurité et Bonnes Pratiques
321
+
322
+ ✅ **À faire** :
323
+ - Utiliser des mots de passe forts (12+ caractères)
324
+ - Garder le dataset de stockage **PRIVÉ**
325
+ - Révoquer les tokens après l'étude
326
+ - Sauvegarder régulièrement sur HF
327
+ - Tester avec un annotateur avant le déploiement complet
328
+
329
+ ❌ **À éviter** :
330
+ - Partager le token HF publiquement
331
+ - Utiliser le même mot de passe que vos autres comptes
332
+ - Rendre le dataset de stockage public
333
+ - Compter uniquement sur la sauvegarde locale
334
+ - Modifier la config en cours d'annotation
335
+
336
+ ---
337
+
338
+ ## Support
339
+
340
+ Pour toute question ou problème :
341
+ - Issues GitHub : [LIEN_REPO]
342
+ - Email : [VOTRE_EMAIL]
343
+ - Documentation FFGen : `FFGen/README.md`
Dockerfile CHANGED
@@ -1,20 +1,34 @@
1
- FROM python:3.13.5-slim
2
 
3
  WORKDIR /app
4
 
 
5
  RUN apt-get update && apt-get install -y \
6
  build-essential \
7
  curl \
8
  git \
9
  && rm -rf /var/lib/apt/lists/*
10
 
 
11
  COPY requirements.txt ./
12
- COPY src/ ./src/
13
 
14
- RUN pip3 install -r requirements.txt
 
 
 
15
 
16
- EXPOSE 8501
 
17
 
18
- HEALTHCHECK CMD curl --fail http://localhost:8501/_stcore/health
 
19
 
20
- ENTRYPOINT ["streamlit", "run", "src/streamlit_app.py", "--server.port=8501", "--server.address=0.0.0.0"]
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
 
3
  WORKDIR /app
4
 
5
+ # Install system dependencies
6
  RUN apt-get update && apt-get install -y \
7
  build-essential \
8
  curl \
9
  git \
10
  && rm -rf /var/lib/apt/lists/*
11
 
12
+ # Copy requirements and install dependencies
13
  COPY requirements.txt ./
14
+ RUN pip3 install --no-cache-dir -r requirements.txt
15
 
16
+ # Copy application files
17
+ COPY app.py ./
18
+ COPY backend/ ./backend/
19
+ COPY frontend/ ./frontend/
20
 
21
+ # Create data directory
22
+ RUN mkdir -p /app/data
23
 
24
+ # Create .streamlit directory and empty secrets file to avoid warnings
25
+ RUN mkdir -p /app/.streamlit && touch /app/.streamlit/secrets.toml
26
 
27
+ # Expose Streamlit port
28
+ EXPOSE 7860
29
+
30
+ # Health check for Hugging Face Spaces
31
+ HEALTHCHECK CMD curl --fail http://localhost:7860/_stcore/health
32
+
33
+ # Run the application
34
+ CMD ["streamlit", "run", "app.py", "--server.port=7860", "--server.address=0.0.0.0", "--server.enableCORS=false", "--server.enableXsrfProtection=false"]
QUICKSTART.md ADDED
@@ -0,0 +1,265 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Guide de Démarrage Rapide
2
+
3
+ Configuration en 5 minutes pour déployer l'application d'annotation sécurisée.
4
+
5
+ ## Prérequis
6
+
7
+ - Un compte HuggingFace
8
+ - Accès à votre Space `matis35/feedbacks-scoring-app`
9
+ - Le dataset à annoter (format JSONL ou sur HF Hub)
10
+
11
+ ## Étape 1 : Créer le Dataset de Stockage (2 min)
12
+
13
+ 1. Allez sur https://huggingface.co/new-dataset
14
+ 2. Nom : `ffgen-annotations-storage`
15
+ 3. Visibilité : **Private** (très important!)
16
+ 4. Cliquez sur "Create dataset"
17
+ 5. Laissez-le vide, ne chargez rien
18
+
19
+ ✅ Vous avez maintenant : `matis35/ffgen-annotations-storage`
20
+
21
+ ## Étape 2 : Créer un Token HF (1 min)
22
+
23
+ 1. Allez sur https://huggingface.co/settings/tokens
24
+ 2. Cliquez "New token"
25
+ 3. Nom : `annotation-app-write`
26
+ 4. Type : **Write** (important!)
27
+ 5. Cliquez "Generate token"
28
+ 6. **Copiez le token** (format `hf_xxxxxxxxxxxx`)
29
+
30
+ ⚠️ Gardez ce token en sécurité, ne le partagez pas!
31
+
32
+ ## Étape 3 : Configurer les Secrets (2 min)
33
+
34
+ 1. Allez sur votre Space : https://huggingface.co/spaces/matis35/feedbacks-scoring-app
35
+ 2. Cliquez sur **Settings** (en haut)
36
+ 3. Scrollez vers "Variables and secrets"
37
+ 4. Ajoutez ces 3 secrets :
38
+
39
+ ### Secret 1 : APP_PASSWORD
40
+ - Nom : `APP_PASSWORD`
41
+ - Valeur : Choisissez un mot de passe fort (ex: `Annotator2025!`)
42
+ - Type : Secret
43
+
44
+ ### Secret 2 : HF_TOKEN
45
+ - Nom : `HF_TOKEN`
46
+ - Valeur : Le token copié à l'étape 2 (`hf_xxxxx`)
47
+ - Type : Secret
48
+
49
+ ### Secret 3 : HF_DATASET_REPO
50
+ - Nom : `HF_DATASET_REPO`
51
+ - Valeur : `matis35/ffgen-annotations-storage`
52
+ - Type : Secret
53
+
54
+ ✅ Les 3 secrets doivent être visibles dans la liste
55
+
56
+ ## Étape 4 : Configurer les Annotateurs (2 min)
57
+
58
+ ### Option A : Configuration basique (3 annotateurs)
59
+
60
+ Copiez le fichier exemple :
61
+ ```bash
62
+ cd feedbacks-scoring-app
63
+ cp data/annotators.json.example data/annotators.json
64
+ ```
65
+
66
+ Éditez `data/annotators.json` selon votre dataset :
67
+ ```json
68
+ {
69
+ "annotator_1": {
70
+ "name": "Alice",
71
+ "start_idx": 0,
72
+ "end_idx": 100
73
+ },
74
+ "annotator_2": {
75
+ "name": "Bob",
76
+ "start_idx": 100,
77
+ "end_idx": 200
78
+ },
79
+ "annotator_3": {
80
+ "name": "Charlie",
81
+ "start_idx": 200,
82
+ "end_idx": 300
83
+ }
84
+ }
85
+ ```
86
+
87
+ ### Option B : Utiliser vos subsets FFGen
88
+
89
+ Si vous avez utilisé `create_annotation_study.py` :
90
+
91
+ ```bash
92
+ # Vous avez créé 10 subsets avec create_annotation_study.py
93
+ # Configurez 10 annotateurs correspondants
94
+
95
+ # Exemple pour 10 annotateurs, 40 items chacun
96
+ python -c "
97
+ import json
98
+
99
+ config = {}
100
+ for i in range(10):
101
+ config[f'annotator_{i+1}'] = {
102
+ 'name': f'Annotateur {i+1}',
103
+ 'start_idx': i * 40,
104
+ 'end_idx': (i + 1) * 40,
105
+ 'description': f'Subset {i+1}/10'
106
+ }
107
+
108
+ with open('data/annotators.json', 'w') as f:
109
+ json.dump(config, f, indent=2)
110
+
111
+ print('✅ Config créée pour 10 annotateurs')
112
+ "
113
+ ```
114
+
115
+ ## Étape 5 : Pousser sur HF (1 min)
116
+
117
+ ```bash
118
+ cd feedbacks-scoring-app
119
+
120
+ # Vérifier les changements
121
+ git status
122
+
123
+ # Commiter
124
+ git add .
125
+ git commit -m "Add secure authentication and HF persistence"
126
+
127
+ # Pousser vers HF Spaces
128
+ git push origin main
129
+ ```
130
+
131
+ Le Space va rebuilder automatiquement (3-5 minutes).
132
+
133
+ ## Étape 6 : Tester (2 min)
134
+
135
+ 1. Attendez que le Space soit "Running" (vert)
136
+ 2. Ouvrez l'app : https://huggingface.co/spaces/matis35/feedbacks-scoring-app
137
+ 3. Testez la connexion :
138
+ - Entrez le mot de passe (`APP_PASSWORD`)
139
+ - Sélectionnez un annotateur
140
+ - Vérifiez que ça fonctionne
141
+
142
+ ## Étape 7 : Distribuer aux Annotateurs
143
+
144
+ Envoyez ce message à vos annotateurs :
145
+
146
+ ```
147
+ Bonjour,
148
+
149
+ Voici les informations pour accéder à l'outil d'annotation :
150
+
151
+ URL : https://huggingface.co/spaces/matis35/feedbacks-scoring-app
152
+ Mot de passe : [VOTRE_APP_PASSWORD]
153
+ Votre identifiant : [annotator_X]
154
+
155
+ Instructions :
156
+ 1. Ouvrez l'URL et entrez le mot de passe
157
+ 2. Sélectionnez votre identifiant dans la liste
158
+ 3. Chargez le dataset (je vous enverrai le lien/fichier)
159
+ 4. Annotez les feedbacks selon les critères ci-dessous
160
+ 5. IMPORTANT : Cliquez sur "☁️ Sauvegarder sur HF" toutes les 30-60 minutes
161
+ 6. Vous pouvez fermer et reprendre plus tard
162
+
163
+ Critères d'annotation :
164
+ - Score 1 : [DÉFINIR]
165
+ - Score 2 : [DÉFINIR]
166
+ - Score 3 : [DÉFINIR]
167
+ - Score 4 : [DÉFINIR]
168
+ - Score 5 : [DÉFINIR]
169
+
170
+ Questions ? Contactez-moi : [VOTRE_EMAIL]
171
+ ```
172
+
173
+ ## Vérification Post-Déploiement
174
+
175
+ ✅ Checklist de vérification :
176
+
177
+ - [ ] Le Space démarre sans erreur
178
+ - [ ] L'authentification fonctionne
179
+ - [ ] La sélection d'annotateur fonctionne
180
+ - [ ] Le chargement de dataset fonctionne
181
+ - [ ] Le filtrage par portion fonctionne (vérifier les nombres)
182
+ - [ ] La sauvegarde HF fonctionne (vérifier dans le dataset)
183
+ - [ ] L'export JSONL fonctionne
184
+ - [ ] Les annotateurs peuvent se connecter
185
+
186
+ ## Commandes Utiles
187
+
188
+ ### Voir les logs du Space
189
+ ```bash
190
+ # Via l'interface web : Settings > Logs
191
+ # Ou regarder en temps réel depuis l'onglet "Logs"
192
+ ```
193
+
194
+ ### Vérifier les annotations sauvegardées
195
+ ```bash
196
+ # Allez sur : https://huggingface.co/datasets/matis35/ffgen-annotations-storage
197
+ # Vous devriez voir un dossier annotations/ avec des fichiers .json
198
+ ```
199
+
200
+ ### Télécharger toutes les annotations
201
+ ```bash
202
+ huggingface-cli download \
203
+ matis35/ffgen-annotations-storage \
204
+ --repo-type dataset \
205
+ --local-dir ./collected_annotations
206
+ ```
207
+
208
+ ### Analyser l'accord inter-annotateurs (si gold items)
209
+ ```bash
210
+ cd FFGen/3_data_processing
211
+
212
+ python analyze_agreement.py \
213
+ ../../collected_annotations/annotations/*.json \
214
+ --gold-file annotation_study/gold_standard.json
215
+ ```
216
+
217
+ ## Dépannage Express
218
+
219
+ ### Le Space ne démarre pas
220
+ ```bash
221
+ # Vérifiez les logs
222
+ # Problème courant : secret mal configuré
223
+ # Solution : Vérifiez Settings > Variables and secrets
224
+ ```
225
+
226
+ ### "HF Storage not configured"
227
+ ```bash
228
+ # Il manque HF_TOKEN ou HF_DATASET_REPO
229
+ # Ajoutez-les dans Settings > Secrets
230
+ ```
231
+
232
+ ### "Authentication failed"
233
+ ```bash
234
+ # APP_PASSWORD incorrect ou manquant
235
+ # Vérifiez Settings > Secrets
236
+ ```
237
+
238
+ ### Un annotateur voit tout le dataset
239
+ ```bash
240
+ # Problème dans annotators.json
241
+ # Vérifiez start_idx et end_idx
242
+ # Rechargez le dataset après correction
243
+ ```
244
+
245
+ ### Les annotations disparaissent
246
+ ```bash
247
+ # Les annotateurs n'ont pas sauvegardé sur HF
248
+ # Rappelez-leur de cliquer sur "☁️ Sauvegarder sur HF"
249
+ # La sauvegarde locale est perdue tous les 48h
250
+ ```
251
+
252
+ ## Support
253
+
254
+ Documentation complète : [CONFIGURATION.md](CONFIGURATION.md)
255
+
256
+ Problèmes ?
257
+ - Vérifiez d'abord les logs du Space
258
+ - Consultez la section Dépannage de CONFIGURATION.md
259
+ - Ouvrez une issue GitHub si nécessaire
260
+
261
+ ---
262
+
263
+ **Temps total : ~15 minutes**
264
+
265
+ Prochain fichier à lire : [CONFIGURATION.md](CONFIGURATION.md) pour les détails complets.
README.md CHANGED
@@ -1,19 +1,148 @@
1
  ---
2
- title: Feedbacks Scoring
3
- emoji: 🚀
4
- colorFrom: red
5
- colorTo: red
6
  sdk: docker
7
- app_port: 8501
8
  tags:
9
  - streamlit
 
 
 
10
  pinned: false
11
- short_description: Streamlit template space
12
  ---
13
 
14
- # Welcome to Streamlit!
15
 
16
- Edit `/src/streamlit_app.py` to customize this app to your heart's desire. :heart:
17
 
18
- If you have any questions, checkout our [documentation](https://docs.streamlit.io) and [community
19
- forums](https://discuss.streamlit.io).
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Feedback Scoring App
3
+ emoji: 🔐
4
+ colorFrom: blue
5
+ colorTo: purple
6
  sdk: docker
7
+ app_port: 7860
8
  tags:
9
  - streamlit
10
+ - annotation
11
+ - feedback
12
+ - secure
13
  pinned: false
14
+ short_description: Secure code feedback annotation with multi-annotator support
15
  ---
16
 
17
+ # Feedback Scoring Tool - Production Version
18
 
19
+ Application Streamlit sécurisée pour l'annotation et le scoring de feedbacks de code par plusieurs annotateurs. Version prête pour le déploiement en production avec authentification, dispatch et persistance cloud.
20
 
21
+ ## Fonctionnalités
22
+
23
+ ### Annotation
24
+ - 📊 **Interface de scoring intuitive** : Notation de 1 à 5 pour chaque feedback
25
+ - 📈 **Statistiques en temps réel** : Suivi de votre progression et distribution des scores
26
+ - 💬 **Commentaires** : Ajoutez des notes sur chaque annotation
27
+ - 📥 **Import flexible** : Support des fichiers JSONL locaux et datasets HuggingFace
28
+
29
+ ### Sécurité et Collaboration
30
+ - 🔐 **Authentification** : Accès protégé par mot de passe
31
+ - 👥 **Multi-annotateurs** : Système de dispatch automatique par portion
32
+ - 🎯 **Isolation** : Chaque annotateur ne voit que sa portion du dataset
33
+ - 📋 **Traçabilité** : Toutes les annotations sont identifiées par annotateur
34
+
35
+ ### Persistance
36
+ - ☁️ **Sauvegarde cloud** : Stockage permanent sur HuggingFace Dataset privé
37
+ - 💾 **Auto-sauvegarde** : Progression sauvegardée automatiquement en local
38
+ - 🔄 **Reprise de session** : Continuez où vous en étiez, même après redémarrage
39
+ - 📤 **Export JSONL** : Téléchargez vos annotations à tout moment
40
+
41
+ ## Configuration pour Production
42
+
43
+ Cette application nécessite une configuration avant déploiement. Voir **[CONFIGURATION.md](CONFIGURATION.md)** pour le guide complet.
44
+
45
+ ### Configuration rapide
46
+
47
+ 1. **Créer un dataset HF privé** pour stocker les annotations
48
+ 2. **Configurer les secrets** dans Settings du Space :
49
+ - `APP_PASSWORD` : Mot de passe d'accès
50
+ - `HF_TOKEN` : Token avec droits d'écriture
51
+ - `HF_DATASET_REPO` : Nom du dataset de stockage
52
+ 3. **Configurer les annotateurs** dans `data/annotators.json`
53
+ 4. **Pousser sur HF Spaces**
54
+
55
+ 📖 **Documentation complète** : [CONFIGURATION.md](CONFIGURATION.md)
56
+
57
+ ## Utilisation (Annotateurs)
58
+
59
+ ### Workflow
60
+ 1. **Connexion** : Entrez le mot de passe fourni
61
+ 2. **Identification** : Sélectionnez votre identifiant
62
+ 3. **Chargement** : L'app charge automatiquement votre portion
63
+ 4. **Annotation** : Scorez les feedbacks (1-5) avec commentaires
64
+ 5. **Sauvegarde** : Cliquez sur "☁️ Sauvegarder sur HF" régulièrement
65
+ 6. **Reprise** : Vous pouvez fermer et reprendre plus tard
66
+
67
+ ### Format du dataset
68
+
69
+ ```json
70
+ {
71
+ "anchor": "code source",
72
+ "positive": "feedback positif",
73
+ "language": "python"
74
+ }
75
+ ```
76
+
77
+ ## Architecture
78
+
79
+ ```
80
+ backend/
81
+ ├── auth.py # Authentification
82
+ ├── annotator_config.py # Configuration annotateurs
83
+ ├── hf_storage.py # Sauvegarde HuggingFace
84
+ ├── data_loader.py # Chargement datasets
85
+ ├── persistence.py # Sauvegarde locale
86
+ ├── export.py # Export JSONL
87
+ └── statistics.py # Métriques
88
+
89
+ frontend/
90
+ ├── components.py # Composants UI
91
+ ├── styles.py # Styles CSS
92
+ └── help_page.py # Page d'aide
93
+
94
+ data/
95
+ ├── annotators.json # Config annotateurs
96
+ └── [sessions locales] # Sauvegardes temporaires
97
+ ```
98
+
99
+ ## Développement
100
+
101
+ ### Local
102
+ ```bash
103
+ pip install -r requirements.txt
104
+
105
+ # Définir les variables d'environnement
106
+ export APP_PASSWORD="dev"
107
+ export HF_TOKEN="hf_xxxxx"
108
+ export HF_DATASET_REPO="username/annotations"
109
+
110
+ streamlit run app.py
111
+ ```
112
+
113
+ ### Docker
114
+ ```bash
115
+ docker build -t feedback-scoring-app .
116
+ docker run -p 7860:7860 \
117
+ -e APP_PASSWORD="dev" \
118
+ -e HF_TOKEN="hf_xxxxx" \
119
+ -e HF_DATASET_REPO="username/annotations" \
120
+ feedback-scoring-app
121
+ ```
122
+
123
+ ## Intégration avec FFGen
124
+
125
+ Cette application s'intègre avec le pipeline [FFGen](https://github.com/YOUR_USERNAME/FFGen) :
126
+
127
+ 1. **Préparation** : Utilisez `create_annotation_study.py` pour créer les subsets
128
+ 2. **Annotation** : Utilisez cette app pour distribuer et collecter les scores
129
+ 3. **Analyse** : Utilisez `analyze_agreement.py` pour calculer l'accord
130
+ 4. **Fusion** : Utilisez `merge_scores.py` pour créer le dataset final
131
+
132
+ ## Sécurité
133
+
134
+ - ✅ Authentification par mot de passe
135
+ - ✅ Dataset de stockage privé
136
+ - ✅ Isolation des annotateurs
137
+ - ✅ Traçabilité complète
138
+ - ✅ Pas de données sensibles en clair dans le code
139
+
140
+ ## Support
141
+
142
+ - 📖 Documentation : [CONFIGURATION.md](CONFIGURATION.md)
143
+ - 🐛 Issues : [GitHub Issues](https://github.com/YOUR_USERNAME/feedbacks-scoring-app/issues)
144
+ - 📧 Contact : your.email@example.com
145
+
146
+ ## Licence
147
+
148
+ Ce projet est open source.
app.py ADDED
@@ -0,0 +1,415 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Application Streamlit standalone pour le scoring de feedbacks
3
+ Architecture backend/frontend modulaire avec persistance
4
+ """
5
+
6
+ import streamlit as st
7
+ from pathlib import Path
8
+
9
+ from backend.data_loader import (
10
+ load_dataset_from_jsonl,
11
+ load_dataset_from_hf,
12
+ filter_items_with_positive
13
+ )
14
+ from backend.export import prepare_export, export_to_jsonl
15
+ from backend.statistics import (
16
+ compute_progress,
17
+ compute_score_distribution,
18
+ compute_average_score,
19
+ compute_most_common_score,
20
+ find_unscored_indices
21
+ )
22
+ from backend.persistence import SessionManager
23
+
24
+
25
+ from frontend.styles import apply_custom_css
26
+ from frontend.components import (
27
+ render_navigation,
28
+ render_code_block,
29
+ render_feedback_block,
30
+ render_score_slider,
31
+ render_comment_field,
32
+ render_progress_metrics,
33
+ render_statistics,
34
+ render_export_section,
35
+ render_quick_actions
36
+ )
37
+ from frontend.help_page import render_help_page
38
+
39
+ # Configuration de la page
40
+ st.set_page_config(
41
+ page_title="Feedback Scoring Tool",
42
+ page_icon="⭐",
43
+ layout="wide",
44
+ initial_sidebar_state="expanded"
45
+ )
46
+
47
+
48
+ apply_custom_css()
49
+
50
+
51
+ DATA_DIR = Path("/app/data")
52
+ DATA_DIR.mkdir(exist_ok=True)
53
+
54
+ # Initialize session manager
55
+ session_manager = SessionManager(DATA_DIR)
56
+
57
+ # Initialize session state
58
+ if 'dataset' not in st.session_state:
59
+ st.session_state.dataset = None
60
+ if 'feedback_scores' not in st.session_state:
61
+ st.session_state.feedback_scores = {}
62
+ if 'scoring_index' not in st.session_state:
63
+ st.session_state.scoring_index = 0
64
+ if 'feedback_comments' not in st.session_state:
65
+ st.session_state.feedback_comments = {}
66
+ if 'items_with_positive' not in st.session_state:
67
+ st.session_state.items_with_positive = []
68
+ if 'show_help' not in st.session_state:
69
+ st.session_state.show_help = False
70
+ if 'session_loaded' not in st.session_state:
71
+ st.session_state.session_loaded = False
72
+
73
+
74
+ if not st.session_state.session_loaded:
75
+ session_info = session_manager.get_session_info()
76
+ if session_info:
77
+ st.session_state.session_loaded = True
78
+
79
+
80
+ # Sidebar - Configuration
81
+ with st.sidebar:
82
+ st.markdown('<div class="section-header">Menu</div>', unsafe_allow_html=True)
83
+
84
+ # Help button - Change text based on current state
85
+ button_text = "Retour aux annotations" if st.session_state.show_help else "Aide & Documentation"
86
+ button_type = "secondary" if st.session_state.show_help else "primary"
87
+
88
+ if st.button(button_text, use_container_width=True, type=button_type):
89
+ st.session_state.show_help = not st.session_state.show_help
90
+ st.rerun()
91
+
92
+ # Check for saved session
93
+ session_info = session_manager.get_session_info()
94
+ if session_info and st.session_state.dataset is None:
95
+ st.markdown('<div class="section-header">Session Sauvegardée</div>', unsafe_allow_html=True)
96
+
97
+ st.info(f"""
98
+ **Session trouvée**
99
+ - Dataset: {session_info['dataset_size']} exemples
100
+ - Scorés: {session_info['total_scored']}
101
+ - Position: {session_info['scoring_index'] + 1}
102
+ - Dernière sauvegarde: {session_info['last_saved'][:19]}
103
+ """)
104
+
105
+ col1, col2 = st.columns(2)
106
+ with col1:
107
+ if st.button("Reprendre", use_container_width=True, type="primary"):
108
+ with st.spinner("Chargement de la session..."):
109
+ dataset, _ = session_manager.load_dataset()
110
+ session_data = session_manager.load_session()
111
+
112
+ st.session_state.dataset = dataset
113
+ st.session_state.feedback_scores = session_data['feedback_scores']
114
+ st.session_state.feedback_comments = session_data['feedback_comments']
115
+ st.session_state.scoring_index = session_data['scoring_index']
116
+ st.session_state.show_help = False
117
+
118
+ st.success("Session reprise")
119
+ st.rerun()
120
+
121
+ with col2:
122
+ if st.button("Nouvelle", use_container_width=True):
123
+ if 'confirm_clear' not in st.session_state:
124
+ st.session_state.confirm_clear = True
125
+ st.warning("Cliquer encore pour confirmer")
126
+ else:
127
+ session_manager.clear_session()
128
+ st.session_state.confirm_clear = False
129
+ st.success("Session effacée")
130
+ st.rerun()
131
+
132
+ st.markdown("---")
133
+
134
+ st.markdown('<div class="section-header">Nouveau Dataset</div>', unsafe_allow_html=True)
135
+
136
+ # Charger un dataset
137
+ upload_option = st.radio(
138
+ "Source:",
139
+ ["Fichier local (.jsonl)", "HuggingFace Hub"],
140
+ label_visibility="collapsed"
141
+ )
142
+
143
+ if upload_option == "Fichier local (.jsonl)":
144
+ uploaded_file = st.file_uploader("Fichier JSONL", type=['jsonl'], label_visibility="collapsed")
145
+ if uploaded_file is not None:
146
+ if st.button("Charger", use_container_width=True):
147
+ with st.spinner("Chargement..."):
148
+ dataset = load_dataset_from_jsonl(uploaded_file)
149
+
150
+ # Save dataset
151
+ session_manager.save_dataset(dataset, {
152
+ 'source': 'local_file',
153
+ 'filename': uploaded_file.name
154
+ })
155
+
156
+ st.session_state.dataset = dataset
157
+ st.session_state.scoring_index = 0
158
+ st.session_state.feedback_scores = {}
159
+ st.session_state.feedback_comments = {}
160
+ st.session_state.show_help = False
161
+
162
+ # Save initial empty session
163
+ session_manager.save_session(
164
+ st.session_state.feedback_scores,
165
+ st.session_state.feedback_comments,
166
+ st.session_state.scoring_index
167
+ )
168
+
169
+ st.success(f"Dataset chargé: {len(dataset)} exemples")
170
+ st.rerun()
171
+
172
+ else: # HuggingFace Hub
173
+ hf_dataset = st.text_input("Dataset HF", placeholder="username/dataset", label_visibility="collapsed")
174
+ hf_split = st.text_input("Split", value="train", label_visibility="collapsed")
175
+
176
+ if st.button("Charger depuis HF", use_container_width=True):
177
+ if hf_dataset:
178
+ with st.spinner(f"Téléchargement de {hf_dataset}..."):
179
+ try:
180
+ dataset = load_dataset_from_hf(hf_dataset, hf_split)
181
+
182
+ # Save dataset locally
183
+ session_manager.save_dataset(dataset, {
184
+ 'source': 'huggingface',
185
+ 'dataset_name': hf_dataset,
186
+ 'split': hf_split
187
+ })
188
+
189
+ st.session_state.dataset = dataset
190
+ st.session_state.scoring_index = 0
191
+ st.session_state.feedback_scores = {}
192
+ st.session_state.feedback_comments = {}
193
+ st.session_state.show_help = False
194
+
195
+ # Save initial empty session
196
+ session_manager.save_session(
197
+ st.session_state.feedback_scores,
198
+ st.session_state.feedback_comments,
199
+ st.session_state.scoring_index
200
+ )
201
+
202
+ st.success(f"Dataset chargé et sauvegardé: {len(dataset)} exemples")
203
+ st.rerun()
204
+ except Exception as e:
205
+ st.error(f"Erreur: {str(e)}")
206
+ else:
207
+ st.warning("Entrez un nom de dataset")
208
+
209
+ # Main content - Show help or scoring interface
210
+ if st.session_state.show_help:
211
+ render_help_page()
212
+ st.stop()
213
+
214
+ # Titre
215
+ st.title("Scoring des Feedbacks")
216
+
217
+ # Main content
218
+ if not st.session_state.dataset:
219
+ st.warning("Aucun dataset chargé.")
220
+ st.info("Vérifiez la sidebar : vous avez peut-être une session sauvegardée à reprendre")
221
+ st.info("Cliquez sur Aide & Documentation pour le guide complet")
222
+ st.stop()
223
+
224
+ # Filter items with positive feedback
225
+ dataset = st.session_state.dataset
226
+ items_with_positive = filter_items_with_positive(dataset)
227
+ st.session_state.items_with_positive = items_with_positive
228
+
229
+ if not items_with_positive:
230
+ st.error("Aucun feedback positif trouvé dans le dataset.")
231
+ st.info("Le dataset doit contenir un champ 'positive' avec du texte.")
232
+ st.stop()
233
+
234
+ # Navigation
235
+ new_index = render_navigation(st.session_state.scoring_index, len(items_with_positive))
236
+ if new_index is not None:
237
+ st.session_state.scoring_index = new_index
238
+ # Auto-save on navigation
239
+ session_manager.save_session(
240
+ st.session_state.feedback_scores,
241
+ st.session_state.feedback_comments,
242
+ st.session_state.scoring_index
243
+ )
244
+ st.rerun()
245
+
246
+ st.markdown("---")
247
+
248
+ # Get current item
249
+ original_idx, current_item = items_with_positive[st.session_state.scoring_index]
250
+
251
+ # Display code
252
+ code_text = current_item.get('anchor', current_item.get('code', 'N/A'))
253
+ language = current_item.get('language', 'python')
254
+ render_code_block(code_text, language)
255
+
256
+ st.markdown("---")
257
+
258
+ # Display positive feedback
259
+ positive_feedback = current_item.get('positive', 'N/A')
260
+ render_feedback_block(positive_feedback)
261
+
262
+ st.markdown("---")
263
+
264
+ # Scoring interface
265
+ score_key = f"score_{original_idx}"
266
+ current_score = st.session_state.feedback_scores.get(original_idx, 3)
267
+ score = render_score_slider(score_key, current_score)
268
+
269
+ # Auto-save when score changes
270
+ if score != current_score:
271
+ st.session_state.feedback_scores[original_idx] = score
272
+ session_manager.save_session(
273
+ st.session_state.feedback_scores,
274
+ st.session_state.feedback_comments,
275
+ st.session_state.scoring_index
276
+ )
277
+
278
+ st.session_state.feedback_scores[original_idx] = score
279
+
280
+ # Comment field
281
+ comment_key = f"comment_{original_idx}"
282
+ current_comment = st.session_state.feedback_comments.get(original_idx, "")
283
+ comment = render_comment_field(comment_key, current_comment)
284
+ st.session_state.feedback_comments[original_idx] = comment
285
+
286
+ # Auto-save when comment changes
287
+ if comment != current_comment:
288
+ session_manager.save_session(
289
+ st.session_state.feedback_scores,
290
+ st.session_state.feedback_comments,
291
+ st.session_state.scoring_index
292
+ )
293
+
294
+ st.markdown("---")
295
+
296
+ # Progress and statistics
297
+ scored_items = len(st.session_state.feedback_scores)
298
+ total_items = len(items_with_positive)
299
+ progress_pct = compute_progress(scored_items, total_items) * 100
300
+
301
+ render_progress_metrics(
302
+ total=total_items,
303
+ scored=scored_items,
304
+ remaining=total_items - scored_items,
305
+ progress_pct=progress_pct
306
+ )
307
+
308
+ # Statistics
309
+ if st.session_state.feedback_scores:
310
+ avg_score = compute_average_score(st.session_state.feedback_scores)
311
+ most_common = compute_most_common_score(st.session_state.feedback_scores)
312
+ score_counts = compute_score_distribution(st.session_state.feedback_scores)
313
+
314
+ render_statistics(avg_score, most_common, score_counts)
315
+
316
+ st.markdown("---")
317
+
318
+ # Export section
319
+ export_data = prepare_export(
320
+ items_with_positive,
321
+ st.session_state.feedback_scores,
322
+ st.session_state.feedback_comments
323
+ )
324
+
325
+ col1, col2 = render_export_section(export_data)
326
+
327
+ # Téléchargement JSONL
328
+ with col2:
329
+ if export_data:
330
+ jsonl_content = export_to_jsonl(export_data)
331
+
332
+ st.download_button(
333
+ label="Télécharger JSONL",
334
+ data=jsonl_content,
335
+ file_name="feedback_scores.jsonl",
336
+ mime="application/jsonl",
337
+ use_container_width=True,
338
+ type="primary"
339
+ )
340
+ else:
341
+ st.button(
342
+ "📥 Télécharger JSONL",
343
+ disabled=True,
344
+ use_container_width=True,
345
+ help="Aucun score enregistré"
346
+ )
347
+
348
+ # Show export preview
349
+ if export_data:
350
+ with st.expander("Aperçu Export (5 premiers)"):
351
+ preview_items = export_data[:5]
352
+ st.json(preview_items)
353
+
354
+ # Quick actions
355
+ st.markdown("---")
356
+
357
+ reset_requested, jump_to_unscored, jump_to_index = render_quick_actions(
358
+ items_with_positive,
359
+ st.session_state.feedback_scores,
360
+ st.session_state.scoring_index
361
+ )
362
+
363
+ # Handle quick actions
364
+ if reset_requested:
365
+ if st.session_state.feedback_scores:
366
+ if 'confirm_reset' not in st.session_state:
367
+ st.session_state.confirm_reset = True
368
+ st.warning("Cliquez à nouveau pour confirmer la suppression de tous les scores")
369
+ else:
370
+ st.session_state.feedback_scores = {}
371
+ st.session_state.feedback_comments = {}
372
+ st.session_state.confirm_reset = False
373
+
374
+ # Save cleared session
375
+ session_manager.save_session(
376
+ st.session_state.feedback_scores,
377
+ st.session_state.feedback_comments,
378
+ st.session_state.scoring_index
379
+ )
380
+
381
+ st.success("Scores réinitialisés")
382
+ st.rerun()
383
+
384
+ if jump_to_unscored:
385
+ unscored_indices = find_unscored_indices(items_with_positive, st.session_state.feedback_scores)
386
+ if unscored_indices:
387
+ for pos, (idx, _) in enumerate(items_with_positive):
388
+ if idx == unscored_indices[0]:
389
+ st.session_state.scoring_index = pos
390
+
391
+ # Save position
392
+ session_manager.save_session(
393
+ st.session_state.feedback_scores,
394
+ st.session_state.feedback_comments,
395
+ st.session_state.scoring_index
396
+ )
397
+
398
+ st.rerun()
399
+ break
400
+
401
+ if jump_to_index is not None:
402
+ st.session_state.scoring_index = jump_to_index
403
+
404
+ # Save position
405
+ session_manager.save_session(
406
+ st.session_state.feedback_scores,
407
+ st.session_state.feedback_comments,
408
+ st.session_state.scoring_index
409
+ )
410
+
411
+ st.rerun()
412
+
413
+ # Footer
414
+ st.markdown("---")
415
+ st.caption("Sauvegarde automatique activée | Vous pouvez fermer et reprendre plus tard")
backend/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ """
2
+ Backend module for scoring app
3
+ """
backend/annotator_config.py ADDED
@@ -0,0 +1,279 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Configuration et gestion des annotateurs
3
+ Permet de définir qui annote quelle partie du dataset
4
+ """
5
+
6
+ import json
7
+ import os
8
+ import streamlit as st
9
+ from pathlib import Path
10
+
11
+
12
+ # Configuration par défaut des annotateurs
13
+ DEFAULT_ANNOTATOR_CONFIG = {
14
+ "annotator_1": {
15
+ "name": "Expert A",
16
+ "start_idx": 0,
17
+ "end_idx": 100,
18
+ "description": "Première portion du dataset"
19
+ },
20
+ "annotator_2": {
21
+ "name": "Expert B",
22
+ "start_idx": 100,
23
+ "end_idx": 200,
24
+ "description": "Deuxième portion du dataset"
25
+ },
26
+ "annotator_3": {
27
+ "name": "Expert C",
28
+ "start_idx": 200,
29
+ "end_idx": 300,
30
+ "description": "Troisième portion du dataset"
31
+ },
32
+ }
33
+
34
+
35
+ def load_annotator_config():
36
+ """
37
+ Charge la configuration des annotateurs.
38
+
39
+ Ordre de priorité:
40
+ 1. Fichier annotators.json dans /app/data/
41
+ 2. st.secrets["ANNOTATOR_CONFIG"]
42
+ 3. Variable d'environnement ANNOTATOR_CONFIG
43
+ 4. Configuration par défaut
44
+
45
+ Returns:
46
+ dict: Configuration des annotateurs
47
+ """
48
+ # 1. Essayer de charger depuis un fichier local
49
+ config_file = Path("/app/data/annotators.json")
50
+ if config_file.exists():
51
+ try:
52
+ with open(config_file, 'r', encoding='utf-8') as f:
53
+ config = json.load(f)
54
+ return config
55
+ except Exception as e:
56
+ st.warning(f"⚠️ Erreur lors du chargement de annotators.json: {e}")
57
+
58
+ # 2. Essayer st.secrets (HF Spaces)
59
+ try:
60
+ config_str = st.secrets.get("ANNOTATOR_CONFIG")
61
+ if config_str:
62
+ return json.loads(config_str)
63
+ except (FileNotFoundError, KeyError, json.JSONDecodeError):
64
+ pass
65
+
66
+ # 3. Essayer variable d'environnement
67
+ config_str = os.getenv("ANNOTATOR_CONFIG")
68
+ if config_str:
69
+ try:
70
+ return json.loads(config_str)
71
+ except json.JSONDecodeError:
72
+ st.warning("⚠️ ANNOTATOR_CONFIG mal formaté")
73
+
74
+ # 4. Retourner la config par défaut
75
+ return DEFAULT_ANNOTATOR_CONFIG
76
+
77
+
78
+ def save_annotator_config(config):
79
+ """
80
+ Sauvegarde la configuration des annotateurs dans un fichier local.
81
+
82
+ Args:
83
+ config: Dict de configuration
84
+
85
+ Returns:
86
+ bool: True si succès
87
+ """
88
+ try:
89
+ config_file = Path("/app/data/annotators.json")
90
+ config_file.parent.mkdir(exist_ok=True)
91
+
92
+ with open(config_file, 'w', encoding='utf-8') as f:
93
+ json.dump(config, f, indent=2, ensure_ascii=False)
94
+
95
+ return True
96
+ except Exception as e:
97
+ st.error(f"❌ Erreur lors de la sauvegarde: {e}")
98
+ return False
99
+
100
+
101
+ def get_annotator_config(annotator_id):
102
+ """
103
+ Récupère la configuration d'un annotateur spécifique.
104
+
105
+ Args:
106
+ annotator_id: ID de l'annotateur
107
+
108
+ Returns:
109
+ dict ou None: Configuration de l'annotateur
110
+ """
111
+ config = load_annotator_config()
112
+ return config.get(annotator_id)
113
+
114
+
115
+ def filter_dataset_for_annotator(dataset, annotator_config):
116
+ """
117
+ Filtre un dataset pour ne garder que la portion d'un annotateur.
118
+
119
+ Args:
120
+ dataset: Liste d'items du dataset
121
+ annotator_config: Config de l'annotateur avec start_idx et end_idx
122
+
123
+ Returns:
124
+ list: Portion filtrée du dataset
125
+ """
126
+ start = annotator_config.get("start_idx", 0)
127
+ end = annotator_config.get("end_idx", len(dataset))
128
+
129
+ # S'assurer que les indices sont valides
130
+ start = max(0, min(start, len(dataset)))
131
+ end = max(start, min(end, len(dataset)))
132
+
133
+ return dataset[start:end]
134
+
135
+
136
+ def validate_annotator_config(config):
137
+ """
138
+ Valide une configuration d'annotateurs.
139
+
140
+ Args:
141
+ config: Dict de configuration
142
+
143
+ Returns:
144
+ (bool, list): (is_valid, list_of_errors)
145
+ """
146
+ errors = []
147
+
148
+ if not isinstance(config, dict):
149
+ errors.append("La configuration doit être un dictionnaire")
150
+ return False, errors
151
+
152
+ for ann_id, ann_config in config.items():
153
+ if not isinstance(ann_config, dict):
154
+ errors.append(f"{ann_id}: La configuration doit être un dict")
155
+ continue
156
+
157
+ # Vérifier les champs requis
158
+ required_fields = ["name", "start_idx", "end_idx"]
159
+ for field in required_fields:
160
+ if field not in ann_config:
161
+ errors.append(f"{ann_id}: Champ '{field}' manquant")
162
+
163
+ # Vérifier les types
164
+ if "start_idx" in ann_config and not isinstance(ann_config["start_idx"], int):
165
+ errors.append(f"{ann_id}: start_idx doit être un entier")
166
+
167
+ if "end_idx" in ann_config and not isinstance(ann_config["end_idx"], int):
168
+ errors.append(f"{ann_id}: end_idx doit être un entier")
169
+
170
+ # Vérifier la logique
171
+ if "start_idx" in ann_config and "end_idx" in ann_config:
172
+ if ann_config["start_idx"] >= ann_config["end_idx"]:
173
+ errors.append(f"{ann_id}: start_idx doit être < end_idx")
174
+
175
+ return len(errors) == 0, errors
176
+
177
+
178
+ def create_annotator_config_from_chunks(num_annotators, total_items):
179
+ """
180
+ Crée automatiquement une configuration pour diviser un dataset en chunks.
181
+
182
+ Args:
183
+ num_annotators: Nombre d'annotateurs
184
+ total_items: Nombre total d'items dans le dataset
185
+
186
+ Returns:
187
+ dict: Configuration générée
188
+ """
189
+ items_per_annotator = total_items // num_annotators
190
+ config = {}
191
+
192
+ for i in range(num_annotators):
193
+ ann_id = f"annotator_{i+1}"
194
+ start_idx = i * items_per_annotator
195
+
196
+ # Le dernier annotateur prend tout ce qui reste
197
+ if i == num_annotators - 1:
198
+ end_idx = total_items
199
+ else:
200
+ end_idx = (i + 1) * items_per_annotator
201
+
202
+ config[ann_id] = {
203
+ "name": f"Annotateur {i+1}",
204
+ "start_idx": start_idx,
205
+ "end_idx": end_idx,
206
+ "description": f"Items {start_idx} à {end_idx-1}"
207
+ }
208
+
209
+ return config
210
+
211
+
212
+ def show_annotator_config_editor():
213
+ """
214
+ Affiche un éditeur de configuration des annotateurs (admin).
215
+ """
216
+ st.markdown("## ⚙️ Configuration des Annotateurs")
217
+
218
+ config = load_annotator_config()
219
+
220
+ st.info("Cette section permet de configurer les portions du dataset pour chaque annotateur")
221
+
222
+ # Afficher la config actuelle
223
+ st.json(config)
224
+
225
+ # Option pour créer une nouvelle config
226
+ with st.expander("Créer une nouvelle configuration"):
227
+ col1, col2 = st.columns(2)
228
+
229
+ with col1:
230
+ num_annotators = st.number_input(
231
+ "Nombre d'annotateurs",
232
+ min_value=1,
233
+ max_value=20,
234
+ value=3
235
+ )
236
+
237
+ with col2:
238
+ total_items = st.number_input(
239
+ "Nombre total d'items",
240
+ min_value=1,
241
+ value=300
242
+ )
243
+
244
+ if st.button("Générer la configuration", type="primary"):
245
+ new_config = create_annotator_config_from_chunks(num_annotators, total_items)
246
+ st.json(new_config)
247
+
248
+ if st.button("Sauvegarder cette configuration"):
249
+ if save_annotator_config(new_config):
250
+ st.success("✅ Configuration sauvegardée")
251
+ st.rerun()
252
+
253
+ # Éditeur manuel
254
+ with st.expander("Éditer manuellement (JSON)"):
255
+ st.markdown("**Format attendu:**")
256
+ st.code(json.dumps(DEFAULT_ANNOTATOR_CONFIG, indent=2), language="json")
257
+
258
+ config_text = st.text_area(
259
+ "Configuration JSON",
260
+ value=json.dumps(config, indent=2),
261
+ height=300
262
+ )
263
+
264
+ if st.button("Valider et sauvegarder"):
265
+ try:
266
+ new_config = json.loads(config_text)
267
+ is_valid, errors = validate_annotator_config(new_config)
268
+
269
+ if is_valid:
270
+ if save_annotator_config(new_config):
271
+ st.success("✅ Configuration validée et sauvegardée")
272
+ st.rerun()
273
+ else:
274
+ st.error("❌ Configuration invalide:")
275
+ for error in errors:
276
+ st.error(f" - {error}")
277
+
278
+ except json.JSONDecodeError as e:
279
+ st.error(f"❌ JSON invalide: {e}")
backend/auth.py ADDED
@@ -0,0 +1,148 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Module d'authentification pour l'application de scoring
3
+ Gère l'authentification par mot de passe et l'identification des annotateurs
4
+ """
5
+
6
+ import streamlit as st
7
+ import os
8
+ import warnings
9
+
10
+ # Supprimer les warnings de secrets manquants
11
+ warnings.filterwarnings('ignore', message='.*secrets.*')
12
+
13
+
14
+ def check_password():
15
+ """
16
+ Vérifie que l'utilisateur a le bon mot de passe.
17
+ Returns True si authentifié, False sinon.
18
+ """
19
+
20
+ def password_entered():
21
+ """Vérifie si le mot de passe est correct"""
22
+ # Support pour secrets (HF Spaces) ou variable d'environnement
23
+ correct_password = None
24
+
25
+ # Essayer d'abord st.secrets (HF Spaces) - sans message d'erreur
26
+ try:
27
+ correct_password = st.secrets.get("APP_PASSWORD")
28
+ except:
29
+ pass
30
+
31
+ # Fallback sur variable d'environnement
32
+ if not correct_password:
33
+ correct_password = os.getenv("APP_PASSWORD")
34
+
35
+ # Fallback sur mot de passe par défaut
36
+ if not correct_password:
37
+ correct_password = "annotator2025"
38
+
39
+ if st.session_state["password"] == correct_password:
40
+ st.session_state["password_correct"] = True
41
+ del st.session_state["password"] # Ne pas garder le mot de passe en clair
42
+ else:
43
+ st.session_state["password_correct"] = False
44
+
45
+ # Premier affichage ou non authentifié
46
+ if "password_correct" not in st.session_state:
47
+ st.markdown("## 🔐 Authentification Annotateur")
48
+ st.info("Entrez le mot de passe fourni par l'équipe de recherche")
49
+ st.text_input(
50
+ "Mot de passe",
51
+ type="password",
52
+ on_change=password_entered,
53
+ key="password",
54
+ label_visibility="collapsed"
55
+ )
56
+ return False
57
+
58
+ elif not st.session_state["password_correct"]:
59
+ st.markdown("## 🔐 Authentification Annotateur")
60
+ st.text_input(
61
+ "Mot de passe",
62
+ type="password",
63
+ on_change=password_entered,
64
+ key="password",
65
+ label_visibility="collapsed"
66
+ )
67
+ st.error("❌ Mot de passe incorrect")
68
+ return False
69
+
70
+ else:
71
+ # Authentifié
72
+ return True
73
+
74
+
75
+ def get_annotator_identity():
76
+ """
77
+ Récupère ou demande l'identité de l'annotateur.
78
+ Returns: (annotator_id, annotator_name) ou None si non défini
79
+ """
80
+ if "annotator_id" not in st.session_state:
81
+ st.session_state.annotator_id = None
82
+ st.session_state.annotator_name = None
83
+
84
+ return st.session_state.annotator_id, st.session_state.annotator_name
85
+
86
+
87
+ def set_annotator_identity(annotator_id, annotator_name):
88
+ """
89
+ Définit l'identité de l'annotateur dans la session
90
+ """
91
+ st.session_state.annotator_id = annotator_id
92
+ st.session_state.annotator_name = annotator_name
93
+
94
+
95
+ def show_annotator_selector(annotator_config):
96
+ """
97
+ Affiche un sélecteur d'annotateur basé sur la configuration.
98
+
99
+ Args:
100
+ annotator_config: Dict avec la configuration des annotateurs
101
+ Format: {
102
+ "annotator_1": {
103
+ "name": "Expert A",
104
+ "start_idx": 0,
105
+ "end_idx": 100
106
+ },
107
+ ...
108
+ }
109
+
110
+ Returns:
111
+ (annotator_id, config) ou (None, None) si pas sélectionné
112
+ """
113
+ st.markdown("## 👤 Sélection de l'annotateur")
114
+ st.info("Sélectionnez votre identifiant pour charger votre portion du dataset")
115
+
116
+ # Créer la liste des options
117
+ options = ["-- Choisissez votre identifiant --"]
118
+ annotator_mapping = {}
119
+
120
+ for ann_id, ann_config in annotator_config.items():
121
+ display_name = f"{ann_config['name']} (Items {ann_config['start_idx']}-{ann_config['end_idx']})"
122
+ options.append(display_name)
123
+ annotator_mapping[display_name] = (ann_id, ann_config)
124
+
125
+ selected = st.selectbox(
126
+ "Annotateur",
127
+ options,
128
+ key="annotator_selector",
129
+ label_visibility="collapsed"
130
+ )
131
+
132
+ if selected == options[0]:
133
+ return None, None
134
+
135
+ annotator_id, config = annotator_mapping[selected]
136
+
137
+ # Afficher un résumé
138
+ st.success(f"""
139
+ ✅ **{config['name']}**
140
+ - Items à annoter: {config['end_idx'] - config['start_idx']}
141
+ - Range: [{config['start_idx']}, {config['end_idx']}[
142
+ """)
143
+
144
+ if st.button("Confirmer et continuer", type="primary", use_container_width=True):
145
+ set_annotator_identity(annotator_id, config['name'])
146
+ return annotator_id, config
147
+
148
+ return None, None
backend/data_loader.py ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Data loading utilities
3
+ """
4
+
5
+ import json
6
+ from datasets import load_dataset
7
+
8
+
9
+ def load_dataset_from_jsonl(file):
10
+ """Charge un dataset depuis un fichier JSONL"""
11
+ data = []
12
+ content = file.getvalue().decode('utf-8')
13
+ for line in content.split('\n'):
14
+ if line.strip():
15
+ data.append(json.loads(line))
16
+ return data
17
+
18
+
19
+ def load_dataset_from_hf(dataset_name, split='train'):
20
+ """Charge un dataset depuis HuggingFace"""
21
+ dataset = load_dataset(dataset_name, split=split)
22
+ return list(dataset)
23
+
24
+
25
+ def filter_items_with_positive(dataset):
26
+ """Filtre les items qui ont un champ 'positive' non vide"""
27
+ items_with_positive = []
28
+ for idx, item in enumerate(dataset):
29
+ if 'positive' in item and item['positive']:
30
+ items_with_positive.append((idx, item))
31
+ return items_with_positive
backend/export.py ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Export utilities
3
+ """
4
+
5
+ import json
6
+
7
+
8
+ def prepare_export(items_with_positive, feedback_scores, feedback_comments=None):
9
+ """
10
+ Prépare les données pour l'export avec identifiants uniques
11
+
12
+ Args:
13
+ items_with_positive: Liste de tuples (original_idx, item)
14
+ feedback_scores: Dict {idx: score}
15
+ feedback_comments: Dict {idx: comment} (optionnel)
16
+
17
+ Returns:
18
+ Liste de dicts prêts pour l'export
19
+ """
20
+ export_data = []
21
+
22
+ for original_idx, item in items_with_positive:
23
+ if original_idx in feedback_scores:
24
+ export_item = {
25
+ 'code': item.get('anchor', item.get('code', '')),
26
+ 'positive_feedback': item.get('positive', ''),
27
+ 'score': feedback_scores[original_idx],
28
+ }
29
+
30
+ # Add unique identifiers for merging
31
+ # Priority: code_id > author_id > hash of content
32
+ if 'code_id' in item:
33
+ export_item['code_id'] = item['code_id']
34
+ if 'author_id' in item:
35
+ export_item['author_id'] = item['author_id']
36
+
37
+ # Add optional fields
38
+ if 'language' in item:
39
+ export_item['language'] = item['language']
40
+
41
+ # Add comment if exists
42
+ if feedback_comments and original_idx in feedback_comments:
43
+ comment = feedback_comments[original_idx]
44
+ if comment.strip():
45
+ export_item['comment'] = comment
46
+
47
+ # Add original index for reference (local to this file)
48
+ export_item['original_index'] = original_idx
49
+
50
+ export_data.append(export_item)
51
+
52
+ return export_data
53
+
54
+
55
+ def export_to_jsonl(export_data):
56
+ """
57
+ Convertit les données en format JSONL
58
+
59
+ Args:
60
+ export_data: Liste de dicts
61
+
62
+ Returns:
63
+ String JSONL
64
+ """
65
+ return "\n".join(json.dumps(item, ensure_ascii=False) for item in export_data)
backend/hf_storage.py ADDED
@@ -0,0 +1,237 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Module de sauvegarde des annotations sur HuggingFace Dataset
3
+ Permet une persistance permanente même si le Space redémarre
4
+ """
5
+
6
+ import json
7
+ import os
8
+ from datetime import datetime
9
+ from pathlib import Path
10
+ import tempfile
11
+ import streamlit as st
12
+
13
+
14
+ def get_hf_config():
15
+ """
16
+ Récupère la configuration HuggingFace depuis les secrets ou variables d'env.
17
+
18
+ Returns:
19
+ (hf_token, dataset_repo) ou (None, None) si non configuré
20
+ """
21
+ hf_token = None
22
+ dataset_repo = None
23
+
24
+ # Essayer d'abord st.secrets (HF Spaces)
25
+ try:
26
+ hf_token = st.secrets.get("HF_TOKEN")
27
+ dataset_repo = st.secrets.get("HF_DATASET_REPO")
28
+ except (FileNotFoundError, KeyError):
29
+ pass
30
+
31
+ # Fallback sur variables d'environnement
32
+ if not hf_token:
33
+ hf_token = os.getenv("HF_TOKEN")
34
+ if not dataset_repo:
35
+ dataset_repo = os.getenv("HF_DATASET_REPO")
36
+
37
+ return hf_token, dataset_repo
38
+
39
+
40
+ def is_hf_storage_enabled():
41
+ """
42
+ Vérifie si la sauvegarde HF est configurée et disponible.
43
+
44
+ Returns:
45
+ bool: True si configuré, False sinon
46
+ """
47
+ hf_token, dataset_repo = get_hf_config()
48
+ return hf_token is not None and dataset_repo is not None
49
+
50
+
51
+ def save_annotations_to_hf(
52
+ annotator_id,
53
+ annotator_name,
54
+ feedback_scores,
55
+ feedback_comments,
56
+ dataset_metadata=None
57
+ ):
58
+ """
59
+ Sauvegarde les annotations sur un Dataset HuggingFace.
60
+
61
+ Args:
62
+ annotator_id: ID de l'annotateur
63
+ annotator_name: Nom de l'annotateur
64
+ feedback_scores: Dict des scores {item_id: score}
65
+ feedback_comments: Dict des commentaires {item_id: comment}
66
+ dataset_metadata: Metadata du dataset annoté (optionnel)
67
+
68
+ Returns:
69
+ bool: True si succès, False sinon
70
+ """
71
+ try:
72
+ from huggingface_hub import HfApi
73
+
74
+ hf_token, dataset_repo = get_hf_config()
75
+
76
+ if not hf_token or not dataset_repo:
77
+ st.error("❌ Configuration HuggingFace manquante (HF_TOKEN ou HF_DATASET_REPO)")
78
+ return False
79
+
80
+ # Préparer les données d'annotation
81
+ timestamp = datetime.now().isoformat()
82
+ annotation_data = {
83
+ "annotator_id": annotator_id,
84
+ "annotator_name": annotator_name,
85
+ "timestamp": timestamp,
86
+ "scores": feedback_scores,
87
+ "comments": feedback_comments,
88
+ "num_scored": len(feedback_scores),
89
+ "metadata": dataset_metadata or {}
90
+ }
91
+
92
+ # Créer un fichier temporaire
93
+ filename = f"annotation_{annotator_id}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
94
+
95
+ with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False, encoding='utf-8') as f:
96
+ json.dump(annotation_data, f, indent=2, ensure_ascii=False)
97
+ temp_path = f.name
98
+
99
+ try:
100
+ # Upload vers le Dataset HF
101
+ api = HfApi(token=hf_token)
102
+
103
+ # Créer le repo s'il n'existe pas
104
+ try:
105
+ api.create_repo(
106
+ repo_id=dataset_repo,
107
+ repo_type="dataset",
108
+ private=True,
109
+ exist_ok=True
110
+ )
111
+ except Exception as e:
112
+ # Le repo existe déjà, c'est OK
113
+ pass
114
+
115
+ # Upload le fichier
116
+ api.upload_file(
117
+ path_or_fileobj=temp_path,
118
+ path_in_repo=f"annotations/{filename}",
119
+ repo_id=dataset_repo,
120
+ repo_type="dataset",
121
+ commit_message=f"Add annotations from {annotator_name} ({annotator_id})"
122
+ )
123
+
124
+ return True
125
+
126
+ finally:
127
+ # Nettoyer le fichier temporaire
128
+ try:
129
+ os.unlink(temp_path)
130
+ except:
131
+ pass
132
+
133
+ except ImportError:
134
+ st.error("❌ Package 'huggingface_hub' non installé")
135
+ return False
136
+ except Exception as e:
137
+ st.error(f"❌ Erreur lors de la sauvegarde HF: {str(e)}")
138
+ return False
139
+
140
+
141
+ def load_annotations_from_hf(annotator_id):
142
+ """
143
+ Charge les annotations d'un annotateur depuis le Dataset HF.
144
+
145
+ Args:
146
+ annotator_id: ID de l'annotateur
147
+
148
+ Returns:
149
+ dict ou None: Données d'annotation si trouvées, None sinon
150
+ """
151
+ try:
152
+ from huggingface_hub import HfApi, hf_hub_download
153
+
154
+ hf_token, dataset_repo = get_hf_config()
155
+
156
+ if not hf_token or not dataset_repo:
157
+ return None
158
+
159
+ api = HfApi(token=hf_token)
160
+
161
+ # Lister les fichiers dans le repo
162
+ try:
163
+ files = api.list_repo_files(repo_id=dataset_repo, repo_type="dataset")
164
+ except Exception:
165
+ # Le repo n'existe pas encore
166
+ return None
167
+
168
+ # Chercher les fichiers de cet annotateur
169
+ annotation_files = [
170
+ f for f in files
171
+ if f.startswith(f"annotations/annotation_{annotator_id}_")
172
+ ]
173
+
174
+ if not annotation_files:
175
+ return None
176
+
177
+ # Prendre le plus récent
178
+ latest_file = sorted(annotation_files)[-1]
179
+
180
+ # Télécharger et charger
181
+ local_path = hf_hub_download(
182
+ repo_id=dataset_repo,
183
+ filename=latest_file,
184
+ repo_type="dataset",
185
+ token=hf_token
186
+ )
187
+
188
+ with open(local_path, 'r', encoding='utf-8') as f:
189
+ return json.load(f)
190
+
191
+ except ImportError:
192
+ return None
193
+ except Exception as e:
194
+ st.warning(f"⚠️ Impossible de charger les annotations HF: {str(e)}")
195
+ return None
196
+
197
+
198
+ def get_all_annotator_files(annotator_id=None):
199
+ """
200
+ Liste tous les fichiers d'annotations (optionnellement pour un annotateur).
201
+
202
+ Args:
203
+ annotator_id: ID de l'annotateur (None = tous)
204
+
205
+ Returns:
206
+ list: Liste des chemins de fichiers
207
+ """
208
+ try:
209
+ from huggingface_hub import HfApi
210
+
211
+ hf_token, dataset_repo = get_hf_config()
212
+
213
+ if not hf_token or not dataset_repo:
214
+ return []
215
+
216
+ api = HfApi(token=hf_token)
217
+
218
+ try:
219
+ files = api.list_repo_files(repo_id=dataset_repo, repo_type="dataset")
220
+ except Exception:
221
+ return []
222
+
223
+ # Filtrer les fichiers d'annotation
224
+ annotation_files = [f for f in files if f.startswith("annotations/")]
225
+
226
+ if annotator_id:
227
+ annotation_files = [
228
+ f for f in annotation_files
229
+ if f.startswith(f"annotations/annotation_{annotator_id}_")
230
+ ]
231
+
232
+ return sorted(annotation_files)
233
+
234
+ except ImportError:
235
+ return []
236
+ except Exception:
237
+ return []
backend/persistence.py ADDED
@@ -0,0 +1,157 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Persistence utilities for datasets and scores
3
+ """
4
+
5
+ import json
6
+ from pathlib import Path
7
+ from datetime import datetime
8
+
9
+
10
+ class SessionManager:
11
+ """Gère la persistance des sessions de scoring"""
12
+
13
+ def __init__(self, data_dir="/app/data"):
14
+ self.data_dir = Path(data_dir)
15
+ self.data_dir.mkdir(exist_ok=True)
16
+
17
+ self.dataset_file = self.data_dir / "dataset.jsonl"
18
+ self.session_file = self.data_dir / "session.json"
19
+
20
+ def save_dataset(self, dataset, source_info=None):
21
+ """
22
+ Sauvegarde le dataset en JSONL
23
+
24
+ Args:
25
+ dataset: Liste de dicts
26
+ source_info: Dict avec info sur la source (nom HF, fichier, etc.)
27
+ """
28
+ # Sauvegarder le dataset
29
+ with open(self.dataset_file, 'w', encoding='utf-8') as f:
30
+ for item in dataset:
31
+ f.write(json.dumps(item, ensure_ascii=False) + '\n')
32
+
33
+ # Sauvegarder les métadonnées
34
+ metadata = {
35
+ 'saved_at': datetime.now().isoformat(),
36
+ 'total_items': len(dataset),
37
+ 'source_info': source_info or {}
38
+ }
39
+
40
+ metadata_file = self.data_dir / "dataset_metadata.json"
41
+ with open(metadata_file, 'w', encoding='utf-8') as f:
42
+ json.dump(metadata, f, indent=2, ensure_ascii=False)
43
+
44
+ return True
45
+
46
+ def load_dataset(self):
47
+ """
48
+ Charge le dataset sauvegardé
49
+
50
+ Returns:
51
+ Tuple (dataset, metadata) ou (None, None) si pas de dataset sauvegardé
52
+ """
53
+ if not self.dataset_file.exists():
54
+ return None, None
55
+
56
+ # Charger le dataset
57
+ dataset = []
58
+ with open(self.dataset_file, 'r', encoding='utf-8') as f:
59
+ for line in f:
60
+ if line.strip():
61
+ dataset.append(json.loads(line))
62
+
63
+ # Charger les métadonnées
64
+ metadata_file = self.data_dir / "dataset_metadata.json"
65
+ metadata = None
66
+ if metadata_file.exists():
67
+ with open(metadata_file, 'r', encoding='utf-8') as f:
68
+ metadata = json.load(f)
69
+
70
+ return dataset, metadata
71
+
72
+ def save_session(self, feedback_scores, feedback_comments, scoring_index):
73
+ """
74
+ Sauvegarde l'état de la session (scores, commentaires, position)
75
+
76
+ Args:
77
+ feedback_scores: Dict {idx: score}
78
+ feedback_comments: Dict {idx: comment}
79
+ scoring_index: Index actuel
80
+ """
81
+ session_data = {
82
+ 'feedback_scores': {str(k): v for k, v in feedback_scores.items()},
83
+ 'feedback_comments': {str(k): v for k, v in feedback_comments.items()},
84
+ 'scoring_index': scoring_index,
85
+ 'last_saved': datetime.now().isoformat(),
86
+ 'total_scored': len(feedback_scores)
87
+ }
88
+
89
+ with open(self.session_file, 'w', encoding='utf-8') as f:
90
+ json.dump(session_data, f, indent=2, ensure_ascii=False)
91
+
92
+ return True
93
+
94
+ def load_session(self):
95
+ """
96
+ Charge l'état de la session sauvegardée
97
+
98
+ Returns:
99
+ Dict avec feedback_scores, feedback_comments, scoring_index
100
+ ou None si pas de session sauvegardée
101
+ """
102
+ if not self.session_file.exists():
103
+ return None
104
+
105
+ with open(self.session_file, 'r', encoding='utf-8') as f:
106
+ session_data = json.load(f)
107
+
108
+ # Convertir les clés en int
109
+ session_data['feedback_scores'] = {
110
+ int(k): v for k, v in session_data['feedback_scores'].items()
111
+ }
112
+ session_data['feedback_comments'] = {
113
+ int(k): v for k, v in session_data['feedback_comments'].items()
114
+ }
115
+
116
+ return session_data
117
+
118
+ def has_saved_session(self):
119
+ """Vérifie s'il existe une session sauvegardée"""
120
+ return self.dataset_file.exists() and self.session_file.exists()
121
+
122
+ def clear_session(self):
123
+ """Supprime la session sauvegardée"""
124
+ if self.dataset_file.exists():
125
+ self.dataset_file.unlink()
126
+ if self.session_file.exists():
127
+ self.session_file.unlink()
128
+
129
+ metadata_file = self.data_dir / "dataset_metadata.json"
130
+ if metadata_file.exists():
131
+ metadata_file.unlink()
132
+
133
+ return True
134
+
135
+ def get_session_info(self):
136
+ """
137
+ Récupère les informations sur la session sauvegardée
138
+
139
+ Returns:
140
+ Dict avec les infos ou None
141
+ """
142
+ if not self.has_saved_session():
143
+ return None
144
+
145
+ dataset, dataset_metadata = self.load_dataset()
146
+ session_data = self.load_session()
147
+
148
+ if dataset and session_data:
149
+ return {
150
+ 'dataset_size': len(dataset),
151
+ 'total_scored': session_data['total_scored'],
152
+ 'last_saved': session_data['last_saved'],
153
+ 'scoring_index': session_data['scoring_index'],
154
+ 'source_info': dataset_metadata.get('source_info', {}) if dataset_metadata else {}
155
+ }
156
+
157
+ return None
backend/statistics.py ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Statistics computation utilities
3
+ """
4
+
5
+
6
+ def compute_progress(scored_count, total_count):
7
+ """Calcule le pourcentage de progression"""
8
+ if total_count == 0:
9
+ return 0.0
10
+ return scored_count / total_count
11
+
12
+
13
+ def compute_score_distribution(feedback_scores):
14
+ """
15
+ Calcule la distribution des scores
16
+
17
+ Args:
18
+ feedback_scores: Dict {idx: score}
19
+
20
+ Returns:
21
+ Dict {score: count} pour scores 0-5
22
+ """
23
+ scores_list = list(feedback_scores.values())
24
+ return {i: scores_list.count(i) for i in range(6)}
25
+
26
+
27
+ def compute_average_score(feedback_scores):
28
+ """Calcule le score moyen"""
29
+ if not feedback_scores:
30
+ return 0.0
31
+ scores_list = list(feedback_scores.values())
32
+ return sum(scores_list) / len(scores_list)
33
+
34
+
35
+ def compute_most_common_score(feedback_scores):
36
+ """
37
+ Trouve le score le plus fréquent
38
+
39
+ Returns:
40
+ Tuple (score, count)
41
+ """
42
+ distribution = compute_score_distribution(feedback_scores)
43
+ return max(distribution.items(), key=lambda x: x[1])
44
+
45
+
46
+ def find_unscored_indices(items_with_positive, feedback_scores):
47
+ """
48
+ Trouve les indices des items non encore scorés
49
+
50
+ Returns:
51
+ Liste d'indices
52
+ """
53
+ return [idx for idx, _ in items_with_positive if idx not in feedback_scores]
frontend/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ """
2
+ Frontend module for scoring app
3
+ """
frontend/components.py ADDED
@@ -0,0 +1,189 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ UI Components
3
+ """
4
+
5
+ import streamlit as st
6
+
7
+
8
+ def render_navigation(current_index, total_items):
9
+ """
10
+ Affiche les boutons de navigation
11
+
12
+ Returns:
13
+ Nouvelle valeur de l'index si changement, None sinon
14
+ """
15
+ col1, col2, col3 = st.columns([1, 2, 1])
16
+
17
+ new_index = None
18
+
19
+ with col1:
20
+ if st.button("Précédent", use_container_width=True, disabled=current_index == 0):
21
+ new_index = current_index - 1
22
+
23
+ with col2:
24
+ st.markdown(f"<div class='center-text'>Exemple {current_index + 1} / {total_items}</div>", unsafe_allow_html=True)
25
+
26
+ with col3:
27
+ if st.button("Suivant", use_container_width=True, disabled=current_index >= total_items - 1):
28
+ new_index = current_index + 1
29
+
30
+ return new_index
31
+
32
+
33
+ def render_code_block(code_text, language="python"):
34
+ """Affiche un bloc de code avec syntax highlighting"""
35
+ st.markdown("### Code")
36
+ st.code(code_text, language=language)
37
+
38
+
39
+ def render_feedback_block(feedback_text):
40
+ """Affiche le feedback dans un bloc stylisé adapté au thème"""
41
+ st.markdown("### Feedback")
42
+
43
+ # Utilise la classe CSS qui s'adapte automatiquement au thème
44
+ st.markdown(f"""
45
+ <div class="feedback-block">
46
+ {feedback_text}
47
+ </div>
48
+ """, unsafe_allow_html=True)
49
+
50
+
51
+ def render_score_slider(score_key, current_score=3):
52
+ """
53
+ Affiche le slider de notation
54
+
55
+ Returns:
56
+ Score sélectionné
57
+ """
58
+ st.markdown("### Notez le feedback")
59
+ st.caption("À quel point ce feedback est-il utile et pertinent pour ce code ?")
60
+
61
+ score = st.slider(
62
+ "Score",
63
+ min_value=0,
64
+ max_value=5,
65
+ value=current_score,
66
+ step=1,
67
+ format="%d",
68
+ key=score_key,
69
+ help="0 = Pas utile du tout, 5 = Extrêmement utile"
70
+ )
71
+
72
+ # Show score meaning
73
+ score_meanings = {
74
+ 0: "Pas utile / Incorrect",
75
+ 1: "Très peu utile",
76
+ 2: "Peu utile",
77
+ 3: "Moyennement utile",
78
+ 4: "Utile",
79
+ 5: "Extrêmement utile"
80
+ }
81
+
82
+ st.info(f"{score} - {score_meanings[score]}")
83
+
84
+ return score
85
+
86
+
87
+ def render_comment_field(comment_key, current_comment=""):
88
+ """
89
+ Affiche le champ de commentaire optionnel
90
+
91
+ Returns:
92
+ Commentaire saisi
93
+ """
94
+ with st.expander("Ajouter un commentaire (optionnel)"):
95
+ comment = st.text_area(
96
+ "Commentaire",
97
+ value=current_comment,
98
+ key=comment_key,
99
+ placeholder="Pourquoi ce score ? (optionnel)",
100
+ label_visibility="collapsed"
101
+ )
102
+ return comment
103
+
104
+
105
+ def render_progress_metrics(total, scored, remaining, progress_pct):
106
+ """Affiche les métriques de progression"""
107
+ st.markdown("### Progression")
108
+
109
+ col1, col2, col3, col4 = st.columns(4)
110
+
111
+ with col1:
112
+ st.metric("Total", total)
113
+
114
+ with col2:
115
+ st.metric("Scorés", scored)
116
+
117
+ with col3:
118
+ st.metric("Restants", remaining)
119
+
120
+ with col4:
121
+ st.metric("Progression", f"{progress_pct:.0f}%")
122
+
123
+ st.progress(progress_pct / 100)
124
+
125
+
126
+ def render_statistics(avg_score, most_common_score, score_counts):
127
+ """Affiche les statistiques détaillées"""
128
+ st.markdown("### Statistiques des Scores")
129
+
130
+ col1, col2 = st.columns(2)
131
+
132
+ with col1:
133
+ st.metric("Score Moyen", f"{avg_score:.2f}")
134
+
135
+ with col2:
136
+ st.metric("Score le plus fréquent", f"{most_common_score[0]} ({most_common_score[1]}x)")
137
+
138
+ st.bar_chart(score_counts, height=200)
139
+
140
+
141
+ def render_export_section(export_data):
142
+ """Affiche la section d'export"""
143
+ st.markdown("### Export")
144
+
145
+ col1, col2 = st.columns(2)
146
+
147
+ with col1:
148
+ st.metric("Items à exporter", len(export_data))
149
+
150
+ return col2
151
+
152
+
153
+ def render_quick_actions(items_with_positive, feedback_scores, current_index):
154
+ """
155
+ Affiche les actions rapides
156
+
157
+ Returns:
158
+ Tuple (reset_requested, jump_to_unscored, jump_to_index)
159
+ """
160
+ st.markdown("### Actions Rapides")
161
+
162
+ col1, col2, col3 = st.columns(3)
163
+
164
+ reset_requested = False
165
+ jump_to_unscored = False
166
+ jump_to_index = None
167
+
168
+ with col1:
169
+ if st.button("Réinitialiser tous les scores", use_container_width=True):
170
+ reset_requested = True
171
+
172
+ with col2:
173
+ unscored_indices = [idx for idx, _ in items_with_positive if idx not in feedback_scores]
174
+ if unscored_indices and st.button("Aller au prochain non-scoré", use_container_width=True):
175
+ jump_to_unscored = True
176
+
177
+ with col3:
178
+ jump_to = st.number_input(
179
+ "Aller à l'exemple",
180
+ min_value=1,
181
+ max_value=len(items_with_positive),
182
+ value=current_index + 1,
183
+ step=1,
184
+ key="jump_to"
185
+ )
186
+ if st.button("Aller", use_container_width=True):
187
+ jump_to_index = jump_to - 1
188
+
189
+ return reset_requested, jump_to_unscored, jump_to_index
frontend/help_page.py ADDED
@@ -0,0 +1,308 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Page d'aide détaillée
3
+ """
4
+
5
+ import streamlit as st
6
+
7
+
8
+ def render_help_page():
9
+ """Affiche la page d'aide complète"""
10
+
11
+ st.title("Guide d'utilisation")
12
+ st.markdown("Tout ce que vous devez savoir pour utiliser l'application de scoring de feedbacks")
13
+
14
+ st.markdown("---")
15
+
16
+ # Section 1: Introduction
17
+ st.markdown("## Objectif de l'application")
18
+ st.markdown("""
19
+ Cette application permet de **noter la qualité des feedbacks** générés pour des snippets de code.
20
+ Vous allez évaluer si les feedbacks sont utiles, pertinents et bien formulés.
21
+
22
+ **Échelle de notation : 0 à 5**
23
+ - **0** : Pas utile / Incorrect
24
+ - **1** : Très peu utile
25
+ - **2** : Peu utile
26
+ - **3** : Moyennement utile
27
+ - **4** : Utile
28
+ - **5** : Extrêmement utile
29
+ """)
30
+
31
+ st.markdown("---")
32
+
33
+ # Section 2: Charger un dataset
34
+ st.markdown("## Charger un dataset")
35
+
36
+ col1, col2 = st.columns(2)
37
+
38
+ with col1:
39
+ st.markdown("### Option 1 : Fichier local")
40
+ st.markdown("""
41
+ 1. Cliquez sur **"Choisissez une option: Fichier local (.jsonl)"**
42
+ 2. Cliquez sur **"Browse files"**
43
+ 3. Sélectionnez votre fichier `.jsonl`
44
+ 4. Cliquez sur **"Charger le fichier"**
45
+
46
+ **Format attendu du JSONL :**
47
+ ```json
48
+ {"anchor": "code...", "positive": "feedback..."}
49
+ {"anchor": "code...", "positive": "feedback..."}
50
+ ```
51
+
52
+ Chaque ligne doit contenir au minimum :
53
+ - `anchor` ou `code` : le code source
54
+ - `positive` : le feedback à évaluer
55
+ """)
56
+
57
+ with col2:
58
+ st.markdown("### Option 2 : HuggingFace Hub")
59
+ st.markdown("""
60
+ 1. Cliquez sur **"Choisissez une option: HuggingFace Hub"**
61
+ 2. Entrez le nom du dataset (ex: `username/dataset-name`)
62
+ 3. Choisissez le split (généralement `train`)
63
+ 4. Cliquez sur **" Charger depuis HF"**
64
+
65
+ **Exemples de datasets :**
66
+ - `username/my-feedback-dataset`
67
+ - `organization/code-feedbacks-v1`
68
+
69
+ Le dataset doit être public ou vous devez être authentifié
70
+ """)
71
+
72
+ st.markdown("---")
73
+
74
+ # Section 3: Navigation
75
+ st.markdown("## Navigation entre les exemples")
76
+
77
+ st.markdown("""
78
+ ### Boutons de navigation
79
+ - **< Précédent** : Revenir à l'exemple précédent
80
+ - **Suivant >** : Passer à l'exemple suivant
81
+ - **Compteur central** : Affiche votre position (ex: "Exemple 5 / 100")
82
+
83
+ ### Navigation rapide
84
+ - **Aller au prochain non-scoré** : Saute directement au prochain exemple sans score
85
+ - **Aller à l'exemple N** : Entrez un numéro et cliquez sur "Aller" pour y accéder directement
86
+
87
+ **Astuce** : Utilisez les boutons Précédent/Suivant pour naviguer rapidement
88
+ """)
89
+
90
+ st.markdown("---")
91
+
92
+ # Section 4: Scoring
93
+ st.markdown("## Noter un feedback")
94
+
95
+ st.markdown("""
96
+ ### 1. Lisez le code
97
+ Le code source s'affiche en haut avec coloration syntaxique.
98
+
99
+ ### 2. Lisez le feedback
100
+ Le feedback à évaluer s'affiche dans un encadré vert.
101
+
102
+ ### 3. Attribuez un score
103
+ Déplacez le **slider de 0 à 5** selon votre évaluation :
104
+
105
+ | Score | Signification | Quand l'utiliser |
106
+ |-------|---------------|------------------|
107
+ | **0** | Pas utile / Incorrect | Le feedback est faux, inutile ou hors sujet |
108
+ | **1** | Très peu utile | Le feedback est vague ou très générique |
109
+ | **2** | Peu utile | Le feedback manque de précision ou de profondeur |
110
+ | **3** | Moyennement utile | Le feedback est correct mais sans détails |
111
+ | **4** | Utile | Le feedback est pertinent et bien expliqué |
112
+ | **5** | Extrêmement utile | Le feedback est excellent, précis et actionnable |
113
+
114
+ ### 4. Ajoutez un commentaire (optionnel)
115
+ Cliquez sur **"Ajouter un commentaire"** pour justifier votre score.
116
+ Utile pour :
117
+ - Expliquer un score particulier
118
+ - Noter des détails spécifiques
119
+ - Documenter votre raisonnement
120
+ """)
121
+
122
+ st.markdown("---")
123
+
124
+ # Section 5: Progression
125
+ st.markdown("## Suivre votre progression")
126
+
127
+ st.markdown("""
128
+ ### Métriques en temps réel
129
+ - **Total** : Nombre total d'exemples dans le dataset
130
+ - **Scorés** : Nombre d'exemples que vous avez notés
131
+ - **Restants** : Nombre d'exemples non scorés
132
+ - **Progression** : Pourcentage d'avancement
133
+
134
+ ### Barre de progression
135
+ Une barre visuelle vous montre votre avancement global.
136
+
137
+ ### Statistiques des scores
138
+ Quand vous avez noté au moins un exemple, vous verrez :
139
+ - **Score moyen** : Moyenne de tous vos scores
140
+ - **Score le plus fréquent** : Le score que vous utilisez le plus
141
+ - **Graphique de distribution** : Répartition de vos scores
142
+
143
+ Ces statistiques vous aident à voir si vous êtes cohérent dans vos notations
144
+ """)
145
+
146
+ st.markdown("---")
147
+
148
+ # Section 6: Export
149
+ st.markdown("## Exporter vos scores")
150
+
151
+ st.markdown("""
152
+ ### Quand exporter ?
153
+ - À la fin de votre session de notation
154
+ - Régulièrement pour sauvegarder votre travail
155
+ - Quand vous voulez partager vos résultats
156
+
157
+ ### Comment exporter ?
158
+ 1. Scrollez jusqu'à la section **"Export"**
159
+ 2. Vérifiez le nombre d'items à exporter
160
+ 3. Cliquez sur **"Télécharger JSONL"**
161
+ 4. Le fichier sera téléchargé : `feedback_scores.jsonl`
162
+
163
+ ### Format du fichier exporté
164
+ ```json
165
+ {
166
+ "code": "code source...",
167
+ "feedback": "le feedback évalué...",
168
+ "score": 4,
169
+ "comment": "votre commentaire...",
170
+ "original_index": 42
171
+ }
172
+ ```
173
+
174
+ ### Aperçu de l'export
175
+ Cliquez sur **"Aperçu Export (5 premiers)"** pour voir à quoi ressemblera votre fichier.
176
+
177
+ **Important** : Les scores sont automatiquement enregistrés, mais téléchargez régulièrement votre fichier JSONL !
178
+ """)
179
+
180
+ st.markdown("---")
181
+
182
+ # Section 7: Actions avancées
183
+ st.markdown("## Actions avancées")
184
+
185
+ st.markdown("""
186
+ ### Réinitialiser tous les scores
187
+ **Attention** : Cette action supprime TOUS vos scores !
188
+ 1. Cliquez sur **"Réinitialiser tous les scores"**
189
+ 2. Un avertissement apparaît
190
+ 3. Cliquez à nouveau pour confirmer
191
+
192
+ Pensez à exporter avant de réinitialiser !
193
+
194
+ ### Modifier un score existant
195
+ Vous pouvez revenir sur un exemple déjà noté et changer le score.
196
+ Le nouveau score remplacera automatiquement l'ancien.
197
+ """)
198
+
199
+ st.markdown("---")
200
+
201
+ # Section 8: Bonnes pratiques
202
+ st.markdown("## ✅ Bonnes pratiques")
203
+
204
+ col1, col2 = st.columns(2)
205
+
206
+ with col1:
207
+ st.markdown("""
208
+ ### Pour une notation cohérente
209
+ - Lisez d'abord plusieurs exemples avant de commencer
210
+ - Définissez vos critères de notation
211
+ - Utilisez les commentaires pour les cas limites
212
+ - Relisez régulièrement vos premiers scores
213
+ - Vérifiez votre distribution de scores
214
+ """)
215
+
216
+ with col2:
217
+ st.markdown("""
218
+ ### Pour éviter les erreurs
219
+ - Exportez régulièrement (toutes les 50 notations)
220
+ - N'utilisez pas "Réinitialiser" par erreur
221
+ - Lisez bien le code ET le feedback
222
+ - Faites des pauses régulières
223
+ - En cas de doute, mettez 3 et commentez
224
+ """)
225
+
226
+ st.markdown("---")
227
+
228
+ # Section 9: FAQ
229
+ st.markdown("## Questions fréquentes")
230
+
231
+ with st.expander("Mes scores sont-ils sauvegardés automatiquement ?"):
232
+ st.markdown("""
233
+ **Oui !** Chaque score est enregistré dès que vous bougez le slider.
234
+
235
+ Cependant, ils sont stockés temporairement dans l'application.
236
+ **Pensez à télécharger votre fichier JSONL** régulièrement pour ne pas perdre votre travail.
237
+ """)
238
+
239
+ with st.expander("Puis-je fermer l'application et revenir plus tard ?"):
240
+ st.markdown("""
241
+ **Attention** : Si vous fermez le navigateur ou redémarrez Docker, vos scores seront perdus.
242
+
243
+ **Solution** : Téléchargez votre fichier JSONL avant de fermer, et rechargez-le à votre retour.
244
+ """)
245
+
246
+ with st.expander("Comment gérer les feedbacks ambigus ?"):
247
+ st.markdown("""
248
+ Pour les feedbacks difficiles à évaluer :
249
+ 1. Mettez un score **3** (moyennement utile)
250
+ 2. Ajoutez un **commentaire** expliquant pourquoi c'est ambigu
251
+ 3. Continuez et revenez-y plus tard si besoin
252
+ """)
253
+
254
+ with st.expander("Que faire si un feedback est partiellement correct ?"):
255
+ st.markdown("""
256
+ Utilisez l'échelle graduée :
257
+ - Partiellement incorrect → **1-2**
258
+ - Partiellement utile → **2-3**
259
+ - Majoritairement utile avec petits défauts → **3-4**
260
+
261
+ Le score **3** est parfait pour "c'est correct mais sans plus".
262
+ """)
263
+
264
+ with st.expander("Le dataset ne se charge pas ?"):
265
+ st.markdown("""
266
+ Vérifiez :
267
+ - Le fichier est bien au format `.jsonl`
268
+ - Chaque ligne contient `{"anchor": "...", "positive": "..."}`
269
+ - Le fichier n'est pas corrompu
270
+ - Pour HuggingFace : le dataset existe et est accessible
271
+ """)
272
+
273
+ with st.expander("Comment partager mes résultats ?"):
274
+ st.markdown("""
275
+ 1. Téléchargez votre fichier JSONL
276
+ 2. Envoyez-le par email, Slack, Drive, etc.
277
+ 3. Le fichier contient tous vos scores et commentaires
278
+
279
+ Le fichier est lisible et peut être rechargé dans l'application.
280
+ """)
281
+
282
+ st.markdown("---")
283
+
284
+ # Section 10: Raccourcis
285
+ st.markdown("## ⌨Raccourcis et astuces")
286
+
287
+ st.markdown("""
288
+ ### Navigation rapide
289
+ - Utilisez **Tab** pour naviguer entre les boutons
290
+ - **Espace** ou **Entrée** pour cliquer sur un bouton
291
+ - Cliquez directement sur le slider pour changer le score rapidement
292
+
293
+ ### Workflow efficace
294
+ 1. **Première passe** : Notez rapidement tous les exemples évidents
295
+ 2. **Deuxième passe** : Revenez sur les cas ambigus avec "Aller au prochain non-scoré"
296
+ 3. **Révision** : Vérifiez vos statistiques et ajustez si besoin
297
+ 4. **Export** : Téléchargez votre travail
298
+
299
+ ### Organisation
300
+ - Notez par sessions de 20-50 exemples
301
+ - Exportez à chaque fin de session
302
+ - Nommez vos exports : `scores_session1.jsonl`, `scores_session2.jsonl`
303
+ """)
304
+
305
+ st.markdown("---")
306
+
307
+ st.success("Vous êtes maintenant prêt à noter des feedbacks efficacement !")
308
+ st.caption("Pour toute question, consultez cette page d'aide ou contactez matis.codjia@epita.fr")
frontend/styles.py ADDED
@@ -0,0 +1,132 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Custom CSS styling
3
+ """
4
+
5
+ import streamlit as st
6
+
7
+
8
+ def apply_custom_css():
9
+ """Apply custom CSS styling to the Streamlit app with dark mode support"""
10
+ st.markdown("""
11
+ <style>
12
+ /* Clean typography */
13
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
14
+
15
+ * {
16
+ font-family: 'Inter', sans-serif;
17
+ }
18
+
19
+ /* Main header - Light & Dark */
20
+ .main-header {
21
+ font-size: 2.8rem;
22
+ font-weight: 700;
23
+ margin-bottom: 0.5rem;
24
+ letter-spacing: -0.02em;
25
+ }
26
+
27
+ .subtitle {
28
+ font-size: 1rem;
29
+ font-weight: 400;
30
+ margin-bottom: 2rem;
31
+ }
32
+
33
+ /* Clean buttons - Adapt to theme */
34
+ .stButton > button {
35
+ border-radius: 8px;
36
+ font-weight: 500;
37
+ transition: all 0.2s;
38
+ }
39
+
40
+ .stButton > button:hover {
41
+ border-color: #3b82f6;
42
+ box-shadow: 0 4px 12px rgba(59,130,246,0.15);
43
+ transform: translateY(-1px);
44
+ }
45
+
46
+ .stButton > button:active {
47
+ transform: translateY(0);
48
+ }
49
+
50
+ /* Primary button */
51
+ button[kind="primary"] {
52
+ background: #3b82f6 !important;
53
+ color: white !important;
54
+ border: none !important;
55
+ }
56
+
57
+ button[kind="primary"]:hover {
58
+ background: #2563eb !important;
59
+ }
60
+
61
+ /* Metrics */
62
+ [data-testid="stMetricValue"] {
63
+ font-size: 2rem;
64
+ font-weight: 600;
65
+ }
66
+
67
+ /* Section headers - Light mode */
68
+ [data-theme="light"] .section-header {
69
+ font-size: 1.25rem;
70
+ font-weight: 600;
71
+ color: #1a1a1a;
72
+ margin: 1.5rem 0 1rem 0;
73
+ padding-bottom: 0.5rem;
74
+ border-bottom: 2px solid #e5e7eb;
75
+ }
76
+
77
+ /* Section headers - Dark mode */
78
+ [data-theme="dark"] .section-header {
79
+ font-size: 1.25rem;
80
+ font-weight: 600;
81
+ color: #f3f4f6;
82
+ margin: 1.5rem 0 1rem 0;
83
+ padding-bottom: 0.5rem;
84
+ border-bottom: 2px solid #374151;
85
+ }
86
+
87
+ /* Sidebar - Light mode */
88
+ [data-theme="light"] [data-testid="stSidebar"] {
89
+ background: #fafafa;
90
+ border-right: 1px solid #e5e7eb;
91
+ }
92
+
93
+ /* Sidebar - Dark mode */
94
+ [data-theme="dark"] [data-testid="stSidebar"] {
95
+ background: #1f2937;
96
+ border-right: 1px solid #374151;
97
+ }
98
+
99
+ /* Info boxes */
100
+ .stAlert {
101
+ border-radius: 8px;
102
+ border-left: 3px solid;
103
+ }
104
+
105
+ /* Feedback block - Light mode */
106
+ .feedback-block {
107
+ padding: 1.25rem;
108
+ border-radius: 8px;
109
+ border-left: 3px solid #10b981;
110
+ }
111
+
112
+ [data-theme="light"] .feedback-block {
113
+ background: linear-gradient(to bottom right, #ecfdf5, #f0fdf4);
114
+ border: 1px solid #d1fae5;
115
+ color: #064e3b;
116
+ }
117
+
118
+ /* Feedback block - Dark mode */
119
+ [data-theme="dark"] .feedback-block {
120
+ background: linear-gradient(to bottom right, #064e3b, #065f46);
121
+ border: 1px solid #065f46;
122
+ color: #d1fae5;
123
+ }
124
+
125
+ /* Center text style */
126
+ .center-text {
127
+ text-align: center;
128
+ padding: 10px;
129
+ font-weight: bold;
130
+ }
131
+ </style>
132
+ """, unsafe_allow_html=True)
requirements.txt CHANGED
@@ -1,3 +1,5 @@
1
- altair
2
- pandas
3
- streamlit
 
 
 
1
+ streamlit==1.29.0
2
+ datasets==2.16.1
3
+ pandas==2.1.4
4
+ numpy==1.26.2
5
+ huggingface_hub>=0.20.0