feat(media): Phase 8.G noeuds-ecoles + popup RAG info + lien Bonpote + migration Nebius
- CartePensees: noeuds ecole visibles (cercles proportionnels count auteurs, cliquables, emit select-ecole) - CartePensees: collision D3 ajustee pour repulsion auteurs autour des noeuds ecole - FicheEcole: nouveau composant modal (liste auteurs ingeres/non-ingeres, interroger RAG) - media: header lien Bonpote V2 cliquable + bouton i info RAG - media: popup FRACAS (description RAG, 662 dimensions, 3 couches, localStorage 1ere visite) - media: FicheEcole branchee (select-ecole, select-auteur-from-ecole, interroger-ecole) - ChatbotPensees: suppression mention corpusCount hardcoded (double source de verite) - chatbot, chatbot-v2, chatbot-reseaux, chatbot-taff: migration Mistral -> Nebius DeepSeek-V3.2 - nuxt.config: ajout nebiusApiKey runtime config Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -39,7 +39,7 @@ const LINKS_INFLUENCE = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
const props = defineProps<{ data: PenseesData | null; active?: boolean }>()
|
const props = defineProps<{ data: PenseesData | null; active?: boolean }>()
|
||||||
const emit = defineEmits<{ 'select-auteur': [id: string] }>()
|
const emit = defineEmits<{ 'select-auteur': [id: string]; 'select-ecole': [id: string] }>()
|
||||||
|
|
||||||
const svgRef = ref<SVGElement | null>(null)
|
const svgRef = ref<SVGElement | null>(null)
|
||||||
const tooltipRef = ref<HTMLElement | null>(null)
|
const tooltipRef = ref<HTMLElement | null>(null)
|
||||||
@@ -201,6 +201,12 @@ async function initGraph() {
|
|||||||
fx: W * e.x_hint, fy: H * e.y_hint,
|
fx: W * e.x_hint, fy: H * e.y_hint,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
// Rayon proportionnel au nombre d'auteurs de l'ecole
|
||||||
|
const ecoleAuteurCounts = new Map<string, number>()
|
||||||
|
props.data.ecoles.forEach(e => ecoleAuteurCounts.set(e.id, 0))
|
||||||
|
props.data.auteurs.forEach(a => ecoleAuteurCounts.set(a.ecole_principale, (ecoleAuteurCounts.get(a.ecole_principale) ?? 0) + 1))
|
||||||
|
const ecoleRadius = (count: number) => Math.max(16, Math.min(36, 13 + count * 1.5))
|
||||||
|
|
||||||
const allNodes = [...ecoleFixedNodes, ...auteurNodes]
|
const allNodes = [...ecoleFixedNodes, ...auteurNodes]
|
||||||
|
|
||||||
if (simulation) simulation.stop()
|
if (simulation) simulation.stop()
|
||||||
@@ -209,7 +215,7 @@ async function initGraph() {
|
|||||||
.force('link', d3.forceLink(links).id((d: any) => d.id).distance(85).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(-30))
|
.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' ? 12 : 0))
|
.force('collision', d3.forceCollide().radius((d: any) => d.type === 'ecole-fixed' ? ecoleRadius(ecoleAuteurCounts.get(d.ecoleId) ?? 0) + 4 : 12))
|
||||||
.force('forceX', d3.forceX<any>((d: any) => {
|
.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)
|
||||||
@@ -225,6 +231,33 @@ async function initGraph() {
|
|||||||
return H / 2
|
return H / 2
|
||||||
}).strength(0.15))
|
}).strength(0.15))
|
||||||
|
|
||||||
|
// ---- NOEUDS ECOLES visibles (couche 3.5) ----
|
||||||
|
// Cercles proportionnels au count d'auteurs, fixes aux centroids Bonpote, cliquables
|
||||||
|
const gEcoles = g.append('g').attr('class', 'ecoles-nodes')
|
||||||
|
ecoleFixedNodes.forEach(eNode => {
|
||||||
|
const ecole = ecoleMap.get(eNode.ecoleId)
|
||||||
|
if (!ecole) return
|
||||||
|
const count = ecoleAuteurCounts.get(eNode.ecoleId) ?? 0
|
||||||
|
const r = ecoleRadius(count)
|
||||||
|
gEcoles.append('circle')
|
||||||
|
.attr('cx', eNode.fx).attr('cy', eNode.fy).attr('r', r)
|
||||||
|
.attr('fill', ecole.color + '22').attr('stroke', ecole.color).attr('stroke-width', 2.5)
|
||||||
|
.attr('class', 'ecole-node').style('cursor', 'pointer')
|
||||||
|
.on('mouseenter', (e: any) => {
|
||||||
|
if (!tooltipRef.value) return
|
||||||
|
tooltipRef.value.innerHTML = `<strong>${ecole.label}</strong> <span style="opacity:0.6;font-size:0.7rem;">${count} auteur${count > 1 ? 's' : ''}</span><br><span style="opacity:0.75;font-size:0.72rem;">${ecole.description}</span>`
|
||||||
|
tooltipRef.value.style.opacity = '1'
|
||||||
|
})
|
||||||
|
.on('mousemove', (e: any) => {
|
||||||
|
if (!tooltipRef.value || !svgEl) return
|
||||||
|
const rect = (svgEl as HTMLElement).getBoundingClientRect()
|
||||||
|
tooltipRef.value.style.left = (e.clientX - rect.left + 14) + 'px'
|
||||||
|
tooltipRef.value.style.top = (e.clientY - rect.top - 10) + 'px'
|
||||||
|
})
|
||||||
|
.on('mouseleave', () => { if (tooltipRef.value) tooltipRef.value.style.opacity = '0' })
|
||||||
|
.on('click', (e: any) => { e.stopPropagation(); emit('select-ecole', eNode.ecoleId) })
|
||||||
|
})
|
||||||
|
|
||||||
// ---- LIENS APPARTENANCE (couche 4) ----
|
// ---- LIENS APPARTENANCE (couche 4) ----
|
||||||
const gLinks = g.append('g').attr('class', 'links-appartenance')
|
const gLinks = g.append('g').attr('class', 'links-appartenance')
|
||||||
d3LinkSel = gLinks.selectAll('line').data(links).join('line')
|
d3LinkSel = gLinks.selectAll('line').data(links).join('line')
|
||||||
@@ -353,6 +386,13 @@ defineExpose({ triggerResize })
|
|||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ecole-node {
|
||||||
|
transition: opacity 0.15s, r 0.15s;
|
||||||
|
}
|
||||||
|
.ecole-node:hover {
|
||||||
|
opacity: 0.75;
|
||||||
|
}
|
||||||
|
|
||||||
/* ---- Labels ecoles : calque separe NON-blurre (Phase 8.D) ---- */
|
/* ---- Labels ecoles : calque separe NON-blurre (Phase 8.D) ---- */
|
||||||
.voronoi-labels {
|
.voronoi-labels {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
|||||||
@@ -20,7 +20,6 @@
|
|||||||
<div class="flex items-center justify-between px-4 py-3 shrink-0" style="border-bottom:1px solid var(--nav-bg-alt);background:var(--nav-bg);">
|
<div 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>
|
<div>
|
||||||
<p class="text-sm font-bold" style="color:var(--nav-text);">RAG Pensees Ecologiques</p>
|
<p class="text-sm font-bold" style="color:var(--nav-text);">RAG Pensees Ecologiques</p>
|
||||||
<p class="text-xs" style="color:var(--nav-text-muted);">{{ corpusCount }} auteurs ingeres</p>
|
|
||||||
</div>
|
</div>
|
||||||
<button @click="open = false" class="flex items-center justify-center w-7 h-7 rounded-full hover:opacity-70"
|
<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">
|
style="background:var(--nav-bg-alt);color:var(--nav-text-muted);" aria-label="Fermer">
|
||||||
@@ -119,7 +118,6 @@
|
|||||||
<div class="flex items-center justify-between px-4 py-2 shrink-0" style="border-bottom:1px solid var(--nav-bg-alt);background:var(--nav-bg);">
|
<div class="flex items-center justify-between px-4 py-2 shrink-0" style="border-bottom:1px solid var(--nav-bg-alt);background:var(--nav-bg);">
|
||||||
<div>
|
<div>
|
||||||
<p class="text-sm font-bold" style="color:var(--nav-text);">RAG Pensees Ecologiques</p>
|
<p class="text-sm font-bold" style="color:var(--nav-text);">RAG Pensees Ecologiques</p>
|
||||||
<p class="text-xs" style="color:var(--nav-text-muted);">{{ corpusCount }} auteurs ingeres</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
101
components/FicheEcole.vue
Normal file
101
components/FicheEcole.vue
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
<template>
|
||||||
|
<Teleport to="body">
|
||||||
|
<Transition name="backdrop">
|
||||||
|
<div v-if="open && ecole" class="fixed inset-0 z-[1500]" style="background: rgba(26,34,56,0.55);" @click="emit('close')" aria-hidden="true" />
|
||||||
|
</Transition>
|
||||||
|
<Transition name="modal">
|
||||||
|
<div v-if="open && ecole" class="fixed z-[1501] left-1/2 flex flex-col"
|
||||||
|
style="top:50%;transform:translate(-50%,-50%);width:min(540px,94vw);max-height:85vh;background:var(--nav-bg);border-radius:14px;box-shadow:0 16px 64px rgba(26,34,56,0.28);overflow:hidden;"
|
||||||
|
role="dialog" aria-modal="true">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="flex items-start justify-between px-5 py-4 shrink-0"
|
||||||
|
:style="`border-bottom: 3px solid ${ecole.color}; background: var(--nav-surface);`">
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<span class="px-2 py-0.5 rounded-full text-xs font-semibold" :style="`background:${ecole.color}22;color:${ecole.color};`">Ecole de pensee</span>
|
||||||
|
<h2 class="mt-2 font-bold text-lg leading-tight" style="color:var(--nav-text);">{{ ecole.label }}</h2>
|
||||||
|
<p class="text-sm mt-1 leading-relaxed" style="color:var(--nav-text-muted);">{{ ecole.description }}</p>
|
||||||
|
</div>
|
||||||
|
<button @click="emit('close')" class="ml-3 shrink-0 flex items-center justify-center w-8 h-8 rounded-full hover:opacity-70"
|
||||||
|
style="background:var(--nav-bg-alt);color:var(--nav-text-muted);" aria-label="Fermer">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round">
|
||||||
|
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<!-- Body -->
|
||||||
|
<div class="flex-1 overflow-y-auto px-5 py-4 flex flex-col gap-4">
|
||||||
|
<div v-if="auteursIngeres.length">
|
||||||
|
<p class="text-xs font-bold uppercase tracking-widest mb-2" style="color:var(--nav-text-muted);">Dans le RAG ({{ auteursIngeres.length }})</p>
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
<button v-for="a in auteursIngeres" :key="a.id"
|
||||||
|
class="px-2.5 py-1 rounded-full text-xs font-medium hover:opacity-80 transition-opacity"
|
||||||
|
:style="`background:${ecole.color}22;color:${ecole.color};border:1px solid ${ecole.color}44;cursor:pointer;`"
|
||||||
|
@click="onSelectAuteur(a.id)">
|
||||||
|
{{ a.nom }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="auteursNonIngeres.length">
|
||||||
|
<p class="text-xs font-bold uppercase tracking-widest mb-2" style="color:var(--nav-text-muted);">Presents dans Bonpote, pas encore dans le RAG ({{ auteursNonIngeres.length }})</p>
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
<span v-for="a in auteursNonIngeres" :key="a.id"
|
||||||
|
class="px-2.5 py-1 rounded-full text-xs"
|
||||||
|
style="background:var(--nav-bg-alt);color:var(--nav-text-muted);">
|
||||||
|
{{ a.nom }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Footer -->
|
||||||
|
<div class="shrink-0 px-5 py-3 border-t" style="border-color:var(--nav-bg-alt);">
|
||||||
|
<button @click="emit('interroger-ecole', ecoleId!)" class="w-full py-2.5 rounded-lg text-sm font-semibold hover:opacity-80"
|
||||||
|
:style="`background:${ecole.color};color:white;`">
|
||||||
|
Interroger le RAG sur {{ ecole.label }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
</Teleport>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
interface AuteurData { id: string; nom: string; ecoles: string[]; ecole_principale: string; ingere: boolean }
|
||||||
|
interface EcoleData { id: string; label: string; description: string; color: string }
|
||||||
|
interface PenseesData { ecoles: EcoleData[]; auteurs: AuteurData[] }
|
||||||
|
|
||||||
|
const props = defineProps<{ open: boolean; ecoleId: string | null; data: PenseesData | null }>()
|
||||||
|
const emit = defineEmits<{ close: []; 'select-auteur': [auteurId: string]; 'interroger-ecole': [ecoleId: string] }>()
|
||||||
|
|
||||||
|
const ecole = computed<EcoleData | null>(() => {
|
||||||
|
if (!props.ecoleId || !props.data) return null
|
||||||
|
return props.data.ecoles.find(e => e.id === props.ecoleId) ?? null
|
||||||
|
})
|
||||||
|
|
||||||
|
const auteursIngeres = computed(() => {
|
||||||
|
if (!props.ecoleId || !props.data) return []
|
||||||
|
return props.data.auteurs.filter(a => a.ecole_principale === props.ecoleId && (a as any).ingere)
|
||||||
|
})
|
||||||
|
|
||||||
|
const auteursNonIngeres = computed(() => {
|
||||||
|
if (!props.ecoleId || !props.data) return []
|
||||||
|
return props.data.auteurs.filter(a => a.ecole_principale === props.ecoleId && !(a as any).ingere)
|
||||||
|
})
|
||||||
|
|
||||||
|
function onSelectAuteur(id: string) {
|
||||||
|
emit('close')
|
||||||
|
emit('select-auteur', id)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onKey(e: KeyboardEvent) { if (e.key === 'Escape' && props.open) emit('close') }
|
||||||
|
onMounted(() => window.addEventListener('keydown', onKey))
|
||||||
|
onUnmounted(() => window.removeEventListener('keydown', onKey))
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.backdrop-enter-active,.backdrop-leave-active { transition: opacity 0.2s; }
|
||||||
|
.backdrop-enter-from,.backdrop-leave-to { opacity: 0; }
|
||||||
|
.modal-enter-active { transition: opacity 0.2s, transform 0.22s cubic-bezier(0.34,1.56,0.64,1); }
|
||||||
|
.modal-leave-active { transition: opacity 0.18s, transform 0.18s ease-in; }
|
||||||
|
.modal-enter-from { opacity: 0; transform: translate(-50%,-48%) scale(0.94); }
|
||||||
|
.modal-leave-to { opacity: 0; transform: translate(-50%,-48%) scale(0.96); }
|
||||||
|
</style>
|
||||||
@@ -16,6 +16,7 @@ export default defineNuxtConfig({
|
|||||||
commentTableId: process.env.COMMENT_TABLE_ID || process.env.AVIS_TABLE_ID,
|
commentTableId: process.env.COMMENT_TABLE_ID || process.env.AVIS_TABLE_ID,
|
||||||
statsTableId: process.env.STATS_TABLE_ID || 'mbbq7n47ixy19mc',
|
statsTableId: process.env.STATS_TABLE_ID || 'mbbq7n47ixy19mc',
|
||||||
mistralApiKey: process.env.MISTRAL_API_KEY,
|
mistralApiKey: process.env.MISTRAL_API_KEY,
|
||||||
|
nebiusApiKey: process.env.NEBIUS_API_KEY,
|
||||||
redisUrl: process.env.REDIS_URL || 'redis://127.0.0.1:6379',
|
redisUrl: process.env.REDIS_URL || 'redis://127.0.0.1:6379',
|
||||||
resendApiKey: process.env.RESEND_API_KEY,
|
resendApiKey: process.env.RESEND_API_KEY,
|
||||||
emailJules: process.env.EMAIL_JULES || 'jules@trans-former.fr',
|
emailJules: process.env.EMAIL_JULES || 'jules@trans-former.fr',
|
||||||
|
|||||||
114
pages/media.vue
114
pages/media.vue
@@ -6,11 +6,25 @@
|
|||||||
|
|
||||||
<!-- Header onglet -->
|
<!-- Header onglet -->
|
||||||
<div class="shrink-0 px-5 py-3"
|
<div class="shrink-0 px-5 py-3"
|
||||||
style="background: var(--nav-surface); border-bottom: 1px solid var(--nav-bg-alt);">
|
style="background: var(--nav-surface); border-bottom: 1px solid var(--nav-bg-alt); display:flex; align-items:center; justify-content:space-between; gap:12px;">
|
||||||
<h1 class="font-bold text-base" style="color: var(--nav-text);">ATIS Media</h1>
|
<div>
|
||||||
<p class="text-xs mt-0.5" style="color: var(--nav-text-muted);">
|
<h1 class="font-bold text-base" style="color: var(--nav-text);">ATIS Media</h1>
|
||||||
{{ corpusCount }} auteurs ingeres dans le RAG - carte FRACAS Bonpote V2
|
<p class="text-xs mt-0.5" style="color: var(--nav-text-muted);">
|
||||||
</p>
|
{{ corpusCount }} auteurs ingeres dans le RAG -
|
||||||
|
<a href="https://bonpote.com/wp-content/uploads/2024/10/FRACAS_BONPOTE_CARTE_VERSO_V2-OCT2024.pdf"
|
||||||
|
target="_blank" rel="noopener"
|
||||||
|
style="color: var(--nav-primary, #3b6ea5); text-decoration: underline; text-underline-offset: 2px;">
|
||||||
|
carte FRACAS Bonpote V2
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
@click="ragInfoOpen = true"
|
||||||
|
title="A propos du RAG FRACAS"
|
||||||
|
style="width:26px;height:26px;border-radius:50%;border:1.5px solid var(--nav-text-muted);color:var(--nav-text-muted);font-size:0.72rem;font-weight:700;cursor:pointer;flex-shrink:0;background:var(--nav-bg-alt);display:flex;align-items:center;justify-content:center;"
|
||||||
|
aria-label="A propos du RAG">
|
||||||
|
i
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Conteneur split / plein ecran -->
|
<!-- Conteneur split / plein ecran -->
|
||||||
@@ -32,6 +46,7 @@
|
|||||||
:data="penseesData"
|
:data="penseesData"
|
||||||
:active="true"
|
:active="true"
|
||||||
@select-auteur="onSelectAuteur"
|
@select-auteur="onSelectAuteur"
|
||||||
|
@select-ecole="onSelectEcole"
|
||||||
/>
|
/>
|
||||||
<template #fallback>
|
<template #fallback>
|
||||||
<div class="w-full h-full flex items-center justify-center" style="color: var(--nav-text-muted);">
|
<div class="w-full h-full flex items-center justify-center" style="color: var(--nav-text-muted);">
|
||||||
@@ -116,6 +131,61 @@
|
|||||||
@interroger-rag="onInterrogerRag"
|
@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/wp-content/uploads/2024/10/FRACAS_BONPOTE_CARTE_VERSO_V2-OCT2024.pdf" target="_blank" rel="noopener" style="text-decoration:underline;">reference 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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -133,6 +203,9 @@ const DEFAULT_SPLIT_RATIO = 0.66
|
|||||||
|
|
||||||
const ficheOpen = ref(false)
|
const ficheOpen = ref(false)
|
||||||
const ficheAuteurId = ref<string | null>(null)
|
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 chatbotAuteur = ref<string | null>(null)
|
||||||
const penseesData = ref<PenseesData | null>(null)
|
const penseesData = ref<PenseesData | null>(null)
|
||||||
const layoutMode = ref<LayoutMode>('split')
|
const layoutMode = ref<LayoutMode>('split')
|
||||||
@@ -179,7 +252,6 @@ function onHandleMouseup() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
// Restaurer le mode de layout depuis localStorage
|
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
const saved = localStorage.getItem(STORAGE_KEY) as LayoutMode | null
|
const saved = localStorage.getItem(STORAGE_KEY) as LayoutMode | null
|
||||||
if (saved && ['split', 'carte-full', 'chatbot-full'].includes(saved)) {
|
if (saved && ['split', 'carte-full', 'chatbot-full'].includes(saved)) {
|
||||||
@@ -189,6 +261,11 @@ onMounted(async () => {
|
|||||||
if (!isNaN(savedRatio) && savedRatio >= 0.20 && savedRatio <= 0.80) {
|
if (!isNaN(savedRatio) && savedRatio >= 0.20 && savedRatio <= 0.80) {
|
||||||
splitRatio.value = savedRatio
|
splitRatio.value = savedRatio
|
||||||
}
|
}
|
||||||
|
// Afficher le popup info RAG a la premiere visite
|
||||||
|
if (!localStorage.getItem('rag-fracas-info-seen')) {
|
||||||
|
ragInfoOpen.value = true
|
||||||
|
localStorage.setItem('rag-fracas-info-seen', '1')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
penseesData.value = await $fetch<PenseesData>('/data/auteurs-pensees.json')
|
penseesData.value = await $fetch<PenseesData>('/data/auteurs-pensees.json')
|
||||||
@@ -223,6 +300,23 @@ function onSelectAuteur(id: string) {
|
|||||||
chatbotAuteur.value = null
|
chatbotAuteur.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onSelectEcole(id: string) {
|
||||||
|
ficheEcoleId.value = id
|
||||||
|
ficheEcoleOpen.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSelectAuteurFromEcole(auteurId: string) {
|
||||||
|
ficheEcoleOpen.value = false
|
||||||
|
onSelectAuteur(auteurId)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onInterrogerEcole(ecoleId: string) {
|
||||||
|
ficheEcoleOpen.value = false
|
||||||
|
const ecole = penseesData.value?.ecoles.find(e => e.id === ecoleId)
|
||||||
|
chatbotAuteur.value = ecole?.label ?? null
|
||||||
|
if (layoutMode.value === 'carte-full') setLayoutMode('split')
|
||||||
|
}
|
||||||
|
|
||||||
function onInterrogerRag(auteurId: string) {
|
function onInterrogerRag(auteurId: string) {
|
||||||
ficheOpen.value = false
|
ficheOpen.value = false
|
||||||
const auteur = penseesData.value?.auteurs.find(a => a.id === auteurId)
|
const auteur = penseesData.value?.auteurs.find(a => a.id === auteurId)
|
||||||
@@ -392,6 +486,14 @@ useHead({ title: 'AEP - Media - Carte FRACAS Bonpote' })
|
|||||||
overflow: hidden;
|
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) --- */
|
/* --- Responsive mobile (<768px) --- */
|
||||||
/* Stack vertical : carte 60vh + chatbot 40vh en mode split */
|
/* Stack vertical : carte 60vh + chatbot 40vh en mode split */
|
||||||
@media (max-width: 767px) {
|
@media (max-width: 767px) {
|
||||||
|
|||||||
@@ -82,18 +82,18 @@ export default defineEventHandler(async (event) => {
|
|||||||
|
|
||||||
const systemPrompt = SYSTEM_PROMPT.replace('{{STRUCTURES_JSON}}', JSON.stringify(context, null, 0))
|
const systemPrompt = SYSTEM_PROMPT.replace('{{STRUCTURES_JSON}}', JSON.stringify(context, null, 0))
|
||||||
|
|
||||||
const mistralApiKey = config.mistralApiKey as string
|
const nebiusApiKey = config.nebiusApiKey as string
|
||||||
if (!mistralApiKey) throw createError({ statusCode: 500, message: 'Clé API Mistral manquante.' })
|
if (!nebiusApiKey) throw createError({ statusCode: 500, message: 'Clé API Nebius manquante.' })
|
||||||
|
|
||||||
let mistralRaw: string
|
let mistralRaw: string
|
||||||
try {
|
try {
|
||||||
const res = await $fetch<{ choices: { message: { content: string } }[] }>(
|
const res = await $fetch<{ choices: { message: { content: string } }[] }>(
|
||||||
'https://api.mistral.ai/v1/chat/completions',
|
'https://api.tokenfactory.nebius.com/v1/chat/completions',
|
||||||
{
|
{
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { Authorization: `Bearer ${mistralApiKey}`, 'Content-Type': 'application/json' },
|
headers: { Authorization: `Bearer ${nebiusApiKey}`, 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
model: 'mistral-small-latest',
|
model: 'deepseek-ai/DeepSeek-V3.2',
|
||||||
temperature: 0.3,
|
temperature: 0.3,
|
||||||
max_tokens: 700,
|
max_tokens: 700,
|
||||||
response_format: { type: 'json_object' },
|
response_format: { type: 'json_object' },
|
||||||
|
|||||||
@@ -91,20 +91,20 @@ export default defineEventHandler(async (event) => {
|
|||||||
|
|
||||||
const systemPrompt = SYSTEM_PROMPT.replace('{{PLATEFORMES_JSON}}', JSON.stringify(context, null, 0))
|
const systemPrompt = SYSTEM_PROMPT.replace('{{PLATEFORMES_JSON}}', JSON.stringify(context, null, 0))
|
||||||
|
|
||||||
const mistralApiKey = config.mistralApiKey as string
|
const nebiusApiKey = config.nebiusApiKey as string
|
||||||
if (!mistralApiKey) {
|
if (!nebiusApiKey) {
|
||||||
throw createError({ statusCode: 500, statusMessage: 'Clé API Mistral manquante.' })
|
throw createError({ statusCode: 500, statusMessage: 'Clé API Nebius manquante.' })
|
||||||
}
|
}
|
||||||
|
|
||||||
let mistralRaw: string
|
let mistralRaw: string
|
||||||
try {
|
try {
|
||||||
const res = await $fetch<{ choices: { message: { content: string } }[] }>(
|
const res = await $fetch<{ choices: { message: { content: string } }[] }>(
|
||||||
'https://api.mistral.ai/v1/chat/completions',
|
'https://api.tokenfactory.nebius.com/v1/chat/completions',
|
||||||
{
|
{
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { Authorization: `Bearer ${mistralApiKey}`, 'Content-Type': 'application/json' },
|
headers: { Authorization: `Bearer ${nebiusApiKey}`, 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
model: 'mistral-small-latest',
|
model: 'deepseek-ai/DeepSeek-V3.2',
|
||||||
temperature: 0.3,
|
temperature: 0.3,
|
||||||
max_tokens: 700,
|
max_tokens: 700,
|
||||||
response_format: { type: 'json_object' },
|
response_format: { type: 'json_object' },
|
||||||
|
|||||||
@@ -145,19 +145,22 @@ export default defineEventHandler(async (event) => {
|
|||||||
|
|
||||||
const systemPrompt = SYSTEM_PROMPT_V2.replace('{{CONTEXTE_RAG}}', contextStr)
|
const systemPrompt = SYSTEM_PROMPT_V2.replace('{{CONTEXTE_RAG}}', contextStr)
|
||||||
|
|
||||||
// 7. Mistral Small - génération réponse
|
// 7. Nebius DeepSeek-V3.2 - génération réponse
|
||||||
|
const nebiusApiKey = config.nebiusApiKey as string
|
||||||
|
if (!nebiusApiKey) throw createError({ statusCode: 500, statusMessage: 'Clé API Nebius manquante.' })
|
||||||
|
|
||||||
let mistralRaw: string
|
let mistralRaw: string
|
||||||
try {
|
try {
|
||||||
const mistralRes = await $fetch<{
|
const nebiusRes = await $fetch<{
|
||||||
choices: { message: { content: string } }[]
|
choices: { message: { content: string } }[]
|
||||||
}>('https://api.mistral.ai/v1/chat/completions', {
|
}>('https://api.tokenfactory.nebius.com/v1/chat/completions', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${mistralApiKey}`,
|
Authorization: `Bearer ${nebiusApiKey}`,
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
model: 'mistral-small-latest',
|
model: 'deepseek-ai/DeepSeek-V3.2',
|
||||||
temperature: 0.3,
|
temperature: 0.3,
|
||||||
max_tokens: 600,
|
max_tokens: 600,
|
||||||
response_format: { type: 'json_object' },
|
response_format: { type: 'json_object' },
|
||||||
@@ -167,10 +170,10 @@ export default defineEventHandler(async (event) => {
|
|||||||
]
|
]
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
mistralRaw = mistralRes.choices?.[0]?.message?.content ?? '{}'
|
mistralRaw = nebiusRes.choices?.[0]?.message?.content ?? '{}'
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error('[chatbot-v2] Erreur Mistral Small :', e?.message ?? e)
|
console.error('[chatbot-v2] Erreur Nebius DeepSeek :', e?.message ?? e)
|
||||||
throw createError({ statusCode: 502, statusMessage: 'Erreur appel Mistral Small.' })
|
throw createError({ statusCode: 502, statusMessage: 'Erreur appel Nebius DeepSeek.' })
|
||||||
}
|
}
|
||||||
|
|
||||||
// 8. Parse JSON
|
// 8. Parse JSON
|
||||||
|
|||||||
@@ -247,13 +247,13 @@ export default defineEventHandler(async (event) => {
|
|||||||
JSON.stringify(fichesContext, null, 0),
|
JSON.stringify(fichesContext, null, 0),
|
||||||
)
|
)
|
||||||
|
|
||||||
// 6. Appel Mistral Small
|
// 6. Appel Nebius DeepSeek-V3.2
|
||||||
const mistralApiKey = config.mistralApiKey as string
|
const nebiusApiKey = config.nebiusApiKey as string
|
||||||
|
|
||||||
if (!mistralApiKey) {
|
if (!nebiusApiKey) {
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 500,
|
statusCode: 500,
|
||||||
statusMessage: 'Clé API Mistral manquante.',
|
statusMessage: 'Clé API Nebius manquante.',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -262,17 +262,17 @@ export default defineEventHandler(async (event) => {
|
|||||||
let tokensOut = 0
|
let tokensOut = 0
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const mistralRes = await $fetch<{
|
const nebiusRes = await $fetch<{
|
||||||
choices: { message: { content: string } }[]
|
choices: { message: { content: string } }[]
|
||||||
usage?: { prompt_tokens: number; completion_tokens: number }
|
usage?: { prompt_tokens: number; completion_tokens: number }
|
||||||
}>('https://api.mistral.ai/v1/chat/completions', {
|
}>('https://api.tokenfactory.nebius.com/v1/chat/completions', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${mistralApiKey}`,
|
Authorization: `Bearer ${nebiusApiKey}`,
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
model: 'mistral-small-latest',
|
model: 'deepseek-ai/DeepSeek-V3.2',
|
||||||
temperature: 0.3,
|
temperature: 0.3,
|
||||||
max_tokens: 600,
|
max_tokens: 600,
|
||||||
response_format: { type: 'json_object' },
|
response_format: { type: 'json_object' },
|
||||||
@@ -283,11 +283,11 @@ export default defineEventHandler(async (event) => {
|
|||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
mistralRaw = mistralRes.choices?.[0]?.message?.content ?? '{}'
|
mistralRaw = nebiusRes.choices?.[0]?.message?.content ?? '{}'
|
||||||
tokensIn = mistralRes.usage?.prompt_tokens ?? 0
|
tokensIn = nebiusRes.usage?.prompt_tokens ?? 0
|
||||||
tokensOut = mistralRes.usage?.completion_tokens ?? 0
|
tokensOut = nebiusRes.usage?.completion_tokens ?? 0
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error('[chatbot] Erreur Mistral Small:', e?.message ?? e)
|
console.error('[chatbot] Erreur Nebius DeepSeek:', e?.message ?? e)
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 502,
|
statusCode: 502,
|
||||||
statusMessage: 'Erreur appel IA — réessaie dans quelques instants.',
|
statusMessage: 'Erreur appel IA — réessaie dans quelques instants.',
|
||||||
|
|||||||
Reference in New Issue
Block a user