Compare commits
4 Commits
feat/outil
...
feat/outil
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
db8f614928 | ||
|
|
59cb81a055 | ||
|
|
91e3466ec6 | ||
|
|
cb75889231 |
372
components/CartePensees.vue
Normal file
372
components/CartePensees.vue
Normal file
@@ -0,0 +1,372 @@
|
|||||||
|
<template>
|
||||||
|
<div style="width: 100%; height: 100%; position: relative; background: #f5f3f0;">
|
||||||
|
<svg ref="svgRef" style="width: 100%; height: 100%;"></svg>
|
||||||
|
<div ref="tooltipRef" style="
|
||||||
|
position: absolute; pointer-events: none;
|
||||||
|
background: var(--nav-surface); border: 1px solid var(--nav-bg-alt);
|
||||||
|
border-radius: 6px; padding: 8px 12px; font-size: 0.78rem;
|
||||||
|
color: var(--nav-text); max-width: 240px; box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||||
|
opacity: 0; transition: opacity 0.15s; z-index: 100;
|
||||||
|
"></div>
|
||||||
|
</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; ingere: boolean; livres_rag: LivreRag[]; theses_cles: string[]; bio_courte: string; bio_courte_provisoire?: string }
|
||||||
|
interface PenseesData { ecoles: EcoleData[]; auteurs: AuteurData[] }
|
||||||
|
|
||||||
|
// Liens d'influence inter-ecoles (Phase 7 - matrice de filiation)
|
||||||
|
const LINKS_INFLUENCE = [
|
||||||
|
// filiations directes
|
||||||
|
{ source: 'eco-anarchisme', target: 'technocritique', auteurs_passerelle: ['Bookchin', 'Illich'], type: 'filiation' },
|
||||||
|
{ source: 'eco-anarchisme', target: 'decroissance', auteurs_passerelle: ['Latouche', 'Kropotkine'], type: 'filiation' },
|
||||||
|
{ source: 'ecosocialisme', target: 'decroissance', auteurs_passerelle: ['Saito', 'Gorz'], type: 'filiation' },
|
||||||
|
{ source: 'ecosocialisme', target: 'ecologies-decoloniales', auteurs_passerelle: ['Klein', 'Ferdinand'], type: 'filiation' },
|
||||||
|
{ source: 'ecofeminismes', target: 'ecologies-decoloniales', auteurs_passerelle: ['Shiva', 'Ouassak'], type: 'filiation' },
|
||||||
|
{ source: 'ecofeminismes', target: 'pensees-vivant', auteurs_passerelle: ['Haraway', 'Despret'], type: 'filiation' },
|
||||||
|
{ source: 'technocritique', target: 'decroissance', auteurs_passerelle: ['Ellul', 'Latouche'], type: 'filiation' },
|
||||||
|
{ source: 'decroissance', target: 'pensees-vivant', auteurs_passerelle: ['Servigne', 'Despret'], type: 'filiation' },
|
||||||
|
{ source: 'pensees-vivant', target: 'ethiques-environnementales', auteurs_passerelle: ['Naess', 'Latour'], type: 'filiation' },
|
||||||
|
{ source: 'ecosocialisme', target: 'eco-anarchisme', auteurs_passerelle: ['Gorz', 'Graeber'], type: 'filiation' },
|
||||||
|
// liens de critique
|
||||||
|
{ source: 'ecosocialisme', target: 'capitalisme-vert', auteurs_passerelle: ['Klein', 'Malm'], type: 'critique' },
|
||||||
|
{ source: 'decroissance', target: 'capitalisme-vert', auteurs_passerelle: ['Latouche', 'Meadows'], type: 'critique' },
|
||||||
|
{ source: 'eco-anarchisme', target: 'capitalisme-vert', auteurs_passerelle: ['Bookchin'], type: 'critique' },
|
||||||
|
{ source: 'ethiques-environnementales', target: 'ecofascismes', auteurs_passerelle: ['Naess'], type: 'critique' },
|
||||||
|
{ source: 'capitalisme-vert', target: 'ecofascismes', auteurs_passerelle: [], type: 'critique' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const props = defineProps<{ data: PenseesData | null; active?: boolean }>()
|
||||||
|
const emit = defineEmits<{ 'select-auteur': [id: string]; 'select-ecole': [id: string] }>()
|
||||||
|
|
||||||
|
const svgRef = ref<SVGElement | null>(null)
|
||||||
|
const tooltipRef = ref<HTMLElement | null>(null)
|
||||||
|
let simulation: any = null
|
||||||
|
let d3LinkSel: any = null
|
||||||
|
let d3InfluenceSel: any = null
|
||||||
|
let d3NodeSel: any = null
|
||||||
|
let d3EdgeLabelSel: any = null
|
||||||
|
|
||||||
|
async function initGraph() {
|
||||||
|
if (!svgRef.value || !props.data) return
|
||||||
|
const d3 = await import('d3')
|
||||||
|
|
||||||
|
const svgEl = svgRef.value
|
||||||
|
const W = svgEl.clientWidth || 900
|
||||||
|
const H = svgEl.clientHeight || 600
|
||||||
|
|
||||||
|
d3.select(svgEl).selectAll('*').remove()
|
||||||
|
const svg = d3.select(svgEl).attr('viewBox', `0 0 ${W} ${H}`)
|
||||||
|
const g = svg.append('g')
|
||||||
|
|
||||||
|
svg.call(d3.zoom<SVGElement, unknown>().scaleExtent([0.3, 4]).on('zoom', (e) => g.attr('transform', e.transform)) as any)
|
||||||
|
|
||||||
|
const ecoleMap = new Map<string, EcoleData>(props.data.ecoles.map(e => [e.id, e]))
|
||||||
|
|
||||||
|
// Positions fixes des ecoles (base pour forces D3)
|
||||||
|
const ecolePositions = new Map<string, { tx: number; ty: number }>()
|
||||||
|
props.data.ecoles.forEach(e => {
|
||||||
|
ecolePositions.set(e.id, { tx: W * e.x_hint, ty: H * e.y_hint })
|
||||||
|
})
|
||||||
|
|
||||||
|
// ---- LIENS D'INFLUENCE INTER-ECOLES (couche 3) ----
|
||||||
|
const gInfluence = g.append('g').attr('class', 'links-influence')
|
||||||
|
|
||||||
|
LINKS_INFLUENCE.forEach(link => {
|
||||||
|
const src = ecolePositions.get(link.source)
|
||||||
|
const tgt = ecolePositions.get(link.target)
|
||||||
|
if (!src || !tgt) return
|
||||||
|
|
||||||
|
const isCritique = link.type === 'critique'
|
||||||
|
const lineEl = gInfluence.append('line')
|
||||||
|
.attr('class', 'influence-link')
|
||||||
|
.attr('x1', src.tx).attr('y1', src.ty)
|
||||||
|
.attr('x2', tgt.tx).attr('y2', tgt.ty)
|
||||||
|
.attr('stroke', isCritique ? '#d99' : '#9aa')
|
||||||
|
.attr('stroke-width', 1)
|
||||||
|
.attr('stroke-dasharray', isCritique ? '4,3' : '6,4')
|
||||||
|
.attr('stroke-opacity', isCritique ? 0.2 : 0.22)
|
||||||
|
|
||||||
|
if (link.auteurs_passerelle && link.auteurs_passerelle.length > 0) {
|
||||||
|
lineEl
|
||||||
|
.on('mouseenter', (e: any) => {
|
||||||
|
if (!tooltipRef.value) return
|
||||||
|
tooltipRef.value.innerHTML = `<strong>Influence</strong><br><span style="opacity:0.8;font-size:0.72rem;">Passerelles : ${link.auteurs_passerelle.join(', ')}</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' })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// ---- SIMULATION D3 (auteurs) ----
|
||||||
|
const auteurNodes: any[] = props.data.auteurs.map(a => {
|
||||||
|
const ecole = ecoleMap.get(a.ecole_principale)
|
||||||
|
const jitter = () => (Math.random() - 0.5) * 80
|
||||||
|
return {
|
||||||
|
id: a.id, type: 'auteur', nom: a.nom, dates: a.dates,
|
||||||
|
bio_courte: a.bio_courte,
|
||||||
|
bio_provisoire: a.bio_courte_provisoire ?? '',
|
||||||
|
ingere: a.ingere,
|
||||||
|
ecole_principale: a.ecole_principale,
|
||||||
|
color: ecole?.color ?? '#888', r: 11,
|
||||||
|
x: W * (ecole?.x_hint ?? 0.5) + jitter(),
|
||||||
|
y: H * (ecole?.y_hint ?? 0.5) + jitter(),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Liens appartenance auteur -> ecole (vers centroid fixe)
|
||||||
|
const links: any[] = []
|
||||||
|
props.data.auteurs.forEach(a => {
|
||||||
|
links.push({ source: a.id, target: a.ecole_principale, strength: 0.65, isSubcourant: false })
|
||||||
|
a.ecoles.filter(e => e !== a.ecole_principale).forEach(e => {
|
||||||
|
links.push({ source: a.id, target: e, strength: 0.25, isSubcourant: true })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// Nodes fictifs fixes pour les ecoles (cibles des liens appartenance)
|
||||||
|
const ecoleFixedNodes: any[] = props.data.ecoles.map(e => ({
|
||||||
|
id: e.id, type: 'ecole-fixed', ecoleId: e.id,
|
||||||
|
x: W * e.x_hint, y: 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]
|
||||||
|
|
||||||
|
if (simulation) simulation.stop()
|
||||||
|
simulation = d3.forceSimulation(allNodes)
|
||||||
|
.force('link', d3.forceLink(links).id((d: any) => d.id).distance(120).strength((d: any) => d.strength ?? 0.5))
|
||||||
|
.force('charge', d3.forceManyBody().strength(-70))
|
||||||
|
.force('center', d3.forceCenter(W / 2, H / 2).strength(0.02))
|
||||||
|
.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) => {
|
||||||
|
if (d.type === 'auteur') {
|
||||||
|
const pos = ecolePositions.get(d.ecole_principale)
|
||||||
|
return pos ? pos.tx : W / 2
|
||||||
|
}
|
||||||
|
return W / 2
|
||||||
|
}).strength(0.15))
|
||||||
|
.force('forceY', d3.forceY<any>((d: any) => {
|
||||||
|
if (d.type === 'auteur') {
|
||||||
|
const pos = ecolePositions.get(d.ecole_principale)
|
||||||
|
return pos ? pos.ty : H / 2
|
||||||
|
}
|
||||||
|
return H / 2
|
||||||
|
}).strength(0.15))
|
||||||
|
|
||||||
|
// ---- NOEUDS ECOLES visibles (couche 3.5) ----
|
||||||
|
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).attr('fill-opacity', 0.82).attr('stroke', ecole.color).attr('stroke-width', 2)
|
||||||
|
.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) })
|
||||||
|
|
||||||
|
// ---- TITRES ECOLES visibles en permanence ----
|
||||||
|
const labelText = ecole.label
|
||||||
|
const words = labelText.split(' ')
|
||||||
|
const fontSize = Math.max(12, r * 0.45)
|
||||||
|
if (words.length > 2 || labelText.length > 12) {
|
||||||
|
const mid = Math.ceil(words.length / 2)
|
||||||
|
const line1 = words.slice(0, mid).join(' ')
|
||||||
|
const line2 = words.slice(mid).join(' ')
|
||||||
|
const textEl = gEcoles.append('text')
|
||||||
|
.attr('x', eNode.fx)
|
||||||
|
.attr('y', eNode.fy)
|
||||||
|
.attr('text-anchor', 'middle')
|
||||||
|
.attr('dominant-baseline', 'middle')
|
||||||
|
.style('pointer-events', 'none')
|
||||||
|
.style('font-weight', '700')
|
||||||
|
.style('font-size', `${fontSize}px`)
|
||||||
|
.style('fill', '#ffffff')
|
||||||
|
.style('text-shadow', '0 1px 3px rgba(0,0,0,0.5)')
|
||||||
|
.style('user-select', 'none')
|
||||||
|
textEl.append('tspan')
|
||||||
|
.attr('x', eNode.fx)
|
||||||
|
.attr('dy', `-${fontSize * 0.6}px`)
|
||||||
|
.text(line1)
|
||||||
|
textEl.append('tspan')
|
||||||
|
.attr('x', eNode.fx)
|
||||||
|
.attr('dy', `${fontSize * 1.2}px`)
|
||||||
|
.text(line2)
|
||||||
|
} else {
|
||||||
|
gEcoles.append('text')
|
||||||
|
.attr('x', eNode.fx)
|
||||||
|
.attr('y', eNode.fy)
|
||||||
|
.attr('text-anchor', 'middle')
|
||||||
|
.attr('dominant-baseline', 'middle')
|
||||||
|
.style('pointer-events', 'none')
|
||||||
|
.style('font-weight', '700')
|
||||||
|
.style('font-size', `${fontSize}px`)
|
||||||
|
.style('fill', '#ffffff')
|
||||||
|
.style('user-select', 'none')
|
||||||
|
.text(labelText)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// ---- LIENS APPARTENANCE (couche 4) ----
|
||||||
|
const gLinks = g.append('g').attr('class', 'links-appartenance')
|
||||||
|
d3LinkSel = gLinks.selectAll('line').data(links).join('line')
|
||||||
|
.attr('stroke', 'rgba(150,150,150,0.28)').attr('stroke-width', 1.2)
|
||||||
|
|
||||||
|
// ---- EDGE LABELS - sous-courants (couche 4b) ----
|
||||||
|
const subcourantLinks = links.filter((l: any) => l.isSubcourant)
|
||||||
|
d3EdgeLabelSel = gLinks.selectAll('text.pensees-edge-label')
|
||||||
|
.data(subcourantLinks)
|
||||||
|
.join('text')
|
||||||
|
.attr('class', 'pensees-edge-label')
|
||||||
|
|
||||||
|
// ---- NODES AUTEURS (couche 5) ----
|
||||||
|
const gAuteurs = g.append('g').attr('class', 'auteurs')
|
||||||
|
d3NodeSel = gAuteurs.selectAll('g').data(auteurNodes).join('g')
|
||||||
|
.style('cursor', (d: any) => d.ingere ? 'pointer' : 'default')
|
||||||
|
.call(d3.drag<any, any>()
|
||||||
|
.on('start', (e: any, d: any) => { if (!e.active) simulation.alphaTarget(0.3).restart(); d.fx = d.x; d.fy = d.y })
|
||||||
|
.on('drag', (e: any, d: any) => { d.fx = e.x; d.fy = e.y })
|
||||||
|
.on('end', (e: any, d: any) => { if (!e.active) simulation.alphaTarget(0); d.fx = null; d.fy = null }))
|
||||||
|
.on('click', (e: any, d: any) => {
|
||||||
|
if (!d.ingere) return
|
||||||
|
e.stopPropagation()
|
||||||
|
emit('select-auteur', d.id)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Phase 8.D : grisage conditionnel auteurs non-ingeres
|
||||||
|
d3NodeSel.append('circle')
|
||||||
|
.attr('r', (d: any) => d.r)
|
||||||
|
.attr('fill', (d: any) => d.ingere ? (d.color + 'cc') : '#bbbbbb')
|
||||||
|
.attr('stroke', (d: any) => d.ingere ? d.color : '#999999')
|
||||||
|
.attr('stroke-width', 1.5)
|
||||||
|
.attr('opacity', (d: any) => d.ingere ? 1 : 0.35)
|
||||||
|
|
||||||
|
// ---- LABELS AUTEURS (couche 6 - drop-shadow blanc) ----
|
||||||
|
d3NodeSel.append('text')
|
||||||
|
.attr('class', 'pensees-auteur-label')
|
||||||
|
.text((d: any) => d.nom.split(' ').pop() ?? d.nom)
|
||||||
|
.attr('text-anchor', 'middle')
|
||||||
|
.attr('dy', (d: any) => -(d.r + 4))
|
||||||
|
.style('pointer-events', 'none')
|
||||||
|
.style('opacity', (d: any) => d.ingere ? 1 : 0.3)
|
||||||
|
.style('fill', (d: any) => d.ingere ? '#1a1a1a' : '#777777')
|
||||||
|
|
||||||
|
d3NodeSel
|
||||||
|
.on('mouseenter', (e: any, d: any) => {
|
||||||
|
if (!tooltipRef.value) return
|
||||||
|
let tooltipHtml = ''
|
||||||
|
if (d.ingere) {
|
||||||
|
const rawBio = d.bio_courte || ''
|
||||||
|
const bio = rawBio.length > 90 ? rawBio.slice(0, 87) + '...' : rawBio
|
||||||
|
tooltipHtml = `<strong>${d.nom}</strong> <span style="opacity:0.6;font-size:0.7rem;">${d.dates}</span><br><span style="opacity:0.75;font-size:0.72rem;">${bio || 'Dans le RAG ATIS.'}</span>`
|
||||||
|
} else {
|
||||||
|
tooltipHtml = `<strong>${d.nom}</strong> <span style="opacity:0.6;font-size:0.7rem;">${d.dates}</span><br><span style="opacity:0.65;font-size:0.72rem;font-style:italic;">Présent dans Bonpote, pas encore ingéré dans le RAG ATIS.</span>`
|
||||||
|
}
|
||||||
|
tooltipRef.value.innerHTML = tooltipHtml
|
||||||
|
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' })
|
||||||
|
|
||||||
|
simulation.on('tick', () => {
|
||||||
|
d3LinkSel
|
||||||
|
.attr('x1', (d: any) => d.source.x).attr('y1', (d: any) => d.source.y)
|
||||||
|
.attr('x2', (d: any) => d.target.x).attr('y2', (d: any) => d.target.y)
|
||||||
|
|
||||||
|
d3EdgeLabelSel
|
||||||
|
.attr('x', (d: any) => (d.source.x + d.target.x) / 2)
|
||||||
|
.attr('y', (d: any) => (d.source.y + d.target.y) / 2)
|
||||||
|
.text((d: any) => {
|
||||||
|
const targetId = typeof d.target === 'object' ? d.target.id : d.target
|
||||||
|
return targetId
|
||||||
|
})
|
||||||
|
|
||||||
|
d3NodeSel.attr('transform', (d: any) => `translate(${d.x},${d.y})`)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(() => props.active, (val) => {
|
||||||
|
if (val && import.meta.client && props.data)
|
||||||
|
requestAnimationFrame(() => requestAnimationFrame(() => initGraph()))
|
||||||
|
})
|
||||||
|
watch(() => props.data, (val) => {
|
||||||
|
if (val && props.active && import.meta.client)
|
||||||
|
requestAnimationFrame(() => requestAnimationFrame(() => initGraph()))
|
||||||
|
})
|
||||||
|
onMounted(async () => {
|
||||||
|
if (import.meta.client && props.data && props.active) {
|
||||||
|
await nextTick()
|
||||||
|
initGraph()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
onUnmounted(() => { if (simulation) simulation.stop() })
|
||||||
|
|
||||||
|
function triggerResize() {
|
||||||
|
if (simulation) {
|
||||||
|
simulation.alpha(0.3).restart()
|
||||||
|
} else if (import.meta.client && props.data && props.active) {
|
||||||
|
initGraph()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
defineExpose({ triggerResize })
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.pensees-auteur-label {
|
||||||
|
fill: #1a1a1a;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 10px;
|
||||||
|
filter: drop-shadow(0 0 2.5px rgba(255,255,255,0.95));
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pensees-edge-label {
|
||||||
|
fill: #555;
|
||||||
|
font-size: 8.5px;
|
||||||
|
font-style: italic;
|
||||||
|
opacity: 0.7;
|
||||||
|
text-anchor: middle;
|
||||||
|
dominant-baseline: middle;
|
||||||
|
user-select: none;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ecole-node {
|
||||||
|
transition: opacity 0.15s, r 0.15s;
|
||||||
|
}
|
||||||
|
.ecole-node:hover {
|
||||||
|
opacity: 0.75;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
30
components/MediaTabBackend.vue
Normal file
30
components/MediaTabBackend.vue
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<template>
|
||||||
|
<div class="media-tab-backend" style="padding: 2rem; overflow-y: auto;">
|
||||||
|
<div style="max-width: 640px;">
|
||||||
|
<h2 style="font-weight: 700; font-size: 1.1rem; margin-bottom: 0.75rem; color: var(--nav-text);">LightRAG backend</h2>
|
||||||
|
<p style="font-size: 0.9rem; line-height: 1.6; color: var(--nav-text); margin-bottom: 0.5rem;">
|
||||||
|
Voici l'interface brute du <strong>LightRAG</strong> qui alimente la carte des pensées écologiques.
|
||||||
|
C'est la "cuisine" du RAG : ingestion de documents, extraction d'entités, relations, requêtes.
|
||||||
|
</p>
|
||||||
|
</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
|
||||||
|
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;"
|
||||||
|
title="LightRAG backend AEP — lecture seule"
|
||||||
|
sandbox="allow-same-origin allow-scripts"
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
-->
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
67
components/MediaTabProjets.vue
Normal file
67
components/MediaTabProjets.vue
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
<template>
|
||||||
|
<div class="media-tab-projets" style="padding: 1.5rem; overflow-y: auto;">
|
||||||
|
<div style="max-width: 70ch; margin-bottom: 1.5rem;">
|
||||||
|
<h2 style="font-weight: 700; font-size: 1.1rem; margin-bottom: 0.5rem; color: var(--nav-text);">PFE engagés</h2>
|
||||||
|
<p style="font-size: 0.9rem; line-height: 1.6; color: var(--nav-text);">
|
||||||
|
Mutualiser le savoir. Voici les PFE engagés publiés en ligne dont nous avons connaissance.
|
||||||
|
Partage-nous le lien de ton travail si tu veux participer à cette initiative.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="projets-grid">
|
||||||
|
<article v-for="p in projets" :key="p.id" class="projet-card">
|
||||||
|
<img v-if="p.thumb" :src="p.thumb" :alt="p.titre" class="projet-thumb" loading="lazy" />
|
||||||
|
<div v-else class="projet-thumb projet-thumb--placeholder">📐</div>
|
||||||
|
|
||||||
|
<h3 style="font-weight: 600; font-size: 0.95rem; margin: 0.5rem 0 0.25rem; color: var(--nav-text);">{{ p.titre }}</h3>
|
||||||
|
<p style="font-size: 0.8rem; color: var(--nav-text-muted); margin-bottom: 0.5rem;">
|
||||||
|
{{ (p.auteurs || []).filter((a: string) => a !== 'Inconnu').join(', ') }}
|
||||||
|
<template v-if="p.ecole && p.ecole !== 'Inconnu'"> · {{ p.ecole }}</template>
|
||||||
|
<template v-if="p.annee && p.annee !== 'Inconnu'"> · {{ p.annee }}</template>
|
||||||
|
</p>
|
||||||
|
<p style="font-size: 0.875rem; line-height: 1.5; color: var(--nav-text); flex: 1; margin-bottom: 0.75rem;">{{ p.description }}</p>
|
||||||
|
|
||||||
|
<a v-if="p.url" :href="p.url" target="_blank" rel="noopener" style="color: var(--nav-primary-solid, #3b6ea5); font-weight: 600; font-size: 0.875rem; text-decoration: none;">
|
||||||
|
Découvrir →
|
||||||
|
</a>
|
||||||
|
<span v-if="p.link_status === 'broken'" style="color: #e67e22; font-size: 0.8rem; display: block; margin-top: 0.25rem;">⚠ Lien d'origine cassé</span>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p style="margin-top: 2rem; font-size: 0.875rem; color: var(--nav-text-muted);">
|
||||||
|
Tu as un PFE engagé à partager ? <a href="mailto:contact@trans-former.fr" style="color: var(--nav-primary-solid);">Écris-moi</a>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const { data: pfeData } = await useFetch<{ projets: any[] }>('/data/pfe-engages.json')
|
||||||
|
const projets = computed(() => pfeData.value?.projets ?? [])
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.projets-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||||
|
gap: 1.25rem;
|
||||||
|
}
|
||||||
|
.projet-card {
|
||||||
|
border: 1px solid var(--nav-bg-alt, #eee);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 1rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: var(--nav-surface);
|
||||||
|
}
|
||||||
|
.projet-thumb {
|
||||||
|
width: 100%;
|
||||||
|
height: 140px;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: var(--nav-bg-alt, #f5f5f5);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
604
components/MediaTabVisuel.vue
Normal file
604
components/MediaTabVisuel.vue
Normal file
@@ -0,0 +1,604 @@
|
|||||||
|
<template>
|
||||||
|
<div class="media-visuel">
|
||||||
|
|
||||||
|
<!-- 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 } : {}"
|
||||||
|
style="position: relative;"
|
||||||
|
>
|
||||||
|
<ClientOnly>
|
||||||
|
<CartePensees
|
||||||
|
ref="cartePenseesRef"
|
||||||
|
:data="penseesData"
|
||||||
|
:active="true"
|
||||||
|
@select-auteur="onSelectAuteur"
|
||||||
|
@select-ecole="onSelectEcole"
|
||||||
|
/>
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<!-- Overlay PDF FRACAS -->
|
||||||
|
<div
|
||||||
|
v-if="showFracasPdf"
|
||||||
|
class="fracas-overlay"
|
||||||
|
:style="{ opacity: fracasOpacity / 100 }"
|
||||||
|
>
|
||||||
|
<embed
|
||||||
|
src="/cartes/carte-fracas-bonpote-v2.pdf"
|
||||||
|
type="application/pdf"
|
||||||
|
style="width: 100%; height: 100%;"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</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>
|
||||||
|
<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 -->
|
||||||
|
<label class="layer-toggle" title="Superposer la carte FRACAS Bonpote V2 en PDF">
|
||||||
|
<input type="checkbox" v-model="showFracasPdf" />
|
||||||
|
📄 Carte FRACAS (PDF)
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
v-if="showFracasPdf"
|
||||||
|
type="range"
|
||||||
|
min="0"
|
||||||
|
max="100"
|
||||||
|
v-model.number="fracasOpacity"
|
||||||
|
class="opacity-slider"
|
||||||
|
:title="`Opacité ${fracasOpacity}%`"
|
||||||
|
/>
|
||||||
|
</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>
|
||||||
|
|
||||||
|
<!-- Vue Bonpote V2 -->
|
||||||
|
<div
|
||||||
|
v-if="layoutMode === 'bonpote'"
|
||||||
|
class="flex-1 overflow-y-auto px-6 py-8"
|
||||||
|
style="max-width: 680px; margin: 0 auto;"
|
||||||
|
>
|
||||||
|
<div class="mb-6">
|
||||||
|
<p class="text-xs font-bold uppercase tracking-widest mb-2" style="color: var(--nav-text-muted);">Reference editoriale</p>
|
||||||
|
<h2 class="text-xl font-bold mb-3" style="color: var(--nav-text);">Carte FRACAS des pensees ecologiques</h2>
|
||||||
|
<p class="text-sm leading-relaxed mb-4" style="color: var(--nav-text);">
|
||||||
|
FRACAS (Familles, Racines et Arpentages des Courants et Alternatives Solidaires) est une carte des ecoles de pensee ecologique publiee par Bonpote en octobre 2024. Elle reference ~140 auteurs et autrices reparti-es en 10 ecoles de pensee, depuis l'ecosocialisme jusqu'a l'ethique environnementale.
|
||||||
|
</p>
|
||||||
|
<p class="text-sm leading-relaxed mb-6" style="color: var(--nav-text);">
|
||||||
|
Le RAG ATIS est construit sur cette reference : chaque auteur ingere dans la bibliotheque correspond a une entree de la carte FRACAS. Les ecoles de pensee, les positions et les couleurs de notre carte sont transposees 1:1 depuis Bonpote V2.
|
||||||
|
</p>
|
||||||
|
<div class="flex flex-col gap-3">
|
||||||
|
<a href="https://bonpote.com/la-carte-des-pensees-ecologiques/"
|
||||||
|
target="_blank" rel="noopener"
|
||||||
|
class="flex items-center gap-3 px-4 py-3 rounded-lg hover:opacity-80 transition-opacity"
|
||||||
|
style="background: var(--nav-primary, #3b6ea5); color: white; font-size: 0.875rem; font-weight: 600; text-decoration: none;">
|
||||||
|
<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="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>
|
||||||
|
Lire l'article Bonpote + carte interactive
|
||||||
|
</a>
|
||||||
|
<a href="https://bonpote.com/wp-content/uploads/2024/10/FRACAS_BONPOTE_CARTE_VERSO_V2-OCT2024.pdf"
|
||||||
|
target="_blank" rel="noopener"
|
||||||
|
class="flex items-center gap-3 px-4 py-3 rounded-lg hover:opacity-80 transition-opacity"
|
||||||
|
style="background: var(--nav-bg-alt); color: var(--nav-text); font-size: 0.875rem; font-weight: 500; text-decoration: none;">
|
||||||
|
<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 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
|
||||||
|
Telecharger le poster PDF (recto/verso)
|
||||||
|
</a>
|
||||||
|
<button
|
||||||
|
@click="setLayoutMode('split')"
|
||||||
|
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;">
|
||||||
|
<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>
|
||||||
|
Interroger le RAG ATIS sur ces pensees
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-xs font-bold uppercase tracking-widest mb-3" style="color: var(--nav-text-muted);">Les 10 ecoles de pensee (FRACAS V2)</p>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<div v-for="ecole in (penseesData?.ecoles ?? [])" :key="ecole.id"
|
||||||
|
class="flex items-start gap-3 px-3 py-2 rounded-lg"
|
||||||
|
style="background: var(--nav-bg-alt);">
|
||||||
|
<span class="w-3 h-3 rounded-full shrink-0 mt-1" :style="`background:${ecole.color};`"></span>
|
||||||
|
<div>
|
||||||
|
<p class="text-sm font-semibold" style="color: var(--nav-text);">{{ ecole.label }}</p>
|
||||||
|
<p class="text-xs mt-0.5 leading-relaxed" style="color: var(--nav-text-muted);">{{ ecole.description }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Fiche auteur modal -->
|
||||||
|
<FicheAuteur
|
||||||
|
:open="ficheOpen"
|
||||||
|
:auteurId="ficheAuteurId"
|
||||||
|
:data="penseesData"
|
||||||
|
@close="ficheOpen = false"
|
||||||
|
@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/la-carte-des-pensees-ecologiques/" target="_blank" rel="noopener" style="text-decoration:underline;">article + 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>
|
||||||
|
</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' | 'bonpote'
|
||||||
|
|
||||||
|
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 ficheEcoleOpen = ref(false)
|
||||||
|
const ficheEcoleId = ref<string | null>(null)
|
||||||
|
const ragInfoOpen = ref(false)
|
||||||
|
const chatbotAuteur = ref<string | null>(null)
|
||||||
|
const layoutMode = ref<LayoutMode>('split')
|
||||||
|
const cartePenseesRef = ref<{ triggerResize: () => void } | null>(null)
|
||||||
|
|
||||||
|
// Toggle PDF FRACAS
|
||||||
|
const showFracasPdf = ref(false)
|
||||||
|
const fracasOpacity = ref(60)
|
||||||
|
|
||||||
|
// Props injectées depuis le parent (penseesData)
|
||||||
|
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 carteFlexBasis = computed(() => `${splitRatio.value * 100}%`)
|
||||||
|
const chatbotFlexBasis = computed(() => `${(1 - splitRatio.value) * 100}%`)
|
||||||
|
|
||||||
|
// Logique poignee draggable
|
||||||
|
let dragStartY = 0
|
||||||
|
let dragStartRatio = DEFAULT_SPLIT_RATIO
|
||||||
|
let containerHeight = 0
|
||||||
|
|
||||||
|
function onHandleMousedown(e: MouseEvent) {
|
||||||
|
dragStartY = e.clientY
|
||||||
|
dragStartRatio = splitRatio.value
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
cartePenseesRef.value?.triggerResize()
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
const saved = localStorage.getItem(STORAGE_KEY) as LayoutMode | null
|
||||||
|
if (saved && ['split', 'carte-full', 'chatbot-full', 'bonpote'].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
|
||||||
|
}
|
||||||
|
if (!localStorage.getItem('rag-fracas-info-seen')) {
|
||||||
|
ragInfoOpen.value = true
|
||||||
|
localStorage.setItem('rag-fracas-info-seen', '1')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
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) => {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
localStorage.setItem(STORAGE_KEY, v)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function onSelectAuteur(id: string) {
|
||||||
|
ficheAuteurId.value = id
|
||||||
|
ficheOpen.value = true
|
||||||
|
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 = props.penseesData?.ecoles.find(e => e.id === ecoleId)
|
||||||
|
chatbotAuteur.value = ecole?.label ?? null
|
||||||
|
if (layoutMode.value === 'carte-full') setLayoutMode('split')
|
||||||
|
}
|
||||||
|
|
||||||
|
function onInterrogerRag(auteurId: string) {
|
||||||
|
ficheOpen.value = false
|
||||||
|
const auteur = props.penseesData?.auteurs.find(a => a.id === auteurId)
|
||||||
|
chatbotAuteur.value = auteur?.nom ?? null
|
||||||
|
if (layoutMode.value === 'carte-full') {
|
||||||
|
setLayoutMode('split')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.media-visuel {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Overlay PDF FRACAS --- */
|
||||||
|
.fracas-overlay {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 50;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- 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;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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 layer PDF FRACAS --- */
|
||||||
|
.layer-toggle {
|
||||||
|
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;
|
||||||
|
user-select: none;
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layer-toggle input[type="checkbox"] {
|
||||||
|
margin: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opacity-slider {
|
||||||
|
width: 80px;
|
||||||
|
cursor: pointer;
|
||||||
|
accent-color: var(--nav-primary, #3b6ea5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- 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) --- */
|
||||||
|
@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>
|
||||||
63
pages/media.vue
Normal file
63
pages/media.vue
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
<template>
|
||||||
|
<div class="media-page" style="background: var(--nav-bg);">
|
||||||
|
<nav class="subtabs" style="display:flex; gap:0; border-bottom: 1px solid var(--nav-bg-alt); background: var(--nav-surface); padding: 0 1rem;">
|
||||||
|
<button
|
||||||
|
:class="['subtab-btn', { active: tab === 'visuel' }]"
|
||||||
|
@click="tab = 'visuel'"
|
||||||
|
>
|
||||||
|
🌳 RAG visuel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
:class="['subtab-btn', { active: tab === 'backend' }]"
|
||||||
|
@click="tab = 'backend'"
|
||||||
|
>
|
||||||
|
⚙ LightRAG backend
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
:class="['subtab-btn', { active: tab === 'projets' }]"
|
||||||
|
@click="tab = 'projets'"
|
||||||
|
>
|
||||||
|
📚 Projets
|
||||||
|
</button>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<MediaTabVisuel v-if="tab === 'visuel'" />
|
||||||
|
<MediaTabBackend v-else-if="tab === 'backend'" />
|
||||||
|
<MediaTabProjets v-else-if="tab === 'projets'" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const tab = ref<'visuel' | 'backend' | 'projets'>(
|
||||||
|
(['visuel', 'backend', 'projets'].includes(route.query.tab as string)
|
||||||
|
? route.query.tab as 'visuel' | 'backend' | 'projets'
|
||||||
|
: 'visuel')
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(tab, (newTab) => {
|
||||||
|
router.replace({ query: { ...route.query, tab: newTab } })
|
||||||
|
})
|
||||||
|
|
||||||
|
useHead({ title: 'AEP - Media' })
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.media-page { display: flex; flex-direction: column; height: 100%; overflow: hidden; }
|
||||||
|
.subtabs { display: flex; gap: 0; flex-shrink: 0; }
|
||||||
|
.subtab-btn {
|
||||||
|
padding: 10px 18px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 500;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
border-bottom: 2px solid transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--nav-text-muted);
|
||||||
|
transition: color 0.15s, border-color 0.15s;
|
||||||
|
}
|
||||||
|
.subtab-btn:hover { color: var(--nav-text); }
|
||||||
|
.subtab-btn.active { color: var(--nav-primary-solid); border-bottom-color: var(--nav-primary-solid); font-weight: 600; }
|
||||||
|
</style>
|
||||||
16873
public/cartes/carte-fracas-bonpote-v2.pdf
Normal file
16873
public/cartes/carte-fracas-bonpote-v2.pdf
Normal file
File diff suppressed because one or more lines are too long
70
public/data/pfe-engages.json
Normal file
70
public/data/pfe-engages.json
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
{
|
||||||
|
"projets": [
|
||||||
|
{
|
||||||
|
"id": "quartier-2030",
|
||||||
|
"titre": "Votre quartier en 2030",
|
||||||
|
"auteurs": ["Inconnu"],
|
||||||
|
"annee": "2020",
|
||||||
|
"ecole": "Inconnu",
|
||||||
|
"url": "https://quartier-2030.firebaseapp.com/",
|
||||||
|
"description": "Exploration prospective confrontant smart city, no future, résilience et deep ecology à l'échelle du quartier. Le travail donne à voir plusieurs futurs urbains contrastés, de l'utopie technologique au retrait radical, en laissant le visiteur naviguer entre les scénarios. Un travail d'orfèvre pour sortir de la pensée linéaire sur la ville.",
|
||||||
|
"thumb": null,
|
||||||
|
"link_status": "ok"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "seine-nature",
|
||||||
|
"titre": "Seine — nature urbaine",
|
||||||
|
"auteurs": ["Inconnu"],
|
||||||
|
"annee": "2019",
|
||||||
|
"ecole": "Inconnu",
|
||||||
|
"url": "http://www.seine.natureurbaine.com/00_index/page_theme/theme.html",
|
||||||
|
"description": "Projet de transformation territoriale collective autour de la Seine, pensé comme une démarche systémique et pluridisciplinaire. L'intervention se concentre sur les marges périurbaines, traitées par une logique d'acupuncture : des micro-interventions précises pour enclencher des dynamiques plus larges. L'approche refuse le grand projet unique au profit d'un réseau de petites transformations.",
|
||||||
|
"thumb": null,
|
||||||
|
"link_status": "ok"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "tmip",
|
||||||
|
"titre": "TMIP — Transformation de la Maison Individuelle Périurbaine",
|
||||||
|
"auteurs": ["Jules Nény"],
|
||||||
|
"annee": "2019",
|
||||||
|
"ecole": "ENSA Paris-Belleville",
|
||||||
|
"url": "https://issuu.com/transformationresilientes/docs/tmip_archijeunes_cstb_",
|
||||||
|
"description": "Étude de la maison périurbaine sous l'angle des Gilets jaunes : comment ce lieu de vie concentre les tensions entre émancipation individuelle et dépendance structurelle (voiture, énergie, services). Le projet propose un réseau de micro-infrastructures partagées pour transformer ces maisons isolées en systèmes résilients interconnectés. Publié avec ARCHI'JEUNES et le CSTB.",
|
||||||
|
"thumb": null,
|
||||||
|
"link_status": "ok"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "filiere-bois",
|
||||||
|
"titre": "Enquête sur les paysages forestiers franciliens",
|
||||||
|
"auteurs": ["Quid Architecture"],
|
||||||
|
"annee": "2021",
|
||||||
|
"ecole": "Inconnu",
|
||||||
|
"url": "https://www.faireparis.com/fr/projets/faire-2021/enquete-sur-les-paysages-forestiers-franciliens-2159.html",
|
||||||
|
"description": "Projet lauréat FAIRE 2021. Enquête sur les dysfonctionnements de la filière bois en Île-de-France, aux interfaces entre sylviculteurs, scieries, artisans et maîtres d'ouvrage. Le travail cartographie les ruptures de filière et propose des interventions concrètes pour réparer les liens entre forêt et construction. Une démarche systémique rare dans les études architecturales.",
|
||||||
|
"thumb": null,
|
||||||
|
"link_status": "ok"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "jeu-champagne",
|
||||||
|
"titre": "Jeu de rôle Champagne PFE — Plateau",
|
||||||
|
"auteurs": ["Inconnu"],
|
||||||
|
"annee": "2020",
|
||||||
|
"ecole": "Inconnu",
|
||||||
|
"url": "https://campfe2020.wixsite.com/champagnepfe/plateau",
|
||||||
|
"description": "Dispositif ludique et coopératif développé comme outil de médiation entre acteurs d'un territoire. Le jeu de rôle permet de traverser des problèmes complexes en engageant simultanément des parties prenantes aux intérêts divergents. Une exploration de l'architecture comme processus collectif plutôt que comme objet produit.",
|
||||||
|
"thumb": null,
|
||||||
|
"link_status": "ok"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "transition-agricole",
|
||||||
|
"titre": "Transition agricole — réinvestissement de fermes traditionnelles",
|
||||||
|
"auteurs": ["Inconnu"],
|
||||||
|
"annee": "2020",
|
||||||
|
"ecole": "Inconnu",
|
||||||
|
"url": "https://www.calameo.com/books/007306483e0b23edb1db7",
|
||||||
|
"description": "Projet sur la transformation de fermes traditionnelles dans une logique agricole moderne et diversifiée. L'étude explore comment l'architecture peut accompagner les transitions d'usage des bâtiments ruraux, en articulant patrimonial et fonctionnel. Voir aussi le projet complémentaire sur la Seine aval : https://www.calameo.com/books/007063623f4d4b800b01d",
|
||||||
|
"thumb": null,
|
||||||
|
"link_status": "ok"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user