- Positions x_hint/y_hint repos depuis OCR vision Sonnet sur PDF Bonpote V2 - Couleurs ecoles pastel Bonpote-aligned (10 clusters) - Labels Bonpote V2 longs : Ecologies libertaires + Ecologies anti-industrielles (ids JSON eco-anarchisme/technocritique inchanges, compat code) - CSS .voronoi-bg filter:blur(10px) + labels separes sur calque non-blurre - Grisage auteurs ingere:false : #bbb opacity 0.35 non-cliquables - Tooltip non-ingeres : "Present dans Bonpote, pas encore ingere dans le RAG ATIS." - D3 sim ajustee pour 171 auteurs : linkDistance 85, charge -30, forceXY 0.15 - corpusCount = auteurs ingeres uniquement (32, pas 171 total) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
415 lines
12 KiB
Vue
415 lines
12 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' : '',
|
|
]"
|
|
:style="layoutMode === 'split' ? { flexBasis: carteFlexBasis } : {}"
|
|
>
|
|
<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
|
|
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>
|
|
</div>
|
|
|
|
<!-- Poignee draggable (visible uniquement en mode split, pas sur mobile) -->
|
|
<div
|
|
v-if="layoutMode === 'split'"
|
|
class="split-handle"
|
|
@mousedown.prevent="onHandleMousedown"
|
|
title="Redimensionner"
|
|
>
|
|
<span class="split-handle-grip"></span>
|
|
</div>
|
|
|
|
<!-- Slot chatbot inline -->
|
|
<div
|
|
class="chatbot-slot"
|
|
:class="[
|
|
layoutMode === 'split' ? 'chatbot-split' : '',
|
|
layoutMode === 'chatbot-full' ? 'chatbot-full-mode' : '',
|
|
layoutMode === 'carte-full' ? 'chatbot-hidden' : '',
|
|
]"
|
|
:style="layoutMode === 'split' ? { flexBasis: chatbotFlexBasis } : {}"
|
|
>
|
|
<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 SPLIT_RATIO_KEY = 'media-split-ratio'
|
|
const DEFAULT_SPLIT_RATIO = 0.66
|
|
|
|
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)
|
|
|
|
// Ratio de la carte vs chatbot en mode split (0.2 a 0.8)
|
|
const splitRatio = ref(DEFAULT_SPLIT_RATIO)
|
|
const carteFlexBasis = computed(() => `${splitRatio.value * 100}%`)
|
|
const chatbotFlexBasis = computed(() => `${(1 - splitRatio.value) * 100}%`)
|
|
|
|
// 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
|
|
let dragStartY = 0
|
|
let dragStartRatio = DEFAULT_SPLIT_RATIO
|
|
let containerHeight = 0
|
|
|
|
function onHandleMousedown(e: MouseEvent) {
|
|
dragStartY = e.clientY
|
|
dragStartRatio = splitRatio.value
|
|
// Hauteur du layout-container (carte + handle + chatbot)
|
|
const container = (e.target as HTMLElement)?.closest('.layout-container') as HTMLElement | null
|
|
containerHeight = container ? container.clientHeight : window.innerHeight
|
|
|
|
window.addEventListener('mousemove', onHandleMousemove)
|
|
window.addEventListener('mouseup', onHandleMouseup)
|
|
}
|
|
|
|
function onHandleMousemove(e: MouseEvent) {
|
|
const delta = e.clientY - dragStartY
|
|
const newRatio = dragStartRatio + delta / containerHeight
|
|
splitRatio.value = Math.min(0.80, Math.max(0.20, newRatio))
|
|
}
|
|
|
|
function onHandleMouseup() {
|
|
window.removeEventListener('mousemove', onHandleMousemove)
|
|
window.removeEventListener('mouseup', onHandleMouseup)
|
|
if (typeof window !== 'undefined') {
|
|
localStorage.setItem(SPLIT_RATIO_KEY, String(splitRatio.value))
|
|
}
|
|
// Notifier D3 du resize apres relachement
|
|
cartePenseesRef.value?.triggerResize()
|
|
}
|
|
|
|
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
|
|
}
|
|
const savedRatio = parseFloat(localStorage.getItem(SPLIT_RATIO_KEY) ?? '')
|
|
if (!isNaN(savedRatio) && savedRatio >= 0.20 && savedRatio <= 0.80) {
|
|
splitRatio.value = savedRatio
|
|
}
|
|
}
|
|
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: opacity 0.2s ease;
|
|
}
|
|
|
|
.carte-split {
|
|
flex: 0 0 66%;
|
|
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);
|
|
}
|
|
|
|
/* --- Poignee draggable entre carte et chatbot --- */
|
|
.split-handle {
|
|
flex-shrink: 0;
|
|
height: 8px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
cursor: row-resize;
|
|
background: transparent;
|
|
position: relative;
|
|
z-index: 10;
|
|
user-select: none;
|
|
}
|
|
|
|
.split-handle:hover {
|
|
background: rgba(180, 170, 160, 0.18);
|
|
}
|
|
|
|
.split-handle-grip {
|
|
display: block;
|
|
width: 32px;
|
|
height: 4px;
|
|
border-radius: 2px;
|
|
background: repeating-linear-gradient(
|
|
to bottom,
|
|
rgba(160, 150, 140, 0.55) 0px,
|
|
rgba(160, 150, 140, 0.55) 1px,
|
|
transparent 1px,
|
|
transparent 3px
|
|
);
|
|
}
|
|
|
|
/* Masquer la poignee sur mobile (ratio fixe) */
|
|
@media (max-width: 767px) {
|
|
.split-handle {
|
|
display: none;
|
|
}
|
|
}
|
|
|
|
/* --- Slot chatbot --- */
|
|
.chatbot-slot {
|
|
overflow: hidden;
|
|
position: relative;
|
|
transition: opacity 0.2s ease;
|
|
border-top: 1px solid rgba(180, 170, 160, 0.28);
|
|
}
|
|
|
|
.chatbot-split {
|
|
flex: 0 0 34%;
|
|
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>
|