6 Commits

Author SHA1 Message Date
Jules Neny
cd8fe9e258 fix(media): toolbar remise entre carte et chatbot + nav renommée
- fix: layout-toggle-bar à l'intérieur du layout-container (entre carte D3 et chatbot)
- fix: chatbot de nouveau visible en mode split
- feat: nav "Écosystème Entraide Architecture" → "Écosystème Entraide"

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 15:50:22 +02:00
Jules Neny
ea7c8cc91e fix(server): useEvent() → event param dans la server route auteurs-pensees
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 15:26:27 +02:00
Jules Neny
538c490e76 fix(media): carte D3 + chatbot restaurés + refonte toolbar + nav
- fix: server route /data/auteurs-pensees.json (contournement bug manifest Nitro)
- fix: contentView indépendant du layoutMode — boutons CARTE PRINCIPALE / bonpote / RAG backend ne modifient pas l'état carte-full/chatbot-full
- feat: bouton CARTE PRINCIPALE → restaure la vue D3 + chatbot split
- fix: /rag redirige vers /media (301)
- feat: nav "RAG en construction" → "recherche-média" lien /media

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 15:24:29 +02:00
Jules Neny
d584d04e3d merge(feat/outils-v1): page Outils V1 + bibliothèque pensées écologiques refonte
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 14:52:42 +02:00
Jules Neny
bd95c0f00d 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>
2026-05-22 14:52:20 +02:00
Jules Neny
db8f614928 fix(CartePensees): reformatage syntaxe - fichier condensé sur 1 ligne cassait le build Vite/Vue 2026-05-22 11:07:10 +02:00
7 changed files with 527 additions and 197 deletions

View File

@@ -34,7 +34,7 @@
class="nav-tab" class="nav-tab"
:class="{ 'nav-tab--active': route.path === '/' }" :class="{ 'nav-tab--active': route.path === '/' }"
> >
Écosystème Entraide Architecture Écosystème Entraide
</NuxtLink> </NuxtLink>
<NuxtLink <NuxtLink
to="/agences" to="/agences"
@@ -58,12 +58,11 @@
Codev Codev
</NuxtLink> </NuxtLink>
<NuxtLink <NuxtLink
to="/rag" to="/media"
class="nav-tab" class="nav-tab"
:class="{ 'nav-tab--active': route.path === '/rag' }" :class="{ 'nav-tab--active': route.path.startsWith('/media') }"
> >
RAG recherche-média
<span class="nav-tab-badge">en construction</span>
</NuxtLink> </NuxtLink>
</nav> </nav>

File diff suppressed because one or more lines are too long

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

@@ -1,11 +1,12 @@
<template> <template>
<div class="media-visuel"> <div class="media-visuel">
<!-- Conteneur split / plein ecran --> <!-- Conteneur principal : carte toolbar chatbot (ou bonpote/rag-backend) -->
<div class="layout-container"> <div class="layout-container">
<!-- Slot carte D3 --> <!-- SLOT CARTE D3 (mode carte uniquement) -->
<div <div
v-if="contentView === 'carte'"
class="carte-slot" class="carte-slot"
:class="[ :class="[
layoutMode === 'split' ? 'carte-split' : '', layoutMode === 'split' ? 'carte-split' : '',
@@ -44,74 +45,109 @@
</div> </div>
</div> </div>
<!-- Barre de toggle --> <!-- BARRE DE TOGGLE (entre carte et chatbot, toujours visible) -->
<div class="layout-toggle-bar shrink-0"> <div class="layout-toggle-bar shrink-0">
<button
@click="setLayoutMode('carte-full')"
:class="{ active: layoutMode === 'carte-full' }"
class="toggle-btn"
title="Carte en plein ecran"
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<polyline points="15 3 21 3 21 9"/><polyline points="9 21 3 21 3 15"/>
<line x1="21" y1="3" x2="14" y2="10"/><line x1="3" y1="21" x2="10" y2="14"/>
</svg>
Carte plein ecran
</button>
<button
v-if="layoutMode !== 'split'"
@click="setLayoutMode('split')"
class="toggle-btn"
title="Vue partagee"
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<rect x="3" y="3" width="18" height="18" rx="2"/><line x1="3" y1="12" x2="21" y2="12"/>
</svg>
Vue partagee
</button>
<button
@click="setLayoutMode('chatbot-full')"
:class="{ active: layoutMode === 'chatbot-full' }"
class="toggle-btn"
title="Chatbot en plein ecran"
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
</svg>
Chatbot plein ecran
</button>
<button
@click="setLayoutMode('bonpote')"
:class="{ active: layoutMode === 'bonpote' }"
class="toggle-btn"
title="A propos de la carte FRACAS Bonpote V2"
style="margin-left: auto;"
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<circle cx="12" cy="12" r="10"/><polyline points="12 8 12 12 14 14"/>
</svg>
Bonpote V2
</button>
<!-- Toggle PDF FRACAS --> <!-- Gauche : contrôles layout (seulement en mode carte) -->
<label class="layer-toggle" title="Superposer la carte FRACAS Bonpote V2 en PDF"> <template v-if="contentView === 'carte'">
<input type="checkbox" v-model="showFracasPdf" /> <button
📄 Carte FRACAS (PDF) @click="layoutMode = 'carte-full'"
</label> :class="{ active: layoutMode === 'carte-full' }"
<input class="toggle-btn"
v-if="showFracasPdf" title="Carte en plein ecran"
type="range" >
min="0" <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">
max="100" <polyline points="15 3 21 3 21 9"/><polyline points="9 21 3 21 3 15"/>
v-model.number="fracasOpacity" <line x1="21" y1="3" x2="14" y2="10"/><line x1="3" y1="21" x2="10" y2="14"/>
class="opacity-slider" </svg>
:title="`Opacité ${fracasOpacity}%`" Carte plein ecran
/> </button>
<button
v-if="layoutMode !== 'split'"
@click="layoutMode = 'split'"
class="toggle-btn"
title="Vue partagee"
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<rect x="3" y="3" width="18" height="18" rx="2"/><line x1="3" y1="12" x2="21" y2="12"/>
</svg>
Vue partagee
</button>
<button
@click="layoutMode = 'chatbot-full'"
:class="{ active: layoutMode === 'chatbot-full' }"
class="toggle-btn"
title="Chatbot plein ecran"
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
</svg>
Chatbot plein ecran
</button>
</template>
<!-- Droite : contrôles contenu (toujours, indépendants du layoutMode) -->
<div style="margin-left: auto; display: flex; align-items: center; gap: 4px; flex-wrap: wrap;">
<!-- Slider opacité PDF -->
<input
v-if="showFracasPdf && contentView === 'carte'"
type="range"
min="0"
max="100"
v-model.number="fracasOpacity"
class="opacity-slider"
:title="`Opacité ${fracasOpacity}%`"
/>
<!-- CARTE PRINCIPALE -->
<button
@click="showCarte"
:class="{ active: contentView === 'carte' }"
class="toggle-btn"
title="Vue principale : carte D3 + chatbot"
>
<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="2"/><path d="M12 2a10 10 0 0 0-7.07 17.07M12 2a10 10 0 0 1 7.07 17.07M3.34 7h17.32M3.34 17h17.32"/>
</svg>
CARTE PRINCIPALE
</button>
<!-- Tickbox PDF + carte des pensées -->
<div class="carte-pensees-ctrl">
<input
type="checkbox"
v-model="showFracasPdf"
class="fracas-check"
title="Superposer la carte FRACAS en PDF"
/>
<button
@click="contentView = 'bonpote'"
:class="{ active: contentView === '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>
<!-- RAG backend -->
<button
@click="contentView = 'rag-backend'"
:class="{ active: contentView === '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 (split uniquement) -->
<div <div
v-if="layoutMode === 'split'" v-if="contentView === 'carte' && layoutMode === 'split'"
class="split-handle" class="split-handle"
@mousedown.prevent="onHandleMousedown" @mousedown.prevent="onHandleMousedown"
title="Redimensionner" title="Redimensionner"
@@ -119,8 +155,9 @@
<span class="split-handle-grip"></span> <span class="split-handle-grip"></span>
</div> </div>
<!-- Slot chatbot inline --> <!-- SLOT CHATBOT (mode carte uniquement) -->
<div <div
v-if="contentView === 'carte'"
class="chatbot-slot" class="chatbot-slot"
:class="[ :class="[
layoutMode === 'split' ? 'chatbot-split' : '', layoutMode === 'split' ? 'chatbot-split' : '',
@@ -134,9 +171,9 @@
</ClientOnly> </ClientOnly>
</div> </div>
<!-- Vue Bonpote V2 --> <!-- VUE BONPOTE -->
<div <div
v-if="layoutMode === 'bonpote'" v-if="contentView === 'bonpote'"
class="flex-1 overflow-y-auto px-6 py-8" class="flex-1 overflow-y-auto px-6 py-8"
style="max-width: 680px; margin: 0 auto;" style="max-width: 680px; margin: 0 auto;"
> >
@@ -165,7 +202,7 @@
Telecharger le poster PDF (recto/verso) Telecharger le poster PDF (recto/verso)
</a> </a>
<button <button
@click="setLayoutMode('split')" @click="showCarte"
class="flex items-center gap-3 px-4 py-3 rounded-lg hover:opacity-80 transition-opacity text-left" class="flex items-center gap-3 px-4 py-3 rounded-lg hover:opacity-80 transition-opacity text-left"
style="background: var(--nav-bg-alt); color: var(--nav-text); font-size: 0.875rem; font-weight: 500; border: none; cursor: pointer;"> style="background: var(--nav-bg-alt); color: var(--nav-text); font-size: 0.875rem; font-weight: 500; border: none; cursor: pointer;">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
@@ -189,6 +226,14 @@
</div> </div>
</div> </div>
<!-- VUE RAG BACKEND -->
<div
v-if="contentView === 'rag-backend'"
style="flex: 1; overflow: hidden; display: flex; flex-direction: column;"
>
<MediaTabBackend />
</div>
</div> </div>
<!-- Fiche auteur modal --> <!-- Fiche auteur modal -->
@@ -264,9 +309,11 @@ 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'
type ContentView = 'carte' | 'bonpote' | 'rag-backend'
const STORAGE_KEY = 'media-layout-mode' const LAYOUT_KEY = 'media-layout-mode'
const CONTENT_KEY = 'media-content-view'
const SPLIT_RATIO_KEY = 'media-split-ratio' const SPLIT_RATIO_KEY = 'media-split-ratio'
const DEFAULT_SPLIT_RATIO = 0.66 const DEFAULT_SPLIT_RATIO = 0.66
@@ -276,32 +323,39 @@ const ficheEcoleOpen = ref(false)
const ficheEcoleId = ref<string | null>(null) const ficheEcoleId = ref<string | null>(null)
const ragInfoOpen = ref(false) const ragInfoOpen = ref(false)
const chatbotAuteur = ref<string | null>(null) const chatbotAuteur = ref<string | null>(null)
const layoutMode = ref<LayoutMode>('split') const layoutMode = ref<LayoutMode>('split')
const contentView = ref<ContentView>('carte')
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
function showCarte() {
contentView.value = 'carte'
layoutMode.value = 'split'
if (typeof window !== 'undefined') {
localStorage.setItem(CONTENT_KEY, 'carte')
localStorage.setItem(LAYOUT_KEY, 'split')
}
nextTick(() => cartePenseesRef.value?.triggerResize())
}
function onHandleMousedown(e: MouseEvent) { function onHandleMousedown(e: MouseEvent) {
dragStartY = e.clientY dragStartY = e.clientY
dragStartRatio = splitRatio.value dragStartRatio = splitRatio.value
const container = (e.target as HTMLElement)?.closest('.layout-container') as HTMLElement | null const container = (e.target as HTMLElement)?.closest('.layout-container') as HTMLElement | null
containerHeight = container ? container.clientHeight : window.innerHeight containerHeight = container ? container.clientHeight : window.innerHeight
window.addEventListener('mousemove', onHandleMousemove) window.addEventListener('mousemove', onHandleMousemove)
window.addEventListener('mouseup', onHandleMouseup) window.addEventListener('mouseup', onHandleMouseup)
} }
@@ -315,17 +369,19 @@ function onHandleMousemove(e: MouseEvent) {
function onHandleMouseup() { function onHandleMouseup() {
window.removeEventListener('mousemove', onHandleMousemove) window.removeEventListener('mousemove', onHandleMousemove)
window.removeEventListener('mouseup', onHandleMouseup) window.removeEventListener('mouseup', onHandleMouseup)
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') localStorage.setItem(SPLIT_RATIO_KEY, String(splitRatio.value))
localStorage.setItem(SPLIT_RATIO_KEY, String(splitRatio.value))
}
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 savedLayout = localStorage.getItem(LAYOUT_KEY) as LayoutMode | null
if (saved && ['split', 'carte-full', 'chatbot-full', 'bonpote'].includes(saved)) { if (savedLayout && (['split', 'carte-full', 'chatbot-full'] as string[]).includes(savedLayout)) {
layoutMode.value = saved layoutMode.value = savedLayout
}
const savedContent = localStorage.getItem(CONTENT_KEY) as ContentView | null
if (savedContent && (['carte', 'bonpote', 'rag-backend'] as string[]).includes(savedContent)) {
contentView.value = savedContent
} }
const savedRatio = parseFloat(localStorage.getItem(SPLIT_RATIO_KEY) ?? '') const savedRatio = parseFloat(localStorage.getItem(SPLIT_RATIO_KEY) ?? '')
if (!isNaN(savedRatio) && savedRatio >= 0.20 && savedRatio <= 0.80) { if (!isNaN(savedRatio) && savedRatio >= 0.20 && savedRatio <= 0.80) {
@@ -336,26 +392,24 @@ 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')
} catch (e) {
console.error('Erreur chargement auteurs-pensees.json', e)
}
}) })
function setLayoutMode(mode: LayoutMode) {
layoutMode.value = mode
if (typeof window !== 'undefined') {
localStorage.setItem(STORAGE_KEY, mode)
}
if (mode !== 'chatbot-full') {
setTimeout(() => {
cartePenseesRef.value?.triggerResize()
}, 350)
}
}
watch(layoutMode, (v) => { watch(layoutMode, (v) => {
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') localStorage.setItem(LAYOUT_KEY, v)
localStorage.setItem(STORAGE_KEY, v) if (v === 'split' || v === 'carte-full') {
setTimeout(() => cartePenseesRef.value?.triggerResize(), 350)
} }
}) })
watch(contentView, (v) => {
if (typeof window !== 'undefined') localStorage.setItem(CONTENT_KEY, v)
})
function onSelectAuteur(id: string) { function onSelectAuteur(id: string) {
ficheAuteurId.value = id ficheAuteurId.value = id
ficheOpen.value = true ficheOpen.value = true
@@ -374,18 +428,18 @@ 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 (contentView.value !== 'carte') showCarte()
else if (layoutMode.value === 'carte-full') layoutMode.value = '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 (contentView.value !== 'carte') showCarte()
setLayoutMode('split') else if (layoutMode.value === 'carte-full') layoutMode.value = 'split'
}
} }
</script> </script>
@@ -398,7 +452,6 @@ function onInterrogerRag(auteurId: string) {
min-height: 0; min-height: 0;
} }
/* Conteneur des slots carte + toggle + chatbot */
.layout-container { .layout-container {
flex: 1; flex: 1;
display: flex; display: flex;
@@ -441,7 +494,7 @@ function onInterrogerRag(auteurId: string) {
pointer-events: none; pointer-events: none;
} }
/* --- Barre de toggle --- */ /* --- Barre de toggle (entre carte et chatbot) --- */
.layout-toggle-bar { .layout-toggle-bar {
flex-shrink: 0; flex-shrink: 0;
display: flex; display: flex;
@@ -481,26 +534,25 @@ 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 --- */
.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;
} }
.opacity-slider { .opacity-slider {
@@ -509,7 +561,7 @@ function onInterrogerRag(auteurId: string) {
accent-color: var(--nav-primary, #3b6ea5); accent-color: var(--nav-primary, #3b6ea5);
} }
/* --- Poignee draggable entre carte et chatbot --- */ /* --- Poignee draggable --- */
.split-handle { .split-handle {
flex-shrink: 0; flex-shrink: 0;
height: 8px; height: 8px;
@@ -541,11 +593,8 @@ function onInterrogerRag(auteurId: string) {
); );
} }
/* Masquer la poignee sur mobile (ratio fixe) */
@media (max-width: 767px) { @media (max-width: 767px) {
.split-handle { .split-handle { display: none; }
display: none;
}
} }
/* --- Slot chatbot --- */ /* --- Slot chatbot --- */
@@ -583,19 +632,16 @@ function onInterrogerRag(auteurId: string) {
.modal-enter-from { opacity: 0; transform: translate(-50%,-48%) scale(0.94); } .modal-enter-from { opacity: 0; transform: translate(-50%,-48%) scale(0.94); }
.modal-leave-to { opacity: 0; transform: translate(-50%,-48%) scale(0.96); } .modal-leave-to { opacity: 0; transform: translate(-50%,-48%) scale(0.96); }
/* --- Responsive mobile (<768px) --- */ /* --- Mobile --- */
@media (max-width: 767px) { @media (max-width: 767px) {
.carte-split { .carte-split {
flex: 0 0 60vh; flex: 0 0 60vh;
height: 60vh; height: 60vh;
} }
.chatbot-split { .chatbot-split {
flex: 0 0 calc(40vh - 38px); flex: 0 0 calc(40vh - 38px);
height: calc(40vh - 38px); height: calc(40vh - 38px);
} }
.toggle-btn span,
.toggle-btn { .toggle-btn {
font-size: 0.7rem; font-size: 0.7rem;
padding: 3px 7px; padding: 3px 7px;

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')
) )

View File

@@ -1,38 +1,3 @@
<template>
<div class="flex flex-col items-center justify-center h-full gap-6" style="background: var(--nav-bg);">
<div class="text-center max-w-md px-6">
<div
class="inline-flex items-center justify-center w-14 h-14 rounded-2xl mb-5"
style="background: var(--nav-bg-alt);"
>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="color: var(--nav-text-muted);">
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/>
<polyline points="3.27 6.96 12 12.01 20.73 6.96"/>
<line x1="12" y1="22.08" x2="12" y2="12"/>
</svg>
</div>
<h1 class="text-2xl font-bold mb-3" style="color: var(--nav-text);">RAG Retrieval Augmented Generation</h1>
<p class="text-sm leading-relaxed mb-6" style="color: var(--nav-text-muted);">
Une base de connaissances interrogeable par IA textes, rapports, manifestes et ressources documentaires sur l'architecture d'écologie politique.
</p>
<p class="text-xs font-semibold uppercase tracking-widest mb-6" style="color: var(--nav-text-muted); opacity: 0.6;">
Bientôt disponible
</p>
<NuxtLink
to="/"
class="inline-flex items-center gap-2 px-4 py-2 rounded-xl text-sm font-semibold transition-all hover:opacity-80"
style="background: var(--nav-primary); color: var(--nav-text-on-primary);"
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" aria-hidden="true">
<line x1="19" y1="12" x2="5" y2="12"/>
<polyline points="12 19 5 12 12 5"/>
</svg>
Retour à l'écosystème
</NuxtLink>
</div>
</div>
</template>
<script setup lang="ts"> <script setup lang="ts">
useHead({ title: 'RAG AEP (bientôt disponible)' }) navigateTo('/media', { redirectCode: 301 })
</script> </script>

View File

@@ -0,0 +1,10 @@
import { readFileSync } from 'node:fs'
import { join } from 'node:path'
export default defineEventHandler((event) => {
const path = join(process.cwd(), 'public', 'data', 'auteurs-pensees.json')
const raw = readFileSync(path, 'utf-8')
setResponseHeader(event, 'content-type', 'application/json; charset=utf-8')
setResponseHeader(event, 'cache-control', 'public, max-age=300')
return raw
})