Files
nav-carte/V2-cadrage/E-spec-frontend.md
2026-04-28 14:00:05 +02:00

1160 lines
50 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# NAV V2 — Spec front-end
Date : 2026-04-14
Auteur : ATIS agent
Version : 1.0
---
## Sommaire
```
1. Wireframes ASCII
1.1 Page accueil desktop
1.2 Page accueil mobile
1.3 Page fiche /fiche/[id] desktop
1.4 Page fiche /fiche/[id] mobile
1.5 Bottom-sheet chatbot mobile (ouvert)
1.6 Bottom-sheet chatbot desktop (ouvert)
1.7 Modale formulaire "Ajouter une fiche"
1.8 Bandeau bas
2. Routes Nuxt
3. Composants à créer
4. States & data flow
5. Responsive breakpoints
6. Détails chatbot (bottom-sheet)
7. Bandeau bas : données affichées
8. Page fiche détaillée
9. Formulaire "Ajouter une fiche"
10. Accessibilité & sobriété
```
---
## 1. Wireframes ASCII
### 1.1 Page accueil — desktop (> 1024px)
```
┌──────────────────────────────────────────────────────────────────────────┐
│ NAV [Contribuer une fiche +] │
│ Navigateur Architecture │
├───────────────────┬──────────────────────────────────────────────────────┤
│ SIDEBAR (320px) │ CARTE CENTRALE │
│ │ │
│ [ Recherche 🔍]│ ┌─────────────────────────────────────────────────┐ │
│ │ │ [Métropole] [Outre-mer ▼] Carte Leaflet│ │
│ ── ÉCHELLE ── │ │ │ │
│ ○ National │ │ · point carte · point │ │
│ ● Régional │ │ · point · │ │
│ ○ Départemental │ │ · point sélectionné ● │ │
│ ○ Local │ │ · · │ │
│ │ │ · · │ │
│ ── FONCTION ── │ │ │ │
│ [Juridique ×] │ │ [ + ] [-] [⟳] (OSM © contrib.) │ │
│ [Technique ×] │ └─────────────────────────────────────────────────┘ │
│ [Économique ] │ │
│ [Administratif ] │ Résultats : 23 fiches │
│ [Chantier ] │ ┌──────────────────┐ ┌──────────────────┐ │
│ [Comptabilité ] │ │ FicheCard │ │ FicheCard │ │
│ [Prospection ] │ │ Nom organisation │ │ Nom organisation │ │
│ [RH ] │ │ Échelle · Fonct. │ │ Échelle · Fonct. │ │
│ [Santé mentale ] │ │ Ville │ │ Ville │ │
│ │ └──────────────────┘ └──────────────────┘ │
│ 23 résultats │ │
├───────────────────┴──────────────────────────────────────────────────────┤
│ BandeauBas │
│ Ce mois-ci : 0,12€ / 8K tokens / ~0,001 kWh [♥ Soutenir NAV] Semaine : 3 fiches / 12 requêtes │
└──────────────────────────────────────────────────────────────────────────┘
[🤖 Aide IA] ← BD
```
### 1.2 Page accueil — mobile (< 768px)
```
┌─────────────────────────────┐
│ NAV [≡] │
├─────────────────────────────┤
│ [Métropole] [Outre-mer ▼] │
├─────────────────────────────┤
│ │
│ Carte Leaflet │
│ (plein écran, ~60vh) │
│ │
│ · point · point │
│ · ●sélect. │
│ · · │
│ · │
│ [+] [-] (OSM ©) │
│ │
├─────────────────────────────┤
│ [Filtres ▼] 23 résultats │
├─────────────────────────────┤
│ ┌─────────────────────────┐│
│ │ FicheCard ││
│ │ Nom organisation ││
│ │ [Régional] [Juridique] ││
│ │ Paris ││
│ └─────────────────────────┘│
│ ┌─────────────────────────┐│
│ │ FicheCard ││
│ └─────────────────────────┘│
│ ┌─────────────────────────┐│
│ │ FicheCard ││
│ └─────────────────────────┘│
│ │
├─────────────────────────────┤
│ Ce mois-ci : 0,12€ [♥ Don] │
└─────────────────────────────┘
[🤖] ← BD
```
**Drawer sidebar mobile** — déclenché par [≡] :
```
┌─────────────────────────────┐
│ ✕ Filtres │
│ │
│ Recherche : [ ] │
│ │
│ ÉCHELLE │
│ ○ National │
│ ● Régional │
│ ○ Départemental │
│ ○ Local │
│ │
│ FONCTION (15) │
│ ☑ Juridique ☐ Technique │
│ ☐ Économique ☐ Admin │
│ ☐ Chantier ☐ Compta │
│ ☐ Prospection ☐ RH │
│ ☐ Santé mentale │
│ │
│ [ Appliquer les filtres ] │
└─────────────────────────────┘
```
### 1.3 Page fiche `/fiche/[id]` — desktop
```
┌──────────────────────────────────────────────────────────────────────────┐
│ NAV [Contribuer une fiche +] │
├──────────────────────────────────────────────────────────────────────────┤
│ │
│ ← Retour carte nav.trans-former.fr/fiche/[id] │
│ │
│ ┌──────────────────────────────────────────┬──────────────────────────┐ │
│ │ HEADER FICHE │ Mini-carte (250px) │ │
│ │ Nom de l'organisation │ │ │
│ │ [Régional] [Juridique] [Technique] │ · localisation │ │
│ │ ↳ Paris | Site : architectes-idf.org │ │ │
│ │ │ (Leaflet, zoom 10, │ │
│ │ Description courte (contributeur) │ non interactif) │ │
│ │ ────────────────────────────────── │ │ │
│ │ Description enrichie IA └──────────────────────────┘ │
│ │ (scraping + synthèse Mistral) │ │
│ │ │ │
│ │ Points clés : │ │
│ │ • [point clé 1] │ │
│ │ • [point clé 2] │ │
│ │ • [point clé 3] │ │
│ │ │ │
│ │ [→ Visiter le site] │ │
│ └───────────────────────────────────────────────────────────────────────┘ │
│ │
│ COMMENTAIRES (3) │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ [Avatar] Prénom A. — 2026-03-01 │ │
│ │ "Très utile pour les questions de droit du travail en IDF." │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ [Avatar] Prénom B. — 2026-03-14 │ │
│ │ "Permanences juridiques réactives, réponse en 48h." │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ Ajouter un commentaire : │
│ [ ] (max 500 car.) │
│ Pseudo (optionnel) : [ ] [Envoyer] │
│ → Vos commentaires sont filtrés par une IA avant publication. │
│ │
├──────────────────────────────────────────────────────────────────────────┤
│ BandeauBas │
│ Ce mois-ci : 0,12€ / 8K tokens [♥ Soutenir NAV] 3 fiches / 12 req │
└──────────────────────────────────────────────────────────────────────────┘
[🤖 Aide IA]
```
### 1.4 Page fiche `/fiche/[id]` — mobile
```
┌─────────────────────────────┐
│ ← Retour NAV [≡] │
├─────────────────────────────┤
│ Nom de l'organisation │
│ [Régional] [Juridique] │
│ [Technique] │
│ Paris | architectes-idf.fr │
├─────────────────────────────┤
│ Mini-carte (200px height) │
│ · localisation │
├─────────────────────────────┤
│ Description courte │
│ ───────────────────────── │
│ Description enrichie IA │
│ │
│ Points clés : │
│ • point clé 1 │
│ • point clé 2 │
│ • point clé 3 │
│ │
│ [→ Visiter le site] │
├─────────────────────────────┤
│ COMMENTAIRES (3) │
│ ───────────────────────── │
│ Prénom A. — 2026-03-01 │
│ "Très utile pour les..." │
│ ───────────────────────── │
│ Ajouter un commentaire : │
│ [ ] │
│ [Envoyer] │
├─────────────────────────────┤
│ Ce mois-ci : 0,12€ [♥ Don]│
└─────────────────────────────┘
[🤖] ← BD
```
### 1.5 Bottom-sheet chatbot — mobile (ouvert)
```
┌─────────────────────────────┐
│ (carte visible en fond) │
│ (opacifiée 30%) │
│ │
│ │
├─────────────────────────────┤ ← drag handle
│ 🤖 Aide IA ✕ Fermer │
│ │
│ ┌─────────────────────────┐│
│ │ Ce chatbot fonctionne ││
│ │ sur un serveur EU ││
│ │ souverain (Mistral FR, ││
│ │ zéro rétention). ││
│ │ ││
│ │ Formule ta requête : ││
│ │ • Besoin : [ce que...] ││
│ │ • Thématique : [...] ││
│ │ • Lieu : [région/ville] ││
│ │ ││
│ │ Exemple : "Salarié ││
│ │ d'agence, litige ││
│ │ employeur, juridique ││
│ │ droit du travail, IDF." ││
│ └─────────────────────────┘│
│ │
│ ┌─────────────────────────┐│
│ │ (historique messages) ││
│ │ ││
│ │ ││
│ └─────────────────────────┘│
│ │
│ [ Écris ta question... ] │
│ [ Envoyer →] │
└─────────────────────────────┘
```
### 1.6 Bottom-sheet chatbot — desktop (ouvert)
```
┌──────────────────────────────────────────────────────────────────────────┐
│ (carte + sidebar en fond, opacifiée 20%) │
│ │
│ │
├──────────────────────────────────────────┬───────────────────────────────┤
│ │ 🤖 Aide IA ✕ │
│ │ │
│ │ ┌─────────────────────────┐ │
│ │ │ Ce chatbot fonctionne │ │
│ │ │ sur un serveur EU │ │
│ │ │ souverain (Mistral FR, │ │
│ │ │ zéro rétention). │ │
│ │ │ │ │
│ │ │ Formule ta requête : │ │
│ │ │ • Besoin : │ │
│ │ │ • Thématique : │ │
│ │ │ • Lieu : │ │
│ │ └─────────────────────────┘ │
│ │ │
│ │ (historique messages) │
│ │ │
│ │ [ Écris ta question... ] │
│ │ [ Envoyer →] │
├──────────────────────────────────────────┴───────────────────────────────┤
│ BandeauBas │
└──────────────────────────────────────────────────────────────────────────┘
```
Note : sur desktop, le chatbot s'ouvre en panneau latéral droit (400px) plutôt qu'en bottom-sheet plein écran. Même logique UX, adaptation de surface.
### 1.7 Modale formulaire "Ajouter une fiche"
```
┌──────────────────────────────────────────────────────────────┐
│ Proposer une ressource ✕ Fermer │
├──────────────────────────────────────────────────────────────┤
│ │
│ Nom de l'organisation * │
│ [ ] │
│ │
│ URL du site (optionnel, recommandé) │
│ [ https://... ] │
│ │
│ Description courte * (max 200 caractères) │
│ [ │
│ ] │
│ 0/200 │
│ │
│ Échelle * (une seule) │
│ ○ National ○ Régional ○ Départemental ○ Local │
│ │
│ Fonctions * (1 à 5, l'ordre de clic = priorité) │
│ ☐ Juridique ☐ Technique ☐ Économique │
│ ☐ Administratif ☐ Chantier ☐ Comptabilité │
│ ☐ Prospection ☐ RH ☐ Santé mentale │
│ │
│ Territoire * │
│ ☑ Métropole ☐ Guadeloupe ☐ Martinique │
│ ☐ Guyane ☐ Réunion ☐ Mayotte │
│ │
│ Ville (pour géolocalisation) * │
│ [ ] │
│ │
│ Votre email (optionnel — pour le suivi de modération) │
│ [ ] │
│ │
│ ──────────────────────────────────────────────────────── │
│ [ Annuler ] [ Proposer la fiche → ] │
└──────────────────────────────────────────────────────────────┘
```
**Message post-submit** (remplace le contenu de la modale) :
```
┌──────────────────────────────────────────────────────────────┐
│ Merci ! ✕ Fermer │
├──────────────────────────────────────────────────────────────┤
│ │
│ Ta fiche est en cours de traitement. │
│ │
│ Une IA va scraper le site et enrichir la description. │
│ Jules (et bientôt une équipe de modération) valide │
│ sous 7 jours. │
│ │
│ Tu peux suivre l'avancement ici : │
│ nav.trans-former.fr/suivi/[token-temp] │
│ │
│ [ Fermer ] │
└──────────────────────────────────────────────────────────────┘
```
### 1.8 Bandeau bas
```
┌──────────────────────────────────────────────────────────────────────────┐
│ Ce mois-ci : 0,12€ consommés [♥ Soutenir NAV] Cette semaine : │
│ 8 423 tokens · ~0,001 kWh 3 nouvelles fiches │
│ 12 requêtes chatbot │
└──────────────────────────────────────────────────────────────────────────┘
```
Sur mobile (< 768px) : bandeau réduit à 1 ligne, seul le bouton don et le résumé très court.
```
┌─────────────────────────────┐
│ 0,12€ · 8K tok [♥ Don] │
└─────────────────────────────┘
```
---
## 2. Routes Nuxt
```
/ → page accueil (carte + sidebar + liste fiches)
/fiche/[id] → page fiche détaillée (SSR, OG meta, partage)
/a-propos → page souveraineté + gouvernance + crédits
/contribuer → formulaire soumission fiche (page dédiée mobile) ou
redirige vers / avec ouverture modale (desktop)
/suivi/[token] → page de suivi statut modération (token temp)
/api/search → POST : recherche IA (chatbot) → réponse Mistral Small
/api/submit → POST : soumission nouvelle fiche → NocoDB + worker
/api/comment → POST : ajout commentaire → filtre éthique → NocoDB
/api/stats → GET : récupération données bandeau bas (stats_usage)
/api/geocode → POST : ville → coordonnées (via Nominatim, gratuit)
```
Notes :
- `/contribuer` route duale : en mobile elle rend une page dédiée pour l'UX formulaire ; en desktop elle redirige vers `/` avec un query param `?contribute=1` qui ouvre la modale automatiquement.
- `/suivi/[token]` : page légère affichant le statut de la fiche soumise (en attente / enrichie IA / validée / rejetée).
- Toutes les routes `/api/*` sont des server routes Nuxt (dossier `server/api/`).
---
## 3. Composants à créer
### 3.1 Composants de layout
**`AppHeader.vue`**
- Rôle : barre de navigation principale (logo + bouton "Contribuer")
- Props : aucune (état global géré par store)
- Slots : aucun
- Note : sticky, z-index au-dessus de la sidebar
**`BandeauBas.vue`**
- Rôle : bandeau bas fixe avec stats coûts/tokens/carbone, bouton don, stats activité
- Props : `stats: StatsUsage` (objet issu de `/api/stats`)
- Émettre : rien (lecture seule)
- Note : hauteur fixe 48px desktop, 36px mobile ; `position: fixed; bottom: 0`
### 3.2 Sidebar & filtres
**`NavSidebar.vue`**
- Rôle : sidebar gauche avec recherche texte + sélection ÉCHELLE + sélection FONCTION
- Props :
- `modelValue: Filters` (v-model)
- `resultCount: number`
- Émet : `update:modelValue` à chaque changement de filtre
- Contient : `SearchInput`, `EchelleFilter`, `FonctionFilter`
- Note : sur mobile, ce composant est rendu dans `DrawerSidebar`
**`DrawerSidebar.vue`**
- Rôle : enveloppe modale/drawer pour la sidebar en mobile
- Props : `open: boolean`
- Émet : `close`
- Contient : `NavSidebar`
**`EchelleFilter.vue`**
- Rôle : ligne de filtres radio ÉCHELLE (mono-sélection obligatoire)
- Props :
- `modelValue: string | null` (valeur sélectionnée)
- `options: string[]` (National / Régional / Départemental / Local)
- Émet : `update:modelValue`
- Note : le premier click sur une valeur déjà sélectionnée la désélectionne (équivalent "tout")
**`FonctionFilter.vue`**
- Rôle : grille de tags FONCTION (multi-sélection 05, ordre de clic = priorité)
- Props :
- `modelValue: string[]` (tableau ordonné par priorité)
- `options: string[]`
- `maxSelected: number` (défaut: 5)
- Émet : `update:modelValue`
- Note : afficher le rang (1, 2, 3...) sur les tags sélectionnés
**`SearchInput.vue`**
- Rôle : champ de recherche textuelle (filtre côté client ou appel API)
- Props :
- `modelValue: string`
- `placeholder: string`
- Émet : `update:modelValue`, `search` (sur Enter ou debounce 300ms)
### 3.3 Carte
**`NavMap.vue`**
- Rôle : carte Leaflet avec markers, clustering, synchronisation avec filtres sidebar
- Props :
- `orgs: Organisation[]` (fiches filtrées)
- `territoireActif: string` (Métropole | Guadeloupe | Martinique | Guyane | Réunion | Mayotte)
- `highlightedIds: string[]` (fiches à mettre en avant, émises par chatbot)
- `selectedId: string | null`
- Émet :
- `selectOrg(id: string)` — clic sur un marker
- `mapMoved(bounds: LatLngBounds)` — si on veut filtrer par bbox à terme
- Note : utiliser `leaflet.markercluster` pour le clustering des points proches
- Note SSR : Leaflet est client-only (`<ClientOnly>` ou plugin avec `process.client`)
**`TerritoireToggle.vue`**
- Rôle : toggle Métropole / Outre-mer avec sous-onglets DOM-TOM si Outre-mer actif
- Props :
- `modelValue: string` (territoire actif)
- `territoires: string[]` (Métropole + 5 DOM-TOM)
- Émet : `update:modelValue`
- Note : quand Outre-mer sélectionné, afficher un second rang de sous-onglets (Guadeloupe, Martinique, Guyane, Réunion, Mayotte). La carte recentre automatiquement sur le territoire sélectionné.
**`MarkerPopup.vue`**
- Rôle : popup Leaflet affichée au clic sur un marker (mini-fiche)
- Props : `org: Organisation`
- Contient : nom, tags échelle/fonction, lien vers `/fiche/[id]`
- Note : composant Vue rendu dans le popup Leaflet via `teleport` ou mounting manuel
### 3.4 Fiches
**`FicheCard.vue`**
- Rôle : carte de résumé d'une fiche dans la liste sidebar
- Props :
- `org: Organisation`
- `selected: boolean`
- Émet : `select(id: string)`
- Contient : `TagBadge` pour échelle et fonctions
**`TagBadge.vue`**
- Rôle : badge visuel pour un tag (échelle ou fonction)
- Props :
- `label: string`
- `type: 'echelle' | 'fonction'`
- `priority?: number` (rang si fonction prioritaire)
- `active?: boolean`
- Note : couleurs distinctes entre les deux types de tags
**`FicheHeader.vue`**
- Rôle : header de la page fiche (nom + tags + lien retour + meta)
- Props : `org: Organisation`
**`FicheDescription.vue`**
- Rôle : corps de la page fiche (description user + description IA + points clés)
- Props :
- `descriptionUser: string`
- `descriptionIA: string | null`
- `pointsCles: string[]`
- `siteUrl: string | null`
- Note : si `descriptionIA` est null (fiche pas encore traitée), afficher un placeholder "Enrichissement IA en cours..."
**`FicheMiniMap.vue`**
- Rôle : mini-carte Leaflet centrée sur la localisation d'une fiche
- Props :
- `lat: number`
- `lng: number`
- `nom: string`
- Note : non interactif (scrollWheelZoom: false, dragging: false), zoom fixe 10
**`CommentsList.vue`**
- Rôle : liste des commentaires validés d'une fiche
- Props : `comments: Comment[]`
- Contient : `CommentItem`
**`CommentItem.vue`**
- Rôle : affichage d'un commentaire individuel
- Props : `comment: Comment` (pseudo, date, texte)
**`CommentForm.vue`**
- Rôle : formulaire d'ajout de commentaire
- Props : `ficheId: string`
- Émet : `submitted`
- Note : affiche un message de confirmation post-soumission avec mention du filtre IA
### 3.5 Chatbot
**`ChatbotBubble.vue`**
- Rôle : bubble fixe bottom-right déclenchant l'ouverture du chatbot
- Props : `open: boolean`
- Émet : `toggle`
- Note : icône robot + label "Aide IA" sur desktop, icône seule sur mobile
**`ChatbotSheet.vue`**
- Rôle : bottom-sheet chatbot (mobile) / panneau latéral droit (desktop)
- Props :
- `open: boolean`
- `messages: ChatMessage[]`
- `loading: boolean`
- Émet :
- `close`
- `sendMessage(text: string)`
- `highlightOrgs(ids: string[])` — si la réponse IA mentionne des fiches
- Contient : `ChatOnboarding`, `ChatMessageList`, `ChatInput`
**`ChatOnboarding.vue`**
- Rôle : message initial affiché avant la première interaction
- Props : aucune
- Note : texte souveraineté + format de requête recommandé (voir section 6)
**`ChatMessageList.vue`**
- Rôle : liste scrollable des messages échangés
- Props : `messages: ChatMessage[]`
- Note : scroll automatique vers le bas à chaque nouveau message
**`ChatInput.vue`**
- Rôle : champ de saisie + bouton envoi
- Props : `loading: boolean`
- Émet : `send(text: string)`
- Note : Enter pour envoyer, Shift+Enter pour saut de ligne
### 3.6 Formulaire soumission
**`SubmitModal.vue`**
- Rôle : modale d'ajout d'une fiche (desktop) ou page `/contribuer` (mobile)
- Props :
- `open: boolean` (uniquement en mode modale)
- Émet : `close`, `submitted`
- Contient : `SubmitForm`
**`SubmitForm.vue`**
- Rôle : formulaire de soumission d'une nouvelle fiche
- Props : aucune
- Émet : `submitted(submissionId: string)`
- Note : validation côté client avant envoi (champs obligatoires) ; appel `/api/submit`
**`SubmitSuccess.vue`**
- Rôle : message de confirmation post-soumission
- Props : `trackingUrl: string`
- Note : remplace `SubmitForm` dans la modale après succès
### 3.7 Divers
**`OrgList.vue`**
- Rôle : liste des fiches filtrées sous la carte (desktop) ou sous la carte (mobile)
- Props :
- `orgs: Organisation[]`
- `loading: boolean`
- `selectedId: string | null`
- Émet : `select(id: string)`
- Contient : `FicheCard`
---
## 4. States & data flow
### 4.1 Architecture du store
Trois stores Pinia :
```
useFiltersStore → état des filtres sidebar
useOrgsStore → données fiches (fetch NocoDB + réactivité)
useChatbotStore → état chatbot + messages
```
**`useFiltersStore`** :
```typescript
{
searchText: string,
echelle: string | null, // National | Régional | Départemental | Local | null
fonctions: string[], // tableau ordonné (ordre de clic = priorité)
territoire: string, // Métropole | Guadeloupe | ...
}
```
**`useOrgsStore`** :
```typescript
{
orgs: Organisation[], // toutes les fiches validées
filteredOrgs: Organisation[], // calculé (computed) via getters
selectedId: string | null,
loading: boolean,
highlightedIds: string[], // fiches mises en avant par le chatbot
}
```
**`useChatbotStore`** :
```typescript
{
open: boolean,
messages: ChatMessage[],
loading: boolean,
}
```
### 4.2 URL comme source de vérité pour les filtres
Les filtres actifs sont reflétés dans l'URL via query params. Cela permet :
- de partager un lien filtré (ex : `/?echelle=Regional&fonctions=Juridique,Technique`)
- de conserver les filtres au rechargement de page
- de naviguer avec le bouton "retour" du navigateur
Mapping :
```
?q= → searchText
?echelle= → echelle (National | Regional | Departemental | Local)
?fonctions= → fonctions séparées par virgule, ordre = priorité
?territoire= → territoire actif
```
Implémentation : `useRoute` + `useRouter` de Nuxt dans `useFiltersStore`, watchers bidirectionnels store ↔ URL.
### 4.3 Interaction filtres → carte
```
Sidebar (EchelleFilter | FonctionFilter | SearchInput)
↓ update:modelValue
useFiltersStore.update(filters)
↓ watch (computed)
useOrgsStore.filteredOrgs (getter réactif)
↓ prop :orgs
NavMap.vue → re-render markers
OrgList.vue → re-render liste
```
Le filtrage est côté client (toutes les fiches sont chargées au démarrage). Pour des volumes > 500 fiches, envisager un filtrage serveur via `/api/search`.
### 4.4 Chatbot → highlight carte
Quand la réponse du chatbot mentionne des organisations (détection par ID ou nom), l'API `/api/search` renvoie une liste d'IDs. Le chatbot store propage ces IDs :
```
ChatbotSheet émet highlightOrgs(ids)
useChatbotStore → useOrgsStore.setHighlightedIds(ids)
↓ prop :highlightedIds
NavMap.vue → markers concernés passent en surbrillance (icône colorée différente)
OrgList.vue → fiches concernées remontées en haut de liste
```
### 4.5 Sélection d'une fiche
```
Clic marker (NavMap) ──┐
Clic FicheCard (OrgList) ─┤→ useOrgsStore.setSelected(id)
MarkerPopup visible + FicheCard en surbrillance
+ bouton "Voir la fiche" → navigate('/fiche/[id]')
```
---
## 5. Responsive breakpoints
| Breakpoint | Taille | Comportement |
|------------|--------|--------------|
| Mobile | < 768px | Sidebar en drawer (hors-canvas), carte plein écran (~60vh), liste fiches sous la carte, chatbot en bottom-sheet plein largeur, bandeau bas réduit (1 ligne) |
| Tablet | 7681024px | Sidebar réduite (220px), carte occupe le reste, chatbot en bottom-sheet 80% largeur centrée, même layout que desktop mais compressé |
| Desktop | > 1024px | Sidebar fixe 320px, carte prend tout l'espace restant, chatbot en panneau latéral droit 400px (remonte depuis le bas), bandeau bas 3 colonnes |
### Grille Tailwind utilisée
```css
/* Mobile first */
.sidebar { @apply hidden } /* < 768 : drawer */
.map { @apply h-[60vh] w-full }
.org-list { @apply w-full }
/* Tablet */
@media (min-width: 768px) {
.sidebar { @apply block w-[220px] flex-shrink-0 }
.map { @apply flex-1 h-full }
}
/* Desktop */
@media (min-width: 1024px) {
.sidebar { @apply w-[320px] }
}
```
### Hauteur disponible
Le layout global est `100dvh` (dynamic viewport height) pour gérer les barres mobiles. Structure :
```
AppHeader (48px) + [Sidebar | Carte + Liste] (flex-1) + BandeauBas (48px)
```
La carte et la liste scrollent indépendamment dans leur conteneur (overflow-y: auto).
---
## 6. Détails chatbot (bottom-sheet)
### États
```
fermé → ChatbotBubble visible en bas à droite (fixed, z-50)
icône robot + label "Aide IA" (desktop) ou icône seule (mobile)
ouvert → ChatbotSheet monte à 70% de la hauteur écran (mobile)
ou panneau latéral droit 400px (desktop)
ChatbotBubble disparaît (remplacé par le bouton ✕ dans le sheet)
chargement → spinner dans ChatInput pendant la requête Mistral Small
```
### Animation
```css
/* Mobile : slide-up depuis le bas */
.chatbot-sheet {
transition: transform 300ms ease-out;
}
.chatbot-sheet[data-open="false"] {
transform: translateY(100%);
}
.chatbot-sheet[data-open="true"] {
transform: translateY(0);
}
```
Respecter `prefers-reduced-motion` : si activé, supprimer la transition (affichage/masquage instantané).
### Drag handle mobile
Le sheet mobile expose un drag handle en haut (barre grise 40px de large, 4px de haut). Le glisser vers le bas ferme le sheet si le déplacement dépasse 30% de la hauteur du sheet.
### Message d'onboarding
Affiché avant la première question, disparaît après envoi du premier message :
```
Ce chatbot fonctionne sur un serveur européen souverain
(Mistral FR, zéro rétention), conçu sobre en énergie.
Pour m'aider à te répondre efficacement,
formule ta requête ainsi :
• Besoin : [ce que tu cherches]
• Thématique : [juridique / technique / économique / ...]
• Lieu : [région ou ville]
Exemple : "Je suis salarié d'agence, litige avec mon
employeur, besoin conseil juridique droit du travail,
Île-de-France."
```
### Prompt système envoyé à Mistral Small (`/api/search`)
Le prompt système doit :
1. Présenter le contexte (base de ressources pour architectes FR)
2. Injecter les fiches filtrées correspondant à la requête (retrieved via Nemo)
3. Demander de répondre en citant les organisations par ID ou nom exact
4. Indiquer de ne pas inventer d'organisations absentes de la base
La réponse de l'API inclut les IDs des organisations mentionnées dans un champ structuré (JSON) pour alimenter le highlight de la carte.
### Stratégie deux modèles
- **Mistral Nemo** (filtre/worker) : enrichissement de fiches post-soumission, filtre éthique commentaires → coût $0,02/$0,04 par 1M tokens
- **Mistral Small** (chatbot) : réponses conversationnelles → coût $0,20/$0,60 par 1M tokens
---
## 7. Bandeau bas : données affichées
### Table NocoDB à créer : `stats_usage`
| Colonne | Type | Description |
|---------|------|-------------|
| `id` | AutoNumber | clé primaire |
| `period_start` | DateTime | début de la période (mois ou semaine) |
| `period_type` | SingleLineText | `monthly` ou `weekly` |
| `tokens_input` | Number | tokens d'entrée consommés |
| `tokens_output` | Number | tokens de sortie consommés |
| `cost_eur` | Decimal | coût estimé en euros |
| `kwh_estimated` | Decimal | consommation estimée kWh (calcul : ~0,001 kWh / 1000 tokens, à affiner) |
| `chatbot_requests` | Number | nombre de requêtes chatbot |
| `new_fiches` | Number | nouvelles fiches validées |
| `new_comments` | Number | nouveaux commentaires validés |
| `donations_count` | Number | nombre de dons reçus ce mois (Liberapay webhook si disponible) |
### Calcul kWh et carbone
Formule approximative (à afficher avec mention "estimé") :
```
kWh = (tokens_input + tokens_output) / 1 000 000 × 0,5 kWh/1M tokens
(Mistral Nemo sur infrastructure FR, estimation conservative)
CO₂e = kWh × 40 gCO₂e/kWh (mix électrique France, données RTE 2024)
```
### Mise à jour des stats
Chaque appel à `/api/search`, `/api/submit`, `/api/comment` incrémente la table `stats_usage` via un helper serveur. La route `/api/stats` retourne l'agrégat du mois courant et de la semaine courante.
### Affichage desktop (3 colonnes)
```
[Gauche] [Milieu] [Droite]
Ce mois-ci : Cette semaine :
0,12 € consommés [♥ Soutenir NAV] 3 nouvelles fiches
8 423 tokens (→ Liberapay) 12 req. chatbot
~0,004 kWh · ~0,16 gCO₂e 2 commentaires
```
### Affichage mobile (1 ligne compressée)
```
0,12€ · 8K tok [♥ Soutenir] 3 fiches · 12 req
```
---
## 8. Page fiche détaillée (`/fiche/[id]`)
### Données chargées (SSR)
La page est rendue en SSR (Nuxt `useFetch` côté serveur) pour le SEO et le partage.
Données récupérées depuis NocoDB :
```typescript
interface Organisation {
id: string
nom: string
url?: string
description_user: string
description_ia?: string // null si pas encore traité
points_cles?: string[] // array JSON
echelle: string // National | Régional | Départemental | Local
fonctions: string[] // tableau ordonné
territoire: string[] // Métropole + DOM-TOM
ville: string
lat?: number
lng?: number
statut: 'pending' | 'enrichi' | 'valide' | 'rejete'
created_at: string
}
```
### SEO
Balises `useHead` dans la page `/fiche/[id].vue` :
```typescript
useHead({
title: `${org.nom} — NAV, Navigateur Architecture`,
meta: [
{ name: 'description', content: org.description_user.slice(0, 160) },
{ property: 'og:title', content: org.nom },
{ property: 'og:description', content: org.description_user.slice(0, 160) },
{ property: 'og:url', content: `https://nav.trans-former.fr/fiche/${org.id}` },
{ property: 'og:type', content: 'article' },
]
})
```
### Fil d'Ariane / retour carte
Le bouton "← Retour" utilise `router.back()` si l'utilisateur vient de la carte, sinon `navigateTo('/')`. Détecter via `document.referrer` ou un query param `?from=carte`.
### Commentaires
Les commentaires sont chargés côté client (pas SSR) pour ne pas bloquer le rendu initial.
Filtre éthique avant publication :
1. POST `/api/comment` avec le texte
2. Appel Mistral Nemo : vérifier absence de contenu offensant, spam, non-pertinent
3. Si OK → insert NocoDB avec `statut: 'pending'` (Jules valide manuellement dans un premier temps)
4. Si KO → message d'erreur explicatif côté client
Message de confirmation post-commentaire :
```
Merci pour ton commentaire. Il sera publié après validation
(généralement sous 48h).
```
---
## 9. Formulaire "Ajouter une fiche"
### Champs et validation
| Champ | Type | Requis | Validation |
|-------|------|--------|------------|
| `nom` | text | oui | min 3 chars, max 150 chars |
| `url` | url | non | format URL valide si renseigné |
| `description_user` | textarea | oui | min 20 chars, max 200 chars |
| `echelle` | radio | oui | une des 4 valeurs |
| `fonctions` | checkbox | oui | 1 à 5 sélectionnées |
| `territoire` | checkbox | oui | au moins 1 |
| `ville` | text | oui | min 2 chars (géocodage côté serveur) |
| `email` | email | non | format email valide si renseigné |
### Flow post-submit
```
SubmitForm → POST /api/submit
Server : geocode ville → NocoDB insert (statut: 'pending')
Server : trigger worker async → scraping url + enrichissement Mistral Nemo
Réponse : { submissionId, trackingUrl }
SubmitForm → SubmitSuccess (afficher trackingUrl)
```
### Géocodage
L'API `/api/geocode` appelle **Nominatim** (OpenStreetMap, gratuit, usage raisonnable) :
```
GET https://nominatim.openstreetmap.org/search?q={ville}&format=json&limit=1
```
Header User-Agent obligatoire : `NAV/2.0 contact@trans-former.fr`
Fallback : si géocodage échoue, la fiche est stockée sans coordonnées et Jules complète manuellement dans NocoDB.
### Worker d'enrichissement IA
Processus asynchrone déclenché après insert NocoDB :
1. Scraping URL avec `node-fetch` (ou `cheerio` pour extraction propre)
2. Prompt Mistral Nemo :
```
Tu enrichis une fiche d'une ressource pour architectes FR.
Contenu scraped : {contenu}
Description courte saisie : {description_user}
Produis :
1. Description enrichie (max 400 caractères, neutre, factuelle)
2. Points clés : liste de 3 à 5 items (chacun < 80 caractères)
Format JSON :
{ "description_ia": "...", "points_cles": ["...", "..."] }
```
3. Update NocoDB : `description_ia`, `points_cles`, `statut: 'enrichi'`
4. Jules valide dans NocoDB → `statut: 'valide'`
---
## 10. Accessibilité & sobriété
### Accessibilité
**Contraste AA minimum :**
- Tous les textes > 14px : ratio 4,5:1 minimum
- Textes larges (> 18px bold) : ratio 3:1 minimum
- Les tags colorés (TagBadge) : vérifier le contraste texte/fond avec un outil (ex. Colour Contrast Analyser)
**Navigation clavier :**
- Tous les éléments interactifs ont un `tabindex` naturel (pas de `tabindex > 0`)
- Focus visible sur tous les éléments (ne pas supprimer `outline`, styliser avec `ring` Tailwind)
- Le drawer sidebar (mobile) et la modale SubmitModal implémentent le **focus trap** : Tab reste dans le composant ouvert
- Fermeture des modales et drawers sur `Escape`
- Le chatbot est accessible au clavier : Tab pour naviguer dans les messages, Enter pour envoyer
**Semantic HTML :**
- `<nav>` pour AppHeader et NavSidebar
- `<main>` pour le contenu principal
- `<aside>` pour la sidebar
- `<section>` pour les grandes zones de la page fiche
- `<h1>` unique par page, hiérarchie `h2/h3` cohérente
**ARIA :**
- `aria-label` sur les boutons icône (ChatbotBubble, boutons fermeture modales)
- `aria-expanded` sur les filtres accordéon (mobile)
- `aria-live="polite"` sur la zone de résultats (nombre de fiches) et les messages chatbot
- `role="dialog"` + `aria-modal="true"` sur SubmitModal et DrawerSidebar
### Sobriété numérique
**Polices :**
- Utiliser les system fonts : `font-family: system-ui, -apple-system, sans-serif`
- Pas de Google Fonts, pas de chargement de police externe
- Si une police spécifique est requise : self-host avec `font-display: swap`
**Images :**
- Pas d'image décorative hero
- Si logos d'organisations ajoutés à terme : WebP, lazy loading (`loading="lazy"`), dimensions explicites pour éviter le CLS
- Pas d'icônes SVG externes : utiliser une bibliothèque locale (Heroicons via `@heroicons/vue` ou SVG inline)
**JavaScript :**
- Leaflet est lourd (~150KB gzippé) : importer uniquement côté client (`<ClientOnly>` ou `import('leaflet')` dynamique)
- Pas de bibliothèques d'animation lourdes (GSAP, etc.) — transitions CSS suffisent
**`prefers-reduced-motion` :**
```css
@media (prefers-reduced-motion: reduce) {
.chatbot-sheet,
.drawer-sidebar,
.submit-modal {
transition: none;
}
}
```
**Lazy loading des composants lourds :**
- `NavMap.vue` : importé dynamiquement (`defineAsyncComponent`) — ne bloque pas le First Contentful Paint
- `ChatbotSheet.vue` : importé dynamiquement, monté uniquement au premier clic sur la bubble
**Cache des données :**
- Les fiches sont chargées une fois au mount de la page d'accueil, mises en cache dans `useOrgsStore`
- Revalidation : au rechargement de page ou après soumission d'une nouvelle fiche
- Pas de polling — les stats du bandeau se rechargent au mount (pas en temps réel)
**Tuiles cartographiques :**
- Utiliser **OpenStreetMap** (Raster Tiles) via le CDN OpenStreetMap, avec attribution obligatoire
- Envisager à terme un tile server auto-hébergé (Hetzner) si volumes importants — pas nécessaire au lancement
---
## Types TypeScript partagés
À définir dans `types/nav.ts` (utilisé par composants et API routes) :
```typescript
interface Organisation {
id: string
nom: string
url?: string
description_user: string
description_ia?: string
points_cles?: string[]
echelle: 'National' | 'Régional' | 'Départemental' | 'Local'
fonctions: string[]
territoire: string[]
ville: string
lat?: number
lng?: number
statut: 'pending' | 'enrichi' | 'valide' | 'rejete'
created_at: string
}
interface Filters {
searchText: string
echelle: Organisation['echelle'] | null
fonctions: string[]
territoire: string
}
interface ChatMessage {
id: string
role: 'user' | 'assistant'
content: string
timestamp: number
highlightedOrgIds?: string[]
}
interface Comment {
id: string
ficheId: string
pseudo?: string
texte: string
created_at: string
statut: 'pending' | 'valide' | 'rejete'
}
interface StatsUsage {
period_type: 'monthly' | 'weekly'
cost_eur: number
tokens_total: number
kwh_estimated: number
co2_g: number
chatbot_requests: number
new_fiches: number
new_comments: number
}
```
---
## Dépendances Nuxt à installer
```json
{
"dependencies": {
"leaflet": "^1.9.x",
"leaflet.markercluster": "^1.5.x",
"@pinia/nuxt": "^0.5.x",
"@heroicons/vue": "^2.x",
"nuxt": "^3.x"
},
"devDependencies": {
"@types/leaflet": "^1.9.x"
}
}
```
Note : `@mistral-ai/client` n'est **pas** une dépendance front-end — les appels Mistral se font uniquement dans les server routes Nuxt (`server/api/`), jamais côté client.
---
## Ce qui n'est pas dans cette spec (à traiter séparément)
- Authentification / espace modération Jules (NocoDB admin ou interface dédiée)
- Système de notifications email (soumission reçue, fiche validée)
- Webhooks Liberapay pour mettre à jour `donations_count` en temps réel
- Internationalisation (i18n) — non nécessaire au lancement
- Tests end-to-end (Playwright) — à prévoir pour les parcours critiques (soumission, chatbot)
- Monitoring et alertes (Sentry, UptimeRobot) — infrastructure, pas front-end
- Migration des 94 fiches V1 vers le schéma V2 (opération de base de données)