Compare commits
15 Commits
a1c47002d5
...
feat/aep-r
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
22a7a42206 | ||
|
|
b2fbbcb198 | ||
|
|
110fd58ec2 | ||
|
|
3917717eb1 | ||
|
|
55d7e4de55 | ||
|
|
88716657a2 | ||
|
|
86b95fa18e | ||
|
|
3a07d368f0 | ||
|
|
20d228fde7 | ||
|
|
fd33debf06 | ||
|
|
40b406bd41 | ||
|
|
46f57ae5fe | ||
|
|
b36587cb08 | ||
|
|
89608d894c | ||
|
|
fdd9d02859 |
@@ -1,5 +1,6 @@
|
||||
---
|
||||
type: journal
|
||||
note_J8: Phase 8.G LIVREE 2026-05-14 - voir PILOTE-RAG-PE.md pour details
|
||||
project: NAV V2
|
||||
created: 2026-04-14
|
||||
status: actif
|
||||
@@ -1053,3 +1054,57 @@ Incohérence entre `deploy.sh` (rsync vers `/opt/nav-carte/`) et `nav-carte.serv
|
||||
- **Caching API organisations** — actuellement re-fetch NocoDB à chaque render
|
||||
- **Full-text search côté client** — Fuse.js sur descriptions enrichies
|
||||
- **Mode offline / PWA** — manifest + service worker pour usage terrain
|
||||
|
||||
---
|
||||
|
||||
## Décisions structurantes (mémoire profonde)
|
||||
|
||||
> Archive des décisions structurantes du projet nav-carte (AEP V1/V2/Codev/Carte 3), déchargée hebdo depuis coordo-agent-dev. Tri chronologique inverse (plus récent en haut). Copies verbatim, pas de reformulation.
|
||||
|
||||
### 2026-W19 (décharge 2026-05-13)
|
||||
|
||||
- [W19 — 2026-05-08 19:21] **AEP nav-carte fix mobile batch1+2 LIVE + cause racine chatbot Carte1=Carte2 résolue** (~3h pilote direct, 2 batches successifs, 11 fichiers). **Cause racine identifiée** après 5 cycles de fix précédents infructueux : `/api/chatbot-reseaux` était **404 en prod** (jamais déployé). Le code source nav-carte était correct depuis le début. Rebuild + redeploy = bug résolu (vérifié curl POST sur les 2 endpoints, réponses distinctes). **Batch 1** : hamburger refondu (Jobs/Manifeste/Soutenir), FAB cœur jaune retiré, 3e onglet Graphe mobile sur agences, /a-propos refondue + scroll latéral fixé, page /manifeste créée (texte version page-carto-V1), MissionPopup générique avec slot/props (storageKey, title, ctaLabel) auto-show 1ère visite, filtres Jobs mobile repliables (toggle "Filtres [N]"), FicheModal/V2 décalage `top:76px` mobile pour ne plus mordre header. **Batch 2** : pop-up Carte 2 Réseaux AEP, logo header `Architecture d'Écologie Politique` en clair sur 2 lignes (logo-line-1/2 responsive), onglet "Plateformes B2C" → "Pour archi indépendants", intro pédagogique repliable Jobs (2 onglets / 3 tags / 5 axes éthiques), labels noms structures sur GraphView (D3 append text + halo via `paint-order: stroke`). **Pattern d'opération critique découvert** : Dropbox sync efface .output entre `npm run build` et `tar | ssh` du deploy.sh — 1er deploy batch 2 a uploadé un .output quasi-vide sans erreur visible (HTTP 200 trompeur). Réflexe : `grep "fragment-modif" .output/public/_nuxt/*.js | head` AVANT deploy. **Pattern de communication** : Jules a signalé "modifs pas faites" alors que HTML prod contenait bien les modifs (vérifié curl) — cache navigateur / service worker. Réflexe : si "ça apparaît pas", curl bypass cache AVANT chercher bug, puis demander hard refresh. Git : commit propre + push gitea/main (`yes y | bash deploy.sh` pour skip confirm interactif `.env diff`). Prompt batch 3 dans `0 INBOX/PROMPTS/cascade-megaboum/REPRISE-aep-carto-fix-batch3.md`.
|
||||
|
||||
- [W19 — 2026-05-07 17:18] **AEP Carte 3 "Trouver du taf" — T2 scoring 5 axes + T3/T4 LIVE** : 24 plateformes scorées (7✅/14⚠️/3❌), plateformes-taff.json prod, page /trouver-du-taf complète (filtres, grille 1 col, modal fiche, chatbot FAB Guide IA). Endpoints /api/chatbot-taff (import JSON statique + Mistral Small) + /api/chatbot-reseaux (keyword search 120 structures). ChatbotReseaux.vue créé (standalone Carte 2). useMarkdown.ts inline styles CSS-free. Onglet Jobs nav desktop. Bug dev : cache .nuxt corrompu par agent concurrent → bat rmdir+délai 12s. Chatbot séparation + markdown à vérifier en PROD (pas dev). Menu hamburger mobile Jobs manquant. Branche main.
|
||||
|
||||
- [W19 — 2026-05-07 01:11] **Codev MVP LIVRÉ en prod** — cascade M1→M5 complète (5 agents Sonnet), merge feat/codev-mvp→main, push Gitea. App entraide /codev live avec lock screen, formulaire, graphe D3 force-directed, annuaire table sticky, 2 modes matching (Solution tokenize direct + Alliance Jaccard), mode admin (DELETE fiche), QR code public, panel mobile bottom sheet. Décision build : TOUJOURS depuis C:\tmp\nav-build (Dropbox corrompt cache Vite = 500 prod). Algo fix critique : Solution compare textes besoin↔offre directement (ignore hashtags), évite que les 3 modes donnent le même graphe.
|
||||
|
||||
- [W19 — 2026-05-06 17:11] **MP-TAFF T1b scraping compléments DONE** — BrowserMCP utilisé pour débloquer Trustpilot (7 pages) + instao.fr + francemarches.com. `T1-output-plateformes.json` finalisé : 25 plateformes, 7 avec feedback Trustpilot. Signaux forts T2 : Archionline 2.4/5 (spam + permis PLU non conformes), hemea = courtier déguisé en MOE, TMA = meilleur ratio pros. Prochaine étape : **MP-TAFF T2 scoring 5 axes éthiques**.
|
||||
|
||||
- [W19 — 2026-05-06 13:30] **Cascade dispatchable Codev MVP livrée — 9 fichiers prêts à dispatcher en session Opus dédiée** : Cadrage `/orchestrateur` app entraide / co-développement Jules pour facilitation IRL jeudi 8/05 ~10 amis. 5 intersections tranchées : URL `/codev` (sous-route aep.trans-former.fr pas nouveau domaine), naming "Co-développement"/"Entraide entre pairs", mot de passe partagé `merci`, persistance NocoDB nouvelle table `codev_fiches` (carto vit hebdo, pas effacée), démo route publique `/codev/demo` (10 prénoms factices pour pitch univ). **Démêlage trinôme** (problème mathématique flaggé par Jules) : matching pair-only en MVP, trinômes émergent visuellement du D3 force-directed (triangles dans le graphe), pas de logique trinôme explicite codée — économie combinatoire majeure. 3 modes : Solution (besoin→offre asymétrique avec flèche), Alliance (besoin↔besoin symétrique), Surprise (offre↔offre symétrique). **Algo matching MVP simple** sans IA : Jaccard sur hashtags si présents, sinon Jaccard sur tokens FR (stop-words filtrés) seuil 0.15 — V2 embeddings si scaling. **Réutilisation pattern GraphView.vue** (~700 lignes existant) en simplifié (~200-300 lignes pour CodevGraph) — pas de greenfield. Cascade dans `codev-build/` (même pattern que `aep-communaute-build/`) : INDEX (table+décisions+statut) + META-PROMPT-OPUS (preflight+dispatch séquentiel+1 checkpoint deploy+format récap) + M1 (NocoDB table API + 3 endpoints + runtimeConfig) + M2 (lock+fiche+middleware auth skip /codev+/codev/demo) + M3 (CodevGraph D3 + page carto affichage seul) + M4 (matching 3 modes + boutons + animation force.alpha(0.5).restart()) + M5 split phase 1 (démo+build local+stop) + phase 2 (deploy prod après checkpoint Jules) + FEEDBACK-PASSES (10 risques pré-dispatch corrigés) + PHRASE-LANCEMENT (one-shot pour session Opus). **Patches en cours de session** : M1 fait création table NocoDB en autonomie via API (token NocoDB `e9rU...` déjà dans nav-carte/.env, endpoint `POST /api/v2/meta/bases/{baseId}/tables`), M5 phase 2 sync .env VPS automatiquement (ssh append + restart aep), règle d'or "couper M4/M5 si timing serré" retirée du META car Jules a confirmé scope OK. **1 seul checkpoint Jules** : entre M5 phase 1 (build local 200) et M5 phase 2 (deploy prod). Pattern méga-dispatch consécutif #5 (avant : Simulateur V2 30/04, Méga-cascade AEP V2 30/04, MP-TAFF cascade-megaboum 06/05, AEP V2 graphe PV2-5 micro-itérations 06/05). À dispatcher en session Opus dédiée (jauge propre, ~1h30 cascade attendue + ~5 min action manuelle Jules NocoDB).
|
||||
|
||||
- [W19 — 2026-05-06 21:30→01:45] **AEP V2 graphe interactif PV2-5b/e/f/g + chatbot v2 vivant + decouverte 2 repos imbriqués** : 4 commits vault `feat/aep-v2-cartobifurcation` (062337a sidebar+chatbot intégré, 2adffdf toggle Familles/Pratiques+popover famille, 5d7556a carte unifiée layers superposables+popover hashtag+lisibilité, e1ae1b9 popovers enrichis+FicheFamilleModal). 4 micro-itérations dispatch agent Sonnet en séquentiel, pilote commit lui-même chaque fois (pattern anti-hallucination établi après agent 1 a inventé hash 755d1ef). **Décisions design** : skip PV2-5c bicolores (8/120 structures = effet marginal), toggle PV2-5e exclusif fusionné en layers superposables PV2-5f (intuition Jules : séparation artificielle), Pratiques default OFF perf-friendly (174 noeuds + 640 liens si tout coché), FicheFamilleModal composant dédié réutilisable, skip définitions hashtags (pas de contenu source) → ligne générique "portée par N structures de M familles". **Découverte 2 repos git imbriqués** (vault parent ATIS-IPCJRA branche v2 + sous-repo nav-carte/ branche v1.1 distincte) → expliquait les "hallucinations" branche des agents. Notée dans `ATIS-Dev.md` section "Pièges connus" + réflexe pilote `git rev-parse --show-toplevel` au démarrage. **Chatbot v2** : endpoint v1→v2 dans ChatbotPlaceholder.vue (commit sous-repo 5878c56), vectorize-v2.js renommé en .cjs (incompat ESM type=module), payload Mistral fixed `inputs`→`input`, 120 embeddings générés (3.5MB embeddings-v2.json gitignored), patch vectorSearch.ts process.cwd() au lieu d'import.meta.url (bug Nitro bundle). **Rotation clé Mistral** : nouvelle clé `PXsPUhk...` notée _System/API-credentials.md, appliquée local .env + VPS /opt/aep/.env (backup .env.bak.before-rotation-20260506) + restart aep + smoke test prod chatbot v1 OK. **Doc features graphe** créée : `aep-communaute-build/PV2-5-FEATURES-GRAPHE.md` (briefing agent qui découvre en 30 sec). **Backlog différé** : PV2-5d sous-noeuds projets emblématiques (perf-critique 480 noeuds), définitions hashtags (session écriture éditoriale Jules), décision repo imbriqués (intersection stratégique demain). **Test live chatbot v2 bloqué** par lock Dropbox sur .nuxt/dev → prompt cloture demain `PV2-5h-test-chatbot-v2-local.md`.
|
||||
|
||||
- [W19 — 2026-05-06 02:30] **MP-TAFF V2 cadré + prompt scraping autonome séparé + rename atis-humain** : Brainstorm divergent Jules pour Carte 3 AEP "Trouver du taf en archi". 5 axes scoring éthique AEP validés (Rémunération / Transparence / Pratiques pro / Écologie / Matching) avec définitions + critères + échelles ✅⚠️❌ — c'est le différenciant central vs annuaires neutres. Décisions verrouillées : freelance only V1 (70% archis indé = cible la plus en galère), IA applique scoring (critères validés une fois = pas de validation fiche par fiche), onglet `aep.trans-former.fr` (pas sous-domaine), branche parallèle `feat/aep-taff-v1` (pas attendre merge V2), SEO reporté V2 (skill `/seo-page-aep` à créer). MP-TAFF V2 ~430 lignes avec 2 tours auto-feedback (table décision tag global, format desc IA 5 sections, §risque juridique nominatif, calibrage chatbot 3 questions, préflight conflit branche V2 + i18n). **MP-TAFF-T1-scraping-autonome.md créé** (~270 lignes) — sortable sur PC séparé pour parallélisation pendant qu'ATIS Dev bosse T0/T2/T3+. Pattern routing scraping documenté : Jina d'abord → crawl4ai SPA → BrowserMCP RGPD/auth → manuel flag. Forums commu intégrés : Team.Archi, Reddit r/Architecture FR, presse pro (Le Moniteur, AMC, D'A). Output JSON structuré consommable par T2. 🔒1 simplifié = récap scope synthétique (10 min Jules). Backlog cascade : MP-MENTOR (carte 4 entraide), MP-CROSS (n8n + GitHub OS) restent prêts. **Rename `tara` → `atis-humain`** : skill renommée, routing patché dans `atis-archi.md` ligne 390 et `ATIS-agents-specialises.md` ligne 29. Anciennes refs à Tara la personne (Mediathèque, done.md, ton-jules.md) inchangées.
|
||||
|
||||
- [W19 — 2026-05-05] **AEP V2 PV2-4+5+8 DONE + vue graphique D3** : PV2-4 (887 edges, 480 projets, reseaux-bifurcation.json 847KB). PV2-5 UI (NavMapV2, HashtagFilter, IntentionBanner, FicheModalV2, palette 5 familles, geocodage 118/120, GraphView.vue D3 force-directed). PV2-8 RAG (chatbot-v2.post.ts + vectorize-v2.js). Fixes UI : onglets outremer desktop, sidebar scroll, chips colorees, hashtags repliables, F6 filtre, intention overlay localStorage. EDITO-V2.md cree. 13 commits feat/aep-v2-cartobifurcation. 🔒 PV2-5 checkpoint visuel Jules en attente.
|
||||
|
||||
### 2026-W18 (décharge 2026-05-13)
|
||||
|
||||
- [W18 — 2026-05-03] **AEP V2 PV2-2ter DONE** : 10 emails récupérés. Volet A F2 (amaco/LTE), F4 (toitsdechoix/HPO/HabiterAuvergne/EmmanuelleDucos). Confirmed not public : F3 (LALCA/Sentiers/AOA/METALAB), F4 (unitoit/atelier15/a-tipic/HPF/atcoop). Blocages : rfcp.fr GravityView JS, a-tipic HTTP 400. Volet B F6 : 7 flags + 4 emails (Forensic/Centrala/NBL/Assemble) + 1 new fiche Collectif Etc (contact@collectifetc.com). Seed 122 fiches. Commit `7ce8e12`.
|
||||
|
||||
- [W18 — 2026-05-03] **AEP V2 PV2-2 F1 DONE** : 26 fiches réemploi & filières (14 V1 + 12 nouvelles). Nouvelles : Cycle Up (contact@cycle-up.fr), Backacia (form), Mobius (contact@mobius-corp.com), AD VITAM MATERIAL (reemploi@embuild.be), Cirkla (c/o insitu), CANCAN (contact@collectifcancan.fr), HArquitectes (harquitectes@harquitectes.com), isla (press@isla-architects.com), jdviv BE (EUmies 2026 co-winner), SalvoWEB, B+L Architectes, REFAIR/BDR. 6/12 emails high. Hashtag nouveau proposé : #amo-reemploi (AMO/diagnostic PEMD spécialisés). BrowserMCP off toute la session → Jina only (Reuse Foundation non scrapée, jdviv URL à confirmer). Commit `656cc2d`. **PV2-2 5/5 familles DONE.** → Reste PV2-3+PV2-4.
|
||||
|
||||
- [W18 — 2026-05-03] **AEP V2 PV2-2 F2 DONE** : 36 fiches frugalité & low-tech (22 V1 + 14 nouvelles). Nouvelles : Lacaton&Vassal (Pritzker 2021), Kéré Architecture (mail@kerearchitecture.com), Anna Heringer, CRATerre (secretariat@craterre.org), Les Grands Ateliers, AsTerre (secretariat@asterre.org), RFCP, EnvirobatBDM, NUNC, LAPS, Dorodango, BEES, amàco (contact@amaco.org), Lehm Ton Erde. 4/14 emails high conf. Sources productrices : AsTerre annuaire (19 agences archi identifiées), Pritzker (2 nouvelles), frugalite.org réseau. Blocages : RFCP annuaire JS, lehmtonerde.at 422, OFF laureats non scraped, BrowserMCP instable (3 décos). Commits `8808a35`+`301c3be`. → Reste F1 Réemploi.
|
||||
|
||||
- [W18 — 2026-05-03] **AEP V2 PV2-2 F3 DONE** : 22 fiches architecture sociale & précarités (11 V1 + 11 nouvelles). Nouvelles : PEROU, Plateau Urbain (SCIC), Bellastock, ASF France, Rural Studio, Forensic Architecture, Collectif Parenthèse, WoMa, Fab City Grand Paris, CivicWise. 6/11 emails high conf (PEROU·Bellastock·Rural Studio·Parenthèse·WoMa·CivicWise). Sources : Quatorze /partenaires-new (meilleur hub), YWC lieux (Grands Voisins + Coco Velten), A&P filtres. 4 multi-famille (Bellastock F3+F5, Parenthèse F3+F4, WoMa F3+F4, YWC F3+F4+F5). Commit `d2028f5`. → Reste F1 + F2.
|
||||
|
||||
- [W18 — 2026-05-02 23:23] **AEP V2 PV2-2 F4 DONE** : 20 nouvelles fiches collectifs/écolieux/AMO via Jina (BrowserMCP déco → pivot Jina). 11/20 emails high conf. Structures-clés : RAHP, HPF, Habicoop, Hab-Fab SCIC, Regain SCIC, Coopérative Oasis, Mietshäuser Syndikat. 9 contacts partiels (tel/form) à compléter BrowserMCP. Commit `f54afe3`.
|
||||
|
||||
- [W18 — 2026-05-02 19:51] **AEP V2 PV2-2 F5 DONE** : 15 fiches urbanisme transition via BrowserMCP. 7 emails high (CLER/TEPOS/Coloco/Bas Smets/EP/FNAU/Atelier Georges). Commit `56c93eb`.
|
||||
|
||||
- [W18 — 2026-05-02 18:17] **C3 smoke test + PV2-1 scrape DONE** : C3 = 2 bugs (P0 algo-config.json 404, P1 redirect 301 manquant) + rapport `C3-RAPPORT.md`. PV2-1 = 5/5 emails trouvés via BrowserMCP (Opalis/Frugalité/Quatorze/Tepop/Transition France), commit 6df5b84. Stack BrowserMCP validé pour batch PV2-2. Patcher P0+P1 avant merge master simulateur.
|
||||
|
||||
- [W18 — 2026-04-30 12:00] **AEP Cascade V2 Phase A2+A3 figées + PILOTE-V2 doc pilote vivant** : Session "AEP COMMU V3 suite" /atis-archi puis /atis-dev. SPEC-V2-FEEDBACK-DEV.md livré (faisabilité pipeline 3 passes, email cascade 5 niveaux estim 65-80%, reclaim JWT HS256 30j single-use, grain JSON suffisant si desc_longue 600+ + 3 sources, branches 2 dédiées, pre-flights standardisés, fix BOM). PILOTE-V2.md créé comme **source de vérité vivante** à la racine `aep-communaute-build/` (Jules pilote depuis ce fichier ; tout Opus suivant le lit en premier). 13 prompts PV2-X dans `0 INBOX/PROMPTS/aep-v2-cartobifurcation/` (README + PV2-0 preflight + PV2-1 scrape test 5 fiches + PV2-2 5 agents recherche par famille en parallèle (idée Jules : recoupement multi-famille = signal politique transversalité) + PV2-2bis recoupement + PV2-3 passe2 analyse + PV2-4 passe3 croisements + PV2-5 refonte UI + PV2-6 reclaim + PV2-7 badges statut + PV2-8 RAG coexistence v1+v2 + PV2-9 bandeau regards d'ailleurs + PV2-10 E2E build + PV2-11 batch emails + tri DOM-TOM). NEXT-SESSION-PROMPT-V3.md créé pour relais. **Amendements Jules sur SPEC-V2** : famille 1 "Réemploi & matière" → "Réemploi & filières", AMO ajouté famille 4 (Tepop/Hab-Fab/Habicoop), famille 5 Urbanisme transition gardée fermement (cibler scrape agressif), centres ressources DOM-TOM → carte ressources existante (sauf Caribois praticien direct), pas de cap fiches sur agents recherche, stratégie snowball depuis nodes établis (Frugalité, Opalis, A&P) + crawl collaborateurs/influences/prix/distinctions. Sessions batch nocturnes dimensionnées Claude Max 5h Opus. PV2-0 partiel exécuté : branche `feat/aep-v2-cartobifurcation` créée depuis origin/main (tracking unset = anti-push main accidentel) + BOM UTF-8 retiré de `nav-carte/deploy.sh`. PV2-0 effectif (checkout + structure `nav-carte/V2-cascade/` + hook pre-commit no-BOM + sources-par-famille.md) à faire prochaine session après commit/stash des 830 fichiers pending sur `feat/aep-website-v1.1`.
|
||||
|
||||
- [W18 — 2026-04-30] **Cascade MEGABOUM opérationnelle — 4 MP rédigés + cockpit lisible** : `0 INBOX/PROMPTS/cascade-megaboum/` créé avec `00-COCKPIT-CASCADE.md` (index lisible en 3 min par tout Opus dispatché, format différent de la mégaspec — celle-ci reste lecture profonde optionnelle). 4 méga-prompts prêts à dispatcher : **MP-TAFF** (app trouver du taf B2C/B2B/appels publics, ~5h, étend `aep-communaute-build/`), **MP-MENTOR** (app mentorat-entraide M7-M, ~5h, post-TAFF), **MP-CROSS** (pipeline cross-posting n8n LinkedIn+Substack+Listmonk+@aep.politique + GitHub OS publish skills/lightrag/vps-kit, ~5h, parallèle), **MP-DESIGN** (création agent atis-design, prompt court ~1h via /create-agent + scrape Prisme.one). Brief archive `MP10-manifeste-aep-INFO-BRIEF.md` (chantier déjà lancé par Jules). Chaque MP démarre par CHECKPOINT 0 réflexion faisabilité (l'Opus lit, propose, attend OK Jules avant dispatch). Backlog explicite : page-cerveau Astro from scratch, méga-RAG FRACAS vague 1, atis-philosophe, frontend-slides, rename atis-humain (P1 30 min), Insta @julesneny n8n (Q3). LightRAG VPS DOWN **déclassé** : pas bloqueur P0 semaine si on ne fait pas méga-RAG, devient bloqueur quand on attaquera RAG. Cadence : Jules pilote au fil des jours, 1-2 MP/jour, 2 clusters max simultanés.
|
||||
|
||||
- [W18 — 2026-04-30 02:46] **Méga-cascade V2 AEP Phase A1 figée + 3 agents background livrés** : Session Opus orchestrateur "AEP commu V2". 9 intersections tranchées par Jules en un message (5 familles fusion F4+F5, UX vignette + template carte 1, scope FR+Europe francophone + capture incidente régénératifs hors scope, articulation pensées<->structures reportée V2, passe profonde GO, email champ soft, charte reportée, filtre échelle drop, A3 absorbé A1). SPEC-V2.md figée. 3 agents Sonnet dispatchés en parallèle background : VOIE 2 V1.1 nav-carte (4 commits `feat/aep-v1.1-nav-carte` basée sur feat/aep-pratiques-regeneratives car main pré-V1 ; PA1 DOM-TOM pattern desktop 2 onglets, PA3 bouton Proposer contextuel, PA5 chatbot pratiques régé Mistral, 6/6 bugs E2E M1-M3+L1-L3 corrigés, build Nuxt OK), VOIE 3 website (1 commit `feat/aep-website-v1.1` e95f693 ; PB1 hamburger 4 entrées + stubs /manifeste /ressources /signaler ; **/!\ livré sur renovation-energetique.trans-former.fr - website pro, pas aep.trans-former.fr - à clarifier prochaine session**), PASSE PROFONDE (52 pratiques régé + 99 ressources institutionnelles analysés ; 5 familles confirmées avec garde-fous F5 ; 46 hashtags ; 7 cas limites + 4 hors-grille type "mouvement-manifeste" potentiel ; **226 acteurs candidats enrichissement carte ressources : P1=56 fiches urgentes dont 30 CAUE manquants top dpts + 4 CAUE DOM-TOM + 9 MA régionales + 2 CROA DOM ; constat critique : 0 DOM-TOM + 6/92 CAUE dans carte ressources actuelle**). 5 questions stratégiques remontées pour Phase A2 (Q-PP1 5 vs 6 familles, Q-PP2 type mouvement-manifeste, Q-PP3 F5 différée passe 2, Q-PP4 double-référencement KEBATI/AQUAA/RBD/Envirobat, Q-V3 site cible hamburger). NEXT-SESSION-PROMPT.md pré-écrit pour reprise propre Phase A2 /atis-dev. Pattern méga-dispatch consécutif #4. Tokens : 130k orchestrateur + ~451k délégués sous-agents.
|
||||
|
||||
- [W18 — 2026-04-30 01:42] **Méga-cascade V2 AEP cadrée** : `MEGA-V2.md` master orchestration 3 voies créé. VOIE 1 = V2 conceptuelle (refonte carte écosystème AEP en carte des réseaux de bifurcation, 5-6 familles éditoriales, reclaim email magique, pipeline IA cascade 3 passes, ~75-100 fiches). VOIE 2 = V1.1 nav-carte (items 1+2+4+5+8 ; item 3 absorbé par VOIE 1). VOIE 3 = website astro-site (hamburger + manifeste + ressources). Décisions Jules : `/atis-archi` pilote la spec V2 conceptuelle Phase A1, `/atis-dev` en relai Phase A2. Apports critiques : champ email obligatoire dans le scrape (sans email = pas de reclaim), passe profonde sur fiches existantes (~52+80) pour faire remonter hashtags sous-familles, item 4 (filtre échelle) à questionner, A3 absorbable dans A1, page Manifeste à ajouter au hamburger website. Phrase d'amorce + effort `high` recommandé pour la session Opus dédiée demain. PHRASE-LANCEMENT-OPUS-V2.md marqué SUPERSEDED. 2 briefs INBOX (V2-BRIEF-AGENT-OPUS + V2-RECAP-PROJET) déplacés dans aep-communaute-build/.
|
||||
|
||||
- [W18 — 2026-04-29 11:48] **AEP scrape P1-P7 FAIT** : BrowserMCP (P1 architecture-precarites.fr : 200+ projets, 5 catégories) + Jina (P4 vegetal-e ✅, P6 colorado-architecture ✅, P7 karibati ✅, P3 caribois ✅). P2 archidev bot-protégé + P5 envirobat 422 → consultation manuelle. `scrape-browsermcp.json` créé (7 entrées). Email auteurs architecture-precarites.fr envoyé par Jules. INCLURE : vegetal-e (5/8), envirobat-RE (7/8), karibati (5/8). EXCLURE : caribois (2/8), colorado (3/8).
|
||||
|
||||
- [W18 — 2026-04-29] **AEP V1 E2E PASS** : 5/5 scénarios OK (3 mobile, 3 laptop). Branche `feat/aep-pratiques-regeneratives` prête à merger main. `E2E-RESULTS.md` créé. Bugs mineurs capturés : M1 chips a11y + M2 reset searchbox + M3 floating button + L1 redirection.
|
||||
|
||||
- [W18 — 2026-04-29 08:11] **AEP V1 LIVRÉE** : 52 fiches prod (`aep.trans-former.fr/pratiques-regeneratives`), 12 commits feat/. V1.1 mode divergent cadrée (8 items brain-dump Jules).
|
||||
|
||||
4
app.vue
4
app.vue
@@ -55,7 +55,7 @@
|
||||
class="nav-tab"
|
||||
:class="{ 'nav-tab--active': route.path === '/media' }"
|
||||
>
|
||||
MEDIA
|
||||
recherche-média
|
||||
</NuxtLink>
|
||||
</nav>
|
||||
|
||||
@@ -234,7 +234,7 @@
|
||||
<NuxtLink to="/" class="block px-4 py-2.5 text-sm font-medium transition-opacity hover:opacity-70" :style="route.path === '/' ? 'color: var(--nav-primary-solid); font-weight: 700;' : 'color: var(--nav-text);'">Écosystème Entraide Architecture</NuxtLink>
|
||||
<NuxtLink to="/agences" class="block px-4 py-2.5 text-sm transition-opacity hover:opacity-70" :style="route.path === '/agences' ? 'color: var(--nav-primary-solid); font-weight: 700;' : 'color: var(--nav-text);'">Réseaux AEP</NuxtLink>
|
||||
<NuxtLink to="/trouver-du-taf" class="block px-4 py-2.5 text-sm transition-opacity hover:opacity-70" :style="route.path === '/trouver-du-taf' ? 'color: var(--nav-primary-solid); font-weight: 700;' : 'color: var(--nav-text);'">Jobs</NuxtLink>
|
||||
<NuxtLink to="/media" class="block px-4 py-2.5 text-sm transition-opacity hover:opacity-70" :style="route.path === '/media' ? 'color: var(--nav-primary-solid); font-weight: 700;' : 'color: var(--nav-text);'">MEDIA</NuxtLink>
|
||||
<NuxtLink to="/media" class="block px-4 py-2.5 text-sm transition-opacity hover:opacity-70" :style="route.path === '/media' ? 'color: var(--nav-primary-solid); font-weight: 700;' : 'color: var(--nav-text);'">recherche-média</NuxtLink>
|
||||
<NuxtLink to="/codev" class="block px-4 py-2.5 text-sm transition-opacity hover:opacity-70" :style="route.path.startsWith('/codev') ? 'color: var(--nav-primary-solid); font-weight: 700;' : 'color: var(--nav-text);'">Codev</NuxtLink>
|
||||
<div style="height: 1px; background: var(--nav-bg-alt); margin: 4px 0;"></div>
|
||||
<NuxtLink to="/manifeste" class="block px-4 py-2.5 text-sm transition-opacity hover:opacity-70" :style="route.path === '/manifeste' ? 'color: var(--nav-primary-solid); font-weight: 700;' : 'color: var(--nav-text-muted);'">Manifeste</NuxtLink>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<script setup lang="ts">
|
||||
interface EcoleData { id: string; label: string; description: string; color: string; x_hint: number; y_hint: number }
|
||||
interface LivreRag { slug: string; titre: string; annee: number; couches: string[] }
|
||||
interface AuteurData { id: string; nom: string; dates: string; ecoles: string[]; ecole_principale: string; livres_rag: LivreRag[]; theses_cles: string[]; bio_courte: string }
|
||||
interface AuteurData { id: string; nom: string; dates: string; ecoles: string[]; ecole_principale: string; ingere: boolean; livres_rag: LivreRag[]; theses_cles: string[]; bio_courte: string; bio_courte_provisoire?: string }
|
||||
interface PenseesData { ecoles: EcoleData[]; auteurs: AuteurData[] }
|
||||
|
||||
// Liens d'influence inter-ecoles (Phase 7 - matrice de filiation)
|
||||
@@ -39,7 +39,7 @@ const LINKS_INFLUENCE = [
|
||||
]
|
||||
|
||||
const props = defineProps<{ data: PenseesData | null; active?: boolean }>()
|
||||
const emit = defineEmits<{ 'select-auteur': [id: string] }>()
|
||||
const emit = defineEmits<{ 'select-auteur': [id: string]; 'select-ecole': [id: string] }>()
|
||||
|
||||
const svgRef = ref<SVGElement | null>(null)
|
||||
const tooltipRef = ref<HTMLElement | null>(null)
|
||||
@@ -52,7 +52,6 @@ let d3EdgeLabelSel: any = null
|
||||
async function initGraph() {
|
||||
if (!svgRef.value || !props.data) return
|
||||
const d3 = await import('d3')
|
||||
const { Delaunay } = await import('d3-delaunay')
|
||||
|
||||
const svgEl = svgRef.value
|
||||
const W = svgEl.clientWidth || 900
|
||||
@@ -66,70 +65,12 @@ async function initGraph() {
|
||||
|
||||
const ecoleMap = new Map<string, EcoleData>(props.data.ecoles.map(e => [e.id, e]))
|
||||
|
||||
// Positions fixes des ecoles (base pour Voronoi)
|
||||
// Positions fixes des ecoles (base pour forces D3)
|
||||
const ecolePositions = new Map<string, { tx: number; ty: number }>()
|
||||
props.data.ecoles.forEach(e => {
|
||||
ecolePositions.set(e.id, { tx: W * e.x_hint, ty: H * e.y_hint })
|
||||
})
|
||||
|
||||
// ---- VORONOI BACKGROUND (couche 1) ----
|
||||
const ecolesArr = props.data.ecoles
|
||||
const points: [number, number][] = ecolesArr.map(e => [W * e.x_hint, H * e.y_hint])
|
||||
|
||||
const delaunay = Delaunay.from(points)
|
||||
const voronoi = delaunay.voronoi([0, 0, W, H])
|
||||
|
||||
// Groupe Voronoi (fond, couche 1)
|
||||
const gVoronoi = g.append('g').attr('class', 'voronoi-bg')
|
||||
|
||||
ecolesArr.forEach((ecole, i) => {
|
||||
const cellPath = voronoi.renderCell(i)
|
||||
const poly = voronoi.cellPolygon(i)
|
||||
|
||||
gVoronoi.append('path')
|
||||
.attr('d', cellPath)
|
||||
.attr('fill', ecole.color)
|
||||
.attr('fill-opacity', 0.48)
|
||||
.attr('class', 'voronoi-cell')
|
||||
.attr('data-ecole', ecole.id)
|
||||
.on('mouseenter', (e: any) => {
|
||||
if (!tooltipRef.value) return
|
||||
tooltipRef.value.innerHTML = `<strong>${ecole.label}</strong><br><span style="opacity:0.75;font-size:0.72rem;">${ecole.description}</span>`
|
||||
tooltipRef.value.style.opacity = '1'
|
||||
})
|
||||
.on('mousemove', (e: any) => {
|
||||
if (!tooltipRef.value || !svgEl) return
|
||||
const rect = (svgEl as HTMLElement).getBoundingClientRect()
|
||||
tooltipRef.value.style.left = (e.clientX - rect.left + 14) + 'px'
|
||||
tooltipRef.value.style.top = (e.clientY - rect.top - 10) + 'px'
|
||||
})
|
||||
.on('mouseleave', () => { if (tooltipRef.value) tooltipRef.value.style.opacity = '0' })
|
||||
|
||||
// Label ecole dans la cellule (centroid du polygone)
|
||||
if (poly && poly.length > 0) {
|
||||
const centroid = d3.polygonCentroid(poly as [number, number][])
|
||||
if (centroid && !isNaN(centroid[0]) && !isNaN(centroid[1])) {
|
||||
const words = ecole.label.split(' ')
|
||||
const labelEl = gVoronoi.append('text')
|
||||
.attr('class', 'voronoi-cell-label')
|
||||
.attr('x', centroid[0])
|
||||
.attr('y', centroid[1])
|
||||
.attr('text-anchor', 'middle')
|
||||
.attr('dominant-baseline', 'middle')
|
||||
.style('pointer-events', 'none')
|
||||
.style('user-select', 'none')
|
||||
|
||||
if (words.length <= 2) {
|
||||
labelEl.text(ecole.label)
|
||||
} else {
|
||||
const mid = Math.ceil(words.length / 2)
|
||||
labelEl.append('tspan').attr('x', centroid[0]).attr('dy', '-0.55em').text(words.slice(0, mid).join(' '))
|
||||
labelEl.append('tspan').attr('x', centroid[0]).attr('dy', '1.1em').text(words.slice(mid).join(' '))
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// ---- LIENS D'INFLUENCE INTER-ECOLES (couche 3) ----
|
||||
const gInfluence = g.append('g').attr('class', 'links-influence')
|
||||
|
||||
@@ -171,7 +112,10 @@ async function initGraph() {
|
||||
const ecole = ecoleMap.get(a.ecole_principale)
|
||||
const jitter = () => (Math.random() - 0.5) * 80
|
||||
return {
|
||||
id: a.id, type: 'auteur', nom: a.nom, dates: a.dates, bio_courte: a.bio_courte,
|
||||
id: a.id, type: 'auteur', nom: a.nom, dates: a.dates,
|
||||
bio_courte: a.bio_courte,
|
||||
bio_provisoire: a.bio_courte_provisoire ?? '',
|
||||
ingere: a.ingere,
|
||||
ecole_principale: a.ecole_principale,
|
||||
color: ecole?.color ?? '#888', r: 11,
|
||||
x: W * (ecole?.x_hint ?? 0.5) + jitter(),
|
||||
@@ -195,28 +139,62 @@ async function initGraph() {
|
||||
fx: W * e.x_hint, fy: H * e.y_hint,
|
||||
}))
|
||||
|
||||
// Rayon proportionnel au nombre d'auteurs de l'ecole
|
||||
const ecoleAuteurCounts = new Map<string, number>()
|
||||
props.data.ecoles.forEach(e => ecoleAuteurCounts.set(e.id, 0))
|
||||
props.data.auteurs.forEach(a => ecoleAuteurCounts.set(a.ecole_principale, (ecoleAuteurCounts.get(a.ecole_principale) ?? 0) + 1))
|
||||
const ecoleRadius = (count: number) => Math.max(16, Math.min(36, 13 + count * 1.5))
|
||||
|
||||
const allNodes = [...ecoleFixedNodes, ...auteurNodes]
|
||||
|
||||
if (simulation) simulation.stop()
|
||||
// Phase 8.D : sim ajustee pour 171 auteurs (vs 28 v2.1, densite 6x)
|
||||
simulation = d3.forceSimulation(allNodes)
|
||||
.force('link', d3.forceLink(links).id((d: any) => d.id).distance(110).strength((d: any) => d.strength ?? 0.5))
|
||||
.force('charge', d3.forceManyBody().strength(-45))
|
||||
.force('link', d3.forceLink(links).id((d: any) => d.id).distance(120).strength((d: any) => d.strength ?? 0.5))
|
||||
.force('charge', d3.forceManyBody().strength(-70))
|
||||
.force('center', d3.forceCenter(W / 2, H / 2).strength(0.02))
|
||||
.force('collision', d3.forceCollide().radius((d: any) => d.type === 'auteur' ? 14 : 0))
|
||||
.force('collision', d3.forceCollide().radius((d: any) => d.type === 'ecole-fixed' ? ecoleRadius(ecoleAuteurCounts.get(d.ecoleId) ?? 0) + 4 : 12))
|
||||
.force('forceX', d3.forceX<any>((d: any) => {
|
||||
if (d.type === 'auteur') {
|
||||
const pos = ecolePositions.get(d.ecole_principale)
|
||||
return pos ? pos.tx : W / 2
|
||||
}
|
||||
return W / 2
|
||||
}).strength(0.12))
|
||||
}).strength(0.15))
|
||||
.force('forceY', d3.forceY<any>((d: any) => {
|
||||
if (d.type === 'auteur') {
|
||||
const pos = ecolePositions.get(d.ecole_principale)
|
||||
return pos ? pos.ty : H / 2
|
||||
}
|
||||
return H / 2
|
||||
}).strength(0.12))
|
||||
}).strength(0.15))
|
||||
|
||||
// ---- NOEUDS ECOLES visibles (couche 3.5) ----
|
||||
// Cercles proportionnels au count d'auteurs, fixes aux centroids Bonpote, cliquables
|
||||
const gEcoles = g.append('g').attr('class', 'ecoles-nodes')
|
||||
ecoleFixedNodes.forEach(eNode => {
|
||||
const ecole = ecoleMap.get(eNode.ecoleId)
|
||||
if (!ecole) return
|
||||
const count = ecoleAuteurCounts.get(eNode.ecoleId) ?? 0
|
||||
const r = ecoleRadius(count)
|
||||
gEcoles.append('circle')
|
||||
.attr('cx', eNode.fx).attr('cy', eNode.fy).attr('r', r)
|
||||
.attr('fill', ecole.color).attr('fill-opacity', 0.82).attr('stroke', ecole.color).attr('stroke-width', 2)
|
||||
.attr('class', 'ecole-node').style('cursor', 'pointer')
|
||||
.on('mouseenter', (e: any) => {
|
||||
if (!tooltipRef.value) return
|
||||
tooltipRef.value.innerHTML = `<strong>${ecole.label}</strong> <span style="opacity:0.6;font-size:0.7rem;">${count} auteur${count > 1 ? 's' : ''}</span><br><span style="opacity:0.75;font-size:0.72rem;">${ecole.description}</span>`
|
||||
tooltipRef.value.style.opacity = '1'
|
||||
})
|
||||
.on('mousemove', (e: any) => {
|
||||
if (!tooltipRef.value || !svgEl) return
|
||||
const rect = (svgEl as HTMLElement).getBoundingClientRect()
|
||||
tooltipRef.value.style.left = (e.clientX - rect.left + 14) + 'px'
|
||||
tooltipRef.value.style.top = (e.clientY - rect.top - 10) + 'px'
|
||||
})
|
||||
.on('mouseleave', () => { if (tooltipRef.value) tooltipRef.value.style.opacity = '0' })
|
||||
.on('click', (e: any) => { e.stopPropagation(); emit('select-ecole', eNode.ecoleId) })
|
||||
})
|
||||
|
||||
// ---- LIENS APPARTENANCE (couche 4) ----
|
||||
const gLinks = g.append('g').attr('class', 'links-appartenance')
|
||||
@@ -234,18 +212,24 @@ async function initGraph() {
|
||||
// ---- NODES AUTEURS (couche 5) ----
|
||||
const gAuteurs = g.append('g').attr('class', 'auteurs')
|
||||
d3NodeSel = gAuteurs.selectAll('g').data(auteurNodes).join('g')
|
||||
.style('cursor', 'pointer')
|
||||
.style('cursor', (d: any) => d.ingere ? 'pointer' : 'default')
|
||||
.call(d3.drag<any, any>()
|
||||
.on('start', (e: any, d: any) => { if (!e.active) simulation.alphaTarget(0.3).restart(); d.fx = d.x; d.fy = d.y })
|
||||
.on('drag', (e: any, d: any) => { d.fx = e.x; d.fy = e.y })
|
||||
.on('end', (e: any, d: any) => { if (!e.active) simulation.alphaTarget(0); d.fx = null; d.fy = null }))
|
||||
.on('click', (e: any, d: any) => { e.stopPropagation(); emit('select-auteur', d.id) })
|
||||
.on('click', (e: any, d: any) => {
|
||||
if (!d.ingere) return
|
||||
e.stopPropagation()
|
||||
emit('select-auteur', d.id)
|
||||
})
|
||||
|
||||
// Phase 8.D : grisage conditionnel auteurs non-ingeres (ingere:false)
|
||||
d3NodeSel.append('circle')
|
||||
.attr('r', (d: any) => d.r)
|
||||
.attr('fill', (d: any) => d.color + 'cc')
|
||||
.attr('stroke', (d: any) => d.color)
|
||||
.attr('fill', (d: any) => d.ingere ? (d.color + 'cc') : '#bbbbbb')
|
||||
.attr('stroke', (d: any) => d.ingere ? d.color : '#999999')
|
||||
.attr('stroke-width', 1.5)
|
||||
.attr('opacity', (d: any) => d.ingere ? 1 : 0.35)
|
||||
|
||||
// ---- LABELS AUTEURS (couche 6 - fix 7.1 : drop-shadow blanc) ----
|
||||
d3NodeSel.append('text')
|
||||
@@ -254,12 +238,21 @@ async function initGraph() {
|
||||
.attr('text-anchor', 'middle')
|
||||
.attr('dy', (d: any) => -(d.r + 4))
|
||||
.style('pointer-events', 'none')
|
||||
.style('opacity', (d: any) => d.ingere ? 1 : 0.3)
|
||||
.style('fill', (d: any) => d.ingere ? '#1a1a1a' : '#777777')
|
||||
|
||||
d3NodeSel
|
||||
.on('mouseenter', (e: any, d: any) => {
|
||||
if (!tooltipRef.value) return
|
||||
const bio = d.bio_courte.length > 90 ? d.bio_courte.slice(0, 87) + '...' : d.bio_courte
|
||||
tooltipRef.value.innerHTML = `<strong>${d.nom}</strong> <span style="opacity:0.6;font-size:0.7rem;">${d.dates}</span><br><span style="opacity:0.75;font-size:0.72rem;">${bio}</span>`
|
||||
let tooltipHtml = ''
|
||||
if (d.ingere) {
|
||||
const rawBio = d.bio_courte || ''
|
||||
const bio = rawBio.length > 90 ? rawBio.slice(0, 87) + '...' : rawBio
|
||||
tooltipHtml = `<strong>${d.nom}</strong> <span style="opacity:0.6;font-size:0.7rem;">${d.dates}</span><br><span style="opacity:0.75;font-size:0.72rem;">${bio || 'Dans le RAG ATIS.'}</span>`
|
||||
} else {
|
||||
tooltipHtml = `<strong>${d.nom}</strong> <span style="opacity:0.6;font-size:0.7rem;">${d.dates}</span><br><span style="opacity:0.65;font-size:0.72rem;font-style:italic;">Présent dans Bonpote, pas encore ingéré dans le RAG ATIS.</span>`
|
||||
}
|
||||
tooltipRef.value.innerHTML = tooltipHtml
|
||||
tooltipRef.value.style.opacity = '1'
|
||||
})
|
||||
.on('mousemove', (e: any) => {
|
||||
@@ -325,20 +318,14 @@ defineExpose({ triggerResize })
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* ---- Voronoi cellules ---- */
|
||||
.voronoi-cell {
|
||||
stroke: rgba(255,255,255,0.4);
|
||||
stroke-width: 1.5px;
|
||||
cursor: default;
|
||||
/* ---- Voronoi cellules : non-blurre Phase 8.F (revert Phase 8.D) ---- */
|
||||
/* Blur retire ; les cellules colorees Bonpote-aligned suffisent visuellement. */
|
||||
|
||||
.ecole-node {
|
||||
transition: opacity 0.15s, r 0.15s;
|
||||
}
|
||||
.ecole-node:hover {
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
/* ---- Labels ecoles dans cellules Voronoi ---- */
|
||||
.voronoi-cell-label {
|
||||
fill: rgba(40,40,40,0.52);
|
||||
font-size: 17px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.3px;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
<div class="flex items-center justify-between px-4 py-3 shrink-0" style="border-bottom:1px solid var(--nav-bg-alt);background:var(--nav-bg);">
|
||||
<div>
|
||||
<p class="text-sm font-bold" style="color:var(--nav-text);">RAG Pensees Ecologiques</p>
|
||||
<p class="text-xs" style="color:var(--nav-text-muted);">{{ corpusCount }} auteurs ingeres</p>
|
||||
</div>
|
||||
<button @click="open = false" class="flex items-center justify-center w-7 h-7 rounded-full hover:opacity-70"
|
||||
style="background:var(--nav-bg-alt);color:var(--nav-text-muted);" aria-label="Fermer">
|
||||
@@ -76,11 +75,24 @@
|
||||
|
||||
<!-- Input overlay -->
|
||||
<div class="shrink-0 px-3 py-3" style="border-top:1px solid var(--nav-bg-alt);">
|
||||
<div class="flex items-center gap-2">
|
||||
<input ref="inputElOverlay" v-model="q" type="text" placeholder="Ta question..." maxlength="500"
|
||||
<div class="flex items-center gap-2" style="position:relative;">
|
||||
<!-- Hashtag autocomplete dropdown (Slack/Discord pattern, au-dessus de l'input) -->
|
||||
<div v-if="hashtagDropdownOpen && hashtagSuggestions.length"
|
||||
class="hashtag-dropdown"
|
||||
style="position:absolute;bottom:100%;left:0;right:0;margin-bottom:6px;max-height:220px;overflow-y:auto;background:var(--nav-surface);border:1px solid var(--nav-bg-alt);border-radius:8px;box-shadow:0 -4px 12px rgba(0,0,0,0.12);z-index:50;">
|
||||
<div v-for="(auteur, idx) in hashtagSuggestions" :key="auteur.id"
|
||||
@mousedown.prevent="applyHashtagSuggestion(auteur)"
|
||||
@mouseenter="hashtagSelectedIndex = idx"
|
||||
class="px-3 py-2 cursor-pointer text-sm"
|
||||
:style="idx === hashtagSelectedIndex ? 'background:var(--nav-primary);color:var(--nav-text-on-primary);' : 'color:var(--nav-text);'">
|
||||
<span style="font-weight:600;">#{{ auteur.id }}</span>
|
||||
<span :style="idx === hashtagSelectedIndex ? 'opacity:0.85;margin-left:8px;font-size:0.78rem;' : 'opacity:0.65;margin-left:8px;font-size:0.78rem;color:var(--nav-text-muted);'">{{ auteur.nom }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<input ref="inputElOverlay" v-model="q" type="text" placeholder="Ta question, tape #auteur pour cibler" maxlength="500"
|
||||
class="flex-1 px-3 py-2 rounded-lg text-sm outline-none"
|
||||
style="background:var(--nav-bg);color:var(--nav-text);border:1px solid var(--nav-bg-alt);"
|
||||
@keydown.enter.prevent="send" />
|
||||
@keydown="onInputKeydown" />
|
||||
<button @click="send" :disabled="loading || !q.trim()"
|
||||
class="flex items-center justify-center w-9 h-9 rounded-lg"
|
||||
:style="loading||!q.trim() ? 'background:var(--nav-bg-alt);opacity:0.5;cursor:not-allowed;' : 'background:var(--nav-primary);cursor:pointer;'"
|
||||
@@ -106,7 +118,6 @@
|
||||
<div class="flex items-center justify-between px-4 py-2 shrink-0" style="border-bottom:1px solid var(--nav-bg-alt);background:var(--nav-bg);">
|
||||
<div>
|
||||
<p class="text-sm font-bold" style="color:var(--nav-text);">RAG Pensees Ecologiques</p>
|
||||
<p class="text-xs" style="color:var(--nav-text-muted);">{{ corpusCount }} auteurs ingeres</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -156,11 +167,24 @@
|
||||
|
||||
<!-- Input inline -->
|
||||
<div class="shrink-0 px-3 py-3" style="border-top:1px solid var(--nav-bg-alt);">
|
||||
<div class="flex items-center gap-2">
|
||||
<input ref="inputElInline" v-model="q" type="text" placeholder="Ta question..." maxlength="500"
|
||||
<div class="flex items-center gap-2" style="position:relative;">
|
||||
<!-- Hashtag autocomplete dropdown (Slack/Discord pattern, au-dessus de l'input) -->
|
||||
<div v-if="hashtagDropdownOpen && hashtagSuggestions.length"
|
||||
class="hashtag-dropdown"
|
||||
style="position:absolute;bottom:100%;left:0;right:0;margin-bottom:6px;max-height:220px;overflow-y:auto;background:var(--nav-surface);border:1px solid var(--nav-bg-alt);border-radius:8px;box-shadow:0 -4px 12px rgba(0,0,0,0.12);z-index:50;">
|
||||
<div v-for="(auteur, idx) in hashtagSuggestions" :key="auteur.id"
|
||||
@mousedown.prevent="applyHashtagSuggestion(auteur)"
|
||||
@mouseenter="hashtagSelectedIndex = idx"
|
||||
class="px-3 py-2 cursor-pointer text-sm"
|
||||
:style="idx === hashtagSelectedIndex ? 'background:var(--nav-primary);color:var(--nav-text-on-primary);' : 'color:var(--nav-text);'">
|
||||
<span style="font-weight:600;">#{{ auteur.id }}</span>
|
||||
<span :style="idx === hashtagSelectedIndex ? 'opacity:0.85;margin-left:8px;font-size:0.78rem;' : 'opacity:0.65;margin-left:8px;font-size:0.78rem;color:var(--nav-text-muted);'">{{ auteur.nom }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<input ref="inputElInline" v-model="q" type="text" placeholder="Ta question, tape #auteur pour cibler" maxlength="500"
|
||||
class="flex-1 px-3 py-2 rounded-lg text-sm outline-none"
|
||||
style="background:var(--nav-bg);color:var(--nav-text);border:1px solid var(--nav-bg-alt);"
|
||||
@keydown.enter.prevent="send" />
|
||||
@keydown="onInputKeydown" />
|
||||
<button @click="send" :disabled="loading || !q.trim()"
|
||||
class="flex items-center justify-center w-9 h-9 rounded-lg"
|
||||
:style="loading||!q.trim() ? 'background:var(--nav-bg-alt);opacity:0.5;cursor:not-allowed;' : 'background:var(--nav-primary);cursor:pointer;'"
|
||||
@@ -177,6 +201,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
interface Message { role: 'user' | 'assistant'; content: string }
|
||||
interface AuteurMini { id: string; nom: string }
|
||||
|
||||
type CorpusMode = 'pensees' | 'projets' | 'both'
|
||||
|
||||
@@ -211,13 +236,111 @@ const inputElOverlay = ref<HTMLInputElement | null>(null)
|
||||
const inputElInline = ref<HTMLInputElement | null>(null)
|
||||
const corpusCount = 18
|
||||
|
||||
const corpus = ref<CorpusMode>('both')
|
||||
const corpus = ref<CorpusMode>('pensees')
|
||||
|
||||
onMounted(() => {
|
||||
// Phase 8.E : hashtag mentions
|
||||
const auteursIngeres = ref<AuteurMini[]>([])
|
||||
const hashtagSuggestions = ref<AuteurMini[]>([])
|
||||
const hashtagDropdownOpen = ref(false)
|
||||
const hashtagSelectedIndex = ref(0)
|
||||
|
||||
function getActiveInput(): HTMLInputElement | null {
|
||||
return props.inline ? inputElInline.value : inputElOverlay.value
|
||||
}
|
||||
|
||||
function detectHashtagAtCursor(input: string, cursorPos: number): { start: number; partial: string } | null {
|
||||
const before = input.slice(0, cursorPos)
|
||||
const m = before.match(/#([a-z0-9-]*)$/i)
|
||||
if (!m) return null
|
||||
return { start: m.index ?? 0, partial: (m[1] || '').toLowerCase() }
|
||||
}
|
||||
|
||||
function updateHashtagSuggestions() {
|
||||
const el = getActiveInput()
|
||||
const cursorPos = el?.selectionStart ?? q.value.length
|
||||
const detection = detectHashtagAtCursor(q.value, cursorPos)
|
||||
// Ouvrir dès que le # est présent (partial vide accepté pour afficher la liste)
|
||||
if (!detection) {
|
||||
hashtagDropdownOpen.value = false
|
||||
return
|
||||
}
|
||||
const partial = detection.partial
|
||||
const list = partial.length === 0
|
||||
? auteursIngeres.value.slice(0, 8)
|
||||
: auteursIngeres.value
|
||||
.filter(a => a.id.toLowerCase().includes(partial) || a.nom.toLowerCase().includes(partial))
|
||||
.slice(0, 8)
|
||||
hashtagSuggestions.value = list
|
||||
hashtagDropdownOpen.value = list.length > 0
|
||||
hashtagSelectedIndex.value = 0
|
||||
}
|
||||
|
||||
function applyHashtagSuggestion(auteur: AuteurMini) {
|
||||
const el = getActiveInput()
|
||||
const cursorPos = el?.selectionStart ?? q.value.length
|
||||
const detection = detectHashtagAtCursor(q.value, cursorPos)
|
||||
if (!detection) return
|
||||
const before = q.value.slice(0, detection.start)
|
||||
const after = q.value.slice(cursorPos)
|
||||
const insert = '#' + auteur.id + ' '
|
||||
q.value = before + insert + after
|
||||
hashtagDropdownOpen.value = false
|
||||
nextTick(() => {
|
||||
const focusEl = getActiveInput()
|
||||
if (!focusEl) return
|
||||
focusEl.focus()
|
||||
const newPos = before.length + insert.length
|
||||
focusEl.setSelectionRange(newPos, newPos)
|
||||
})
|
||||
}
|
||||
|
||||
function onInputKeydown(e: KeyboardEvent) {
|
||||
if (hashtagDropdownOpen.value && hashtagSuggestions.value.length > 0) {
|
||||
if (e.key === 'ArrowDown') {
|
||||
e.preventDefault()
|
||||
hashtagSelectedIndex.value = (hashtagSelectedIndex.value + 1) % hashtagSuggestions.value.length
|
||||
return
|
||||
}
|
||||
if (e.key === 'ArrowUp') {
|
||||
e.preventDefault()
|
||||
hashtagSelectedIndex.value = (hashtagSelectedIndex.value - 1 + hashtagSuggestions.value.length) % hashtagSuggestions.value.length
|
||||
return
|
||||
}
|
||||
if (e.key === 'Enter' || e.key === 'Tab') {
|
||||
e.preventDefault()
|
||||
applyHashtagSuggestion(hashtagSuggestions.value[hashtagSelectedIndex.value])
|
||||
return
|
||||
}
|
||||
if (e.key === 'Escape') {
|
||||
e.preventDefault()
|
||||
hashtagDropdownOpen.value = false
|
||||
return
|
||||
}
|
||||
}
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault()
|
||||
send()
|
||||
}
|
||||
}
|
||||
|
||||
watch(q, () => {
|
||||
updateHashtagSuggestions()
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
const saved = window.localStorage.getItem(CORPUS_STORAGE_KEY) as CorpusMode | null
|
||||
if (saved && ['pensees', 'projets', 'both'].includes(saved)) {
|
||||
corpus.value = saved
|
||||
}
|
||||
// Chargement liste auteurs ingérés pour autocomplete hashtag
|
||||
try {
|
||||
const data = await $fetch<any>('/data/auteurs-pensees.json')
|
||||
auteursIngeres.value = (data?.auteurs ?? [])
|
||||
.filter((a: any) => a.ingere === true)
|
||||
.map((a: any) => ({ id: String(a.id), nom: String(a.nom) }))
|
||||
} catch (e) {
|
||||
console.error('Erreur chargement auteurs-pensees.json pour hashtag', e)
|
||||
}
|
||||
})
|
||||
|
||||
function setCorpus(val: CorpusMode) {
|
||||
@@ -240,21 +363,48 @@ watch(() => props.auteurContext, (ctx) => {
|
||||
async function send() {
|
||||
const query = q.value.trim()
|
||||
if (!query || loading.value) return
|
||||
|
||||
// Extraire le premier hashtag matchant un auteur ingéré
|
||||
let auteurSlug: string | null = null
|
||||
const matches = [...query.matchAll(/#([a-z0-9-]+)/gi)]
|
||||
for (const m of matches) {
|
||||
const slug = m[1].toLowerCase()
|
||||
if (auteursIngeres.value.find(a => a.id === slug)) {
|
||||
auteurSlug = slug
|
||||
break
|
||||
}
|
||||
}
|
||||
// Premier hashtag non-matché (pour info utilisateur si jamais ne match aucun)
|
||||
let auteurSlugUnmatched: string | null = null
|
||||
if (!auteurSlug && matches.length > 0) {
|
||||
auteurSlugUnmatched = matches[0][1].toLowerCase()
|
||||
}
|
||||
|
||||
err.value = ''
|
||||
messages.value.push({ role: 'user', content: query })
|
||||
q.value = ''
|
||||
hashtagDropdownOpen.value = false
|
||||
loading.value = true
|
||||
await nextTick()
|
||||
scrollBottom()
|
||||
try {
|
||||
const res = await $fetch<{ response: string }>('/api/chatbot-pensees', {
|
||||
const res = await $fetch<any>('/api/chatbot-pensees', {
|
||||
method: 'POST',
|
||||
body: { query, mode: 'hybrid', corpus: corpus.value },
|
||||
body: {
|
||||
query,
|
||||
mode: 'hybrid',
|
||||
corpus: corpus.value,
|
||||
auteur_slug: auteurSlug ?? auteurSlugUnmatched,
|
||||
},
|
||||
})
|
||||
messages.value.push({ role: 'assistant', content: res.response ?? '' })
|
||||
let responseText = res.response ?? ''
|
||||
if (res.auteur_unmatched) {
|
||||
responseText = `*(Aucun livre de #${res.auteur_unmatched} n'est ingéré dans le RAG. Je réponds depuis la carte entière.)*\n\n` + responseText
|
||||
}
|
||||
messages.value.push({ role: 'assistant', content: responseText })
|
||||
} catch (e: any) {
|
||||
const s = e?.response?.status ?? e?.statusCode
|
||||
err.value = s === 429 ? 'Limite atteinte.' : s === 503 ? 'RAG indisponible.' : 'Erreur - reessaie.'
|
||||
err.value = s === 429 ? 'Limite atteinte.' : s === 503 ? 'RAG indisponible.' : 'Erreur, reessaie.'
|
||||
} finally {
|
||||
loading.value = false
|
||||
await nextTick()
|
||||
|
||||
101
components/FicheEcole.vue
Normal file
101
components/FicheEcole.vue
Normal file
@@ -0,0 +1,101 @@
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<Transition name="backdrop">
|
||||
<div v-if="open && ecole" class="fixed inset-0 z-[1500]" style="background: rgba(26,34,56,0.55);" @click="emit('close')" aria-hidden="true" />
|
||||
</Transition>
|
||||
<Transition name="modal">
|
||||
<div v-if="open && ecole" class="fixed z-[1501] left-1/2 flex flex-col"
|
||||
style="top:50%;transform:translate(-50%,-50%);width:min(540px,94vw);max-height:85vh;background:var(--nav-bg);border-radius:14px;box-shadow:0 16px 64px rgba(26,34,56,0.28);overflow:hidden;"
|
||||
role="dialog" aria-modal="true">
|
||||
<!-- Header -->
|
||||
<div class="flex items-start justify-between px-5 py-4 shrink-0"
|
||||
:style="`border-bottom: 3px solid ${ecole.color}; background: var(--nav-surface);`">
|
||||
<div class="flex-1 min-w-0">
|
||||
<span class="px-2 py-0.5 rounded-full text-xs font-semibold" :style="`background:${ecole.color}22;color:${ecole.color};`">Ecole de pensee</span>
|
||||
<h2 class="mt-2 font-bold text-lg leading-tight" style="color:var(--nav-text);">{{ ecole.label }}</h2>
|
||||
<p class="text-sm mt-1 leading-relaxed" style="color:var(--nav-text-muted);">{{ ecole.description }}</p>
|
||||
</div>
|
||||
<button @click="emit('close')" class="ml-3 shrink-0 flex items-center justify-center w-8 h-8 rounded-full hover:opacity-70"
|
||||
style="background:var(--nav-bg-alt);color:var(--nav-text-muted);" aria-label="Fermer">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round">
|
||||
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<!-- Body -->
|
||||
<div class="flex-1 overflow-y-auto px-5 py-4 flex flex-col gap-4">
|
||||
<div v-if="auteursIngeres.length">
|
||||
<p class="text-xs font-bold uppercase tracking-widest mb-2" style="color:var(--nav-text-muted);">Dans le RAG ({{ auteursIngeres.length }})</p>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button v-for="a in auteursIngeres" :key="a.id"
|
||||
class="px-2.5 py-1 rounded-full text-xs font-medium hover:opacity-80 transition-opacity"
|
||||
:style="`background:${ecole.color}22;color:${ecole.color};border:1px solid ${ecole.color}44;cursor:pointer;`"
|
||||
@click="onSelectAuteur(a.id)">
|
||||
{{ a.nom }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="auteursNonIngeres.length">
|
||||
<p class="text-xs font-bold uppercase tracking-widest mb-2" style="color:var(--nav-text-muted);">Presents dans Bonpote, pas encore dans le RAG ({{ auteursNonIngeres.length }})</p>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<span v-for="a in auteursNonIngeres" :key="a.id"
|
||||
class="px-2.5 py-1 rounded-full text-xs"
|
||||
style="background:var(--nav-bg-alt);color:var(--nav-text-muted);">
|
||||
{{ a.nom }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Footer -->
|
||||
<div class="shrink-0 px-5 py-3 border-t" style="border-color:var(--nav-bg-alt);">
|
||||
<button @click="emit('interroger-ecole', ecoleId!)" class="w-full py-2.5 rounded-lg text-sm font-semibold hover:opacity-80"
|
||||
:style="`background:${ecole.color};color:white;`">
|
||||
Interroger le RAG sur {{ ecole.label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
interface AuteurData { id: string; nom: string; ecoles: string[]; ecole_principale: string; ingere: boolean }
|
||||
interface EcoleData { id: string; label: string; description: string; color: string }
|
||||
interface PenseesData { ecoles: EcoleData[]; auteurs: AuteurData[] }
|
||||
|
||||
const props = defineProps<{ open: boolean; ecoleId: string | null; data: PenseesData | null }>()
|
||||
const emit = defineEmits<{ close: []; 'select-auteur': [auteurId: string]; 'interroger-ecole': [ecoleId: string] }>()
|
||||
|
||||
const ecole = computed<EcoleData | null>(() => {
|
||||
if (!props.ecoleId || !props.data) return null
|
||||
return props.data.ecoles.find(e => e.id === props.ecoleId) ?? null
|
||||
})
|
||||
|
||||
const auteursIngeres = computed(() => {
|
||||
if (!props.ecoleId || !props.data) return []
|
||||
return props.data.auteurs.filter(a => a.ecole_principale === props.ecoleId && (a as any).ingere)
|
||||
})
|
||||
|
||||
const auteursNonIngeres = computed(() => {
|
||||
if (!props.ecoleId || !props.data) return []
|
||||
return props.data.auteurs.filter(a => a.ecole_principale === props.ecoleId && !(a as any).ingere)
|
||||
})
|
||||
|
||||
function onSelectAuteur(id: string) {
|
||||
emit('close')
|
||||
emit('select-auteur', id)
|
||||
}
|
||||
|
||||
function onKey(e: KeyboardEvent) { if (e.key === 'Escape' && props.open) emit('close') }
|
||||
onMounted(() => window.addEventListener('keydown', onKey))
|
||||
onUnmounted(() => window.removeEventListener('keydown', onKey))
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.backdrop-enter-active,.backdrop-leave-active { transition: opacity 0.2s; }
|
||||
.backdrop-enter-from,.backdrop-leave-to { opacity: 0; }
|
||||
.modal-enter-active { transition: opacity 0.2s, transform 0.22s cubic-bezier(0.34,1.56,0.64,1); }
|
||||
.modal-leave-active { transition: opacity 0.18s, transform 0.18s ease-in; }
|
||||
.modal-enter-from { opacity: 0; transform: translate(-50%,-48%) scale(0.94); }
|
||||
.modal-leave-to { opacity: 0; transform: translate(-50%,-48%) scale(0.96); }
|
||||
</style>
|
||||
@@ -30,9 +30,9 @@ LOCAL_ENV_CONTENT=$(cat "$LOCAL_ENV" 2>/dev/null || echo "")
|
||||
if [ "$LOCAL_ENV_CONTENT" != "$REMOTE_ENV_CONTENT" ]; then
|
||||
log "AVERTISSEMENT : .env.production local != .env VPS"
|
||||
log " --- Local ---"
|
||||
echo "$LOCAL_ENV_CONTENT" | sed 's/TOKEN=.*/TOKEN=***/' | sed 's/^/ /'
|
||||
echo "$LOCAL_ENV_CONTENT" | sed -E 's/(TOKEN|API_KEY|PASSWORD|SECRET)=.*$/\1=***/' | sed 's/^/ /'
|
||||
log " --- VPS ---"
|
||||
echo "$REMOTE_ENV_CONTENT" | sed 's/TOKEN=.*/TOKEN=***/' | sed 's/^/ /'
|
||||
echo "$REMOTE_ENV_CONTENT" | sed -E 's/(TOKEN|API_KEY|PASSWORD|SECRET)=.*$/\1=***/' | sed 's/^/ /'
|
||||
read -p "Continuer malgre la difference ? [y/N] " CONFIRM
|
||||
[ "$CONFIRM" = "y" ] || { log "Deploiement annule."; exit 1; }
|
||||
fi
|
||||
|
||||
@@ -16,6 +16,7 @@ export default defineNuxtConfig({
|
||||
commentTableId: process.env.COMMENT_TABLE_ID || process.env.AVIS_TABLE_ID,
|
||||
statsTableId: process.env.STATS_TABLE_ID || 'mbbq7n47ixy19mc',
|
||||
mistralApiKey: process.env.MISTRAL_API_KEY,
|
||||
nebiusApiKey: process.env.NEBIUS_API_KEY,
|
||||
redisUrl: process.env.REDIS_URL || 'redis://127.0.0.1:6379',
|
||||
resendApiKey: process.env.RESEND_API_KEY,
|
||||
emailJules: process.env.EMAIL_JULES || 'jules@trans-former.fr',
|
||||
|
||||
@@ -128,12 +128,6 @@
|
||||
: 'color: var(--nav-text-muted); border-bottom: 2px solid transparent;'"
|
||||
@click="desktopMapView = 'graphe'"
|
||||
>Vue graphique</button>
|
||||
<NuxtLink
|
||||
to="/media"
|
||||
class="px-5 py-2 text-sm font-medium transition-colors"
|
||||
style="color: var(--nav-text-muted); border-bottom: 2px solid transparent;"
|
||||
active-class="!color-nav-text"
|
||||
>Média</NuxtLink>
|
||||
</div>
|
||||
|
||||
<!-- Carte Métropole desktop -->
|
||||
@@ -225,11 +219,6 @@
|
||||
: 'color: var(--nav-text-muted); border-bottom: 2px solid transparent;'"
|
||||
@click="mobileMapView = 'graphe'"
|
||||
>Graphe</button>
|
||||
<NuxtLink
|
||||
to="/media"
|
||||
class="flex-1 py-2 text-sm font-medium transition-colors text-center"
|
||||
style="color: var(--nav-text-muted); border-bottom: 2px solid transparent;"
|
||||
>Média</NuxtLink>
|
||||
</div>
|
||||
|
||||
<div class="lg:hidden flex-1 relative overflow-hidden">
|
||||
|
||||
191
pages/media.vue
191
pages/media.vue
@@ -6,12 +6,26 @@
|
||||
|
||||
<!-- Header onglet -->
|
||||
<div class="shrink-0 px-5 py-3"
|
||||
style="background: var(--nav-surface); border-bottom: 1px solid var(--nav-bg-alt);">
|
||||
style="background: var(--nav-surface); border-bottom: 1px solid var(--nav-bg-alt); display:flex; align-items:center; justify-content:space-between; gap:12px;">
|
||||
<div>
|
||||
<h1 class="font-bold text-base" style="color: var(--nav-text);">ATIS Media</h1>
|
||||
<p class="text-xs mt-0.5" style="color: var(--nav-text-muted);">
|
||||
{{ corpusCount }} auteurs ingeres dans le RAG - carte FRACAS Bonpote V2
|
||||
{{ corpusCount }} auteurs / {{ livresCount }} livres ingeres dans le RAG -
|
||||
<a href="https://bonpote.com/la-carte-des-pensees-ecologiques/"
|
||||
target="_blank" rel="noopener"
|
||||
style="color: var(--nav-primary, #3b6ea5); text-decoration: underline; text-underline-offset: 2px;">
|
||||
carte FRACAS Bonpote V2
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
@click="ragInfoOpen = true"
|
||||
title="A propos du RAG FRACAS"
|
||||
style="width:26px;height:26px;border-radius:50%;border:1.5px solid var(--nav-text-muted);color:var(--nav-text-muted);font-size:0.72rem;font-weight:700;cursor:pointer;flex-shrink:0;background:var(--nav-bg-alt);display:flex;align-items:center;justify-content:center;"
|
||||
aria-label="A propos du RAG">
|
||||
i
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Conteneur split / plein ecran -->
|
||||
<div class="layout-container">
|
||||
@@ -32,6 +46,7 @@
|
||||
:data="penseesData"
|
||||
:active="true"
|
||||
@select-auteur="onSelectAuteur"
|
||||
@select-ecole="onSelectEcole"
|
||||
/>
|
||||
<template #fallback>
|
||||
<div class="w-full h-full flex items-center justify-center" style="color: var(--nav-text-muted);">
|
||||
@@ -77,6 +92,18 @@
|
||||
</svg>
|
||||
Chatbot plein ecran
|
||||
</button>
|
||||
<button
|
||||
@click="setLayoutMode('bonpote')"
|
||||
:class="{ active: layoutMode === 'bonpote' }"
|
||||
class="toggle-btn"
|
||||
title="A propos de la carte FRACAS Bonpote V2"
|
||||
style="margin-left: auto;"
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<circle cx="12" cy="12" r="10"/><polyline points="12 8 12 12 14 14"/>
|
||||
</svg>
|
||||
Bonpote V2
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Poignee draggable (visible uniquement en mode split, pas sur mobile) -->
|
||||
@@ -104,6 +131,61 @@
|
||||
</ClientOnly>
|
||||
</div>
|
||||
|
||||
<!-- Vue Bonpote V2 -->
|
||||
<div
|
||||
v-if="layoutMode === 'bonpote'"
|
||||
class="flex-1 overflow-y-auto px-6 py-8"
|
||||
style="max-width: 680px; margin: 0 auto;"
|
||||
>
|
||||
<div class="mb-6">
|
||||
<p class="text-xs font-bold uppercase tracking-widest mb-2" style="color: var(--nav-text-muted);">Reference editoriale</p>
|
||||
<h2 class="text-xl font-bold mb-3" style="color: var(--nav-text);">Carte FRACAS des pensees ecologiques</h2>
|
||||
<p class="text-sm leading-relaxed mb-4" style="color: var(--nav-text);">
|
||||
FRACAS (Familles, Racines et Arpentages des Courants et Alternatives Solidaires) est une carte des ecoles de pensee ecologique publiee par Bonpote en octobre 2024. Elle reference ~140 auteurs et autrices reparti-es en 10 ecoles de pensee, depuis l'ecosocialisme jusqu'a l'ethique environnementale.
|
||||
</p>
|
||||
<p class="text-sm leading-relaxed mb-6" style="color: var(--nav-text);">
|
||||
Le RAG ATIS est construit sur cette reference : chaque auteur ingere dans la bibliotheque correspond a une entree de la carte FRACAS. Les ecoles de pensee, les positions et les couleurs de notre carte sont transposees 1:1 depuis Bonpote V2.
|
||||
</p>
|
||||
<div class="flex flex-col gap-3">
|
||||
<a href="https://bonpote.com/la-carte-des-pensees-ecologiques/"
|
||||
target="_blank" rel="noopener"
|
||||
class="flex items-center gap-3 px-4 py-3 rounded-lg hover:opacity-80 transition-opacity"
|
||||
style="background: var(--nav-primary, #3b6ea5); color: white; font-size: 0.875rem; font-weight: 600; text-decoration: none;">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>
|
||||
Lire l'article Bonpote + carte interactive
|
||||
</a>
|
||||
<a href="https://bonpote.com/wp-content/uploads/2024/10/FRACAS_BONPOTE_CARTE_VERSO_V2-OCT2024.pdf"
|
||||
target="_blank" rel="noopener"
|
||||
class="flex items-center gap-3 px-4 py-3 rounded-lg hover:opacity-80 transition-opacity"
|
||||
style="background: var(--nav-bg-alt); color: var(--nav-text); font-size: 0.875rem; font-weight: 500; text-decoration: none;">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
|
||||
Telecharger le poster PDF (recto/verso)
|
||||
</a>
|
||||
<button
|
||||
@click="setLayoutMode('split')"
|
||||
class="flex items-center gap-3 px-4 py-3 rounded-lg hover:opacity-80 transition-opacity text-left"
|
||||
style="background: var(--nav-bg-alt); color: var(--nav-text); font-size: 0.875rem; font-weight: 500; border: none; cursor: pointer;">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
|
||||
Interroger le RAG ATIS sur ces pensees
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs font-bold uppercase tracking-widest mb-3" style="color: var(--nav-text-muted);">Les 10 ecoles de pensee (FRACAS V2)</p>
|
||||
<div class="flex flex-col gap-2">
|
||||
<div v-for="ecole in (penseesData?.ecoles ?? [])" :key="ecole.id"
|
||||
class="flex items-start gap-3 px-3 py-2 rounded-lg"
|
||||
style="background: var(--nav-bg-alt);">
|
||||
<span class="w-3 h-3 rounded-full shrink-0 mt-1" :style="`background:${ecole.color};`"></span>
|
||||
<div>
|
||||
<p class="text-sm font-semibold" style="color: var(--nav-text);">{{ ecole.label }}</p>
|
||||
<p class="text-xs mt-0.5 leading-relaxed" style="color: var(--nav-text-muted);">{{ ecole.description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -116,6 +198,61 @@
|
||||
@interroger-rag="onInterrogerRag"
|
||||
/>
|
||||
|
||||
<!-- Fiche ecole modal -->
|
||||
<FicheEcole
|
||||
:open="ficheEcoleOpen"
|
||||
:ecoleId="ficheEcoleId"
|
||||
:data="penseesData"
|
||||
@close="ficheEcoleOpen = false"
|
||||
@select-auteur="onSelectAuteurFromEcole"
|
||||
@interroger-ecole="onInterrogerEcole"
|
||||
/>
|
||||
|
||||
<!-- Modal info RAG -->
|
||||
<Teleport to="body">
|
||||
<Transition name="backdrop">
|
||||
<div v-if="ragInfoOpen" class="fixed inset-0 z-[2000]" style="background:rgba(26,34,56,0.55);" @click="ragInfoOpen = false" aria-hidden="true" />
|
||||
</Transition>
|
||||
<Transition name="modal">
|
||||
<div v-if="ragInfoOpen" class="fixed z-[2001] left-1/2 flex flex-col"
|
||||
style="top:50%;transform:translate(-50%,-50%);width:min(580px,94vw);max-height:85vh;background:var(--nav-bg);border-radius:14px;box-shadow:0 16px 64px rgba(26,34,56,0.28);overflow:hidden;"
|
||||
role="dialog" aria-modal="true" aria-label="A propos du RAG FRACAS">
|
||||
<div class="flex items-center justify-between px-5 py-4 shrink-0"
|
||||
style="border-bottom:2px solid var(--nav-bg-alt);background:var(--nav-surface);">
|
||||
<h2 class="font-bold text-base" style="color:var(--nav-text);">FRACAS - Bibliotheque des pensees ecologiques</h2>
|
||||
<button @click="ragInfoOpen = false" class="ml-3 shrink-0 flex items-center justify-center w-8 h-8 rounded-full hover:opacity-70"
|
||||
style="background:var(--nav-bg-alt);color:var(--nav-text-muted);" aria-label="Fermer">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round">
|
||||
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex-1 overflow-y-auto px-5 py-4" style="color:var(--nav-text);font-size:0.875rem;line-height:1.6;">
|
||||
<p class="mb-3">Une bibliotheque parlante politisee - des pensees ecologiques de gauche, organisees pour aider a creer une pensee complexe et nuancee, critiquer le recit dominant et soutenir des alternatives concretes et des projets collectifs.</p>
|
||||
<p class="mb-4" style="color:var(--nav-text-muted);font-size:0.8rem;">Projet open source, ouvert a toutes et a tous - <a href="https://bonpote.com/la-carte-des-pensees-ecologiques/" target="_blank" rel="noopener" style="text-decoration:underline;">article + carte FRACAS Bonpote V2</a>.</p>
|
||||
<div class="flex flex-col gap-3">
|
||||
<div class="p-3 rounded-lg" style="background:var(--nav-bg-alt);">
|
||||
<p class="font-semibold mb-1" style="font-size:0.8rem;color:var(--nav-text-muted);text-transform:uppercase;letter-spacing:0.05em;">Ce qu'est un RAG</p>
|
||||
<p>Les textes sont vectorises dans un espace de 662 dimensions - chaque livre devient un nuage de points semantiques. La proximite entre les points capture la proximite entre les idees, pas les mots.</p>
|
||||
</div>
|
||||
<div class="p-3 rounded-lg" style="background:var(--nav-bg-alt);">
|
||||
<p class="font-semibold mb-1" style="font-size:0.8rem;color:var(--nav-text-muted);text-transform:uppercase;letter-spacing:0.05em;">Chunking intelligent</p>
|
||||
<p>Lors de l'ingestion, nous selectionnons les entites cles (concepts, auteurs, relations entre idees) plutot que de decouper mecaniquement les textes.</p>
|
||||
</div>
|
||||
<div class="p-3 rounded-lg" style="background:var(--nav-bg-alt);">
|
||||
<p class="font-semibold mb-2" style="font-size:0.8rem;color:var(--nav-text-muted);text-transform:uppercase;letter-spacing:0.05em;">Trois couches d'analyse</p>
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<div class="flex gap-2"><span class="font-semibold" style="min-width:70px;">Fond</span><span>Les idees, les theses, les arguments - ce qu'on interroge directement.</span></div>
|
||||
<div class="flex gap-2"><span class="font-semibold" style="min-width:70px;">Forme</span><span>Les modeles narratifs, la rhetorique, la construction argumentative.</span></div>
|
||||
<div class="flex gap-2"><span class="font-semibold" style="min-width:70px;">Structure</span><span>L'architecture des livres - comment les auteurs construisent leur pensee.</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -125,7 +262,7 @@ interface LivreRag { slug: string; titre: string; annee: number; couches: string
|
||||
interface AuteurData { id: string; nom: string; dates: string; ecoles: string[]; ecole_principale: string; livres_rag: LivreRag[]; theses_cles: string[]; bio_courte: string }
|
||||
interface PenseesData { meta: any; ecoles: EcoleData[]; auteurs: AuteurData[] }
|
||||
|
||||
type LayoutMode = 'split' | 'carte-full' | 'chatbot-full'
|
||||
type LayoutMode = 'split' | 'carte-full' | 'chatbot-full' | 'bonpote'
|
||||
|
||||
const STORAGE_KEY = 'media-layout-mode'
|
||||
const SPLIT_RATIO_KEY = 'media-split-ratio'
|
||||
@@ -133,6 +270,9 @@ const DEFAULT_SPLIT_RATIO = 0.66
|
||||
|
||||
const ficheOpen = ref(false)
|
||||
const ficheAuteurId = ref<string | null>(null)
|
||||
const ficheEcoleOpen = ref(false)
|
||||
const ficheEcoleId = ref<string | null>(null)
|
||||
const ragInfoOpen = ref(false)
|
||||
const chatbotAuteur = ref<string | null>(null)
|
||||
const penseesData = ref<PenseesData | null>(null)
|
||||
const layoutMode = ref<LayoutMode>('split')
|
||||
@@ -143,7 +283,15 @@ const splitRatio = ref(DEFAULT_SPLIT_RATIO)
|
||||
const carteFlexBasis = computed(() => `${splitRatio.value * 100}%`)
|
||||
const chatbotFlexBasis = computed(() => `${(1 - splitRatio.value) * 100}%`)
|
||||
|
||||
const corpusCount = computed(() => penseesData.value?.auteurs.length ?? 0)
|
||||
const corpusCount = computed(() => penseesData.value?.auteurs.filter((a: any) => a.ingere).length ?? 0)
|
||||
const livresCount = computed(() => {
|
||||
if (!penseesData.value) return 0
|
||||
const slugs = new Set<string>()
|
||||
penseesData.value.auteurs
|
||||
.filter((a: any) => a.ingere)
|
||||
.forEach((a: any) => (a.livres_rag ?? []).forEach((l: any) => slugs.add(l.slug)))
|
||||
return slugs.size
|
||||
})
|
||||
|
||||
// Logique poignee draggable
|
||||
let dragStartY = 0
|
||||
@@ -178,19 +326,23 @@ function onHandleMouseup() {
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
// Restaurer le mode de layout depuis localStorage
|
||||
if (typeof window !== 'undefined') {
|
||||
const saved = localStorage.getItem(STORAGE_KEY) as LayoutMode | null
|
||||
if (saved && ['split', 'carte-full', 'chatbot-full'].includes(saved)) {
|
||||
if (saved && ['split', 'carte-full', 'chatbot-full', 'bonpote'].includes(saved)) {
|
||||
layoutMode.value = saved
|
||||
}
|
||||
const savedRatio = parseFloat(localStorage.getItem(SPLIT_RATIO_KEY) ?? '')
|
||||
if (!isNaN(savedRatio) && savedRatio >= 0.20 && savedRatio <= 0.80) {
|
||||
splitRatio.value = savedRatio
|
||||
}
|
||||
// Afficher le popup info RAG a la premiere visite
|
||||
if (!localStorage.getItem('rag-fracas-info-seen')) {
|
||||
ragInfoOpen.value = true
|
||||
localStorage.setItem('rag-fracas-info-seen', '1')
|
||||
}
|
||||
}
|
||||
try {
|
||||
penseesData.value = await $fetch<PenseesData>('/data/auteurs-pensees.json')
|
||||
penseesData.value = await $fetch<PenseesData>('/data/auteurs-pensees.json?v=4.2')
|
||||
} catch (e) {
|
||||
console.error('Erreur chargement auteurs-pensees.json', e)
|
||||
}
|
||||
@@ -222,6 +374,23 @@ function onSelectAuteur(id: string) {
|
||||
chatbotAuteur.value = null
|
||||
}
|
||||
|
||||
function onSelectEcole(id: string) {
|
||||
ficheEcoleId.value = id
|
||||
ficheEcoleOpen.value = true
|
||||
}
|
||||
|
||||
function onSelectAuteurFromEcole(auteurId: string) {
|
||||
ficheEcoleOpen.value = false
|
||||
onSelectAuteur(auteurId)
|
||||
}
|
||||
|
||||
function onInterrogerEcole(ecoleId: string) {
|
||||
ficheEcoleOpen.value = false
|
||||
const ecole = penseesData.value?.ecoles.find(e => e.id === ecoleId)
|
||||
chatbotAuteur.value = ecole?.label ?? null
|
||||
if (layoutMode.value === 'carte-full') setLayoutMode('split')
|
||||
}
|
||||
|
||||
function onInterrogerRag(auteurId: string) {
|
||||
ficheOpen.value = false
|
||||
const auteur = penseesData.value?.auteurs.find(a => a.id === auteurId)
|
||||
@@ -391,6 +560,14 @@ useHead({ title: 'AEP - Media - Carte FRACAS Bonpote' })
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* --- Transitions modal RAG info --- */
|
||||
.backdrop-enter-active,.backdrop-leave-active { transition: opacity 0.2s; }
|
||||
.backdrop-enter-from,.backdrop-leave-to { opacity: 0; }
|
||||
.modal-enter-active { transition: opacity 0.2s, transform 0.22s cubic-bezier(0.34,1.56,0.64,1); }
|
||||
.modal-leave-active { transition: opacity 0.18s, transform 0.18s ease-in; }
|
||||
.modal-enter-from { opacity: 0; transform: translate(-50%,-48%) scale(0.94); }
|
||||
.modal-leave-to { opacity: 0; transform: translate(-50%,-48%) scale(0.96); }
|
||||
|
||||
/* --- Responsive mobile (<768px) --- */
|
||||
/* Stack vertical : carte 60vh + chatbot 40vh en mode split */
|
||||
@media (max-width: 767px) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
376
scripts/build_authors_v3.mjs
Normal file
376
scripts/build_authors_v3.mjs
Normal file
@@ -0,0 +1,376 @@
|
||||
// Build auteurs-pensees.json v3.0 — Phase 8.A
|
||||
// Sync corpus JSON unifié : Bonpote authors + LightRAG ingestion flags
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
const JSON_PATH = 'C:\\Users\\jules\\Dropbox\\ATIS - IPCJRA\\1 PROJETS\\TECH - infra VPS, website pro, RAG\\nav-carte\\public\\data\\auteurs-pensees.json';
|
||||
|
||||
// === LightRAG slug prefixes (from /documents endpoint 2026-05-12) ===
|
||||
const LIGHTRAG_PREFIX_TO_AUTHOR_SLUG = {
|
||||
bookchin: 'murray-bookchin',
|
||||
brand: 'steward-brand',
|
||||
carson: 'rachel-carson',
|
||||
charbonneau: 'bernard-charbonneau',
|
||||
descola: 'philippe-descola',
|
||||
despret: 'vinciane-despret',
|
||||
eaubonne: 'francoise-deaubonne',
|
||||
ellul: 'jacques-ellul',
|
||||
federici: 'silvia-federici',
|
||||
ferdinand: 'malcolm-ferdinand',
|
||||
figueres: 'christiana-figueres',
|
||||
georgescu: 'nicholas-georgescu-roegen',
|
||||
gorz: 'andre-gorz',
|
||||
graeber: 'david-graeber',
|
||||
keith: 'david-keith',
|
||||
klein: 'naomi-klein',
|
||||
kropotkine: 'pierre-kropotkine',
|
||||
latouche: 'serge-latouche',
|
||||
latour: 'bruno-latour',
|
||||
lowy: 'michael-lowy',
|
||||
malm: 'andreas-malm',
|
||||
marx: 'karl-marx',
|
||||
meadows: 'donella-meadows',
|
||||
morizot: 'baptiste-morizot',
|
||||
naess: 'arne-naess',
|
||||
ouassak: 'fatima-ouassak',
|
||||
reclus: 'elisee-reclus',
|
||||
saito: 'kohei-saito',
|
||||
servigne: 'pablo-servigne',
|
||||
shiva: 'vandana-shiva',
|
||||
stengers: 'isabelle-stengers',
|
||||
vettese: 'troy-vettese',
|
||||
};
|
||||
|
||||
const INGESTED_AUTHOR_SLUGS = new Set(Object.values(LIGHTRAG_PREFIX_TO_AUTHOR_SLUG));
|
||||
|
||||
// === Bonpote authors (nom, dates, ecole_principale, ecoles_secondaires[]) ===
|
||||
const BONPOTE_AUTHORS = [
|
||||
// Éco-anarchisme
|
||||
['Pierre Kropotkine', '1842-1921', 'eco-anarchisme', []],
|
||||
['Élisée Reclus', '1830-1905', 'eco-anarchisme', []],
|
||||
['Murray Bookchin', '1921-2006', 'eco-anarchisme', []],
|
||||
['David Graeber', '1961-2020', 'eco-anarchisme', []],
|
||||
['James C. Scott', '1936-2024', 'eco-anarchisme', []],
|
||||
['Marshall Sahlins', '1930-2021', 'eco-anarchisme', []],
|
||||
['Pierre Clastres', '1934-1977', 'eco-anarchisme', []],
|
||||
['Cornélius Castoriadis', '1922-1997', 'eco-anarchisme', []],
|
||||
['David Harvey', '1935-', 'eco-anarchisme', ['ecosocialisme']],
|
||||
['Henri Lefebvre', '1901-1991', 'eco-anarchisme', ['ecosocialisme']],
|
||||
['Émile Gravelle', '1855-1920', 'eco-anarchisme', []],
|
||||
['Henri Zisly', '1872-1945', 'eco-anarchisme', []],
|
||||
['Edward Carpenter', '1844-1929', 'eco-anarchisme', []],
|
||||
['William Morris', '1834-1896', 'eco-anarchisme', []],
|
||||
['John Ruskin', '1819-1900', 'eco-anarchisme', []],
|
||||
['Kirkpatrick Sale', '1937-', 'eco-anarchisme', []],
|
||||
['Wendell Berry', '1934-', 'eco-anarchisme', []],
|
||||
['Kristin Ross', '1953-', 'eco-anarchisme', []],
|
||||
['Theodore Kaczynski', '1942-2023', 'eco-anarchisme', ['technocritique']],
|
||||
['Saint-Simon', '1760-1825', 'eco-anarchisme', []],
|
||||
['Auguste Comte', '1798-1857', 'eco-anarchisme', []],
|
||||
['Alberto Magnaghi', '1941-2023', 'eco-anarchisme', []],
|
||||
['Peter Berg', '1937-2011', 'eco-anarchisme', []],
|
||||
['Andreas Malm', '1977-', 'ecosocialisme', ['eco-anarchisme']],
|
||||
|
||||
// Écosocialisme
|
||||
['Karl Marx', '1818-1883', 'ecosocialisme', []],
|
||||
['Friedrich Engels', '1820-1895', 'ecosocialisme', []],
|
||||
['Rosa Luxemburg', '1871-1919', 'ecosocialisme', []],
|
||||
['Walter Benjamin', '1892-1940', 'ecosocialisme', []],
|
||||
['John Maynard Keynes', '1883-1946', 'ecosocialisme', []],
|
||||
['Pascal Lamy', '1947-', 'ecosocialisme', []],
|
||||
['Ann Pettifor', '1947-', 'ecosocialisme', []],
|
||||
['Holly Jean Buck', '', 'ecosocialisme', []],
|
||||
['Cédric Durand', '1975-', 'ecosocialisme', []],
|
||||
['Kim Stanley Robinson', '1952-', 'ecosocialisme', []],
|
||||
['André Gorz', '1923-2007', 'ecosocialisme', ['decroissance', 'technocritique']],
|
||||
['Kohei Saito', '1987-', 'ecosocialisme', ['decroissance']],
|
||||
['Razmig Keucheyan', '1975-', 'ecosocialisme', []],
|
||||
['Dominique Méda', '1962-', 'ecosocialisme', []],
|
||||
['Dominique Bourg', '1953-', 'ecosocialisme', []],
|
||||
['Troy Vettese', '', 'ecosocialisme', []],
|
||||
['Loïc Blondiaux', '1962-', 'ecosocialisme', []],
|
||||
['Drew Pendergrass', '', 'ecosocialisme', []],
|
||||
['Jason W. Moore', '', 'ecosocialisme', []],
|
||||
["James O'Connor", '1930-2017', 'ecosocialisme', []],
|
||||
['Herman Daly', '1938-2022', 'ecosocialisme', ['capitalisme-vert']],
|
||||
['John Bellamy Foster', '1953-', 'ecosocialisme', []],
|
||||
['Michael Löwy', '1938-', 'ecosocialisme', []],
|
||||
['Joel Kovel', '1936-2018', 'ecosocialisme', []],
|
||||
['Naomi Klein', '1970-', 'ecosocialisme', []],
|
||||
|
||||
// Technocritique
|
||||
['Jacques Ellul', '1912-1994', 'technocritique', []],
|
||||
['Bernard Charbonneau', '1910-1996', 'technocritique', []],
|
||||
['Lewis Mumford', '1895-1990', 'technocritique', []],
|
||||
['Alain Caillé', '1944-', 'technocritique', []],
|
||||
['Hans Jonas', '1903-1993', 'technocritique', ['ethiques-environnementales']],
|
||||
['Herbert Marcuse', '1898-1979', 'technocritique', []],
|
||||
['Günther Anders', '1902-1992', 'technocritique', []],
|
||||
['Pierre Fournier', '1937-1973', 'technocritique', []],
|
||||
['Alexandre Grothendieck', '1928-2014', 'technocritique', []],
|
||||
['Patrick Viveret', '1948-', 'technocritique', []],
|
||||
['Philippe Bihouix', '1971-', 'technocritique', []],
|
||||
['Jean Baudrillard', '1929-2007', 'technocritique', []],
|
||||
['Serge Latouche', '1940-', 'decroissance', ['technocritique']],
|
||||
['Ivan Illich', '1926-2002', 'technocritique', ['decroissance']],
|
||||
['Leopold Kohr', '1909-1994', 'technocritique', ['decroissance']],
|
||||
['Ernst Schumacher', '1911-1977', 'technocritique', ['decroissance']],
|
||||
['Nicholas Georgescu-Roegen', '1906-1994', 'decroissance', ['technocritique']],
|
||||
|
||||
// Écoféminismes
|
||||
["Françoise d'Eaubonne", '1920-2005', 'ecofeminismes', []],
|
||||
['Vandana Shiva', '1952-', 'ecofeminismes', ['ecologies-decoloniales']],
|
||||
['Starhawk', '1951-', 'ecofeminismes', []],
|
||||
['Ariel Salleh', '1944-', 'ecofeminismes', []],
|
||||
['Maria Mies', '1931-2023', 'ecofeminismes', []],
|
||||
['Carolyn Merchant', '1936-', 'ecofeminismes', []],
|
||||
['Silvia Federici', '1942-', 'ecofeminismes', []],
|
||||
['Val Plumwood', '1939-2008', 'ecofeminismes', []],
|
||||
['Susan Griffin', '1943-', 'ecofeminismes', []],
|
||||
['Veronika Bennholdt-Thomsen', '1944-', 'ecofeminismes', []],
|
||||
['Geneviève Pruvost', '1973-', 'ecofeminismes', []],
|
||||
['Donna Haraway', '1944-', 'ecofeminismes', ['pensees-vivant']],
|
||||
['Émilie Hache', '', 'ecofeminismes', []],
|
||||
['Joanna Macy', '1929-', 'ecofeminismes', ['ethiques-environnementales']],
|
||||
|
||||
// Capitalisme vert
|
||||
['Bill Gates', '1955-', 'capitalisme-vert', []],
|
||||
['Christiana Figueres', '1956-', 'capitalisme-vert', []],
|
||||
['Nicholas Stern', '1946-', 'capitalisme-vert', []],
|
||||
['Jeffrey Sachs', '1954-', 'capitalisme-vert', []],
|
||||
['Jared Diamond', '1937-', 'capitalisme-vert', ['decroissance']],
|
||||
['Jørgen Randers', '1945-', 'capitalisme-vert', ['decroissance']],
|
||||
['Donella Meadows', '1941-2001', 'decroissance', ['capitalisme-vert']],
|
||||
['Dennis Meadows', '1942-', 'decroissance', ['capitalisme-vert']],
|
||||
['Kate Raworth', '1970-', 'capitalisme-vert', []],
|
||||
['Al Gore', '1948-', 'capitalisme-vert', []],
|
||||
['Hal Harvey', '1960-', 'capitalisme-vert', []],
|
||||
['Laurence Tubiana', '1951-', 'capitalisme-vert', []],
|
||||
['Amory Lovins', '1947-', 'capitalisme-vert', []],
|
||||
['David Pearce', '1959-', 'capitalisme-vert', []],
|
||||
['Kerry Turner', '1948-', 'capitalisme-vert', []],
|
||||
['David Keith', '1963-', 'capitalisme-vert', []],
|
||||
['Ted Nordhaus', '1965-', 'capitalisme-vert', []],
|
||||
['Michael Shellenberger', '1971-', 'capitalisme-vert', []],
|
||||
['Pavan Sukhdev', '1960-', 'capitalisme-vert', []],
|
||||
['Janine Benyus', '1958-', 'capitalisme-vert', []],
|
||||
['Robert Costanza', '1950-', 'capitalisme-vert', []],
|
||||
['Peter Kareiva', '1951-', 'capitalisme-vert', []],
|
||||
['Michelle Marvier', '', 'capitalisme-vert', []],
|
||||
['Robert Lalasz', '1915-2003', 'capitalisme-vert', []],
|
||||
['Steward Brand', '1938-', 'capitalisme-vert', []],
|
||||
['Paul Crutzen', '1933-2021', 'capitalisme-vert', []],
|
||||
['Kenneth Boulding', '1910-1993', 'capitalisme-vert', []],
|
||||
['Eugene Odum', '1913-2002', 'capitalisme-vert', []],
|
||||
['Howard Odum', '1924-2002', 'capitalisme-vert', []],
|
||||
['Jean-Marc Jancovici', '1962-', 'capitalisme-vert', []],
|
||||
['Yves Cochet', '1946-', 'capitalisme-vert', ['decroissance']],
|
||||
['Pablo Servigne', '1978-', 'decroissance', ['capitalisme-vert']],
|
||||
['Gauthier Chapelle', '1968-', 'decroissance', ['capitalisme-vert']],
|
||||
|
||||
// Écologies décoloniales
|
||||
['Malcom Ferdinand', '1985-', 'ecologies-decoloniales', []],
|
||||
['Frantz Fanon', '1925-1961', 'ecologies-decoloniales', []],
|
||||
['Édouard Glissant', '1928-2011', 'ecologies-decoloniales', []],
|
||||
['Aimé Césaire', '1913-2008', 'ecologies-decoloniales', []],
|
||||
['Mohamad Amer Meziane', '', 'ecologies-decoloniales', []],
|
||||
['Chico Mendes', '1944-1988', 'ecologies-decoloniales', []],
|
||||
['Joan Martínez Alier', '1939-', 'ecologies-decoloniales', []],
|
||||
['Arturo Escobar', '1951-', 'ecologies-decoloniales', []],
|
||||
['Sous-commandant Marcos', '1957-', 'ecologies-decoloniales', []],
|
||||
['Alberto Acosta', '1948-', 'ecologies-decoloniales', []],
|
||||
['Jérôme Baschet', '1960-', 'ecologies-decoloniales', []],
|
||||
['Fatima Ouassak', '1976-', 'ecofeminismes', ['ecologies-decoloniales']],
|
||||
['William Acker', '1991-', 'ecologies-decoloniales', []],
|
||||
['Giorgos Kallis', '1972-', 'ecologies-decoloniales', ['decroissance']],
|
||||
['Bernard Lambert', '1931-1984', 'ecologies-decoloniales', []],
|
||||
|
||||
// Écofascismes
|
||||
['Alain de Benoist', '1943-', 'ecofascismes', []],
|
||||
['Paul Ralph Ehrlich', '1932-', 'ecofascismes', []],
|
||||
['Garrett Hardin', '1915-2003', 'ecofascismes', []],
|
||||
['Edward Osborne Wilson', '1929-2021', 'ecofascismes', []],
|
||||
['Thomas Malthus', '1803-1882', 'ecofascismes', []],
|
||||
['David Foreman', '1946-2022', 'ecofascismes', []],
|
||||
['Piero San Giorgio', '1971-', 'ecofascismes', []],
|
||||
|
||||
// Éthique environnementale
|
||||
['Arne Næss', '1912-2009', 'ethiques-environnementales', []],
|
||||
['Rachel Carson', '1907-1964', 'ethiques-environnementales', []],
|
||||
['Aldo Leopold', '1887-1948', 'ethiques-environnementales', []],
|
||||
['Imanishi Kinji', '1902-1992', 'ethiques-environnementales', []],
|
||||
['Paul Watson', '1950-', 'ethiques-environnementales', []],
|
||||
['John Muir', '1838-1914', 'ethiques-environnementales', []],
|
||||
['Edward Abbey', '1927-1989', 'ethiques-environnementales', []],
|
||||
['John Baird Callicott', '1941-', 'ethiques-environnementales', []],
|
||||
['Bill Mollison', '1928-2016', 'ethiques-environnementales', []],
|
||||
['David Holmgren', '1955-', 'ethiques-environnementales', []],
|
||||
['Peter Singer', '1946-', 'ethiques-environnementales', []],
|
||||
['Pierre Rabhi', '1938-2021', 'ethiques-environnementales', []],
|
||||
['Rob Hopkins', '1968-', 'ethiques-environnementales', []],
|
||||
['Cyril Dion', '1978-', 'ethiques-environnementales', []],
|
||||
['Gandhi', '1869-1948', 'ethiques-environnementales', []],
|
||||
['Gifford Pinchot', '1865-1946', 'ethiques-environnementales', []],
|
||||
['Lanza del Vasto', '1901-1981', 'ethiques-environnementales', []],
|
||||
['Jorge Mario Bergoglio', '1936-', 'ethiques-environnementales', []],
|
||||
['Gary Snyder', '1930-', 'ethiques-environnementales', []],
|
||||
['Henry David Thoreau', '1817-1862', 'ethiques-environnementales', []],
|
||||
['Ralph Waldo Emerson', '1803-1882', 'ethiques-environnementales', []],
|
||||
['José Bové', '1953-', 'ethiques-environnementales', []],
|
||||
['Glenn Albrecht', '1953-', 'ethiques-environnementales', []],
|
||||
|
||||
// Pensées du vivant
|
||||
['Bruno Latour', '1947-2022', 'pensees-vivant', []],
|
||||
['Isabelle Stengers', '1949-', 'pensees-vivant', []],
|
||||
['Vinciane Despret', '1959-', 'pensees-vivant', []],
|
||||
['Baptiste Morizot', '1983-', 'pensees-vivant', []],
|
||||
['Philippe Descola', '1949-', 'pensees-vivant', []],
|
||||
['Eduardo Viveiros de Castro', '1951-', 'pensees-vivant', []],
|
||||
['Anna Tsing', '1952-', 'pensees-vivant', []],
|
||||
['Deborah Bird Rose', '1946-2018', 'pensees-vivant', []],
|
||||
['Lynn Margulis', '1938-2011', 'pensees-vivant', []],
|
||||
['James Lovelock', '1919-2022', 'pensees-vivant', []],
|
||||
['Serge Moscovici', '1925-2014', 'pensees-vivant', []],
|
||||
['Theodore Roszak', '1933-2011', 'pensees-vivant', []],
|
||||
['Baruch Spinoza', '1632-1677', 'pensees-vivant', []],
|
||||
];
|
||||
|
||||
// Special slug overrides (match v2.1 IDs + ligatures)
|
||||
const NAME_TO_SLUG_OVERRIDES = {
|
||||
'Malcom Ferdinand': 'malcolm-ferdinand',
|
||||
"Françoise d'Eaubonne": 'francoise-deaubonne',
|
||||
'Donella Meadows': 'donella-meadows',
|
||||
'Dennis Meadows': 'dennis-meadows',
|
||||
'Arne Næss': 'arne-naess',
|
||||
'Jørgen Randers': 'jorgen-randers',
|
||||
};
|
||||
|
||||
function slugify(name) {
|
||||
// Pre-process special ligatures and chars not handled by NFKD
|
||||
let pre = name
|
||||
.replace(/[æÆ]/g, 'ae')
|
||||
.replace(/[øØ]/g, 'o')
|
||||
.replace(/[œŒ]/g, 'oe')
|
||||
.replace(/ß/g, 'ss');
|
||||
// Remove diacritical marks
|
||||
const noAccent = pre.normalize('NFKD').replace(/[̀-ͯ]/g, '');
|
||||
return noAccent
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/^-+|-+$/g, '');
|
||||
}
|
||||
|
||||
function getAuthorSlug(name) {
|
||||
if (NAME_TO_SLUG_OVERRIDES[name]) return NAME_TO_SLUG_OVERRIDES[name];
|
||||
return slugify(name);
|
||||
}
|
||||
|
||||
function main() {
|
||||
const raw = fs.readFileSync(JSON_PATH, 'utf-8');
|
||||
const data = JSON.parse(raw);
|
||||
|
||||
const existingBySlug = {};
|
||||
for (const a of data.auteurs) existingBySlug[a.id] = a;
|
||||
|
||||
const newAuthors = [];
|
||||
const seenSlugs = new Set();
|
||||
|
||||
for (const [nom, dates, ecolePrincipale, ecolesSecondaires] of BONPOTE_AUTHORS) {
|
||||
const slug = getAuthorSlug(nom);
|
||||
if (seenSlugs.has(slug)) {
|
||||
console.error(`DUPLICATE SKIP: ${nom} -> ${slug}`);
|
||||
continue;
|
||||
}
|
||||
seenSlugs.add(slug);
|
||||
|
||||
const ingere = INGESTED_AUTHOR_SLUGS.has(slug);
|
||||
const ecoles = [ecolePrincipale, ...ecolesSecondaires];
|
||||
|
||||
if (existingBySlug[slug]) {
|
||||
// Preserve enriched entry
|
||||
const entry = { ...existingBySlug[slug], ingere };
|
||||
newAuthors.push(entry);
|
||||
} else {
|
||||
// New minimal entry
|
||||
const bioProvisoire = ingere
|
||||
? `Auteur·ice ingéré·e dans le RAG ATIS, bio à enrichir lors de PRG-5.`
|
||||
: `Théoricien·ne présent·e sur le poster Bonpote (${ecolePrincipale}), non ingéré·e dans le RAG ATIS.`;
|
||||
newAuthors.push({
|
||||
id: slug,
|
||||
nom,
|
||||
dates,
|
||||
ecoles,
|
||||
ecole_principale: ecolePrincipale,
|
||||
livres_rag: [],
|
||||
theses_cles_attendues: [],
|
||||
bio_courte_provisoire: bioProvisoire,
|
||||
ingere,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Preserve any v2.1 author not in Bonpote list
|
||||
for (const [slug, entry] of Object.entries(existingBySlug)) {
|
||||
if (!seenSlugs.has(slug)) {
|
||||
const copy = { ...entry };
|
||||
if (!('ingere' in copy)) copy.ingere = INGESTED_AUTHOR_SLUGS.has(slug);
|
||||
newAuthors.push(copy);
|
||||
seenSlugs.add(slug);
|
||||
console.error(`NOTE: preserved v2.1 author not in Bonpote canonical: ${slug}`);
|
||||
}
|
||||
}
|
||||
|
||||
const auteursCount = newAuthors.length;
|
||||
const auteursIngeresCount = newAuthors.filter(a => a.ingere).length;
|
||||
|
||||
data.meta.version = '3.0';
|
||||
data.meta.updated = '2026-05-12';
|
||||
data.meta.auteurs_count = auteursCount;
|
||||
data.meta.auteurs_ingeres_count = auteursIngeresCount;
|
||||
data.meta.source = 'FRACAS Bonpote V2 oct 2024 + LightRAG corpus 12/05/2026 (v3.0 sync)';
|
||||
data.meta.note_v3_0 = 'Phase 8.A sync corpus unifie : ~140 auteurs Bonpote integres, flag ingere:true/false selon LightRAG VPS. Auteurs non-ingeres = entrees minimales (bio provisoire, livres_rag vide), a enrichir lors de PRG-4/PRG-5.';
|
||||
|
||||
data.auteurs = newAuthors;
|
||||
|
||||
fs.writeFileSync(JSON_PATH, JSON.stringify(data, null, 2), 'utf-8');
|
||||
|
||||
// Validate parse-back
|
||||
const parsedBack = JSON.parse(fs.readFileSync(JSON_PATH, 'utf-8'));
|
||||
if (parsedBack.auteurs.length !== auteursCount) {
|
||||
console.error('PARSE-BACK MISMATCH');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Stats
|
||||
const schoolsStats = {};
|
||||
for (const a of newAuthors) {
|
||||
const ep = a.ecole_principale || '?';
|
||||
if (!schoolsStats[ep]) schoolsStats[ep] = { total: 0, ingere: 0 };
|
||||
schoolsStats[ep].total++;
|
||||
if (a.ingere) schoolsStats[ep].ingere++;
|
||||
}
|
||||
|
||||
console.log('\n=== JSON v3.0 written ===');
|
||||
console.log(`Total auteurs : ${auteursCount}`);
|
||||
console.log(`Ingeres : ${auteursIngeresCount}`);
|
||||
console.log(`Non-ingeres : ${auteursCount - auteursIngeresCount}`);
|
||||
console.log(`Parse-back : OK (${parsedBack.auteurs.length} auteurs)`);
|
||||
console.log('\nPer school (ecole_principale):');
|
||||
const sortedSchools = Object.entries(schoolsStats).sort((a, b) => b[1].total - a[1].total);
|
||||
for (const [school, st] of sortedSchools) {
|
||||
console.log(` ${school.padEnd(30)} total=${String(st.total).padStart(3)} ingere=${String(st.ingere).padStart(3)} non-ing=${String(st.total - st.ingere).padStart(3)}`);
|
||||
}
|
||||
|
||||
// Top 5 schools with most non-ingested
|
||||
const nonIngStats = sortedSchools
|
||||
.map(([k, v]) => [k, v.total - v.ingere])
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.slice(0, 5);
|
||||
console.log('\nTop 5 ecoles avec le plus de non-ingeres (PRG-4 priorities):');
|
||||
for (const [school, count] of nonIngStats) {
|
||||
console.log(` ${school.padEnd(30)} non-ing=${count}`);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -1,4 +1,6 @@
|
||||
import type { H3Event } from 'h3'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import { checkRateLimitJson } from '~/server/utils/rateLimitJson'
|
||||
|
||||
interface ChatbotPenseesRequest {
|
||||
@@ -7,11 +9,25 @@ interface ChatbotPenseesRequest {
|
||||
corpus?: 'pensees' | 'projets' | 'both'
|
||||
filter_couche?: 'fond' | 'forme' | 'structure' | null
|
||||
filter_ecole?: string | null
|
||||
auteur_slug?: string | null
|
||||
history?: Array<{ role: 'user' | 'assistant'; content: string }>
|
||||
}
|
||||
|
||||
interface LightRAGReference {
|
||||
reference_id?: string
|
||||
file_path?: string
|
||||
content?: string | null
|
||||
}
|
||||
|
||||
interface LightRAGQueryResponse {
|
||||
response: string
|
||||
references?: LightRAGReference[]
|
||||
}
|
||||
|
||||
interface AuteurMini {
|
||||
id: string
|
||||
nom: string
|
||||
ingere?: boolean
|
||||
}
|
||||
|
||||
const SYSTEM_PREFACE_PENSEES = `Tu es un agent du RAG Pensées Écologiques, infrastructure militante du collectif trans-former.fr.
|
||||
@@ -47,6 +63,37 @@ Règles :
|
||||
- Ton praticien militant : direct, pas neutre, ancré dans la pratique architecturale.
|
||||
- Réponse en français, dense, sans délayage.`
|
||||
|
||||
function buildPrefaceAuteur(nomAuteur: string, slug: string): string {
|
||||
return `Tu réponds EXCLUSIVEMENT depuis les livres de ${nomAuteur} présents dans le RAG (fichiers commençant par "${slug}__").
|
||||
Si la question sort du périmètre de cet auteur, indique-le et propose de l'aborder sans le hashtag pour interroger la carte entière. Reste fidèle au style et à la pensée de ${nomAuteur}. Cite toujours le livre.
|
||||
|
||||
Règles :
|
||||
- Cite les sources (titre du livre) à chaque assertion.
|
||||
- Pas d'hallucination. Si l'info n'est pas dans le corpus de cet auteur, dis-le.
|
||||
- N'introduis JAMAIS d'autres auteurs sauf si ${nomAuteur} les commente explicitement.
|
||||
- Ton politique direct, pas de neutralité fade.
|
||||
- Réponse en français, dense, sans délayage.`
|
||||
}
|
||||
|
||||
// Chargement (et cache) de la liste des auteurs ingérés pour validation du slug
|
||||
let auteursIngeresCache: AuteurMini[] | null = null
|
||||
function loadAuteursIngeres(): AuteurMini[] {
|
||||
if (auteursIngeresCache) return auteursIngeresCache
|
||||
try {
|
||||
const jsonPath = join(process.cwd(), 'public', 'data', 'auteurs-pensees.json')
|
||||
const raw = readFileSync(jsonPath, 'utf-8')
|
||||
const data = JSON.parse(raw)
|
||||
const list: AuteurMini[] = (data.auteurs ?? [])
|
||||
.filter((a: any) => a.ingere === true)
|
||||
.map((a: any) => ({ id: String(a.id), nom: String(a.nom), ingere: true }))
|
||||
auteursIngeresCache = list
|
||||
return list
|
||||
} catch {
|
||||
auteursIngeresCache = []
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
export default defineEventHandler(async (event: H3Event) => {
|
||||
const config = useRuntimeConfig(event)
|
||||
|
||||
@@ -72,13 +119,26 @@ export default defineEventHandler(async (event: H3Event) => {
|
||||
const corpus = body.corpus || 'both'
|
||||
const ragUrl = (config.ragPeUrl as string) || 'http://localhost:9621'
|
||||
|
||||
// Préface adaptative selon corpus demandé
|
||||
const systemPreface =
|
||||
corpus === 'pensees'
|
||||
? SYSTEM_PREFACE_PENSEES
|
||||
: corpus === 'projets'
|
||||
? SYSTEM_PREFACE_PROJETS
|
||||
: SYSTEM_PREFACE_BOTH
|
||||
// Validation auteur_slug (Phase 8.E) : match contre la liste des auteurs ingérés
|
||||
const auteurSlug = body.auteur_slug?.trim().toLowerCase() || null
|
||||
let nomAuteurMatch: string | null = null
|
||||
if (auteurSlug) {
|
||||
const ingeres = loadAuteursIngeres()
|
||||
const auteur = ingeres.find(a => a.id === auteurSlug)
|
||||
nomAuteurMatch = auteur?.nom ?? null
|
||||
}
|
||||
|
||||
// Préface adaptative : auteur prioritaire si slug matché, sinon corpus
|
||||
let systemPreface: string
|
||||
if (auteurSlug && nomAuteurMatch) {
|
||||
systemPreface = buildPrefaceAuteur(nomAuteurMatch, auteurSlug)
|
||||
} else if (corpus === 'pensees') {
|
||||
systemPreface = SYSTEM_PREFACE_PENSEES
|
||||
} else if (corpus === 'projets') {
|
||||
systemPreface = SYSTEM_PREFACE_PROJETS
|
||||
} else {
|
||||
systemPreface = SYSTEM_PREFACE_BOTH
|
||||
}
|
||||
|
||||
// 3. Health guard — LightRAG down = erreur claire, pas de fallback hallucinatoire
|
||||
try {
|
||||
@@ -93,11 +153,20 @@ export default defineEventHandler(async (event: H3Event) => {
|
||||
// 4. Call LightRAG VPS — préface système injectée dans la query
|
||||
const ragQuery = `${systemPreface}\n\nQuestion : ${query}`
|
||||
|
||||
// Construction du body : hl_keywords + ll_keywords si auteur ciblé
|
||||
// NB : LightRAG ne supporte ni keyword_filter ni ids ni metadata_filter (preflight OpenAPI confirmé).
|
||||
// hl_keywords / ll_keywords sont les seuls leviers natifs de priorisation par auteur.
|
||||
const ragBody: Record<string, unknown> = { query: ragQuery, mode }
|
||||
if (auteurSlug && nomAuteurMatch) {
|
||||
ragBody.hl_keywords = [nomAuteurMatch, auteurSlug]
|
||||
ragBody.ll_keywords = [auteurSlug]
|
||||
}
|
||||
|
||||
let ragResponse: LightRAGQueryResponse
|
||||
try {
|
||||
ragResponse = await $fetch<LightRAGQueryResponse>(`${ragUrl}/query`, {
|
||||
method: 'POST',
|
||||
body: { query: ragQuery, mode },
|
||||
body: ragBody,
|
||||
timeout: 90000,
|
||||
})
|
||||
} catch (e: any) {
|
||||
@@ -108,11 +177,28 @@ export default defineEventHandler(async (event: H3Event) => {
|
||||
throw createError({ statusCode: 504, message: 'RAG en cours de processing — réessaie dans quelques secondes.' })
|
||||
}
|
||||
|
||||
// Fallback post-process : si auteur ciblé et que les references LightRAG remontent
|
||||
// des chunks hors slug__, on l'indique pour transparence. La préface LLM est la garde principale.
|
||||
let chunksOffTarget = 0
|
||||
let chunksOnTarget = 0
|
||||
if (auteurSlug && nomAuteurMatch && Array.isArray(ragResponse.references)) {
|
||||
const slugPrefix = `${auteurSlug}__`
|
||||
for (const ref of ragResponse.references) {
|
||||
const fp = (ref.file_path ?? '').toLowerCase()
|
||||
if (!fp) continue
|
||||
if (fp.startsWith(slugPrefix)) chunksOnTarget++
|
||||
else chunksOffTarget++
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Retour formaté
|
||||
return {
|
||||
response: ragResponse.response ?? '',
|
||||
mode,
|
||||
corpus,
|
||||
auteur: auteurSlug && nomAuteurMatch ? { slug: auteurSlug, nom: nomAuteurMatch } : null,
|
||||
auteur_unmatched: auteurSlug && !nomAuteurMatch ? auteurSlug : null,
|
||||
auteur_chunks: auteurSlug && nomAuteurMatch ? { on_target: chunksOnTarget, off_target: chunksOffTarget } : null,
|
||||
filter: { couche: body.filter_couche ?? null, ecole: body.filter_ecole ?? null },
|
||||
timestamp: new Date().toISOString(),
|
||||
}
|
||||
|
||||
@@ -82,18 +82,18 @@ export default defineEventHandler(async (event) => {
|
||||
|
||||
const systemPrompt = SYSTEM_PROMPT.replace('{{STRUCTURES_JSON}}', JSON.stringify(context, null, 0))
|
||||
|
||||
const mistralApiKey = config.mistralApiKey as string
|
||||
if (!mistralApiKey) throw createError({ statusCode: 500, message: 'Clé API Mistral manquante.' })
|
||||
const nebiusApiKey = config.nebiusApiKey as string
|
||||
if (!nebiusApiKey) throw createError({ statusCode: 500, message: 'Clé API Nebius manquante.' })
|
||||
|
||||
let mistralRaw: string
|
||||
try {
|
||||
const res = await $fetch<{ choices: { message: { content: string } }[] }>(
|
||||
'https://api.mistral.ai/v1/chat/completions',
|
||||
'https://api.tokenfactory.nebius.com/v1/chat/completions',
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { Authorization: `Bearer ${mistralApiKey}`, 'Content-Type': 'application/json' },
|
||||
headers: { Authorization: `Bearer ${nebiusApiKey}`, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
model: 'mistral-small-latest',
|
||||
model: 'deepseek-ai/DeepSeek-V3.2',
|
||||
temperature: 0.3,
|
||||
max_tokens: 700,
|
||||
response_format: { type: 'json_object' },
|
||||
|
||||
@@ -91,20 +91,20 @@ export default defineEventHandler(async (event) => {
|
||||
|
||||
const systemPrompt = SYSTEM_PROMPT.replace('{{PLATEFORMES_JSON}}', JSON.stringify(context, null, 0))
|
||||
|
||||
const mistralApiKey = config.mistralApiKey as string
|
||||
if (!mistralApiKey) {
|
||||
throw createError({ statusCode: 500, statusMessage: 'Clé API Mistral manquante.' })
|
||||
const nebiusApiKey = config.nebiusApiKey as string
|
||||
if (!nebiusApiKey) {
|
||||
throw createError({ statusCode: 500, statusMessage: 'Clé API Nebius manquante.' })
|
||||
}
|
||||
|
||||
let mistralRaw: string
|
||||
try {
|
||||
const res = await $fetch<{ choices: { message: { content: string } }[] }>(
|
||||
'https://api.mistral.ai/v1/chat/completions',
|
||||
'https://api.tokenfactory.nebius.com/v1/chat/completions',
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { Authorization: `Bearer ${mistralApiKey}`, 'Content-Type': 'application/json' },
|
||||
headers: { Authorization: `Bearer ${nebiusApiKey}`, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
model: 'mistral-small-latest',
|
||||
model: 'deepseek-ai/DeepSeek-V3.2',
|
||||
temperature: 0.3,
|
||||
max_tokens: 700,
|
||||
response_format: { type: 'json_object' },
|
||||
|
||||
@@ -145,19 +145,22 @@ export default defineEventHandler(async (event) => {
|
||||
|
||||
const systemPrompt = SYSTEM_PROMPT_V2.replace('{{CONTEXTE_RAG}}', contextStr)
|
||||
|
||||
// 7. Mistral Small - génération réponse
|
||||
// 7. Nebius DeepSeek-V3.2 - génération réponse
|
||||
const nebiusApiKey = config.nebiusApiKey as string
|
||||
if (!nebiusApiKey) throw createError({ statusCode: 500, statusMessage: 'Clé API Nebius manquante.' })
|
||||
|
||||
let mistralRaw: string
|
||||
try {
|
||||
const mistralRes = await $fetch<{
|
||||
const nebiusRes = await $fetch<{
|
||||
choices: { message: { content: string } }[]
|
||||
}>('https://api.mistral.ai/v1/chat/completions', {
|
||||
}>('https://api.tokenfactory.nebius.com/v1/chat/completions', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${mistralApiKey}`,
|
||||
Authorization: `Bearer ${nebiusApiKey}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: 'mistral-small-latest',
|
||||
model: 'deepseek-ai/DeepSeek-V3.2',
|
||||
temperature: 0.3,
|
||||
max_tokens: 600,
|
||||
response_format: { type: 'json_object' },
|
||||
@@ -167,10 +170,10 @@ export default defineEventHandler(async (event) => {
|
||||
]
|
||||
})
|
||||
})
|
||||
mistralRaw = mistralRes.choices?.[0]?.message?.content ?? '{}'
|
||||
mistralRaw = nebiusRes.choices?.[0]?.message?.content ?? '{}'
|
||||
} catch (e: any) {
|
||||
console.error('[chatbot-v2] Erreur Mistral Small :', e?.message ?? e)
|
||||
throw createError({ statusCode: 502, statusMessage: 'Erreur appel Mistral Small.' })
|
||||
console.error('[chatbot-v2] Erreur Nebius DeepSeek :', e?.message ?? e)
|
||||
throw createError({ statusCode: 502, statusMessage: 'Erreur appel Nebius DeepSeek.' })
|
||||
}
|
||||
|
||||
// 8. Parse JSON
|
||||
|
||||
@@ -247,13 +247,13 @@ export default defineEventHandler(async (event) => {
|
||||
JSON.stringify(fichesContext, null, 0),
|
||||
)
|
||||
|
||||
// 6. Appel Mistral Small
|
||||
const mistralApiKey = config.mistralApiKey as string
|
||||
// 6. Appel Nebius DeepSeek-V3.2
|
||||
const nebiusApiKey = config.nebiusApiKey as string
|
||||
|
||||
if (!mistralApiKey) {
|
||||
if (!nebiusApiKey) {
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: 'Clé API Mistral manquante.',
|
||||
statusMessage: 'Clé API Nebius manquante.',
|
||||
})
|
||||
}
|
||||
|
||||
@@ -262,17 +262,17 @@ export default defineEventHandler(async (event) => {
|
||||
let tokensOut = 0
|
||||
|
||||
try {
|
||||
const mistralRes = await $fetch<{
|
||||
const nebiusRes = await $fetch<{
|
||||
choices: { message: { content: string } }[]
|
||||
usage?: { prompt_tokens: number; completion_tokens: number }
|
||||
}>('https://api.mistral.ai/v1/chat/completions', {
|
||||
}>('https://api.tokenfactory.nebius.com/v1/chat/completions', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${mistralApiKey}`,
|
||||
Authorization: `Bearer ${nebiusApiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: 'mistral-small-latest',
|
||||
model: 'deepseek-ai/DeepSeek-V3.2',
|
||||
temperature: 0.3,
|
||||
max_tokens: 600,
|
||||
response_format: { type: 'json_object' },
|
||||
@@ -283,11 +283,11 @@ export default defineEventHandler(async (event) => {
|
||||
}),
|
||||
})
|
||||
|
||||
mistralRaw = mistralRes.choices?.[0]?.message?.content ?? '{}'
|
||||
tokensIn = mistralRes.usage?.prompt_tokens ?? 0
|
||||
tokensOut = mistralRes.usage?.completion_tokens ?? 0
|
||||
mistralRaw = nebiusRes.choices?.[0]?.message?.content ?? '{}'
|
||||
tokensIn = nebiusRes.usage?.prompt_tokens ?? 0
|
||||
tokensOut = nebiusRes.usage?.completion_tokens ?? 0
|
||||
} catch (e: any) {
|
||||
console.error('[chatbot] Erreur Mistral Small:', e?.message ?? e)
|
||||
console.error('[chatbot] Erreur Nebius DeepSeek:', e?.message ?? e)
|
||||
throw createError({
|
||||
statusCode: 502,
|
||||
statusMessage: 'Erreur appel IA — réessaie dans quelques instants.',
|
||||
|
||||
Reference in New Issue
Block a user