Compare commits
3 Commits
a1c47002d5
...
b36587cb08
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b36587cb08 | ||
|
|
89608d894c | ||
|
|
fdd9d02859 |
@@ -14,7 +14,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
interface EcoleData { id: string; label: string; description: string; color: string; x_hint: number; y_hint: number }
|
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 LivreRag { slug: string; titre: string; annee: number; couches: string[] }
|
||||||
interface AuteurData { id: string; nom: string; dates: string; ecoles: string[]; ecole_principale: string; livres_rag: LivreRag[]; theses_cles: string[]; bio_courte: string }
|
interface AuteurData { id: string; nom: string; dates: string; ecoles: string[]; ecole_principale: string; ingere: boolean; livres_rag: LivreRag[]; theses_cles: string[]; bio_courte: string; bio_courte_provisoire?: string }
|
||||||
interface PenseesData { ecoles: EcoleData[]; auteurs: AuteurData[] }
|
interface PenseesData { ecoles: EcoleData[]; auteurs: AuteurData[] }
|
||||||
|
|
||||||
// Liens d'influence inter-ecoles (Phase 7 - matrice de filiation)
|
// Liens d'influence inter-ecoles (Phase 7 - matrice de filiation)
|
||||||
@@ -79,8 +79,11 @@ async function initGraph() {
|
|||||||
const delaunay = Delaunay.from(points)
|
const delaunay = Delaunay.from(points)
|
||||||
const voronoi = delaunay.voronoi([0, 0, W, H])
|
const voronoi = delaunay.voronoi([0, 0, W, H])
|
||||||
|
|
||||||
// Groupe Voronoi (fond, couche 1)
|
// Groupe Voronoi : separation Phase 8.D
|
||||||
|
// - gVoronoi : cells colorees, BLURRED via CSS .voronoi-bg
|
||||||
|
// - gVoronoiLabels : labels ecoles, NOT blurred (lisibilite 17px)
|
||||||
const gVoronoi = g.append('g').attr('class', 'voronoi-bg')
|
const gVoronoi = g.append('g').attr('class', 'voronoi-bg')
|
||||||
|
const gVoronoiLabels = g.append('g').attr('class', 'voronoi-labels')
|
||||||
|
|
||||||
ecolesArr.forEach((ecole, i) => {
|
ecolesArr.forEach((ecole, i) => {
|
||||||
const cellPath = voronoi.renderCell(i)
|
const cellPath = voronoi.renderCell(i)
|
||||||
@@ -105,12 +108,12 @@ async function initGraph() {
|
|||||||
})
|
})
|
||||||
.on('mouseleave', () => { if (tooltipRef.value) tooltipRef.value.style.opacity = '0' })
|
.on('mouseleave', () => { if (tooltipRef.value) tooltipRef.value.style.opacity = '0' })
|
||||||
|
|
||||||
// Label ecole dans la cellule (centroid du polygone)
|
// Label ecole dans la cellule (centroid du polygone) - calque non-blurre
|
||||||
if (poly && poly.length > 0) {
|
if (poly && poly.length > 0) {
|
||||||
const centroid = d3.polygonCentroid(poly as [number, number][])
|
const centroid = d3.polygonCentroid(poly as [number, number][])
|
||||||
if (centroid && !isNaN(centroid[0]) && !isNaN(centroid[1])) {
|
if (centroid && !isNaN(centroid[0]) && !isNaN(centroid[1])) {
|
||||||
const words = ecole.label.split(' ')
|
const words = ecole.label.split(' ')
|
||||||
const labelEl = gVoronoi.append('text')
|
const labelEl = gVoronoiLabels.append('text')
|
||||||
.attr('class', 'voronoi-cell-label')
|
.attr('class', 'voronoi-cell-label')
|
||||||
.attr('x', centroid[0])
|
.attr('x', centroid[0])
|
||||||
.attr('y', centroid[1])
|
.attr('y', centroid[1])
|
||||||
@@ -171,7 +174,10 @@ async function initGraph() {
|
|||||||
const ecole = ecoleMap.get(a.ecole_principale)
|
const ecole = ecoleMap.get(a.ecole_principale)
|
||||||
const jitter = () => (Math.random() - 0.5) * 80
|
const jitter = () => (Math.random() - 0.5) * 80
|
||||||
return {
|
return {
|
||||||
id: a.id, type: 'auteur', nom: a.nom, dates: a.dates, bio_courte: a.bio_courte,
|
id: a.id, type: 'auteur', nom: a.nom, dates: a.dates,
|
||||||
|
bio_courte: a.bio_courte,
|
||||||
|
bio_provisoire: a.bio_courte_provisoire ?? '',
|
||||||
|
ingere: a.ingere,
|
||||||
ecole_principale: a.ecole_principale,
|
ecole_principale: a.ecole_principale,
|
||||||
color: ecole?.color ?? '#888', r: 11,
|
color: ecole?.color ?? '#888', r: 11,
|
||||||
x: W * (ecole?.x_hint ?? 0.5) + jitter(),
|
x: W * (ecole?.x_hint ?? 0.5) + jitter(),
|
||||||
@@ -198,25 +204,26 @@ async function initGraph() {
|
|||||||
const allNodes = [...ecoleFixedNodes, ...auteurNodes]
|
const allNodes = [...ecoleFixedNodes, ...auteurNodes]
|
||||||
|
|
||||||
if (simulation) simulation.stop()
|
if (simulation) simulation.stop()
|
||||||
|
// Phase 8.D : sim ajustee pour 171 auteurs (vs 28 v2.1, densite 6x)
|
||||||
simulation = d3.forceSimulation(allNodes)
|
simulation = d3.forceSimulation(allNodes)
|
||||||
.force('link', d3.forceLink(links).id((d: any) => d.id).distance(110).strength((d: any) => d.strength ?? 0.5))
|
.force('link', d3.forceLink(links).id((d: any) => d.id).distance(85).strength((d: any) => d.strength ?? 0.5))
|
||||||
.force('charge', d3.forceManyBody().strength(-45))
|
.force('charge', d3.forceManyBody().strength(-30))
|
||||||
.force('center', d3.forceCenter(W / 2, H / 2).strength(0.02))
|
.force('center', d3.forceCenter(W / 2, H / 2).strength(0.02))
|
||||||
.force('collision', d3.forceCollide().radius((d: any) => d.type === 'auteur' ? 14 : 0))
|
.force('collision', d3.forceCollide().radius((d: any) => d.type === 'auteur' ? 12 : 0))
|
||||||
.force('forceX', d3.forceX<any>((d: any) => {
|
.force('forceX', d3.forceX<any>((d: any) => {
|
||||||
if (d.type === 'auteur') {
|
if (d.type === 'auteur') {
|
||||||
const pos = ecolePositions.get(d.ecole_principale)
|
const pos = ecolePositions.get(d.ecole_principale)
|
||||||
return pos ? pos.tx : W / 2
|
return pos ? pos.tx : W / 2
|
||||||
}
|
}
|
||||||
return W / 2
|
return W / 2
|
||||||
}).strength(0.12))
|
}).strength(0.15))
|
||||||
.force('forceY', d3.forceY<any>((d: any) => {
|
.force('forceY', d3.forceY<any>((d: any) => {
|
||||||
if (d.type === 'auteur') {
|
if (d.type === 'auteur') {
|
||||||
const pos = ecolePositions.get(d.ecole_principale)
|
const pos = ecolePositions.get(d.ecole_principale)
|
||||||
return pos ? pos.ty : H / 2
|
return pos ? pos.ty : H / 2
|
||||||
}
|
}
|
||||||
return H / 2
|
return H / 2
|
||||||
}).strength(0.12))
|
}).strength(0.15))
|
||||||
|
|
||||||
// ---- LIENS APPARTENANCE (couche 4) ----
|
// ---- LIENS APPARTENANCE (couche 4) ----
|
||||||
const gLinks = g.append('g').attr('class', 'links-appartenance')
|
const gLinks = g.append('g').attr('class', 'links-appartenance')
|
||||||
@@ -234,18 +241,24 @@ async function initGraph() {
|
|||||||
// ---- NODES AUTEURS (couche 5) ----
|
// ---- NODES AUTEURS (couche 5) ----
|
||||||
const gAuteurs = g.append('g').attr('class', 'auteurs')
|
const gAuteurs = g.append('g').attr('class', 'auteurs')
|
||||||
d3NodeSel = gAuteurs.selectAll('g').data(auteurNodes).join('g')
|
d3NodeSel = gAuteurs.selectAll('g').data(auteurNodes).join('g')
|
||||||
.style('cursor', 'pointer')
|
.style('cursor', (d: any) => d.ingere ? 'pointer' : 'default')
|
||||||
.call(d3.drag<any, any>()
|
.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('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('drag', (e: any, d: any) => { d.fx = e.x; d.fy = e.y })
|
||||||
.on('end', (e: any, d: any) => { if (!e.active) simulation.alphaTarget(0); d.fx = null; d.fy = null }))
|
.on('end', (e: any, d: any) => { if (!e.active) simulation.alphaTarget(0); d.fx = null; d.fy = null }))
|
||||||
.on('click', (e: any, d: any) => { e.stopPropagation(); emit('select-auteur', d.id) })
|
.on('click', (e: any, d: any) => {
|
||||||
|
if (!d.ingere) return
|
||||||
|
e.stopPropagation()
|
||||||
|
emit('select-auteur', d.id)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Phase 8.D : grisage conditionnel auteurs non-ingeres (ingere:false)
|
||||||
d3NodeSel.append('circle')
|
d3NodeSel.append('circle')
|
||||||
.attr('r', (d: any) => d.r)
|
.attr('r', (d: any) => d.r)
|
||||||
.attr('fill', (d: any) => d.color + 'cc')
|
.attr('fill', (d: any) => d.ingere ? (d.color + 'cc') : '#bbbbbb')
|
||||||
.attr('stroke', (d: any) => d.color)
|
.attr('stroke', (d: any) => d.ingere ? d.color : '#999999')
|
||||||
.attr('stroke-width', 1.5)
|
.attr('stroke-width', 1.5)
|
||||||
|
.attr('opacity', (d: any) => d.ingere ? 1 : 0.35)
|
||||||
|
|
||||||
// ---- LABELS AUTEURS (couche 6 - fix 7.1 : drop-shadow blanc) ----
|
// ---- LABELS AUTEURS (couche 6 - fix 7.1 : drop-shadow blanc) ----
|
||||||
d3NodeSel.append('text')
|
d3NodeSel.append('text')
|
||||||
@@ -258,8 +271,14 @@ async function initGraph() {
|
|||||||
d3NodeSel
|
d3NodeSel
|
||||||
.on('mouseenter', (e: any, d: any) => {
|
.on('mouseenter', (e: any, d: any) => {
|
||||||
if (!tooltipRef.value) return
|
if (!tooltipRef.value) return
|
||||||
|
let tooltipHtml = ''
|
||||||
|
if (d.ingere) {
|
||||||
const bio = d.bio_courte.length > 90 ? d.bio_courte.slice(0, 87) + '...' : d.bio_courte
|
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>`
|
tooltipHtml = `<strong>${d.nom}</strong> <span style="opacity:0.6;font-size:0.7rem;">${d.dates}</span><br><span style="opacity:0.75;font-size:0.72rem;">${bio}</span>`
|
||||||
|
} else {
|
||||||
|
tooltipHtml = `<strong>${d.nom}</strong> <span style="opacity:0.6;font-size:0.7rem;">${d.dates}</span><br><span style="opacity:0.65;font-size:0.72rem;font-style:italic;">Présent dans Bonpote, pas encore ingéré dans le RAG ATIS.</span>`
|
||||||
|
}
|
||||||
|
tooltipRef.value.innerHTML = tooltipHtml
|
||||||
tooltipRef.value.style.opacity = '1'
|
tooltipRef.value.style.opacity = '1'
|
||||||
})
|
})
|
||||||
.on('mousemove', (e: any) => {
|
.on('mousemove', (e: any) => {
|
||||||
@@ -325,14 +344,23 @@ defineExpose({ triggerResize })
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---- Voronoi cellules ---- */
|
/* ---- Voronoi cellules (Phase 8.D : blur 10px aquarelle Bonpote) ---- */
|
||||||
|
.voronoi-bg {
|
||||||
|
filter: blur(10px);
|
||||||
|
opacity: 0.65;
|
||||||
|
}
|
||||||
|
|
||||||
.voronoi-cell {
|
.voronoi-cell {
|
||||||
stroke: rgba(255,255,255,0.4);
|
stroke: rgba(255, 255, 255, 0.3);
|
||||||
stroke-width: 1.5px;
|
stroke-width: 1px;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---- Labels ecoles dans cellules Voronoi ---- */
|
/* ---- Labels ecoles : calque separe NON-blurre (Phase 8.D) ---- */
|
||||||
|
.voronoi-labels {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
.voronoi-cell-label {
|
.voronoi-cell-label {
|
||||||
fill: rgba(40,40,40,0.52);
|
fill: rgba(40,40,40,0.52);
|
||||||
font-size: 17px;
|
font-size: 17px;
|
||||||
|
|||||||
@@ -76,11 +76,24 @@
|
|||||||
|
|
||||||
<!-- Input overlay -->
|
<!-- Input overlay -->
|
||||||
<div class="shrink-0 px-3 py-3" style="border-top:1px solid var(--nav-bg-alt);">
|
<div class="shrink-0 px-3 py-3" style="border-top:1px solid var(--nav-bg-alt);">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2" style="position:relative;">
|
||||||
<input ref="inputElOverlay" v-model="q" type="text" placeholder="Ta question..." maxlength="500"
|
<!-- Hashtag autocomplete dropdown (Slack/Discord pattern, au-dessus de l'input) -->
|
||||||
|
<div v-if="hashtagDropdownOpen && hashtagSuggestions.length"
|
||||||
|
class="hashtag-dropdown"
|
||||||
|
style="position:absolute;bottom:100%;left:0;right:0;margin-bottom:6px;max-height:220px;overflow-y:auto;background:var(--nav-surface);border:1px solid var(--nav-bg-alt);border-radius:8px;box-shadow:0 -4px 12px rgba(0,0,0,0.12);z-index:50;">
|
||||||
|
<div v-for="(auteur, idx) in hashtagSuggestions" :key="auteur.id"
|
||||||
|
@mousedown.prevent="applyHashtagSuggestion(auteur)"
|
||||||
|
@mouseenter="hashtagSelectedIndex = idx"
|
||||||
|
class="px-3 py-2 cursor-pointer text-sm"
|
||||||
|
:style="idx === hashtagSelectedIndex ? 'background:var(--nav-primary);color:var(--nav-text-on-primary);' : 'color:var(--nav-text);'">
|
||||||
|
<span style="font-weight:600;">#{{ auteur.id }}</span>
|
||||||
|
<span :style="idx === hashtagSelectedIndex ? 'opacity:0.85;margin-left:8px;font-size:0.78rem;' : 'opacity:0.65;margin-left:8px;font-size:0.78rem;color:var(--nav-text-muted);'">{{ auteur.nom }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<input ref="inputElOverlay" v-model="q" type="text" placeholder="Ta question, tape #auteur pour cibler" maxlength="500"
|
||||||
class="flex-1 px-3 py-2 rounded-lg text-sm outline-none"
|
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);"
|
style="background:var(--nav-bg);color:var(--nav-text);border:1px solid var(--nav-bg-alt);"
|
||||||
@keydown.enter.prevent="send" />
|
@keydown="onInputKeydown" />
|
||||||
<button @click="send" :disabled="loading || !q.trim()"
|
<button @click="send" :disabled="loading || !q.trim()"
|
||||||
class="flex items-center justify-center w-9 h-9 rounded-lg"
|
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;'"
|
:style="loading||!q.trim() ? 'background:var(--nav-bg-alt);opacity:0.5;cursor:not-allowed;' : 'background:var(--nav-primary);cursor:pointer;'"
|
||||||
@@ -156,11 +169,24 @@
|
|||||||
|
|
||||||
<!-- Input inline -->
|
<!-- Input inline -->
|
||||||
<div class="shrink-0 px-3 py-3" style="border-top:1px solid var(--nav-bg-alt);">
|
<div class="shrink-0 px-3 py-3" style="border-top:1px solid var(--nav-bg-alt);">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2" style="position:relative;">
|
||||||
<input ref="inputElInline" v-model="q" type="text" placeholder="Ta question..." maxlength="500"
|
<!-- Hashtag autocomplete dropdown (Slack/Discord pattern, au-dessus de l'input) -->
|
||||||
|
<div v-if="hashtagDropdownOpen && hashtagSuggestions.length"
|
||||||
|
class="hashtag-dropdown"
|
||||||
|
style="position:absolute;bottom:100%;left:0;right:0;margin-bottom:6px;max-height:220px;overflow-y:auto;background:var(--nav-surface);border:1px solid var(--nav-bg-alt);border-radius:8px;box-shadow:0 -4px 12px rgba(0,0,0,0.12);z-index:50;">
|
||||||
|
<div v-for="(auteur, idx) in hashtagSuggestions" :key="auteur.id"
|
||||||
|
@mousedown.prevent="applyHashtagSuggestion(auteur)"
|
||||||
|
@mouseenter="hashtagSelectedIndex = idx"
|
||||||
|
class="px-3 py-2 cursor-pointer text-sm"
|
||||||
|
:style="idx === hashtagSelectedIndex ? 'background:var(--nav-primary);color:var(--nav-text-on-primary);' : 'color:var(--nav-text);'">
|
||||||
|
<span style="font-weight:600;">#{{ auteur.id }}</span>
|
||||||
|
<span :style="idx === hashtagSelectedIndex ? 'opacity:0.85;margin-left:8px;font-size:0.78rem;' : 'opacity:0.65;margin-left:8px;font-size:0.78rem;color:var(--nav-text-muted);'">{{ auteur.nom }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<input ref="inputElInline" v-model="q" type="text" placeholder="Ta question, tape #auteur pour cibler" maxlength="500"
|
||||||
class="flex-1 px-3 py-2 rounded-lg text-sm outline-none"
|
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);"
|
style="background:var(--nav-bg);color:var(--nav-text);border:1px solid var(--nav-bg-alt);"
|
||||||
@keydown.enter.prevent="send" />
|
@keydown="onInputKeydown" />
|
||||||
<button @click="send" :disabled="loading || !q.trim()"
|
<button @click="send" :disabled="loading || !q.trim()"
|
||||||
class="flex items-center justify-center w-9 h-9 rounded-lg"
|
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;'"
|
:style="loading||!q.trim() ? 'background:var(--nav-bg-alt);opacity:0.5;cursor:not-allowed;' : 'background:var(--nav-primary);cursor:pointer;'"
|
||||||
@@ -177,6 +203,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
interface Message { role: 'user' | 'assistant'; content: string }
|
interface Message { role: 'user' | 'assistant'; content: string }
|
||||||
|
interface AuteurMini { id: string; nom: string }
|
||||||
|
|
||||||
type CorpusMode = 'pensees' | 'projets' | 'both'
|
type CorpusMode = 'pensees' | 'projets' | 'both'
|
||||||
|
|
||||||
@@ -213,11 +240,109 @@ const corpusCount = 18
|
|||||||
|
|
||||||
const corpus = ref<CorpusMode>('both')
|
const corpus = ref<CorpusMode>('both')
|
||||||
|
|
||||||
onMounted(() => {
|
// Phase 8.E : hashtag mentions
|
||||||
|
const auteursIngeres = ref<AuteurMini[]>([])
|
||||||
|
const hashtagSuggestions = ref<AuteurMini[]>([])
|
||||||
|
const hashtagDropdownOpen = ref(false)
|
||||||
|
const hashtagSelectedIndex = ref(0)
|
||||||
|
|
||||||
|
function getActiveInput(): HTMLInputElement | null {
|
||||||
|
return props.inline ? inputElInline.value : inputElOverlay.value
|
||||||
|
}
|
||||||
|
|
||||||
|
function detectHashtagAtCursor(input: string, cursorPos: number): { start: number; partial: string } | null {
|
||||||
|
const before = input.slice(0, cursorPos)
|
||||||
|
const m = before.match(/#([a-z0-9-]*)$/i)
|
||||||
|
if (!m) return null
|
||||||
|
return { start: m.index ?? 0, partial: (m[1] || '').toLowerCase() }
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateHashtagSuggestions() {
|
||||||
|
const el = getActiveInput()
|
||||||
|
const cursorPos = el?.selectionStart ?? q.value.length
|
||||||
|
const detection = detectHashtagAtCursor(q.value, cursorPos)
|
||||||
|
// Ouvrir dès que le # est présent (partial vide accepté pour afficher la liste)
|
||||||
|
if (!detection) {
|
||||||
|
hashtagDropdownOpen.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const partial = detection.partial
|
||||||
|
const list = partial.length === 0
|
||||||
|
? auteursIngeres.value.slice(0, 8)
|
||||||
|
: auteursIngeres.value
|
||||||
|
.filter(a => a.id.toLowerCase().includes(partial) || a.nom.toLowerCase().includes(partial))
|
||||||
|
.slice(0, 8)
|
||||||
|
hashtagSuggestions.value = list
|
||||||
|
hashtagDropdownOpen.value = list.length > 0
|
||||||
|
hashtagSelectedIndex.value = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyHashtagSuggestion(auteur: AuteurMini) {
|
||||||
|
const el = getActiveInput()
|
||||||
|
const cursorPos = el?.selectionStart ?? q.value.length
|
||||||
|
const detection = detectHashtagAtCursor(q.value, cursorPos)
|
||||||
|
if (!detection) return
|
||||||
|
const before = q.value.slice(0, detection.start)
|
||||||
|
const after = q.value.slice(cursorPos)
|
||||||
|
const insert = '#' + auteur.id + ' '
|
||||||
|
q.value = before + insert + after
|
||||||
|
hashtagDropdownOpen.value = false
|
||||||
|
nextTick(() => {
|
||||||
|
const focusEl = getActiveInput()
|
||||||
|
if (!focusEl) return
|
||||||
|
focusEl.focus()
|
||||||
|
const newPos = before.length + insert.length
|
||||||
|
focusEl.setSelectionRange(newPos, newPos)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function onInputKeydown(e: KeyboardEvent) {
|
||||||
|
if (hashtagDropdownOpen.value && hashtagSuggestions.value.length > 0) {
|
||||||
|
if (e.key === 'ArrowDown') {
|
||||||
|
e.preventDefault()
|
||||||
|
hashtagSelectedIndex.value = (hashtagSelectedIndex.value + 1) % hashtagSuggestions.value.length
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (e.key === 'ArrowUp') {
|
||||||
|
e.preventDefault()
|
||||||
|
hashtagSelectedIndex.value = (hashtagSelectedIndex.value - 1 + hashtagSuggestions.value.length) % hashtagSuggestions.value.length
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (e.key === 'Enter' || e.key === 'Tab') {
|
||||||
|
e.preventDefault()
|
||||||
|
applyHashtagSuggestion(hashtagSuggestions.value[hashtagSelectedIndex.value])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
e.preventDefault()
|
||||||
|
hashtagDropdownOpen.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault()
|
||||||
|
send()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(q, () => {
|
||||||
|
updateHashtagSuggestions()
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
const saved = window.localStorage.getItem(CORPUS_STORAGE_KEY) as CorpusMode | null
|
const saved = window.localStorage.getItem(CORPUS_STORAGE_KEY) as CorpusMode | null
|
||||||
if (saved && ['pensees', 'projets', 'both'].includes(saved)) {
|
if (saved && ['pensees', 'projets', 'both'].includes(saved)) {
|
||||||
corpus.value = saved
|
corpus.value = saved
|
||||||
}
|
}
|
||||||
|
// Chargement liste auteurs ingérés pour autocomplete hashtag
|
||||||
|
try {
|
||||||
|
const data = await $fetch<any>('/data/auteurs-pensees.json')
|
||||||
|
auteursIngeres.value = (data?.auteurs ?? [])
|
||||||
|
.filter((a: any) => a.ingere === true)
|
||||||
|
.map((a: any) => ({ id: String(a.id), nom: String(a.nom) }))
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Erreur chargement auteurs-pensees.json pour hashtag', e)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
function setCorpus(val: CorpusMode) {
|
function setCorpus(val: CorpusMode) {
|
||||||
@@ -240,21 +365,48 @@ watch(() => props.auteurContext, (ctx) => {
|
|||||||
async function send() {
|
async function send() {
|
||||||
const query = q.value.trim()
|
const query = q.value.trim()
|
||||||
if (!query || loading.value) return
|
if (!query || loading.value) return
|
||||||
|
|
||||||
|
// Extraire le premier hashtag matchant un auteur ingéré
|
||||||
|
let auteurSlug: string | null = null
|
||||||
|
const matches = [...query.matchAll(/#([a-z0-9-]+)/gi)]
|
||||||
|
for (const m of matches) {
|
||||||
|
const slug = m[1].toLowerCase()
|
||||||
|
if (auteursIngeres.value.find(a => a.id === slug)) {
|
||||||
|
auteurSlug = slug
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Premier hashtag non-matché (pour info utilisateur si jamais ne match aucun)
|
||||||
|
let auteurSlugUnmatched: string | null = null
|
||||||
|
if (!auteurSlug && matches.length > 0) {
|
||||||
|
auteurSlugUnmatched = matches[0][1].toLowerCase()
|
||||||
|
}
|
||||||
|
|
||||||
err.value = ''
|
err.value = ''
|
||||||
messages.value.push({ role: 'user', content: query })
|
messages.value.push({ role: 'user', content: query })
|
||||||
q.value = ''
|
q.value = ''
|
||||||
|
hashtagDropdownOpen.value = false
|
||||||
loading.value = true
|
loading.value = true
|
||||||
await nextTick()
|
await nextTick()
|
||||||
scrollBottom()
|
scrollBottom()
|
||||||
try {
|
try {
|
||||||
const res = await $fetch<{ response: string }>('/api/chatbot-pensees', {
|
const res = await $fetch<any>('/api/chatbot-pensees', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: { query, mode: 'hybrid', corpus: corpus.value },
|
body: {
|
||||||
|
query,
|
||||||
|
mode: 'hybrid',
|
||||||
|
corpus: corpus.value,
|
||||||
|
auteur_slug: auteurSlug ?? auteurSlugUnmatched,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
messages.value.push({ role: 'assistant', content: res.response ?? '' })
|
let responseText = res.response ?? ''
|
||||||
|
if (res.auteur_unmatched) {
|
||||||
|
responseText = `*(Aucun livre de #${res.auteur_unmatched} n'est ingéré dans le RAG. Je réponds depuis la carte entière.)*\n\n` + responseText
|
||||||
|
}
|
||||||
|
messages.value.push({ role: 'assistant', content: responseText })
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
const s = e?.response?.status ?? e?.statusCode
|
const s = e?.response?.status ?? e?.statusCode
|
||||||
err.value = s === 429 ? 'Limite atteinte.' : s === 503 ? 'RAG indisponible.' : 'Erreur - reessaie.'
|
err.value = s === 429 ? 'Limite atteinte.' : s === 503 ? 'RAG indisponible.' : 'Erreur, reessaie.'
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
await nextTick()
|
await nextTick()
|
||||||
|
|||||||
@@ -143,7 +143,8 @@ const splitRatio = ref(DEFAULT_SPLIT_RATIO)
|
|||||||
const carteFlexBasis = computed(() => `${splitRatio.value * 100}%`)
|
const carteFlexBasis = computed(() => `${splitRatio.value * 100}%`)
|
||||||
const chatbotFlexBasis = computed(() => `${(1 - splitRatio.value) * 100}%`)
|
const chatbotFlexBasis = computed(() => `${(1 - splitRatio.value) * 100}%`)
|
||||||
|
|
||||||
const corpusCount = computed(() => penseesData.value?.auteurs.length ?? 0)
|
// Phase 8.D : compteur = auteurs ingere:true uniquement (32 reels, pas 171 total)
|
||||||
|
const corpusCount = computed(() => penseesData.value?.auteurs.filter(a => a.ingere).length ?? 0)
|
||||||
|
|
||||||
// Logique poignee draggable
|
// Logique poignee draggable
|
||||||
let dragStartY = 0
|
let dragStartY = 0
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
376
scripts/build_authors_v3.mjs
Normal file
376
scripts/build_authors_v3.mjs
Normal file
@@ -0,0 +1,376 @@
|
|||||||
|
// Build auteurs-pensees.json v3.0 — Phase 8.A
|
||||||
|
// Sync corpus JSON unifié : Bonpote authors + LightRAG ingestion flags
|
||||||
|
|
||||||
|
import fs from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
|
||||||
|
const JSON_PATH = 'C:\\Users\\jules\\Dropbox\\ATIS - IPCJRA\\1 PROJETS\\TECH - infra VPS, website pro, RAG\\nav-carte\\public\\data\\auteurs-pensees.json';
|
||||||
|
|
||||||
|
// === LightRAG slug prefixes (from /documents endpoint 2026-05-12) ===
|
||||||
|
const LIGHTRAG_PREFIX_TO_AUTHOR_SLUG = {
|
||||||
|
bookchin: 'murray-bookchin',
|
||||||
|
brand: 'steward-brand',
|
||||||
|
carson: 'rachel-carson',
|
||||||
|
charbonneau: 'bernard-charbonneau',
|
||||||
|
descola: 'philippe-descola',
|
||||||
|
despret: 'vinciane-despret',
|
||||||
|
eaubonne: 'francoise-deaubonne',
|
||||||
|
ellul: 'jacques-ellul',
|
||||||
|
federici: 'silvia-federici',
|
||||||
|
ferdinand: 'malcolm-ferdinand',
|
||||||
|
figueres: 'christiana-figueres',
|
||||||
|
georgescu: 'nicholas-georgescu-roegen',
|
||||||
|
gorz: 'andre-gorz',
|
||||||
|
graeber: 'david-graeber',
|
||||||
|
keith: 'david-keith',
|
||||||
|
klein: 'naomi-klein',
|
||||||
|
kropotkine: 'pierre-kropotkine',
|
||||||
|
latouche: 'serge-latouche',
|
||||||
|
latour: 'bruno-latour',
|
||||||
|
lowy: 'michael-lowy',
|
||||||
|
malm: 'andreas-malm',
|
||||||
|
marx: 'karl-marx',
|
||||||
|
meadows: 'donella-meadows',
|
||||||
|
morizot: 'baptiste-morizot',
|
||||||
|
naess: 'arne-naess',
|
||||||
|
ouassak: 'fatima-ouassak',
|
||||||
|
reclus: 'elisee-reclus',
|
||||||
|
saito: 'kohei-saito',
|
||||||
|
servigne: 'pablo-servigne',
|
||||||
|
shiva: 'vandana-shiva',
|
||||||
|
stengers: 'isabelle-stengers',
|
||||||
|
vettese: 'troy-vettese',
|
||||||
|
};
|
||||||
|
|
||||||
|
const INGESTED_AUTHOR_SLUGS = new Set(Object.values(LIGHTRAG_PREFIX_TO_AUTHOR_SLUG));
|
||||||
|
|
||||||
|
// === Bonpote authors (nom, dates, ecole_principale, ecoles_secondaires[]) ===
|
||||||
|
const BONPOTE_AUTHORS = [
|
||||||
|
// Éco-anarchisme
|
||||||
|
['Pierre Kropotkine', '1842-1921', 'eco-anarchisme', []],
|
||||||
|
['Élisée Reclus', '1830-1905', 'eco-anarchisme', []],
|
||||||
|
['Murray Bookchin', '1921-2006', 'eco-anarchisme', []],
|
||||||
|
['David Graeber', '1961-2020', 'eco-anarchisme', []],
|
||||||
|
['James C. Scott', '1936-2024', 'eco-anarchisme', []],
|
||||||
|
['Marshall Sahlins', '1930-2021', 'eco-anarchisme', []],
|
||||||
|
['Pierre Clastres', '1934-1977', 'eco-anarchisme', []],
|
||||||
|
['Cornélius Castoriadis', '1922-1997', 'eco-anarchisme', []],
|
||||||
|
['David Harvey', '1935-', 'eco-anarchisme', ['ecosocialisme']],
|
||||||
|
['Henri Lefebvre', '1901-1991', 'eco-anarchisme', ['ecosocialisme']],
|
||||||
|
['Émile Gravelle', '1855-1920', 'eco-anarchisme', []],
|
||||||
|
['Henri Zisly', '1872-1945', 'eco-anarchisme', []],
|
||||||
|
['Edward Carpenter', '1844-1929', 'eco-anarchisme', []],
|
||||||
|
['William Morris', '1834-1896', 'eco-anarchisme', []],
|
||||||
|
['John Ruskin', '1819-1900', 'eco-anarchisme', []],
|
||||||
|
['Kirkpatrick Sale', '1937-', 'eco-anarchisme', []],
|
||||||
|
['Wendell Berry', '1934-', 'eco-anarchisme', []],
|
||||||
|
['Kristin Ross', '1953-', 'eco-anarchisme', []],
|
||||||
|
['Theodore Kaczynski', '1942-2023', 'eco-anarchisme', ['technocritique']],
|
||||||
|
['Saint-Simon', '1760-1825', 'eco-anarchisme', []],
|
||||||
|
['Auguste Comte', '1798-1857', 'eco-anarchisme', []],
|
||||||
|
['Alberto Magnaghi', '1941-2023', 'eco-anarchisme', []],
|
||||||
|
['Peter Berg', '1937-2011', 'eco-anarchisme', []],
|
||||||
|
['Andreas Malm', '1977-', 'ecosocialisme', ['eco-anarchisme']],
|
||||||
|
|
||||||
|
// Écosocialisme
|
||||||
|
['Karl Marx', '1818-1883', 'ecosocialisme', []],
|
||||||
|
['Friedrich Engels', '1820-1895', 'ecosocialisme', []],
|
||||||
|
['Rosa Luxemburg', '1871-1919', 'ecosocialisme', []],
|
||||||
|
['Walter Benjamin', '1892-1940', 'ecosocialisme', []],
|
||||||
|
['John Maynard Keynes', '1883-1946', 'ecosocialisme', []],
|
||||||
|
['Pascal Lamy', '1947-', 'ecosocialisme', []],
|
||||||
|
['Ann Pettifor', '1947-', 'ecosocialisme', []],
|
||||||
|
['Holly Jean Buck', '', 'ecosocialisme', []],
|
||||||
|
['Cédric Durand', '1975-', 'ecosocialisme', []],
|
||||||
|
['Kim Stanley Robinson', '1952-', 'ecosocialisme', []],
|
||||||
|
['André Gorz', '1923-2007', 'ecosocialisme', ['decroissance', 'technocritique']],
|
||||||
|
['Kohei Saito', '1987-', 'ecosocialisme', ['decroissance']],
|
||||||
|
['Razmig Keucheyan', '1975-', 'ecosocialisme', []],
|
||||||
|
['Dominique Méda', '1962-', 'ecosocialisme', []],
|
||||||
|
['Dominique Bourg', '1953-', 'ecosocialisme', []],
|
||||||
|
['Troy Vettese', '', 'ecosocialisme', []],
|
||||||
|
['Loïc Blondiaux', '1962-', 'ecosocialisme', []],
|
||||||
|
['Drew Pendergrass', '', 'ecosocialisme', []],
|
||||||
|
['Jason W. Moore', '', 'ecosocialisme', []],
|
||||||
|
["James O'Connor", '1930-2017', 'ecosocialisme', []],
|
||||||
|
['Herman Daly', '1938-2022', 'ecosocialisme', ['capitalisme-vert']],
|
||||||
|
['John Bellamy Foster', '1953-', 'ecosocialisme', []],
|
||||||
|
['Michael Löwy', '1938-', 'ecosocialisme', []],
|
||||||
|
['Joel Kovel', '1936-2018', 'ecosocialisme', []],
|
||||||
|
['Naomi Klein', '1970-', 'ecosocialisme', []],
|
||||||
|
|
||||||
|
// Technocritique
|
||||||
|
['Jacques Ellul', '1912-1994', 'technocritique', []],
|
||||||
|
['Bernard Charbonneau', '1910-1996', 'technocritique', []],
|
||||||
|
['Lewis Mumford', '1895-1990', 'technocritique', []],
|
||||||
|
['Alain Caillé', '1944-', 'technocritique', []],
|
||||||
|
['Hans Jonas', '1903-1993', 'technocritique', ['ethiques-environnementales']],
|
||||||
|
['Herbert Marcuse', '1898-1979', 'technocritique', []],
|
||||||
|
['Günther Anders', '1902-1992', 'technocritique', []],
|
||||||
|
['Pierre Fournier', '1937-1973', 'technocritique', []],
|
||||||
|
['Alexandre Grothendieck', '1928-2014', 'technocritique', []],
|
||||||
|
['Patrick Viveret', '1948-', 'technocritique', []],
|
||||||
|
['Philippe Bihouix', '1971-', 'technocritique', []],
|
||||||
|
['Jean Baudrillard', '1929-2007', 'technocritique', []],
|
||||||
|
['Serge Latouche', '1940-', 'decroissance', ['technocritique']],
|
||||||
|
['Ivan Illich', '1926-2002', 'technocritique', ['decroissance']],
|
||||||
|
['Leopold Kohr', '1909-1994', 'technocritique', ['decroissance']],
|
||||||
|
['Ernst Schumacher', '1911-1977', 'technocritique', ['decroissance']],
|
||||||
|
['Nicholas Georgescu-Roegen', '1906-1994', 'decroissance', ['technocritique']],
|
||||||
|
|
||||||
|
// Écoféminismes
|
||||||
|
["Françoise d'Eaubonne", '1920-2005', 'ecofeminismes', []],
|
||||||
|
['Vandana Shiva', '1952-', 'ecofeminismes', ['ecologies-decoloniales']],
|
||||||
|
['Starhawk', '1951-', 'ecofeminismes', []],
|
||||||
|
['Ariel Salleh', '1944-', 'ecofeminismes', []],
|
||||||
|
['Maria Mies', '1931-2023', 'ecofeminismes', []],
|
||||||
|
['Carolyn Merchant', '1936-', 'ecofeminismes', []],
|
||||||
|
['Silvia Federici', '1942-', 'ecofeminismes', []],
|
||||||
|
['Val Plumwood', '1939-2008', 'ecofeminismes', []],
|
||||||
|
['Susan Griffin', '1943-', 'ecofeminismes', []],
|
||||||
|
['Veronika Bennholdt-Thomsen', '1944-', 'ecofeminismes', []],
|
||||||
|
['Geneviève Pruvost', '1973-', 'ecofeminismes', []],
|
||||||
|
['Donna Haraway', '1944-', 'ecofeminismes', ['pensees-vivant']],
|
||||||
|
['Émilie Hache', '', 'ecofeminismes', []],
|
||||||
|
['Joanna Macy', '1929-', 'ecofeminismes', ['ethiques-environnementales']],
|
||||||
|
|
||||||
|
// Capitalisme vert
|
||||||
|
['Bill Gates', '1955-', 'capitalisme-vert', []],
|
||||||
|
['Christiana Figueres', '1956-', 'capitalisme-vert', []],
|
||||||
|
['Nicholas Stern', '1946-', 'capitalisme-vert', []],
|
||||||
|
['Jeffrey Sachs', '1954-', 'capitalisme-vert', []],
|
||||||
|
['Jared Diamond', '1937-', 'capitalisme-vert', ['decroissance']],
|
||||||
|
['Jørgen Randers', '1945-', 'capitalisme-vert', ['decroissance']],
|
||||||
|
['Donella Meadows', '1941-2001', 'decroissance', ['capitalisme-vert']],
|
||||||
|
['Dennis Meadows', '1942-', 'decroissance', ['capitalisme-vert']],
|
||||||
|
['Kate Raworth', '1970-', 'capitalisme-vert', []],
|
||||||
|
['Al Gore', '1948-', 'capitalisme-vert', []],
|
||||||
|
['Hal Harvey', '1960-', 'capitalisme-vert', []],
|
||||||
|
['Laurence Tubiana', '1951-', 'capitalisme-vert', []],
|
||||||
|
['Amory Lovins', '1947-', 'capitalisme-vert', []],
|
||||||
|
['David Pearce', '1959-', 'capitalisme-vert', []],
|
||||||
|
['Kerry Turner', '1948-', 'capitalisme-vert', []],
|
||||||
|
['David Keith', '1963-', 'capitalisme-vert', []],
|
||||||
|
['Ted Nordhaus', '1965-', 'capitalisme-vert', []],
|
||||||
|
['Michael Shellenberger', '1971-', 'capitalisme-vert', []],
|
||||||
|
['Pavan Sukhdev', '1960-', 'capitalisme-vert', []],
|
||||||
|
['Janine Benyus', '1958-', 'capitalisme-vert', []],
|
||||||
|
['Robert Costanza', '1950-', 'capitalisme-vert', []],
|
||||||
|
['Peter Kareiva', '1951-', 'capitalisme-vert', []],
|
||||||
|
['Michelle Marvier', '', 'capitalisme-vert', []],
|
||||||
|
['Robert Lalasz', '1915-2003', 'capitalisme-vert', []],
|
||||||
|
['Steward Brand', '1938-', 'capitalisme-vert', []],
|
||||||
|
['Paul Crutzen', '1933-2021', 'capitalisme-vert', []],
|
||||||
|
['Kenneth Boulding', '1910-1993', 'capitalisme-vert', []],
|
||||||
|
['Eugene Odum', '1913-2002', 'capitalisme-vert', []],
|
||||||
|
['Howard Odum', '1924-2002', 'capitalisme-vert', []],
|
||||||
|
['Jean-Marc Jancovici', '1962-', 'capitalisme-vert', []],
|
||||||
|
['Yves Cochet', '1946-', 'capitalisme-vert', ['decroissance']],
|
||||||
|
['Pablo Servigne', '1978-', 'decroissance', ['capitalisme-vert']],
|
||||||
|
['Gauthier Chapelle', '1968-', 'decroissance', ['capitalisme-vert']],
|
||||||
|
|
||||||
|
// Écologies décoloniales
|
||||||
|
['Malcom Ferdinand', '1985-', 'ecologies-decoloniales', []],
|
||||||
|
['Frantz Fanon', '1925-1961', 'ecologies-decoloniales', []],
|
||||||
|
['Édouard Glissant', '1928-2011', 'ecologies-decoloniales', []],
|
||||||
|
['Aimé Césaire', '1913-2008', 'ecologies-decoloniales', []],
|
||||||
|
['Mohamad Amer Meziane', '', 'ecologies-decoloniales', []],
|
||||||
|
['Chico Mendes', '1944-1988', 'ecologies-decoloniales', []],
|
||||||
|
['Joan Martínez Alier', '1939-', 'ecologies-decoloniales', []],
|
||||||
|
['Arturo Escobar', '1951-', 'ecologies-decoloniales', []],
|
||||||
|
['Sous-commandant Marcos', '1957-', 'ecologies-decoloniales', []],
|
||||||
|
['Alberto Acosta', '1948-', 'ecologies-decoloniales', []],
|
||||||
|
['Jérôme Baschet', '1960-', 'ecologies-decoloniales', []],
|
||||||
|
['Fatima Ouassak', '1976-', 'ecofeminismes', ['ecologies-decoloniales']],
|
||||||
|
['William Acker', '1991-', 'ecologies-decoloniales', []],
|
||||||
|
['Giorgos Kallis', '1972-', 'ecologies-decoloniales', ['decroissance']],
|
||||||
|
['Bernard Lambert', '1931-1984', 'ecologies-decoloniales', []],
|
||||||
|
|
||||||
|
// Écofascismes
|
||||||
|
['Alain de Benoist', '1943-', 'ecofascismes', []],
|
||||||
|
['Paul Ralph Ehrlich', '1932-', 'ecofascismes', []],
|
||||||
|
['Garrett Hardin', '1915-2003', 'ecofascismes', []],
|
||||||
|
['Edward Osborne Wilson', '1929-2021', 'ecofascismes', []],
|
||||||
|
['Thomas Malthus', '1803-1882', 'ecofascismes', []],
|
||||||
|
['David Foreman', '1946-2022', 'ecofascismes', []],
|
||||||
|
['Piero San Giorgio', '1971-', 'ecofascismes', []],
|
||||||
|
|
||||||
|
// Éthique environnementale
|
||||||
|
['Arne Næss', '1912-2009', 'ethiques-environnementales', []],
|
||||||
|
['Rachel Carson', '1907-1964', 'ethiques-environnementales', []],
|
||||||
|
['Aldo Leopold', '1887-1948', 'ethiques-environnementales', []],
|
||||||
|
['Imanishi Kinji', '1902-1992', 'ethiques-environnementales', []],
|
||||||
|
['Paul Watson', '1950-', 'ethiques-environnementales', []],
|
||||||
|
['John Muir', '1838-1914', 'ethiques-environnementales', []],
|
||||||
|
['Edward Abbey', '1927-1989', 'ethiques-environnementales', []],
|
||||||
|
['John Baird Callicott', '1941-', 'ethiques-environnementales', []],
|
||||||
|
['Bill Mollison', '1928-2016', 'ethiques-environnementales', []],
|
||||||
|
['David Holmgren', '1955-', 'ethiques-environnementales', []],
|
||||||
|
['Peter Singer', '1946-', 'ethiques-environnementales', []],
|
||||||
|
['Pierre Rabhi', '1938-2021', 'ethiques-environnementales', []],
|
||||||
|
['Rob Hopkins', '1968-', 'ethiques-environnementales', []],
|
||||||
|
['Cyril Dion', '1978-', 'ethiques-environnementales', []],
|
||||||
|
['Gandhi', '1869-1948', 'ethiques-environnementales', []],
|
||||||
|
['Gifford Pinchot', '1865-1946', 'ethiques-environnementales', []],
|
||||||
|
['Lanza del Vasto', '1901-1981', 'ethiques-environnementales', []],
|
||||||
|
['Jorge Mario Bergoglio', '1936-', 'ethiques-environnementales', []],
|
||||||
|
['Gary Snyder', '1930-', 'ethiques-environnementales', []],
|
||||||
|
['Henry David Thoreau', '1817-1862', 'ethiques-environnementales', []],
|
||||||
|
['Ralph Waldo Emerson', '1803-1882', 'ethiques-environnementales', []],
|
||||||
|
['José Bové', '1953-', 'ethiques-environnementales', []],
|
||||||
|
['Glenn Albrecht', '1953-', 'ethiques-environnementales', []],
|
||||||
|
|
||||||
|
// Pensées du vivant
|
||||||
|
['Bruno Latour', '1947-2022', 'pensees-vivant', []],
|
||||||
|
['Isabelle Stengers', '1949-', 'pensees-vivant', []],
|
||||||
|
['Vinciane Despret', '1959-', 'pensees-vivant', []],
|
||||||
|
['Baptiste Morizot', '1983-', 'pensees-vivant', []],
|
||||||
|
['Philippe Descola', '1949-', 'pensees-vivant', []],
|
||||||
|
['Eduardo Viveiros de Castro', '1951-', 'pensees-vivant', []],
|
||||||
|
['Anna Tsing', '1952-', 'pensees-vivant', []],
|
||||||
|
['Deborah Bird Rose', '1946-2018', 'pensees-vivant', []],
|
||||||
|
['Lynn Margulis', '1938-2011', 'pensees-vivant', []],
|
||||||
|
['James Lovelock', '1919-2022', 'pensees-vivant', []],
|
||||||
|
['Serge Moscovici', '1925-2014', 'pensees-vivant', []],
|
||||||
|
['Theodore Roszak', '1933-2011', 'pensees-vivant', []],
|
||||||
|
['Baruch Spinoza', '1632-1677', 'pensees-vivant', []],
|
||||||
|
];
|
||||||
|
|
||||||
|
// Special slug overrides (match v2.1 IDs + ligatures)
|
||||||
|
const NAME_TO_SLUG_OVERRIDES = {
|
||||||
|
'Malcom Ferdinand': 'malcolm-ferdinand',
|
||||||
|
"Françoise d'Eaubonne": 'francoise-deaubonne',
|
||||||
|
'Donella Meadows': 'donella-meadows',
|
||||||
|
'Dennis Meadows': 'dennis-meadows',
|
||||||
|
'Arne Næss': 'arne-naess',
|
||||||
|
'Jørgen Randers': 'jorgen-randers',
|
||||||
|
};
|
||||||
|
|
||||||
|
function slugify(name) {
|
||||||
|
// Pre-process special ligatures and chars not handled by NFKD
|
||||||
|
let pre = name
|
||||||
|
.replace(/[æÆ]/g, 'ae')
|
||||||
|
.replace(/[øØ]/g, 'o')
|
||||||
|
.replace(/[œŒ]/g, 'oe')
|
||||||
|
.replace(/ß/g, 'ss');
|
||||||
|
// Remove diacritical marks
|
||||||
|
const noAccent = pre.normalize('NFKD').replace(/[̀-ͯ]/g, '');
|
||||||
|
return noAccent
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[^a-z0-9]+/g, '-')
|
||||||
|
.replace(/^-+|-+$/g, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAuthorSlug(name) {
|
||||||
|
if (NAME_TO_SLUG_OVERRIDES[name]) return NAME_TO_SLUG_OVERRIDES[name];
|
||||||
|
return slugify(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
function main() {
|
||||||
|
const raw = fs.readFileSync(JSON_PATH, 'utf-8');
|
||||||
|
const data = JSON.parse(raw);
|
||||||
|
|
||||||
|
const existingBySlug = {};
|
||||||
|
for (const a of data.auteurs) existingBySlug[a.id] = a;
|
||||||
|
|
||||||
|
const newAuthors = [];
|
||||||
|
const seenSlugs = new Set();
|
||||||
|
|
||||||
|
for (const [nom, dates, ecolePrincipale, ecolesSecondaires] of BONPOTE_AUTHORS) {
|
||||||
|
const slug = getAuthorSlug(nom);
|
||||||
|
if (seenSlugs.has(slug)) {
|
||||||
|
console.error(`DUPLICATE SKIP: ${nom} -> ${slug}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
seenSlugs.add(slug);
|
||||||
|
|
||||||
|
const ingere = INGESTED_AUTHOR_SLUGS.has(slug);
|
||||||
|
const ecoles = [ecolePrincipale, ...ecolesSecondaires];
|
||||||
|
|
||||||
|
if (existingBySlug[slug]) {
|
||||||
|
// Preserve enriched entry
|
||||||
|
const entry = { ...existingBySlug[slug], ingere };
|
||||||
|
newAuthors.push(entry);
|
||||||
|
} else {
|
||||||
|
// New minimal entry
|
||||||
|
const bioProvisoire = ingere
|
||||||
|
? `Auteur·ice ingéré·e dans le RAG ATIS, bio à enrichir lors de PRG-5.`
|
||||||
|
: `Théoricien·ne présent·e sur le poster Bonpote (${ecolePrincipale}), non ingéré·e dans le RAG ATIS.`;
|
||||||
|
newAuthors.push({
|
||||||
|
id: slug,
|
||||||
|
nom,
|
||||||
|
dates,
|
||||||
|
ecoles,
|
||||||
|
ecole_principale: ecolePrincipale,
|
||||||
|
livres_rag: [],
|
||||||
|
theses_cles_attendues: [],
|
||||||
|
bio_courte_provisoire: bioProvisoire,
|
||||||
|
ingere,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Preserve any v2.1 author not in Bonpote list
|
||||||
|
for (const [slug, entry] of Object.entries(existingBySlug)) {
|
||||||
|
if (!seenSlugs.has(slug)) {
|
||||||
|
const copy = { ...entry };
|
||||||
|
if (!('ingere' in copy)) copy.ingere = INGESTED_AUTHOR_SLUGS.has(slug);
|
||||||
|
newAuthors.push(copy);
|
||||||
|
seenSlugs.add(slug);
|
||||||
|
console.error(`NOTE: preserved v2.1 author not in Bonpote canonical: ${slug}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const auteursCount = newAuthors.length;
|
||||||
|
const auteursIngeresCount = newAuthors.filter(a => a.ingere).length;
|
||||||
|
|
||||||
|
data.meta.version = '3.0';
|
||||||
|
data.meta.updated = '2026-05-12';
|
||||||
|
data.meta.auteurs_count = auteursCount;
|
||||||
|
data.meta.auteurs_ingeres_count = auteursIngeresCount;
|
||||||
|
data.meta.source = 'FRACAS Bonpote V2 oct 2024 + LightRAG corpus 12/05/2026 (v3.0 sync)';
|
||||||
|
data.meta.note_v3_0 = 'Phase 8.A sync corpus unifie : ~140 auteurs Bonpote integres, flag ingere:true/false selon LightRAG VPS. Auteurs non-ingeres = entrees minimales (bio provisoire, livres_rag vide), a enrichir lors de PRG-4/PRG-5.';
|
||||||
|
|
||||||
|
data.auteurs = newAuthors;
|
||||||
|
|
||||||
|
fs.writeFileSync(JSON_PATH, JSON.stringify(data, null, 2), 'utf-8');
|
||||||
|
|
||||||
|
// Validate parse-back
|
||||||
|
const parsedBack = JSON.parse(fs.readFileSync(JSON_PATH, 'utf-8'));
|
||||||
|
if (parsedBack.auteurs.length !== auteursCount) {
|
||||||
|
console.error('PARSE-BACK MISMATCH');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stats
|
||||||
|
const schoolsStats = {};
|
||||||
|
for (const a of newAuthors) {
|
||||||
|
const ep = a.ecole_principale || '?';
|
||||||
|
if (!schoolsStats[ep]) schoolsStats[ep] = { total: 0, ingere: 0 };
|
||||||
|
schoolsStats[ep].total++;
|
||||||
|
if (a.ingere) schoolsStats[ep].ingere++;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('\n=== JSON v3.0 written ===');
|
||||||
|
console.log(`Total auteurs : ${auteursCount}`);
|
||||||
|
console.log(`Ingeres : ${auteursIngeresCount}`);
|
||||||
|
console.log(`Non-ingeres : ${auteursCount - auteursIngeresCount}`);
|
||||||
|
console.log(`Parse-back : OK (${parsedBack.auteurs.length} auteurs)`);
|
||||||
|
console.log('\nPer school (ecole_principale):');
|
||||||
|
const sortedSchools = Object.entries(schoolsStats).sort((a, b) => b[1].total - a[1].total);
|
||||||
|
for (const [school, st] of sortedSchools) {
|
||||||
|
console.log(` ${school.padEnd(30)} total=${String(st.total).padStart(3)} ingere=${String(st.ingere).padStart(3)} non-ing=${String(st.total - st.ingere).padStart(3)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Top 5 schools with most non-ingested
|
||||||
|
const nonIngStats = sortedSchools
|
||||||
|
.map(([k, v]) => [k, v.total - v.ingere])
|
||||||
|
.sort((a, b) => b[1] - a[1])
|
||||||
|
.slice(0, 5);
|
||||||
|
console.log('\nTop 5 ecoles avec le plus de non-ingeres (PRG-4 priorities):');
|
||||||
|
for (const [school, count] of nonIngStats) {
|
||||||
|
console.log(` ${school.padEnd(30)} non-ing=${count}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
import type { H3Event } from 'h3'
|
import type { H3Event } from 'h3'
|
||||||
|
import { readFileSync } from 'node:fs'
|
||||||
|
import { join } from 'node:path'
|
||||||
import { checkRateLimitJson } from '~/server/utils/rateLimitJson'
|
import { checkRateLimitJson } from '~/server/utils/rateLimitJson'
|
||||||
|
|
||||||
interface ChatbotPenseesRequest {
|
interface ChatbotPenseesRequest {
|
||||||
@@ -7,11 +9,25 @@ interface ChatbotPenseesRequest {
|
|||||||
corpus?: 'pensees' | 'projets' | 'both'
|
corpus?: 'pensees' | 'projets' | 'both'
|
||||||
filter_couche?: 'fond' | 'forme' | 'structure' | null
|
filter_couche?: 'fond' | 'forme' | 'structure' | null
|
||||||
filter_ecole?: string | null
|
filter_ecole?: string | null
|
||||||
|
auteur_slug?: string | null
|
||||||
history?: Array<{ role: 'user' | 'assistant'; content: string }>
|
history?: Array<{ role: 'user' | 'assistant'; content: string }>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface LightRAGReference {
|
||||||
|
reference_id?: string
|
||||||
|
file_path?: string
|
||||||
|
content?: string | null
|
||||||
|
}
|
||||||
|
|
||||||
interface LightRAGQueryResponse {
|
interface LightRAGQueryResponse {
|
||||||
response: string
|
response: string
|
||||||
|
references?: LightRAGReference[]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AuteurMini {
|
||||||
|
id: string
|
||||||
|
nom: string
|
||||||
|
ingere?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const SYSTEM_PREFACE_PENSEES = `Tu es un agent du RAG Pensées Écologiques, infrastructure militante du collectif trans-former.fr.
|
const SYSTEM_PREFACE_PENSEES = `Tu es un agent du RAG Pensées Écologiques, infrastructure militante du collectif trans-former.fr.
|
||||||
@@ -47,6 +63,37 @@ Règles :
|
|||||||
- Ton praticien militant : direct, pas neutre, ancré dans la pratique architecturale.
|
- Ton praticien militant : direct, pas neutre, ancré dans la pratique architecturale.
|
||||||
- Réponse en français, dense, sans délayage.`
|
- Réponse en français, dense, sans délayage.`
|
||||||
|
|
||||||
|
function buildPrefaceAuteur(nomAuteur: string, slug: string): string {
|
||||||
|
return `Tu réponds EXCLUSIVEMENT depuis les livres de ${nomAuteur} présents dans le RAG (fichiers commençant par "${slug}__").
|
||||||
|
Si la question sort du périmètre de cet auteur, indique-le et propose de l'aborder sans le hashtag pour interroger la carte entière. Reste fidèle au style et à la pensée de ${nomAuteur}. Cite toujours le livre.
|
||||||
|
|
||||||
|
Règles :
|
||||||
|
- Cite les sources (titre du livre) à chaque assertion.
|
||||||
|
- Pas d'hallucination. Si l'info n'est pas dans le corpus de cet auteur, dis-le.
|
||||||
|
- N'introduis JAMAIS d'autres auteurs sauf si ${nomAuteur} les commente explicitement.
|
||||||
|
- Ton politique direct, pas de neutralité fade.
|
||||||
|
- Réponse en français, dense, sans délayage.`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chargement (et cache) de la liste des auteurs ingérés pour validation du slug
|
||||||
|
let auteursIngeresCache: AuteurMini[] | null = null
|
||||||
|
function loadAuteursIngeres(): AuteurMini[] {
|
||||||
|
if (auteursIngeresCache) return auteursIngeresCache
|
||||||
|
try {
|
||||||
|
const jsonPath = join(process.cwd(), 'public', 'data', 'auteurs-pensees.json')
|
||||||
|
const raw = readFileSync(jsonPath, 'utf-8')
|
||||||
|
const data = JSON.parse(raw)
|
||||||
|
const list: AuteurMini[] = (data.auteurs ?? [])
|
||||||
|
.filter((a: any) => a.ingere === true)
|
||||||
|
.map((a: any) => ({ id: String(a.id), nom: String(a.nom), ingere: true }))
|
||||||
|
auteursIngeresCache = list
|
||||||
|
return list
|
||||||
|
} catch {
|
||||||
|
auteursIngeresCache = []
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default defineEventHandler(async (event: H3Event) => {
|
export default defineEventHandler(async (event: H3Event) => {
|
||||||
const config = useRuntimeConfig(event)
|
const config = useRuntimeConfig(event)
|
||||||
|
|
||||||
@@ -72,13 +119,26 @@ export default defineEventHandler(async (event: H3Event) => {
|
|||||||
const corpus = body.corpus || 'both'
|
const corpus = body.corpus || 'both'
|
||||||
const ragUrl = (config.ragPeUrl as string) || 'http://localhost:9621'
|
const ragUrl = (config.ragPeUrl as string) || 'http://localhost:9621'
|
||||||
|
|
||||||
// Préface adaptative selon corpus demandé
|
// Validation auteur_slug (Phase 8.E) : match contre la liste des auteurs ingérés
|
||||||
const systemPreface =
|
const auteurSlug = body.auteur_slug?.trim().toLowerCase() || null
|
||||||
corpus === 'pensees'
|
let nomAuteurMatch: string | null = null
|
||||||
? SYSTEM_PREFACE_PENSEES
|
if (auteurSlug) {
|
||||||
: corpus === 'projets'
|
const ingeres = loadAuteursIngeres()
|
||||||
? SYSTEM_PREFACE_PROJETS
|
const auteur = ingeres.find(a => a.id === auteurSlug)
|
||||||
: SYSTEM_PREFACE_BOTH
|
nomAuteurMatch = auteur?.nom ?? null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Préface adaptative : auteur prioritaire si slug matché, sinon corpus
|
||||||
|
let systemPreface: string
|
||||||
|
if (auteurSlug && nomAuteurMatch) {
|
||||||
|
systemPreface = buildPrefaceAuteur(nomAuteurMatch, auteurSlug)
|
||||||
|
} else if (corpus === 'pensees') {
|
||||||
|
systemPreface = SYSTEM_PREFACE_PENSEES
|
||||||
|
} else if (corpus === 'projets') {
|
||||||
|
systemPreface = SYSTEM_PREFACE_PROJETS
|
||||||
|
} else {
|
||||||
|
systemPreface = SYSTEM_PREFACE_BOTH
|
||||||
|
}
|
||||||
|
|
||||||
// 3. Health guard — LightRAG down = erreur claire, pas de fallback hallucinatoire
|
// 3. Health guard — LightRAG down = erreur claire, pas de fallback hallucinatoire
|
||||||
try {
|
try {
|
||||||
@@ -93,11 +153,20 @@ export default defineEventHandler(async (event: H3Event) => {
|
|||||||
// 4. Call LightRAG VPS — préface système injectée dans la query
|
// 4. Call LightRAG VPS — préface système injectée dans la query
|
||||||
const ragQuery = `${systemPreface}\n\nQuestion : ${query}`
|
const ragQuery = `${systemPreface}\n\nQuestion : ${query}`
|
||||||
|
|
||||||
|
// Construction du body : hl_keywords + ll_keywords si auteur ciblé
|
||||||
|
// NB : LightRAG ne supporte ni keyword_filter ni ids ni metadata_filter (preflight OpenAPI confirmé).
|
||||||
|
// hl_keywords / ll_keywords sont les seuls leviers natifs de priorisation par auteur.
|
||||||
|
const ragBody: Record<string, unknown> = { query: ragQuery, mode }
|
||||||
|
if (auteurSlug && nomAuteurMatch) {
|
||||||
|
ragBody.hl_keywords = [nomAuteurMatch, auteurSlug]
|
||||||
|
ragBody.ll_keywords = [auteurSlug]
|
||||||
|
}
|
||||||
|
|
||||||
let ragResponse: LightRAGQueryResponse
|
let ragResponse: LightRAGQueryResponse
|
||||||
try {
|
try {
|
||||||
ragResponse = await $fetch<LightRAGQueryResponse>(`${ragUrl}/query`, {
|
ragResponse = await $fetch<LightRAGQueryResponse>(`${ragUrl}/query`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: { query: ragQuery, mode },
|
body: ragBody,
|
||||||
timeout: 90000,
|
timeout: 90000,
|
||||||
})
|
})
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
@@ -108,11 +177,28 @@ export default defineEventHandler(async (event: H3Event) => {
|
|||||||
throw createError({ statusCode: 504, message: 'RAG en cours de processing — réessaie dans quelques secondes.' })
|
throw createError({ statusCode: 504, message: 'RAG en cours de processing — réessaie dans quelques secondes.' })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fallback post-process : si auteur ciblé et que les references LightRAG remontent
|
||||||
|
// des chunks hors slug__, on l'indique pour transparence. La préface LLM est la garde principale.
|
||||||
|
let chunksOffTarget = 0
|
||||||
|
let chunksOnTarget = 0
|
||||||
|
if (auteurSlug && nomAuteurMatch && Array.isArray(ragResponse.references)) {
|
||||||
|
const slugPrefix = `${auteurSlug}__`
|
||||||
|
for (const ref of ragResponse.references) {
|
||||||
|
const fp = (ref.file_path ?? '').toLowerCase()
|
||||||
|
if (!fp) continue
|
||||||
|
if (fp.startsWith(slugPrefix)) chunksOnTarget++
|
||||||
|
else chunksOffTarget++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 5. Retour formaté
|
// 5. Retour formaté
|
||||||
return {
|
return {
|
||||||
response: ragResponse.response ?? '',
|
response: ragResponse.response ?? '',
|
||||||
mode,
|
mode,
|
||||||
corpus,
|
corpus,
|
||||||
|
auteur: auteurSlug && nomAuteurMatch ? { slug: auteurSlug, nom: nomAuteurMatch } : null,
|
||||||
|
auteur_unmatched: auteurSlug && !nomAuteurMatch ? auteurSlug : null,
|
||||||
|
auteur_chunks: auteurSlug && nomAuteurMatch ? { on_target: chunksOnTarget, off_target: chunksOffTarget } : null,
|
||||||
filter: { couche: body.filter_couche ?? null, ecole: body.filter_ecole ?? null },
|
filter: { couche: body.filter_couche ?? null, ecole: body.filter_ecole ?? null },
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user