6 Commits

Author SHA1 Message Date
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
Jules Neny
59cb81a055 Merge P3 media modif 2026-05-22 11:02:05 +02:00
Jules Neny
91e3466ec6 Merge P2 outils build 2026-05-22 11:01:30 +02:00
Jules Neny
422f45116f feat(outils): page Outils V1 + composants TreeASCII/OutilCard/SimulateurFeature + nav premier onglet 2026-05-22 10:58:39 +02:00
13 changed files with 1665 additions and 177 deletions

View File

@@ -22,6 +22,13 @@
<!-- ── Onglets desktop (≥1024px) — remplace la barre de recherche ── --> <!-- ── Onglets desktop (≥1024px) — remplace la barre de recherche ── -->
<nav class="hidden lg:flex flex-1 justify-center items-end gap-0 mx-6" aria-label="Navigation projets"> <nav class="hidden lg:flex flex-1 justify-center items-end gap-0 mx-6" aria-label="Navigation projets">
<NuxtLink
to="/outils"
class="nav-tab"
:class="{ 'nav-tab--active': route.path === '/outils' }"
>
Outils
</NuxtLink>
<NuxtLink <NuxtLink
to="/" to="/"
class="nav-tab" class="nav-tab"
@@ -172,6 +179,7 @@
style="background: var(--nav-surface); border: 1px solid var(--nav-bg-alt); z-index: 9999;" style="background: var(--nav-surface); border: 1px solid var(--nav-bg-alt); z-index: 9999;"
@click="hamburgerOpen = false" @click="hamburgerOpen = false"
> >
<NuxtLink to="/outils" class="block px-4 py-2.5 text-sm font-medium transition-opacity hover:opacity-70" :style="route.path === '/outils' ? 'color: var(--nav-primary-solid); font-weight: 700;' : 'color: var(--nav-text);'">Outils</NuxtLink>
<NuxtLink to="/" class="block px-4 py-2.5 text-sm font-medium transition-opacity hover:opacity-70" :style="route.path === '/' ? 'color: var(--nav-primary-solid); font-weight: 700;' : 'color: var(--nav-text);'">Écosystème Entraide Architecture</NuxtLink> <NuxtLink to="/" class="block px-4 py-2.5 text-sm font-medium transition-opacity hover:opacity-70" :style="route.path === '/' ? 'color: var(--nav-primary-solid); font-weight: 700;' : 'color: var(--nav-text);'">Écosystème Entraide Architecture</NuxtLink>
<NuxtLink to="/agences" class="block px-4 py-2.5 text-sm transition-opacity hover:opacity-70" :style="route.path === '/agences' ? 'color: var(--nav-primary-solid); font-weight: 700;' : 'color: var(--nav-text);'">Réseaux AEP</NuxtLink> <NuxtLink to="/agences" class="block px-4 py-2.5 text-sm transition-opacity hover:opacity-70" :style="route.path === '/agences' ? 'color: var(--nav-primary-solid); font-weight: 700;' : 'color: var(--nav-text);'">Réseaux AEP</NuxtLink>
<NuxtLink to="/trouver-du-taf" class="block px-4 py-2.5 text-sm transition-opacity hover:opacity-70" :style="route.path === '/trouver-du-taf' ? 'color: var(--nav-primary-solid); font-weight: 700;' : 'color: var(--nav-text);'">Jobs</NuxtLink> <NuxtLink to="/trouver-du-taf" class="block px-4 py-2.5 text-sm transition-opacity hover:opacity-70" :style="route.path === '/trouver-du-taf' ? 'color: var(--nav-primary-solid); font-weight: 700;' : 'color: var(--nav-text);'">Jobs</NuxtLink>

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

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

150
components/OutilCard.vue Normal file
View File

@@ -0,0 +1,150 @@
<template>
<component
:is="url ? 'a' : 'div'"
v-bind="url ? { href: url, target: '_blank', rel: 'noopener noreferrer' } : {}"
class="outil-card"
:class="{ 'outil-card--link': !!url, 'outil-card--disabled': !url }"
>
<div class="outil-card__header">
<span class="outil-card__icon" aria-hidden="true">{{ icon }}</span>
<span :class="['outil-card__badge', `outil-card__badge--${tag}`]">{{ tagLabel }}</span>
</div>
<h3 class="outil-card__titre">{{ titre }}</h3>
<p class="outil-card__desc">{{ description }}</p>
<span v-if="cta && url" class="outil-card__cta">{{ cta }}</span>
<span v-else-if="!url" class="outil-card__cta outil-card__cta--disabled">Bientôt disponible</span>
</component>
</template>
<script setup lang="ts">
const props = defineProps<{
icon?: string
titre: string
url?: string | null
description?: string
cta?: string
tag?: string
}>()
const tagLabels: Record<string, string> = {
'outil-aep': 'Outil AEP',
'inspiration-externe': 'Inspiration',
'disponible': 'Disponible',
'recommande': 'Recommandé',
'a-venir': 'À venir',
}
const tagLabel = computed(() => props.tag ? (tagLabels[props.tag] ?? props.tag) : '')
</script>
<style scoped>
.outil-card {
display: flex;
flex-direction: column;
gap: 0.5rem;
padding: 1rem 1.25rem;
border-radius: 10px;
border: 1px solid var(--nav-bg-alt);
background: var(--nav-surface);
text-decoration: none;
color: var(--nav-text);
transition: box-shadow 0.15s, border-color 0.15s;
}
.outil-card--link:hover {
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
border-color: var(--nav-primary-solid);
}
.outil-card--disabled {
opacity: 0.65;
}
.outil-card__header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.5rem;
}
.outil-card__icon {
font-size: 1.3rem;
line-height: 1;
}
.outil-card__badge {
font-size: 0.65rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.04em;
padding: 2px 7px;
border-radius: 999px;
}
.outil-card__badge--outil-aep {
background: #d1fae5;
color: #065f46;
}
.outil-card__badge--inspiration-externe {
background: #fef3c7;
color: #92400e;
}
.outil-card__badge--disponible {
background: #d1fae5;
color: #065f46;
}
.outil-card__badge--recommande {
background: #dbeafe;
color: #1e40af;
}
.outil-card__badge--a-venir {
background: var(--nav-bg-alt);
color: var(--nav-text-muted);
}
.outil-card__titre {
font-size: 0.9rem;
font-weight: 600;
color: var(--nav-text);
margin: 0;
line-height: 1.35;
}
.outil-card__desc {
font-size: 0.82rem;
color: var(--nav-text-muted);
margin: 0;
line-height: 1.5;
}
.outil-card__cta {
font-size: 0.78rem;
font-weight: 600;
color: var(--nav-primary-solid);
margin-top: 0.25rem;
}
.outil-card__cta--disabled {
color: var(--nav-text-muted);
font-weight: 400;
font-style: italic;
}
/* Dark mode badge overrides */
:global(.dark) .outil-card__badge--outil-aep {
background: #064e3b;
color: #a7f3d0;
}
:global(.dark) .outil-card__badge--inspiration-externe {
background: #78350f;
color: #fde68a;
}
:global(.dark) .outil-card__badge--disponible {
background: #064e3b;
color: #a7f3d0;
}
:global(.dark) .outil-card__badge--recommande {
background: #1e3a5f;
color: #93c5fd;
}
</style>

View File

@@ -0,0 +1,156 @@
<template>
<component
:is="url ? 'a' : 'div'"
v-bind="url ? { href: url, target: '_blank', rel: 'noopener noreferrer' } : {}"
class="simu-feature"
:class="{ 'simu-feature--link': !!url }"
>
<div class="simu-feature__inner">
<div class="simu-feature__left">
<span class="simu-feature__icon" aria-hidden="true">{{ icon }}</span>
<div class="simu-feature__body">
<div class="simu-feature__header">
<h3 class="simu-feature__titre">{{ titre }}</h3>
<span v-if="tag" :class="['simu-feature__badge', `simu-feature__badge--${tag}`]">{{ tagLabel }}</span>
</div>
<p class="simu-feature__desc">{{ description }}</p>
</div>
</div>
<span v-if="cta && url" class="simu-feature__cta">{{ cta }}</span>
</div>
</component>
</template>
<script setup lang="ts">
const props = defineProps<{
icon?: string
titre: string
url?: string | null
description?: string
cta?: string
tag?: string
}>()
const tagLabels: Record<string, string> = {
'outil-aep': 'Outil AEP',
'inspiration-externe': 'Inspiration externe',
'disponible': 'Disponible',
'recommande': 'Recommandé',
'a-venir': 'À venir',
}
const tagLabel = computed(() => props.tag ? (tagLabels[props.tag] ?? props.tag) : '')
</script>
<style scoped>
.simu-feature {
display: block;
padding: 1.5rem 1.75rem;
border-radius: 14px;
border: 1.5px solid var(--nav-bg-alt);
background: var(--nav-surface);
text-decoration: none;
color: var(--nav-text);
transition: box-shadow 0.2s, border-color 0.2s, transform 0.15s;
}
.simu-feature--link:hover {
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
border-color: var(--nav-primary-solid);
transform: translateY(-2px);
}
.simu-feature__inner {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1.5rem;
flex-wrap: wrap;
}
.simu-feature__left {
display: flex;
align-items: flex-start;
gap: 1rem;
flex: 1;
min-width: 0;
}
.simu-feature__icon {
font-size: 2rem;
line-height: 1;
flex-shrink: 0;
margin-top: 2px;
}
.simu-feature__body {
flex: 1;
min-width: 0;
}
.simu-feature__header {
display: flex;
align-items: center;
gap: 0.75rem;
flex-wrap: wrap;
margin-bottom: 0.4rem;
}
.simu-feature__titre {
font-size: 1.05rem;
font-weight: 700;
color: var(--nav-text);
margin: 0;
line-height: 1.3;
}
.simu-feature__badge {
font-size: 0.65rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.04em;
padding: 2px 8px;
border-radius: 999px;
background: #d1fae5;
color: #065f46;
}
.simu-feature__badge--inspiration-externe {
background: #fef3c7;
color: #92400e;
}
.simu-feature__desc {
font-size: 0.88rem;
color: var(--nav-text-muted);
margin: 0;
line-height: 1.55;
}
.simu-feature__cta {
display: inline-flex;
align-items: center;
padding: 0.6rem 1.25rem;
background: var(--nav-primary-solid);
color: var(--nav-text-on-primary);
border-radius: 8px;
font-size: 0.85rem;
font-weight: 600;
white-space: nowrap;
transition: opacity 0.15s;
flex-shrink: 0;
}
.simu-feature--link:hover .simu-feature__cta {
opacity: 0.88;
}
:global(.dark) .simu-feature__badge {
background: #064e3b;
color: #a7f3d0;
}
:global(.dark) .simu-feature__badge--inspiration-externe {
background: #78350f;
color: #fde68a;
}
</style>

201
components/TreeASCII.vue Normal file
View File

@@ -0,0 +1,201 @@
<template>
<ul class="tree-ascii" :class="{ 'tree-ascii--root': depth === 0 }" :style="{ '--depth': depth }">
<li
v-for="(node, i) in tree.children"
:key="i"
class="tree-ascii__node"
>
<!-- Nœud avec enfants : bouton toggle -->
<template v-if="node.children && node.children.length">
<button
class="tree-ascii__branch"
:aria-expanded="!!open[i]"
@click="toggle(i)"
>
<span class="tree-ascii__chevron" aria-hidden="true">{{ open[i] ? '▼' : '▶' }}</span>
<span class="tree-ascii__label">{{ node.name }}</span>
<span class="tree-ascii__count">({{ node.children.length }})</span>
</button>
<TreeASCII
v-if="open[i]"
:tree="node"
:depth="depth + 1"
/>
</template>
<!-- Feuille avec URL : lien cliquable -->
<template v-else-if="node.url">
<a
:href="node.url"
target="_blank"
rel="noopener noreferrer"
class="tree-ascii__leaf tree-ascii__leaf--link"
>
<span class="tree-ascii__prefix" aria-hidden="true"></span>
<span class="tree-ascii__label">{{ node.name }}</span>
<span v-if="node.desc" class="tree-ascii__desc"> {{ node.desc }}</span>
<svg class="tree-ascii__ext" width="10" height="10" 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>
</a>
</template>
<!-- Feuille sans URL -->
<template v-else>
<span class="tree-ascii__leaf">
<span class="tree-ascii__prefix" aria-hidden="true"></span>
<span class="tree-ascii__label">{{ node.name }}</span>
<span v-if="node.desc" class="tree-ascii__desc"> {{ node.desc }}</span>
</span>
</template>
</li>
</ul>
</template>
<script setup lang="ts">
export interface TreeNode {
name: string
url?: string
desc?: string
children?: TreeNode[]
}
export interface TreeData {
name?: string
children?: TreeNode[]
}
const props = withDefaults(defineProps<{
tree: TreeData
depth?: number
}>(), {
depth: 0
})
// Toutes les branches fermées par défaut
const open = ref<Record<number, boolean>>({})
function toggle(i: number) {
open.value[i] = !open.value[i]
}
</script>
<style scoped>
.tree-ascii {
list-style: none;
padding: 0;
margin: 0;
font-family: 'JetBrains Mono', 'Fira Code', 'Courier New', monospace;
font-size: 0.82rem;
padding-left: calc(var(--depth, 0) * 1.25rem + 0.5rem);
}
.tree-ascii--root {
padding-left: 0;
}
.tree-ascii__node {
margin: 2px 0;
line-height: 1.6;
}
/* Bouton branche (nœud avec enfants) */
.tree-ascii__branch {
display: inline-flex;
align-items: center;
gap: 0.35rem;
background: none;
border: none;
cursor: pointer;
padding: 2px 6px;
border-radius: 4px;
color: var(--nav-text);
font-family: inherit;
font-size: inherit;
font-weight: 600;
transition: background 0.1s, color 0.1s;
text-align: left;
}
.tree-ascii__branch:hover {
background: var(--nav-bg-alt);
color: var(--nav-primary-solid);
}
.tree-ascii__chevron {
font-size: 0.65rem;
color: var(--nav-text-muted);
width: 12px;
text-align: center;
flex-shrink: 0;
}
.tree-ascii__count {
font-size: 0.7rem;
color: var(--nav-text-muted);
font-weight: 400;
}
/* Feuille */
.tree-ascii__leaf {
display: inline-flex;
align-items: baseline;
gap: 0.25rem;
padding: 1px 6px;
border-radius: 4px;
text-decoration: none;
color: var(--nav-text-muted);
}
.tree-ascii__leaf--link {
color: var(--nav-text);
cursor: pointer;
transition: background 0.1s, color 0.1s;
}
.tree-ascii__leaf--link:hover {
background: var(--nav-bg-alt);
color: var(--nav-primary-solid);
text-decoration: underline;
}
.tree-ascii__prefix {
color: var(--nav-text-muted);
opacity: 0.5;
font-size: 0.75rem;
flex-shrink: 0;
}
.tree-ascii__label {
font-weight: 500;
}
.tree-ascii__leaf--link .tree-ascii__label {
font-weight: 400;
}
.tree-ascii__desc {
color: var(--nav-text-muted);
font-size: 0.78rem;
font-style: italic;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 60ch;
}
.tree-ascii__ext {
opacity: 0.4;
flex-shrink: 0;
margin-left: 2px;
vertical-align: middle;
}
@media (max-width: 640px) {
.tree-ascii__desc {
display: none;
}
}
</style>

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

533
pages/outils.vue Normal file
View File

@@ -0,0 +1,533 @@
<template>
<div class="outils-page">
<!-- EN-TÊTE PAGE -->
<header class="outils-header">
<div class="outils-header__inner">
<div class="outils-header__icon-wrap" aria-hidden="true">
<img src="/icons/outils-wrench.svg" alt="" class="outils-header__icon" />
</div>
<div>
<h1 class="outils-header__title">Outils</h1>
<p class="outils-header__intro">
En tant qu'architecte, on jongle avec une multitude d'outils simulation, dessin,
calcul, recherche, partage. Les mutualiser, se conseiller dessus, savoir lequel
utiliser quand : c'est une forme d'entraide concrète. Voici ceux que je propose
dans un premier temps. Chacun peut contribuer pour enrichir cette boîte à outils
commune.
</p>
</div>
</div>
</header>
<main class="outils-main">
<!-- SECTION 1 Simulateurs métier -->
<section class="outils-section outils-section--simulateurs" aria-labelledby="sec-simulateurs">
<h2 id="sec-simulateurs" class="outils-section__title">
<span aria-hidden="true">🧮</span> Simulateurs métier
</h2>
<p class="outils-section__subtitle">Créés par AEP outils de calcul situés.</p>
<div class="simu-grid">
<SimulateurFeature
v-for="s in outils?.simulateurs"
:key="s.id"
:icon="s.icon"
:titre="s.titre"
:url="s.url"
:description="s.description"
:cta="s.cta"
:tag="s.tag"
/>
</div>
<!-- Inspirations -->
<div v-if="outils?.simulateurs_inspirations?.length" class="simu-inspirations">
<p class="simu-inspirations__label">Inspiration externe</p>
<div class="outil-cards-grid">
<OutilCard
v-for="s in outils.simulateurs_inspirations"
:key="s.id"
:icon="s.icon"
:titre="s.titre"
:url="s.url"
:description="s.description"
:tag="s.tag"
/>
</div>
</div>
</section>
<!-- SECTION 2 Open source recommandés -->
<section class="outils-section outils-section--opensource" aria-labelledby="sec-opensource">
<h2 id="sec-opensource" class="outils-section__title">
<span aria-hidden="true">🔧</span> Outils tech open source
</h2>
<p class="outils-section__subtitle">Quelques recommandations directes. Le cœur de l'onglet, c'est la section FMHY plus bas.</p>
<div class="outil-cards-grid">
<OutilCard
v-for="outil in outils?.opensource"
:key="outil.id"
:icon="outil.icon"
:titre="outil.titre"
:url="outil.url"
:description="outil.description"
:tag="outil.tag"
/>
</div>
</section>
<!-- SECTION 3 Bifurcation -->
<section class="outils-section" aria-labelledby="sec-bifurcation">
<h2 id="sec-bifurcation" class="outils-section__title">
<span aria-hidden="true">🌿</span> Bifurcation post-études d'archi
</h2>
<p class="outils-section__desc">
{{ outils?.bifurcation?.intro }}
</p>
<!-- 3.1 Vidéos OFQA -->
<div class="bifurcation-block">
<h3 class="bifurcation-block__title">Série vidéo OFQA / ENSA-PB</h3>
<ul class="ofqa-list">
<li
v-for="ep in outils?.bifurcation?.videos_ofqa"
:key="ep.ep"
class="ofqa-list__item"
>
<component
:is="ep.url ? 'a' : 'span'"
v-bind="ep.url ? { href: ep.url, target: '_blank', rel: 'noopener noreferrer' } : {}"
class="ofqa-list__link"
:class="{ 'ofqa-list__link--disabled': !ep.url }"
>
<span class="ofqa-list__ep">EP/{{ ep.ep }}</span>
<span class="ofqa-list__titre">{{ ep.titre }}</span>
<span class="ofqa-list__personnes">— {{ ep.personnes }}</span>
<span v-if="ep.note" class="ofqa-list__note">({{ ep.note }})</span>
</component>
</li>
</ul>
</div>
<!-- 3.2 Coalition -->
<div v-if="outils?.bifurcation?.coalition_ensa_pb" class="bifurcation-block bifurcation-block--coalition">
<h3 class="bifurcation-block__title">{{ outils.bifurcation.coalition_ensa_pb.titre }}</h3>
<p class="bifurcation-block__desc">{{ outils.bifurcation.coalition_ensa_pb.description }}</p>
</div>
<!-- 3.3 Ressources externes -->
<div v-if="outils?.bifurcation?.ressources_externes?.length" class="bifurcation-block">
<h3 class="bifurcation-block__title">Ressources externes</h3>
<div class="outil-cards-grid">
<OutilCard
v-for="r in outils.bifurcation.ressources_externes"
:key="r.id"
:icon="r.icon"
:titre="r.titre"
:url="r.url"
:description="r.description"
/>
</div>
</div>
</section>
<!-- ══════════════ SECTION 4 — FMHY (cœur de la page) ══════════════ -->
<section class="outils-section outils-section--fmhy" aria-labelledby="sec-fmhy">
<h2 id="sec-fmhy" class="outils-section__title">
<span aria-hidden="true">🌳</span> Bibliothèque de ressources libres
</h2>
<p class="outils-section__desc">
Le vrai trésor de l'onglet Outils. FMHY (Free Media Heck Yeah) est la plus grosse
base communautaire d'outils, services et ressources libres/gratuits du web. J'en ai
curé ~50 entrées pertinentes pour un architecte : IA, lecture, dev, vie privée,
formation, médias. Clique sur les branches pour explorer.
</p>
<div class="fmhy-tree-wrap">
<div v-if="fmhyPending" class="fmhy-loading" aria-label="Chargement…">
<span>Chargement</span>
</div>
<div v-else-if="fmhyError" class="fmhy-error">
Impossible de charger les ressources. <a href="https://fmhy.net/" target="_blank" rel="noopener noreferrer">Explorer fmhy.net </a>
</div>
<TreeASCII v-else-if="fmhyData" :tree="fmhyData" :depth="0" />
</div>
<div class="fmhy-footer">
<a href="https://fmhy.net/" target="_blank" rel="noopener noreferrer" class="fmhy-footer__link">
Explorer tout l'arbre → fmhy.net
</a>
</div>
</section>
<!-- ══════════════ SECTION 5 — Placeholder login ══════════════ -->
<section class="outils-section outils-section--placeholder" aria-labelledby="sec-logiciels">
<div class="placeholder-block">
<span class="placeholder-block__badge">Bientôt — nécessite un compte</span>
<h2 id="sec-logiciels" class="placeholder-block__title">{{ outils?.section_5_placeholder?.titre }}</h2>
<p class="placeholder-block__desc">{{ outils?.section_5_placeholder?.description }}</p>
</div>
</section>
<!-- ══════════════ FOOTER CONTRIBUTION ══════════════ -->
<footer class="outils-footer">
<p class="outils-footer__text">
{{ outils?.footer_contribution }}
</p>
</footer>
</main>
</div>
</template>
<script setup lang="ts">
// Chargement des données
const { data: outils } = await useFetch('/data/outils.json')
const { data: fmhyData, pending: fmhyPending, error: fmhyError } = await useFetch('/data/fmhy-curated.json')
useSeoMeta({
title: 'Outils AEP',
description: 'Outils partagés entre architectes : simulateurs, open source, ressources libres FMHY, bifurcation post-études.'
})
</script>
<style scoped>
/* ── Layout global ──────────────────────────────────────────── */
.outils-page {
max-width: 860px;
margin: 0 auto;
padding: 2rem 1.5rem 4rem;
color: var(--nav-text);
}
/* ── Header ─────────────────────────────────────────────────── */
.outils-header {
margin-bottom: 2.5rem;
}
.outils-header__inner {
display: flex;
align-items: flex-start;
gap: 1.25rem;
}
.outils-header__icon-wrap {
width: 3rem;
height: 3rem;
border-radius: 10px;
background: var(--nav-bg-alt);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
padding: 0.6rem;
}
.outils-header__icon {
width: 100%;
height: 100%;
object-fit: contain;
opacity: 0.75;
}
.outils-header__title {
font-size: 1.75rem;
font-weight: 700;
margin: 0 0 0.5rem;
color: var(--nav-text);
}
.outils-header__intro {
font-size: 0.9rem;
color: var(--nav-text-muted);
line-height: 1.65;
margin: 0;
max-width: 70ch;
}
/* ── Sections ───────────────────────────────────────────────── */
.outils-main {
display: flex;
flex-direction: column;
gap: 3rem;
}
.outils-section {
display: flex;
flex-direction: column;
gap: 1rem;
}
.outils-section__title {
font-size: 1.15rem;
font-weight: 700;
margin: 0;
color: var(--nav-text);
display: flex;
align-items: center;
gap: 0.5rem;
padding-bottom: 0.5rem;
border-bottom: 1.5px solid var(--nav-bg-alt);
}
.outils-section__subtitle {
font-size: 0.82rem;
color: var(--nav-text-muted);
margin: -0.5rem 0 0;
font-style: italic;
}
.outils-section__desc {
font-size: 0.88rem;
color: var(--nav-text-muted);
line-height: 1.65;
margin: 0;
max-width: 72ch;
}
/* ── Simulateurs ────────────────────────────────────────────── */
.outils-section--simulateurs {
gap: 1.25rem;
}
.simu-grid {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.simu-inspirations {
margin-top: 0.5rem;
}
.simu-inspirations__label {
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--nav-text-muted);
margin-bottom: 0.5rem;
}
/* ── Cards grid ─────────────────────────────────────────────── */
.outil-cards-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
gap: 0.75rem;
}
/* ── Section FMHY ───────────────────────────────────────────── */
.outils-section--fmhy {
background: var(--nav-bg-alt);
border-radius: 14px;
padding: 1.75rem;
gap: 1.25rem;
margin: 0 -0.25rem;
}
.fmhy-tree-wrap {
background: var(--nav-surface);
border: 1px solid var(--nav-bg-alt);
border-radius: 10px;
padding: 1.25rem 1.5rem;
overflow-x: auto;
}
.fmhy-loading,
.fmhy-error {
font-size: 0.85rem;
color: var(--nav-text-muted);
padding: 0.5rem 0;
}
.fmhy-error a {
color: var(--nav-primary-solid);
}
.fmhy-footer {
text-align: right;
}
.fmhy-footer__link {
font-size: 0.82rem;
font-weight: 600;
color: var(--nav-primary-solid);
text-decoration: none;
transition: opacity 0.15s;
}
.fmhy-footer__link:hover {
opacity: 0.75;
text-decoration: underline;
}
/* ── Bifurcation ────────────────────────────────────────────── */
.bifurcation-block {
display: flex;
flex-direction: column;
gap: 0.6rem;
}
.bifurcation-block__title {
font-size: 0.92rem;
font-weight: 600;
color: var(--nav-text);
margin: 0;
}
.bifurcation-block__desc {
font-size: 0.84rem;
color: var(--nav-text-muted);
margin: 0;
line-height: 1.55;
}
.bifurcation-block--coalition {
background: var(--nav-bg-alt);
border-radius: 8px;
padding: 0.875rem 1rem;
}
/* ── Liste OFQA ─────────────────────────────────────────────── */
.ofqa-list {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: 3px;
}
.ofqa-list__item {
display: flex;
}
.ofqa-list__link {
display: inline-flex;
align-items: baseline;
gap: 0.4rem;
flex-wrap: wrap;
padding: 3px 6px;
border-radius: 5px;
font-size: 0.84rem;
text-decoration: none;
color: var(--nav-text);
transition: background 0.1s;
}
.ofqa-list__link:not(.ofqa-list__link--disabled):hover {
background: var(--nav-bg-alt);
color: var(--nav-primary-solid);
text-decoration: underline;
}
.ofqa-list__link--disabled {
color: var(--nav-text-muted);
cursor: default;
}
.ofqa-list__ep {
font-family: 'JetBrains Mono', 'Courier New', monospace;
font-size: 0.75rem;
font-weight: 600;
color: var(--nav-text-muted);
flex-shrink: 0;
min-width: 4.5rem;
}
.ofqa-list__titre {
font-weight: 500;
}
.ofqa-list__personnes {
color: var(--nav-text-muted);
font-size: 0.82rem;
}
.ofqa-list__note {
color: var(--nav-text-muted);
font-size: 0.78rem;
font-style: italic;
}
/* ── Section 5 placeholder ──────────────────────────────────── */
.outils-section--placeholder {
opacity: 0.6;
}
.placeholder-block {
border: 1.5px dashed var(--nav-bg-alt);
border-radius: 12px;
padding: 1.25rem 1.5rem;
display: flex;
flex-direction: column;
gap: 0.4rem;
}
.placeholder-block__badge {
font-size: 0.68rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--nav-text-muted);
}
.placeholder-block__title {
font-size: 0.95rem;
font-weight: 600;
color: var(--nav-text);
margin: 0;
}
.placeholder-block__desc {
font-size: 0.84rem;
color: var(--nav-text-muted);
margin: 0;
line-height: 1.5;
}
/* ── Footer ─────────────────────────────────────────────────── */
.outils-footer {
padding-top: 1rem;
border-top: 1px solid var(--nav-bg-alt);
text-align: center;
}
.outils-footer__text {
font-size: 0.84rem;
color: var(--nav-text-muted);
margin: 0;
}
/* ── Mobile ─────────────────────────────────────────────────── */
@media (max-width: 640px) {
.outils-page {
padding: 1.25rem 1rem 4rem;
}
.outils-header__inner {
flex-direction: column;
gap: 0.75rem;
}
.outils-header__title {
font-size: 1.4rem;
}
.outil-cards-grid {
grid-template-columns: 1fr;
}
.outils-section--fmhy {
padding: 1.25rem 1rem;
margin: 0 -0.5rem;
}
.fmhy-tree-wrap {
padding: 0.875rem 0.75rem;
}
}
</style>

View File

@@ -1,99 +1,102 @@
{ {
"tree": { "name": "FMHY — Sélection Architecte",
"name": "FMHY — Sélection AEP", "description": "~50 ressources curées depuis FMHY (Free Media Heck Yeah) — pertinentes pour un architecte, un praticien de la transition, un créateur solo.",
"children": [ "children": [
{ {
"name": "IA & LLM", "name": "IA & Outils cognitifs",
"children": [ "children": [
{ "name": "DeepSeek", "url": "https://chat.deepseek.com/", "desc": "DeepSeek V3.2 — modèle libre puissant, sans limite, compte requis" }, { "name": "ChatGPT (OpenAI)", "url": "https://chat.openai.com/", "desc": "LLM généraliste, référence." },
{ "name": "Mistral Chat", "url": "https://chat.mistral.ai/", "desc": "Mistral Large 3, natif français, sans limite" }, { "name": "Claude (Anthropic)", "url": "https://claude.ai/", "desc": "Excellent pour rédaction longue et analyse de documents." },
{ "name": "Google AI Studio", "url": "https://aistudio.google.com/app/prompts/new_chat", "desc": "Gemini 3.1 Pro + API gratuite, idéal pour expérimenter" }, { "name": "Mistral Le Chat", "url": "https://chat.mistral.ai/", "desc": "LLM français, souverain, gratuit." },
{ "name": "Kimi", "url": "https://www.kimi.com/", "desc": "Kimi K2.6 Thinking — raisonnement avancé, login requis" }, { "name": "Perplexity", "url": "https://www.perplexity.ai/", "desc": "Moteur de recherche IA avec sources citées." },
{ "name": "NVIDIA NIM", "url": "https://build.nvidia.com/models", "desc": "Multi-modèles libres (Kimi, Qwen, GLM) sans compte" }, { "name": "Hugging Face", "url": "https://huggingface.co/", "desc": "Hub de modèles open source. Indispensable." },
{ "name": "NotebookLM", "url": "https://notebooklm.google.com/", "desc": "Chatbot sur tes propres documents, prise de notes augmentée" }, { "name": "LM Studio", "url": "https://lmstudio.ai/", "desc": "Faire tourner des LLM localement, sans cloud." }
{ "name": "Ollama", "url": "https://ollama.com/", "desc": "Héberger des modèles IA localement, toutes plateformes" }, ]
{ "name": "Open WebUI", "url": "https://openwebui.com/", "desc": "Interface web pour LLMs locaux (Ollama), self-hostable" }, },
{ "name": "LM Studio", "url": "https://lmstudio.ai/", "desc": "Appli desktop pour modèles IA locaux, sans cloud" }, {
{ "name": "Perplexity", "url": "https://www.perplexity.ai/", "desc": "Moteur de recherche IA avec sources citées" } "name": "Lecture & Documentation",
] "children": [
}, { "name": "Anna's Archive", "url": "https://annas-archive.org/", "desc": "Bibliothèque shadow la plus complète du web. Livres, articles, thèses." },
{ { "name": "Sci-Hub", "url": "https://sci-hub.se/", "desc": "Accès libre aux articles scientifiques payants." },
"name": "Recherche & articles", { "name": "Library Genesis", "url": "https://libgen.is/", "desc": "Livres techniques et académiques en PDF." },
"children": [ { "name": "Z-Library", "url": "https://z-lib.id/", "desc": "Bibliothèque numérique massive, interface soignée." },
{ "name": "SciSpace", "url": "https://scispace.com/", "desc": "Chatbot sur publications scientifiques, explications simplifiées" }, { "name": "OpenLibrary (Internet Archive)", "url": "https://openlibrary.org/", "desc": "Prêt numérique gratuit, millions de livres." },
{ "name": "Alphaxiv", "url": "https://www.alphaxiv.org/", "desc": "Chatbot sur les papiers de recherche arxiv" }, { "name": "Calibre", "url": "https://calibre-ebook.com/", "desc": "Gestion de bibliothèque numérique, convertisseur de formats." }
{ "name": "Unpaywall", "url": "https://unpaywall.org/", "desc": "Accès libre aux articles académiques — extension navigateur" }, ]
{ "name": "Bypass Paywalls Clean", "url": "https://gitflic.ru/project/magnolia1234/bpc_uploads", "desc": "Extension pour bypasser les paywalls d'articles de presse" }, },
{ "name": "Freedium", "url": "https://freedium-mirror.cfd/", "desc": "Accéder aux articles Medium sans abonnement" } {
] "name": "Dessin & Modélisation",
}, "children": [
{ { "name": "FreeCAD", "url": "https://www.freecad.org/", "desc": "Modélisation 3D open source, paramétrique. Alternative à Rhino pour usage simple." },
"name": "Livres & archives", { "name": "Blender", "url": "https://www.blender.org/", "desc": "3D, rendu, animation. La référence open source." },
"children": [ { "name": "Inkscape", "url": "https://inkscape.org/", "desc": "Dessin vectoriel. Alternative à Illustrator." },
{ "name": "Anna's Archive", "url": "https://annas-archive.gl/", "desc": "Meta-search des bibliothèques libres (Z-Lib, LibGen, Sci-Hub)" }, { "name": "GIMP", "url": "https://www.gimp.org/", "desc": "Retouche photo. Alternative à Photoshop." },
{ "name": "Z-Library", "url": "https://z-lib.gd/", "desc": "Bibliothèque de livres/articles, téléchargement direct" }, { "name": "Krita", "url": "https://krita.org/", "desc": "Dessin digital et croquis. Excellent pour les concepts." },
{ "name": "Library Genesis", "url": "https://libgen.li/", "desc": "Livres techniques, universitaires, comics en accès libre" }, { "name": "LibreOffice Draw", "url": "https://www.libreoffice.org/", "desc": "Diagrammes, plans rapides, sans suite Adobe." }
{ "name": "Internet Archive", "url": "https://archive.org/details/texts", "desc": "Livres, magazines, archives historiques numérisés" }, ]
{ "name": "Project Gutenberg", "url": "https://www.gutenberg.org/", "desc": "Classiques du domaine public, 70k+ livres" }, },
{ "name": "Open Library", "url": "https://openlibrary.org/", "desc": "Bibliothèque libre, emprunt numérique de livres" } {
] "name": "Productivité & Texte",
}, "children": [
{ { "name": "Obsidian", "url": "https://obsidian.md/", "desc": "PKM / prise de notes en Markdown. Gratuit pour usage personnel." },
"name": "Cours & formation", { "name": "Logseq", "url": "https://logseq.com/", "desc": "PKM open source, graphe de connaissances." },
"children": [ { "name": "Zotero", "url": "https://www.zotero.org/", "desc": "Gestionnaire de références bibliographiques. Indispensable pour la recherche." },
{ "name": "MIT OpenCourseWare", "url": "https://ocw.mit.edu/", "desc": "Cours MIT en accès libre — toutes disciplines, niveau universitaire" }, { "name": "Marktext", "url": "https://github.com/marktext/marktext", "desc": "Éditeur Markdown WYSIWYG, open source." },
{ "name": "Khan Academy", "url": "https://www.khanacademy.org/", "desc": "Cours gratuits et interactifs, toutes matières, du lycée au supérieur" }, { "name": "Typst", "url": "https://typst.app/", "desc": "Alternative moderne à LaTeX pour la mise en page de documents." },
{ "name": "edX", "url": "https://www.edx.org/", "desc": "MOOCs certifiants des meilleures universités mondiales" }, { "name": "Pandoc", "url": "https://pandoc.org/", "desc": "Conversion universelle entre formats de documents (MD, DOCX, PDF, HTML...)." }
{ "name": "Learn Anything", "url": "https://learn-anything.xyz/", "desc": "Moteur de recherche de parcours d'apprentissage structurés" }, ]
{ "name": "OpenCulture", "url": "https://www.openculture.com/", "desc": "Cours, films, livres audio en accès libre, liens curatés" }, },
{ "name": "Appropedia", "url": "https://www.appropedia.org/", "desc": "Wiki de la durabilité, technologies appropriées, habitat écologique" } {
] "name": "Dev & Infrastructure",
}, "children": [
{ { "name": "VS Code", "url": "https://code.visualstudio.com/", "desc": "Éditeur de code. La référence, gratuit." },
"name": "Outils dev & infra", { "name": "Coolify", "url": "https://coolify.io/", "desc": "Self-hosting simplifié. Alternative à Heroku/Vercel sur son propre VPS." },
"children": [ { "name": "Hetzner Cloud", "url": "https://www.hetzner.com/cloud/", "desc": "VPS européen, tarifs très bas, data centers Allemagne." },
{ "name": "Free for Developers", "url": "https://free-for.dev/", "desc": "Index exhaustif des outils gratuits pour développeurs" }, { "name": "Caddy", "url": "https://caddyserver.com/", "desc": "Serveur web avec HTTPS automatique. Plus simple que Nginx." },
{ "name": "DevDocs", "url": "https://devdocs.io/", "desc": "Documentation développeur unifiée, consultable hors ligne" }, { "name": "n8n", "url": "https://n8n.io/", "desc": "Automatisation open source (comme Zapier mais self-hostable)." },
{ "name": "N8N", "url": "https://n8n.io/", "desc": "Automatisation de workflows open source, self-hostable" }, { "name": "Gitea", "url": "https://gitea.io/", "desc": "Hébergement Git self-hosted. Alternative à GitHub." }
{ "name": "NocoDB", "url": "https://github.com/nocodb/nocodb", "desc": "Base de données no-code open source, alternative Airtable" }, ]
{ "name": "Crontab Guru", "url": "https://crontab.guru/", "desc": "Éditeur de tâches cron avec explication lisible" }, },
{ "name": "Dokploy", "url": "https://github.com/dokploy/dokploy", "desc": "Déploiement d'apps auto-hébergé, alternative Coolify/Heroku" }, {
{ "name": "Open Source Guides", "url": "https://opensource.guide/", "desc": "Guides pour contribuer et gérer des projets open source" } "name": "Vie privée & Sécurité",
] "children": [
}, { "name": "Bitwarden", "url": "https://bitwarden.com/", "desc": "Gestionnaire de mots de passe open source. Self-hostable." },
{ { "name": "ProtonMail", "url": "https://proton.me/mail", "desc": "Email chiffré, hébergement Suisse." },
"name": "Texte & documents", { "name": "Signal", "url": "https://signal.org/", "desc": "Messagerie chiffrée E2E. La référence." },
"children": [ { "name": "uBlock Origin", "url": "https://ublockorigin.com/", "desc": "Bloqueur de publicités et trackers, le plus efficace." },
{ "name": "DeepL", "url": "https://www.deepl.com/translator", "desc": "Traduction IA, meilleure qualité disponible en FR/EN/DE" }, { "name": "Mullvad VPN", "url": "https://mullvad.net/", "desc": "VPN respectueux de la vie privée, sans compte email requis." },
{ "name": "Paperless-ngx", "url": "https://docs.paperless-ngx.com/", "desc": "Gestion documentaire self-hosted avec OCR et indexation" }, { "name": "Privacy Guides", "url": "https://www.privacyguides.org/", "desc": "Recommandations d'outils respectueux de la vie privée, par thème." }
{ "name": "Whisper (OpenAI)", "url": "https://github.com/openai/whisper", "desc": "Transcription audio open source, multi-langues, qualité professionnelle" }, ]
{ "name": "GitHub Gists", "url": "https://gist.github.com/", "desc": "Partage de snippets code / notes, versionné, dans l'écosystème git" }, },
{ "name": "PrivateBin", "url": "https://privatebin.net/", "desc": "Pastebin chiffré zero-knowledge, self-hostable" }, {
{ "name": "LibreTranslate", "url": "https://libretranslate.com/", "desc": "Traducteur FOSS, self-hostable, sans tracking" } "name": "Formation & Apprentissage",
] "children": [
}, { "name": "MIT OpenCourseWare", "url": "https://ocw.mit.edu/", "desc": "Cours du MIT en libre accès, toutes disciplines." },
{ { "name": "Khan Academy", "url": "https://www.khanacademy.org/", "desc": "Maths, sciences, programmation — gratuit, pédagogie excellente." },
"name": "Vie privée & sécurité", { "name": "YouTube (canaux techniques)", "url": "https://www.youtube.com/", "desc": "Channals : The Coding Train, Fireship, 3Blue1Brown, etc." },
"children": [ { "name": "freeCodeCamp", "url": "https://www.freecodecamp.org/", "desc": "Apprendre le développement web de zéro, gratuit et certifiant." },
{ "name": "uBlock Origin", "url": "https://github.com/gorhill/uBlock", "desc": "Bloqueur de publicités et trackers — le plus efficace du marché" }, { "name": "Coursera (audit gratuit)", "url": "https://www.coursera.org/", "desc": "Cours universitaires, audit gratuit disponible sur la plupart." }
{ "name": "Bitwarden", "url": "https://bitwarden.com/", "desc": "Gestionnaire de mots de passe open source, cloud ou self-hosted" }, ]
{ "name": "KeePassXC", "url": "https://keepassxc.org/", "desc": "Gestionnaire de mots de passe local, base chiffrée, sans cloud" }, },
{ "name": "Pi-Hole", "url": "https://pi-hole.net/", "desc": "Bloqueur de pub DNS pour tout le réseau domestique, self-hosted" }, {
{ "name": "SponsorBlock", "url": "https://sponsor.ajay.app/", "desc": "Extension pour skipper les segments sponsorisés sur YouTube" }, "name": "Médias & Audio",
{ "name": "AdGuard Home", "url": "https://adguard.com/en/adguard-home/overview.html", "desc": "Alternative Pi-Hole, DNS adblocking self-hosted pour tout le réseau" } "children": [
] { "name": "Audacity", "url": "https://www.audacityteam.org/", "desc": "Enregistrement et édition audio. Référence open source." },
}, { "name": "yt-dlp", "url": "https://github.com/yt-dlp/yt-dlp", "desc": "Télécharger des vidéos/audio depuis YouTube et 1000+ sites." },
{ { "name": "VLC", "url": "https://www.videolan.org/vlc/", "desc": "Lecteur multimédia universel." },
"name": "Documentaires & médias", { "name": "Kdenlive", "url": "https://kdenlive.org/", "desc": "Montage vidéo open source, non linéaire." },
"children": [ { "name": "OBS Studio", "url": "https://obsproject.com/", "desc": "Enregistrement et streaming vidéo. La référence gratuite." }
{ "name": "Films For Action", "url": "https://www.filmsforaction.org/", "desc": "Films documentaires engagés, alternatives sociales et écologiques" }, ]
{ "name": "ARTE (replay)", "url": "https://www.arte.tv/", "desc": "Documentaires et programmes culturels FR/DE, replay gratuit" }, },
{ "name": "Documentary+", "url": "https://www.docplus.com/", "desc": "Plateforme de documentaires en streaming gratuit" }, {
{ "name": "TED Talks", "url": "https://www.ted.com/", "desc": "Conférences inspirantes sur science, société, design, architecture" } "name": "Divers Utiles",
] "children": [
} { "name": "Nextcloud", "url": "https://nextcloud.com/", "desc": "Cloud personnel self-hosted. Alternative à Google Drive/Dropbox." },
] { "name": "Joplin", "url": "https://joplinapp.org/", "desc": "Notes chiffrées, sync Nextcloud, open source." },
}, { "name": "draw.io / diagrams.net", "url": "https://app.diagrams.net/", "desc": "Diagrammes et schémas, gratuit, pas de compte requis." },
"source": "fmhy.net (sélection AEP 2026-05-22)", { "name": "Excalidraw", "url": "https://excalidraw.com/", "desc": "Tableau blanc collaboratif, style hand-drawn, open source." },
"total_entries": 50 { "name": "Fmhy.net (complet)", "url": "https://fmhy.net/", "desc": "L'arbre complet. Des milliers de ressources organisées par thème." }
]
}
]
} }

90
public/data/outils.json Normal file
View File

@@ -0,0 +1,90 @@
{
"simulateurs": [
{
"id": "autonomie",
"icon": "🟢",
"titre": "Simulateur Autonomie",
"url": "https://calculs.trans-former.fr/autonomie/",
"description": "Évaluer le degré d'autonomie d'une famille à un site donné selon les ressources locales (eau, énergie, alimentation).",
"cta": "Lancer le simulateur →",
"tag": "outil-aep"
}
],
"simulateurs_inspirations": [
{
"id": "florquin-prix-m2",
"icon": "💡",
"titre": "Estimation prix au m² — Florquin Studio",
"url": "https://offre.florquinstudio.com/",
"description": "Une agence parisienne qui a construit un système d'estimation au m² assez fin. Pas dans nos outils, mais inspirant pour qui veut industrialiser son chiffrage.",
"tag": "inspiration-externe"
}
],
"opensource": [
{
"id": "dictee-universelle-groq",
"icon": "🎤",
"titre": "Dictée universelle Groq",
"url": "https://github.com/Jayjay-nene/dictee-universelle-groq",
"description": "Appuie sur une touche, parle, le texte apparaît au curseur avec ponctuation et majuscules auto. Transcription Whisper Groq < 1s. Marche dans toutes les applis Windows. Outil par Jayjay-nene.",
"tag": "recommande"
},
{
"id": "atis-voice",
"icon": "🎙",
"titre": "Atis Voice (text-to-speech)",
"url": null,
"description": "Pipeline TTS pour transformer un texte en audio.",
"tag": "disponible"
},
{
"id": "install-vps",
"icon": "🖥",
"titre": "Install VPS open source",
"url": null,
"description": "Setup pas-à-pas pour monter son propre VPS (Hetzner, Coolify, Caddy, Postgres…) en mode reproductible.",
"tag": "a-venir"
},
{
"id": "skills-claude-code",
"icon": "⚙",
"titre": "Skills Claude Code",
"url": null,
"description": "Skills custom pour booster sa pratique avec un agent IA.",
"tag": "a-venir"
}
],
"bifurcation": {
"intro": "Beaucoup de jeunes diplômés en archi cherchent des chemins alternatifs. Cette section rassemble des témoignages, expériences, et ressources sur ce que c'est que de bifurquer.",
"videos_ofqa": [
{ "ep": "01", "titre": "Architectes indépendants", "personnes": "Jules Nény & Imane Fatmi", "url": "https://youtu.be/aMreB5KdNhY" },
{ "ep": "02", "titre": "Artiste & Maçon", "personnes": "Romane Dutour & Maël Canal", "url": "https://youtu.be/9gpjokx2ndI" },
{ "ep": "03", "titre": "Social & BTP", "personnes": "Célia Berdy & Esilda Perrot", "url": null, "note": "vidéo perdue, doc PDF seulement" },
{ "ep": "04", "titre": "Menuisier & Paysagiste", "personnes": "Adel Mohamedi & Julie Bowie", "url": "https://youtu.be/yKaRQhA3Z6g" },
{ "ep": "05", "titre": "Éco-construction", "personnes": "Edouard Vermès", "url": "https://youtu.be/97bDg1BjeuQ" },
{ "ep": "06", "titre": "Musicien & Urbaniste", "personnes": "Ruben Madar & Antoine Troccaz", "url": "https://drive.google.com/drive/folders/14g8YBn5bZAy8aIkHzQlOTrTtnWOaqRO3" },
{ "ep": "07", "titre": "AMO & Réemploi", "personnes": "Domitille Chaigne & Clémence Bondon", "url": "https://drive.google.com/file/d/1Q9Za81CElszmMn5n8dBsG0pJkiWmB32c/view" },
{ "ep": "08", "titre": "Gouvernance école", "personnes": "Solenn Guével", "url": "https://drive.google.com/drive/folders/1UaLsSyQcJydkXyV71klrY1tv9KgAh-mG" },
{ "ep": "bonus", "titre": "PFE — invitation à faire collectif", "personnes": "Jules Nény & Imane Fatmi", "url": "https://youtu.be/4qTEIC2Lmqw" }
],
"coalition_ensa_pb": {
"titre": "Coalition inter-asso ENSA-PB — victoire salle des enseignants",
"description": "Une coalition d'associations étudiantes a obtenu l'usage de la salle des enseignants pour des temps de travail collectif."
},
"ressources_externes": [
{
"id": "drop-the-kutch",
"icon": "🎧",
"titre": "Podcast Drop the Kutch — Sâm Afchar",
"url": "https://podcasts-francais.fr/podcast/drop-the-kutsch",
"description": "Témoignages de bifurcations post études d'archi. Super taf."
}
]
},
"section_5_placeholder": {
"titre": "[V2 — Login] Logiciels pro",
"description": "Logiciels lourds (Adobe, AutoCAD…) — accès mutualisé. Disponible après création de compte AEP.",
"status": "bientot-login"
},
"footer_contribution": "Tu utilises un outil qui mérite d'être ici ? Écris-moi : contact@trans-former.fr"
}

12
public/icons/CREDITS.md Normal file
View File

@@ -0,0 +1,12 @@
# Icons Credits
## outils-wrench.svg
**Source :** Heroicons — `wrench-screwdriver` icon
**Licence :** MIT
**URL :** https://heroicons.com/
**Note :** Fallback Heroicons utilisé car l'API Noun Project a retourné 403 Forbidden au moment du build (2026-05-22). L'icône `wrench-screwdriver` de Heroicons (MIT) est une clé à molette + tournevis, style line art minimaliste, compatible avec l'identité visuelle AEP.
Si tu veux substituer par une icône Noun Project, utilise l'endpoint :
`GET https://api.thenounproject.com/v2/icon?query=wrench&limit=20`
avec les credentials OAuth 1.0a stockées dans `_System/API-credentials.md`.

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="M11.42 15.17 17.25 21A2.652 2.652 0 0 0 21 17.25l-5.877-5.877M11.42 15.17l2.496-3.03c.317-.384.74-.626 1.208-.766M11.42 15.17l-4.655 5.653a2.548 2.548 0 1 1-3.586-3.586l5.654-4.654m5.598-2.337 3.07-3.293a2.25 2.25 0 0 0-3.182-3.182l-3.293 3.07M6.75 12.75l-2.25.75.75-2.25 2.25-.75-.75 2.25Z" />
</svg>

After

Width:  |  Height:  |  Size: 493 B