50 KiB
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 (1–5) │
│ ☑ 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 :
/contribuerroute duale : en mobile elle rend une page dédiée pour l'UX formulaire ; en desktop elle redirige vers/avec un query param?contribute=1qui 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 (dossierserver/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 0–5, 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: stringplaceholder: 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 markermapMoved(bounds: LatLngBounds)— si on veut filtrer par bbox à terme
- Note : utiliser
leaflet.markerclusterpour le clustering des points proches - Note SSR : Leaflet est client-only (
<ClientOnly>ou plugin avecprocess.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
teleportou mounting manuel
3.4 Fiches
FicheCard.vue
- Rôle : carte de résumé d'une fiche dans la liste sidebar
- Props :
org: Organisationselected: boolean
- Émet :
select(id: string) - Contient :
TagBadgepour échelle et fonctions
TagBadge.vue
- Rôle : badge visuel pour un tag (échelle ou fonction)
- Props :
label: stringtype: '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: stringdescriptionIA: string | nullpointsCles: string[]siteUrl: string | null
- Note : si
descriptionIAest 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: numberlng: numbernom: 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: booleanmessages: ChatMessage[]loading: boolean
- Émet :
closesendMessage(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
SubmitFormdans 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: booleanselectedId: 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 :
{
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 :
{
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 :
{
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 | 768–1024px | 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
/* 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
/* 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 :
- Présenter le contexte (base de ressources pour architectes FR)
- Injecter les fiches filtrées correspondant à la requête (retrieved via Nemo)
- Demander de répondre en citant les organisations par ID ou nom exact
- 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 :
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 :
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 :
- POST
/api/commentavec le texte - Appel Mistral Nemo : vérifier absence de contenu offensant, spam, non-pertinent
- Si OK → insert NocoDB avec
statut: 'pending'(Jules valide manuellement dans un premier temps) - 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 |
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 :
- Scraping URL avec
node-fetch(oucheeriopour extraction propre) - 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": ["...", "..."] } - Update NocoDB :
description_ia,points_cles,statut: 'enrichi' - 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
tabindexnaturel (pas detabindex > 0) - Focus visible sur tous les éléments (ne pas supprimer
outline, styliser avecringTailwind) - 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érarchieh2/h3cohérente
ARIA :
aria-labelsur les boutons icône (ChatbotBubble, boutons fermeture modales)aria-expandedsur les filtres accordéon (mobile)aria-live="polite"sur la zone de résultats (nombre de fiches) et les messages chatbotrole="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/vueou SVG inline)
JavaScript :
- Leaflet est lourd (~150KB gzippé) : importer uniquement côté client (
<ClientOnly>ouimport('leaflet')dynamique) - Pas de bibliothèques d'animation lourdes (GSAP, etc.) — transitions CSS suffisent
prefers-reduced-motion :
@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 PaintChatbotSheet.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) :
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
{
"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_counten 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)