Files
nav-carte/pages/media.vue

330 lines
8.9 KiB
Vue

<template>
<div class="media-page" style="background: var(--nav-bg);">
<!-- ZONE PRINCIPALE (pleine largeur, pas de sidebar) -->
<main class="media-main">
<!-- Header onglet -->
<div class="shrink-0 px-5 py-3"
style="background: var(--nav-surface); border-bottom: 1px solid var(--nav-bg-alt);">
<h1 class="font-bold text-base" style="color: var(--nav-text);">ATIS Media</h1>
<p class="text-xs mt-0.5" style="color: var(--nav-text-muted);">
{{ corpusCount }} auteurs ingeres dans le RAG - carte FRACAS Bonpote V2
</p>
</div>
<!-- Conteneur split / plein ecran -->
<div class="layout-container">
<!-- Slot carte D3 -->
<div
class="carte-slot"
:class="[
layoutMode === 'split' ? 'carte-split' : '',
layoutMode === 'carte-full' ? 'carte-full' : '',
layoutMode === 'chatbot-full' ? 'carte-hidden' : '',
]"
>
<ClientOnly>
<CartePensees
ref="cartePenseesRef"
:data="penseesData"
:active="true"
@select-auteur="onSelectAuteur"
/>
<template #fallback>
<div class="w-full h-full flex items-center justify-center" style="color: var(--nav-text-muted);">
Chargement de la carte...
</div>
</template>
</ClientOnly>
</div>
<!-- Barre de toggle -->
<div class="layout-toggle-bar shrink-0">
<button
@click="setLayoutMode('carte-full')"
:class="{ active: layoutMode === 'carte-full' }"
class="toggle-btn"
title="Carte en plein ecran"
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<polyline points="15 3 21 3 21 9"/><polyline points="9 21 3 21 3 15"/>
<line x1="21" y1="3" x2="14" y2="10"/><line x1="3" y1="21" x2="10" y2="14"/>
</svg>
Carte plein ecran
</button>
<button
@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
v-if="layoutMode !== 'split'"
@click="setLayoutMode('split')"
class="toggle-btn toggle-btn-reset"
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>
</div>
<!-- Slot chatbot inline -->
<div
class="chatbot-slot"
:class="[
layoutMode === 'split' ? 'chatbot-split' : '',
layoutMode === 'chatbot-full' ? 'chatbot-full-mode' : '',
layoutMode === 'carte-full' ? 'chatbot-hidden' : '',
]"
>
<ClientOnly>
<ChatbotPensees :auteurContext="chatbotAuteur" :inline="true" />
</ClientOnly>
</div>
</div>
</main>
<!-- Fiche auteur modal -->
<FicheAuteur
:open="ficheOpen"
:auteurId="ficheAuteurId"
:data="penseesData"
@close="ficheOpen = false"
@interroger-rag="onInterrogerRag"
/>
</div>
</template>
<script setup lang="ts">
interface EcoleData { id: string; label: string; description: string; color: string; x_hint: number; y_hint: number }
interface LivreRag { slug: string; titre: string; annee: number; couches: string[] }
interface AuteurData { id: string; nom: string; dates: string; ecoles: string[]; ecole_principale: string; livres_rag: LivreRag[]; theses_cles: string[]; bio_courte: string }
interface PenseesData { meta: any; ecoles: EcoleData[]; auteurs: AuteurData[] }
type LayoutMode = 'split' | 'carte-full' | 'chatbot-full'
const STORAGE_KEY = 'media-layout-mode'
const ficheOpen = ref(false)
const ficheAuteurId = ref<string | null>(null)
const chatbotAuteur = ref<string | null>(null)
const penseesData = ref<PenseesData | null>(null)
const layoutMode = ref<LayoutMode>('split')
const cartePenseesRef = ref<{ triggerResize: () => void } | null>(null)
const corpusCount = computed(() => penseesData.value?.auteurs.length ?? 0)
onMounted(async () => {
// Restaurer le mode de layout depuis localStorage
if (typeof window !== 'undefined') {
const saved = localStorage.getItem(STORAGE_KEY) as LayoutMode | null
if (saved && ['split', 'carte-full', 'chatbot-full'].includes(saved)) {
layoutMode.value = saved
}
}
try {
penseesData.value = await $fetch<PenseesData>('/data/auteurs-pensees.json')
} catch (e) {
console.error('Erreur chargement auteurs-pensees.json', e)
}
})
// Persister + reset D3 apres transition
function setLayoutMode(mode: LayoutMode) {
layoutMode.value = mode
if (typeof window !== 'undefined') {
localStorage.setItem(STORAGE_KEY, mode)
}
// Restart simulation D3 apres la fin de la transition CSS (300ms)
if (mode !== 'chatbot-full') {
setTimeout(() => {
cartePenseesRef.value?.triggerResize()
}, 350)
}
}
watch(layoutMode, (v) => {
if (typeof window !== 'undefined') {
localStorage.setItem(STORAGE_KEY, v)
}
})
function onSelectAuteur(id: string) {
ficheAuteurId.value = id
ficheOpen.value = true
chatbotAuteur.value = null
}
function onInterrogerRag(auteurId: string) {
ficheOpen.value = false
const auteur = penseesData.value?.auteurs.find(a => a.id === auteurId)
chatbotAuteur.value = auteur?.nom ?? null
// Basculer en split pour que le chatbot soit visible
if (layoutMode.value === 'carte-full') {
setLayoutMode('split')
}
}
useHead({ title: 'AEP - Media - Carte FRACAS Bonpote' })
</script>
<style scoped>
/* Page container : flex column, prend toute la hauteur viewport */
.media-page {
display: flex;
height: 100%;
overflow: hidden;
}
.media-main {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
position: relative;
}
/* Conteneur des slots carte + toggle + chatbot */
.layout-container {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
min-height: 0;
}
/* --- Slot carte --- */
.carte-slot {
overflow: hidden;
position: relative;
transition: flex-basis 0.3s ease, height 0.3s ease, opacity 0.2s ease;
}
.carte-split {
flex: 2 1 0;
min-height: 0;
opacity: 1;
}
.carte-full {
flex: 1 1 100%;
min-height: 0;
opacity: 1;
}
.carte-hidden {
flex: 0 0 0;
height: 0;
opacity: 0;
overflow: hidden;
}
/* --- Barre de toggle --- */
.layout-toggle-bar {
flex-shrink: 0;
display: flex;
align-items: center;
gap: 6px;
padding: 4px 12px;
background: var(--nav-bg);
border-top: 1px solid rgba(180, 170, 160, 0.22);
border-bottom: 1px solid rgba(180, 170, 160, 0.22);
min-height: 38px;
}
.toggle-btn {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 4px 10px;
border-radius: 6px;
font-size: 0.75rem;
font-weight: 500;
cursor: pointer;
background: var(--nav-bg-alt);
color: var(--nav-text-muted);
border: 1px solid transparent;
transition: background 0.15s, color 0.15s, border-color 0.15s;
}
.toggle-btn:hover {
background: var(--nav-surface);
color: var(--nav-text);
}
.toggle-btn.active {
background: var(--nav-primary);
color: var(--nav-text-on-primary);
border-color: var(--nav-primary);
}
.toggle-btn-reset {
margin-left: auto;
background: var(--nav-surface);
color: var(--nav-text);
}
.toggle-btn-reset:hover {
background: var(--nav-bg-alt);
}
/* --- Slot chatbot --- */
.chatbot-slot {
overflow: hidden;
position: relative;
transition: flex-basis 0.3s ease, height 0.3s ease, opacity 0.2s ease;
border-top: 1px solid rgba(180, 170, 160, 0.28);
}
.chatbot-split {
flex: 1 1 0;
min-height: 0;
opacity: 1;
}
.chatbot-full-mode {
flex: 1 1 100%;
min-height: 0;
opacity: 1;
}
.chatbot-hidden {
flex: 0 0 0;
height: 0;
opacity: 0;
overflow: hidden;
}
/* --- Responsive mobile (<768px) --- */
/* Stack vertical : carte 60vh + chatbot 40vh en mode split */
@media (max-width: 767px) {
.carte-split {
flex: 0 0 60vh;
height: 60vh;
}
.chatbot-split {
flex: 0 0 calc(40vh - 38px);
height: calc(40vh - 38px);
}
.toggle-btn span,
.toggle-btn {
font-size: 0.7rem;
padding: 3px 7px;
}
}
</style>