Compare commits
11 Commits
feat/outil
...
04b023518a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
04b023518a | ||
|
|
f3ab28bd69 | ||
|
|
cd8fe9e258 | ||
|
|
ea7c8cc91e | ||
|
|
538c490e76 | ||
|
|
d584d04e3d | ||
|
|
bd95c0f00d | ||
|
|
db8f614928 | ||
|
|
59cb81a055 | ||
|
|
91e3466ec6 | ||
|
|
422f45116f |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -7,3 +7,4 @@ dist
|
|||||||
*.log
|
*.log
|
||||||
|
|
||||||
*.tmp.*
|
*.tmp.*
|
||||||
|
.firecrawl/
|
||||||
|
|||||||
17
app.vue
17
app.vue
@@ -22,12 +22,19 @@
|
|||||||
|
|
||||||
<!-- ── 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"
|
||||||
:class="{ 'nav-tab--active': route.path === '/' }"
|
:class="{ 'nav-tab--active': route.path === '/' }"
|
||||||
>
|
>
|
||||||
Écosystème Entraide Architecture
|
Écosystème Entraide
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
<NuxtLink
|
<NuxtLink
|
||||||
to="/agences"
|
to="/agences"
|
||||||
@@ -51,12 +58,11 @@
|
|||||||
Codev
|
Codev
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
<NuxtLink
|
<NuxtLink
|
||||||
to="/rag"
|
to="/media"
|
||||||
class="nav-tab"
|
class="nav-tab"
|
||||||
:class="{ 'nav-tab--active': route.path === '/rag' }"
|
:class="{ 'nav-tab--active': route.path.startsWith('/media') }"
|
||||||
>
|
>
|
||||||
RAG
|
recherche-média
|
||||||
<span class="nav-tab-badge">en construction</span>
|
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
@@ -172,6 +178,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
448
components/ChatbotPensees.vue
Normal file
448
components/ChatbotPensees.vue
Normal file
@@ -0,0 +1,448 @@
|
|||||||
|
<template>
|
||||||
|
<div class="chatbot-pensees" :class="{ inline: inline }">
|
||||||
|
<!-- Header compact -->
|
||||||
|
<div class="chatbot-header">
|
||||||
|
<div class="chatbot-header-left">
|
||||||
|
<div class="chatbot-icon">
|
||||||
|
<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">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
<span class="chatbot-title">
|
||||||
|
<template v-if="auteurContext">{{ auteurContext }}</template>
|
||||||
|
<template v-else>Bibliothèque des pensées</template>
|
||||||
|
</span>
|
||||||
|
<span v-if="auteurContext" class="chatbot-corpus-badge">auteur</span>
|
||||||
|
<span v-else class="chatbot-corpus-badge">FRACAS</span>
|
||||||
|
</div>
|
||||||
|
<div class="chatbot-header-right">
|
||||||
|
<button
|
||||||
|
v-if="messages.length > 0"
|
||||||
|
class="chatbot-clear-btn"
|
||||||
|
title="Effacer la conversation"
|
||||||
|
@click="clearMessages"
|
||||||
|
>
|
||||||
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" aria-hidden="true">
|
||||||
|
<polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<select v-model="corpusMode" class="chatbot-corpus-select" title="Corpus interrogé">
|
||||||
|
<option value="pensees">Pensées éco.</option>
|
||||||
|
<option value="both">Pensées + Projets</option>
|
||||||
|
<option value="projets">Projets</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Zone messages -->
|
||||||
|
<div ref="messagesContainer" class="chatbot-messages">
|
||||||
|
<!-- Onboarding -->
|
||||||
|
<div v-if="messages.length === 0" class="chatbot-onboarding">
|
||||||
|
<template v-if="auteurContext">
|
||||||
|
<p>Interroge le corpus de <strong>{{ auteurContext }}</strong> — ses livres, ses thèses, ses arguments.</p>
|
||||||
|
<p class="chatbot-onboarding-hint">Ex : "Quelle est sa critique du capitalisme vert ?"</p>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<p>Bibliothèque des pensées écologiques — <strong>~140 auteurs FRACAS</strong>, de l'écosocialisme à l'éthique environnementale.</p>
|
||||||
|
<p class="chatbot-onboarding-hint">Ex : "Quelles différences entre décroissance et doughnut économics ?"</p>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Messages -->
|
||||||
|
<template v-for="(msg, i) in messages" :key="i">
|
||||||
|
<div v-if="msg.role === 'user'" class="chatbot-bubble user">{{ msg.content }}</div>
|
||||||
|
<div v-else class="chatbot-bubble assistant">
|
||||||
|
<div class="chatbot-md" v-html="renderMd(msg.content)" />
|
||||||
|
<div v-if="msg.auteur" class="chatbot-auteur-tag">
|
||||||
|
<span>{{ msg.auteur.nom }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Chargement -->
|
||||||
|
<div v-if="loading" class="chatbot-bubble assistant loading">
|
||||||
|
<span class="dot" /><span class="dot" /><span class="dot" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Erreur -->
|
||||||
|
<div v-if="errorMsg" class="chatbot-error">{{ errorMsg }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Input -->
|
||||||
|
<div class="chatbot-input-row">
|
||||||
|
<input
|
||||||
|
v-model="inputText"
|
||||||
|
type="text"
|
||||||
|
:disabled="loading"
|
||||||
|
:placeholder="auteurContext ? `Interroger ${auteurContext}…` : 'Pose ta question sur les pensées éco…'"
|
||||||
|
class="chatbot-input"
|
||||||
|
@keydown.enter.prevent="sendMessage"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
:disabled="loading || !inputText.trim()"
|
||||||
|
class="chatbot-send-btn"
|
||||||
|
aria-label="Envoyer"
|
||||||
|
@click="sendMessage"
|
||||||
|
>
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||||
|
<line x1="22" y1="2" x2="11" y2="13"/>
|
||||||
|
<polygon points="22 2 15 22 11 13 2 9 22 2"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useMarkdown } from '~/composables/useMarkdown'
|
||||||
|
|
||||||
|
const { render: renderMd } = useMarkdown()
|
||||||
|
|
||||||
|
interface ChatMessage {
|
||||||
|
role: 'user' | 'assistant'
|
||||||
|
content: string
|
||||||
|
auteur?: { slug: string; nom: string } | null
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
auteurContext: string | null
|
||||||
|
inline?: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const messages = ref<ChatMessage[]>([])
|
||||||
|
const inputText = ref('')
|
||||||
|
const loading = ref(false)
|
||||||
|
const errorMsg = ref('')
|
||||||
|
const corpusMode = ref<'pensees' | 'projets' | 'both'>('pensees')
|
||||||
|
const messagesContainer = ref<HTMLElement | null>(null)
|
||||||
|
|
||||||
|
watch(() => props.auteurContext, () => {
|
||||||
|
messages.value = []
|
||||||
|
errorMsg.value = ''
|
||||||
|
})
|
||||||
|
|
||||||
|
function clearMessages() {
|
||||||
|
messages.value = []
|
||||||
|
errorMsg.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function scrollToBottom() {
|
||||||
|
if (messagesContainer.value) {
|
||||||
|
messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendMessage() {
|
||||||
|
const query = inputText.value.trim()
|
||||||
|
if (!query || loading.value) return
|
||||||
|
|
||||||
|
inputText.value = ''
|
||||||
|
errorMsg.value = ''
|
||||||
|
messages.value.push({ role: 'user', content: query })
|
||||||
|
loading.value = true
|
||||||
|
|
||||||
|
await nextTick()
|
||||||
|
scrollToBottom()
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await $fetch<{
|
||||||
|
response: string
|
||||||
|
auteur: { slug: string; nom: string } | null
|
||||||
|
}>('/api/chatbot-pensees', {
|
||||||
|
method: 'POST',
|
||||||
|
body: {
|
||||||
|
query,
|
||||||
|
corpus: corpusMode.value,
|
||||||
|
auteur_slug: props.auteurContext ? slugify(props.auteurContext) : undefined,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
messages.value.push({
|
||||||
|
role: 'assistant',
|
||||||
|
content: res.response,
|
||||||
|
auteur: res.auteur ?? null,
|
||||||
|
})
|
||||||
|
} catch (e: any) {
|
||||||
|
const status = e?.statusCode ?? e?.status
|
||||||
|
if (status === 429) {
|
||||||
|
errorMsg.value = 'Limite de 20 questions par jour atteinte.'
|
||||||
|
} else if (status === 503) {
|
||||||
|
errorMsg.value = 'Le RAG est indisponible pour l\'instant — réessaie dans quelques minutes.'
|
||||||
|
} else if (status === 504) {
|
||||||
|
errorMsg.value = 'Le RAG met du temps à répondre — réessaie dans quelques secondes.'
|
||||||
|
} else {
|
||||||
|
errorMsg.value = 'Une erreur est survenue. Réessaie dans quelques instants.'
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
await nextTick()
|
||||||
|
scrollToBottom()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function slugify(str: string): string {
|
||||||
|
return str.toLowerCase()
|
||||||
|
.normalize('NFD').replace(/[̀-ͯ]/g, '')
|
||||||
|
.replace(/[^a-z0-9]+/g, '-')
|
||||||
|
.replace(/^-|-$/g, '')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.chatbot-pensees {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 0;
|
||||||
|
background: var(--nav-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header */
|
||||||
|
.chatbot-header {
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 6px 12px;
|
||||||
|
background: var(--nav-surface);
|
||||||
|
border-bottom: 1px solid rgba(180, 170, 160, 0.22);
|
||||||
|
min-height: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatbot-header-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatbot-icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--nav-primary);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: var(--nav-text-on-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatbot-title {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--nav-text);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
max-width: 180px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatbot-corpus-badge {
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-size: 0.65rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
color: var(--nav-text-muted);
|
||||||
|
background: var(--nav-bg-alt);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 1px 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatbot-header-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatbot-clear-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--nav-text-muted);
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.15s, color 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatbot-clear-btn:hover {
|
||||||
|
background: var(--nav-bg-alt);
|
||||||
|
color: var(--nav-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatbot-corpus-select {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
padding: 2px 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid rgba(180, 170, 160, 0.3);
|
||||||
|
background: var(--nav-bg-alt);
|
||||||
|
color: var(--nav-text-muted);
|
||||||
|
cursor: pointer;
|
||||||
|
max-width: 110px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Messages */
|
||||||
|
.chatbot-messages {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 10px 12px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatbot-onboarding {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
line-height: 1.55;
|
||||||
|
color: var(--nav-text-muted);
|
||||||
|
background: var(--nav-bg-alt);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatbot-onboarding p { margin: 0 0 6px; }
|
||||||
|
.chatbot-onboarding p:last-child { margin-bottom: 0; }
|
||||||
|
.chatbot-onboarding strong { color: var(--nav-text); font-weight: 600; }
|
||||||
|
.chatbot-onboarding-hint { font-style: italic; font-size: 0.75rem; opacity: 0.75; }
|
||||||
|
|
||||||
|
/* Bulles */
|
||||||
|
.chatbot-bubble {
|
||||||
|
max-width: 88%;
|
||||||
|
font-size: 0.825rem;
|
||||||
|
line-height: 1.55;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 8px 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatbot-bubble.user {
|
||||||
|
align-self: flex-end;
|
||||||
|
background: var(--nav-primary);
|
||||||
|
color: var(--nav-text-on-primary);
|
||||||
|
border-radius: 10px 10px 3px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatbot-bubble.assistant {
|
||||||
|
align-self: flex-start;
|
||||||
|
background: var(--nav-surface);
|
||||||
|
border: 1px solid rgba(180, 170, 160, 0.22);
|
||||||
|
color: var(--nav-text);
|
||||||
|
border-radius: 10px 10px 10px 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Markdown dans les bulles */
|
||||||
|
:deep(.chatbot-md) { font-size: inherit; line-height: 1.55; }
|
||||||
|
:deep(.chatbot-md p) { margin: 0 0 0.35em; }
|
||||||
|
:deep(.chatbot-md p:last-child) { margin-bottom: 0; }
|
||||||
|
:deep(.chatbot-md strong) { font-weight: 700; }
|
||||||
|
:deep(.chatbot-md em) { font-style: italic; }
|
||||||
|
:deep(.chatbot-md ul) { margin: 0.25em 0 0.25em 1em; list-style: disc; padding: 0; }
|
||||||
|
:deep(.chatbot-md li) { margin-bottom: 0.1em; }
|
||||||
|
|
||||||
|
.chatbot-auteur-tag {
|
||||||
|
margin-top: 6px;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
color: var(--nav-text-muted);
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loading */
|
||||||
|
.chatbot-bubble.loading {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--nav-text-muted);
|
||||||
|
animation: blink 1.2s infinite ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot:nth-child(2) { animation-delay: 0.2s; }
|
||||||
|
.dot:nth-child(3) { animation-delay: 0.4s; }
|
||||||
|
|
||||||
|
@keyframes blink {
|
||||||
|
0%, 80%, 100% { opacity: 0.25; }
|
||||||
|
40% { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Erreur */
|
||||||
|
.chatbot-error {
|
||||||
|
align-self: flex-start;
|
||||||
|
font-size: 0.775rem;
|
||||||
|
color: #a85d3e;
|
||||||
|
background: rgba(168, 93, 62, 0.08);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 7px 10px;
|
||||||
|
max-width: 90%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Input */
|
||||||
|
.chatbot-input-row {
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
border-top: 1px solid rgba(180, 170, 160, 0.22);
|
||||||
|
background: var(--nav-surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatbot-input {
|
||||||
|
flex: 1;
|
||||||
|
padding: 6px 10px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid rgba(180, 170, 160, 0.3);
|
||||||
|
background: var(--nav-bg);
|
||||||
|
color: var(--nav-text);
|
||||||
|
font-family: var(--nav-font);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
min-width: 0;
|
||||||
|
transition: border-color 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatbot-input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--nav-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatbot-input:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatbot-send-btn {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
border-radius: 7px;
|
||||||
|
border: none;
|
||||||
|
background: var(--nav-primary);
|
||||||
|
color: var(--nav-text-on-primary);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: opacity 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatbot-send-btn:disabled {
|
||||||
|
opacity: 0.35;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatbot-send-btn:not(:disabled):hover {
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="media-visuel">
|
<div class="media-visuel">
|
||||||
|
|
||||||
<!-- Conteneur split / plein ecran -->
|
<!-- Conteneur principal : carte → toolbar → chatbot (ou bonpote/rag-backend) -->
|
||||||
<div class="layout-container">
|
<div class="layout-container">
|
||||||
|
|
||||||
<!-- Slot carte D3 -->
|
<!-- SLOT CARTE D3 (mode carte uniquement) -->
|
||||||
<div
|
<div
|
||||||
|
v-if="contentView === 'carte'"
|
||||||
class="carte-slot"
|
class="carte-slot"
|
||||||
:class="[
|
:class="[
|
||||||
layoutMode === 'split' ? 'carte-split' : '',
|
layoutMode === 'split' ? 'carte-split' : '',
|
||||||
@@ -44,10 +45,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Barre de toggle -->
|
<!-- BARRE DE TOGGLE (entre carte et chatbot, toujours visible) -->
|
||||||
<div class="layout-toggle-bar shrink-0">
|
<div class="layout-toggle-bar shrink-0">
|
||||||
|
|
||||||
|
<!-- Gauche : contrôles layout (seulement en mode carte) -->
|
||||||
|
<template v-if="contentView === 'carte'">
|
||||||
<button
|
<button
|
||||||
@click="setLayoutMode('carte-full')"
|
@click="layoutMode = 'carte-full'"
|
||||||
:class="{ active: layoutMode === 'carte-full' }"
|
:class="{ active: layoutMode === 'carte-full' }"
|
||||||
class="toggle-btn"
|
class="toggle-btn"
|
||||||
title="Carte en plein ecran"
|
title="Carte en plein ecran"
|
||||||
@@ -60,7 +64,7 @@
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="layoutMode !== 'split'"
|
v-if="layoutMode !== 'split'"
|
||||||
@click="setLayoutMode('split')"
|
@click="layoutMode = 'split'"
|
||||||
class="toggle-btn"
|
class="toggle-btn"
|
||||||
title="Vue partagee"
|
title="Vue partagee"
|
||||||
>
|
>
|
||||||
@@ -70,36 +74,23 @@
|
|||||||
Vue partagee
|
Vue partagee
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
@click="setLayoutMode('chatbot-full')"
|
@click="layoutMode = 'chatbot-full'"
|
||||||
:class="{ active: layoutMode === 'chatbot-full' }"
|
:class="{ active: layoutMode === 'chatbot-full' }"
|
||||||
class="toggle-btn"
|
class="toggle-btn"
|
||||||
title="Chatbot en plein ecran"
|
title="Chatbot plein ecran"
|
||||||
>
|
>
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
<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"/>
|
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
|
||||||
</svg>
|
</svg>
|
||||||
Chatbot plein ecran
|
Chatbot plein ecran
|
||||||
</button>
|
</button>
|
||||||
<button
|
</template>
|
||||||
@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 -->
|
<!-- Droite : contrôles contenu (toujours, indépendants du layoutMode) -->
|
||||||
<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" />
|
<!-- Slider opacité PDF -->
|
||||||
📄 Carte FRACAS (PDF)
|
|
||||||
</label>
|
|
||||||
<input
|
<input
|
||||||
v-if="showFracasPdf"
|
v-if="showFracasPdf && contentView === 'carte'"
|
||||||
type="range"
|
type="range"
|
||||||
min="0"
|
min="0"
|
||||||
max="100"
|
max="100"
|
||||||
@@ -107,11 +98,56 @@
|
|||||||
class="opacity-slider"
|
class="opacity-slider"
|
||||||
:title="`Opacité ${fracasOpacity}%`"
|
:title="`Opacité ${fracasOpacity}%`"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<!-- CARTE PRINCIPALE -->
|
||||||
|
<button
|
||||||
|
@click="showCarte"
|
||||||
|
:class="{ active: contentView === 'carte' }"
|
||||||
|
class="toggle-btn"
|
||||||
|
title="Vue principale : carte D3 + chatbot"
|
||||||
|
>
|
||||||
|
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||||
|
<circle cx="12" cy="12" r="2"/><path d="M12 2a10 10 0 0 0-7.07 17.07M12 2a10 10 0 0 1 7.07 17.07M3.34 7h17.32M3.34 17h17.32"/>
|
||||||
|
</svg>
|
||||||
|
CARTE PRINCIPALE
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Tickbox PDF + carte des pensées -->
|
||||||
|
<div class="carte-pensees-ctrl">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
v-model="showFracasPdf"
|
||||||
|
class="fracas-check"
|
||||||
|
title="Superposer la carte FRACAS en PDF"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
@click="contentView = 'bonpote'"
|
||||||
|
:class="{ active: contentView === 'bonpote' }"
|
||||||
|
class="toggle-btn carte-pensees-btn"
|
||||||
|
title="Carte des pensées écologiques — référence FRACAS Bonpote V2"
|
||||||
|
>
|
||||||
|
📗 carte des pensées écologiques
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Poignee draggable (visible uniquement en mode split, pas sur mobile) -->
|
<!-- RAG backend -->
|
||||||
|
<button
|
||||||
|
@click="contentView = 'rag-backend'"
|
||||||
|
:class="{ active: contentView === 'rag-backend' }"
|
||||||
|
class="toggle-btn"
|
||||||
|
title="Interface LightRAG backend"
|
||||||
|
>
|
||||||
|
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||||
|
<circle cx="12" cy="12" r="3"/><path d="M19.07 4.93a10 10 0 0 1 0 14.14M4.93 4.93a10 10 0 0 0 0 14.14"/>
|
||||||
|
</svg>
|
||||||
|
RAG backend
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- POIGNEE DRAGGABLE (split uniquement) -->
|
||||||
<div
|
<div
|
||||||
v-if="layoutMode === 'split'"
|
v-if="contentView === 'carte' && layoutMode === 'split'"
|
||||||
class="split-handle"
|
class="split-handle"
|
||||||
@mousedown.prevent="onHandleMousedown"
|
@mousedown.prevent="onHandleMousedown"
|
||||||
title="Redimensionner"
|
title="Redimensionner"
|
||||||
@@ -119,8 +155,9 @@
|
|||||||
<span class="split-handle-grip"></span>
|
<span class="split-handle-grip"></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Slot chatbot inline -->
|
<!-- SLOT CHATBOT (mode carte uniquement) -->
|
||||||
<div
|
<div
|
||||||
|
v-if="contentView === 'carte'"
|
||||||
class="chatbot-slot"
|
class="chatbot-slot"
|
||||||
:class="[
|
:class="[
|
||||||
layoutMode === 'split' ? 'chatbot-split' : '',
|
layoutMode === 'split' ? 'chatbot-split' : '',
|
||||||
@@ -134,9 +171,9 @@
|
|||||||
</ClientOnly>
|
</ClientOnly>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Vue Bonpote V2 -->
|
<!-- VUE BONPOTE -->
|
||||||
<div
|
<div
|
||||||
v-if="layoutMode === 'bonpote'"
|
v-if="contentView === 'bonpote'"
|
||||||
class="flex-1 overflow-y-auto px-6 py-8"
|
class="flex-1 overflow-y-auto px-6 py-8"
|
||||||
style="max-width: 680px; margin: 0 auto;"
|
style="max-width: 680px; margin: 0 auto;"
|
||||||
>
|
>
|
||||||
@@ -165,7 +202,7 @@
|
|||||||
Telecharger le poster PDF (recto/verso)
|
Telecharger le poster PDF (recto/verso)
|
||||||
</a>
|
</a>
|
||||||
<button
|
<button
|
||||||
@click="setLayoutMode('split')"
|
@click="showCarte"
|
||||||
class="flex items-center gap-3 px-4 py-3 rounded-lg hover:opacity-80 transition-opacity text-left"
|
class="flex items-center gap-3 px-4 py-3 rounded-lg hover:opacity-80 transition-opacity text-left"
|
||||||
style="background: var(--nav-bg-alt); color: var(--nav-text); font-size: 0.875rem; font-weight: 500; border: none; cursor: pointer;">
|
style="background: var(--nav-bg-alt); color: var(--nav-text); font-size: 0.875rem; font-weight: 500; border: none; cursor: pointer;">
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
|
||||||
@@ -189,6 +226,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- VUE RAG BACKEND -->
|
||||||
|
<div
|
||||||
|
v-if="contentView === 'rag-backend'"
|
||||||
|
style="flex: 1; overflow: hidden; display: flex; flex-direction: column;"
|
||||||
|
>
|
||||||
|
<MediaTabBackend />
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Fiche auteur modal -->
|
<!-- Fiche auteur modal -->
|
||||||
@@ -264,9 +309,11 @@ interface LivreRag { slug: string; titre: string; annee: number; couches: string
|
|||||||
interface AuteurData { id: string; nom: string; dates: string; ecoles: string[]; ecole_principale: string; livres_rag: LivreRag[]; theses_cles: string[]; bio_courte: string }
|
interface AuteurData { id: string; nom: string; dates: string; ecoles: string[]; ecole_principale: string; livres_rag: LivreRag[]; theses_cles: string[]; bio_courte: string }
|
||||||
interface PenseesData { meta: any; ecoles: EcoleData[]; auteurs: AuteurData[] }
|
interface PenseesData { meta: any; ecoles: EcoleData[]; auteurs: AuteurData[] }
|
||||||
|
|
||||||
type LayoutMode = 'split' | 'carte-full' | 'chatbot-full' | 'bonpote'
|
type LayoutMode = 'split' | 'carte-full' | 'chatbot-full'
|
||||||
|
type ContentView = 'carte' | 'bonpote' | 'rag-backend'
|
||||||
|
|
||||||
const STORAGE_KEY = 'media-layout-mode'
|
const LAYOUT_KEY = 'media-layout-mode'
|
||||||
|
const CONTENT_KEY = 'media-content-view'
|
||||||
const SPLIT_RATIO_KEY = 'media-split-ratio'
|
const SPLIT_RATIO_KEY = 'media-split-ratio'
|
||||||
const DEFAULT_SPLIT_RATIO = 0.66
|
const DEFAULT_SPLIT_RATIO = 0.66
|
||||||
|
|
||||||
@@ -276,32 +323,39 @@ const ficheEcoleOpen = ref(false)
|
|||||||
const ficheEcoleId = ref<string | null>(null)
|
const ficheEcoleId = ref<string | null>(null)
|
||||||
const ragInfoOpen = ref(false)
|
const ragInfoOpen = ref(false)
|
||||||
const chatbotAuteur = ref<string | null>(null)
|
const chatbotAuteur = ref<string | null>(null)
|
||||||
|
|
||||||
const layoutMode = ref<LayoutMode>('split')
|
const layoutMode = ref<LayoutMode>('split')
|
||||||
|
const contentView = ref<ContentView>('carte')
|
||||||
const cartePenseesRef = ref<{ triggerResize: () => void } | null>(null)
|
const cartePenseesRef = ref<{ triggerResize: () => void } | null>(null)
|
||||||
|
|
||||||
// Toggle PDF FRACAS
|
|
||||||
const showFracasPdf = ref(false)
|
const showFracasPdf = ref(false)
|
||||||
const fracasOpacity = ref(60)
|
const fracasOpacity = ref(60)
|
||||||
|
|
||||||
// Props injectées depuis le parent (penseesData)
|
const penseesData = ref<PenseesData | null>(null)
|
||||||
const props = defineProps<{ penseesData: PenseesData | null }>()
|
|
||||||
|
|
||||||
// Ratio de la carte vs chatbot en mode split (0.2 a 0.8)
|
|
||||||
const splitRatio = ref(DEFAULT_SPLIT_RATIO)
|
const splitRatio = ref(DEFAULT_SPLIT_RATIO)
|
||||||
const carteFlexBasis = computed(() => `${splitRatio.value * 100}%`)
|
const carteFlexBasis = computed(() => `${splitRatio.value * 100}%`)
|
||||||
const chatbotFlexBasis = computed(() => `${(1 - splitRatio.value) * 100}%`)
|
const chatbotFlexBasis = computed(() => `${(1 - splitRatio.value) * 100}%`)
|
||||||
|
|
||||||
// Logique poignee draggable
|
|
||||||
let dragStartY = 0
|
let dragStartY = 0
|
||||||
let dragStartRatio = DEFAULT_SPLIT_RATIO
|
let dragStartRatio = DEFAULT_SPLIT_RATIO
|
||||||
let containerHeight = 0
|
let containerHeight = 0
|
||||||
|
|
||||||
|
function showCarte() {
|
||||||
|
contentView.value = 'carte'
|
||||||
|
layoutMode.value = 'split'
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
localStorage.setItem(CONTENT_KEY, 'carte')
|
||||||
|
localStorage.setItem(LAYOUT_KEY, 'split')
|
||||||
|
}
|
||||||
|
nextTick(() => cartePenseesRef.value?.triggerResize())
|
||||||
|
}
|
||||||
|
|
||||||
function onHandleMousedown(e: MouseEvent) {
|
function onHandleMousedown(e: MouseEvent) {
|
||||||
dragStartY = e.clientY
|
dragStartY = e.clientY
|
||||||
dragStartRatio = splitRatio.value
|
dragStartRatio = splitRatio.value
|
||||||
const container = (e.target as HTMLElement)?.closest('.layout-container') as HTMLElement | null
|
const container = (e.target as HTMLElement)?.closest('.layout-container') as HTMLElement | null
|
||||||
containerHeight = container ? container.clientHeight : window.innerHeight
|
containerHeight = container ? container.clientHeight : window.innerHeight
|
||||||
|
|
||||||
window.addEventListener('mousemove', onHandleMousemove)
|
window.addEventListener('mousemove', onHandleMousemove)
|
||||||
window.addEventListener('mouseup', onHandleMouseup)
|
window.addEventListener('mouseup', onHandleMouseup)
|
||||||
}
|
}
|
||||||
@@ -315,17 +369,19 @@ function onHandleMousemove(e: MouseEvent) {
|
|||||||
function onHandleMouseup() {
|
function onHandleMouseup() {
|
||||||
window.removeEventListener('mousemove', onHandleMousemove)
|
window.removeEventListener('mousemove', onHandleMousemove)
|
||||||
window.removeEventListener('mouseup', onHandleMouseup)
|
window.removeEventListener('mouseup', onHandleMouseup)
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') localStorage.setItem(SPLIT_RATIO_KEY, String(splitRatio.value))
|
||||||
localStorage.setItem(SPLIT_RATIO_KEY, String(splitRatio.value))
|
|
||||||
}
|
|
||||||
cartePenseesRef.value?.triggerResize()
|
cartePenseesRef.value?.triggerResize()
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(async () => {
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
const saved = localStorage.getItem(STORAGE_KEY) as LayoutMode | null
|
const savedLayout = localStorage.getItem(LAYOUT_KEY) as LayoutMode | null
|
||||||
if (saved && ['split', 'carte-full', 'chatbot-full', 'bonpote'].includes(saved)) {
|
if (savedLayout && (['split', 'carte-full', 'chatbot-full'] as string[]).includes(savedLayout)) {
|
||||||
layoutMode.value = saved
|
layoutMode.value = savedLayout
|
||||||
|
}
|
||||||
|
const savedContent = localStorage.getItem(CONTENT_KEY) as ContentView | null
|
||||||
|
if (savedContent && (['carte', 'bonpote', 'rag-backend'] as string[]).includes(savedContent)) {
|
||||||
|
contentView.value = savedContent
|
||||||
}
|
}
|
||||||
const savedRatio = parseFloat(localStorage.getItem(SPLIT_RATIO_KEY) ?? '')
|
const savedRatio = parseFloat(localStorage.getItem(SPLIT_RATIO_KEY) ?? '')
|
||||||
if (!isNaN(savedRatio) && savedRatio >= 0.20 && savedRatio <= 0.80) {
|
if (!isNaN(savedRatio) && savedRatio >= 0.20 && savedRatio <= 0.80) {
|
||||||
@@ -336,26 +392,24 @@ onMounted(() => {
|
|||||||
localStorage.setItem('rag-fracas-info-seen', '1')
|
localStorage.setItem('rag-fracas-info-seen', '1')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
|
penseesData.value = await $fetch<PenseesData>('/data/auteurs-pensees.json')
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Erreur chargement auteurs-pensees.json', e)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
function setLayoutMode(mode: LayoutMode) {
|
|
||||||
layoutMode.value = mode
|
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
localStorage.setItem(STORAGE_KEY, mode)
|
|
||||||
}
|
|
||||||
if (mode !== 'chatbot-full') {
|
|
||||||
setTimeout(() => {
|
|
||||||
cartePenseesRef.value?.triggerResize()
|
|
||||||
}, 350)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(layoutMode, (v) => {
|
watch(layoutMode, (v) => {
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') localStorage.setItem(LAYOUT_KEY, v)
|
||||||
localStorage.setItem(STORAGE_KEY, v)
|
if (v === 'split' || v === 'carte-full') {
|
||||||
|
setTimeout(() => cartePenseesRef.value?.triggerResize(), 350)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
watch(contentView, (v) => {
|
||||||
|
if (typeof window !== 'undefined') localStorage.setItem(CONTENT_KEY, v)
|
||||||
|
})
|
||||||
|
|
||||||
function onSelectAuteur(id: string) {
|
function onSelectAuteur(id: string) {
|
||||||
ficheAuteurId.value = id
|
ficheAuteurId.value = id
|
||||||
ficheOpen.value = true
|
ficheOpen.value = true
|
||||||
@@ -374,18 +428,18 @@ function onSelectAuteurFromEcole(auteurId: string) {
|
|||||||
|
|
||||||
function onInterrogerEcole(ecoleId: string) {
|
function onInterrogerEcole(ecoleId: string) {
|
||||||
ficheEcoleOpen.value = false
|
ficheEcoleOpen.value = false
|
||||||
const ecole = props.penseesData?.ecoles.find(e => e.id === ecoleId)
|
const ecole = penseesData.value?.ecoles.find(e => e.id === ecoleId)
|
||||||
chatbotAuteur.value = ecole?.label ?? null
|
chatbotAuteur.value = ecole?.label ?? null
|
||||||
if (layoutMode.value === 'carte-full') setLayoutMode('split')
|
if (contentView.value !== 'carte') showCarte()
|
||||||
|
else if (layoutMode.value === 'carte-full') layoutMode.value = 'split'
|
||||||
}
|
}
|
||||||
|
|
||||||
function onInterrogerRag(auteurId: string) {
|
function onInterrogerRag(auteurId: string) {
|
||||||
ficheOpen.value = false
|
ficheOpen.value = false
|
||||||
const auteur = props.penseesData?.auteurs.find(a => a.id === auteurId)
|
const auteur = penseesData.value?.auteurs.find(a => a.id === auteurId)
|
||||||
chatbotAuteur.value = auteur?.nom ?? null
|
chatbotAuteur.value = auteur?.nom ?? null
|
||||||
if (layoutMode.value === 'carte-full') {
|
if (contentView.value !== 'carte') showCarte()
|
||||||
setLayoutMode('split')
|
else if (layoutMode.value === 'carte-full') layoutMode.value = 'split'
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -398,7 +452,6 @@ function onInterrogerRag(auteurId: string) {
|
|||||||
min-height: 0;
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Conteneur des slots carte + toggle + chatbot */
|
|
||||||
.layout-container {
|
.layout-container {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -441,7 +494,7 @@ function onInterrogerRag(auteurId: string) {
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- Barre de toggle --- */
|
/* --- Barre de toggle (entre carte et chatbot) --- */
|
||||||
.layout-toggle-bar {
|
.layout-toggle-bar {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -481,26 +534,25 @@ function onInterrogerRag(auteurId: string) {
|
|||||||
border-color: var(--nav-primary);
|
border-color: var(--nav-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- Toggle layer PDF FRACAS --- */
|
/* --- Contrôle fusionné carte des pensées --- */
|
||||||
.layer-toggle {
|
.carte-pensees-ctrl {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 5px;
|
gap: 0;
|
||||||
padding: 4px 10px;
|
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
font-size: 0.75rem;
|
overflow: hidden;
|
||||||
font-weight: 500;
|
border: 1px solid rgba(180, 170, 160, 0.3);
|
||||||
cursor: pointer;
|
|
||||||
background: var(--nav-bg-alt);
|
|
||||||
color: var(--nav-text-muted);
|
|
||||||
border: 1px solid transparent;
|
|
||||||
user-select: none;
|
|
||||||
margin-left: 4px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.layer-toggle input[type="checkbox"] {
|
.fracas-check {
|
||||||
margin: 0;
|
margin: 0 2px 0 7px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
accent-color: var(--nav-primary, #3b6ea5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.carte-pensees-btn {
|
||||||
|
border-radius: 0;
|
||||||
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.opacity-slider {
|
.opacity-slider {
|
||||||
@@ -509,7 +561,7 @@ function onInterrogerRag(auteurId: string) {
|
|||||||
accent-color: var(--nav-primary, #3b6ea5);
|
accent-color: var(--nav-primary, #3b6ea5);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- Poignee draggable entre carte et chatbot --- */
|
/* --- Poignee draggable --- */
|
||||||
.split-handle {
|
.split-handle {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
height: 8px;
|
height: 8px;
|
||||||
@@ -541,11 +593,8 @@ function onInterrogerRag(auteurId: string) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Masquer la poignee sur mobile (ratio fixe) */
|
|
||||||
@media (max-width: 767px) {
|
@media (max-width: 767px) {
|
||||||
.split-handle {
|
.split-handle { display: none; }
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- Slot chatbot --- */
|
/* --- Slot chatbot --- */
|
||||||
@@ -557,8 +606,8 @@ function onInterrogerRag(auteurId: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.chatbot-split {
|
.chatbot-split {
|
||||||
flex: 0 0 34%;
|
flex: 1 1 0;
|
||||||
min-height: 0;
|
min-height: 120px;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -583,19 +632,16 @@ function onInterrogerRag(auteurId: string) {
|
|||||||
.modal-enter-from { opacity: 0; transform: translate(-50%,-48%) scale(0.94); }
|
.modal-enter-from { opacity: 0; transform: translate(-50%,-48%) scale(0.94); }
|
||||||
.modal-leave-to { opacity: 0; transform: translate(-50%,-48%) scale(0.96); }
|
.modal-leave-to { opacity: 0; transform: translate(-50%,-48%) scale(0.96); }
|
||||||
|
|
||||||
/* --- Responsive mobile (<768px) --- */
|
/* --- Mobile --- */
|
||||||
@media (max-width: 767px) {
|
@media (max-width: 767px) {
|
||||||
.carte-split {
|
.carte-split {
|
||||||
flex: 0 0 60vh;
|
flex: 0 0 60vh;
|
||||||
height: 60vh;
|
height: 60vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chatbot-split {
|
.chatbot-split {
|
||||||
flex: 0 0 calc(40vh - 38px);
|
flex: 1 1 0;
|
||||||
height: calc(40vh - 38px);
|
min-height: 120px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toggle-btn span,
|
|
||||||
.toggle-btn {
|
.toggle-btn {
|
||||||
font-size: 0.7rem;
|
font-size: 0.7rem;
|
||||||
padding: 3px 7px;
|
padding: 3px 7px;
|
||||||
|
|||||||
150
components/OutilCard.vue
Normal file
150
components/OutilCard.vue
Normal 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>
|
||||||
156
components/SimulateurFeature.vue
Normal file
156
components/SimulateurFeature.vue
Normal 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
201
components/TreeASCII.vue
Normal 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>
|
||||||
@@ -23,6 +23,7 @@ export default defineNuxtConfig({
|
|||||||
codevPassword: 'merci', // NUXT_CODEV_PASSWORD - défaut "merci", overridable
|
codevPassword: 'merci', // NUXT_CODEV_PASSWORD - défaut "merci", overridable
|
||||||
codevBaseId: '', // NUXT_CODEV_BASE_ID - base NocoDB (ex: pipilvsi7dibo80)
|
codevBaseId: '', // NUXT_CODEV_BASE_ID - base NocoDB (ex: pipilvsi7dibo80)
|
||||||
codevAdminPassword: 'admin2026', // NUXT_CODEV_ADMIN_PASSWORD
|
codevAdminPassword: 'admin2026', // NUXT_CODEV_ADMIN_PASSWORD
|
||||||
|
ragPeUrl: process.env.RAG_PE_URL || 'http://localhost:9621',
|
||||||
},
|
},
|
||||||
|
|
||||||
// Leaflet ne fonctionne pas en SSR — forcer le rendu côté client
|
// Leaflet ne fonctionne pas en SSR — forcer le rendu côté client
|
||||||
|
|||||||
@@ -190,12 +190,38 @@
|
|||||||
</a>
|
</a>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<!-- ══════════════════════════════════════════════════════════
|
||||||
|
SECTION 8 - API publique
|
||||||
|
══════════════════════════════════════════════════════════ -->
|
||||||
|
<section class="section section-opensource">
|
||||||
|
<h2>API publique</h2>
|
||||||
|
<p class="section-text">
|
||||||
|
Les données de ce site internet sont publiques, toutes consultables via API directe, pour que des agents IA, des développeurs, ou d'autres sites puissent interroger la base sans passer par l'interface graphique.
|
||||||
|
</p>
|
||||||
|
<p class="section-text">
|
||||||
|
<strong>Données accessibles :</strong> organisations d'entraide, pratiques d'architecture d'écologie politique (graphe complet + projets), plateformes d'emploi, RAG et ses auteurs, et toutes autres données à venir.
|
||||||
|
</p>
|
||||||
|
<div class="api-copy-box">
|
||||||
|
<code class="api-url">https://api.trans-former.fr</code>
|
||||||
|
<button class="btn-copy" @click="copyApiUrl">{{ copied ? 'Copié !' : 'Copier' }}</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
useHead({ title: 'À propos - AEP' })
|
useHead({ title: 'À propos - AEP' })
|
||||||
|
|
||||||
|
const copied = ref(false)
|
||||||
|
function copyApiUrl() {
|
||||||
|
navigator.clipboard.writeText('https://api.trans-former.fr')
|
||||||
|
copied.value = true
|
||||||
|
setTimeout(() => { copied.value = false }, 2000)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -512,6 +538,44 @@ useHead({ title: 'À propos - AEP' })
|
|||||||
background: var(--nav-bg-alt);
|
background: var(--nav-bg-alt);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── API copy box ────────────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
.api-copy-box {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
background: var(--nav-surface);
|
||||||
|
border: 1px solid rgba(26, 34, 56, 0.2);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-url {
|
||||||
|
font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: var(--nav-primary-solid);
|
||||||
|
user-select: all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-copy {
|
||||||
|
padding: 0.375rem 0.75rem;
|
||||||
|
background: var(--nav-bg-alt);
|
||||||
|
border: 1px solid rgba(26, 34, 56, 0.15);
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--nav-text);
|
||||||
|
transition: background 0.15s;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-copy:hover {
|
||||||
|
background: var(--nav-primary-solid);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Responsive général ──────────────────────────────────────────────────────── */
|
/* ── Responsive général ──────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
@media (max-width: 480px) {
|
||||||
|
|||||||
@@ -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
533
pages/outils.vue
Normal 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>
|
||||||
@@ -1,38 +1,3 @@
|
|||||||
<template>
|
|
||||||
<div class="flex flex-col items-center justify-center h-full gap-6" style="background: var(--nav-bg);">
|
|
||||||
<div class="text-center max-w-md px-6">
|
|
||||||
<div
|
|
||||||
class="inline-flex items-center justify-center w-14 h-14 rounded-2xl mb-5"
|
|
||||||
style="background: var(--nav-bg-alt);"
|
|
||||||
>
|
|
||||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="color: var(--nav-text-muted);">
|
|
||||||
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/>
|
|
||||||
<polyline points="3.27 6.96 12 12.01 20.73 6.96"/>
|
|
||||||
<line x1="12" y1="22.08" x2="12" y2="12"/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<h1 class="text-2xl font-bold mb-3" style="color: var(--nav-text);">RAG — Retrieval Augmented Generation</h1>
|
|
||||||
<p class="text-sm leading-relaxed mb-6" style="color: var(--nav-text-muted);">
|
|
||||||
Une base de connaissances interrogeable par IA — textes, rapports, manifestes et ressources documentaires sur l'architecture d'écologie politique.
|
|
||||||
</p>
|
|
||||||
<p class="text-xs font-semibold uppercase tracking-widest mb-6" style="color: var(--nav-text-muted); opacity: 0.6;">
|
|
||||||
Bientôt disponible
|
|
||||||
</p>
|
|
||||||
<NuxtLink
|
|
||||||
to="/"
|
|
||||||
class="inline-flex items-center gap-2 px-4 py-2 rounded-xl text-sm font-semibold transition-all hover:opacity-80"
|
|
||||||
style="background: var(--nav-primary); color: var(--nav-text-on-primary);"
|
|
||||||
>
|
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" aria-hidden="true">
|
|
||||||
<line x1="19" y1="12" x2="5" y2="12"/>
|
|
||||||
<polyline points="12 19 5 12 12 5"/>
|
|
||||||
</svg>
|
|
||||||
Retour à l'écosystème
|
|
||||||
</NuxtLink>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
useHead({ title: 'RAG — AEP (bientôt disponible)' })
|
navigateTo('/media', { redirectCode: 301 })
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
3765
public/data/auteurs-pensees.json
Normal file
3765
public/data/auteurs-pensees.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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": "Recherche & articles",
|
"name": "Lecture & Documentation",
|
||||||
"children": [
|
"children": [
|
||||||
{ "name": "SciSpace", "url": "https://scispace.com/", "desc": "Chatbot sur publications scientifiques, explications simplifiées" },
|
{ "name": "Anna's Archive", "url": "https://annas-archive.org/", "desc": "Bibliothèque shadow la plus complète du web. Livres, articles, thèses." },
|
||||||
{ "name": "Alphaxiv", "url": "https://www.alphaxiv.org/", "desc": "Chatbot sur les papiers de recherche arxiv" },
|
{ "name": "Sci-Hub", "url": "https://sci-hub.se/", "desc": "Accès libre aux articles scientifiques payants." },
|
||||||
{ "name": "Unpaywall", "url": "https://unpaywall.org/", "desc": "Accès libre aux articles académiques — extension navigateur" },
|
{ "name": "Library Genesis", "url": "https://libgen.is/", "desc": "Livres techniques et académiques en PDF." },
|
||||||
{ "name": "Bypass Paywalls Clean", "url": "https://gitflic.ru/project/magnolia1234/bpc_uploads", "desc": "Extension pour bypasser les paywalls d'articles de presse" },
|
{ "name": "Z-Library", "url": "https://z-lib.id/", "desc": "Bibliothèque numérique massive, interface soignée." },
|
||||||
{ "name": "Freedium", "url": "https://freedium-mirror.cfd/", "desc": "Accéder aux articles Medium sans abonnement" }
|
{ "name": "OpenLibrary (Internet Archive)", "url": "https://openlibrary.org/", "desc": "Prêt numérique gratuit, millions de livres." },
|
||||||
|
{ "name": "Calibre", "url": "https://calibre-ebook.com/", "desc": "Gestion de bibliothèque numérique, convertisseur de formats." }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Livres & archives",
|
"name": "Dessin & Modélisation",
|
||||||
"children": [
|
"children": [
|
||||||
{ "name": "Anna's Archive", "url": "https://annas-archive.gl/", "desc": "Meta-search des bibliothèques libres (Z-Lib, LibGen, Sci-Hub)" },
|
{ "name": "FreeCAD", "url": "https://www.freecad.org/", "desc": "Modélisation 3D open source, paramétrique. Alternative à Rhino pour usage simple." },
|
||||||
{ "name": "Z-Library", "url": "https://z-lib.gd/", "desc": "Bibliothèque de livres/articles, téléchargement direct" },
|
{ "name": "Blender", "url": "https://www.blender.org/", "desc": "3D, rendu, animation. La référence open source." },
|
||||||
{ "name": "Library Genesis", "url": "https://libgen.li/", "desc": "Livres techniques, universitaires, comics en accès libre" },
|
{ "name": "Inkscape", "url": "https://inkscape.org/", "desc": "Dessin vectoriel. Alternative à Illustrator." },
|
||||||
{ "name": "Internet Archive", "url": "https://archive.org/details/texts", "desc": "Livres, magazines, archives historiques numérisés" },
|
{ "name": "GIMP", "url": "https://www.gimp.org/", "desc": "Retouche photo. Alternative à Photoshop." },
|
||||||
{ "name": "Project Gutenberg", "url": "https://www.gutenberg.org/", "desc": "Classiques du domaine public, 70k+ livres" },
|
{ "name": "Krita", "url": "https://krita.org/", "desc": "Dessin digital et croquis. Excellent pour les concepts." },
|
||||||
{ "name": "Open Library", "url": "https://openlibrary.org/", "desc": "Bibliothèque libre, emprunt numérique de livres" }
|
{ "name": "LibreOffice Draw", "url": "https://www.libreoffice.org/", "desc": "Diagrammes, plans rapides, sans suite Adobe." }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Cours & formation",
|
"name": "Productivité & Texte",
|
||||||
"children": [
|
"children": [
|
||||||
{ "name": "MIT OpenCourseWare", "url": "https://ocw.mit.edu/", "desc": "Cours MIT en accès libre — toutes disciplines, niveau universitaire" },
|
{ "name": "Obsidian", "url": "https://obsidian.md/", "desc": "PKM / prise de notes en Markdown. Gratuit pour usage personnel." },
|
||||||
{ "name": "Khan Academy", "url": "https://www.khanacademy.org/", "desc": "Cours gratuits et interactifs, toutes matières, du lycée au supérieur" },
|
{ "name": "Logseq", "url": "https://logseq.com/", "desc": "PKM open source, graphe de connaissances." },
|
||||||
{ "name": "edX", "url": "https://www.edx.org/", "desc": "MOOCs certifiants des meilleures universités mondiales" },
|
{ "name": "Zotero", "url": "https://www.zotero.org/", "desc": "Gestionnaire de références bibliographiques. Indispensable pour la recherche." },
|
||||||
{ "name": "Learn Anything", "url": "https://learn-anything.xyz/", "desc": "Moteur de recherche de parcours d'apprentissage structurés" },
|
{ "name": "Marktext", "url": "https://github.com/marktext/marktext", "desc": "Éditeur Markdown WYSIWYG, open source." },
|
||||||
{ "name": "OpenCulture", "url": "https://www.openculture.com/", "desc": "Cours, films, livres audio en accès libre, liens curatés" },
|
{ "name": "Typst", "url": "https://typst.app/", "desc": "Alternative moderne à LaTeX pour la mise en page de documents." },
|
||||||
{ "name": "Appropedia", "url": "https://www.appropedia.org/", "desc": "Wiki de la durabilité, technologies appropriées, habitat écologique" }
|
{ "name": "Pandoc", "url": "https://pandoc.org/", "desc": "Conversion universelle entre formats de documents (MD, DOCX, PDF, HTML...)." }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Outils dev & infra",
|
"name": "Dev & Infrastructure",
|
||||||
"children": [
|
"children": [
|
||||||
{ "name": "Free for Developers", "url": "https://free-for.dev/", "desc": "Index exhaustif des outils gratuits pour développeurs" },
|
{ "name": "VS Code", "url": "https://code.visualstudio.com/", "desc": "Éditeur de code. La référence, gratuit." },
|
||||||
{ "name": "DevDocs", "url": "https://devdocs.io/", "desc": "Documentation développeur unifiée, consultable hors ligne" },
|
{ "name": "Coolify", "url": "https://coolify.io/", "desc": "Self-hosting simplifié. Alternative à Heroku/Vercel sur son propre VPS." },
|
||||||
{ "name": "N8N", "url": "https://n8n.io/", "desc": "Automatisation de workflows open source, self-hostable" },
|
{ "name": "Hetzner Cloud", "url": "https://www.hetzner.com/cloud/", "desc": "VPS européen, tarifs très bas, data centers Allemagne." },
|
||||||
{ "name": "NocoDB", "url": "https://github.com/nocodb/nocodb", "desc": "Base de données no-code open source, alternative Airtable" },
|
{ "name": "Caddy", "url": "https://caddyserver.com/", "desc": "Serveur web avec HTTPS automatique. Plus simple que Nginx." },
|
||||||
{ "name": "Crontab Guru", "url": "https://crontab.guru/", "desc": "Éditeur de tâches cron avec explication lisible" },
|
{ "name": "n8n", "url": "https://n8n.io/", "desc": "Automatisation open source (comme Zapier mais self-hostable)." },
|
||||||
{ "name": "Dokploy", "url": "https://github.com/dokploy/dokploy", "desc": "Déploiement d'apps auto-hébergé, alternative Coolify/Heroku" },
|
{ "name": "Gitea", "url": "https://gitea.io/", "desc": "Hébergement Git self-hosted. Alternative à GitHub." }
|
||||||
{ "name": "Open Source Guides", "url": "https://opensource.guide/", "desc": "Guides pour contribuer et gérer des projets open source" }
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Texte & documents",
|
"name": "Vie privée & Sécurité",
|
||||||
"children": [
|
"children": [
|
||||||
{ "name": "DeepL", "url": "https://www.deepl.com/translator", "desc": "Traduction IA, meilleure qualité disponible en FR/EN/DE" },
|
{ "name": "Bitwarden", "url": "https://bitwarden.com/", "desc": "Gestionnaire de mots de passe open source. Self-hostable." },
|
||||||
{ "name": "Paperless-ngx", "url": "https://docs.paperless-ngx.com/", "desc": "Gestion documentaire self-hosted avec OCR et indexation" },
|
{ "name": "ProtonMail", "url": "https://proton.me/mail", "desc": "Email chiffré, hébergement Suisse." },
|
||||||
{ "name": "Whisper (OpenAI)", "url": "https://github.com/openai/whisper", "desc": "Transcription audio open source, multi-langues, qualité professionnelle" },
|
{ "name": "Signal", "url": "https://signal.org/", "desc": "Messagerie chiffrée E2E. La référence." },
|
||||||
{ "name": "GitHub Gists", "url": "https://gist.github.com/", "desc": "Partage de snippets code / notes, versionné, dans l'écosystème git" },
|
{ "name": "uBlock Origin", "url": "https://ublockorigin.com/", "desc": "Bloqueur de publicités et trackers, le plus efficace." },
|
||||||
{ "name": "PrivateBin", "url": "https://privatebin.net/", "desc": "Pastebin chiffré zero-knowledge, self-hostable" },
|
{ "name": "Mullvad VPN", "url": "https://mullvad.net/", "desc": "VPN respectueux de la vie privée, sans compte email requis." },
|
||||||
{ "name": "LibreTranslate", "url": "https://libretranslate.com/", "desc": "Traducteur FOSS, self-hostable, sans tracking" }
|
{ "name": "Privacy Guides", "url": "https://www.privacyguides.org/", "desc": "Recommandations d'outils respectueux de la vie privée, par thème." }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Vie privée & sécurité",
|
"name": "Formation & Apprentissage",
|
||||||
"children": [
|
"children": [
|
||||||
{ "name": "uBlock Origin", "url": "https://github.com/gorhill/uBlock", "desc": "Bloqueur de publicités et trackers — le plus efficace du marché" },
|
{ "name": "MIT OpenCourseWare", "url": "https://ocw.mit.edu/", "desc": "Cours du MIT en libre accès, toutes disciplines." },
|
||||||
{ "name": "Bitwarden", "url": "https://bitwarden.com/", "desc": "Gestionnaire de mots de passe open source, cloud ou self-hosted" },
|
{ "name": "Khan Academy", "url": "https://www.khanacademy.org/", "desc": "Maths, sciences, programmation — gratuit, pédagogie excellente." },
|
||||||
{ "name": "KeePassXC", "url": "https://keepassxc.org/", "desc": "Gestionnaire de mots de passe local, base chiffrée, sans cloud" },
|
{ "name": "YouTube (canaux techniques)", "url": "https://www.youtube.com/", "desc": "Channals : The Coding Train, Fireship, 3Blue1Brown, etc." },
|
||||||
{ "name": "Pi-Hole", "url": "https://pi-hole.net/", "desc": "Bloqueur de pub DNS pour tout le réseau domestique, self-hosted" },
|
{ "name": "freeCodeCamp", "url": "https://www.freecodecamp.org/", "desc": "Apprendre le développement web de zéro, gratuit et certifiant." },
|
||||||
{ "name": "SponsorBlock", "url": "https://sponsor.ajay.app/", "desc": "Extension pour skipper les segments sponsorisés sur YouTube" },
|
{ "name": "Coursera (audit gratuit)", "url": "https://www.coursera.org/", "desc": "Cours universitaires, audit gratuit disponible sur la plupart." }
|
||||||
{ "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" }
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Documentaires & médias",
|
"name": "Médias & Audio",
|
||||||
"children": [
|
"children": [
|
||||||
{ "name": "Films For Action", "url": "https://www.filmsforaction.org/", "desc": "Films documentaires engagés, alternatives sociales et écologiques" },
|
{ "name": "Audacity", "url": "https://www.audacityteam.org/", "desc": "Enregistrement et édition audio. Référence open source." },
|
||||||
{ "name": "ARTE (replay)", "url": "https://www.arte.tv/", "desc": "Documentaires et programmes culturels FR/DE, replay gratuit" },
|
{ "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": "Documentary+", "url": "https://www.docplus.com/", "desc": "Plateforme de documentaires en streaming gratuit" },
|
{ "name": "VLC", "url": "https://www.videolan.org/vlc/", "desc": "Lecteur multimédia universel." },
|
||||||
{ "name": "TED Talks", "url": "https://www.ted.com/", "desc": "Conférences inspirantes sur science, société, design, architecture" }
|
{ "name": "Kdenlive", "url": "https://kdenlive.org/", "desc": "Montage vidéo open source, non linéaire." },
|
||||||
|
{ "name": "OBS Studio", "url": "https://obsproject.com/", "desc": "Enregistrement et streaming vidéo. La référence gratuite." }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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." },
|
||||||
|
{ "name": "Excalidraw", "url": "https://excalidraw.com/", "desc": "Tableau blanc collaboratif, style hand-drawn, open source." },
|
||||||
|
{ "name": "Fmhy.net (complet)", "url": "https://fmhy.net/", "desc": "L'arbre complet. Des milliers de ressources organisées par thème." }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
|
||||||
"source": "fmhy.net (sélection AEP 2026-05-22)",
|
|
||||||
"total_entries": 50
|
|
||||||
}
|
}
|
||||||
|
|||||||
90
public/data/outils.json
Normal file
90
public/data/outils.json
Normal 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
12
public/icons/CREDITS.md
Normal 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`.
|
||||||
3
public/icons/outils-wrench.svg
Normal file
3
public/icons/outils-wrench.svg
Normal 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 |
162
server/api/chatbot-pensees.post.ts
Normal file
162
server/api/chatbot-pensees.post.ts
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
/**
|
||||||
|
* POST /api/chatbot-pensees
|
||||||
|
* Chatbot RAG Pensées Écologiques — corpus FRACAS Bonpote
|
||||||
|
* Appelle LightRAG PE (localhost:9621) avec contexte auteur optionnel.
|
||||||
|
*/
|
||||||
|
import { readFileSync } from 'node:fs'
|
||||||
|
import { join } from 'node:path'
|
||||||
|
import { checkRateLimitJson } from '~/server/utils/rateLimitJson'
|
||||||
|
|
||||||
|
const SYSTEM_PREFACE_PENSEES = `Tu es un agent du RAG Pensées Écologiques, infrastructure militante du collectif trans-former.fr.
|
||||||
|
Tu réponds en t'appuyant STRICTEMENT sur le corpus ingéré (auteurs FRACAS Bonpote : écosocialisme, éco-anarchisme, écoféminismes, écologies décoloniales, technocritique, pensées du vivant, décroissance...).
|
||||||
|
|
||||||
|
Règles :
|
||||||
|
- Cite les sources (auteur, livre) à chaque assertion importante.
|
||||||
|
- Si la question dépasse le corpus, dis-le clairement. Pas d'hallucination.
|
||||||
|
- Ton politique direct, pas de neutralité fade.
|
||||||
|
- Réponse en français, dense, sans délayage.
|
||||||
|
- Distingue les positions selon les écoles quand elles divergent.`
|
||||||
|
|
||||||
|
const SYSTEM_PREFACE_PROJETS = `Tu es un agent du RAG Projets de Jules Nény (architecte, collectif trans-former.fr).
|
||||||
|
Tu réponds STRICTEMENT à partir des documents projet (fichiers butte-pinson__*.md et autres projets archi de Jules).
|
||||||
|
N'utilise PAS le corpus FRACAS Pensées Écologiques pour répondre, sauf si l'usager te le demande explicitement.
|
||||||
|
|
||||||
|
Règles :
|
||||||
|
- Cite les sources (nom de projet, document) à chaque assertion importante.
|
||||||
|
- Si la question dépasse le corpus projet, dis-le clairement. Pas d'hallucination.
|
||||||
|
- Ton praticien réflexif : 1ère personne quand pertinent, narration située.
|
||||||
|
- Réponse en français, dense, sans délayage.`
|
||||||
|
|
||||||
|
const SYSTEM_PREFACE_BOTH = `Tu es un agent du RAG croisé Pensées x Projets de Jules Nény (architecte militant, collectif trans-former.fr).
|
||||||
|
CENTRE TA RÉPONSE sur les documents PROJETS (fichiers butte-pinson__*.md et autres projets archi).
|
||||||
|
Mobilise le corpus FRACAS Pensées (autres fichiers) UNIQUEMENT pour éclairer théoriquement les partis pris des projets, jamais l'inverse.
|
||||||
|
|
||||||
|
Pondération attendue : ~70% ancrage projet concret, ~30% éclairage théorique FRACAS.
|
||||||
|
|
||||||
|
Règles :
|
||||||
|
- Cite les sources (auteur ou nom de projet, document) à chaque assertion.
|
||||||
|
- Si un thème n'est pas couvert par les projets, dis-le clairement avant d'éventuellement étendre au corpus Pensées.
|
||||||
|
- Pas d'hallucination, pas d'extrapolation hors corpus.
|
||||||
|
- Ton praticien militant : direct, pas neutre, ancré dans la pratique architecturale.
|
||||||
|
- Réponse en français, dense, sans délayage.`
|
||||||
|
|
||||||
|
function buildPrefaceAuteur(nomAuteur: string, slug: string): string {
|
||||||
|
return `Tu réponds EXCLUSIVEMENT depuis les livres de ${nomAuteur} présents dans le RAG (fichiers commençant par "${slug}__").
|
||||||
|
Si la question sort du périmètre de cet auteur, indique-le et propose de l'aborder sans le hashtag pour interroger la carte entière. Reste fidèle au style et à la pensée de ${nomAuteur}. Cite toujours le livre.
|
||||||
|
|
||||||
|
Règles :
|
||||||
|
- Cite les sources (titre du livre) à chaque assertion.
|
||||||
|
- Pas d'hallucination. Si l'info n'est pas dans le corpus de cet auteur, dis-le.
|
||||||
|
- N'introduis JAMAIS d'autres auteurs sauf si ${nomAuteur} les commente explicitement.
|
||||||
|
- Ton politique direct, pas de neutralité fade.
|
||||||
|
- Réponse en français, dense, sans délayage.`
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AuteurIngere { id: string; nom: string; ingere: boolean }
|
||||||
|
|
||||||
|
let auteursIngeresCache: AuteurIngere[] | null = null
|
||||||
|
|
||||||
|
function loadAuteursIngeres(): AuteurIngere[] {
|
||||||
|
if (auteursIngeresCache) return auteursIngeresCache
|
||||||
|
try {
|
||||||
|
const jsonPath = join(process.cwd(), 'public', 'data', 'auteurs-pensees.json')
|
||||||
|
const raw = readFileSync(jsonPath, 'utf-8')
|
||||||
|
const data = JSON.parse(raw)
|
||||||
|
const list = (data.auteurs ?? [])
|
||||||
|
.filter((a: any) => a.ingere === true)
|
||||||
|
.map((a: any) => ({ id: String(a.id), nom: String(a.nom), ingere: true }))
|
||||||
|
auteursIngeresCache = list
|
||||||
|
return list
|
||||||
|
} catch {
|
||||||
|
auteursIngeresCache = []
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
const config = useRuntimeConfig()
|
||||||
|
|
||||||
|
const ip = getHeader(event, 'x-forwarded-for')?.split(',')[0].trim()
|
||||||
|
|| event.node.req.socket?.remoteAddress
|
||||||
|
|| '0.0.0.0'
|
||||||
|
const allowed = checkRateLimitJson(ip, 'chatbot-pensees', 20)
|
||||||
|
if (!allowed) throw createError({ statusCode: 429, message: 'Limite de 20 questions par jour atteinte.' })
|
||||||
|
|
||||||
|
const body = await readBody(event)
|
||||||
|
if (!body?.query || body.query.trim().length < 3 || body.query.trim().length > 500) {
|
||||||
|
throw createError({ statusCode: 400, message: 'Query invalide (3-500 caractères).' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const query: string = body.query.trim()
|
||||||
|
const mode: string = body.mode || 'hybrid'
|
||||||
|
const corpus: string = body.corpus || 'both'
|
||||||
|
const ragUrl: string = (config.ragPeUrl as string) || 'http://localhost:9621'
|
||||||
|
const auteurSlug: string | null = body.auteur_slug?.trim().toLowerCase() || null
|
||||||
|
|
||||||
|
let nomAuteurMatch: string | null = null
|
||||||
|
if (auteurSlug) {
|
||||||
|
const ingeres = loadAuteursIngeres()
|
||||||
|
const auteur = ingeres.find(a => a.id === auteurSlug)
|
||||||
|
nomAuteurMatch = auteur?.nom ?? null
|
||||||
|
}
|
||||||
|
|
||||||
|
let systemPreface: string
|
||||||
|
if (auteurSlug && nomAuteurMatch) {
|
||||||
|
systemPreface = buildPrefaceAuteur(nomAuteurMatch, auteurSlug)
|
||||||
|
} else if (corpus === 'pensees') {
|
||||||
|
systemPreface = SYSTEM_PREFACE_PENSEES
|
||||||
|
} else if (corpus === 'projets') {
|
||||||
|
systemPreface = SYSTEM_PREFACE_PROJETS
|
||||||
|
} else {
|
||||||
|
systemPreface = SYSTEM_PREFACE_BOTH
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await $fetch(`${ragUrl}/health`, { timeout: 5000 })
|
||||||
|
} catch {
|
||||||
|
throw createError({ statusCode: 503, message: 'RAG indisponible pour l\'instant — réessaie dans quelques minutes.' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const ragQuery = `${systemPreface}\n\nQuestion : ${query}`
|
||||||
|
const ragBody: Record<string, any> = { query: ragQuery, mode }
|
||||||
|
if (auteurSlug && nomAuteurMatch) {
|
||||||
|
ragBody.hl_keywords = [nomAuteurMatch, auteurSlug]
|
||||||
|
ragBody.ll_keywords = [auteurSlug]
|
||||||
|
}
|
||||||
|
|
||||||
|
let ragResponse: any
|
||||||
|
try {
|
||||||
|
ragResponse = await $fetch(`${ragUrl}/query`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: ragBody,
|
||||||
|
timeout: 90000,
|
||||||
|
})
|
||||||
|
} catch (e: any) {
|
||||||
|
const status = e?.response?.status
|
||||||
|
if (status === 429) throw createError({ statusCode: 429, message: 'RAG saturé — réessaie dans quelques instants.' })
|
||||||
|
throw createError({ statusCode: 504, message: 'RAG en cours de processing — réessaie dans quelques secondes.' })
|
||||||
|
}
|
||||||
|
|
||||||
|
let chunksOnTarget = 0
|
||||||
|
let chunksOffTarget = 0
|
||||||
|
if (auteurSlug && nomAuteurMatch && Array.isArray(ragResponse.references)) {
|
||||||
|
const slugPrefix = `${auteurSlug}__`
|
||||||
|
for (const ref of ragResponse.references) {
|
||||||
|
const fp = (ref.file_path ?? '').toLowerCase()
|
||||||
|
if (!fp) continue
|
||||||
|
if (fp.startsWith(slugPrefix)) chunksOnTarget++
|
||||||
|
else chunksOffTarget++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
response: ragResponse.response ?? '',
|
||||||
|
mode,
|
||||||
|
corpus,
|
||||||
|
auteur: auteurSlug && nomAuteurMatch ? { slug: auteurSlug, nom: nomAuteurMatch } : null,
|
||||||
|
auteur_unmatched: auteurSlug && !nomAuteurMatch ? auteurSlug : null,
|
||||||
|
auteur_chunks: auteurSlug && nomAuteurMatch ? { on_target: chunksOnTarget, off_target: chunksOffTarget } : null,
|
||||||
|
filter: { couche: body.filter_couche ?? null, ecole: body.filter_ecole ?? null },
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
}
|
||||||
|
})
|
||||||
10
server/routes/data/auteurs-pensees.json.get.ts
Normal file
10
server/routes/data/auteurs-pensees.json.get.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { readFileSync } from 'node:fs'
|
||||||
|
import { join } from 'node:path'
|
||||||
|
|
||||||
|
export default defineEventHandler((event) => {
|
||||||
|
const path = join(process.cwd(), 'public', 'data', 'auteurs-pensees.json')
|
||||||
|
const raw = readFileSync(path, 'utf-8')
|
||||||
|
setResponseHeader(event, 'content-type', 'application/json; charset=utf-8')
|
||||||
|
setResponseHeader(event, 'cache-control', 'public, max-age=300')
|
||||||
|
return raw
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user