feat(ux): markdown chatbots + header Jobs centré + cible archi indépendants

- 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>
This commit is contained in:
Jules Neny
2026-05-07 01:26:46 +02:00
parent 6525afd5f5
commit ec9178be08
4 changed files with 67 additions and 9 deletions

View File

@@ -108,3 +108,16 @@
.dark .leaflet-popup-tip { .dark .leaflet-popup-tip {
background: var(--nav-surface); background: var(--nav-surface);
} }
/* ── Rendu Markdown chatbot (useMarkdown composable) ────────────────────── */
.md-content { font-size: inherit; line-height: 1.6; }
.md-content p { margin: 0 0 0.5em; }
.md-content p:last-child { margin-bottom: 0; }
.md-content strong, .md-h1, .md-h2, .md-h3 { font-weight: 700; }
.md-h2 { font-size: 0.9375em; display: block; margin-bottom: 0.25em; }
.md-h3 { font-size: 0.875em; display: block; }
.md-content em { font-style: italic; }
.md-list { margin: 0.375em 0 0.375em 1em; padding: 0; list-style: disc; }
.md-list li { margin-bottom: 0.2em; }
.md-link { text-decoration: underline; opacity: 0.85; }
.md-link:hover { opacity: 1; }

View File

@@ -92,7 +92,7 @@ employeur, besoin conseil juridique droit du travail,
<!-- Message assistant --> <!-- Message assistant -->
<div v-else class="assistant-bubble"> <div v-else class="assistant-bubble">
<p>{{ msg.content }}</p> <div class="md-content" v-html="renderMd(msg.content)" />
<!-- Fiches recommandées --> <!-- Fiches recommandées -->
<div v-if="msg.fiches && msg.fiches.length > 0" class="fiches-list"> <div v-if="msg.fiches && msg.fiches.length > 0" class="fiches-list">
@@ -164,6 +164,8 @@ employeur, besoin conseil juridique droit du travail,
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
const { render: renderMd } = useMarkdown()
interface FicheReco { interface FicheReco {
id: number | string id: number | string
nom: string nom: string

View File

@@ -0,0 +1,35 @@
/**
* Convertit du Markdown Mistral en HTML sécurisé.
* Gère : **bold**, *italic*, ## titres, listes - et •, retours à la ligne.
* Pas de dépendance externe.
*/
export function useMarkdown() {
function render(text: string): string {
if (!text) return ''
let html = text
// Échappement XSS de base
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
// Titres ## et ###
.replace(/^### (.+)$/gm, '<strong class="md-h3">$1</strong>')
.replace(/^## (.+)$/gm, '<strong class="md-h2">$1</strong>')
.replace(/^# (.+)$/gm, '<strong class="md-h1">$1</strong>')
// Bold et italic
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
.replace(/\*(.+?)\*/g, '<em>$1</em>')
// Listes (- item ou • item en début de ligne)
.replace(/^[-•]\s+(.+)$/gm, '<li>$1</li>')
// Liens [texte](url)
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener" class="md-link">$1</a>')
// Grouper les <li> consécutifs dans un <ul>
html = html.replace(/(<li>.*<\/li>\n?)+/g, match => `<ul class="md-list">${match}</ul>`)
// Paragraphes : double saut de ligne → séparateur
html = html.replace(/\n{2,}/g, '</p><p>')
// Saut de ligne simple → <br>
html = html.replace(/\n/g, '<br>')
return `<p>${html}</p>`
}
return { render }
}

View File

@@ -6,9 +6,12 @@
<div class="taff-header-inner"> <div class="taff-header-inner">
<h1 class="taff-title">Trouver du taf en archi</h1> <h1 class="taff-title">Trouver du taf en archi</h1>
<p class="taff-subtitle"> <p class="taff-subtitle">
Annuaire critique des plateformes de mise en relation archi particulier. 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. évaluées sur 5 axes éthiques rémunération, transparence, pratiques pro, écologie, qualité du matching.
Cible : archi freelance indépendant en France. </p>
<p class="taff-cible">
Cette carte s'adresse aux <strong>architectes indépendants</strong> —
70&nbsp;% de la profession et sa part la plus précaire économiquement.
</p> </p>
<div class="taff-stats"> <div class="taff-stats">
<span class="taff-stat" style="color: #3d5534;"> <span class="taff-stat" style="color: #3d5534;">
@@ -186,7 +189,8 @@
<!-- Messages de la conversation --> <!-- Messages de la conversation -->
<template v-for="(msg, i) in chatMessages" :key="i"> <template v-for="(msg, i) in chatMessages" :key="i">
<div :class="['taff-msg', msg.role === 'user' ? 'taff-msg--user' : 'taff-msg--bot']"> <div :class="['taff-msg', msg.role === 'user' ? 'taff-msg--user' : 'taff-msg--bot']">
<p>{{ msg.content }}</p> <div v-if="msg.role === 'bot'" class="md-content" v-html="renderMd(msg.content)" />
<span v-else>{{ msg.content }}</span>
</div> </div>
<!-- Plateformes recommandées --> <!-- Plateformes recommandées -->
<div v-if="msg.role === 'bot' && msg.recommandations?.length" class="taff-chat-recos"> <div v-if="msg.role === 'bot' && msg.recommandations?.length" class="taff-chat-recos">
@@ -499,6 +503,8 @@ interface ChatMessage {
recommandations?: { id: string; nom: string; raison: string }[] recommandations?: { id: string; nom: string; raison: string }[]
} }
const { render: renderMd } = useMarkdown()
const chatOpen = ref(false) const chatOpen = ref(false)
const chatInput = ref('') const chatInput = ref('')
const chatLoading = ref(false) const chatLoading = ref(false)
@@ -562,11 +568,13 @@ const parsedDescription = computed(() => {
<style scoped> <style scoped>
.taff-page { max-width: 1280px; margin: 0 auto; padding-bottom: 3rem; } .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); } .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; } .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-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: 1rem; } .taff-subtitle { font-size: 0.9375rem; color: var(--nav-text-muted); line-height: 1.6; margin-bottom: 0.625rem; }
.taff-stats { display: flex; gap: 1.25rem; flex-wrap: wrap; } .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 { 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-stat-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }