# 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 : - `/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 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: 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 (`` 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 | 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 ```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 :** - `