Compare commits
2 Commits
feat/outil
...
586742d90e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
586742d90e | ||
|
|
668ae5caff |
File diff suppressed because one or more lines are too long
141
components/ChatbotPensees.vue
Normal file
141
components/ChatbotPensees.vue
Normal file
@@ -0,0 +1,141 @@
|
||||
<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">
|
||||
<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>
|
||||
<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;">
|
||||
Pose une question sur les pensees ecologiques : ecosocialisme, decroissance, ecofeminismes, technocritique, deep ecology...
|
||||
</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="parseSrc(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 ({{ parseSrc(msg.content).length }})
|
||||
</button>
|
||||
<div v-if="toggled[i]" class="mt-1 flex flex-col gap-1">
|
||||
<div v-for="(s, si) in parseSrc(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>
|
||||
<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 }
|
||||
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
|
||||
|
||||
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' } })
|
||||
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]))]
|
||||
}
|
||||
</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>
|
||||
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,30 +0,0 @@
|
||||
<template>
|
||||
<div class="media-tab-backend" style="padding: 2rem; overflow-y: auto;">
|
||||
<div style="max-width: 640px;">
|
||||
<h2 style="font-weight: 700; font-size: 1.1rem; margin-bottom: 0.75rem; color: var(--nav-text);">LightRAG backend</h2>
|
||||
<p style="font-size: 0.9rem; line-height: 1.6; color: var(--nav-text); margin-bottom: 0.5rem;">
|
||||
Voici l'interface brute du <strong>LightRAG</strong> qui alimente la carte des pensées écologiques.
|
||||
C'est la "cuisine" du RAG : ingestion de documents, extraction d'entités, relations, requêtes.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- PLACEHOLDER — DNS en attente
|
||||
TODO: Décommenter iframe + supprimer placeholder une fois lightrag.trans-former.fr propagé.
|
||||
DNS A record à créer sur OVH : lightrag → 178.104.106.195 TTL 300
|
||||
-->
|
||||
<div style="margin-top: 1.5rem; padding: 2rem; border: 2px dashed var(--nav-bg-alt, #ddd); border-radius: 8px; text-align: center; color: var(--nav-text-muted);">
|
||||
<p style="font-size: 1rem; font-weight: 600; margin-bottom: 0.5rem;">⏳ Backend en cours d'exposition publique — bientôt accessible.</p>
|
||||
<p style="font-size: 0.85rem;">L'interface LightRAG sera disponible ici dès la mise en place du sous-domaine <code>lightrag.trans-former.fr</code>.</p>
|
||||
</div>
|
||||
|
||||
<!--
|
||||
<iframe
|
||||
src="https://lightrag.trans-former.fr/"
|
||||
style="width: 100%; height: 70vh; border: 1px solid var(--nav-bg-alt, #ddd); border-radius: 8px; margin-top: 1.5rem;"
|
||||
title="LightRAG backend AEP — lecture seule"
|
||||
sandbox="allow-same-origin allow-scripts"
|
||||
loading="lazy"
|
||||
/>
|
||||
-->
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,67 +0,0 @@
|
||||
<template>
|
||||
<div class="media-tab-projets" style="padding: 1.5rem; overflow-y: auto;">
|
||||
<div style="max-width: 70ch; margin-bottom: 1.5rem;">
|
||||
<h2 style="font-weight: 700; font-size: 1.1rem; margin-bottom: 0.5rem; color: var(--nav-text);">PFE engagés</h2>
|
||||
<p style="font-size: 0.9rem; line-height: 1.6; color: var(--nav-text);">
|
||||
Mutualiser le savoir. Voici les PFE engagés publiés en ligne dont nous avons connaissance.
|
||||
Partage-nous le lien de ton travail si tu veux participer à cette initiative.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="projets-grid">
|
||||
<article v-for="p in projets" :key="p.id" class="projet-card">
|
||||
<img v-if="p.thumb" :src="p.thumb" :alt="p.titre" class="projet-thumb" loading="lazy" />
|
||||
<div v-else class="projet-thumb projet-thumb--placeholder">📐</div>
|
||||
|
||||
<h3 style="font-weight: 600; font-size: 0.95rem; margin: 0.5rem 0 0.25rem; color: var(--nav-text);">{{ p.titre }}</h3>
|
||||
<p style="font-size: 0.8rem; color: var(--nav-text-muted); margin-bottom: 0.5rem;">
|
||||
{{ (p.auteurs || []).filter((a: string) => a !== 'Inconnu').join(', ') }}
|
||||
<template v-if="p.ecole && p.ecole !== 'Inconnu'"> · {{ p.ecole }}</template>
|
||||
<template v-if="p.annee && p.annee !== 'Inconnu'"> · {{ p.annee }}</template>
|
||||
</p>
|
||||
<p style="font-size: 0.875rem; line-height: 1.5; color: var(--nav-text); flex: 1; margin-bottom: 0.75rem;">{{ p.description }}</p>
|
||||
|
||||
<a v-if="p.url" :href="p.url" target="_blank" rel="noopener" style="color: var(--nav-primary-solid, #3b6ea5); font-weight: 600; font-size: 0.875rem; text-decoration: none;">
|
||||
Découvrir →
|
||||
</a>
|
||||
<span v-if="p.link_status === 'broken'" style="color: #e67e22; font-size: 0.8rem; display: block; margin-top: 0.25rem;">⚠ Lien d'origine cassé</span>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<p style="margin-top: 2rem; font-size: 0.875rem; color: var(--nav-text-muted);">
|
||||
Tu as un PFE engagé à partager ? <a href="mailto:contact@trans-former.fr" style="color: var(--nav-primary-solid);">Écris-moi</a>.
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const { data: pfeData } = await useFetch<{ projets: any[] }>('/data/pfe-engages.json')
|
||||
const projets = computed(() => pfeData.value?.projets ?? [])
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.projets-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: 1.25rem;
|
||||
}
|
||||
.projet-card {
|
||||
border: 1px solid var(--nav-bg-alt, #eee);
|
||||
border-radius: 10px;
|
||||
padding: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--nav-surface);
|
||||
}
|
||||
.projet-thumb {
|
||||
width: 100%;
|
||||
height: 140px;
|
||||
object-fit: cover;
|
||||
border-radius: 6px;
|
||||
background: var(--nav-bg-alt, #f5f5f5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 2rem;
|
||||
}
|
||||
</style>
|
||||
@@ -1,604 +0,0 @@
|
||||
<template>
|
||||
<div class="media-visuel">
|
||||
|
||||
<!-- Conteneur split / plein ecran -->
|
||||
<div class="layout-container">
|
||||
|
||||
<!-- Slot carte D3 -->
|
||||
<div
|
||||
class="carte-slot"
|
||||
:class="[
|
||||
layoutMode === 'split' ? 'carte-split' : '',
|
||||
layoutMode === 'carte-full' ? 'carte-full' : '',
|
||||
layoutMode === 'chatbot-full' ? 'carte-hidden' : '',
|
||||
]"
|
||||
:style="layoutMode === 'split' ? { flexBasis: carteFlexBasis } : {}"
|
||||
style="position: relative;"
|
||||
>
|
||||
<ClientOnly>
|
||||
<CartePensees
|
||||
ref="cartePenseesRef"
|
||||
: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);">
|
||||
Chargement de la carte...
|
||||
</div>
|
||||
</template>
|
||||
</ClientOnly>
|
||||
|
||||
<!-- Overlay PDF FRACAS -->
|
||||
<div
|
||||
v-if="showFracasPdf"
|
||||
class="fracas-overlay"
|
||||
:style="{ opacity: fracasOpacity / 100 }"
|
||||
>
|
||||
<embed
|
||||
src="/cartes/carte-fracas-bonpote-v2.pdf"
|
||||
type="application/pdf"
|
||||
style="width: 100%; height: 100%;"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Barre de toggle -->
|
||||
<div class="layout-toggle-bar shrink-0">
|
||||
<button
|
||||
@click="setLayoutMode('carte-full')"
|
||||
:class="{ active: layoutMode === 'carte-full' }"
|
||||
class="toggle-btn"
|
||||
title="Carte en plein ecran"
|
||||
>
|
||||
<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">
|
||||
<polyline points="15 3 21 3 21 9"/><polyline points="9 21 3 21 3 15"/>
|
||||
<line x1="21" y1="3" x2="14" y2="10"/><line x1="3" y1="21" x2="10" y2="14"/>
|
||||
</svg>
|
||||
Carte plein ecran
|
||||
</button>
|
||||
<button
|
||||
v-if="layoutMode !== 'split'"
|
||||
@click="setLayoutMode('split')"
|
||||
class="toggle-btn"
|
||||
title="Vue partagee"
|
||||
>
|
||||
<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">
|
||||
<rect x="3" y="3" width="18" height="18" rx="2"/><line x1="3" y1="12" x2="21" y2="12"/>
|
||||
</svg>
|
||||
Vue partagee
|
||||
</button>
|
||||
<button
|
||||
@click="setLayoutMode('chatbot-full')"
|
||||
:class="{ active: layoutMode === 'chatbot-full' }"
|
||||
class="toggle-btn"
|
||||
title="Chatbot en plein ecran"
|
||||
>
|
||||
<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">
|
||||
<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>
|
||||
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>
|
||||
|
||||
<!-- Toggle PDF FRACAS -->
|
||||
<label class="layer-toggle" title="Superposer la carte FRACAS Bonpote V2 en PDF">
|
||||
<input type="checkbox" v-model="showFracasPdf" />
|
||||
📄 Carte FRACAS (PDF)
|
||||
</label>
|
||||
<input
|
||||
v-if="showFracasPdf"
|
||||
type="range"
|
||||
min="0"
|
||||
max="100"
|
||||
v-model.number="fracasOpacity"
|
||||
class="opacity-slider"
|
||||
:title="`Opacité ${fracasOpacity}%`"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Poignee draggable (visible uniquement en mode split, pas sur mobile) -->
|
||||
<div
|
||||
v-if="layoutMode === 'split'"
|
||||
class="split-handle"
|
||||
@mousedown.prevent="onHandleMousedown"
|
||||
title="Redimensionner"
|
||||
>
|
||||
<span class="split-handle-grip"></span>
|
||||
</div>
|
||||
|
||||
<!-- Slot chatbot inline -->
|
||||
<div
|
||||
class="chatbot-slot"
|
||||
:class="[
|
||||
layoutMode === 'split' ? 'chatbot-split' : '',
|
||||
layoutMode === 'chatbot-full' ? 'chatbot-full-mode' : '',
|
||||
layoutMode === 'carte-full' ? 'chatbot-hidden' : '',
|
||||
]"
|
||||
:style="layoutMode === 'split' ? { flexBasis: chatbotFlexBasis } : {}"
|
||||
>
|
||||
<ClientOnly>
|
||||
<ChatbotPensees :auteurContext="chatbotAuteur" :inline="true" />
|
||||
</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>
|
||||
|
||||
<!-- Fiche auteur modal -->
|
||||
<FicheAuteur
|
||||
:open="ficheOpen"
|
||||
:auteurId="ficheAuteurId"
|
||||
:data="penseesData"
|
||||
@close="ficheOpen = false"
|
||||
@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>
|
||||
|
||||
<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[] }
|
||||
|
||||
type LayoutMode = 'split' | 'carte-full' | 'chatbot-full' | 'bonpote'
|
||||
|
||||
const STORAGE_KEY = 'media-layout-mode'
|
||||
const SPLIT_RATIO_KEY = 'media-split-ratio'
|
||||
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 layoutMode = ref<LayoutMode>('split')
|
||||
const cartePenseesRef = ref<{ triggerResize: () => void } | null>(null)
|
||||
|
||||
// Toggle PDF FRACAS
|
||||
const showFracasPdf = ref(false)
|
||||
const fracasOpacity = ref(60)
|
||||
|
||||
// Props injectées depuis le parent (penseesData)
|
||||
const props = defineProps<{ penseesData: PenseesData | null }>()
|
||||
|
||||
// Ratio de la carte vs chatbot en mode split (0.2 a 0.8)
|
||||
const splitRatio = ref(DEFAULT_SPLIT_RATIO)
|
||||
const carteFlexBasis = computed(() => `${splitRatio.value * 100}%`)
|
||||
const chatbotFlexBasis = computed(() => `${(1 - splitRatio.value) * 100}%`)
|
||||
|
||||
// Logique poignee draggable
|
||||
let dragStartY = 0
|
||||
let dragStartRatio = DEFAULT_SPLIT_RATIO
|
||||
let containerHeight = 0
|
||||
|
||||
function onHandleMousedown(e: MouseEvent) {
|
||||
dragStartY = e.clientY
|
||||
dragStartRatio = splitRatio.value
|
||||
const container = (e.target as HTMLElement)?.closest('.layout-container') as HTMLElement | null
|
||||
containerHeight = container ? container.clientHeight : window.innerHeight
|
||||
|
||||
window.addEventListener('mousemove', onHandleMousemove)
|
||||
window.addEventListener('mouseup', onHandleMouseup)
|
||||
}
|
||||
|
||||
function onHandleMousemove(e: MouseEvent) {
|
||||
const delta = e.clientY - dragStartY
|
||||
const newRatio = dragStartRatio + delta / containerHeight
|
||||
splitRatio.value = Math.min(0.80, Math.max(0.20, newRatio))
|
||||
}
|
||||
|
||||
function onHandleMouseup() {
|
||||
window.removeEventListener('mousemove', onHandleMousemove)
|
||||
window.removeEventListener('mouseup', onHandleMouseup)
|
||||
if (typeof window !== 'undefined') {
|
||||
localStorage.setItem(SPLIT_RATIO_KEY, String(splitRatio.value))
|
||||
}
|
||||
cartePenseesRef.value?.triggerResize()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
const saved = localStorage.getItem(STORAGE_KEY) as LayoutMode | null
|
||||
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
|
||||
}
|
||||
if (!localStorage.getItem('rag-fracas-info-seen')) {
|
||||
ragInfoOpen.value = true
|
||||
localStorage.setItem('rag-fracas-info-seen', '1')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
function setLayoutMode(mode: LayoutMode) {
|
||||
layoutMode.value = mode
|
||||
if (typeof window !== 'undefined') {
|
||||
localStorage.setItem(STORAGE_KEY, mode)
|
||||
}
|
||||
if (mode !== 'chatbot-full') {
|
||||
setTimeout(() => {
|
||||
cartePenseesRef.value?.triggerResize()
|
||||
}, 350)
|
||||
}
|
||||
}
|
||||
|
||||
watch(layoutMode, (v) => {
|
||||
if (typeof window !== 'undefined') {
|
||||
localStorage.setItem(STORAGE_KEY, v)
|
||||
}
|
||||
})
|
||||
|
||||
function onSelectAuteur(id: string) {
|
||||
ficheAuteurId.value = id
|
||||
ficheOpen.value = true
|
||||
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 = props.penseesData?.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 = props.penseesData?.auteurs.find(a => a.id === auteurId)
|
||||
chatbotAuteur.value = auteur?.nom ?? null
|
||||
if (layoutMode.value === 'carte-full') {
|
||||
setLayoutMode('split')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.media-visuel {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* Conteneur des slots carte + toggle + chatbot */
|
||||
.layout-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* --- Slot carte --- */
|
||||
.carte-slot {
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.carte-split {
|
||||
flex: 0 0 66%;
|
||||
min-height: 0;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.carte-full {
|
||||
flex: 1 1 100%;
|
||||
min-height: 0;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.carte-hidden {
|
||||
flex: 0 0 0;
|
||||
height: 0;
|
||||
opacity: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* --- Overlay PDF FRACAS --- */
|
||||
.fracas-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 50;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* --- Barre de toggle --- */
|
||||
.layout-toggle-bar {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 4px 12px;
|
||||
background: var(--nav-bg);
|
||||
border-top: 1px solid rgba(180, 170, 160, 0.22);
|
||||
border-bottom: 1px solid rgba(180, 170, 160, 0.22);
|
||||
min-height: 38px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.toggle-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
padding: 4px 10px;
|
||||
border-radius: 6px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
background: var(--nav-bg-alt);
|
||||
color: var(--nav-text-muted);
|
||||
border: 1px solid transparent;
|
||||
transition: background 0.15s, color 0.15s, border-color 0.15s;
|
||||
}
|
||||
|
||||
.toggle-btn:hover {
|
||||
background: var(--nav-surface);
|
||||
color: var(--nav-text);
|
||||
}
|
||||
|
||||
.toggle-btn.active {
|
||||
background: var(--nav-primary);
|
||||
color: var(--nav-text-on-primary);
|
||||
border-color: var(--nav-primary);
|
||||
}
|
||||
|
||||
/* --- Toggle layer PDF FRACAS --- */
|
||||
.layer-toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
padding: 4px 10px;
|
||||
border-radius: 6px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
background: var(--nav-bg-alt);
|
||||
color: var(--nav-text-muted);
|
||||
border: 1px solid transparent;
|
||||
user-select: none;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.layer-toggle input[type="checkbox"] {
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.opacity-slider {
|
||||
width: 80px;
|
||||
cursor: pointer;
|
||||
accent-color: var(--nav-primary, #3b6ea5);
|
||||
}
|
||||
|
||||
/* --- Poignee draggable entre carte et chatbot --- */
|
||||
.split-handle {
|
||||
flex-shrink: 0;
|
||||
height: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: row-resize;
|
||||
background: transparent;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.split-handle:hover {
|
||||
background: rgba(180, 170, 160, 0.18);
|
||||
}
|
||||
|
||||
.split-handle-grip {
|
||||
display: block;
|
||||
width: 32px;
|
||||
height: 4px;
|
||||
border-radius: 2px;
|
||||
background: repeating-linear-gradient(
|
||||
to bottom,
|
||||
rgba(160, 150, 140, 0.55) 0px,
|
||||
rgba(160, 150, 140, 0.55) 1px,
|
||||
transparent 1px,
|
||||
transparent 3px
|
||||
);
|
||||
}
|
||||
|
||||
/* Masquer la poignee sur mobile (ratio fixe) */
|
||||
@media (max-width: 767px) {
|
||||
.split-handle {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* --- Slot chatbot --- */
|
||||
.chatbot-slot {
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
transition: opacity 0.2s ease;
|
||||
border-top: 1px solid rgba(180, 170, 160, 0.28);
|
||||
}
|
||||
|
||||
.chatbot-split {
|
||||
flex: 0 0 34%;
|
||||
min-height: 0;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.chatbot-full-mode {
|
||||
flex: 1 1 100%;
|
||||
min-height: 0;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.chatbot-hidden {
|
||||
flex: 0 0 0;
|
||||
height: 0;
|
||||
opacity: 0;
|
||||
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) --- */
|
||||
@media (max-width: 767px) {
|
||||
.carte-split {
|
||||
flex: 0 0 60vh;
|
||||
height: 60vh;
|
||||
}
|
||||
|
||||
.chatbot-split {
|
||||
flex: 0 0 calc(40vh - 38px);
|
||||
height: calc(40vh - 38px);
|
||||
}
|
||||
|
||||
.toggle-btn span,
|
||||
.toggle-btn {
|
||||
font-size: 0.7rem;
|
||||
padding: 3px 7px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -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="/pensees-ecologiques"
|
||||
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"
|
||||
>Pensees</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="/pensees-ecologiques"
|
||||
class="flex-1 py-2 text-sm font-medium transition-colors text-center"
|
||||
style="color: var(--nav-text-muted); border-bottom: 2px solid transparent;"
|
||||
>Pensees</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"
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
<template>
|
||||
<div class="media-page" style="background: var(--nav-bg);">
|
||||
<nav class="subtabs" style="display:flex; gap:0; border-bottom: 1px solid var(--nav-bg-alt); background: var(--nav-surface); padding: 0 1rem;">
|
||||
<button
|
||||
:class="['subtab-btn', { active: tab === 'visuel' }]"
|
||||
@click="tab = 'visuel'"
|
||||
>
|
||||
🌳 RAG visuel
|
||||
</button>
|
||||
<button
|
||||
:class="['subtab-btn', { active: tab === 'backend' }]"
|
||||
@click="tab = 'backend'"
|
||||
>
|
||||
⚙ LightRAG backend
|
||||
</button>
|
||||
<button
|
||||
:class="['subtab-btn', { active: tab === 'projets' }]"
|
||||
@click="tab = 'projets'"
|
||||
>
|
||||
📚 Projets
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<MediaTabVisuel v-if="tab === 'visuel'" />
|
||||
<MediaTabBackend v-else-if="tab === 'backend'" />
|
||||
<MediaTabProjets v-else-if="tab === 'projets'" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const tab = ref<'visuel' | 'backend' | 'projets'>(
|
||||
(['visuel', 'backend', 'projets'].includes(route.query.tab as string)
|
||||
? route.query.tab as 'visuel' | 'backend' | 'projets'
|
||||
: 'visuel')
|
||||
)
|
||||
|
||||
watch(tab, (newTab) => {
|
||||
router.replace({ query: { ...route.query, tab: newTab } })
|
||||
})
|
||||
|
||||
useHead({ title: 'AEP - Media' })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.media-page { display: flex; flex-direction: column; height: 100%; overflow: hidden; }
|
||||
.subtabs { display: flex; gap: 0; flex-shrink: 0; }
|
||||
.subtab-btn {
|
||||
padding: 10px 18px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
background: none;
|
||||
border: none;
|
||||
border-bottom: 2px solid transparent;
|
||||
cursor: pointer;
|
||||
color: var(--nav-text-muted);
|
||||
transition: color 0.15s, border-color 0.15s;
|
||||
}
|
||||
.subtab-btn:hover { color: var(--nav-text); }
|
||||
.subtab-btn.active { color: var(--nav-primary-solid); border-bottom-color: var(--nav-primary-solid); font-weight: 600; }
|
||||
</style>
|
||||
83
pages/pensees-ecologiques.vue
Normal file
83
pages/pensees-ecologiques.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);">Pensees Ecologiques</h1>
|
||||
<p class="text-xs mt-0.5" style="color: var(--nav-text-muted);">
|
||||
{{ corpusCount }} auteurs ingeres 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 - Pensees Ecologiques - Carte FRACAS' })
|
||||
</script>
|
||||
File diff suppressed because one or more lines are too long
300
public/data/auteurs-pensees.json
Normal file
300
public/data/auteurs-pensees.json
Normal file
@@ -0,0 +1,300 @@
|
||||
{
|
||||
"meta": {
|
||||
"version": "1.0",
|
||||
"source": "FRACAS Bonpote V2 oct 2024 + LightRAG corpus J+2",
|
||||
"corpus_ingere": 27,
|
||||
"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": "eco-anarchisme",
|
||||
"label": "Éco-anarchisme",
|
||||
"description": "Écologies libertaires et anti-industrielles. Contre l'État, le capitalisme et la domination de la nature — pour l'autogestion et le 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": "Technocritique",
|
||||
"description": "Critique radicale de la technique comme système autonome. Contre l'illusion de la technologie comme solution aux crises qu'elle engendre.",
|
||||
"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.",
|
||||
"color": "#6b8e6e",
|
||||
"x_hint": 0.62,
|
||||
"y_hint": 0.58
|
||||
}
|
||||
],
|
||||
"auteurs": [
|
||||
{
|
||||
"id": "murray-bookchin",
|
||||
"nom": "Murray Bookchin",
|
||||
"dates": "1921-2006",
|
||||
"ecoles": ["eco-anarchisme"],
|
||||
"ecole_principale": "eco-anarchisme",
|
||||
"livres_rag": [
|
||||
{ "slug": "bookchin-ecology-of-freedom", "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", "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-pain", "titre": "La Conquête du pain", "annee": 1892, "couches": ["fond", "structure"] }
|
||||
],
|
||||
"theses_cles": ["Entraide vs sélection naturelle darwiniste", "Fédéralisme anarchiste", "Géographie critique"],
|
||||
"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": "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-comment-saboter", "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", "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": "kohei-saito",
|
||||
"nom": "Kohei Saito",
|
||||
"dates": "1987-",
|
||||
"ecoles": ["ecosocialisme"],
|
||||
"ecole_principale": "ecosocialisme",
|
||||
"livres_rag": [
|
||||
{ "slug": "saito-marx-ecosocialisme", "titre": "Marx dans l'Anthropocène", "annee": 2020, "couches": ["fond", "structure"] }
|
||||
],
|
||||
"theses_cles": ["Marx et l'écologie", "Métabolisme social", "Décroissance communiste"],
|
||||
"bio_courte": "Philosophe japonais, auteur d'une relecture écologiste des cahiers tardifs de Marx. Défend une 'décroissance communiste' comme horizon."
|
||||
},
|
||||
{
|
||||
"id": "karl-marx",
|
||||
"nom": "Karl Marx",
|
||||
"dates": "1818-1883",
|
||||
"ecoles": ["ecosocialisme", "eco-anarchisme"],
|
||||
"ecole_principale": "ecosocialisme",
|
||||
"livres_rag": [
|
||||
{ "slug": "marx-manuscrits-1844", "titre": "Manuscrits de 1844", "annee": 1844, "couches": ["fond", "structure"] },
|
||||
{ "slug": "marx-capital", "titre": "Le Capital", "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"],
|
||||
"bio_courte": "Pensée-racine de l'écosocialisme. Les Grundrisse et le Capital contiennent une critique écologique du capitalisme souvent occultée."
|
||||
},
|
||||
{
|
||||
"id": "serge-latouche",
|
||||
"nom": "Serge Latouche",
|
||||
"dates": "1940-",
|
||||
"ecoles": ["decroissance"],
|
||||
"ecole_principale": "decroissance",
|
||||
"livres_rag": [
|
||||
{ "slug": "latouche-decroissance", "titre": "Le Pari de la décroissance", "annee": 2006, "couches": ["fond", "structure"] },
|
||||
{ "slug": "latouche-petit-traite", "titre": "Petit traité de la décroissance sereine", "annee": 2007, "couches": ["fond", "structure"] }
|
||||
],
|
||||
"theses_cles": ["Sereine décroissance", "Critique du développement", "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."
|
||||
},
|
||||
{
|
||||
"id": "pablo-servigne",
|
||||
"nom": "Pablo Servigne",
|
||||
"dates": "1978-",
|
||||
"ecoles": ["decroissance", "pensees-vivant"],
|
||||
"ecole_principale": "decroissance",
|
||||
"livres_rag": [
|
||||
{ "slug": "servigne-comment-tout", "titre": "Comment tout peut s'effondrer", "annee": 2015, "couches": ["fond", "structure"] }
|
||||
],
|
||||
"theses_cles": ["Collapsologie", "Entraide comme résilience", "Transition post-collapse"],
|
||||
"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."
|
||||
},
|
||||
{
|
||||
"id": "donella-meadows",
|
||||
"nom": "Dennis et Donella Meadows",
|
||||
"dates": "1941-2001 / 1942-",
|
||||
"ecoles": ["decroissance"],
|
||||
"ecole_principale": "decroissance",
|
||||
"livres_rag": [
|
||||
{ "slug": "meadows-limites-croissance", "titre": "Les Limites à la croissance", "annee": 1972, "couches": ["fond", "structure"] }
|
||||
],
|
||||
"theses_cles": ["Limites planétaires", "Modèles systémiques", "Overshoot"],
|
||||
"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": "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é)", "Patriarcat et destruction de la nature", "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."
|
||||
},
|
||||
{
|
||||
"id": "silvia-federici",
|
||||
"nom": "Silvia Federici",
|
||||
"dates": "1942-",
|
||||
"ecoles": ["ecofeminismes"],
|
||||
"ecole_principale": "ecofeminismes",
|
||||
"livres_rag": [
|
||||
{ "slug": "federici-caliban", "titre": "Caliban et la sorcière", "annee": 2004, "couches": ["fond", "structure"] }
|
||||
],
|
||||
"theses_cles": ["Accumulation primitive et corps des femmes", "Chasse aux sorcières", "Travail reproductif"],
|
||||
"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."
|
||||
},
|
||||
{
|
||||
"id": "vandana-shiva",
|
||||
"nom": "Vandana Shiva",
|
||||
"dates": "1952-",
|
||||
"ecoles": ["ecofeminismes", "ecologies-decoloniales"],
|
||||
"ecole_principale": "ecofeminismes",
|
||||
"livres_rag": [
|
||||
{ "slug": "shiva-monocultures-esprit", "titre": "Monocultures of the Mind", "annee": 1993, "couches": ["fond", "structure"] }
|
||||
],
|
||||
"theses_cles": ["Biopiraterie", "Souveraineté alimentaire", "Écoféminisme tiers-mondiste"],
|
||||
"bio_courte": "Physicienne et militante indienne, figure mondiale de l'écoféminisme et de la souveraineté alimentaire. Cofondatrice de Navdanya."
|
||||
},
|
||||
{
|
||||
"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"] }
|
||||
],
|
||||
"theses_cles": ["Double fracture coloniale et écologique", "Habiter le monde", "Antillanité et écologie"],
|
||||
"bio_courte": "Ingénieur et philosophe martiniquais. Son oeuvre articule colonialisme et destruction de l'environnement autour de la 'double fracture' historique."
|
||||
},
|
||||
{
|
||||
"id": "jacques-ellul",
|
||||
"nom": "Jacques Ellul",
|
||||
"dates": "1912-1994",
|
||||
"ecoles": ["technocritique"],
|
||||
"ecole_principale": "technocritique",
|
||||
"livres_rag": [
|
||||
{ "slug": "ellul-technique-enjeu", "titre": "La Technique ou l'Enjeu du siècle", "annee": 1954, "couches": ["fond", "structure"] }
|
||||
],
|
||||
"theses_cles": ["Technique comme système autonome", "Efficacité comme valeur unique", "Propagande et technosystème"],
|
||||
"bio_courte": "Juriste, sociologue et théologien bordelais. Son oeuvre fondatrice analyse la Technique comme système autonome qui échappe à tout contrôle humain."
|
||||
},
|
||||
{
|
||||
"id": "david-graeber",
|
||||
"nom": "David Graeber",
|
||||
"dates": "1961-2020",
|
||||
"ecoles": ["eco-anarchisme"],
|
||||
"ecole_principale": "eco-anarchisme",
|
||||
"livres_rag": [
|
||||
{ "slug": "graeber-dette", "titre": "Dette : 5000 ans d'histoire", "annee": 2011, "couches": ["fond", "structure"] }
|
||||
],
|
||||
"theses_cles": ["Dette comme instrument de domination", "Anthropologie anarchiste", "Bullshit jobs"],
|
||||
"bio_courte": "Anthropologue américain, figure du mouvement Occupy. Ses travaux anthropologiques déconstruisent les mythes fondateurs du capitalisme (troc, dette, marché)."
|
||||
},
|
||||
{
|
||||
"id": "philippe-descola",
|
||||
"nom": "Philippe Descola",
|
||||
"dates": "1949-",
|
||||
"ecoles": ["pensees-vivant"],
|
||||
"ecole_principale": "pensees-vivant",
|
||||
"livres_rag": [
|
||||
{ "slug": "descola-par-dela-nature", "titre": "Par-delà nature et culture", "annee": 2005, "couches": ["fond", "structure"] }
|
||||
],
|
||||
"theses_cles": ["Dualisme nature/culture comme exception occidentale", "4 ontologies (animisme, totémisme, analogisme, naturalisme)", "Cosmopolitiques"],
|
||||
"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."
|
||||
},
|
||||
{
|
||||
"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"] }
|
||||
],
|
||||
"theses_cles": ["Impact des pesticides sur les écosystèmes", "Naissance du mouvement environnementaliste moderne", "Responsabilité scientifique"],
|
||||
"bio_courte": "Marine biologist and author américaine. Son livre Printemps silencieux (1962) a lancé le mouvement environnementaliste moderne en dénonçant les pesticides."
|
||||
},
|
||||
{
|
||||
"id": "arne-naess",
|
||||
"nom": "Arne Næss",
|
||||
"dates": "1912-2009",
|
||||
"ecoles": ["ethiques-environnementales"],
|
||||
"ecole_principale": "ethiques-environnementales",
|
||||
"livres_rag": [
|
||||
{ "slug": "naess-ecologie-profonde", "titre": "Écologie, communauté et style de vie", "annee": 1989, "couches": ["fond", "structure"] }
|
||||
],
|
||||
"theses_cles": ["Deep ecology vs écologie superficielle", "Égalité biosphérique", "Réalisation de Soi élargie"],
|
||||
"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."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
{
|
||||
"tree": {
|
||||
"name": "FMHY — Sélection AEP",
|
||||
"children": [
|
||||
{
|
||||
"name": "IA & LLM",
|
||||
"children": [
|
||||
{ "name": "DeepSeek", "url": "https://chat.deepseek.com/", "desc": "DeepSeek V3.2 — modèle libre puissant, sans limite, compte requis" },
|
||||
{ "name": "Mistral Chat", "url": "https://chat.mistral.ai/", "desc": "Mistral Large 3, natif français, sans limite" },
|
||||
{ "name": "Google AI Studio", "url": "https://aistudio.google.com/app/prompts/new_chat", "desc": "Gemini 3.1 Pro + API gratuite, idéal pour expérimenter" },
|
||||
{ "name": "Kimi", "url": "https://www.kimi.com/", "desc": "Kimi K2.6 Thinking — raisonnement avancé, login requis" },
|
||||
{ "name": "NVIDIA NIM", "url": "https://build.nvidia.com/models", "desc": "Multi-modèles libres (Kimi, Qwen, GLM) sans compte" },
|
||||
{ "name": "NotebookLM", "url": "https://notebooklm.google.com/", "desc": "Chatbot sur tes propres documents, prise de notes augmentée" },
|
||||
{ "name": "Ollama", "url": "https://ollama.com/", "desc": "Héberger des modèles IA localement, toutes plateformes" },
|
||||
{ "name": "Open WebUI", "url": "https://openwebui.com/", "desc": "Interface web pour LLMs locaux (Ollama), self-hostable" },
|
||||
{ "name": "LM Studio", "url": "https://lmstudio.ai/", "desc": "Appli desktop pour modèles IA locaux, sans cloud" },
|
||||
{ "name": "Perplexity", "url": "https://www.perplexity.ai/", "desc": "Moteur de recherche IA avec sources citées" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Recherche & articles",
|
||||
"children": [
|
||||
{ "name": "SciSpace", "url": "https://scispace.com/", "desc": "Chatbot sur publications scientifiques, explications simplifiées" },
|
||||
{ "name": "Alphaxiv", "url": "https://www.alphaxiv.org/", "desc": "Chatbot sur les papiers de recherche arxiv" },
|
||||
{ "name": "Unpaywall", "url": "https://unpaywall.org/", "desc": "Accès libre aux articles académiques — extension navigateur" },
|
||||
{ "name": "Bypass Paywalls Clean", "url": "https://gitflic.ru/project/magnolia1234/bpc_uploads", "desc": "Extension pour bypasser les paywalls d'articles de presse" },
|
||||
{ "name": "Freedium", "url": "https://freedium-mirror.cfd/", "desc": "Accéder aux articles Medium sans abonnement" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Livres & archives",
|
||||
"children": [
|
||||
{ "name": "Anna's Archive", "url": "https://annas-archive.gl/", "desc": "Meta-search des bibliothèques libres (Z-Lib, LibGen, Sci-Hub)" },
|
||||
{ "name": "Z-Library", "url": "https://z-lib.gd/", "desc": "Bibliothèque de livres/articles, téléchargement direct" },
|
||||
{ "name": "Library Genesis", "url": "https://libgen.li/", "desc": "Livres techniques, universitaires, comics en accès libre" },
|
||||
{ "name": "Internet Archive", "url": "https://archive.org/details/texts", "desc": "Livres, magazines, archives historiques numérisés" },
|
||||
{ "name": "Project Gutenberg", "url": "https://www.gutenberg.org/", "desc": "Classiques du domaine public, 70k+ livres" },
|
||||
{ "name": "Open Library", "url": "https://openlibrary.org/", "desc": "Bibliothèque libre, emprunt numérique de livres" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Cours & formation",
|
||||
"children": [
|
||||
{ "name": "MIT OpenCourseWare", "url": "https://ocw.mit.edu/", "desc": "Cours MIT en accès libre — toutes disciplines, niveau universitaire" },
|
||||
{ "name": "Khan Academy", "url": "https://www.khanacademy.org/", "desc": "Cours gratuits et interactifs, toutes matières, du lycée au supérieur" },
|
||||
{ "name": "edX", "url": "https://www.edx.org/", "desc": "MOOCs certifiants des meilleures universités mondiales" },
|
||||
{ "name": "Learn Anything", "url": "https://learn-anything.xyz/", "desc": "Moteur de recherche de parcours d'apprentissage structurés" },
|
||||
{ "name": "OpenCulture", "url": "https://www.openculture.com/", "desc": "Cours, films, livres audio en accès libre, liens curatés" },
|
||||
{ "name": "Appropedia", "url": "https://www.appropedia.org/", "desc": "Wiki de la durabilité, technologies appropriées, habitat écologique" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Outils dev & infra",
|
||||
"children": [
|
||||
{ "name": "Free for Developers", "url": "https://free-for.dev/", "desc": "Index exhaustif des outils gratuits pour développeurs" },
|
||||
{ "name": "DevDocs", "url": "https://devdocs.io/", "desc": "Documentation développeur unifiée, consultable hors ligne" },
|
||||
{ "name": "N8N", "url": "https://n8n.io/", "desc": "Automatisation de workflows open source, self-hostable" },
|
||||
{ "name": "NocoDB", "url": "https://github.com/nocodb/nocodb", "desc": "Base de données no-code open source, alternative Airtable" },
|
||||
{ "name": "Crontab Guru", "url": "https://crontab.guru/", "desc": "Éditeur de tâches cron avec explication lisible" },
|
||||
{ "name": "Dokploy", "url": "https://github.com/dokploy/dokploy", "desc": "Déploiement d'apps auto-hébergé, alternative Coolify/Heroku" },
|
||||
{ "name": "Open Source Guides", "url": "https://opensource.guide/", "desc": "Guides pour contribuer et gérer des projets open source" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Texte & documents",
|
||||
"children": [
|
||||
{ "name": "DeepL", "url": "https://www.deepl.com/translator", "desc": "Traduction IA, meilleure qualité disponible en FR/EN/DE" },
|
||||
{ "name": "Paperless-ngx", "url": "https://docs.paperless-ngx.com/", "desc": "Gestion documentaire self-hosted avec OCR et indexation" },
|
||||
{ "name": "Whisper (OpenAI)", "url": "https://github.com/openai/whisper", "desc": "Transcription audio open source, multi-langues, qualité professionnelle" },
|
||||
{ "name": "GitHub Gists", "url": "https://gist.github.com/", "desc": "Partage de snippets code / notes, versionné, dans l'écosystème git" },
|
||||
{ "name": "PrivateBin", "url": "https://privatebin.net/", "desc": "Pastebin chiffré zero-knowledge, self-hostable" },
|
||||
{ "name": "LibreTranslate", "url": "https://libretranslate.com/", "desc": "Traducteur FOSS, self-hostable, sans tracking" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Vie privée & sécurité",
|
||||
"children": [
|
||||
{ "name": "uBlock Origin", "url": "https://github.com/gorhill/uBlock", "desc": "Bloqueur de publicités et trackers — le plus efficace du marché" },
|
||||
{ "name": "Bitwarden", "url": "https://bitwarden.com/", "desc": "Gestionnaire de mots de passe open source, cloud ou self-hosted" },
|
||||
{ "name": "KeePassXC", "url": "https://keepassxc.org/", "desc": "Gestionnaire de mots de passe local, base chiffrée, sans cloud" },
|
||||
{ "name": "Pi-Hole", "url": "https://pi-hole.net/", "desc": "Bloqueur de pub DNS pour tout le réseau domestique, self-hosted" },
|
||||
{ "name": "SponsorBlock", "url": "https://sponsor.ajay.app/", "desc": "Extension pour skipper les segments sponsorisés sur YouTube" },
|
||||
{ "name": "AdGuard Home", "url": "https://adguard.com/en/adguard-home/overview.html", "desc": "Alternative Pi-Hole, DNS adblocking self-hosted pour tout le réseau" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Documentaires & médias",
|
||||
"children": [
|
||||
{ "name": "Films For Action", "url": "https://www.filmsforaction.org/", "desc": "Films documentaires engagés, alternatives sociales et écologiques" },
|
||||
{ "name": "ARTE (replay)", "url": "https://www.arte.tv/", "desc": "Documentaires et programmes culturels FR/DE, replay gratuit" },
|
||||
{ "name": "Documentary+", "url": "https://www.docplus.com/", "desc": "Plateforme de documentaires en streaming gratuit" },
|
||||
{ "name": "TED Talks", "url": "https://www.ted.com/", "desc": "Conférences inspirantes sur science, société, design, architecture" }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"source": "fmhy.net (sélection AEP 2026-05-22)",
|
||||
"total_entries": 50
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
{
|
||||
"projets": [
|
||||
{
|
||||
"id": "quartier-2030",
|
||||
"titre": "Votre quartier en 2030",
|
||||
"auteurs": ["Inconnu"],
|
||||
"annee": "2020",
|
||||
"ecole": "Inconnu",
|
||||
"url": "https://quartier-2030.firebaseapp.com/",
|
||||
"description": "Exploration prospective confrontant smart city, no future, résilience et deep ecology à l'échelle du quartier. Le travail donne à voir plusieurs futurs urbains contrastés, de l'utopie technologique au retrait radical, en laissant le visiteur naviguer entre les scénarios. Un travail d'orfèvre pour sortir de la pensée linéaire sur la ville.",
|
||||
"thumb": null,
|
||||
"link_status": "ok"
|
||||
},
|
||||
{
|
||||
"id": "seine-nature",
|
||||
"titre": "Seine — nature urbaine",
|
||||
"auteurs": ["Inconnu"],
|
||||
"annee": "2019",
|
||||
"ecole": "Inconnu",
|
||||
"url": "http://www.seine.natureurbaine.com/00_index/page_theme/theme.html",
|
||||
"description": "Projet de transformation territoriale collective autour de la Seine, pensé comme une démarche systémique et pluridisciplinaire. L'intervention se concentre sur les marges périurbaines, traitées par une logique d'acupuncture : des micro-interventions précises pour enclencher des dynamiques plus larges. L'approche refuse le grand projet unique au profit d'un réseau de petites transformations.",
|
||||
"thumb": null,
|
||||
"link_status": "ok"
|
||||
},
|
||||
{
|
||||
"id": "tmip",
|
||||
"titre": "TMIP — Transformation de la Maison Individuelle Périurbaine",
|
||||
"auteurs": ["Jules Nény"],
|
||||
"annee": "2019",
|
||||
"ecole": "ENSA Paris-Belleville",
|
||||
"url": "https://issuu.com/transformationresilientes/docs/tmip_archijeunes_cstb_",
|
||||
"description": "Étude de la maison périurbaine sous l'angle des Gilets jaunes : comment ce lieu de vie concentre les tensions entre émancipation individuelle et dépendance structurelle (voiture, énergie, services). Le projet propose un réseau de micro-infrastructures partagées pour transformer ces maisons isolées en systèmes résilients interconnectés. Publié avec ARCHI'JEUNES et le CSTB.",
|
||||
"thumb": null,
|
||||
"link_status": "ok"
|
||||
},
|
||||
{
|
||||
"id": "filiere-bois",
|
||||
"titre": "Enquête sur les paysages forestiers franciliens",
|
||||
"auteurs": ["Quid Architecture"],
|
||||
"annee": "2021",
|
||||
"ecole": "Inconnu",
|
||||
"url": "https://www.faireparis.com/fr/projets/faire-2021/enquete-sur-les-paysages-forestiers-franciliens-2159.html",
|
||||
"description": "Projet lauréat FAIRE 2021. Enquête sur les dysfonctionnements de la filière bois en Île-de-France, aux interfaces entre sylviculteurs, scieries, artisans et maîtres d'ouvrage. Le travail cartographie les ruptures de filière et propose des interventions concrètes pour réparer les liens entre forêt et construction. Une démarche systémique rare dans les études architecturales.",
|
||||
"thumb": null,
|
||||
"link_status": "ok"
|
||||
},
|
||||
{
|
||||
"id": "jeu-champagne",
|
||||
"titre": "Jeu de rôle Champagne PFE — Plateau",
|
||||
"auteurs": ["Inconnu"],
|
||||
"annee": "2020",
|
||||
"ecole": "Inconnu",
|
||||
"url": "https://campfe2020.wixsite.com/champagnepfe/plateau",
|
||||
"description": "Dispositif ludique et coopératif développé comme outil de médiation entre acteurs d'un territoire. Le jeu de rôle permet de traverser des problèmes complexes en engageant simultanément des parties prenantes aux intérêts divergents. Une exploration de l'architecture comme processus collectif plutôt que comme objet produit.",
|
||||
"thumb": null,
|
||||
"link_status": "ok"
|
||||
},
|
||||
{
|
||||
"id": "transition-agricole",
|
||||
"titre": "Transition agricole — réinvestissement de fermes traditionnelles",
|
||||
"auteurs": ["Inconnu"],
|
||||
"annee": "2020",
|
||||
"ecole": "Inconnu",
|
||||
"url": "https://www.calameo.com/books/007306483e0b23edb1db7",
|
||||
"description": "Projet sur la transformation de fermes traditionnelles dans une logique agricole moderne et diversifiée. L'étude explore comment l'architecture peut accompagner les transitions d'usage des bâtiments ruraux, en articulant patrimonial et fonctionnel. Voir aussi le projet complémentaire sur la Seine aval : https://www.calameo.com/books/007063623f4d4b800b01d",
|
||||
"thumb": null,
|
||||
"link_status": "ok"
|
||||
}
|
||||
]
|
||||
}
|
||||
85
server/api/chatbot-pensees.post.ts
Normal file
85
server/api/chatbot-pensees.post.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import type { H3Event } from 'h3'
|
||||
import { checkRateLimitJson } from '~/server/utils/rateLimitJson'
|
||||
|
||||
interface ChatbotPenseesRequest {
|
||||
query: string
|
||||
mode?: 'hybrid' | 'local' | 'global' | 'naive' | 'mix'
|
||||
filter_couche?: 'fond' | 'forme' | 'structure' | null
|
||||
filter_ecole?: string | null
|
||||
history?: Array<{ role: 'user' | 'assistant'; content: string }>
|
||||
}
|
||||
|
||||
interface LightRAGQueryResponse {
|
||||
response: string
|
||||
}
|
||||
|
||||
const SYSTEM_PREFACE = `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.`
|
||||
|
||||
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 ragUrl = (config.ragPeUrl as string) || 'http://localhost:9621'
|
||||
|
||||
// 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 = `${SYSTEM_PREFACE}\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,
|
||||
filter: { couche: body.filter_couche ?? null, ecole: body.filter_ecole ?? null },
|
||||
timestamp: new Date().toISOString(),
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user