Compare commits
5 Commits
feat/outil
...
11732a6a4b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
11732a6a4b | ||
|
|
538c9a1214 | ||
|
|
8d673482b6 | ||
|
|
586742d90e | ||
|
|
668ae5caff |
@@ -11,6 +11,56 @@ Journal technique de la V2. Décisions, anomalies, points bloquants, TODOs.
|
||||
|
||||
---
|
||||
|
||||
## 2026-05-08 — Fix mobile + chatbot prod (cause racine résolue)
|
||||
|
||||
**Commits :** session loggée sur main (pushé sur gitea)
|
||||
**Pattern :** pilote direct, 2 batches successifs, ~3h, 11 fichiers
|
||||
|
||||
### Cause racine bug "chatbot Carte 1 == Carte 2"
|
||||
|
||||
`/api/chatbot-reseaux` était **404 en prod** (jamais déployé) — explique pourquoi 5 cycles de fix précédents (ChatbotReseaux.vue prop endpoint, useRoute fallback, useMarkdown direct, etc.) n'ont rien donné : le code source était correct depuis le début. Le rebuild + redeploy de cette session résout le bug.
|
||||
|
||||
**Verif :** `curl -s -X POST https://aep.trans-former.fr/api/chatbot-reseaux` → 200 + réponse distincte de `/api/chatbot`.
|
||||
|
||||
### Batch 1 — fixes mobile principaux
|
||||
|
||||
- Hamburger app.vue : ajout Jobs + Manifeste + Soutenir, ré-ordonnancement (Manifeste dans 2e groupe avec À propos/Soutenir/Signaler)
|
||||
- BandeauBas.vue : FAB cœur jaune mobile retiré (Soutenir migré dans hamburger via lien Liberapay direct)
|
||||
- agences.vue mobile : 3e onglet "Graphe" ajouté + masquage MobileSheet en mode graphe (canvas fullscreen)
|
||||
- a-propos.vue : section 1 "Mission" retirée (devient pop-up Carte 1) + `overflow-x: hidden` sur `.apropos-page` + retrait `white-space: nowrap` problématique sur `.badge-detail`
|
||||
- pages/manifeste.vue : nouvelle page (texte version `manifeste-page-carto-V1.md`, sans le diagramme ASCII pour V1 web)
|
||||
- components/MissionPopup.vue : nouveau composant générique (props `title`, `ctaLabel`, `storageKey`, slot pour contenu, `:slotted()` pour styles)
|
||||
- index.vue : intégration MissionPopup + bouton (i) `position:fixed` bottom-left + auto-show 1ère visite via `localStorage.aep_mission_seen`
|
||||
- trouver-du-taf.vue : toggle "Filtres [N] [chevron]" mobile-only (`@media max-width: 767px`) avec `taff-filters-collapsible` max-height transition
|
||||
- FicheModal.vue + FicheModalV2.vue : sur mobile `top: 76px` + `max-height: calc(100dvh - 92px)` au lieu de `top: 50% translate(-50%, -50%)` + `max-height: 90vh` qui mordait sur le header
|
||||
|
||||
### Batch 2 — pop-up Carte 2, logo, intro Jobs, labels graphe
|
||||
|
||||
- agences.vue : pop-up Réseaux AEP avec MissionPopup (storageKey `aep_reseaux_seen`, ctaLabel "Explorer les 120 réseaux") + bouton (i) flottant
|
||||
- app.vue logo header : badge AEP + 2 spans `logo-line-1` ("Architecture") / `logo-line-2` ("d'Écologie Politique") avec font-size responsive (0.7rem mobile → 0.85rem ≥1024)
|
||||
- trouver-du-taf.vue : `<details class="taff-pedago" open>` avec 3 blocs (deux onglets, trois étiquettes, cinq axes) + onglet "Plateformes B2C" → "Pour archi indépendants"
|
||||
- GraphView.vue : `d3NodeSelection.filter(type==='structure').append('text')` avec class `graph-struct-label`, `dy: -(d.r + 5)`, font-size 9.5px, halo via `paint-order: stroke; stroke: var(--nav-bg)` (style global non-scoped pour piercer D3)
|
||||
|
||||
### Bug d'opération à retenir
|
||||
|
||||
Lors du 1er déploiement batch 2, `bash deploy.sh` semblait OK (HTTP 200) mais le HTML en prod ne contenait pas les modifs. **Cause** : Dropbox sync a effacé `.output/` entre `npm run build` et le tar SCP — le tar a uploadé un `.output` quasi-vide. Solution : 2e cycle clean (`Remove-Item .nuxt/dist + .output`) + rebuild + redeploy avec `yes y |` (skip confirm interactif `.env diff`).
|
||||
|
||||
**Réflexe à intégrer** : après build, vérifier `grep -o "<un-fragment-de-modif>" .output/public/_nuxt/*.js | head` AVANT le deploy. Si 0 match → ne pas deploy, rebuild.
|
||||
|
||||
### Bug de communication à retenir
|
||||
|
||||
Jules a signalé "le logo n'a pas marché", "B2C pas renommé", "hamburger pas modifié" alors que le HTML en prod contenait bien les modifs (vérifié curl avec `?nc=$(date +%s)`). **Cause** : cache navigateur / service worker Nuxt. Réflexe à mettre en place pour /done de toute session web : si Jules dit "ça n'apparaît pas", vérifier curl en bypass cache AVANT de chercher un bug. Si match curl → demander hard refresh (Ctrl+Shift+R).
|
||||
|
||||
### Reste à faire (batch 3)
|
||||
|
||||
Voir `0 INBOX/PROMPTS/cascade-megaboum/REPRISE-aep-carto-fix-batch3.md` :
|
||||
- Bouton "+" → sélecteur 3 cartes (Entraide/Réseaux/Jobs)
|
||||
- Pop-up explication 5 axes Jobs (paragraphe par axe)
|
||||
- Pop-up Carte 1 visibilité (option à clarifier avec Jules)
|
||||
- GraphView Carte 1 (centres = hashtags, couche échelle activable) — gros chantier session dédiée
|
||||
|
||||
---
|
||||
|
||||
## 2026-04-27 — Session V3 : Finition mobile + Blog Liberapay + 3 deploys
|
||||
|
||||
**Commit :** `a02a555` — feat(mobile): accordéon outremer, hamburger nav, logo AEP, fiches cliquables, chatbot fullscreen
|
||||
|
||||
103
app.vue
103
app.vue
@@ -108,14 +108,52 @@
|
||||
>
|
||||
Signaler
|
||||
</NuxtLink>
|
||||
<!-- Proposer une ressource -->
|
||||
<NuxtLink
|
||||
to="/contribuer"
|
||||
class="px-3 py-1.5 rounded-lg text-sm font-semibold transition-all hover:opacity-80 hidden sm:inline-flex items-center gap-1"
|
||||
<!-- Proposer — popover 3 choix -->
|
||||
<div class="hidden sm:block relative" ref="proposerAnchor" data-proposer-popover>
|
||||
<button
|
||||
@click="proposerOpen = !proposerOpen"
|
||||
class="px-3 py-1.5 rounded-lg text-sm font-semibold transition-all hover:opacity-80 inline-flex items-center gap-1"
|
||||
style="background: var(--nav-accent); color: var(--nav-text);"
|
||||
aria-label="Proposer une contribution"
|
||||
>
|
||||
+ Proposer
|
||||
</button>
|
||||
<div
|
||||
v-if="proposerOpen"
|
||||
class="absolute right-0 top-full mt-1 rounded-lg shadow-lg min-w-[240px] py-1"
|
||||
style="background: var(--nav-surface); border: 1px solid var(--nav-bg-alt); z-index: 9999;"
|
||||
>
|
||||
<NuxtLink
|
||||
to="/contribuer"
|
||||
class="flex items-center justify-between px-4 py-2.5 text-sm font-medium transition-opacity hover:opacity-70"
|
||||
style="color: var(--nav-text);"
|
||||
@click="proposerOpen = false"
|
||||
>
|
||||
<span>Fiche Entraide <span style="color: var(--nav-text-muted); font-weight: 400; font-size: 0.7rem; display: block;">Carte 1 — Écosystème archi</span></span>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="flex-shrink:0; color: var(--nav-text-muted);"><polyline points="9 18 15 12 9 6"/></svg>
|
||||
</NuxtLink>
|
||||
<div style="height: 1px; background: var(--nav-bg-alt); margin: 2px 0;"></div>
|
||||
<NuxtLink
|
||||
to="/contribuer-reseau"
|
||||
class="flex items-center justify-between px-4 py-2.5 text-sm font-medium transition-opacity hover:opacity-70"
|
||||
style="color: var(--nav-text);"
|
||||
@click="proposerOpen = false"
|
||||
>
|
||||
<span>Réseau / collectif <span style="color: var(--nav-text-muted); font-weight: 400; font-size: 0.7rem; display: block;">Carte 2 — Réseaux AEP</span></span>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="flex-shrink:0; color: var(--nav-text-muted);"><polyline points="9 18 15 12 9 6"/></svg>
|
||||
</NuxtLink>
|
||||
<div style="height: 1px; background: var(--nav-bg-alt); margin: 2px 0;"></div>
|
||||
<NuxtLink
|
||||
to="/contribuer-job"
|
||||
class="flex items-center justify-between px-4 py-2.5 text-sm font-medium transition-opacity hover:opacity-70"
|
||||
style="color: var(--nav-text);"
|
||||
@click="proposerOpen = false"
|
||||
>
|
||||
<span>Plateforme jobs <span style="color: var(--nav-text-muted); font-weight: 400; font-size: 0.7rem; display: block;">Carte 3 — Jobs archi</span></span>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="flex-shrink:0; color: var(--nav-text-muted);"><polyline points="9 18 15 12 9 6"/></svg>
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toggle dark mode -->
|
||||
<button
|
||||
@@ -137,18 +175,40 @@
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<!-- Mobile : contribuer icône -->
|
||||
<NuxtLink
|
||||
to="/contribuer"
|
||||
class="sm:hidden p-2 rounded-lg"
|
||||
<!-- Mobile : contribuer icône → popover -->
|
||||
<div class="sm:hidden relative" data-proposer-popover>
|
||||
<button
|
||||
@click="proposerOpen = !proposerOpen"
|
||||
class="p-2 rounded-lg"
|
||||
style="background: var(--nav-accent); color: var(--nav-text);"
|
||||
title="Contribuer une fiche"
|
||||
title="Contribuer"
|
||||
aria-label="Contribuer"
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/>
|
||||
</svg>
|
||||
</button>
|
||||
<div
|
||||
v-if="proposerOpen"
|
||||
class="absolute right-0 top-full mt-1 rounded-lg shadow-lg min-w-[220px] py-1"
|
||||
style="background: var(--nav-surface); border: 1px solid var(--nav-bg-alt); z-index: 9999;"
|
||||
>
|
||||
<NuxtLink to="/contribuer" class="flex items-center justify-between px-4 py-2.5 text-sm font-medium transition-opacity hover:opacity-70" style="color: var(--nav-text);" @click="proposerOpen = false">
|
||||
<span>Fiche Entraide</span>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="flex-shrink:0; color: var(--nav-text-muted);"><polyline points="9 18 15 12 9 6"/></svg>
|
||||
</NuxtLink>
|
||||
<div style="height: 1px; background: var(--nav-bg-alt); margin: 2px 0;"></div>
|
||||
<NuxtLink to="/contribuer-reseau" class="flex items-center justify-between px-4 py-2.5 text-sm font-medium transition-opacity hover:opacity-70" style="color: var(--nav-text);" @click="proposerOpen = false">
|
||||
<span>Réseau / collectif</span>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="flex-shrink:0; color: var(--nav-text-muted);"><polyline points="9 18 15 12 9 6"/></svg>
|
||||
</NuxtLink>
|
||||
<div style="height: 1px; background: var(--nav-bg-alt); margin: 2px 0;"></div>
|
||||
<NuxtLink to="/contribuer-job" class="flex items-center justify-between px-4 py-2.5 text-sm font-medium transition-opacity hover:opacity-70" style="color: var(--nav-text);" @click="proposerOpen = false">
|
||||
<span>Plateforme jobs</span>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="flex-shrink:0; color: var(--nav-text-muted);"><polyline points="9 18 15 12 9 6"/></svg>
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Hamburger mobile (lg:hidden) — toujours en dernier à droite -->
|
||||
<div class="lg:hidden relative">
|
||||
@@ -205,6 +265,31 @@ const route = useRoute()
|
||||
const hamburgerOpen = ref(false)
|
||||
watch(() => route.path, () => { hamburgerOpen.value = false })
|
||||
|
||||
// ── Popover "+ Proposer" ─────────────────────────────────────────────────
|
||||
const proposerOpen = ref(false)
|
||||
const proposerAnchor = ref<HTMLElement | null>(null)
|
||||
|
||||
function onClickOutsideProposer(e: MouseEvent) {
|
||||
// Ferme si le clic est hors de tout élément portant data-proposer-popover
|
||||
const target = e.target as HTMLElement
|
||||
if (!target.closest('[data-proposer-popover]')) {
|
||||
proposerOpen.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watch(proposerOpen, (open) => {
|
||||
if (open) {
|
||||
// Délai court pour ne pas attraper le clic d'ouverture lui-même
|
||||
setTimeout(() => document.addEventListener('click', onClickOutsideProposer, true), 10)
|
||||
} else {
|
||||
document.removeEventListener('click', onClickOutsideProposer, true)
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('click', onClickOutsideProposer, true)
|
||||
})
|
||||
|
||||
// ── Dark mode ─────────────────────────────────────────────────────────────
|
||||
const isDark = ref(false)
|
||||
|
||||
|
||||
135
components/CartePensees.vue
Normal file
135
components/CartePensees.vue
Normal file
@@ -0,0 +1,135 @@
|
||||
<template>
|
||||
<div style="width: 100%; height: 100%; position: relative; background: var(--nav-bg);">
|
||||
<svg ref="svgRef" style="width: 100%; height: 100%;"></svg>
|
||||
<div ref="tooltipRef" style="
|
||||
position: absolute; pointer-events: none;
|
||||
background: var(--nav-surface); border: 1px solid var(--nav-bg-alt);
|
||||
border-radius: 6px; padding: 8px 12px; font-size: 0.78rem;
|
||||
color: var(--nav-text); max-width: 240px; box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||
opacity: 0; transition: opacity 0.15s; z-index: 100;
|
||||
"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<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 PenseesData { ecoles: EcoleData[]; auteurs: AuteurData[] }
|
||||
|
||||
const props = defineProps<{ data: PenseesData | null; active?: boolean }>()
|
||||
const emit = defineEmits<{ 'select-auteur': [id: string] }>()
|
||||
|
||||
const svgRef = ref<SVGElement | null>(null)
|
||||
const tooltipRef = ref<HTMLElement | null>(null)
|
||||
let simulation: any = null
|
||||
let d3NodeSel: any = null
|
||||
let d3LinkSel: any = null
|
||||
|
||||
async function initGraph() {
|
||||
if (!svgRef.value || !props.data) return
|
||||
const d3 = await import('d3')
|
||||
const svgEl = svgRef.value
|
||||
const W = svgEl.clientWidth || 900
|
||||
const H = svgEl.clientHeight || 600
|
||||
|
||||
d3.select(svgEl).selectAll('*').remove()
|
||||
const svg = d3.select(svgEl).attr('viewBox', `0 0 ${W} ${H}`)
|
||||
const g = svg.append('g')
|
||||
|
||||
svg.call(d3.zoom<SVGElement, unknown>().scaleExtent([0.3, 4]).on('zoom', (e) => g.attr('transform', e.transform)) as any)
|
||||
|
||||
const ecoleMap = new Map<string, EcoleData>(props.data.ecoles.map(e => [e.id, e]))
|
||||
|
||||
const ecoleNodes: any[] = props.data.ecoles.map(e => ({
|
||||
id: `ecole-${e.id}`, type: 'ecole', ecoleId: e.id, label: e.label, color: e.color, r: 38,
|
||||
x: W * e.x_hint, y: H * e.y_hint, fx: W * e.x_hint, fy: H * e.y_hint,
|
||||
}))
|
||||
|
||||
const auteurNodes: any[] = props.data.auteurs.map(a => ({
|
||||
id: a.id, type: 'auteur', nom: a.nom, dates: a.dates, bio_courte: a.bio_courte,
|
||||
ecole_principale: a.ecole_principale,
|
||||
color: ecoleMap.get(a.ecole_principale)?.color ?? '#888', r: 11,
|
||||
}))
|
||||
|
||||
const allNodes = [...ecoleNodes, ...auteurNodes]
|
||||
const links: any[] = []
|
||||
props.data.auteurs.forEach(a => {
|
||||
links.push({ source: a.id, target: `ecole-${a.ecole_principale}`, strength: 0.65 })
|
||||
a.ecoles.filter(e => e !== a.ecole_principale).forEach(e => links.push({ source: a.id, target: `ecole-${e}`, strength: 0.25 }))
|
||||
})
|
||||
|
||||
if (simulation) simulation.stop()
|
||||
simulation = d3.forceSimulation(allNodes)
|
||||
.force('link', d3.forceLink(links).id((d: any) => d.id).distance(90).strength((d: any) => d.strength ?? 0.5))
|
||||
.force('charge', d3.forceManyBody().strength(-80))
|
||||
.force('center', d3.forceCenter(W / 2, H / 2))
|
||||
.force('collision', d3.forceCollide().radius((d: any) => d.r + 5))
|
||||
|
||||
d3LinkSel = g.append('g').selectAll('line').data(links).join('line')
|
||||
.attr('stroke', 'rgba(150,150,150,0.3)').attr('stroke-width', 1.2)
|
||||
|
||||
d3NodeSel = g.append('g').selectAll('g').data(allNodes).join('g')
|
||||
.style('cursor', (d: any) => d.type === 'auteur' ? '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); if (d.type !== 'ecole') { d.fx = null; d.fy = null } }))
|
||||
.on('click', (e: any, d: any) => { e.stopPropagation(); if (d.type === 'auteur') emit('select-auteur', d.id) })
|
||||
|
||||
d3NodeSel.append('circle')
|
||||
.attr('r', (d: any) => d.r)
|
||||
.attr('fill', (d: any) => d.type === 'ecole' ? d.color : d.color + 'cc')
|
||||
.attr('stroke', (d: any) => d.type === 'ecole' ? 'rgba(255,255,255,0.6)' : d.color)
|
||||
.attr('stroke-width', (d: any) => d.type === 'ecole' ? 3 : 1.5)
|
||||
|
||||
d3NodeSel.filter((d: any) => d.type === 'ecole').append('text')
|
||||
.attr('text-anchor', 'middle').attr('dy', '0.35em').attr('font-size', '10px').attr('font-weight', '700').attr('fill', 'white')
|
||||
.style('pointer-events', 'none')
|
||||
.each(function(d: any) {
|
||||
const el = d3.select(this as any)
|
||||
const words: string[] = d.label.split(' ')
|
||||
if (words.length <= 2) { el.text(d.label) } else {
|
||||
const mid = Math.ceil(words.length / 2)
|
||||
el.append('tspan').attr('x', 0).attr('dy', '-0.6em').text(words.slice(0, mid).join(' '))
|
||||
el.append('tspan').attr('x', 0).attr('dy', '1.2em').text(words.slice(mid).join(' '))
|
||||
}
|
||||
})
|
||||
|
||||
d3NodeSel.filter((d: any) => d.type === 'auteur').append('text')
|
||||
.attr('class', 'pensees-auteur-label')
|
||||
.text((d: any) => d.nom.split(' ').pop() ?? d.nom)
|
||||
.attr('text-anchor', 'middle').attr('dy', (d: any) => -(d.r + 4)).attr('font-size', '9px').attr('font-weight', '500')
|
||||
.style('pointer-events', 'none')
|
||||
|
||||
d3NodeSel.filter((d: any) => d.type === 'auteur')
|
||||
.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>`
|
||||
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' })
|
||||
|
||||
simulation.on('tick', () => {
|
||||
d3LinkSel.attr('x1', (d: any) => d.source.x).attr('y1', (d: any) => d.source.y)
|
||||
.attr('x2', (d: any) => d.target.x).attr('y2', (d: any) => d.target.y)
|
||||
d3NodeSel.attr('transform', (d: any) => `translate(${d.x},${d.y})`)
|
||||
})
|
||||
}
|
||||
|
||||
watch(() => props.active, (val) => { if (val && import.meta.client && props.data) requestAnimationFrame(() => requestAnimationFrame(() => initGraph())) })
|
||||
watch(() => props.data, (val) => { if (val && props.active && import.meta.client) requestAnimationFrame(() => requestAnimationFrame(() => initGraph())) })
|
||||
onMounted(async () => { if (import.meta.client && props.data && props.active) { await nextTick(); initGraph() } })
|
||||
onUnmounted(() => { if (simulation) simulation.stop() })
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.pensees-auteur-label { fill: var(--nav-text); opacity: 0.75; paint-order: stroke; stroke: var(--nav-bg); stroke-width: 3px; stroke-linejoin: round; user-select: none; }
|
||||
</style>
|
||||
217
components/ChatbotPensees.vue
Normal file
217
components/ChatbotPensees.vue
Normal file
@@ -0,0 +1,217 @@
|
||||
<template>
|
||||
<button v-if="!open" @click="open = true"
|
||||
class="fixed bottom-6 right-6 z-[1000] flex items-center gap-2 px-4 rounded-full shadow-lg"
|
||||
style="height:48px;background:var(--nav-primary);color:var(--nav-text-on-primary);font-size:0.875rem;font-weight:600;"
|
||||
aria-label="Chatbot Pensees Ecologiques">
|
||||
<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>
|
||||
<span>Pensees ?</span>
|
||||
</button>
|
||||
|
||||
<Transition name="cpanel">
|
||||
<div v-if="open" class="fixed bottom-6 right-6 z-[1000] flex flex-col"
|
||||
style="width:min(360px,calc(100vw - 24px));max-height:60vh;background:var(--nav-surface);border-radius:14px;box-shadow:0 8px 32px rgba(26,34,56,0.22);overflow:hidden;border:1px solid var(--nav-bg-alt);"
|
||||
role="dialog" aria-modal="true" aria-label="RAG Pensees Ecologiques">
|
||||
<!-- Header -->
|
||||
<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">
|
||||
<svg width="12" height="12" 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>
|
||||
|
||||
<!-- Corpus toggle -->
|
||||
<div class="shrink-0 px-3 pt-2 pb-1" style="background:var(--nav-bg);border-bottom:1px solid var(--nav-bg-alt);">
|
||||
<div class="flex gap-1" role="group" aria-label="Choisir le corpus">
|
||||
<button
|
||||
v-for="opt in corpusOptions"
|
||||
:key="opt.value"
|
||||
@click="setCorpus(opt.value)"
|
||||
:title="opt.tooltip"
|
||||
class="flex-1 px-2 py-1 rounded text-xs font-medium transition-colors"
|
||||
:style="corpus === opt.value
|
||||
? 'background:var(--nav-primary);color:var(--nav-text-on-primary);'
|
||||
: 'background:var(--nav-bg-alt);color:var(--nav-text-muted);'"
|
||||
:aria-pressed="corpus === opt.value">
|
||||
{{ opt.label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Messages -->
|
||||
<div ref="msgEl" class="flex-1 overflow-y-auto px-4 py-3 flex flex-col gap-3" style="min-height:0;">
|
||||
<div v-if="messages.length === 0" style="font-size:0.8rem;color:var(--nav-text-muted);line-height:1.5;">
|
||||
<template v-if="corpus === 'pensees'">Pose une question sur les pensees ecologiques : ecosocialisme, decroissance, ecofeminismes, technocritique, deep ecology...</template>
|
||||
<template v-else-if="corpus === 'projets'">Pose une question sur les projets d'architecture de Jules : Butte Pinson, strategie thermique, partis pris constructifs...</template>
|
||||
<template v-else>Pose une question sur les pensees ecologiques ancrees dans les projets archi de Jules (corpus croise, defaut).</template>
|
||||
</div>
|
||||
<template v-for="(msg, i) in messages" :key="i">
|
||||
<div v-if="msg.role === 'user'" class="self-end max-w-[85%] px-3 py-2 rounded-xl text-sm"
|
||||
style="background:var(--nav-primary);color:var(--nav-text-on-primary);font-weight:500;">{{ msg.content }}</div>
|
||||
<div v-else class="self-start max-w-full">
|
||||
<div class="px-3 py-2 rounded-xl text-sm leading-relaxed" style="background:var(--nav-bg-alt);color:var(--nav-text);"
|
||||
v-html="renderMd(stripSrc(msg.content))" />
|
||||
<div v-if="filteredSources(msg.content).length" class="mt-1.5">
|
||||
<button @click="toggled[i] = !toggled[i]" class="flex items-center gap-1 text-xs hover:opacity-70" style="color:var(--nav-text-muted);">
|
||||
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"
|
||||
:style="`transform:rotate(${toggled[i] ? 90 : 0}deg);transition:transform 0.15s`"><polyline points="9 18 15 12 9 6"/></svg>
|
||||
Sources ({{ filteredSources(msg.content).length }})
|
||||
</button>
|
||||
<div v-if="toggled[i]" class="mt-1 flex flex-col gap-1">
|
||||
<div v-for="(s, si) in filteredSources(msg.content)" :key="si" class="px-2 py-1 rounded text-xs"
|
||||
style="background:var(--nav-bg-alt);color:var(--nav-text-muted);border-left:2px solid var(--nav-primary-solid);">
|
||||
<span style="font-weight:600;color:var(--nav-text);">[{{ si + 1 }}]</span> {{ s }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div v-if="loading" class="self-start px-3 py-2 rounded-xl" style="background:var(--nav-bg-alt);">
|
||||
<span class="dots"><span/><span style="animation-delay:150ms"/><span style="animation-delay:300ms"/></span>
|
||||
</div>
|
||||
<div v-if="err" class="text-xs px-3 py-2 rounded-xl" style="background:#fee;color:#c0392b;">{{ err }}</div>
|
||||
</div>
|
||||
|
||||
<!-- Input -->
|
||||
<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="inputEl" v-model="q" type="text" placeholder="Ta question..." 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" />
|
||||
<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;'"
|
||||
aria-label="Envoyer">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="color:white;">
|
||||
<line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
interface Message { role: 'user' | 'assistant'; content: string }
|
||||
|
||||
type CorpusMode = 'pensees' | 'projets' | 'both'
|
||||
|
||||
const CORPUS_STORAGE_KEY = 'chatbot-pensees-corpus'
|
||||
|
||||
// Patterns projet : les sources qui matchent sont des refs projet archi
|
||||
// Pattern actuel : butte pinson. Extensible en ajoutant d'autres slugs.
|
||||
const PROJECT_SOURCE_PATTERNS = [/butte.?pinson/i, /butte_pinson/i]
|
||||
|
||||
function isProjectSource(s: string): boolean {
|
||||
return PROJECT_SOURCE_PATTERNS.some(p => p.test(s))
|
||||
}
|
||||
|
||||
const corpusOptions: { value: CorpusMode; label: string; tooltip: string }[] = [
|
||||
{ value: 'pensees', label: 'Pensees', tooltip: 'Corpus FRACAS uniquement (auteurs ecologie politique)' },
|
||||
{ value: 'projets', label: 'Projets', tooltip: 'Projets archi de Jules uniquement' },
|
||||
{ value: 'both', label: 'Croise*', tooltip: 'Projets ancres + pensees en eclairage (defaut)' },
|
||||
]
|
||||
|
||||
const props = defineProps<{ auteurContext?: string | null }>()
|
||||
const open = ref(false)
|
||||
const q = ref('')
|
||||
const messages = ref<Message[]>([])
|
||||
const loading = ref(false)
|
||||
const err = ref('')
|
||||
const toggled = ref<Record<number, boolean>>({})
|
||||
const msgEl = ref<HTMLElement | null>(null)
|
||||
const inputEl = ref<HTMLInputElement | null>(null)
|
||||
const corpusCount = 18
|
||||
|
||||
// Corpus state - init depuis localStorage
|
||||
const corpus = ref<CorpusMode>('both')
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
const saved = window.localStorage.getItem(CORPUS_STORAGE_KEY) as CorpusMode | null
|
||||
if (saved && ['pensees', 'projets', 'both'].includes(saved)) {
|
||||
corpus.value = saved
|
||||
}
|
||||
}
|
||||
|
||||
function setCorpus(val: CorpusMode) {
|
||||
corpus.value = val
|
||||
if (typeof window !== 'undefined') {
|
||||
window.localStorage.setItem(CORPUS_STORAGE_KEY, val)
|
||||
}
|
||||
}
|
||||
|
||||
watch(open, (val) => {
|
||||
if (!val) return
|
||||
nextTick(() => inputEl.value?.focus())
|
||||
if (props.auteurContext && messages.value.length === 0)
|
||||
q.value = `Quelles sont les theses centrales de ${props.auteurContext} ?`
|
||||
})
|
||||
watch(() => props.auteurContext, (ctx) => {
|
||||
if (!ctx) return
|
||||
if (!open.value) open.value = true
|
||||
if (messages.value.length === 0) q.value = `Quelles sont les theses centrales de ${ctx} ?`
|
||||
})
|
||||
|
||||
async function send() {
|
||||
const query = q.value.trim()
|
||||
if (!query || loading.value) return
|
||||
err.value = ''
|
||||
messages.value.push({ role: 'user', content: query })
|
||||
q.value = ''
|
||||
loading.value = true
|
||||
await nextTick(); scrollBottom()
|
||||
try {
|
||||
const res = await $fetch<{ response: string }>('/api/chatbot-pensees', {
|
||||
method: 'POST',
|
||||
body: { query, mode: 'hybrid', corpus: corpus.value },
|
||||
})
|
||||
messages.value.push({ role: 'assistant', content: res.response ?? '' })
|
||||
} catch (e: any) {
|
||||
const s = e?.response?.status ?? e?.statusCode
|
||||
err.value = s === 429 ? 'Limite atteinte.' : s === 503 ? 'RAG indisponible.' : 'Erreur - reessaie.'
|
||||
} finally {
|
||||
loading.value = false
|
||||
await nextTick(); scrollBottom()
|
||||
}
|
||||
}
|
||||
|
||||
function scrollBottom() { if (msgEl.value) msgEl.value.scrollTop = msgEl.value.scrollHeight }
|
||||
|
||||
function renderMd(t: string) {
|
||||
return '<p>' + t.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>').replace(/\*(.+?)\*/g, '<em>$1</em>').replace(/\n\n/g, '</p><p>').replace(/\n/g, '<br>') + '</p>'
|
||||
}
|
||||
function stripSrc(t: string) { return t.replace(/\n*(?:Sources?|References?)\s*:[\s\S]*$/i, '').trim() }
|
||||
|
||||
function parseSrc(t: string): string[] {
|
||||
const bloc = t.match(/\n*(?:Sources?|References?)\s*:\n?([\s\S]+?)$/i)
|
||||
if (bloc) return bloc[1].split('\n').map(l => l.replace(/^[-*\d.[\]]+\s*/, '').trim()).filter(l => l.length > 3)
|
||||
return [...new Set([...t.matchAll(/\[([^\]]{5,80})\]/g)].filter(m => m[1].includes(' - ')).map(m => m[1]))]
|
||||
}
|
||||
|
||||
// Filtrage UI des sources selon corpus actif
|
||||
function filteredSources(t: string): string[] {
|
||||
const all = parseSrc(t)
|
||||
if (corpus.value === 'both') return all
|
||||
if (corpus.value === 'projets') return all.filter(s => isProjectSource(s))
|
||||
// corpus === 'pensees' : exclure les sources projet
|
||||
return all.filter(s => !isProjectSource(s))
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.cpanel-enter-active { transition: opacity 0.2s, transform 0.22s cubic-bezier(0.34,1.56,0.64,1); }
|
||||
.cpanel-leave-active { transition: opacity 0.18s, transform 0.15s ease-in; }
|
||||
.cpanel-enter-from { opacity: 0; transform: translateY(12px) scale(0.95); }
|
||||
.cpanel-leave-to { opacity: 0; transform: translateY(8px) scale(0.97); }
|
||||
.dots span { display:inline-block;width:6px;height:6px;border-radius:50%;background:var(--nav-text-muted);margin:0 2px;animation:bounce 1s infinite; }
|
||||
@keyframes bounce { 0%,80%,100%{transform:translateY(0)}40%{transform:translateY(-5px)} }
|
||||
</style>
|
||||
@@ -52,9 +52,10 @@
|
||||
<div class="chatbot-body-inner" ref="messagesContainer">
|
||||
<!-- Onboarding -->
|
||||
<div v-if="messages.length === 0" class="onboarding-bubble">
|
||||
<p>Explore les 120 structures de la carte par la conversation. Je peux t'aider à trouver des collectifs, agences ou réseaux selon ta situation, ta pratique ou tes inspirations du moment.</p>
|
||||
<p class="example">Exemple : "Je cherche des acteurs de la rénovation de maisons individuelles en France, plutôt en milieu rural, avec des approches biosourcées ou low-tech."</p>
|
||||
<p style="margin-top: 8px; font-size: 0.72rem; opacity: 0.6;">Propulsé par Mistral FR - serveur européen souverain, zéro rétention.</p>
|
||||
<p>Je connais les structures d'entraide pour architectes référencées sur cette carte — appui juridique, technique, économique, formation, santé mentale, gestion d'agence…</p>
|
||||
<p>Décris ta situation, je te propose les fiches les plus pertinentes.</p>
|
||||
<p class="example">Exemple : "Architecte salarié, litige avec mon employeur, besoin d'un appui juridique droit du travail, Île-de-France."</p>
|
||||
<p style="margin-top: 8px; font-size: 0.72rem; opacity: 0.6;">Propulsé par Mistral FR — serveur européen souverain, zéro rétention.</p>
|
||||
</div>
|
||||
|
||||
<!-- Messages -->
|
||||
|
||||
@@ -1,38 +1,16 @@
|
||||
<template>
|
||||
<div class="space-y-1.5">
|
||||
<p class="text-xs font-bold uppercase tracking-widest" style="color: var(--nav-text-muted);">Échelle</p>
|
||||
<!-- Inline sur 1 ligne — même pattern que FonctionFilter -->
|
||||
<div class="flex flex-wrap gap-x-4 gap-y-1.5">
|
||||
<label
|
||||
<div class="space-y-1">
|
||||
<p class="filter-label">ÉCHELLE</p>
|
||||
<div class="chips-row">
|
||||
<span
|
||||
v-for="option in ECHELLES"
|
||||
:key="option"
|
||||
class="flex items-center gap-1.5 cursor-pointer select-none transition-opacity"
|
||||
>
|
||||
<!-- Case carrée -->
|
||||
<span
|
||||
class="flex items-center justify-center shrink-0 transition-all"
|
||||
style="width: 18px; height: 18px; border: 1.5px solid; border-radius: 3px;"
|
||||
class="chip"
|
||||
:style="isSelected(option)
|
||||
? 'background: var(--nav-primary); border-color: var(--nav-primary); color: #ffffff;'
|
||||
: 'background: var(--nav-bg-alt); border-color: rgba(26,34,56,0.25); color: transparent;'"
|
||||
>
|
||||
<svg v-if="isSelected(option)" width="11" height="11" viewBox="0 0 12 12" fill="none">
|
||||
<polyline points="2,6 5,9 10,3" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</span>
|
||||
<!-- Label -->
|
||||
<span
|
||||
class="text-sm leading-tight"
|
||||
:style="isSelected(option) ? 'color: var(--nav-text); font-weight: 600;' : 'color: var(--nav-text);'"
|
||||
? 'background: var(--nav-primary); color: var(--nav-text-on-primary); font-weight: 600;'
|
||||
: 'background: var(--nav-bg-alt); color: var(--nav-text-muted);'"
|
||||
@click="toggle(option)"
|
||||
>{{ option }}</span>
|
||||
<!-- Input réel (masqué) -->
|
||||
<input
|
||||
type="checkbox"
|
||||
class="sr-only"
|
||||
:checked="isSelected(option)"
|
||||
@change="toggle(option)"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -61,3 +39,24 @@ function toggle(option: string) {
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.filter-label {
|
||||
font-size: 0.7rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
color: var(--nav-text-muted);
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.chips-row { display: flex; flex-wrap: wrap; gap: 4px; margin-bottom: 4px; }
|
||||
.chip {
|
||||
cursor: pointer;
|
||||
padding: 3px 10px;
|
||||
border-radius: 9999px;
|
||||
font-size: 0.75rem;
|
||||
transition: all 0.15s;
|
||||
user-select: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
98
components/FicheAuteur.vue
Normal file
98
components/FicheAuteur.vue
Normal file
@@ -0,0 +1,98 @@
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<Transition name="backdrop">
|
||||
<div v-if="open && auteur" 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 && auteur" class="fixed z-[1501] left-1/2 flex flex-col"
|
||||
style="top:50%;transform:translate(-50%,-50%);width:min(520px,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 ${ecoleColor}; background: var(--nav-surface);`">
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-2 flex-wrap">
|
||||
<span class="px-2 py-0.5 rounded-full text-xs font-semibold" :style="`background:${ecoleColor}22;color:${ecoleColor};`">{{ ecoleLabel }}</span>
|
||||
<span v-for="eid in auteur.ecoles.filter(e => e !== auteur.ecole_principale)" :key="eid"
|
||||
class="px-2 py-0.5 rounded-full text-xs" :style="`background:${getEcoleColor(eid)}22;color:${getEcoleColor(eid)};`">{{ getEcoleLabel(eid) }}</span>
|
||||
</div>
|
||||
<h2 class="mt-2 font-bold text-lg leading-tight" style="color:var(--nav-text);">{{ auteur.nom }}</h2>
|
||||
<p class="text-sm" style="color:var(--nav-text-muted);">{{ auteur.dates }}</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">
|
||||
<p class="text-sm leading-relaxed" style="color:var(--nav-text);">{{ auteur.bio_courte }}</p>
|
||||
<div v-if="auteur.theses_cles.length">
|
||||
<p class="text-xs font-bold uppercase tracking-widest mb-2" style="color:var(--nav-text-muted);">Theses cles</p>
|
||||
<ul class="flex flex-col gap-1.5">
|
||||
<li v-for="t in auteur.theses_cles" :key="t" class="flex items-start gap-2 text-sm" style="color:var(--nav-text);">
|
||||
<span class="mt-1.5 w-1.5 h-1.5 rounded-full shrink-0" :style="`background:${ecoleColor};`"></span>
|
||||
<span>{{ t }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div v-if="auteur.livres_rag.length">
|
||||
<p class="text-xs font-bold uppercase tracking-widest mb-2" style="color:var(--nav-text-muted);">Livres dans le RAG</p>
|
||||
<div class="flex flex-col gap-2">
|
||||
<div v-for="l in auteur.livres_rag" :key="l.slug" class="flex items-start gap-3 p-3 rounded-lg" style="background:var(--nav-bg-alt);">
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="text-sm font-semibold leading-snug" style="color:var(--nav-text);">{{ l.titre }}</p>
|
||||
<p class="text-xs mt-0.5" style="color:var(--nav-text-muted);">{{ l.annee }}</p>
|
||||
</div>
|
||||
<div class="flex gap-1 shrink-0">
|
||||
<span v-for="c in l.couches" :key="c" class="px-1.5 py-0.5 rounded text-xs" style="background:var(--nav-surface);color:var(--nav-text-muted);">{{ c }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</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-rag', auteurId!)" class="w-full py-2.5 rounded-lg text-sm font-semibold hover:opacity-80"
|
||||
:style="`background:${ecoleColor};color:white;`">
|
||||
Interroger le RAG sur {{ auteur.nom.split(' ').pop() }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
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 EcoleData { id: string; label: string; color: string }
|
||||
interface PenseesData { ecoles: EcoleData[]; auteurs: AuteurData[] }
|
||||
|
||||
const props = defineProps<{ open: boolean; auteurId: string | null; data: PenseesData | null }>()
|
||||
const emit = defineEmits<{ close: []; 'interroger-rag': [auteurId: string] }>()
|
||||
|
||||
const auteur = computed<AuteurData | null>(() => {
|
||||
if (!props.auteurId || !props.data) return null
|
||||
return props.data.auteurs.find(a => a.id === props.auteurId) ?? null
|
||||
})
|
||||
const ecoleColor = computed(() => props.data?.ecoles.find(e => e.id === auteur.value?.ecole_principale)?.color ?? '#888')
|
||||
const ecoleLabel = computed(() => props.data?.ecoles.find(e => e.id === auteur.value?.ecole_principale)?.label ?? '')
|
||||
function getEcoleColor(id: string) { return props.data?.ecoles.find(e => e.id === id)?.color ?? '#888' }
|
||||
function getEcoleLabel(id: string) { return props.data?.ecoles.find(e => e.id === id)?.label ?? 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>
|
||||
@@ -1,35 +1,33 @@
|
||||
<template>
|
||||
<div class="space-y-1.5">
|
||||
<p class="text-xs font-bold uppercase tracking-widest" style="color: var(--nav-text-muted);">Fonction</p>
|
||||
<div class="space-y-1">
|
||||
<!-- Label + toggle collapse -->
|
||||
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 4px;">
|
||||
<p class="filter-label" style="margin-bottom: 0;">
|
||||
FONCTION
|
||||
<span v-if="modelValue.length" style="font-weight: 400; text-transform: none; letter-spacing: 0; font-size: 0.65rem; margin-left: 4px;">({{ modelValue.length }} active{{ modelValue.length > 1 ? 's' : '' }})</span>
|
||||
</p>
|
||||
<button
|
||||
@click="toggleCollapse"
|
||||
style="font-size: 0.7rem; color: var(--nav-text-muted); background: none; border: none; cursor: pointer; text-decoration: underline; padding: 0; white-space: nowrap;"
|
||||
>{{ isOpen ? 'Replier' : 'Fonctions (' + FONCTIONS.length + ')' }}</button>
|
||||
</div>
|
||||
|
||||
<!-- Chips (visible si ouvert ou si des fonctions sont actives) -->
|
||||
<div v-if="isOpen" class="chips-row">
|
||||
<span
|
||||
v-for="fn in FONCTIONS"
|
||||
:key="fn"
|
||||
@click="toggle(fn)"
|
||||
:aria-pressed="modelValue.includes(fn)"
|
||||
class="flex items-center gap-2.5 w-full rounded px-1 py-0.5 transition-all text-left hover:opacity-80"
|
||||
:style="modelValue.includes(fn) ? 'background: rgba(26,34,56,0.06);' : ''"
|
||||
>
|
||||
<!-- Case : affiche le rang de priorité si actif, sinon le nombre d'orgs -->
|
||||
<span
|
||||
class="flex items-center justify-center shrink-0 text-xs font-bold transition-all"
|
||||
style="width: 24px; height: 24px; border: 1.5px solid; border-radius: 4px;"
|
||||
class="chip"
|
||||
:style="modelValue.includes(fn)
|
||||
? 'background: var(--nav-primary); border-color: var(--nav-primary); color: var(--nav-text-on-primary);'
|
||||
: 'background: var(--nav-bg-alt); border-color: var(--nav-bg-alt); color: var(--nav-text-muted);'"
|
||||
>
|
||||
{{ modelValue.includes(fn) ? (modelValue.indexOf(fn) + 1) : (counts[fn] ?? 0) }}
|
||||
</span>
|
||||
<!-- Label -->
|
||||
<span
|
||||
class="text-sm leading-tight"
|
||||
:style="modelValue.includes(fn) ? 'color: var(--nav-text); font-weight: 600;' : 'color: var(--nav-text);'"
|
||||
? 'background: var(--nav-primary); color: var(--nav-text-on-primary); font-weight: 600;'
|
||||
: 'background: var(--nav-bg-alt); color: var(--nav-text-muted);'"
|
||||
@click="toggle(fn)"
|
||||
>{{ fn }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Effacer (visible même replié si filtres actifs) -->
|
||||
<p v-if="modelValue.length" class="text-xs pt-0.5" style="color: var(--nav-text-muted);">
|
||||
{{ modelValue.length }} actif{{ modelValue.length > 1 ? 's' : '' }}
|
||||
<button @click="emit('update:modelValue', [])" class="ml-2 underline hover:opacity-70">Effacer</button>
|
||||
<button @click="emit('update:modelValue', [])" class="underline hover:opacity-70">Effacer</button>
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
@@ -57,6 +55,25 @@ const emit = defineEmits<{
|
||||
'update:modelValue': [value: string[]]
|
||||
}>()
|
||||
|
||||
// Replié par défaut, ouvre automatiquement quand des filtres sont actifs
|
||||
const manuallyOpen = ref(false)
|
||||
|
||||
const isOpen = computed(() => {
|
||||
return manuallyOpen.value || props.modelValue.length > 0
|
||||
})
|
||||
|
||||
function toggleCollapse() {
|
||||
// Si des filtres actifs forcent l'ouverture, on doit gérer le cas « forcer fermer »
|
||||
if (isOpen.value) {
|
||||
manuallyOpen.value = false
|
||||
// Si des fonctions sont actives, le computed va les réouvrir — on les efface
|
||||
// Non : on laisse le choix à l'utilisateur. On toggle juste manuallyOpen.
|
||||
// Quand replié avec filtres actifs, l'indicateur "(N actives)" reste visible.
|
||||
} else {
|
||||
manuallyOpen.value = true
|
||||
}
|
||||
}
|
||||
|
||||
function toggle(fn: string) {
|
||||
if (props.modelValue.includes(fn)) {
|
||||
emit('update:modelValue', props.modelValue.filter(f => f !== fn))
|
||||
@@ -65,3 +82,23 @@ function toggle(fn: string) {
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.filter-label {
|
||||
font-size: 0.7rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
color: var(--nav-text-muted);
|
||||
display: block;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.chips-row { display: flex; flex-wrap: wrap; gap: 4px; margin-bottom: 4px; }
|
||||
.chip {
|
||||
cursor: pointer;
|
||||
padding: 3px 10px;
|
||||
border-radius: 9999px;
|
||||
font-size: 0.75rem;
|
||||
transition: all 0.15s;
|
||||
user-select: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -877,6 +877,7 @@ onUnmounted(() => {
|
||||
/* Labels des structures dans le graphe (D3 injecte les <text>, donc style global) */
|
||||
.graph-view .graph-struct-label {
|
||||
fill: var(--nav-text);
|
||||
opacity: 0.7;
|
||||
paint-order: stroke;
|
||||
stroke: var(--nav-bg);
|
||||
stroke-width: 3px;
|
||||
|
||||
@@ -23,6 +23,7 @@ export default defineNuxtConfig({
|
||||
codevPassword: 'merci', // NUXT_CODEV_PASSWORD - défaut "merci", overridable
|
||||
codevBaseId: '', // NUXT_CODEV_BASE_ID - base NocoDB (ex: pipilvsi7dibo80)
|
||||
codevAdminPassword: 'admin2026', // NUXT_CODEV_ADMIN_PASSWORD
|
||||
ragPeUrl: process.env.NUXT_RAG_PE_URL || 'http://localhost:9621',
|
||||
},
|
||||
|
||||
// Leaflet ne fonctionne pas en SSR — forcer le rendu côté client
|
||||
|
||||
@@ -128,6 +128,12 @@
|
||||
: '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 -->
|
||||
@@ -219,6 +225,11 @@
|
||||
: '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">
|
||||
@@ -403,6 +414,11 @@
|
||||
@update:modelValue="chatbotOpen = $event"
|
||||
/>
|
||||
|
||||
<!-- ═══════════════════════════════════════ CHATBOT PENSEES (desktop, tous onglets) -->
|
||||
<ClientOnly>
|
||||
<ChatbotPensees />
|
||||
</ClientOnly>
|
||||
|
||||
<!-- ═══════════════════════════════════════ POP-UP MISSION RÉSEAUX AEP -->
|
||||
<button
|
||||
class="reseaux-info-btn"
|
||||
|
||||
@@ -201,10 +201,19 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filtres FONCTION — chips flex-wrap -->
|
||||
<!-- Filtres FONCTION — chips flex-wrap + toggle collapse -->
|
||||
<div class="mt-2">
|
||||
<span class="text-xs font-bold uppercase tracking-wide block mb-1" style="color: var(--nav-text-muted);">FONCTION</span>
|
||||
<div class="flex flex-wrap gap-1">
|
||||
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 4px;">
|
||||
<span class="text-xs font-bold uppercase tracking-wide" style="color: var(--nav-text-muted);">
|
||||
FONCTION
|
||||
<span v-if="fonctions.length" style="font-weight: 400; text-transform: none; letter-spacing: 0; font-size: 0.65rem; margin-left: 4px;">({{ fonctions.length }} active{{ fonctions.length > 1 ? 's' : '' }})</span>
|
||||
</span>
|
||||
<button
|
||||
@click="mobileFonctionsOpen = !mobileFonctionsOpen"
|
||||
style="font-size: 0.65rem; color: var(--nav-text-muted); background: none; border: none; cursor: pointer; text-decoration: underline; padding: 0; white-space: nowrap;"
|
||||
>{{ mobileFonctionsOpen || fonctions.length ? (mobileFonctionsOpen ? 'Replier' : 'Afficher') : 'Fonctions (' + FONCTIONS.length + ')' }}</button>
|
||||
</div>
|
||||
<div v-if="mobileFonctionsOpen || fonctions.length" class="flex flex-wrap gap-1">
|
||||
<span
|
||||
v-for="fn in FONCTIONS"
|
||||
:key="fn"
|
||||
@@ -365,6 +374,7 @@ const ficheModalOpen = ref(false)
|
||||
const ficheModalId = ref<number | null>(null)
|
||||
const mobileMapView = ref<'metropole' | 'outremer'>('metropole')
|
||||
const missionOpen = ref(false)
|
||||
const mobileFonctionsOpen = ref(false)
|
||||
|
||||
onMounted(() => {
|
||||
try {
|
||||
|
||||
83
pages/media.vue
Normal file
83
pages/media.vue
Normal file
@@ -0,0 +1,83 @@
|
||||
<template>
|
||||
<div class="flex h-full overflow-hidden" style="background: var(--nav-bg);">
|
||||
|
||||
<!-- ZONE PRINCIPALE (pleine largeur, pas de sidebar) -->
|
||||
<main class="flex-1 flex flex-col overflow-hidden relative">
|
||||
|
||||
<!-- Header onglet -->
|
||||
<div class="shrink-0 px-5 py-3"
|
||||
style="background: var(--nav-surface); border-bottom: 1px solid var(--nav-bg-alt);">
|
||||
<h1 class="font-bold text-base" style="color: var(--nav-text);">ATIS Média</h1>
|
||||
<p class="text-xs mt-0.5" style="color: var(--nav-text-muted);">
|
||||
{{ corpusCount }} auteurs ingérés dans le RAG - carte FRACAS Bonpote V2
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Carte pensees (D3 force-directed) -->
|
||||
<div class="flex-1 overflow-hidden relative">
|
||||
<ClientOnly>
|
||||
<CartePensees
|
||||
:data="penseesData"
|
||||
:active="true"
|
||||
@select-auteur="onSelectAuteur"
|
||||
/>
|
||||
<template #fallback>
|
||||
<div class="w-full h-full flex items-center justify-center" style="color: var(--nav-text-muted);">
|
||||
Chargement de la carte...
|
||||
</div>
|
||||
</template>
|
||||
</ClientOnly>
|
||||
</div>
|
||||
|
||||
</main>
|
||||
|
||||
<!-- Fiche auteur modal -->
|
||||
<FicheAuteur
|
||||
:open="ficheOpen"
|
||||
:auteurId="ficheAuteurId"
|
||||
:data="penseesData"
|
||||
@close="ficheOpen = false"
|
||||
@interroger-rag="onInterrogerRag"
|
||||
/>
|
||||
|
||||
<!-- Chatbot flottant -->
|
||||
<ChatbotPensees :auteurContext="chatbotAuteur" />
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<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 PenseesData { meta: any; ecoles: EcoleData[]; auteurs: AuteurData[] }
|
||||
|
||||
const ficheOpen = ref(false)
|
||||
const ficheAuteurId = ref<string | null>(null)
|
||||
const chatbotAuteur = ref<string | null>(null)
|
||||
const penseesData = ref<PenseesData | null>(null)
|
||||
|
||||
const corpusCount = computed(() => penseesData.value?.auteurs.length ?? 0)
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
penseesData.value = await $fetch<PenseesData>('/data/auteurs-pensees.json')
|
||||
} catch (e) {
|
||||
console.error('Erreur chargement auteurs-pensees.json', e)
|
||||
}
|
||||
})
|
||||
|
||||
function onSelectAuteur(id: string) {
|
||||
ficheAuteurId.value = id
|
||||
ficheOpen.value = true
|
||||
chatbotAuteur.value = null
|
||||
}
|
||||
|
||||
function onInterrogerRag(auteurId: string) {
|
||||
ficheOpen.value = false
|
||||
const auteur = penseesData.value?.auteurs.find(a => a.id === auteurId)
|
||||
chatbotAuteur.value = auteur?.nom ?? null
|
||||
}
|
||||
|
||||
useHead({ title: 'AEP - Média - Carte FRACAS Bonpote' })
|
||||
</script>
|
||||
491
public/data/auteurs-pensees.json
Normal file
491
public/data/auteurs-pensees.json
Normal file
@@ -0,0 +1,491 @@
|
||||
{
|
||||
"meta": {
|
||||
"version": "2.0",
|
||||
"source": "FRACAS Bonpote V2 oct 2024 + LightRAG corpus 11/05/2026",
|
||||
"corpus_ingere": 141,
|
||||
"auteurs_count": 28,
|
||||
"livres_count": 64,
|
||||
"ecoles_count": 12,
|
||||
"note_doublons_en_fr": "3 livres avec version EN aussi indexee dans le RAG pour cross-language queries : carson-mer-autour-de-nous-fr/EN, graeber-wengrow-aurore-fr/EN, saito-capital-anthropocene/EN. JSON conserve la version FR.",
|
||||
"updated": "2026-05-11"
|
||||
},
|
||||
"ecoles": [
|
||||
{
|
||||
"id": "ecosocialisme",
|
||||
"label": "Écosocialisme",
|
||||
"description": "Synthèse du marxisme et de l'écologie. Articule la critique du capitalisme et la crise écologique comme deux faces d'un même système.",
|
||||
"color": "#c0392b",
|
||||
"x_hint": 0.55,
|
||||
"y_hint": 0.28
|
||||
},
|
||||
{
|
||||
"id": "marxismes-ecologiques",
|
||||
"label": "Marxismes écologiques",
|
||||
"description": "Relecture écologique des écrits de Marx et de ses continuateurs contemporains. Le Capital comme critique du métabolisme homme-nature. Décroissance communiste.",
|
||||
"color": "#8e44ad",
|
||||
"x_hint": 0.65,
|
||||
"y_hint": 0.2
|
||||
},
|
||||
{
|
||||
"id": "eco-anarchisme",
|
||||
"label": "Écologies libertaires",
|
||||
"description": "Filiation des traditions du socialisme ouvrier anglais et de l'anarchisme. Les dominations de l'homme sur l'homme, sur la femme et sur la nature ne peuvent être prises séparément. Éco-communautés, institutions autogérées, démocratie radicale, municipalisme libertaire.",
|
||||
"color": "#2d6a4f",
|
||||
"x_hint": 0.25,
|
||||
"y_hint": 0.3
|
||||
},
|
||||
{
|
||||
"id": "decroissance",
|
||||
"label": "Décroissance",
|
||||
"description": "Critique radicale de la croissance économique comme horizon. Pour une réduction volontaire de la production et de la consommation.",
|
||||
"color": "#e67e22",
|
||||
"x_hint": 0.38,
|
||||
"y_hint": 0.42
|
||||
},
|
||||
{
|
||||
"id": "ecofeminismes",
|
||||
"label": "Écoféminismes",
|
||||
"description": "Connexions entre la domination des femmes et la domination de la nature. Féminisme de la subsistance, critique du développement, commons.",
|
||||
"color": "#e07a5f",
|
||||
"x_hint": 0.48,
|
||||
"y_hint": 0.68
|
||||
},
|
||||
{
|
||||
"id": "technocritique",
|
||||
"label": "Écologies anti-industrielles",
|
||||
"description": "Rejet du productivisme et de l'hyper-mécanisation issus de l'ère industrielle. Approche technocritique : critique du gigantisme productif et de l'État, refus de l'idéologie du Progrès. Considérer la technique comme un système avec ses logiques propres.",
|
||||
"color": "#7f8c8d",
|
||||
"x_hint": 0.2,
|
||||
"y_hint": 0.48
|
||||
},
|
||||
{
|
||||
"id": "ecologies-decoloniales",
|
||||
"label": "Écologies décoloniales",
|
||||
"description": "Articulation des luttes écologiques et des luttes anticoloniales. Critique de l'extractivisme comme continuation du colonialisme.",
|
||||
"color": "#b5451b",
|
||||
"x_hint": 0.3,
|
||||
"y_hint": 0.72
|
||||
},
|
||||
{
|
||||
"id": "ethiques-environnementales",
|
||||
"label": "Éthiques environnementales",
|
||||
"description": "Philosophies de la nature : deep ecology, écocentrisme, droits des non-humains. Valeur intrinsèque du vivant.",
|
||||
"color": "#2c7873",
|
||||
"x_hint": 0.72,
|
||||
"y_hint": 0.72
|
||||
},
|
||||
{
|
||||
"id": "pensees-vivant",
|
||||
"label": "Pensées du vivant",
|
||||
"description": "Anthropologie et ontologies de la nature. Dépasser le dualisme nature/culture. Sympoïèse, multi-espèces, éthologie politique.",
|
||||
"color": "#6b8e6e",
|
||||
"x_hint": 0.62,
|
||||
"y_hint": 0.58
|
||||
},
|
||||
{
|
||||
"id": "collapsologie",
|
||||
"label": "Collapsologie",
|
||||
"description": "Étude interdisciplinaire de l'effondrement de la civilisation industrielle et des voies de résilience. Articule sciences du vivant, géopolitique et psychologie de la transition.",
|
||||
"color": "#34495e",
|
||||
"x_hint": 0.42,
|
||||
"y_hint": 0.22
|
||||
},
|
||||
{
|
||||
"id": "capitalisme-vert",
|
||||
"label": "Capitalisme vert",
|
||||
"description": "Théoriciens du capitalisme qui intègrent la dimension environnementale aux échanges marchands (taxes, compensation, technologies vertes). Certains accélèrent la dynamique capitaliste, voulant contrôler le Système-Terre sans nuire aux intérêts de la classe possédante. Famille critiquée par toutes les autres.",
|
||||
"color": "#6c8a6d",
|
||||
"x_hint": 0.85,
|
||||
"y_hint": 0.5,
|
||||
"corpus_status": "non_ingere",
|
||||
"note_editoriale": "Famille intégrée pour fidélité à la carte FRACAS Bonpote. Pas d'auteurs ingérés dans le RAG ATIS (critique éditoriale assumée)."
|
||||
},
|
||||
{
|
||||
"id": "ecofascismes",
|
||||
"label": "Écofascismes",
|
||||
"description": "Émergés à bas bruit depuis les années 1980, fragmentés. En Europe : éco-différentialisme, séparation des « races »/civilisations adaptées à leur environnement. Aux USA : néo-malthusianisme, xénophobie, apologie de la wilderness, logiques survivalistes. Famille critiquée par toutes les autres.",
|
||||
"color": "#5d4037",
|
||||
"x_hint": 0.92,
|
||||
"y_hint": 0.85,
|
||||
"corpus_status": "non_ingere",
|
||||
"note_editoriale": "Famille intégrée pour fidélité à la carte FRACAS Bonpote. Pas d'auteurs ingérés dans le RAG ATIS (critique éditoriale assumée)."
|
||||
}
|
||||
],
|
||||
"auteurs": [
|
||||
{
|
||||
"id": "murray-bookchin",
|
||||
"nom": "Murray Bookchin",
|
||||
"dates": "1921-2006",
|
||||
"ecoles": ["eco-anarchisme"],
|
||||
"ecole_principale": "eco-anarchisme",
|
||||
"livres_rag": [
|
||||
{ "slug": "bookchin-ecologie-liberte", "titre": "L'Écologie de la liberté", "annee": 1982, "couches": ["fond", "structure"] },
|
||||
{ "slug": "bookchin-post-scarcity", "titre": "Post-Scarcity Anarchism", "annee": 1971, "couches": ["fond", "structure"] },
|
||||
{ "slug": "bookchin-urbanization-citizenship", "titre": "The Rise of Urbanization and the Decline of Citizenship", "annee": 1987, "couches": ["fond", "structure"] }
|
||||
],
|
||||
"theses_cles": ["Municipalisme libertaire", "Écologie sociale", "Hiérarchie comme origine de la domination nature"],
|
||||
"bio_courte": "Théoricien américain de l'écologie sociale et du municipalisme libertaire. A développé le concept d'écologie sociale articulant domination sociale et destruction de la nature."
|
||||
},
|
||||
{
|
||||
"id": "pierre-kropotkine",
|
||||
"nom": "Pierre Kropotkine",
|
||||
"dates": "1842-1921",
|
||||
"ecoles": ["eco-anarchisme"],
|
||||
"ecole_principale": "eco-anarchisme",
|
||||
"livres_rag": [
|
||||
{ "slug": "kropotkine-entraide", "titre": "L'Entraide, un facteur de l'évolution", "annee": 1902, "couches": ["fond", "structure"] },
|
||||
{ "slug": "kropotkine-conquete-pain", "titre": "La Conquête du pain", "annee": 1892, "couches": ["fond", "structure"] },
|
||||
{ "slug": "kropotkine-champs-usines", "titre": "Champs, usines et ateliers", "annee": 1898, "couches": ["fond", "structure"] }
|
||||
],
|
||||
"theses_cles": ["Entraide vs sélection naturelle darwiniste", "Fédéralisme anarchiste", "Géographie critique et décentralisation industrielle"],
|
||||
"bio_courte": "Géographe et révolutionnaire russe. Son oeuvre centrale démontre que l'entraide, et non la compétition, est le moteur principal de l'évolution."
|
||||
},
|
||||
{
|
||||
"id": "elisee-reclus",
|
||||
"nom": "Élisée Reclus",
|
||||
"dates": "1830-1905",
|
||||
"ecoles": ["eco-anarchisme"],
|
||||
"ecole_principale": "eco-anarchisme",
|
||||
"livres_rag": [
|
||||
{ "slug": "reclus-homme-terre", "titre": "L'Homme et la Terre", "annee": 1905, "couches": ["fond", "structure"] },
|
||||
{ "slug": "reclus-evolution-revolution", "titre": "L'Évolution, la révolution et l'idéal anarchique", "annee": 1898, "couches": ["fond", "structure"] },
|
||||
{ "slug": "reclus-histoire-ruisseau", "titre": "Histoire d'un ruisseau", "annee": 1869, "couches": ["fond", "structure"] }
|
||||
],
|
||||
"theses_cles": ["Géographie sociale anarchiste", "Homme comme nature prenant conscience d'elle-même", "Antimilitarisme et internationalisme"],
|
||||
"bio_courte": "Géographe anarchiste français, auteur de la Nouvelle Géographie universelle. Précurseur de l'écologie politique et de la géographie humaine critique."
|
||||
},
|
||||
{
|
||||
"id": "david-graeber",
|
||||
"nom": "David Graeber",
|
||||
"dates": "1961-2020",
|
||||
"ecoles": ["eco-anarchisme"],
|
||||
"ecole_principale": "eco-anarchisme",
|
||||
"livres_rag": [
|
||||
{ "slug": "graeber-bullshit-jobs", "titre": "Bullshit Jobs", "annee": 2018, "couches": ["fond", "structure"] },
|
||||
{ "slug": "graeber-wengrow-aurore", "titre": "Au commencement était... Une nouvelle histoire de l'humanité", "annee": 2021, "couches": ["fond", "structure"] }
|
||||
],
|
||||
"theses_cles": ["Travail sans valeur comme instrument de domination", "Anthropologie anarchiste", "Contre le récit d'une évolution linéaire de l'humanité"],
|
||||
"bio_courte": "Anthropologue américain, figure du mouvement Occupy. Ses travaux déconstruisent les mythes fondateurs du capitalisme et proposent une anthropologie radicalement alternative.",
|
||||
"note_rag": "graeber-wengrow-aurore-fr aussi indexe pour cross-language queries"
|
||||
},
|
||||
{
|
||||
"id": "karl-marx",
|
||||
"nom": "Karl Marx",
|
||||
"dates": "1818-1883",
|
||||
"ecoles": ["marxismes-ecologiques", "ecosocialisme"],
|
||||
"ecole_principale": "marxismes-ecologiques",
|
||||
"livres_rag": [
|
||||
{ "slug": "marx-manuscrits-1844", "titre": "Manuscrits économico-philosophiques de 1844", "annee": 1844, "couches": ["fond", "structure"] },
|
||||
{ "slug": "marx-capital-livre1", "titre": "Le Capital, Livre I", "annee": 1867, "couches": ["fond", "structure"] },
|
||||
{ "slug": "marx-grundrisse", "titre": "Grundrisse", "annee": 1857, "couches": ["fond", "structure"] }
|
||||
],
|
||||
"theses_cles": ["Métabolisme entre travail humain et nature", "Aliénation naturelle", "Accumulation primitive et rupture métabolique"],
|
||||
"bio_courte": "Pensée-racine des marxismes écologiques. Les Grundrisse et le Capital contiennent une critique écologique du capitalisme que le 20e siècle a largement occultée."
|
||||
},
|
||||
{
|
||||
"id": "kohei-saito",
|
||||
"nom": "Kohei Saito",
|
||||
"dates": "1987-",
|
||||
"ecoles": ["marxismes-ecologiques"],
|
||||
"ecole_principale": "marxismes-ecologiques",
|
||||
"livres_rag": [
|
||||
{ "slug": "saito-marx-anthropocene", "titre": "Marx dans l'Anthropocène", "annee": 2016, "couches": ["fond", "structure"] },
|
||||
{ "slug": "saito-decroissance-communisme", "titre": "La Décroissance communiste", "annee": 2020, "couches": ["fond", "structure"] },
|
||||
{ "slug": "saito-capital-anthropocene", "titre": "Le Capital dans l'Anthropocène", "annee": 2020, "couches": ["fond", "structure"] }
|
||||
],
|
||||
"theses_cles": ["Marx et l'écologie dans les cahiers tardifs", "Métabolisme social et rupture métabolique", "Décroissance communiste comme horizon"],
|
||||
"bio_courte": "Philosophe japonais, auteur d'une relecture écologiste des cahiers tardifs de Marx. Défend une décroissance communiste comme seule réponse cohérente à la crise écologique.",
|
||||
"note_rag": "saito-capital-anthropocene-en aussi indexe pour cross-language queries"
|
||||
},
|
||||
{
|
||||
"id": "michael-lowy",
|
||||
"nom": "Michael Löwy",
|
||||
"dates": "1938-",
|
||||
"ecoles": ["ecosocialisme"],
|
||||
"ecole_principale": "ecosocialisme",
|
||||
"livres_rag": [
|
||||
{ "slug": "lowy-ecosocialisme", "titre": "Écosocialisme", "annee": 2011, "couches": ["fond", "structure"] }
|
||||
],
|
||||
"theses_cles": ["Romantisme révolutionnaire", "Anticapitalisme écologique", "Walter Benjamin et l'écologie"],
|
||||
"bio_courte": "Sociologue franco-brésilien, figure centrale de l'écosocialisme. Articule marxisme hétérodoxe et critique de la modernité industrielle."
|
||||
},
|
||||
{
|
||||
"id": "andreas-malm",
|
||||
"nom": "Andreas Malm",
|
||||
"dates": "1977-",
|
||||
"ecoles": ["ecosocialisme"],
|
||||
"ecole_principale": "ecosocialisme",
|
||||
"livres_rag": [
|
||||
{ "slug": "malm-fossil-capital", "titre": "Fossil Capital", "annee": 2016, "couches": ["fond", "structure"] },
|
||||
{ "slug": "malm-saboter-pipeline", "titre": "Comment saboter un pipeline ?", "annee": 2020, "couches": ["fond", "structure"] },
|
||||
{ "slug": "malm-corona-climat", "titre": "Corona, Climate, Chronic Emergency", "annee": 2020, "couches": ["fond", "structure"] }
|
||||
],
|
||||
"theses_cles": ["Capitalisme fossile comme choix historique, non technologique", "Sabotage stratégique", "Urgence climatique et action directe"],
|
||||
"bio_courte": "Professeur d'écologie humaine à Lund. Théoricien du capital fossile et défenseur d'une écologie de guerre pour répondre à l'urgence climatique."
|
||||
},
|
||||
{
|
||||
"id": "naomi-klein",
|
||||
"nom": "Naomi Klein",
|
||||
"dates": "1970-",
|
||||
"ecoles": ["ecosocialisme"],
|
||||
"ecole_principale": "ecosocialisme",
|
||||
"livres_rag": [
|
||||
{ "slug": "klein-strategie-choc", "titre": "La Stratégie du choc", "annee": 2007, "couches": ["fond", "structure"] },
|
||||
{ "slug": "klein-tout-peut-changer", "titre": "Tout peut changer", "annee": 2014, "couches": ["fond", "structure"] },
|
||||
{ "slug": "klein-feu", "titre": "Le Feu qui nous attend", "annee": 2019, "couches": ["fond", "structure"] }
|
||||
],
|
||||
"theses_cles": ["Capitalisme du désastre", "Crise climatique comme opportunité de transformation radicale", "Gestion du choc comme tactique néolibérale"],
|
||||
"bio_courte": "Journaliste et activiste canadienne, une des voix les plus influentes du mouvement clima-justice. Articule critique du capitalisme et urgence écologique."
|
||||
},
|
||||
{
|
||||
"id": "andre-gorz",
|
||||
"nom": "André Gorz",
|
||||
"dates": "1923-2007",
|
||||
"ecoles": ["ecosocialisme", "decroissance", "technocritique"],
|
||||
"ecole_principale": "ecosocialisme",
|
||||
"livres_rag": [
|
||||
{ "slug": "gorz-capitalisme-socialisme-ecologie", "titre": "Capitalisme, Socialisme, Écologie", "annee": 1991, "couches": ["fond", "structure"] },
|
||||
{ "slug": "gorz-immateriel", "titre": "L'Immatériel", "annee": 2003, "couches": ["fond", "structure"] },
|
||||
{ "slug": "gorz-utopie-ou-mort", "titre": "Utopie ou mort", "annee": 1975, "couches": ["fond", "structure"] }
|
||||
],
|
||||
"theses_cles": ["Sortie du capitalisme par la réduction du temps de travail", "Économie de suffisance", "Immatériel comme nouvelle aliénation"],
|
||||
"bio_courte": "Philosophe austro-français, pionnier de l'écosocialisme et de la critique du travail. Relie marxisme, existentialisme et écologie dans une pensée de la libération."
|
||||
},
|
||||
{
|
||||
"id": "serge-latouche",
|
||||
"nom": "Serge Latouche",
|
||||
"dates": "1940-",
|
||||
"ecoles": ["decroissance"],
|
||||
"ecole_principale": "decroissance",
|
||||
"livres_rag": [
|
||||
{ "slug": "latouche-abondance-frugale", "titre": "Bon pour la casse : les déraisons de l'obsolescence programmée", "annee": 2012, "couches": ["fond", "structure"] },
|
||||
{ "slug": "latouche-petit-traite-decroissance", "titre": "Petit traité de la décroissance sereine", "annee": 2007, "couches": ["fond", "structure"] },
|
||||
{ "slug": "latouche-reenchanter-monde", "titre": "Pour un biorégionalisme en bonne intelligence avec les autres", "annee": 2019, "couches": ["fond", "structure"] }
|
||||
],
|
||||
"theses_cles": ["Sereine décroissance", "Critique du développement et de l'occidentalisation", "Société frugale abondante"],
|
||||
"bio_courte": "Économiste hétérodoxe franco-algérien, principal théoricien de la décroissance en France. Critique radical de l'économie du développement et de l'impérialisme culturel."
|
||||
},
|
||||
{
|
||||
"id": "nicholas-georgescu-roegen",
|
||||
"nom": "Nicholas Georgescu-Roegen",
|
||||
"dates": "1906-1994",
|
||||
"ecoles": ["decroissance"],
|
||||
"ecole_principale": "decroissance",
|
||||
"livres_rag": [
|
||||
{ "slug": "georgescu-decroissance", "titre": "Demain la décroissance", "annee": 1979, "couches": ["fond", "structure"] }
|
||||
],
|
||||
"theses_cles": ["Entropie et économie", "Impossibilité thermodynamique de la croissance infinie", "Bioéconomie"],
|
||||
"bio_courte": "Mathématicien et économiste roumain, père fondateur de la bioéconomie. Démontre que la croissance économique est irréversiblement limitée par les lois de la thermodynamique."
|
||||
},
|
||||
{
|
||||
"id": "donella-meadows",
|
||||
"nom": "Dennis et Donella Meadows",
|
||||
"dates": "1941-2001 / 1942-",
|
||||
"ecoles": ["decroissance"],
|
||||
"ecole_principale": "decroissance",
|
||||
"livres_rag": [
|
||||
{ "slug": "meadows-halte-croissance", "titre": "Halte à la croissance ?", "annee": 1972, "couches": ["fond", "structure"] },
|
||||
{ "slug": "meadows-thinking-systems", "titre": "Thinking in Systems", "annee": 2008, "couches": ["fond", "structure"] }
|
||||
],
|
||||
"theses_cles": ["Limites planétaires", "Modèles systémiques de l'overshoot", "Pensée systémique comme outil de transformation"],
|
||||
"bio_courte": "Le rapport Meadows (1972) est le premier modèle systémique démontrant l'impossibilité d'une croissance infinie dans un monde fini."
|
||||
},
|
||||
{
|
||||
"id": "pablo-servigne",
|
||||
"nom": "Pablo Servigne",
|
||||
"dates": "1978-",
|
||||
"ecoles": ["collapsologie"],
|
||||
"ecole_principale": "collapsologie",
|
||||
"livres_rag": [
|
||||
{ "slug": "servigne-effondrer", "titre": "Comment tout peut s'effondrer", "annee": 2015, "couches": ["fond", "structure"] },
|
||||
{ "slug": "servigne-autre-fin-du-monde", "titre": "Une autre fin du monde est possible", "annee": 2018, "couches": ["fond", "structure"] },
|
||||
{ "slug": "servigne-entraide-autre-loi", "titre": "L'Entraide, l'autre loi de la jungle", "annee": 2017, "couches": ["fond", "structure"] }
|
||||
],
|
||||
"theses_cles": ["Collapsologie", "Conditions systémiques de l'effondrement industriel", "Transition post-collapse et résilience collective"],
|
||||
"bio_courte": "Ingénieur agronome belge, cofondateur de la collapsologie. Explore les conditions d'un effondrement de la civilisation industrielle et les voies de résilience collective."
|
||||
},
|
||||
{
|
||||
"id": "francoise-deaubonne",
|
||||
"nom": "Françoise d'Eaubonne",
|
||||
"dates": "1920-2005",
|
||||
"ecoles": ["ecofeminismes"],
|
||||
"ecole_principale": "ecofeminismes",
|
||||
"livres_rag": [
|
||||
{ "slug": "eaubonne-feminisme-mort", "titre": "Le Féminisme ou la mort", "annee": 1974, "couches": ["fond", "structure"] }
|
||||
],
|
||||
"theses_cles": ["Écoféminisme (terme inventé en 1974)", "Patriarcat et destruction de la nature comme même système", "Révolution féministe écologique"],
|
||||
"bio_courte": "Féministe française, inventrice du terme écoféminisme en 1974. Lie patriarcat et destruction de l'environnement dans une même critique radicale."
|
||||
},
|
||||
{
|
||||
"id": "silvia-federici",
|
||||
"nom": "Silvia Federici",
|
||||
"dates": "1942-",
|
||||
"ecoles": ["ecofeminismes"],
|
||||
"ecole_principale": "ecofeminismes",
|
||||
"livres_rag": [
|
||||
{ "slug": "federici-caliban-sorciere", "titre": "Caliban et la sorcière", "annee": 2004, "couches": ["fond", "structure"] },
|
||||
{ "slug": "federici-par-dela-peau", "titre": "Par-delà la peau", "annee": 2019, "couches": ["fond", "structure"] },
|
||||
{ "slug": "federici-point-zero", "titre": "Le Point zéro de la révolution", "annee": 2012, "couches": ["fond", "structure"] }
|
||||
],
|
||||
"theses_cles": ["Accumulation primitive et corps des femmes", "Chasse aux sorcières comme contre-révolution", "Travail reproductif et commons"],
|
||||
"bio_courte": "Philosophe italo-américaine, théoricienne du féminisme marxiste. Caliban et la sorcière relit l'accumulation primitive à travers la domination des femmes et la destruction des commons."
|
||||
},
|
||||
{
|
||||
"id": "vandana-shiva",
|
||||
"nom": "Vandana Shiva",
|
||||
"dates": "1952-",
|
||||
"ecoles": ["ecofeminismes", "ecologies-decoloniales"],
|
||||
"ecole_principale": "ecofeminismes",
|
||||
"livres_rag": [
|
||||
{ "slug": "shiva-staying-alive", "titre": "Staying Alive: Women, Ecology and Development", "annee": 1988, "couches": ["fond", "structure"] }
|
||||
],
|
||||
"theses_cles": ["Biopiraterie et souveraineté alimentaire", "Écoféminisme tiers-mondiste", "Développement comme destruction"],
|
||||
"bio_courte": "Physicienne et militante indienne, figure mondiale de l'écoféminisme et de la souveraineté alimentaire. Cofondatrice de Navdanya, contre la biopiraterie des semences."
|
||||
},
|
||||
{
|
||||
"id": "fatima-ouassak",
|
||||
"nom": "Fatima Ouassak",
|
||||
"dates": "1978-",
|
||||
"ecoles": ["ecofeminismes", "ecologies-decoloniales"],
|
||||
"ecole_principale": "ecofeminismes",
|
||||
"livres_rag": [
|
||||
{ "slug": "ouassak-puissance-meres", "titre": "La Puissance des mères", "annee": 2020, "couches": ["fond", "structure"] }
|
||||
],
|
||||
"theses_cles": ["Féminisme populaire et de couleur", "Écologie de banlieue", "Puissance maternelle comme force politique"],
|
||||
"bio_courte": "Politiste et militante franco-tunisienne. Articule luttes de classes populaires, antiracisme et écologie dans une perspective féministe ancrée dans les quartiers."
|
||||
},
|
||||
{
|
||||
"id": "malcolm-ferdinand",
|
||||
"nom": "Malcom Ferdinand",
|
||||
"dates": "1985-",
|
||||
"ecoles": ["ecologies-decoloniales"],
|
||||
"ecole_principale": "ecologies-decoloniales",
|
||||
"livres_rag": [
|
||||
{ "slug": "ferdinand-ecologie-decoloniale", "titre": "Une écologie décoloniale", "annee": 2019, "couches": ["fond", "structure"] },
|
||||
{ "slug": "ferdinand-monde-en-commun", "titre": "Un monde en commun", "annee": 2022, "couches": ["fond", "structure"] }
|
||||
],
|
||||
"theses_cles": ["Double fracture coloniale et écologique", "Habiter le monde en commun", "Antillanité et écologie politique"],
|
||||
"bio_courte": "Ingénieur et philosophe martiniquais. Son oeuvre articule colonialisme et destruction de l'environnement autour de la double fracture coloniale-écologique."
|
||||
},
|
||||
{
|
||||
"id": "jacques-ellul",
|
||||
"nom": "Jacques Ellul",
|
||||
"dates": "1912-1994",
|
||||
"ecoles": ["technocritique"],
|
||||
"ecole_principale": "technocritique",
|
||||
"livres_rag": [
|
||||
{ "slug": "ellul-technique-enjeu-siecle", "titre": "La Technique ou l'Enjeu du siècle", "annee": 1954, "couches": ["fond", "structure"] },
|
||||
{ "slug": "ellul-systeme-technicien", "titre": "Le Système technicien", "annee": 1977, "couches": ["fond", "structure"] },
|
||||
{ "slug": "ellul-bluff-technologique", "titre": "Le Bluff technologique", "annee": 1988, "couches": ["fond", "structure"] }
|
||||
],
|
||||
"theses_cles": ["Technique comme système autonome échappant au contrôle humain", "Efficacité comme valeur unique de la modernité", "Propagande et technosystème"],
|
||||
"bio_courte": "Juriste, sociologue et théologien bordelais. Son oeuvre fondatrice analyse la Technique comme système autonome, père de la technocritique radicale française."
|
||||
},
|
||||
{
|
||||
"id": "bernard-charbonneau",
|
||||
"nom": "Bernard Charbonneau",
|
||||
"dates": "1910-1996",
|
||||
"ecoles": ["technocritique"],
|
||||
"ecole_principale": "technocritique",
|
||||
"livres_rag": [
|
||||
{ "slug": "charbonneau-jardin-babylone", "titre": "Le Jardin de Babylone", "annee": 1969, "couches": ["fond", "structure"] }
|
||||
],
|
||||
"theses_cles": ["Liberté contre organisation", "Critique de l'aménagement du territoire", "Personnalisme anarchisant et nature"],
|
||||
"bio_courte": "Essayiste béarnais, compagnon d'Ellul. Pionnier méconnu de l'écologie politique française. Défend la liberté contre toute organisation — État, marché, technique."
|
||||
},
|
||||
{
|
||||
"id": "rachel-carson",
|
||||
"nom": "Rachel Carson",
|
||||
"dates": "1907-1964",
|
||||
"ecoles": ["ethiques-environnementales"],
|
||||
"ecole_principale": "ethiques-environnementales",
|
||||
"livres_rag": [
|
||||
{ "slug": "carson-printemps-silencieux", "titre": "Printemps silencieux", "annee": 1962, "couches": ["fond", "structure"] },
|
||||
{ "slug": "carson-mer-autour-de-nous", "titre": "The Sea Around Us", "annee": 1951, "couches": ["fond", "structure"] }
|
||||
],
|
||||
"theses_cles": ["Impact des pesticides sur les écosystèmes", "Naissance du mouvement environnementaliste moderne", "Responsabilité scientifique et démocratie"],
|
||||
"bio_courte": "Marine biologist et autrice américaine. Printemps silencieux (1962) a lancé le mouvement environnementaliste moderne en dénonçant les pesticides.",
|
||||
"note_rag": "carson-mer-autour-de-nous-fr aussi indexe pour cross-language queries"
|
||||
},
|
||||
{
|
||||
"id": "arne-naess",
|
||||
"nom": "Arne Næss",
|
||||
"dates": "1912-2009",
|
||||
"ecoles": ["ethiques-environnementales"],
|
||||
"ecole_principale": "ethiques-environnementales",
|
||||
"livres_rag": [
|
||||
{ "slug": "naess-ecology-of-wisdom", "titre": "Ecology of Wisdom", "annee": 2008, "couches": ["fond", "structure"] }
|
||||
],
|
||||
"theses_cles": ["Deep ecology vs écologie superficielle", "Égalité biosphérique", "Réalisation de Soi élargie au-delà du moi individuel"],
|
||||
"bio_courte": "Philosophe norvégien, fondateur de la deep ecology. Défend une valeur intrinsèque de tous les êtres vivants, indépendamment de leur utilité pour les humains."
|
||||
},
|
||||
{
|
||||
"id": "philippe-descola",
|
||||
"nom": "Philippe Descola",
|
||||
"dates": "1949-",
|
||||
"ecoles": ["pensees-vivant"],
|
||||
"ecole_principale": "pensees-vivant",
|
||||
"livres_rag": [
|
||||
{ "slug": "descola-par-dela-nature-culture", "titre": "Par-delà nature et culture", "annee": 2005, "couches": ["fond", "structure"] },
|
||||
{ "slug": "descola-composition-mondes", "titre": "La Composition des mondes", "annee": 2014, "couches": ["fond", "structure"] }
|
||||
],
|
||||
"theses_cles": ["Dualisme nature/culture comme exception occidentale", "4 ontologies (animisme, totémisme, analogisme, naturalisme)", "Cosmopolitiques et pluriversalité"],
|
||||
"bio_courte": "Anthropologue et ethnologue français, successeur de Lévi-Strauss au Collège de France. Démontre que le dualisme nature/culture est une anomalie culturelle occidentale."
|
||||
},
|
||||
{
|
||||
"id": "vinciane-despret",
|
||||
"nom": "Vinciane Despret",
|
||||
"dates": "1959-",
|
||||
"ecoles": ["pensees-vivant"],
|
||||
"ecole_principale": "pensees-vivant",
|
||||
"livres_rag": [
|
||||
{ "slug": "despret-habiter-oiseau", "titre": "Habiter en oiseau", "annee": 2019, "couches": ["fond", "structure"] },
|
||||
{ "slug": "despret-autobiographie-poulpe", "titre": "Autobiographie d'un poulpe", "annee": 2021, "couches": ["fond", "structure"] },
|
||||
{ "slug": "despret-quand-loup-habitera", "titre": "Quand le loup habitera avec l'agneau", "annee": 2002, "couches": ["fond", "structure"] }
|
||||
],
|
||||
"theses_cles": ["Éthologie politique", "Faire bon ménage avec les non-humains", "Épistémologie du point de vue animal"],
|
||||
"bio_courte": "Philosophe et éthologiste belge. Explore comment penser avec les animaux plutôt que sur eux, développant une éthologie politique de la cohabitation inter-espèces."
|
||||
},
|
||||
{
|
||||
"id": "baptiste-morizot",
|
||||
"nom": "Baptiste Morizot",
|
||||
"dates": "1983-",
|
||||
"ecoles": ["pensees-vivant"],
|
||||
"ecole_principale": "pensees-vivant",
|
||||
"livres_rag": [
|
||||
{ "slug": "morizot-sur-piste-animale", "titre": "Sur la piste animale", "annee": 2018, "couches": ["fond", "structure"] },
|
||||
{ "slug": "morizot-manieres-etre-vivant", "titre": "Manières d'être vivant", "annee": 2020, "couches": ["fond", "structure"] },
|
||||
{ "slug": "morizot-raviver-braises", "titre": "Raviver les braises du vivant", "annee": 2020, "couches": ["fond", "structure"] }
|
||||
],
|
||||
"theses_cles": ["Crise de la sensibilité au vivant", "Diplomatie sauvage", "Désensauvagement comme désorientation ontologique"],
|
||||
"bio_courte": "Philosophe et pisteur français. Propose une diplomatie sauvage fondée sur l'attention au vivant. La crise écologique comme crise de la relation, avant d'être une crise de ressources."
|
||||
},
|
||||
{
|
||||
"id": "bruno-latour",
|
||||
"nom": "Bruno Latour",
|
||||
"dates": "1947-2022",
|
||||
"ecoles": ["pensees-vivant"],
|
||||
"ecole_principale": "pensees-vivant",
|
||||
"livres_rag": [
|
||||
{ "slug": "latour-jamais-ete-modernes", "titre": "Nous n'avons jamais été modernes", "annee": 1991, "couches": ["fond", "structure"] },
|
||||
{ "slug": "latour-face-a-gaia", "titre": "Face à Gaïa", "annee": 2015, "couches": ["fond", "structure"] },
|
||||
{ "slug": "latour-ou-atterrir", "titre": "Où atterrir ?", "annee": 2017, "couches": ["fond", "structure"] }
|
||||
],
|
||||
"theses_cles": ["Modernes n'ayant jamais séparé nature et société", "Gaïa comme entité politique", "Terrestres vs Hors-sol"],
|
||||
"bio_courte": "Sociologue et philosophe français, fondateur de la théorie acteur-réseau. Son dernier travail tourne autour de Gaïa et de la question politique du Terrestre face au dérèglement."
|
||||
},
|
||||
{
|
||||
"id": "isabelle-stengers",
|
||||
"nom": "Isabelle Stengers",
|
||||
"dates": "1949-",
|
||||
"ecoles": ["pensees-vivant"],
|
||||
"ecole_principale": "pensees-vivant",
|
||||
"livres_rag": [
|
||||
{ "slug": "stengers-cosmopolitiques", "titre": "Cosmopolitiques", "annee": 1997, "couches": ["fond", "structure"] },
|
||||
{ "slug": "stengers-reactiver-sens-commun", "titre": "Réactiver le sens commun", "annee": 2020, "couches": ["fond", "structure"] }
|
||||
],
|
||||
"theses_cles": ["Cosmopolitiques : faire droit aux pratiques non-scientifiques", "Capitalisme comme sorcellerie", "Sens commun contre la raison instrumentale"],
|
||||
"bio_courte": "Philosophe des sciences belge. Déploie une pensée cosmopolitique qui fait droit à toutes les pratiques, scientifiques et non-scientifiques, face à la destruction capitaliste."
|
||||
}
|
||||
]
|
||||
}
|
||||
119
server/api/chatbot-pensees.post.ts
Normal file
119
server/api/chatbot-pensees.post.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
import type { H3Event } from 'h3'
|
||||
import { checkRateLimitJson } from '~/server/utils/rateLimitJson'
|
||||
|
||||
interface ChatbotPenseesRequest {
|
||||
query: string
|
||||
mode?: 'hybrid' | 'local' | 'global' | 'naive' | 'mix'
|
||||
corpus?: 'pensees' | 'projets' | 'both'
|
||||
filter_couche?: 'fond' | 'forme' | 'structure' | null
|
||||
filter_ecole?: string | null
|
||||
history?: Array<{ role: 'user' | 'assistant'; content: string }>
|
||||
}
|
||||
|
||||
interface LightRAGQueryResponse {
|
||||
response: string
|
||||
}
|
||||
|
||||
const SYSTEM_PREFACE_PENSEES = `Tu es un agent du RAG Pensées Écologiques, infrastructure militante du collectif trans-former.fr.
|
||||
Tu réponds en t'appuyant STRICTEMENT sur le corpus ingéré (auteurs FRACAS Bonpote : écosocialisme, éco-anarchisme, écoféminismes, écologies décoloniales, technocritique, pensées du vivant, décroissance...).
|
||||
|
||||
Règles :
|
||||
- Cite les sources (auteur, livre) à chaque assertion importante.
|
||||
- Si la question dépasse le corpus, dis-le clairement. Pas d'hallucination.
|
||||
- Ton politique direct, pas de neutralité fade.
|
||||
- Réponse en français, dense, sans délayage.
|
||||
- Distingue les positions selon les écoles quand elles divergent.`
|
||||
|
||||
const SYSTEM_PREFACE_PROJETS = `Tu es un agent du RAG Projets de Jules Nény (architecte, collectif trans-former.fr).
|
||||
Tu réponds STRICTEMENT à partir des documents projet (fichiers butte-pinson__*.md et autres projets archi de Jules).
|
||||
N'utilise PAS le corpus FRACAS Pensées Écologiques pour répondre, sauf si l'usager te le demande explicitement.
|
||||
|
||||
Règles :
|
||||
- Cite les sources (nom de projet, document) à chaque assertion importante.
|
||||
- Si la question dépasse le corpus projet, dis-le clairement. Pas d'hallucination.
|
||||
- Ton praticien réflexif : 1ère personne quand pertinent, narration située.
|
||||
- Réponse en français, dense, sans délayage.`
|
||||
|
||||
const SYSTEM_PREFACE_BOTH = `Tu es un agent du RAG croisé Pensées x Projets de Jules Nény (architecte militant, collectif trans-former.fr).
|
||||
CENTRE TA RÉPONSE sur les documents PROJETS (fichiers butte-pinson__*.md et autres projets archi).
|
||||
Mobilise le corpus FRACAS Pensées (autres fichiers) UNIQUEMENT pour éclairer théoriquement les partis pris des projets, jamais l'inverse.
|
||||
|
||||
Pondération attendue : ~70% ancrage projet concret, ~30% éclairage théorique FRACAS.
|
||||
|
||||
Règles :
|
||||
- Cite les sources (auteur ou nom de projet, document) à chaque assertion.
|
||||
- Si un thème n'est pas couvert par les projets, dis-le clairement avant d'éventuellement étendre au corpus Pensées.
|
||||
- Pas d'hallucination, pas d'extrapolation hors corpus.
|
||||
- Ton praticien militant : direct, pas neutre, ancré dans la pratique architecturale.
|
||||
- Réponse en français, dense, sans délayage.`
|
||||
|
||||
export default defineEventHandler(async (event: H3Event) => {
|
||||
const config = useRuntimeConfig(event)
|
||||
|
||||
// 1. Rate limit (20 req/jour/IP, IP hashée RGPD)
|
||||
const ip =
|
||||
getHeader(event, 'x-forwarded-for')?.split(',')[0].trim() ||
|
||||
event.node.req.socket?.remoteAddress ||
|
||||
'0.0.0.0'
|
||||
|
||||
const allowed = checkRateLimitJson(ip, 'chatbot-pensees', 20)
|
||||
if (!allowed) {
|
||||
throw createError({ statusCode: 429, message: 'Limite de 20 questions par jour atteinte.' })
|
||||
}
|
||||
|
||||
// 2. Body parse + validation
|
||||
const body = await readBody<ChatbotPenseesRequest>(event)
|
||||
if (!body?.query || body.query.trim().length < 3 || body.query.trim().length > 500) {
|
||||
throw createError({ statusCode: 400, message: 'Query invalide (3-500 caractères).' })
|
||||
}
|
||||
|
||||
const query = body.query.trim()
|
||||
const mode = body.mode || 'hybrid'
|
||||
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
|
||||
|
||||
// 3. Health guard — LightRAG down = erreur claire, pas de fallback hallucinatoire
|
||||
try {
|
||||
await $fetch(`${ragUrl}/health`, { timeout: 5000 })
|
||||
} catch {
|
||||
throw createError({
|
||||
statusCode: 503,
|
||||
message: 'RAG indisponible pour l\'instant — réessaie dans quelques minutes.',
|
||||
})
|
||||
}
|
||||
|
||||
// 4. Call LightRAG VPS — préface système injectée dans la query
|
||||
const ragQuery = `${systemPreface}\n\nQuestion : ${query}`
|
||||
|
||||
let ragResponse: LightRAGQueryResponse
|
||||
try {
|
||||
ragResponse = await $fetch<LightRAGQueryResponse>(`${ragUrl}/query`, {
|
||||
method: 'POST',
|
||||
body: { query: ragQuery, mode },
|
||||
timeout: 90000,
|
||||
})
|
||||
} catch (e: any) {
|
||||
const status = e?.response?.status
|
||||
if (status === 429) {
|
||||
throw createError({ statusCode: 429, message: 'RAG saturé — réessaie dans quelques instants.' })
|
||||
}
|
||||
throw createError({ statusCode: 504, message: 'RAG en cours de processing — réessaie dans quelques secondes.' })
|
||||
}
|
||||
|
||||
// 5. Retour formaté
|
||||
return {
|
||||
response: ragResponse.response ?? '',
|
||||
mode,
|
||||
corpus,
|
||||
filter: { couche: body.filter_couche ?? null, ecole: body.filter_ecole ?? null },
|
||||
timestamp: new Date().toISOString(),
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user