- composables/useMarkdown.ts : renderer MD léger (bold/italic/listes/titres) - ChatbotSheet.vue + trouver-du-taf.vue : v-html renderMd() sur messages bot - assets/css/main.css : styles .md-content globaux pour tous les chatbots - taff-header centré + phrase cible 'architectes indépendants, 70% de la profession' Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
809 lines
34 KiB
Vue
809 lines
34 KiB
Vue
<template>
|
||
<div class="taff-page" style="background: var(--nav-bg); min-height: 100%;">
|
||
|
||
<!-- ── Intro ─────────────────────────────────────────────────────── -->
|
||
<div class="taff-header">
|
||
<div class="taff-header-inner">
|
||
<h1 class="taff-title">Trouver du taf en archi</h1>
|
||
<p class="taff-subtitle">
|
||
Annuaire critique des plateformes de mise en relation archi ↔ particulier,
|
||
évaluées sur 5 axes éthiques — rémunération, transparence, pratiques pro, écologie, qualité du matching.
|
||
</p>
|
||
<p class="taff-cible">
|
||
Cette carte s'adresse aux <strong>architectes indépendants</strong> —
|
||
70 % de la profession et sa part la plus précaire économiquement.
|
||
</p>
|
||
<div class="taff-stats">
|
||
<span class="taff-stat" style="color: #3d5534;">
|
||
<span class="taff-stat-dot" style="background: #5a7a4a;"></span>
|
||
{{ stats.recommande }} Recommandé{{ stats.recommande > 1 ? 's' : '' }} AEP
|
||
</span>
|
||
<span class="taff-stat" style="color: #7a5f2a;">
|
||
<span class="taff-stat-dot" style="background: #c4a472;"></span>
|
||
{{ stats.sous_reserve }} Sous réserve
|
||
</span>
|
||
<span class="taff-stat" style="color: #7a3322;">
|
||
<span class="taff-stat-dot" style="background: #a85d3e;"></span>
|
||
{{ stats.a_eviter }} À éviter
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ── Filtres ─────────────────────────────────────────────────── -->
|
||
<div class="taff-filters-bar">
|
||
<div class="taff-filters-inner">
|
||
|
||
<!-- Onglets B2C / AO publics -->
|
||
<div class="taff-tabs">
|
||
<button
|
||
type="button"
|
||
class="taff-tab"
|
||
:class="{ 'taff-tab--active': activeTab === 'b2c' }"
|
||
@click="activeTab = 'b2c'; resetFilters()"
|
||
>
|
||
Plateformes B2C
|
||
<span class="taff-tab-count">{{ b2cCount }}</span>
|
||
</button>
|
||
<button
|
||
type="button"
|
||
class="taff-tab"
|
||
:class="{ 'taff-tab--active': activeTab === 'ao' }"
|
||
@click="activeTab = 'ao'; resetFilters()"
|
||
>
|
||
Appels d'offres publics
|
||
<span class="taff-tab-count">{{ aoCount }}</span>
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Filtres tag global -->
|
||
<div class="taff-filter-group">
|
||
<button
|
||
v-for="t in TAG_OPTIONS"
|
||
:key="t.value"
|
||
type="button"
|
||
class="taff-filter-btn"
|
||
:class="{ 'taff-filter-btn--active': filterTag === t.value }"
|
||
:style="filterTag === t.value
|
||
? `background: ${t.bg}; color: ${t.text}; border-color: ${t.accent};`
|
||
: ''"
|
||
@click="filterTag = filterTag === t.value ? '' : t.value"
|
||
>
|
||
{{ t.emoji }} {{ t.label }}
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Filtre secteur (B2C uniquement) -->
|
||
<div v-if="activeTab === 'b2c'" class="taff-filter-group">
|
||
<button
|
||
v-for="s in SECTEUR_OPTIONS"
|
||
:key="s.value"
|
||
type="button"
|
||
class="taff-filter-btn"
|
||
:class="{ 'taff-filter-btn--active': filterSecteur === s.value }"
|
||
@click="filterSecteur = filterSecteur === s.value ? '' : s.value"
|
||
>
|
||
{{ s.label }}
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Recherche -->
|
||
<label class="taff-search">
|
||
<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" style="color: var(--nav-text-muted); flex-shrink: 0;">
|
||
<circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/>
|
||
</svg>
|
||
<input
|
||
v-model="search"
|
||
type="search"
|
||
placeholder="Rechercher..."
|
||
class="taff-search-input"
|
||
autocomplete="off"
|
||
/>
|
||
<button v-if="search" type="button" @click.stop="search = ''" class="taff-search-clear" aria-label="Effacer">
|
||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round">
|
||
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
|
||
</svg>
|
||
</button>
|
||
</label>
|
||
|
||
<!-- Compteur + reset -->
|
||
<div class="flex items-center gap-3 ml-auto">
|
||
<span class="text-xs font-bold uppercase tracking-widest" style="color: var(--nav-text-muted);">
|
||
{{ filtered.length }} résultat{{ filtered.length > 1 ? 's' : '' }}
|
||
</span>
|
||
<button
|
||
v-if="hasFilters"
|
||
type="button"
|
||
class="text-xs underline hover:opacity-70"
|
||
style="color: var(--nav-text-muted);"
|
||
@click="resetFilters"
|
||
>Effacer</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ── Grille ─────────────────────────────────────────────────── -->
|
||
<div class="taff-grid-wrap">
|
||
<div v-if="filtered.length === 0" class="taff-empty">
|
||
<p style="color: var(--nav-text-muted);">Aucune plateforme ne correspond à ces filtres.</p>
|
||
<button type="button" class="taff-reset-btn" @click="resetFilters">Réinitialiser les filtres</button>
|
||
</div>
|
||
<div v-else class="taff-grid">
|
||
<PlatformeTaffCard
|
||
v-for="p in filtered"
|
||
:key="p.id"
|
||
:plateforme="p"
|
||
@open="openModal"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ── Chatbot FAB ───────────────────────────────────────────── -->
|
||
<Teleport to="body">
|
||
<!-- Bouton flottant -->
|
||
<button
|
||
v-if="!chatOpen"
|
||
class="taff-fab"
|
||
@click="chatOpen = true"
|
||
aria-label="Ouvrir le guide IA — Quel plateforme me convient ?"
|
||
title="Guide IA — Je t'aide à choisir"
|
||
>
|
||
<svg width="20" height="20" 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>
|
||
<span class="taff-fab-label">Guide IA</span>
|
||
</button>
|
||
|
||
<!-- Panel chatbot -->
|
||
<Transition name="taff-chat">
|
||
<div v-if="chatOpen" class="taff-chat-panel" role="dialog" aria-modal="true" aria-label="Guide IA — Choisir sa plateforme">
|
||
|
||
<!-- Header panel -->
|
||
<div class="taff-chat-header">
|
||
<div class="flex items-center gap-2">
|
||
<div class="taff-chat-avatar">
|
||
<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" style="color: var(--nav-text-on-primary);">
|
||
<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>
|
||
<div>
|
||
<div class="text-sm font-semibold" style="color: var(--nav-text);">Guide AEP</div>
|
||
<div class="text-xs" style="color: var(--nav-text-muted);">Je t'aide à choisir ta plateforme</div>
|
||
</div>
|
||
</div>
|
||
<button @click="chatOpen = false" class="taff-chat-close" aria-label="Fermer">
|
||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round">
|
||
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Messages -->
|
||
<div class="taff-chat-messages" ref="chatMessagesEl">
|
||
<!-- Message d'accueil -->
|
||
<div class="taff-msg taff-msg--bot">
|
||
<p>Dis-moi ta situation : ton secteur, ta zone, ce qui te bloque. Je t'oriente parmi les {{ allPlateformes.length }} plateformes de l'annuaire.</p>
|
||
<p class="text-xs mt-1.5 opacity-60">Ex : "Je suis en rénovation à Lyon, je cherche des leads sans commission."</p>
|
||
</div>
|
||
|
||
<!-- Messages de la conversation -->
|
||
<template v-for="(msg, i) in chatMessages" :key="i">
|
||
<div :class="['taff-msg', msg.role === 'user' ? 'taff-msg--user' : 'taff-msg--bot']">
|
||
<div v-if="msg.role === 'bot'" class="md-content" v-html="renderMd(msg.content)" />
|
||
<span v-else>{{ msg.content }}</span>
|
||
</div>
|
||
<!-- Plateformes recommandées -->
|
||
<div v-if="msg.role === 'bot' && msg.recommandations?.length" class="taff-chat-recos">
|
||
<button
|
||
v-for="r in msg.recommandations"
|
||
:key="r.id"
|
||
class="taff-reco-chip"
|
||
@click="openModalById(r.id); chatOpen = false"
|
||
>
|
||
{{ r.nom }}
|
||
<span class="taff-reco-arrow">→</span>
|
||
</button>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- Loader -->
|
||
<div v-if="chatLoading" class="taff-msg taff-msg--bot">
|
||
<span class="taff-typing"><span/><span/><span/></span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Input -->
|
||
<div class="taff-chat-input-wrap">
|
||
<textarea
|
||
v-model="chatInput"
|
||
class="taff-chat-input"
|
||
placeholder="Décris ta situation..."
|
||
rows="2"
|
||
@keydown.enter.exact.prevent="sendChat"
|
||
/>
|
||
<button
|
||
class="taff-chat-send"
|
||
:disabled="chatLoading || !chatInput.trim()"
|
||
@click="sendChat"
|
||
aria-label="Envoyer"
|
||
>
|
||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||
<line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/>
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
<p class="taff-chat-footer-note">Propulsé par Mistral · 20 questions/jour</p>
|
||
</div>
|
||
</Transition>
|
||
</Teleport>
|
||
|
||
<!-- ── Note juridique ────────────────────────────────────────── -->
|
||
<div class="taff-disclaimer">
|
||
<p>
|
||
Évaluations basées sur des données publiques (CGV, Trustpilot, presse spécialisée) collectées en mai 2026.
|
||
AEP est un méta-annuaire critique, pas un opérateur. Les fiches « À éviter ❌ » sont validées manuellement avant publication.
|
||
</p>
|
||
</div>
|
||
|
||
<!-- ── Modal ─────────────────────────────────────────────────── -->
|
||
<Teleport to="body">
|
||
<Transition name="taff-backdrop">
|
||
<div
|
||
v-if="modalPlateforme"
|
||
class="fixed inset-0 z-[10000]"
|
||
style="background: rgba(26,34,56,0.55);"
|
||
@click="closeModal"
|
||
aria-hidden="true"
|
||
/>
|
||
</Transition>
|
||
<Transition name="taff-modal">
|
||
<div
|
||
v-if="modalPlateforme"
|
||
class="fixed z-[10001] left-1/2 -translate-x-1/2 flex flex-col"
|
||
style="width: min(760px, 92vw); top: 72px; max-height: calc(100vh - 88px); background: var(--nav-bg); border-radius: 16px; box-shadow: 0 16px 64px rgba(26,34,56,0.28); overflow: hidden;"
|
||
role="dialog"
|
||
aria-modal="true"
|
||
:aria-label="modalPlateforme.nom"
|
||
tabindex="-1"
|
||
@keydown.esc="closeModal"
|
||
>
|
||
<!-- Header -->
|
||
<div class="flex items-center justify-between px-5 py-3 shrink-0" style="background: var(--nav-surface); border-bottom: 1px solid var(--nav-bg-alt);">
|
||
<div class="flex items-center gap-2 min-w-0">
|
||
<span
|
||
class="inline-flex items-center gap-1 px-2.5 py-1 rounded-full text-sm font-semibold shrink-0"
|
||
:style="`background: ${modalTagConfig.bg}; color: ${modalTagConfig.text};`"
|
||
>{{ modalTagConfig.emoji }} {{ modalTagConfig.label }}</span>
|
||
<span class="font-semibold text-base truncate" style="color: var(--nav-text);">{{ modalPlateforme.nom }}</span>
|
||
</div>
|
||
<div class="flex items-center gap-2 shrink-0 ml-3">
|
||
<a
|
||
:href="modalPlateforme.url"
|
||
target="_blank"
|
||
rel="noopener noreferrer"
|
||
class="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium transition-opacity hover:opacity-70"
|
||
style="background: var(--nav-bg-alt); color: var(--nav-text);"
|
||
>
|
||
<svg width="12" height="12" 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>
|
||
Visiter
|
||
</a>
|
||
<button
|
||
@click="closeModal"
|
||
class="w-8 h-8 rounded-lg flex items-center justify-center hover:opacity-70 transition-opacity"
|
||
style="background: var(--nav-bg-alt); color: var(--nav-text-muted);"
|
||
aria-label="Fermer"
|
||
>
|
||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round">
|
||
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Body -->
|
||
<div class="overflow-y-auto flex-1 px-5 py-5 space-y-5">
|
||
|
||
<!-- Scoring axes -->
|
||
<div>
|
||
<div class="modal-label">Évaluation AEP — 5 axes</div>
|
||
<div class="modal-axes">
|
||
<div
|
||
v-for="axe in AXES"
|
||
:key="axe.id"
|
||
v-show="modalPlateforme.scoring[axe.id] !== null"
|
||
class="modal-axe"
|
||
:style="`background: ${axeScoreBg(modalPlateforme.scoring[axe.id] as string)};`"
|
||
>
|
||
<span class="text-xl leading-none">{{ axe.icon }}</span>
|
||
<div class="flex flex-col">
|
||
<span class="text-xs font-bold uppercase tracking-wider" style="color: var(--nav-text-muted);">{{ axe.label }}</span>
|
||
<span class="text-lg leading-none" :style="`color: ${axeScoreText(modalPlateforme.scoring[axe.id] as string)};`">
|
||
{{ modalPlateforme.scoring[axe.id] }}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<p class="mt-3 text-sm leading-relaxed italic rounded-lg px-3 py-2.5" style="color: var(--nav-text-muted); background: var(--nav-bg-alt);">
|
||
{{ modalPlateforme.scoring.justification_tag }}
|
||
</p>
|
||
</div>
|
||
|
||
<!-- Description -->
|
||
<div>
|
||
<div class="modal-label">Fiche détaillée</div>
|
||
<div class="space-y-3">
|
||
<div v-for="section in parsedDescription" :key="section.title">
|
||
<h5 class="text-sm font-bold mb-1" style="color: var(--nav-text);">{{ section.title }}</h5>
|
||
<p class="text-sm leading-relaxed" style="color: var(--nav-text-muted);">{{ section.body }}</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Infos pratiques -->
|
||
<div>
|
||
<div class="modal-label">Infos pratiques</div>
|
||
<div class="modal-meta-grid">
|
||
<div class="modal-meta-item">
|
||
<span class="modal-meta-key">Type</span>
|
||
<span class="modal-meta-val">{{ modalPlateforme.type === 'b2c-mise-en-relation' ? 'Plateforme B2C' : 'Appels d\'offres publics' }}</span>
|
||
</div>
|
||
<div class="modal-meta-item">
|
||
<span class="modal-meta-key">Coût d'entrée</span>
|
||
<span class="modal-meta-val">{{ coutLabel(modalPlateforme.cout_entree) }}</span>
|
||
</div>
|
||
<div class="modal-meta-item">
|
||
<span class="modal-meta-key">Zone</span>
|
||
<span class="modal-meta-val">France entière</span>
|
||
</div>
|
||
<div class="modal-meta-item">
|
||
<span class="modal-meta-key">Secteurs</span>
|
||
<span class="modal-meta-val">{{ modalPlateforme.secteurs_servis.map(s => SECTEUR_LABELS[s] ?? s).join(', ') }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Footer meta -->
|
||
<div class="flex items-center gap-2 text-xs flex-wrap pb-1" style="color: var(--nav-text-muted);">
|
||
<span>Fiche créée le {{ modalPlateforme.date_creation_fiche }}</span>
|
||
<span>·</span>
|
||
<span>{{ modalPlateforme.source_donnees.length }} source{{ modalPlateforme.source_donnees.length > 1 ? 's' : '' }}</span>
|
||
<span v-if="modalPlateforme.flag_validation_jules" class="font-semibold" style="color: #7a5f2a;">
|
||
· ⚠️ En attente de validation avant publication
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</Transition>
|
||
</Teleport>
|
||
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import type { PlateformeTaff } from '~/types/plateforme-taff'
|
||
|
||
useHead({
|
||
title: 'Trouver du taf en archi — AEP',
|
||
meta: [
|
||
{ name: 'description', content: "Annuaire critique des plateformes B2C archi–particulier. Évaluations éthiques sur 5 axes : rémunération, transparence, pratiques pro, écologie, matching." }
|
||
]
|
||
})
|
||
|
||
const { data } = await useAsyncData('plateformes-taff', () =>
|
||
$fetch<{ meta: any; plateformes: PlateformeTaff[] }>('/data/plateformes-taff.json')
|
||
)
|
||
|
||
const allPlateformes = computed(() => data.value?.plateformes ?? [])
|
||
const stats = computed(() => data.value?.meta?.repartition ?? { recommande: 0, sous_reserve: 0, a_eviter: 0 })
|
||
const b2cCount = computed(() => allPlateformes.value.filter(p => p.type === 'b2c-mise-en-relation').length)
|
||
const aoCount = computed(() => allPlateformes.value.filter(p => p.type === 'appel-offre-public').length)
|
||
|
||
// Filtres
|
||
const activeTab = ref<'b2c' | 'ao'>('b2c')
|
||
const filterTag = ref('')
|
||
const filterSecteur = ref('')
|
||
const search = ref('')
|
||
const hasFilters = computed(() => !!(filterTag.value || filterSecteur.value || search.value))
|
||
|
||
function resetFilters() {
|
||
filterTag.value = ''
|
||
filterSecteur.value = ''
|
||
search.value = ''
|
||
}
|
||
|
||
const filtered = computed(() => {
|
||
let list = allPlateformes.value.filter(p =>
|
||
activeTab.value === 'b2c'
|
||
? p.type === 'b2c-mise-en-relation'
|
||
: p.type === 'appel-offre-public'
|
||
)
|
||
if (filterTag.value)
|
||
list = list.filter(p => p.scoring.tag_global === filterTag.value)
|
||
if (filterSecteur.value)
|
||
list = list.filter(p => (p.secteurs_servis as string[]).includes(filterSecteur.value))
|
||
if (search.value) {
|
||
const q = search.value.toLowerCase()
|
||
list = list.filter(p =>
|
||
p.nom.toLowerCase().includes(q) ||
|
||
p.description_courte.toLowerCase().includes(q)
|
||
)
|
||
}
|
||
const ORDER: Record<string, number> = { 'recommande': 0, 'sous-reserve': 1, 'a-eviter': 2 }
|
||
return [...list].sort((a, b) => (ORDER[a.scoring.tag_global] ?? 9) - (ORDER[b.scoring.tag_global] ?? 9))
|
||
})
|
||
|
||
// Options filtres
|
||
const TAG_OPTIONS = [
|
||
{ value: 'recommande', emoji: '✅', label: 'Recommandé', bg: 'rgba(90,122,74,0.12)', text: '#3d5534', accent: '#5a7a4a' },
|
||
{ value: 'sous-reserve', emoji: '⚠️', label: 'Sous réserve', bg: 'rgba(196,164,114,0.15)', text: '#7a5f2a', accent: '#c4a472' },
|
||
{ value: 'a-eviter', emoji: '❌', label: 'À éviter', bg: 'rgba(168,93,62,0.12)', text: '#7a3322', accent: '#a85d3e' },
|
||
]
|
||
|
||
const SECTEUR_OPTIONS = [
|
||
{ value: 'renovation', label: 'Rénovation' },
|
||
{ value: 'construction-neuve', label: 'Neuf' },
|
||
{ value: 'architecture-interieure', label: 'Archi intérieure' },
|
||
{ value: 'mar-conseil', label: 'MAR / Conseil' },
|
||
{ value: 'urbanisme', label: 'Urbanisme' },
|
||
{ value: 'paysage', label: 'Paysage' },
|
||
{ value: 'transversal', label: 'Transversal' },
|
||
]
|
||
|
||
const SECTEUR_LABELS: Record<string, string> = {
|
||
'renovation': 'Rénovation', 'construction-neuve': 'Neuf',
|
||
'architecture-interieure': 'Archi intérieure', 'urbanisme': 'Urbanisme',
|
||
'paysage': 'Paysage', 'mar-conseil': 'MAR / Conseil', 'transversal': 'Transversal',
|
||
}
|
||
|
||
const COUT_LABELS: Record<string, string> = {
|
||
'gratuit': 'Gratuit', 'freemium': 'Freemium',
|
||
'abonnement': 'Abonnement', 'lead-paye': 'Lead payant', 'commission': 'Commission',
|
||
}
|
||
function coutLabel(c: string) { return COUT_LABELS[c] ?? c }
|
||
|
||
// Axes
|
||
const AXES = [
|
||
{ id: 'remuneration' as const, icon: '🪙', label: 'Rémunération' },
|
||
{ id: 'transparence' as const, icon: '🔍', label: 'Transparence' },
|
||
{ id: 'pratiques' as const, icon: '⚖️', label: 'Pratiques pro' },
|
||
{ id: 'ecologie' as const, icon: '🌿', label: 'Écologie' },
|
||
{ id: 'matching' as const, icon: '🎯', label: 'Matching' },
|
||
]
|
||
|
||
function axeScoreBg(score: string) {
|
||
if (score === '✅') return 'rgba(90,122,74,0.1)'
|
||
if (score === '⚠️') return 'rgba(196,164,114,0.15)'
|
||
if (score === '❌') return 'rgba(168,93,62,0.1)'
|
||
return 'var(--nav-bg-alt)'
|
||
}
|
||
function axeScoreText(score: string) {
|
||
if (score === '✅') return '#3d5534'
|
||
if (score === '⚠️') return '#7a5f2a'
|
||
if (score === '❌') return '#7a3322'
|
||
return 'var(--nav-text-muted)'
|
||
}
|
||
|
||
// Modal
|
||
const modalPlateforme = ref<PlateformeTaff | null>(null)
|
||
function openModal(p: PlateformeTaff) { modalPlateforme.value = p }
|
||
function closeModal() { modalPlateforme.value = null }
|
||
function openModalById(id: string) {
|
||
const p = allPlateformes.value.find(p => p.id === id)
|
||
if (p) modalPlateforme.value = p
|
||
}
|
||
|
||
// Chatbot
|
||
interface ChatMessage {
|
||
role: 'user' | 'bot'
|
||
content: string
|
||
recommandations?: { id: string; nom: string; raison: string }[]
|
||
}
|
||
|
||
const { render: renderMd } = useMarkdown()
|
||
|
||
const chatOpen = ref(false)
|
||
const chatInput = ref('')
|
||
const chatLoading = ref(false)
|
||
const chatMessages = ref<ChatMessage[]>([])
|
||
const chatMessagesEl = ref<HTMLElement | null>(null)
|
||
|
||
async function sendChat() {
|
||
const q = chatInput.value.trim()
|
||
if (!q || chatLoading.value) return
|
||
chatMessages.value.push({ role: 'user', content: q })
|
||
chatInput.value = ''
|
||
chatLoading.value = true
|
||
await nextTick()
|
||
chatMessagesEl.value?.scrollTo({ top: chatMessagesEl.value.scrollHeight, behavior: 'smooth' })
|
||
try {
|
||
const res = await $fetch<{ reponse_texte: string; plateformes_recommandees: { id: string; nom: string; raison: string }[] }>(
|
||
'/api/chatbot-taff',
|
||
{ method: 'POST', body: { question: q } }
|
||
)
|
||
chatMessages.value.push({
|
||
role: 'bot',
|
||
content: res.reponse_texte,
|
||
recommandations: res.plateformes_recommandees ?? [],
|
||
})
|
||
} catch (e: any) {
|
||
chatMessages.value.push({ role: 'bot', content: e?.data?.statusMessage ?? 'Erreur — réessaie dans un instant.' })
|
||
} finally {
|
||
chatLoading.value = false
|
||
await nextTick()
|
||
chatMessagesEl.value?.scrollTo({ top: chatMessagesEl.value.scrollHeight, behavior: 'smooth' })
|
||
}
|
||
}
|
||
|
||
const TAG_CONFIG: Record<string, { emoji: string; label: string; bg: string; text: string }> = {
|
||
'recommande': { emoji: '✅', label: 'Recommandé AEP', bg: 'rgba(90,122,74,0.12)', text: '#3d5534' },
|
||
'sous-reserve': { emoji: '⚠️', label: 'Sous réserve', bg: 'rgba(196,164,114,0.15)', text: '#7a5f2a' },
|
||
'a-eviter': { emoji: '❌', label: 'À éviter', bg: 'rgba(168,93,62,0.12)', text: '#7a3322' },
|
||
}
|
||
const modalTagConfig = computed(() =>
|
||
modalPlateforme.value
|
||
? (TAG_CONFIG[modalPlateforme.value.scoring.tag_global] ?? TAG_CONFIG['sous-reserve'])
|
||
: TAG_CONFIG['sous-reserve']
|
||
)
|
||
|
||
// Parse description (format "## Titre\nContenu\n\n## Titre2\nContenu2")
|
||
const parsedDescription = computed(() => {
|
||
if (!modalPlateforme.value) return []
|
||
const raw = modalPlateforme.value.description
|
||
const sections: { title: string; body: string }[] = []
|
||
const parts = raw.split(/\n\n## /)
|
||
parts.forEach((part, i) => {
|
||
const text = i === 0 ? part.replace(/^## /, '') : part
|
||
const nl = text.indexOf('\n')
|
||
if (nl < 0) return
|
||
sections.push({ title: text.slice(0, nl).trim(), body: text.slice(nl + 1).trim() })
|
||
})
|
||
return sections
|
||
})
|
||
</script>
|
||
|
||
<style scoped>
|
||
.taff-page { max-width: 1280px; margin: 0 auto; padding-bottom: 3rem; }
|
||
|
||
.taff-header { padding: 2.5rem 1.5rem 1.5rem; border-bottom: 1px solid var(--nav-bg-alt); text-align: center; }
|
||
.taff-header-inner { max-width: 680px; margin: 0 auto; }
|
||
.taff-title { font-size: 1.875rem; font-weight: 800; color: var(--nav-text); margin-bottom: 0.5rem; letter-spacing: -0.02em; }
|
||
.taff-subtitle { font-size: 0.9375rem; color: var(--nav-text-muted); line-height: 1.6; margin-bottom: 0.625rem; }
|
||
.taff-cible { font-size: 0.875rem; color: var(--nav-text-muted); line-height: 1.55; margin-bottom: 1rem; font-style: italic; }
|
||
.taff-cible strong { color: var(--nav-text); font-style: normal; }
|
||
.taff-stats { display: flex; gap: 1.25rem; flex-wrap: wrap; justify-content: center; }
|
||
.taff-stat { display: flex; align-items: center; gap: 0.375rem; font-size: 0.8125rem; font-weight: 600; }
|
||
.taff-stat-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
|
||
|
||
.taff-filters-bar { position: sticky; top: 0; z-index: 100; background: var(--nav-surface); border-bottom: 1px solid var(--nav-bg-alt); padding: 0.75rem 1.5rem; box-shadow: 0 2px 8px rgba(26,34,56,0.06); }
|
||
.taff-filters-inner { display: flex; align-items: center; gap: 0.625rem; flex-wrap: wrap; }
|
||
|
||
.taff-tabs { display: flex; border-radius: 8px; overflow: hidden; border: 1px solid var(--nav-bg-alt); flex-shrink: 0; }
|
||
.taff-tab { display: flex; align-items: center; gap: 0.375rem; padding: 0.375rem 0.875rem; font-size: 0.8125rem; font-weight: 500; color: var(--nav-text-muted); background: var(--nav-bg); border: none; cursor: pointer; transition: background 0.15s; }
|
||
.taff-tab:first-child { border-right: 1px solid var(--nav-bg-alt); }
|
||
.taff-tab--active { background: var(--nav-primary-solid); color: var(--nav-text-on-primary); }
|
||
.taff-tab-count { font-size: 0.6875rem; opacity: 0.7; font-weight: 700; }
|
||
|
||
.taff-filter-group { display: flex; gap: 0.375rem; flex-wrap: wrap; }
|
||
.taff-filter-btn { padding: 0.3125rem 0.75rem; border-radius: 9999px; font-size: 0.8125rem; font-weight: 500; border: 1px solid var(--nav-bg-alt); background: var(--nav-bg); color: var(--nav-text-muted); cursor: pointer; transition: all 0.15s; white-space: nowrap; }
|
||
.taff-filter-btn:hover { background: var(--nav-bg-alt); }
|
||
.taff-filter-btn--active { font-weight: 600; }
|
||
|
||
.taff-search { display: flex; align-items: center; gap: 0.5rem; padding: 0.375rem 0.75rem; border-radius: 8px; border: 1px solid var(--nav-bg-alt); background: var(--nav-bg); flex: 1; min-width: 160px; max-width: 240px; }
|
||
.taff-search-input { flex: 1; background: transparent; border: none; outline: none; font-size: 0.8125rem; color: var(--nav-text); min-width: 0; }
|
||
.taff-search-input::placeholder { color: var(--nav-text-muted); }
|
||
.taff-search-clear { color: var(--nav-text-muted); background: none; border: none; cursor: pointer; padding: 0; display: flex; }
|
||
|
||
.taff-grid-wrap { padding: 1.5rem; }
|
||
.taff-grid { display: flex; flex-direction: column; gap: 0.75rem; max-width: 720px; margin: 0 auto; }
|
||
.taff-empty { text-align: center; padding: 3rem; }
|
||
.taff-reset-btn { margin-top: 0.75rem; padding: 0.5rem 1.25rem; border-radius: 8px; background: var(--nav-bg-alt); color: var(--nav-text); font-size: 0.875rem; border: none; cursor: pointer; }
|
||
.taff-reset-btn:hover { opacity: 0.7; }
|
||
|
||
.taff-disclaimer { margin: 0 1.5rem; padding: 0.875rem 1.25rem; border-radius: 10px; font-size: 0.8125rem; line-height: 1.55; color: var(--nav-text-muted); background: var(--nav-bg-alt); }
|
||
|
||
/* Modal body helpers */
|
||
.modal-label { font-size: 0.6875rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em; color: var(--nav-text-muted); margin-bottom: 0.75rem; }
|
||
.modal-axes { display: flex; flex-wrap: wrap; gap: 0.5rem; }
|
||
.modal-axe { display: flex; align-items: center; gap: 0.5rem; padding: 0.5rem 0.875rem; border-radius: 8px; flex: 1 1 130px; min-width: 130px; }
|
||
.modal-meta-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 0.5rem; }
|
||
.modal-meta-item { display: flex; flex-direction: column; gap: 0.15rem; padding: 0.6rem 0.875rem; border-radius: 8px; background: var(--nav-bg-alt); }
|
||
.modal-meta-key { font-size: 0.6875rem; text-transform: uppercase; letter-spacing: 0.05em; font-weight: 700; color: var(--nav-text-muted); }
|
||
.modal-meta-val { font-size: 0.875rem; font-weight: 500; color: var(--nav-text); }
|
||
|
||
/* Transitions modal */
|
||
.taff-backdrop-enter-active, .taff-backdrop-leave-active { transition: opacity 0.2s; }
|
||
.taff-backdrop-enter-from, .taff-backdrop-leave-to { opacity: 0; }
|
||
.taff-modal-enter-active, .taff-modal-leave-active { transition: opacity 0.2s, transform 0.2s; }
|
||
.taff-modal-enter-from, .taff-modal-leave-to { opacity: 0; transform: translateX(-50%) translateY(-12px); }
|
||
|
||
/* ── Chatbot FAB ──────────────────────────────────────────────────── */
|
||
.taff-fab {
|
||
position: fixed;
|
||
bottom: 1.5rem;
|
||
right: 1.5rem;
|
||
z-index: 9998;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
padding: 0.75rem 1.125rem;
|
||
border-radius: 9999px;
|
||
background: var(--nav-primary-solid);
|
||
color: var(--nav-text-on-primary);
|
||
font-size: 0.875rem;
|
||
font-weight: 600;
|
||
border: none;
|
||
cursor: pointer;
|
||
box-shadow: 0 4px 20px rgba(26,34,56,0.3);
|
||
transition: transform 0.15s, box-shadow 0.15s;
|
||
}
|
||
.taff-fab:hover { transform: translateY(-2px); box-shadow: 0 8px 28px rgba(26,34,56,0.35); }
|
||
.taff-fab-label { white-space: nowrap; }
|
||
|
||
/* Panel chatbot */
|
||
.taff-chat-panel {
|
||
position: fixed;
|
||
bottom: 1.5rem;
|
||
right: 1.5rem;
|
||
z-index: 9999;
|
||
width: min(380px, calc(100vw - 2rem));
|
||
max-height: calc(100vh - 4rem);
|
||
background: var(--nav-surface);
|
||
border-radius: 16px;
|
||
box-shadow: 0 8px 40px rgba(26,34,56,0.25);
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
border: 1px solid var(--nav-bg-alt);
|
||
}
|
||
|
||
.taff-chat-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 0.875rem 1rem;
|
||
border-bottom: 1px solid var(--nav-bg-alt);
|
||
background: var(--nav-surface);
|
||
flex-shrink: 0;
|
||
}
|
||
.taff-chat-avatar {
|
||
width: 32px; height: 32px;
|
||
border-radius: 50%;
|
||
background: var(--nav-primary-solid);
|
||
display: flex; align-items: center; justify-content: center;
|
||
flex-shrink: 0;
|
||
}
|
||
.taff-chat-close {
|
||
width: 28px; height: 28px;
|
||
border-radius: 8px;
|
||
background: var(--nav-bg-alt);
|
||
color: var(--nav-text-muted);
|
||
border: none; cursor: pointer;
|
||
display: flex; align-items: center; justify-content: center;
|
||
transition: opacity 0.15s;
|
||
}
|
||
.taff-chat-close:hover { opacity: 0.7; }
|
||
|
||
.taff-chat-messages {
|
||
flex: 1;
|
||
overflow-y: auto;
|
||
padding: 1rem;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 0.625rem;
|
||
}
|
||
|
||
.taff-msg {
|
||
padding: 0.625rem 0.875rem;
|
||
border-radius: 12px;
|
||
font-size: 0.875rem;
|
||
line-height: 1.55;
|
||
max-width: 92%;
|
||
}
|
||
.taff-msg--bot {
|
||
background: var(--nav-bg-alt);
|
||
color: var(--nav-text);
|
||
align-self: flex-start;
|
||
border-bottom-left-radius: 4px;
|
||
}
|
||
.taff-msg--user {
|
||
background: var(--nav-primary-solid);
|
||
color: var(--nav-text-on-primary);
|
||
align-self: flex-end;
|
||
border-bottom-right-radius: 4px;
|
||
}
|
||
|
||
.taff-chat-recos {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 0.375rem;
|
||
align-self: flex-start;
|
||
max-width: 92%;
|
||
}
|
||
.taff-reco-chip {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 0.25rem;
|
||
padding: 0.3rem 0.75rem;
|
||
border-radius: 9999px;
|
||
font-size: 0.8125rem;
|
||
font-weight: 600;
|
||
background: var(--nav-bg);
|
||
color: var(--nav-text);
|
||
border: 1px solid var(--nav-bg-alt);
|
||
cursor: pointer;
|
||
transition: background 0.15s;
|
||
}
|
||
.taff-reco-chip:hover { background: var(--nav-bg-alt); }
|
||
.taff-reco-arrow { opacity: 0.5; }
|
||
|
||
/* Typing indicator */
|
||
.taff-typing { display: inline-flex; gap: 4px; align-items: center; }
|
||
.taff-typing span {
|
||
width: 6px; height: 6px;
|
||
border-radius: 50%;
|
||
background: var(--nav-text-muted);
|
||
animation: taff-bounce 1.2s infinite;
|
||
}
|
||
.taff-typing span:nth-child(2) { animation-delay: 0.2s; }
|
||
.taff-typing span:nth-child(3) { animation-delay: 0.4s; }
|
||
@keyframes taff-bounce {
|
||
0%, 80%, 100% { transform: translateY(0); opacity: 0.4; }
|
||
40% { transform: translateY(-5px); opacity: 1; }
|
||
}
|
||
|
||
.taff-chat-input-wrap {
|
||
display: flex;
|
||
align-items: flex-end;
|
||
gap: 0.5rem;
|
||
padding: 0.75rem;
|
||
border-top: 1px solid var(--nav-bg-alt);
|
||
background: var(--nav-surface);
|
||
flex-shrink: 0;
|
||
}
|
||
.taff-chat-input {
|
||
flex: 1;
|
||
resize: none;
|
||
border: 1px solid var(--nav-bg-alt);
|
||
border-radius: 10px;
|
||
padding: 0.5rem 0.75rem;
|
||
font-size: 0.875rem;
|
||
background: var(--nav-bg);
|
||
color: var(--nav-text);
|
||
font-family: var(--nav-font);
|
||
outline: none;
|
||
line-height: 1.5;
|
||
}
|
||
.taff-chat-input::placeholder { color: var(--nav-text-muted); }
|
||
.taff-chat-input:focus { border-color: var(--nav-primary); }
|
||
|
||
.taff-chat-send {
|
||
width: 36px; height: 36px;
|
||
border-radius: 10px;
|
||
background: var(--nav-primary-solid);
|
||
color: var(--nav-text-on-primary);
|
||
border: none; cursor: pointer;
|
||
display: flex; align-items: center; justify-content: center;
|
||
flex-shrink: 0;
|
||
transition: opacity 0.15s;
|
||
}
|
||
.taff-chat-send:disabled { opacity: 0.4; cursor: not-allowed; }
|
||
.taff-chat-send:not(:disabled):hover { opacity: 0.85; }
|
||
|
||
.taff-chat-footer-note {
|
||
text-align: center;
|
||
font-size: 0.6875rem;
|
||
color: var(--nav-text-muted);
|
||
padding: 0.375rem;
|
||
background: var(--nav-surface);
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
/* Transition panel chatbot */
|
||
.taff-chat-enter-active, .taff-chat-leave-active { transition: opacity 0.2s, transform 0.2s; }
|
||
.taff-chat-enter-from, .taff-chat-leave-to { opacity: 0; transform: translateY(12px) scale(0.97); }
|
||
</style>
|