fix(media): RAG visible + refonte interface bibliothèque pensées écologiques

- fix: penseesData chargé en interne dans MediaTabVisuel (bug prop jamais passée)
- feat: onglet renommé '📚 bibliothèque des pensées écologiques', suppression tab LightRAG backend
- feat: 'RAG backend' devient bouton inline dans toolbar → layout mode 'rag-backend'
- feat: fusion boutons 'Bonpote V2' + 'Carte FRACAS PDF' → contrôle unique avec tickbox intégré
- feat: iframe lightrag.trans-former.fr décommentée (DNS propagé)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Jules Neny
2026-05-22 14:52:20 +02:00
parent db8f614928
commit bd95c0f00d
3 changed files with 80 additions and 76 deletions

View File

@@ -8,16 +8,6 @@
</p> </p>
</div> </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 <iframe
src="https://lightrag.trans-former.fr/" 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;" style="width: 100%; height: 70vh; border: 1px solid var(--nav-bg-alt, #ddd); border-radius: 8px; margin-top: 1.5rem;"
@@ -25,6 +15,5 @@
sandbox="allow-same-origin allow-scripts" sandbox="allow-same-origin allow-scripts"
loading="lazy" loading="lazy"
/> />
-->
</div> </div>
</template> </template>

View File

@@ -10,7 +10,7 @@
:class="[ :class="[
layoutMode === 'split' ? 'carte-split' : '', layoutMode === 'split' ? 'carte-split' : '',
layoutMode === 'carte-full' ? 'carte-full' : '', layoutMode === 'carte-full' ? 'carte-full' : '',
layoutMode === 'chatbot-full' ? 'carte-hidden' : '', (layoutMode === 'chatbot-full' || layoutMode === 'bonpote' || layoutMode === 'rag-backend') ? 'carte-hidden' : '',
]" ]"
:style="layoutMode === 'split' ? { flexBasis: carteFlexBasis } : {}" :style="layoutMode === 'split' ? { flexBasis: carteFlexBasis } : {}"
style="position: relative;" style="position: relative;"
@@ -80,33 +80,46 @@
</svg> </svg>
Chatbot plein ecran Chatbot plein ecran
</button> </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 --> <!-- Groupe droit : carte des pensées + RAG backend -->
<label class="layer-toggle" title="Superposer la carte FRACAS Bonpote V2 en PDF"> <div style="margin-left: auto; display: flex; align-items: center; gap: 4px; flex-wrap: wrap;">
<input type="checkbox" v-model="showFracasPdf" /> <input
📄 Carte FRACAS (PDF) v-if="showFracasPdf"
</label> type="range"
<input min="0"
v-if="showFracasPdf" max="100"
type="range" v-model.number="fracasOpacity"
min="0" class="opacity-slider"
max="100" :title="`Opacité ${fracasOpacity}%`"
v-model.number="fracasOpacity" />
class="opacity-slider" <div class="carte-pensees-ctrl">
:title="`Opacité ${fracasOpacity}%`" <input
/> type="checkbox"
v-model="showFracasPdf"
class="fracas-check"
title="Superposer la carte FRACAS en PDF"
/>
<button
@click="setLayoutMode('bonpote')"
:class="{ active: layoutMode === 'bonpote' }"
class="toggle-btn carte-pensees-btn"
title="Carte des pensées écologiques — référence FRACAS Bonpote V2"
>
📗 carte des pensées écologiques
</button>
</div>
<button
@click="setLayoutMode('rag-backend')"
:class="{ active: layoutMode === 'rag-backend' }"
class="toggle-btn"
title="Interface LightRAG backend"
>
<svg width="13" height="13" 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="3"/><path d="M19.07 4.93a10 10 0 0 1 0 14.14M4.93 4.93a10 10 0 0 0 0 14.14"/>
</svg>
RAG backend
</button>
</div>
</div> </div>
<!-- Poignee draggable (visible uniquement en mode split, pas sur mobile) --> <!-- Poignee draggable (visible uniquement en mode split, pas sur mobile) -->
@@ -125,7 +138,7 @@
:class="[ :class="[
layoutMode === 'split' ? 'chatbot-split' : '', layoutMode === 'split' ? 'chatbot-split' : '',
layoutMode === 'chatbot-full' ? 'chatbot-full-mode' : '', layoutMode === 'chatbot-full' ? 'chatbot-full-mode' : '',
layoutMode === 'carte-full' ? 'chatbot-hidden' : '', (layoutMode === 'carte-full' || layoutMode === 'bonpote' || layoutMode === 'rag-backend') ? 'chatbot-hidden' : '',
]" ]"
:style="layoutMode === 'split' ? { flexBasis: chatbotFlexBasis } : {}" :style="layoutMode === 'split' ? { flexBasis: chatbotFlexBasis } : {}"
> >
@@ -189,6 +202,14 @@
</div> </div>
</div> </div>
<!-- Vue RAG backend -->
<div
v-if="layoutMode === 'rag-backend'"
style="flex: 1; overflow: hidden; display: flex; flex-direction: column;"
>
<MediaTabBackend />
</div>
</div> </div>
<!-- Fiche auteur modal --> <!-- Fiche auteur modal -->
@@ -264,7 +285,7 @@ interface LivreRag { slug: string; titre: string; annee: number; couches: string
interface AuteurData { id: string; nom: string; dates: string; ecoles: string[]; ecole_principale: string; livres_rag: LivreRag[]; theses_cles: string[]; bio_courte: string } interface 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[] } interface PenseesData { meta: any; ecoles: EcoleData[]; auteurs: AuteurData[] }
type LayoutMode = 'split' | 'carte-full' | 'chatbot-full' | 'bonpote' type LayoutMode = 'split' | 'carte-full' | 'chatbot-full' | 'bonpote' | 'rag-backend'
const STORAGE_KEY = 'media-layout-mode' const STORAGE_KEY = 'media-layout-mode'
const SPLIT_RATIO_KEY = 'media-split-ratio' const SPLIT_RATIO_KEY = 'media-split-ratio'
@@ -279,19 +300,15 @@ const chatbotAuteur = ref<string | null>(null)
const layoutMode = ref<LayoutMode>('split') const layoutMode = ref<LayoutMode>('split')
const cartePenseesRef = ref<{ triggerResize: () => void } | null>(null) const cartePenseesRef = ref<{ triggerResize: () => void } | null>(null)
// Toggle PDF FRACAS
const showFracasPdf = ref(false) const showFracasPdf = ref(false)
const fracasOpacity = ref(60) const fracasOpacity = ref(60)
// Props injectées depuis le parent (penseesData) const penseesData = ref<PenseesData | null>(null)
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 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}%`)
// Logique poignee draggable
let dragStartY = 0 let dragStartY = 0
let dragStartRatio = DEFAULT_SPLIT_RATIO let dragStartRatio = DEFAULT_SPLIT_RATIO
let containerHeight = 0 let containerHeight = 0
@@ -321,10 +338,10 @@ function onHandleMouseup() {
cartePenseesRef.value?.triggerResize() cartePenseesRef.value?.triggerResize()
} }
onMounted(() => { onMounted(async () => {
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', 'bonpote'].includes(saved)) { if (saved && (['split', 'carte-full', 'chatbot-full', 'bonpote', 'rag-backend'] as string[]).includes(saved)) {
layoutMode.value = saved layoutMode.value = saved
} }
const savedRatio = parseFloat(localStorage.getItem(SPLIT_RATIO_KEY) ?? '') const savedRatio = parseFloat(localStorage.getItem(SPLIT_RATIO_KEY) ?? '')
@@ -336,6 +353,11 @@ onMounted(() => {
localStorage.setItem('rag-fracas-info-seen', '1') localStorage.setItem('rag-fracas-info-seen', '1')
} }
} }
try {
penseesData.value = await $fetch<PenseesData>('/data/auteurs-pensees.json?v=4.2')
} catch (e) {
console.error('Erreur chargement auteurs-pensees.json', e)
}
}) })
function setLayoutMode(mode: LayoutMode) { function setLayoutMode(mode: LayoutMode) {
@@ -343,7 +365,7 @@ function setLayoutMode(mode: LayoutMode) {
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
localStorage.setItem(STORAGE_KEY, mode) localStorage.setItem(STORAGE_KEY, mode)
} }
if (mode !== 'chatbot-full') { if (mode === 'split' || mode === 'carte-full') {
setTimeout(() => { setTimeout(() => {
cartePenseesRef.value?.triggerResize() cartePenseesRef.value?.triggerResize()
}, 350) }, 350)
@@ -374,14 +396,14 @@ function onSelectAuteurFromEcole(auteurId: string) {
function onInterrogerEcole(ecoleId: string) { function onInterrogerEcole(ecoleId: string) {
ficheEcoleOpen.value = false ficheEcoleOpen.value = false
const ecole = props.penseesData?.ecoles.find(e => e.id === ecoleId) const ecole = penseesData.value?.ecoles.find(e => e.id === ecoleId)
chatbotAuteur.value = ecole?.label ?? null chatbotAuteur.value = ecole?.label ?? null
if (layoutMode.value === 'carte-full') setLayoutMode('split') if (layoutMode.value === 'carte-full') setLayoutMode('split')
} }
function onInterrogerRag(auteurId: string) { function onInterrogerRag(auteurId: string) {
ficheOpen.value = false ficheOpen.value = false
const auteur = props.penseesData?.auteurs.find(a => a.id === auteurId) const auteur = penseesData.value?.auteurs.find(a => a.id === auteurId)
chatbotAuteur.value = auteur?.nom ?? null chatbotAuteur.value = auteur?.nom ?? null
if (layoutMode.value === 'carte-full') { if (layoutMode.value === 'carte-full') {
setLayoutMode('split') setLayoutMode('split')
@@ -481,28 +503,28 @@ function onInterrogerRag(auteurId: string) {
border-color: var(--nav-primary); border-color: var(--nav-primary);
} }
/* --- Toggle layer PDF FRACAS --- */ /* --- Contrôle fusionné carte des pensées + tickbox --- */
.layer-toggle { .carte-pensees-ctrl {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: 5px; gap: 0;
padding: 4px 10px;
border-radius: 6px; border-radius: 6px;
font-size: 0.75rem; overflow: hidden;
font-weight: 500; border: 1px solid rgba(180, 170, 160, 0.3);
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"] { .fracas-check {
margin: 0; margin: 0 2px 0 7px;
cursor: pointer; cursor: pointer;
accent-color: var(--nav-primary, #3b6ea5);
} }
.carte-pensees-btn {
border-radius: 0;
border: none;
}
/* --- Slider opacité PDF --- */
.opacity-slider { .opacity-slider {
width: 80px; width: 80px;
cursor: pointer; cursor: pointer;

View File

@@ -5,24 +5,17 @@
:class="['subtab-btn', { active: tab === 'visuel' }]" :class="['subtab-btn', { active: tab === 'visuel' }]"
@click="tab = 'visuel'" @click="tab = 'visuel'"
> >
🌳 RAG visuel 📚 bibliothèque des pensées écologiques
</button>
<button
:class="['subtab-btn', { active: tab === 'backend' }]"
@click="tab = 'backend'"
>
LightRAG backend
</button> </button>
<button <button
:class="['subtab-btn', { active: tab === 'projets' }]" :class="['subtab-btn', { active: tab === 'projets' }]"
@click="tab = 'projets'" @click="tab = 'projets'"
> >
📚 Projets 📐 Projets
</button> </button>
</nav> </nav>
<MediaTabVisuel v-if="tab === 'visuel'" /> <MediaTabVisuel v-if="tab === 'visuel'" />
<MediaTabBackend v-else-if="tab === 'backend'" />
<MediaTabProjets v-else-if="tab === 'projets'" /> <MediaTabProjets v-else-if="tab === 'projets'" />
</div> </div>
</template> </template>
@@ -31,9 +24,9 @@
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
const tab = ref<'visuel' | 'backend' | 'projets'>( const tab = ref<'visuel' | 'projets'>(
(['visuel', 'backend', 'projets'].includes(route.query.tab as string) (['visuel', 'projets'].includes(route.query.tab as string)
? route.query.tab as 'visuel' | 'backend' | 'projets' ? route.query.tab as 'visuel' | 'projets'
: 'visuel') : 'visuel')
) )