Compare commits
5 Commits
a0e59442f4
...
ef7b91f1ff
| Author | SHA1 | Date | |
|---|---|---|---|
| ef7b91f1ff | |||
| c879fc1830 | |||
| 2878495e7a | |||
| f9ecf6cd1a | |||
| a2c0c78590 |
@ -24,6 +24,13 @@ my-next-site/
|
|||||||
|
|
||||||
**Observabilité** : le chatbot GrasBot est tracé dans une instance **Langfuse self-hosted** (`langfuse.fernandgrascalvet.com`). Chaque question déclenche une trace `ask` avec spans `retrieval` / `prompt_build` / `ollama-chat`, plus des scores auto (`grounded`, `retrieval_relevance`) et des tags. Voir `docs-site-interne/langfuse-observability.md` pour le détail.
|
**Observabilité** : le chatbot GrasBot est tracé dans une instance **Langfuse self-hosted** (`langfuse.fernandgrascalvet.com`). Chaque question déclenche une trace `ask` avec spans `retrieval` / `prompt_build` / `ollama-chat`, plus des scores auto (`grounded`, `retrieval_relevance`) et des tags. Voir `docs-site-interne/langfuse-observability.md` pour le détail.
|
||||||
|
|
||||||
|
## Contenu : compétences, réalisations IA et ordre d’affichage
|
||||||
|
|
||||||
|
- **Ordre** de la liste `/competences` : champ `order` sur le content-type compétence. Selon la version de Strapi, l’API renvoie `attributes.order` (v4) ou `order` (v5) — le front unifie cela (voir `getOrder()` dans `app/competences/page.jsx`).
|
||||||
|
- **Fiche** `/competences/[slug]` : affiche des **vignettes** (projets de type `realisation-ia` liés à la compétence) dès qu’il en existe ; sinon, rendu de la fiche richtext « classique ».
|
||||||
|
- **Détail** d’une réalisation : route `/competences/[slug]/[realisation]` (même gabarit de contenu qu’une fiche portfolio).
|
||||||
|
- **Documentation détaillée** (tableau des routes, champs Strapi, lien avec GrasBot) : `docs-site-interne/02-frontend-next.md` et, côté API chatbot, `docs-site-interne/04-api-llm-et-chatbot.md` (section *Parcours public*).
|
||||||
|
|
||||||
## 🚀 Démarrage Rapide
|
## 🚀 Démarrage Rapide
|
||||||
|
|
||||||
### Script Automatique (Recommandé)
|
### Script Automatique (Recommandé)
|
||||||
@ -287,6 +294,6 @@ Pensez à sauvegarder régulièrement :
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Dernière mise à jour** : $(Get-Date -Format "dd/MM/yyyy HH:mm")
|
**Dernière mise à jour** : 2026-04-24
|
||||||
**Version Cursor** : 2.0.77
|
**Version Cursor** : 2.0.77
|
||||||
**OS** : Windows Server 2025
|
**OS** : Windows Server 2025
|
||||||
|
|||||||
192
README.md
@ -1,163 +1,77 @@
|
|||||||
# Mon site protfolio
|
# Portfolio fernandgrascalvet.com
|
||||||

|
|
||||||
J'ai réalisé ce projet afin d'étendre mes compétences en développement Web.
|
|
||||||
|
|
||||||
Ce projet est un site web basé sur Next.js pour le frontend et Strapi pour le backend, hébergé sur un serveur Windows Server 2025 avec IIS comme serveur web. Il repose sur une architecture Headless CMS, où le contenu est géré via une API REST et affiché dynamiquement sur le frontend.
|

|
||||||
|
|
||||||
🔹 Technologies utilisées
|
Site portfolio **Next.js 15** + **Strapi 5** + **GrasBot** (FastAPI, Ollama, vault `vault-grasbot/`). UI *Digital Atelier* (Manrope, Newsreader, Tailwind). Hébergement typique : Windows Server, IIS, HTTPS (Win-ACME).
|
||||||
|
|
||||||
Frontend (Client) :
|
**Site en ligne :** [fernandgrascalvet.com](https://fernandgrascalvet.com)
|
||||||
|
|
||||||
Framework : Next.js (React, TypeScript, Server-Side Rendering & Static Generation)
|
## Documentation
|
||||||
|
|
||||||
Styling : Tailwind CSS
|
| Ressource | Rôle |
|
||||||
|
|-----------|------|
|
||||||
|
| **[docs-site-interne](docs-site-interne/README.md)** | Architecture, CMS, front, API LLM, feuille de route, état actuel, captures, refonte UI. *À lire en priorité pour le contexte technique.* |
|
||||||
|
| [`CONFIGURATION_SITE.md`](CONFIGURATION_SITE.md) | Opérationnel : ports, commandes, démarrage automatique, dépannage, pare-feu. |
|
||||||
|
| [`vault-grasbot/README.md`](vault-grasbot/README.md) | Base de connaissances GrasBot (retrieval graph + BM25 v3). |
|
||||||
|
|
||||||
Gestion des requêtes API : Fetch API (avec qs pour structurer les requêtes)
|
**Obsidian / export** : le dossier [`obsidian-site-docs/`](obsidian-site-docs/) contient le hub, les commandes, **toute** la doc [`docs-site-interne/`](docs-site-interne/) (copie), `CONFIGURATION_SITE.md` et le README dépôt — prêt à ouvrir comme coffre ou à copier. Resynchro : [`obsidian-site-docs/SYNC-DOC.md`](obsidian-site-docs/SYNC-DOC.md).
|
||||||
|
|
||||||
SEO & Performance : Optimisation des images, pré-rendu des pages
|
## Démarrage rapide (Windows)
|
||||||
|
|
||||||
Backend (Serveur) :
|
Depuis la racine du dépôt (adapter le lecteur / chemin si besoin) :
|
||||||
|
|
||||||
CMS : Strapi (Node.js, API REST)
|
```powershell
|
||||||
|
# Les trois services : Next, Strapi, FastAPI (fenêtres séparées)
|
||||||
|
.\start-my-site.ps1
|
||||||
|
```
|
||||||
|
|
||||||
Base de données : PostgreSQL ou MySQL
|
```powershell
|
||||||
|
# Arrêt propre (ports 3000, 1337, 8000)
|
||||||
|
.\stop-my-site.ps1
|
||||||
|
```
|
||||||
|
|
||||||
Hébergement : IIS sur Windows Server 2025
|
Détail, ports et commandes manuelles : **[`CONFIGURATION_SITE.md`](CONFIGURATION_SITE.md)**.
|
||||||
|
|
||||||
Sécurité : HTTPS activé via Win-ACME (Let’s Encrypt)
|
## Stack (résumé)
|
||||||
|
|
||||||
Déploiement & Infrastructure :
|
- **Front** : Next.js (App Router), TypeScript/JS, Tailwind, Swiper, chatbot global (FAB).
|
||||||
|
- **CMS** : Strapi — `homepage`, `project`, `competence`, `realisation-ia`, `glossaire`.
|
||||||
|
- **Contact** : e-mail via **Brevo** (route Next `POST /api/contact`), pas de stockage Strapi des messages. Voir [`docs-site-interne/contact-flow.md`](docs-site-interne/contact-flow.md).
|
||||||
|
- **IA** : `llm-api/` (FastAPI) → Ollama (ex. Qwen3), base `vault-grasbot/`, observabilité **Langfuse** optionnelle. Voir [`docs-site-interne/04-api-llm-et-chatbot.md`](docs-site-interne/04-api-llm-et-chatbot.md).
|
||||||
|
|
||||||
Système d’exploitation : Windows Server 2025
|
## Rechargement du vault GrasBot (API locale)
|
||||||
|
|
||||||
Serveur Web : IIS 10 (gestion des proxys et reverse proxy pour Next.js & Strapi)
|
Après modification des fichiers dans `vault-grasbot/`, recharger le cache côté API sans redémarrer uvicorn :
|
||||||
|
|
||||||
Gestion des certificats SSL : Win-ACME pour le renouvellement automatique des certificats HTTPS
|
```powershell
|
||||||
|
# Exemple (Invoke-RestMethod)
|
||||||
|
Invoke-RestMethod -Method Post -Uri "http://localhost:8000/reload-vault"
|
||||||
|
```
|
||||||
|
|
||||||
Monitoring : Logs IIS + Console Next.js & Strapi
|
Ou : `POST http://localhost:8000/reload-vault` (HTTP client de votre choix). Voir aussi [`CONFIGURATION_SITE.md`](CONFIGURATION_SITE.md) (santé `/health`, endpoint `/ask`).
|
||||||
|
|
||||||
🔹 Fonctionnalités du site
|
## Dépôts et répertoires utiles
|
||||||
|
|
||||||
✅ Affichage dynamique des compétences (compétences récupérées via API Strapi)
|
```
|
||||||
|
my-next-site/
|
||||||
|
├── app/ # Next.js
|
||||||
|
├── cmsbackend/ # Strapi
|
||||||
|
├── llm-api/ # FastAPI + GrasBot
|
||||||
|
├── vault-grasbot/ # Connaissance (Obsidian) pour le retrieval
|
||||||
|
├── strapi_extraction/ # Extraction / build vault
|
||||||
|
├── docs-site-interne/ # Doc technique détaillée
|
||||||
|
├── obsidian-site-docs/ # Pack Obsidian (résumés + commandes)
|
||||||
|
├── start-my-site.ps1
|
||||||
|
├── stop-my-site.ps1
|
||||||
|
└── CONFIGURATION_SITE.md
|
||||||
|
```
|
||||||
|
|
||||||
✅ Glossaire interactif avec mots-clés détectés dynamiquement
|
## Licence et usage
|
||||||
|
|
||||||
✅ Carousel d'images pour présenter les compétences
|
Projet personnel ; contenu et code sont fournis tels quels pour illustration du portfolio.
|
||||||
|
|
||||||
✅ Navigation rapide et fluide grâce à Next.js
|
---
|
||||||
|
|
||||||
✅ SEO optimisé via les pages statiques et le rendu dynamique
|
*Dernière révision du README : 2026-04 — aligné sur `docs-site-interne` et `CONFIGURATION_SITE.md`.*
|
||||||
|
|
||||||
Ce projet est toujours en développement, je l'agrémenterai de contenu au fil du temps.
|
Pour lancer tâches planifiées: taskschd.msc
|
||||||
|
|
||||||
Il m'a permis brièvement de me familiariser a plusieurs domaines.
|
|
||||||
|
|
||||||
1️⃣ Développement Web 🌐
|
|
||||||
|
|
||||||
Ce projet est principalement un site web dynamique reposant sur Next.js et Strapi, ce qui le place dans la catégorie du développement web moderne.
|
|
||||||
|
|
||||||
Frontend (Next.js, React, TypeScript) → Développement web côté client
|
|
||||||
|
|
||||||
Backend (Strapi, Node.js, API REST) → Développement web côté serveur
|
|
||||||
|
|
||||||
API et Headless CMS → Gestion de contenu via une API
|
|
||||||
|
|
||||||
2️⃣ Hébergement et Administration Systèmes 🖥️
|
|
||||||
|
|
||||||
Étant donné que le site est auto-hébergé sur un serveur Windows Server 2025 avec IIS, il appartient aussi à la catégorie administration système et hébergement web.
|
|
||||||
|
|
||||||
Configuration d’un serveur web (IIS, Windows Server 2025)
|
|
||||||
|
|
||||||
Gestion des certificats SSL avec Win-ACME (HTTPS, sécurité)
|
|
||||||
|
|
||||||
Base de données (PostgreSQL ou MySQL)
|
|
||||||
|
|
||||||
Surveillance et gestion des performances (logs IIS, monitoring)
|
|
||||||
|
|
||||||
3️⃣ Cloud & DevOps (partiellement) ☁️
|
|
||||||
|
|
||||||
Même si ce projet n’utilise pas un service cloud public (Azure, AWS, GCP), il comporte des éléments liés à l’automatisation et à la gestion des déploiements.
|
|
||||||
|
|
||||||
Déploiement d’une application Next.js & Strapi sur un serveur dédié
|
|
||||||
|
|
||||||
Gestion des certificats SSL automatisée (Win-ACME, Let's Encrypt)
|
|
||||||
|
|
||||||
Possibilité d’extensions avec CI/CD pour automatiser les mises à jour
|
|
||||||
|
|
||||||
4️⃣ Sécurité Informatique 🔒
|
|
||||||
|
|
||||||
Avec l’implémentation du HTTPS, de l’authentification API et de la gestion des accès via Strapi et IIS, ce projet a aussi un aspect cybersécurité.
|
|
||||||
|
|
||||||
Chiffrement des connexions avec SSL/TLS (HTTPS activé)
|
|
||||||
|
|
||||||
Protection des API (Cors, Access-Control-Allow-Origin, JWT si activé dans Strapi)
|
|
||||||
|
|
||||||
Gestion des permissions et authentification des utilisateurs (Strapi)
|
|
||||||
|
|
||||||
5️⃣ Expérience Utilisateur & SEO 📈
|
|
||||||
|
|
||||||
Le projet est conçu pour être rapide, interactif et optimisé pour le référencement.
|
|
||||||
|
|
||||||
SEO optimisé avec Next.js (Static Generation, Server-Side Rendering)
|
|
||||||
|
|
||||||
Performance améliorée grâce au préchargement et à la mise en cache
|
|
||||||
|
|
||||||
Expérience utilisateur fluide avec des animations et une navigation rapide
|
|
||||||
|
|
||||||
[lien du site] [https://fernandgrascalvet.com]
|
|
||||||
|
|
||||||
|
|
||||||
Pour lancer uvicrn:
|
|
||||||
uvicorn api:app --host 0.0.0.0 --port 8000 --reload
|
|
||||||
|
|
||||||
📋 Commandes manuelles individuelles
|
|
||||||
|
|
||||||
1. Strapi (Backend CMS)
|
|
||||||
cd J:\my-next-site\cmsbackend
|
|
||||||
npm run develop
|
|
||||||
cd J:\my-next-site\cmsbackendnpm run develop
|
|
||||||
Interface admin : http://localhost:1337/admin
|
|
||||||
API : http://localhost:1337/api
|
|
||||||
|
|
||||||
2. Next.js (Frontend)
|
|
||||||
cd J:\my-next-site
|
|
||||||
npm run dev
|
|
||||||
cd J:\my-next-sitenpm run dev
|
|
||||||
Site web : http://localhost:3000
|
|
||||||
Utilise Turbopack pour un rechargement rapide
|
|
||||||
|
|
||||||
3. FastAPI (LLM API)
|
|
||||||
cd J:\my-next-site\llm-api
|
|
||||||
uvicorn api:app --host 0.0.0.0 --port 8000 --reload
|
|
||||||
cd J:\my-next-site\llm-apiuvicorn api:app --host 0.0.0.0 --port 8000 --reload
|
|
||||||
API IA : http://localhost:8000
|
|
||||||
Endpoint : http://localhost:8000/ask?q=votre_question
|
|
||||||
|
|
||||||
🔧 Commandes de dépannage
|
|
||||||
Arrêter tous les processus Node.js
|
|
||||||
taskkill /f /im node.exe
|
|
||||||
taskkill /f /im python.exe
|
|
||||||
taskkill /f /im node.exetaskkill /f /im python.exe
|
|
||||||
Nettoyer les caches
|
|
||||||
# Next.js
|
|
||||||
cd J:\my-next-site
|
|
||||||
rm -r .next -Force -ErrorAction SilentlyContinue
|
|
||||||
|
|
||||||
# Strapi
|
|
||||||
cd J:\my-next-site\cmsbackend
|
|
||||||
rm -r .cache -Force -ErrorAction SilentlyContinue
|
|
||||||
# Next.jscd J:\my-next-siterm -r .next -Force -ErrorAction SilentlyContinue# Strapicd J:\my-next-site\cmsbackendrm -r .cache -Force -ErrorAction SilentlyContinue
|
|
||||||
Réinstaller les dépendances
|
|
||||||
# Frontend
|
|
||||||
cd J:\my-next-site
|
|
||||||
npm install
|
|
||||||
|
|
||||||
# Backend
|
|
||||||
cd J:\my-next-site\cmsbackend
|
|
||||||
npm install
|
|
||||||
# Frontendcd J:\my-next-sitenpm install# Backendcd J:\my-next-site\cmsbackendnpm install
|
|
||||||
📊 Ports utilisés
|
|
||||||
Next.js : 3000
|
|
||||||
Strapi : 1337
|
|
||||||
FastAPI : 8000
|
|
||||||
Ollama : 11434
|
|
||||||
89
app/Competences/[slug]/[realisation]/page.tsx
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useParams } from "next/navigation";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import ContentSection from "../../../components/ContentSection";
|
||||||
|
import { getApiUrl } from "../../../utils/getApiUrl";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Page détail d'une réalisation liée à une compétence.
|
||||||
|
*
|
||||||
|
* Route : `/competences/[slug]/[realisation]`
|
||||||
|
* - `slug` : slug de la compétence parente (ex. `ia`).
|
||||||
|
* - `realisation` : slug de la réalisation (ex. `grasbot`, `newsletter-ia`…).
|
||||||
|
*
|
||||||
|
* Rendu : on réutilise intégralement `ContentSection` (même carousel Swiper,
|
||||||
|
* même prose Markdown Newsreader, même CTA jewel, même skeleton, même état
|
||||||
|
* 404) avec la collection Strapi `realisation-ias` et un bouton retour qui
|
||||||
|
* renvoie vers la page vignettes de la compétence parente plutôt que vers le
|
||||||
|
* portfolio.
|
||||||
|
*
|
||||||
|
* Le nom exact de la compétence parente est fetché pour personnaliser le
|
||||||
|
* kicker (ex. *"Réalisation · Mon exploration et maîtrise de l'IA"*). Si le
|
||||||
|
* fetch échoue, on tombe silencieusement sur un libellé générique
|
||||||
|
* *"Réalisation · Compétence"*.
|
||||||
|
*/
|
||||||
|
export default function RealisationDetailPage() {
|
||||||
|
const params = useParams();
|
||||||
|
const competenceSlug =
|
||||||
|
typeof params?.slug === "string" ? params.slug : null;
|
||||||
|
const realisationSlug =
|
||||||
|
typeof params?.realisation === "string" ? params.realisation : null;
|
||||||
|
|
||||||
|
const [competenceName, setCompetenceName] = useState<string | null>(null);
|
||||||
|
const apiUrl = getApiUrl();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!competenceSlug) return;
|
||||||
|
|
||||||
|
let cancelled = false;
|
||||||
|
|
||||||
|
async function fetchCompetenceName() {
|
||||||
|
try {
|
||||||
|
const res = await fetch(
|
||||||
|
`${apiUrl}/api/competences?filters[slug][$eq]=${encodeURIComponent(
|
||||||
|
competenceSlug!
|
||||||
|
)}`
|
||||||
|
);
|
||||||
|
if (!res.ok) return;
|
||||||
|
const data = await res.json();
|
||||||
|
const name: string | undefined = data?.data?.[0]?.name;
|
||||||
|
if (!cancelled && name) {
|
||||||
|
setCompetenceName(name);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// silencieux : le kicker restera générique
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchCompetenceName();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, [apiUrl, competenceSlug]);
|
||||||
|
|
||||||
|
if (!competenceSlug || !realisationSlug) {
|
||||||
|
return (
|
||||||
|
<div className="mx-auto max-w-6xl px-4 py-10 text-center text-on-surface-variant">
|
||||||
|
<p className="font-body italic">⏳ Chargement...</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const backHref = `/competences/${competenceSlug}`;
|
||||||
|
const kickerLabel = competenceName
|
||||||
|
? `Réalisation · ${competenceName}`
|
||||||
|
: "Réalisation · Compétence";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ContentSection
|
||||||
|
collection="realisation-ias"
|
||||||
|
slug={realisationSlug}
|
||||||
|
backHref={backHref}
|
||||||
|
backLabel="Réalisations"
|
||||||
|
kickerLabel={kickerLabel}
|
||||||
|
notFoundLabel="Cette réalisation est introuvable."
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -2,21 +2,277 @@
|
|||||||
|
|
||||||
import { useParams } from "next/navigation";
|
import { useParams } from "next/navigation";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { getApiUrl } from "../../utils/getApiUrl";
|
||||||
|
import VignetteCarousel from "../../components/VignetteCarousel";
|
||||||
import ContentSectionCompetencesContainer from "../../components/ContentSectionCompetencesContainer";
|
import ContentSectionCompetencesContainer from "../../components/ContentSectionCompetencesContainer";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Page détail d'une compétence — double rendu selon le contenu Strapi.
|
||||||
|
*
|
||||||
|
* Cas 1 — la compétence est liée à une ou plusieurs `realisation-ia` :
|
||||||
|
* on affiche une **grille de vignettes** alignée sur le pattern éditorial
|
||||||
|
* de `app/portfolio/page.jsx` (grille asymétrique 2/3 + 1/3 alternée). Chaque
|
||||||
|
* vignette est cliquable (lien externe si le champ `link` de la réalisation
|
||||||
|
* est renseigné, sinon lien interne vers la future page détail
|
||||||
|
* `/competences/[slug]/[realisation]` — pour l'instant 404 tant que cette
|
||||||
|
* route n'est pas ajoutée, ce qui est attendu pour l'étape de test du listing).
|
||||||
|
*
|
||||||
|
* Cas 2 — aucune réalisation liée :
|
||||||
|
* on conserve le rendu historique `ContentSectionCompetencesContainer` qui
|
||||||
|
* affiche le `content` richtext de la compétence. Les compétences Web / 3D
|
||||||
|
* restent donc strictement inchangées tant qu'on ne leur associe pas de
|
||||||
|
* réalisation côté Strapi.
|
||||||
|
*
|
||||||
|
* Endpoint Strapi utilisé : `/api/realisation-ias` (pluralName par défaut
|
||||||
|
* Strapi 5 pour un singularName `realisation-ia`). Filtre sur le slug de la
|
||||||
|
* compétence parente via la relation `competences` (many-to-many d'après
|
||||||
|
* le content-type créé côté admin).
|
||||||
|
*/
|
||||||
|
const spanPattern = ["md:col-span-4", "md:col-span-2", "md:col-span-2", "md:col-span-4"];
|
||||||
|
|
||||||
|
type Realisation = {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
slug?: string;
|
||||||
|
description?: string;
|
||||||
|
link?: string;
|
||||||
|
order?: number;
|
||||||
|
picture?: Array<{ url?: string; name?: string }>;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Competence = {
|
||||||
|
id: number;
|
||||||
|
name?: string;
|
||||||
|
slug?: string;
|
||||||
|
content?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export default function CompetencePage() {
|
export default function CompetencePage() {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const [slug, setSlug] = useState<string | null>(null);
|
const slug = typeof params?.slug === "string" ? params.slug : null;
|
||||||
|
|
||||||
|
const [competence, setCompetence] = useState<Competence | null>(null);
|
||||||
|
const [realisations, setRealisations] = useState<Realisation[] | null>(null);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const apiUrl = getApiUrl();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (params?.slug) {
|
if (!slug) return;
|
||||||
setSlug(params.slug as string);
|
|
||||||
|
let cancelled = false;
|
||||||
|
|
||||||
|
async function fetchData() {
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. Compétence parente (pour le titre de la page vignettes).
|
||||||
|
const resCompet = await fetch(
|
||||||
|
`${apiUrl}/api/competences?filters[slug][$eq]=${encodeURIComponent(
|
||||||
|
slug!
|
||||||
|
)}&populate=*`
|
||||||
|
);
|
||||||
|
const dataCompet = resCompet.ok ? await resCompet.json() : null;
|
||||||
|
const fetchedCompetence: Competence | null =
|
||||||
|
dataCompet?.data?.[0] ?? null;
|
||||||
|
|
||||||
|
// 2. Réalisations IA liées à cette compétence.
|
||||||
|
// Si l'endpoint n'existe pas encore côté Strapi (404), on bascule
|
||||||
|
// silencieusement sur le rendu historique au lieu de crasher.
|
||||||
|
let fetchedRealisations: Realisation[] = [];
|
||||||
|
const resReal = await fetch(
|
||||||
|
`${apiUrl}/api/realisation-ias?filters[competences][slug][$eq]=${encodeURIComponent(
|
||||||
|
slug!
|
||||||
|
)}&populate=picture&sort=order:asc`
|
||||||
|
);
|
||||||
|
if (resReal.ok) {
|
||||||
|
const dataReal = await resReal.json();
|
||||||
|
fetchedRealisations = (dataReal?.data ?? []).sort(
|
||||||
|
(a: Realisation, b: Realisation) =>
|
||||||
|
(a.order ?? 999) - (b.order ?? 999)
|
||||||
|
);
|
||||||
|
} else if (resReal.status !== 404) {
|
||||||
|
// 404 = content-type absent, cas légitime → on log seulement les
|
||||||
|
// vraies erreurs HTTP (500, 403, etc.).
|
||||||
|
console.warn(
|
||||||
|
`⚠️ [competences/${slug}] realisation-ias HTTP ${resReal.status}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!cancelled) {
|
||||||
|
setCompetence(fetchedCompetence);
|
||||||
|
setRealisations(fetchedRealisations);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (!cancelled) {
|
||||||
|
console.error("❌ [competences/[slug]] Erreur fetch :", err);
|
||||||
|
setRealisations([]);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (!cancelled) setIsLoading(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [params]);
|
|
||||||
|
fetchData();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, [apiUrl, slug]);
|
||||||
|
|
||||||
if (!slug) {
|
if (!slug) {
|
||||||
return <div className="text-center text-gray-500">⏳ Chargement...</div>;
|
return (
|
||||||
|
<div className="mx-auto max-w-6xl px-4 py-10 text-center text-on-surface-variant">
|
||||||
|
<p className="font-body italic">⏳ Chargement...</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return <ContentSectionCompetencesContainer collection="competences" slug={slug} />;
|
// Squelette commun pendant le premier fetch (avant de savoir s'il y a des vignettes ou pas).
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="mx-auto flex w-full min-w-0 max-w-6xl flex-col gap-5 px-4 pb-10 sm:px-6">
|
||||||
|
<section className="rounded-sheet bg-surface-container-lowest/60 p-6 shadow-ambient-sm backdrop-blur-vellum sm:p-8">
|
||||||
|
<div className="h-3 w-24 animate-pulse rounded-full bg-surface-container-low/80" />
|
||||||
|
<div className="mt-4 h-8 w-2/3 animate-pulse rounded-full bg-surface-container-low/80" />
|
||||||
|
<div className="mt-3 h-4 w-full animate-pulse rounded-full bg-surface-container-low/60" />
|
||||||
|
</section>
|
||||||
|
<div className="grid grid-cols-1 gap-5 sm:grid-cols-2 md:grid-cols-6">
|
||||||
|
{Array.from({ length: 4 }).map((_, idx) => (
|
||||||
|
<div
|
||||||
|
key={idx}
|
||||||
|
className={`${spanPattern[idx % spanPattern.length]} rounded-sheet bg-surface-container-lowest/60 p-5 shadow-ambient-sm backdrop-blur-vellum`}
|
||||||
|
>
|
||||||
|
<div className="aspect-[4/3] w-full animate-pulse rounded-tile bg-surface-container-low/80" />
|
||||||
|
<div className="mt-4 h-5 w-2/3 animate-pulse rounded-full bg-surface-container-low/80" />
|
||||||
|
<div className="mt-2 h-4 w-full animate-pulse rounded-full bg-surface-container-low/60" />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pas de réalisations associées → rendu historique (toutes les compétences hors IA).
|
||||||
|
if (!realisations || realisations.length === 0) {
|
||||||
|
return (
|
||||||
|
<ContentSectionCompetencesContainer collection="competences" slug={slug} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rendu vignettes.
|
||||||
|
return (
|
||||||
|
<div className="mx-auto flex w-full min-w-0 max-w-6xl flex-col gap-5 px-4 pb-10 sm:px-6">
|
||||||
|
<section
|
||||||
|
className="rounded-sheet bg-surface-container-lowest/85 p-5 shadow-ambient backdrop-blur-vellum sm:p-7 md:p-8"
|
||||||
|
aria-labelledby="realisations-title"
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-3 text-center md:text-left">
|
||||||
|
<Link
|
||||||
|
href="/competences"
|
||||||
|
className="inline-flex items-center gap-1.5 self-center font-headline text-xs font-bold uppercase tracking-[0.3em] text-primary transition hover:text-primary/80 md:self-start"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="material-symbols-outlined text-lg"
|
||||||
|
aria-hidden="true"
|
||||||
|
translate="no"
|
||||||
|
>
|
||||||
|
arrow_back
|
||||||
|
</span>
|
||||||
|
Retour aux compétences
|
||||||
|
</Link>
|
||||||
|
<span className="font-headline text-[11px] font-bold uppercase tracking-[0.3em] text-secondary">
|
||||||
|
Compétence · Réalisations
|
||||||
|
</span>
|
||||||
|
<h1
|
||||||
|
id="realisations-title"
|
||||||
|
className="font-headline text-3xl font-extrabold tracking-tight text-on-surface md:text-4xl lg:text-5xl"
|
||||||
|
>
|
||||||
|
{competence?.name ?? "Compétence"}
|
||||||
|
</h1>
|
||||||
|
<p className="font-body text-on-surface-variant sm:text-lg">
|
||||||
|
Une sélection de réalisations qui illustrent cette compétence en
|
||||||
|
contexte — ouvrez une vignette pour en voir le détail.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 gap-5 sm:grid-cols-2 md:grid-cols-6">
|
||||||
|
{realisations.map((realisation, idx) => {
|
||||||
|
const pictures = realisation.picture ?? [];
|
||||||
|
const images = pictures.map((img) => ({
|
||||||
|
url: img?.url ? `${apiUrl}${img.url}` : "/placeholder.jpg",
|
||||||
|
alt: img?.name || `Visuel de la réalisation ${realisation.name}`,
|
||||||
|
}));
|
||||||
|
const firstImage = images[0];
|
||||||
|
|
||||||
|
// Comportement voulu : la vignette renvoie TOUJOURS vers la fiche
|
||||||
|
// détail interne (`/competences/[slug]/[realisation]`). Le lien
|
||||||
|
// externe `realisation.link` est exposé en tant que CTA jewel en bas
|
||||||
|
// de la fiche détail par `ContentSection`, pas en court-circuit
|
||||||
|
// depuis la vignette. Cohérent avec le comportement des fiches
|
||||||
|
// `project` du portfolio.
|
||||||
|
const href = realisation.slug
|
||||||
|
? `/competences/${slug}/${realisation.slug}`
|
||||||
|
: "#";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
key={realisation.id}
|
||||||
|
href={href}
|
||||||
|
className={`${spanPattern[idx % spanPattern.length]} group flex flex-col overflow-hidden rounded-sheet bg-surface-container-lowest/85 shadow-ambient backdrop-blur-vellum transition duration-300 hover:-translate-y-0.5 hover:shadow-jewel focus:outline-none focus-visible:ring-2 focus-visible:ring-primary`}
|
||||||
|
>
|
||||||
|
<div className="relative aspect-[4/3] w-full overflow-hidden bg-surface-container-low">
|
||||||
|
{images.length > 1 ? (
|
||||||
|
<VignetteCarousel images={images} />
|
||||||
|
) : firstImage ? (
|
||||||
|
<img
|
||||||
|
src={firstImage.url}
|
||||||
|
alt={firstImage.alt}
|
||||||
|
className="h-full w-full object-cover transition-transform duration-500 group-hover:scale-[1.03]"
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="flex h-full w-full items-center justify-center text-sm text-on-surface-variant">
|
||||||
|
<span
|
||||||
|
className="material-symbols-outlined text-3xl"
|
||||||
|
aria-hidden="true"
|
||||||
|
translate="no"
|
||||||
|
>
|
||||||
|
image
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-1 flex-col gap-2 p-5 sm:p-6">
|
||||||
|
<span className="font-headline text-[10px] font-bold uppercase tracking-[0.3em] text-secondary">
|
||||||
|
Réalisation
|
||||||
|
</span>
|
||||||
|
<h2 className="font-headline text-xl font-extrabold leading-tight tracking-tight text-primary">
|
||||||
|
{realisation.name}
|
||||||
|
</h2>
|
||||||
|
{realisation.description && (
|
||||||
|
<p className="font-body line-clamp-3 text-sm leading-relaxed text-on-surface-variant sm:text-base">
|
||||||
|
{realisation.description}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<span className="mt-auto inline-flex items-center gap-1.5 pt-3 font-headline text-sm font-bold uppercase tracking-[0.2em] text-primary">
|
||||||
|
Découvrir
|
||||||
|
<span
|
||||||
|
className="material-symbols-outlined text-lg transition-transform duration-300 group-hover:translate-x-1"
|
||||||
|
aria-hidden="true"
|
||||||
|
translate="no"
|
||||||
|
>
|
||||||
|
arrow_forward
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -33,8 +33,12 @@ export default function Page() {
|
|||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
|
// Strapi v4 : `attributes.order` — Strapi v5 (souvent) : `order` à la racine.
|
||||||
|
const getOrder = (item) =>
|
||||||
|
item?.order ?? item?.attributes?.order ?? 999;
|
||||||
|
|
||||||
const sortedCompetences = (data.data ?? []).sort(
|
const sortedCompetences = (data.data ?? []).sort(
|
||||||
(a, b) => ((a.attributes?.order ?? 999) - (b.attributes?.order ?? 999))
|
(a, b) => getOrder(a) - getOrder(b)
|
||||||
);
|
);
|
||||||
|
|
||||||
setCompetences(sortedCompetences);
|
setCompetences(sortedCompetences);
|
||||||
|
|||||||
@ -19,7 +19,18 @@ interface ImageData {
|
|||||||
|
|
||||||
interface ContentData {
|
interface ContentData {
|
||||||
name: string;
|
name: string;
|
||||||
Resum: string;
|
/**
|
||||||
|
* Champ richtext Markdown de la fiche.
|
||||||
|
*
|
||||||
|
* Dette historique : le content-type Strapi `project` utilise `Resum` avec
|
||||||
|
* majuscule (legacy Strapi 4). Les nouveaux content-types (ex.
|
||||||
|
* `realisation-ia`) utilisent `resum` en minuscule, cohérent avec tous les
|
||||||
|
* autres champs (`name`, `slug`, `link`, `order`…). On tolère les deux
|
||||||
|
* orthographes dans le rendu pour ne pas avoir à renommer `Resum` côté
|
||||||
|
* `project` (ce qui casserait les 15+ fiches projet déjà saisies).
|
||||||
|
*/
|
||||||
|
Resum?: string;
|
||||||
|
resum?: string;
|
||||||
picture?: ImageData[];
|
picture?: ImageData[];
|
||||||
link?: string;
|
link?: string;
|
||||||
linkText?: string;
|
linkText?: string;
|
||||||
@ -30,6 +41,26 @@ interface ContentSectionProps {
|
|||||||
slug: string;
|
slug: string;
|
||||||
titleClass?: string;
|
titleClass?: string;
|
||||||
contentClass?: string;
|
contentClass?: string;
|
||||||
|
/**
|
||||||
|
* Lien du bouton retour discret posé en haut de la page.
|
||||||
|
* Défaut : `/portfolio` (comportement historique pour les fiches projet).
|
||||||
|
*/
|
||||||
|
backHref?: string;
|
||||||
|
/**
|
||||||
|
* Libellé du bouton retour. Défaut : `"Portfolio"`.
|
||||||
|
*/
|
||||||
|
backLabel?: string;
|
||||||
|
/**
|
||||||
|
* Kicker affiché au-dessus du titre dans l'en-tête vellum.
|
||||||
|
* Défaut : `"Projet · Portfolio"` (fiches du portfolio).
|
||||||
|
* Exemple pour une réalisation de compétence : `"Réalisation · Compétence IA"`.
|
||||||
|
*/
|
||||||
|
kickerLabel?: string;
|
||||||
|
/**
|
||||||
|
* Message affiché dans l'état 404 (fiche introuvable).
|
||||||
|
* Défaut : `"Ce projet est introuvable."`
|
||||||
|
*/
|
||||||
|
notFoundLabel?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -41,10 +72,21 @@ interface ContentSectionProps {
|
|||||||
* héritées du composant pré-refonte restent acceptées pour compatibilité mais
|
* héritées du composant pré-refonte restent acceptées pour compatibilité mais
|
||||||
* sont ignorées (styles tokenisés désormais) — on les garde dans l'interface
|
* sont ignorées (styles tokenisés désormais) — on les garde dans l'interface
|
||||||
* pour ne pas casser les consommateurs.
|
* pour ne pas casser les consommateurs.
|
||||||
|
*
|
||||||
|
* 2026-04-23 : composant rendu paramétrable pour être réutilisé par la page
|
||||||
|
* détail des réalisations IA (`/competences/[slug]/[realisation]`) avec un
|
||||||
|
* retour vers la compétence parente au lieu du portfolio. Les 4 props
|
||||||
|
* `backHref` / `backLabel` / `kickerLabel` / `notFoundLabel` ont des défauts
|
||||||
|
* strictement identiques au comportement historique → 100 % rétro-compatible
|
||||||
|
* pour les fiches projet existantes.
|
||||||
*/
|
*/
|
||||||
export default function ContentSection({
|
export default function ContentSection({
|
||||||
collection,
|
collection,
|
||||||
slug,
|
slug,
|
||||||
|
backHref = "/portfolio",
|
||||||
|
backLabel = "Portfolio",
|
||||||
|
kickerLabel = "Projet · Portfolio",
|
||||||
|
notFoundLabel = "Ce projet est introuvable.",
|
||||||
}: ContentSectionProps) {
|
}: ContentSectionProps) {
|
||||||
const [data, setData] = useState<ContentData | null>(null);
|
const [data, setData] = useState<ContentData | null>(null);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
@ -89,10 +131,10 @@ export default function ContentSection({
|
|||||||
search_off
|
search_off
|
||||||
</span>
|
</span>
|
||||||
<p className="font-body italic text-on-surface-variant">
|
<p className="font-body italic text-on-surface-variant">
|
||||||
Ce projet est introuvable.
|
{notFoundLabel}
|
||||||
</p>
|
</p>
|
||||||
<Link
|
<Link
|
||||||
href="/portfolio"
|
href={backHref}
|
||||||
className="mt-5 inline-flex items-center gap-1.5 font-headline text-sm font-bold uppercase tracking-[0.2em] text-primary hover:underline"
|
className="mt-5 inline-flex items-center gap-1.5 font-headline text-sm font-bold uppercase tracking-[0.2em] text-primary hover:underline"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
@ -102,14 +144,16 @@ export default function ContentSection({
|
|||||||
>
|
>
|
||||||
arrow_back
|
arrow_back
|
||||||
</span>
|
</span>
|
||||||
Retour au portfolio
|
Retour
|
||||||
</Link>
|
</Link>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { name, Resum: richText, picture, link, linkText } = data;
|
const { name, picture, link, linkText } = data;
|
||||||
|
// Legacy `Resum` (content-type `project`) OU `resum` moderne (nouveaux content-types).
|
||||||
|
const richText = data.Resum ?? data.resum ?? "";
|
||||||
|
|
||||||
const images =
|
const images =
|
||||||
picture?.map((img: ImageData) => ({
|
picture?.map((img: ImageData) => ({
|
||||||
@ -121,7 +165,7 @@ export default function ContentSection({
|
|||||||
<div className="mx-auto flex w-full min-w-0 max-w-3xl flex-col gap-5 px-4 pb-10 sm:px-6">
|
<div className="mx-auto flex w-full min-w-0 max-w-3xl flex-col gap-5 px-4 pb-10 sm:px-6">
|
||||||
{/* Bouton retour discret, posé sur le wallpaper comme une miette de fil d'Ariane. */}
|
{/* Bouton retour discret, posé sur le wallpaper comme une miette de fil d'Ariane. */}
|
||||||
<Link
|
<Link
|
||||||
href="/portfolio"
|
href={backHref}
|
||||||
className="inline-flex w-fit items-center gap-1.5 rounded-full bg-surface-container-lowest/70 px-3 py-1.5 font-headline text-xs font-bold uppercase tracking-[0.2em] text-primary backdrop-blur-vellum transition-colors hover:bg-surface-container-lowest/95 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary"
|
className="inline-flex w-fit items-center gap-1.5 rounded-full bg-surface-container-lowest/70 px-3 py-1.5 font-headline text-xs font-bold uppercase tracking-[0.2em] text-primary backdrop-blur-vellum transition-colors hover:bg-surface-container-lowest/95 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
@ -131,7 +175,7 @@ export default function ContentSection({
|
|||||||
>
|
>
|
||||||
arrow_back
|
arrow_back
|
||||||
</span>
|
</span>
|
||||||
Portfolio
|
{backLabel}
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
{/* En-tête "feuillet de vellum" aligné sur la home et les listes. */}
|
{/* En-tête "feuillet de vellum" aligné sur la home et les listes. */}
|
||||||
@ -141,7 +185,7 @@ export default function ContentSection({
|
|||||||
>
|
>
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
<span className="font-headline text-[11px] font-bold uppercase tracking-[0.3em] text-secondary">
|
<span className="font-headline text-[11px] font-bold uppercase tracking-[0.3em] text-secondary">
|
||||||
Projet · Portfolio
|
{kickerLabel}
|
||||||
</span>
|
</span>
|
||||||
<h1
|
<h1
|
||||||
id="project-title"
|
id="project-title"
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
# Frontend Next.js
|
# Frontend Next.js
|
||||||
|
|
||||||
**Dernière mise à jour :** 2026-04-01
|
**Dernière mise à jour :** 2026-04-24
|
||||||
|
|
||||||
## Stack
|
## Stack
|
||||||
|
|
||||||
@ -14,13 +14,20 @@
|
|||||||
|--------|---------|--------|
|
|--------|---------|--------|
|
||||||
| `/` | `app/page.tsx` | Accueil : `GET /api/homepages?populate=*` |
|
| `/` | `app/page.tsx` | Accueil : `GET /api/homepages?populate=*` |
|
||||||
| `/portfolio` | `app/portfolio/page.jsx` | Liste projets |
|
| `/portfolio` | `app/portfolio/page.jsx` | Liste projets |
|
||||||
| `/portfolio/[slug]` | `app/portfolio/[slug]/page.tsx` | Détail projet (`fetchData('projects', slug)`) |
|
| `/portfolio/[slug]` | `app/portfolio/[slug]/page.tsx` | Détail projet : `ContentSection` + `fetchData('projects', slug)` |
|
||||||
| `/competences` | `app/competences/page.jsx` | Liste compétences |
|
| `/competences` | `app/competences/page.jsx` | Liste compétences — tri par champ Strapi `order` (v4 : `attributes.order`, v5 : `order` à la racine) |
|
||||||
| `/competences/[slug]` | `app/competences/[slug]/page.tsx` | Détail (`fetchData('competences', slug)`) |
|
| `/competences/[slug]` | `app/competences/[slug]/page.tsx` | **Rendu conditionnel** : si au moins une entrée `realisation-ia` est liée à la compétence (filtre API sur le slug), **grille de vignettes** (même rythme visuel que le portfolio) ; sinon fiche richtext historique via `ContentSectionCompetencesContainer` |
|
||||||
|
| `/competences/[slug]/[realisation]` | `app/competences/[slug]/[realisation]/page.tsx` | Fiche d'une **réalisation** (collection Strapi `realisation-ia`) : réutilise `ContentSection` comme les projets (carousel, Markdown `resum` ou `Resum`, CTA `link` externe) |
|
||||||
| `/contact` | `app/contact/page.js` | Formulaire → `/api/contact` (Brevo, voir `contact-flow.md`) |
|
| `/contact` | `app/contact/page.js` | Formulaire → `/api/contact` (Brevo, voir `contact-flow.md`) |
|
||||||
| `/api/contact` | `app/api/contact/route.ts` | Endpoint serveur : envoie un email via Brevo, honeypot + rate-limit |
|
| `/api/contact` | `app/api/contact/route.ts` | Endpoint serveur : envoie un email via Brevo, honeypot + rate-limit |
|
||||||
| `/api/proxy` | `app/api/proxy/route.js` | Proxy GET vers API LLM distante |
|
| `/api/proxy` | `app/api/proxy/route.js` | Proxy GET vers API LLM distante |
|
||||||
|
|
||||||
|
**Strapi — content-types concernés :**
|
||||||
|
|
||||||
|
- `competence` : `name`, `content` (richtext), `picture`, `slug`, `order`
|
||||||
|
- `realisation-ia` : `name`, `description`, `picture`, `slug`, `resum` (richtext, alias accepté côté front : `Resum` pour les `project` uniquement), `link`, `order`, relation `competences` (plusieurs)
|
||||||
|
- Vignette → toujours navigation vers la **fiche détail** interne ; le champ `link` sert de bouton *Voir plus* en bas de fiche (comme sur les fiches `project`).
|
||||||
|
|
||||||
## Layout
|
## Layout
|
||||||
|
|
||||||
- `app/layout.tsx` — **Client Component** (`"use client"`). Header fixe, menu burger mobile, fond décoratif, **Footer**, compteur de visites **localStorage** (`visitCount`).
|
- `app/layout.tsx` — **Client Component** (`"use client"`). Header fixe, menu burger mobile, fond décoratif, **Footer**, compteur de visites **localStorage** (`visitCount`).
|
||||||
@ -43,8 +50,7 @@
|
|||||||
- Carrousels : `Carousel.tsx`, `CarouselCompetences.tsx` (swiper / react-responsive-carousel).
|
- Carrousels : `Carousel.tsx`, `CarouselCompetences.tsx` (swiper / react-responsive-carousel).
|
||||||
- Sections : `ContentSection.tsx`, `ContentSectionCompetences*.tsx`.
|
- Sections : `ContentSection.tsx`, `ContentSectionCompetences*.tsx`.
|
||||||
- `ContactForm.tsx` → `POST /api/contact` → Brevo API (voir `docs-site-interne/contact-flow.md`).
|
- `ContactForm.tsx` → `POST /api/contact` → Brevo API (voir `docs-site-interne/contact-flow.md`).
|
||||||
- `ChatBot.js` → `askAI.js` → `/api/proxy` → FastAPI `/ask` avec `session_id` + `user_id` (UUID anonymes via `app/utils/grasbotIds.js`, voir `docs-site-interne/langfuse-observability.md`).
|
- `GrasBotFab` + `ChatBot.js` → `askAI.js` → `/api/proxy` → FastAPI `/ask` avec `session_id` + `user_id` (UUID anonymes via `app/utils/grasbotIds.js`, voir `docs-site-interne/langfuse-observability.md`).
|
||||||
- `ChatBot.js` → `askAI.js` → `/api/proxy`.
|
|
||||||
- `ModalGlossaire.tsx` — glossaire (données Strapi selon usage dans les pages).
|
- `ModalGlossaire.tsx` — glossaire (données Strapi selon usage dans les pages).
|
||||||
|
|
||||||
## Fichiers clés (liste courte)
|
## Fichiers clés (liste courte)
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
# API LLM et chatbot (GrasBot)
|
# API LLM et chatbot (GrasBot)
|
||||||
|
|
||||||
**Dernière mise à jour :** 2026-04-22 (v3 — bascule graph + BM25)
|
**Dernière mise à jour :** 2026-04-24 (v3 + alignement parcours site)
|
||||||
|
|
||||||
## Vue d'ensemble
|
## Vue d'ensemble
|
||||||
|
|
||||||
@ -31,13 +31,25 @@ par la refonte (`sources`, `grounded`, `model`, `vault_size`) passent dans
|
|||||||
la réponse JSON et pourront être affichés dans une itération suivante
|
la réponse JSON et pourront être affichés dans une itération suivante
|
||||||
(voir [pistes d'évolution](#pistes-dévolution)).
|
(voir [pistes d'évolution](#pistes-dévolution)).
|
||||||
|
|
||||||
|
## Parcours public (hors moteur Python) — cohérence contenu
|
||||||
|
|
||||||
|
Le visiteur découvre les **projets** sur `/portfolio/[slug]` et, pour la
|
||||||
|
compétence **IA** (et toute compétence à laquelle on lie des
|
||||||
|
**`realisation-ia`** dans Strapi), un **parallèle** sur `/competences/[slug]`
|
||||||
|
(vignettes) puis `/competences/[slug]/[realisation]` (fiche identique en
|
||||||
|
gabarit à une fiche projet). Rien n'est servi ici par FastAPI : c'est
|
||||||
|
du **Strapi + Next** uniquement. Le chatbot, lui, interroge toujours
|
||||||
|
**`vault-grasbot/`** via `llm-api/search.py` — mettre à jour le vault
|
||||||
|
(ou l'extraction Strapi → vault) quand on veut que GrasBot **reflète** des
|
||||||
|
faits nouveaux présentés sur le site. Détail des routes : [`02-frontend-next.md`](./02-frontend-next.md).
|
||||||
|
|
||||||
## FastAPI — `llm-api/`
|
## FastAPI — `llm-api/`
|
||||||
|
|
||||||
| Fichier | Rôle |
|
| Fichier | Rôle |
|
||||||
|---------|------|
|
|---------|------|
|
||||||
| `api.py` | Endpoints `GET /ask?q=...`, `GET /health`, `POST /reload-vault`. |
|
| `api.py` | Endpoints `GET /ask?q=...`, `GET /health`, `POST /reload-vault`. |
|
||||||
| `search.py` | `load_vault`, `tokenize_fr`, `score_note`, `expand_by_graph`, `search`, `build_prompt`, `generate`, `answer`. |
|
| `search.py` | `load_vault`, `tokenize_fr`, `score_note`, `expand_by_graph`, `search`, `build_prompt`, `generate`, `answer`. |
|
||||||
| `requirements.txt` | `fastapi`, `uvicorn`, `requests`, `pyyaml`. **Plus besoin** de `chromadb` / `chroma-hnswlib` (supprimés v3). |
|
| `requirements.txt` | `fastapi`, `uvicorn`, `requests`, `pyyaml` ; + `langfuse` (SDK 3.x, plafond strict inférieur à la v4) + `python-dotenv` pour l'observabilité optionnelle. Voir `llm-api/requirements.txt`. **Plus besoin** de `chromadb` / `chroma-hnswlib` (supprimés v3). |
|
||||||
|
|
||||||
Modules supprimés en v3 :
|
Modules supprimés en v3 :
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
# Documentation interne du site
|
# Documentation interne du site
|
||||||
|
|
||||||
**Dernière mise à jour :** 2026-04-01 — captures WebP complètes dans `captures/` (voir INDEX).
|
**Dernière mise à jour :** 2026-04-24
|
||||||
|
|
||||||
Ce dossier décrit l'architecture, le fonctionnement et les décisions du projet (Next.js + Strapi + FastAPI/Ollama). Il est destiné à l'équipe et à l'assistant IA pour retrouver vite le contexte.
|
Ce dossier décrit l'architecture, le fonctionnement et les décisions du projet (Next.js + Strapi + FastAPI/Ollama). Il est destiné à l'équipe et à l'assistant IA pour retrouver vite le contexte.
|
||||||
|
|
||||||
@ -8,7 +8,9 @@ Ce dossier décrit l'architecture, le fonctionnement et les décisions du projet
|
|||||||
|
|
||||||
| Fichier / zone | Rôle |
|
| Fichier / zone | Rôle |
|
||||||
|----------------|------|
|
|----------------|------|
|
||||||
|
| `README.md` (racine du dépôt) | Panorama GitHub, liens vers cette doc, démarrage rapide, `reload-vault`. |
|
||||||
| `CONFIGURATION_SITE.md` (racine) | Guide opérationnel : ports, commandes, dépannage, planificateur de tâches Windows. |
|
| `CONFIGURATION_SITE.md` (racine) | Guide opérationnel : ports, commandes, dépannage, planificateur de tâches Windows. |
|
||||||
|
| `obsidian-site-docs/` (racine) | Pack Obsidian : hub, commandes, **copie** de ce dossier pour export (voir `obsidian-site-docs/SYNC-DOC.md`). |
|
||||||
| Ce dossier | Conception : flux de données, schémas CMS, incohérences connues, feuille de route. |
|
| Ce dossier | Conception : flux de données, schémas CMS, incohérences connues, feuille de route. |
|
||||||
|
|
||||||
**Règle de maintenance :** après une modification notable, mettre à jour le fichier concerné ici ; si le démarrage ou les ports changent, compléter aussi `CONFIGURATION_SITE.md`.
|
**Règle de maintenance :** après une modification notable, mettre à jour le fichier concerné ici ; si le démarrage ou les ports changent, compléter aussi `CONFIGURATION_SITE.md`.
|
||||||
@ -29,6 +31,8 @@ Ce dossier décrit l'architecture, le fonctionnement et les décisions du projet
|
|||||||
| [etat-actuel.md](./etat-actuel.md) | État et dette technique. |
|
| [etat-actuel.md](./etat-actuel.md) | État et dette technique. |
|
||||||
| [feuille-de-route.md](./feuille-de-route.md) | Backlog priorisé. |
|
| [feuille-de-route.md](./feuille-de-route.md) | Backlog priorisé. |
|
||||||
| [REFONTE-VISUELLE.md](./REFONTE-VISUELLE.md) | Journal de bord de la refonte UI Stitch. |
|
| [REFONTE-VISUELLE.md](./REFONTE-VISUELLE.md) | Journal de bord de la refonte UI Stitch. |
|
||||||
|
| [contact-flow.md](./contact-flow.md) | Contact : e-mail Brevo (remplacement du stockage Strapi). |
|
||||||
|
| [langfuse-observability.md](./langfuse-observability.md) | Langfuse : traces GrasBot, tuning pipeline. |
|
||||||
|
|
||||||
## Arborescence utile
|
## Arborescence utile
|
||||||
|
|
||||||
@ -39,8 +43,10 @@ my-next-site/
|
|||||||
├── llm-api/
|
├── llm-api/
|
||||||
├── strapi_extraction/
|
├── strapi_extraction/
|
||||||
├── start-my-site.ps1
|
├── start-my-site.ps1
|
||||||
|
├── stop-my-site.ps1
|
||||||
├── next.config.ts
|
├── next.config.ts
|
||||||
├── CONFIGURATION_SITE.md
|
├── CONFIGURATION_SITE.md
|
||||||
|
├── obsidian-site-docs/ # pack Obsidian (optionnel)
|
||||||
└── docs-site-interne/
|
└── docs-site-interne/
|
||||||
├── captures/ # screenshots de référence (voir INDEX.md)
|
├── captures/ # screenshots de référence (voir INDEX.md)
|
||||||
└── ...
|
└── ...
|
||||||
|
|||||||
@ -1,18 +1,21 @@
|
|||||||
# État actuel du site
|
# État actuel du site
|
||||||
|
|
||||||
**Dernière mise à jour :** 2026-04-22 (post-refonte GrasBot v3)
|
**Dernière mise à jour :** 2026-04-24 (doc compétences / realisation-ia + CONFIGURATION)
|
||||||
|
|
||||||
## Ce qui est en place
|
## Ce qui est en place
|
||||||
|
|
||||||
- **Next.js 15** avec App Router, Tailwind, pages accueil / portfolio / compétences / contact, layout responsive avec menu burger. Design system "Digital Atelier" (Manrope + Newsreader, palette primary indigo-ardoise, vellum cards).
|
- **Next.js 15** avec App Router, Tailwind, pages accueil / portfolio / compétences / contact, layout responsive avec menu burger. Design system "Digital Atelier" (Manrope + Newsreader, palette primary indigo-ardoise, vellum cards).
|
||||||
- **Strapi** avec content-types : homepage, projects, competences, messages, glossaire ; médias et texte riche.
|
- **Strapi** avec content-types : homepage, projects, competences, **realisation-ia** (rattachées aux compétences), glossaire ; médias et texte riche (l’ancien type `message` a été retiré au profit de Brevo).
|
||||||
- **Formulaire contact** : POST vers Strapi `messages`.
|
- **Compétences côté Next** : liste `/competences` (tri `order`) ; fiche `/competences/[slug]` (vignettes des `realisation-ia` liées quand il y en a, sinon fiche richtext) ; détail `/competences/[slug]/[realisation]`. Même logique d’enrichissement que le portfolio (Markdown, galerie, CTA) pour les fiches liées.
|
||||||
|
- **Formulaire contact** : e-mail via **Brevo** (route Next `POST /api/contact`). Voir [contact-flow.md](./contact-flow.md).
|
||||||
- **Chatbot GrasBot v3** : FAB global (`GrasBotFab.tsx`) → proxy Next → API LLM hébergée (`llmapi.fernandgrascalvet.com`).
|
- **Chatbot GrasBot v3** : FAB global (`GrasBotFab.tsx`) → proxy Next → API LLM hébergée (`llmapi.fernandgrascalvet.com`).
|
||||||
- **FastAPI + Ollama** dans `llm-api/` : modèle `qwen3:8b`, pipeline `search.py` (graph + BM25 sur vault Obsidian `vault-grasbot/`, sans embeddings).
|
- **FastAPI + Ollama** dans `llm-api/` : modèle `qwen3:8b`, pipeline `search.py` (graph + BM25 sur vault Obsidian `vault-grasbot/`, sans embeddings).
|
||||||
- **Vault de connaissance `vault-grasbot/`** : 41 notes enrichies (aliases, answers, priority) — source de vérité du chatbot, régénéré depuis Strapi par `strapi_extraction/build-vault.py`.
|
- **Vault de connaissance `vault-grasbot/`** : ~46 notes Markdown, dont 2 fiches projet manuelles (GrasBot, site portfolio) et compétences IA/Web mises à jour (2026-04) — recharger l’API après déploiement si besoin : `POST /reload-vault` (aliases, answers, priority) — source de vérité du chatbot, régénéré depuis Strapi par `strapi_extraction/build-vault.py`. Inclut une note `bio-fernand` courte (priority 10) dédiée aux questions biographiques et un CV complet complémentaire.
|
||||||
|
- **Observabilité Langfuse** : instance self-hosted `langfuse.fernandgrascalvet.com`, instrumentation Python (`llm-api/observability.py`) traçant chaque requête `/ask` (retrieval / prompt_build / ollama-chat) avec session/user IDs anonymes côté front. Mode no-op automatique si les clés sont absentes. Voir [`langfuse-observability.md`](./langfuse-observability.md).
|
||||||
- **Scripts** d'extraction et de doc dans `strapi_extraction/`.
|
- **Scripts** d'extraction et de doc dans `strapi_extraction/`.
|
||||||
- Documentation opérationnelle : `CONFIGURATION_SITE.md`.
|
- Documentation opérationnelle : [`CONFIGURATION_SITE.md`](../CONFIGURATION_SITE.md) à la racine du dépôt (incl. ordre des compétences et routes dédiées, renvoi vers [02-frontend-next.md](./02-frontend-next.md)).
|
||||||
- **Captures d'écran** de référence (WebP) : `docs-site-interne/captures/` — voir `captures/INDEX.md`.
|
- **Captures d'écran** de référence (WebP) : [captures/](./captures/) — voir [INDEX.md](./captures/INDEX.md).
|
||||||
|
- **Décision produit** : une **rubrique homelab / serveur** (souvent évoquée en « phase 3 ») n’est **pas retenue** — pas d’évolution planifiée sur ce thème ; le parcours public reste portfolio, compétences (dont IA + réalisations) et contact.
|
||||||
|
|
||||||
## Dette technique / incohérences connues
|
## Dette technique / incohérences connues
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
# Feuille de route
|
# Feuille de route
|
||||||
|
|
||||||
**Dernière mise à jour :** 2026-04-22
|
**Dernière mise à jour :** 2026-04-24
|
||||||
|
|
||||||
Document vivant : ajuster les statuts et dates au fil du travail.
|
Document vivant : ajuster les statuts et dates au fil du travail.
|
||||||
|
|
||||||
@ -33,7 +33,7 @@ Document vivant : ajuster les statuts et dates au fil du travail.
|
|||||||
|
|
||||||
| Date | Jalon |
|
| Date | Jalon |
|
||||||
|------|--------|
|
|------|--------|
|
||||||
| 2026-04-01 | Création du dossier `docs-site-interne/` et première rédaction basée sur le code. |
|
| 2026-04-01 | Création du dossier `docs-site-interne` (dépôt Git) et première rédaction basée sur le code. |
|
||||||
| 2026-04-01 | Reprise après coupure : vérification complétude ; enrichissement de `04-api-llm-et-chatbot.md` ; suppression de `test.txt`. |
|
| 2026-04-01 | Reprise après coupure : vérification complétude ; enrichissement de `04-api-llm-et-chatbot.md` ; suppression de `test.txt`. |
|
||||||
| 2026-04-01 | Index captures (`captures/INDEX.md`), `captures/README.md`, `07-reference-visuelle-captures.md`, skill `.cursor/skills/site-portfolio-evolution/SKILL.md`. |
|
| 2026-04-01 | Index captures (`captures/INDEX.md`), `captures/README.md`, `07-reference-visuelle-captures.md`, skill `.cursor/skills/site-portfolio-evolution/SKILL.md`. |
|
||||||
| 2026-04-01 | Captures WebP intégrées au dépôt ; INDEX et README `captures/` alignés sur les noms réels (slug portfolio / compétence documentés). |
|
| 2026-04-01 | Captures WebP intégrées au dépôt ; INDEX et README `captures/` alignés sur les noms réels (slug portfolio / compétence documentés). |
|
||||||
@ -56,4 +56,6 @@ Document vivant : ajuster les statuts et dates au fil du travail.
|
|||||||
| 2026-04-22 | **Chatbot GrasBot — migration Mistral → Qwen3 + RAG sur vault Obsidian local**. Passage du modèle `mistral` à `qwen3:8b` dans `llm-api/api.py` (Q4_K_M, ~5 Go VRAM RTX 2080 Ti). Embeddings via `nomic-embed-text` (~500 Mo VRAM, multilingue FR). Nouveau pipeline RAG : `llm-api/rag.py` (embed / retrieve / build_prompt / generate / answer), `llm-api/index_vault.py` (parse frontmatter YAML, chunking par h2 au-delà de 3000 chars, upsert ChromaDB batch 32), `llm-api/requirements.txt` (fastapi, uvicorn, requests, chromadb, pyyaml). Nouveau script `strapi_extraction/build-vault.py` qui convertit `strapi_extraction/docs/*.md` + CV PDF (via `pypdf`) en vault Obsidian structuré `vault-grasbot/` : frontmatter YAML (type, source, domains, tags, linked, related, visibility), wikilinks vers les MOCs, MOCs auto-générés par type et par domaine (15 MOCs). Bootstrap v1 du vault : 17 projets, 4 compétences, 1 CV, 15 MOCs auto + 1 manuel (Technique), 3 notes auto-doc dans `50-Technique/` (architecture-site, grasbot-rag, vault-structure) pour que GrasBot puisse se présenter lui-même. Compatibilité ascendante `askAI.js`/`ChatBot.js` via le champ `response` conservé ; les `sources`, `rag`, `model` ajoutés sont non destructifs. Endpoint `/health` ajouté pour debug. Doc : nouveau [`08-vault-obsidian-rag.md`](./docs-site-interne/08-vault-obsidian-rag.md), mise à jour de [`04-api-llm-et-chatbot.md`](./docs-site-interne/04-api-llm-et-chatbot.md) et [`06-strapi-extraction.md`](./docs-site-interne/06-strapi-extraction.md). Fragilités préexistantes repérées (cleaner `homepages` absent, content-type `glossaire` non extrait) consignées mais non corrigées dans ce lot — à traiter lors du prochain enrichissement vault. |
|
| 2026-04-22 | **Chatbot GrasBot — migration Mistral → Qwen3 + RAG sur vault Obsidian local**. Passage du modèle `mistral` à `qwen3:8b` dans `llm-api/api.py` (Q4_K_M, ~5 Go VRAM RTX 2080 Ti). Embeddings via `nomic-embed-text` (~500 Mo VRAM, multilingue FR). Nouveau pipeline RAG : `llm-api/rag.py` (embed / retrieve / build_prompt / generate / answer), `llm-api/index_vault.py` (parse frontmatter YAML, chunking par h2 au-delà de 3000 chars, upsert ChromaDB batch 32), `llm-api/requirements.txt` (fastapi, uvicorn, requests, chromadb, pyyaml). Nouveau script `strapi_extraction/build-vault.py` qui convertit `strapi_extraction/docs/*.md` + CV PDF (via `pypdf`) en vault Obsidian structuré `vault-grasbot/` : frontmatter YAML (type, source, domains, tags, linked, related, visibility), wikilinks vers les MOCs, MOCs auto-générés par type et par domaine (15 MOCs). Bootstrap v1 du vault : 17 projets, 4 compétences, 1 CV, 15 MOCs auto + 1 manuel (Technique), 3 notes auto-doc dans `50-Technique/` (architecture-site, grasbot-rag, vault-structure) pour que GrasBot puisse se présenter lui-même. Compatibilité ascendante `askAI.js`/`ChatBot.js` via le champ `response` conservé ; les `sources`, `rag`, `model` ajoutés sont non destructifs. Endpoint `/health` ajouté pour debug. Doc : nouveau [`08-vault-obsidian-rag.md`](./docs-site-interne/08-vault-obsidian-rag.md), mise à jour de [`04-api-llm-et-chatbot.md`](./docs-site-interne/04-api-llm-et-chatbot.md) et [`06-strapi-extraction.md`](./docs-site-interne/06-strapi-extraction.md). Fragilités préexistantes repérées (cleaner `homepages` absent, content-type `glossaire` non extrait) consignées mais non corrigées dans ce lot — à traiter lors du prochain enrichissement vault. |
|
||||||
| 2026-04-22 | Refonte visuelle — **étape 7 : fiches détail + glossaire + GrasBot flottant**. Cinq sous-lots. **7.a** : `Carousel.tsx` + `CarouselCompetences.tsx` harmonisés (pagination bullets primary via variables CSS Swiper, flèches primary, `rounded-tile shadow-ambient-sm`, autoplay 3500 ms + `loop` conditionnel, lightbox Stitch refaite avec voile `bg-on-surface/80`, image `object-contain rounded-sheet`, bouton close rond Material Symbol + Escape + verrouillage scroll body). **7.b** : `ContentSection.tsx` (fiche portfolio) — gabarit vellum cohérent avec la home/listes, pastille retour `arrow_back`, kicker `Projet · Portfolio`, titre Manrope, carousel détail plein cadre, corps Markdown en `prose` Stitch (mêmes overrides que la home, y compris pastille `prose-hr`), CTA externe jewel avec `open_in_new`, états loading/404 en vellum. **7.c** : `ContentSectionCompetences.tsx` + container — même gabarit. Refactor glossaire : styles inline `style="color:red/blue"` remplacés par les classes `.glossary-keyword` / `.chatbot-keyword` (ajoutées à `globals.css`, couleur primary + underline dotted offset 3 px). Event listeners `document.body.addEventListener` remplacés par un listener unique scopé au wrapper `contentRef`. Le clic sur « IA locale » ne monte plus un `<ChatBot>` local mais dispatch `window.dispatchEvent(new CustomEvent("grasbot:open"))` capté par le FAB global (7.e). Container refait avec skeleton vellum à la place de `⏳ Chargement...`. **7.d** : `ChatBot.js` entièrement restylé (carte vellum `rounded-sheet shadow-ambient backdrop-blur-vellum`, header primary avec Material Symbol `smart_toy` + sous-titre « Assistant IA locale », bulles user `bg-primary text-white` et bot `bg-surface-container`, input Stitch avec `focus-visible:ring-primary`, bouton envoyer rond jewel Material Symbol `send`, auto-scroll, focus auto, envoi Enter, disabled pendant attente, message d'accueil vide éditorial). **7.e** : nouveau composant `app/components/GrasBotFab.tsx` — FAB jewel `fixed bottom-6 right-6 z-30` rond 56/64 px, `bg-primary shadow-jewel` Material Symbol `smart_toy`/`close`, monté dans `app/layout.tsx` → chatbot accessible depuis **toutes les pages** (plus seulement fiches compétences). Écoute `CustomEvent("grasbot:open")` dispatché depuis le keyword « IA locale ». Fermeture Escape globale, panneau responsive plein largeur mobile / 384 px desktop. Détails dans `REFONTE-VISUELLE.md` §7. |
|
| 2026-04-22 | Refonte visuelle — **étape 7 : fiches détail + glossaire + GrasBot flottant**. Cinq sous-lots. **7.a** : `Carousel.tsx` + `CarouselCompetences.tsx` harmonisés (pagination bullets primary via variables CSS Swiper, flèches primary, `rounded-tile shadow-ambient-sm`, autoplay 3500 ms + `loop` conditionnel, lightbox Stitch refaite avec voile `bg-on-surface/80`, image `object-contain rounded-sheet`, bouton close rond Material Symbol + Escape + verrouillage scroll body). **7.b** : `ContentSection.tsx` (fiche portfolio) — gabarit vellum cohérent avec la home/listes, pastille retour `arrow_back`, kicker `Projet · Portfolio`, titre Manrope, carousel détail plein cadre, corps Markdown en `prose` Stitch (mêmes overrides que la home, y compris pastille `prose-hr`), CTA externe jewel avec `open_in_new`, états loading/404 en vellum. **7.c** : `ContentSectionCompetences.tsx` + container — même gabarit. Refactor glossaire : styles inline `style="color:red/blue"` remplacés par les classes `.glossary-keyword` / `.chatbot-keyword` (ajoutées à `globals.css`, couleur primary + underline dotted offset 3 px). Event listeners `document.body.addEventListener` remplacés par un listener unique scopé au wrapper `contentRef`. Le clic sur « IA locale » ne monte plus un `<ChatBot>` local mais dispatch `window.dispatchEvent(new CustomEvent("grasbot:open"))` capté par le FAB global (7.e). Container refait avec skeleton vellum à la place de `⏳ Chargement...`. **7.d** : `ChatBot.js` entièrement restylé (carte vellum `rounded-sheet shadow-ambient backdrop-blur-vellum`, header primary avec Material Symbol `smart_toy` + sous-titre « Assistant IA locale », bulles user `bg-primary text-white` et bot `bg-surface-container`, input Stitch avec `focus-visible:ring-primary`, bouton envoyer rond jewel Material Symbol `send`, auto-scroll, focus auto, envoi Enter, disabled pendant attente, message d'accueil vide éditorial). **7.e** : nouveau composant `app/components/GrasBotFab.tsx` — FAB jewel `fixed bottom-6 right-6 z-30` rond 56/64 px, `bg-primary shadow-jewel` Material Symbol `smart_toy`/`close`, monté dans `app/layout.tsx` → chatbot accessible depuis **toutes les pages** (plus seulement fiches compétences). Écoute `CustomEvent("grasbot:open")` dispatché depuis le keyword « IA locale ». Fermeture Escape globale, panneau responsive plein largeur mobile / 384 px desktop. Détails dans `REFONTE-VISUELLE.md` §7. |
|
||||||
| 2026-04-22 | Refonte visuelle — **étape 6 : listes portfolio + compétences**. `app/portfolio/page.jsx` et `app/competences/page.jsx` entièrement réécrits. En-tête éditorial (kicker + titre Manrope extrabold + pitch Newsreader) cohérent avec le hero de la home. Grille **asymétrique 2/3 + 1/3** alternée (`md:grid-cols-6` + pattern de `col-span-4`/`col-span-2` sur modulo 4, `sm:grid-cols-2`, `grid-cols-1` mobile) — conforme DESIGN.md §6 "No-Grid-Lock". Cartes « feuillet vellum » alignées home : `rounded-sheet bg-surface-container-lowest/85 backdrop-blur-vellum shadow-ambient`, image `aspect-[4/3]` fixe avec `group-hover:scale-[1.03]`, titre `text-primary`, description `line-clamp-3` en Newsreader, CTA tertiaire « Découvrir → » / « Explorer → » avec Material Symbol `arrow_forward` qui se décale au hover (`translate="no"` appliqué). Hover : `hover:-translate-y-0.5 hover:shadow-jewel` (remplace le `scale-105` qui débordait). **`Swiper` retiré des vignettes de liste** (arbitrage acté § 2 : carousel réservé aux galeries intra-fiche) — une seule image par carte, `loading="lazy"`. États ajoutés : skeletons animés respectant la grille + état vide avec Material Symbol. Régressions corrigées au passage : largeur fixe `w-80` qui débordait sur S25 Ultra, `hover:scale-105` qui tapait sous le header, classes `bg-white/80 rounded-lg` remplacées par les tokens Stitch. Les composants `Carousel.tsx` et `CarouselCompetences.tsx` restent en place pour les fiches détail (étape 7). Détail dans `REFONTE-VISUELLE.md` §6. |
|
| 2026-04-22 | Refonte visuelle — **étape 6 : listes portfolio + compétences**. `app/portfolio/page.jsx` et `app/competences/page.jsx` entièrement réécrits. En-tête éditorial (kicker + titre Manrope extrabold + pitch Newsreader) cohérent avec le hero de la home. Grille **asymétrique 2/3 + 1/3** alternée (`md:grid-cols-6` + pattern de `col-span-4`/`col-span-2` sur modulo 4, `sm:grid-cols-2`, `grid-cols-1` mobile) — conforme DESIGN.md §6 "No-Grid-Lock". Cartes « feuillet vellum » alignées home : `rounded-sheet bg-surface-container-lowest/85 backdrop-blur-vellum shadow-ambient`, image `aspect-[4/3]` fixe avec `group-hover:scale-[1.03]`, titre `text-primary`, description `line-clamp-3` en Newsreader, CTA tertiaire « Découvrir → » / « Explorer → » avec Material Symbol `arrow_forward` qui se décale au hover (`translate="no"` appliqué). Hover : `hover:-translate-y-0.5 hover:shadow-jewel` (remplace le `scale-105` qui débordait). **`Swiper` retiré des vignettes de liste** (arbitrage acté § 2 : carousel réservé aux galeries intra-fiche) — une seule image par carte, `loading="lazy"`. États ajoutés : skeletons animés respectant la grille + état vide avec Material Symbol. Régressions corrigées au passage : largeur fixe `w-80` qui débordait sur S25 Ultra, `hover:scale-105` qui tapait sous le header, classes `bg-white/80 rounded-lg` remplacées par les tokens Stitch. Les composants `Carousel.tsx` et `CarouselCompetences.tsx` restent en place pour les fiches détail (étape 7). Détail dans `REFONTE-VISUELLE.md` §6. |
|
||||||
|
| 2026-04-23 | **GrasBot — tuning pipeline LLM + anti-hallucinations**. Audit des premières traces Langfuse : questions biographiques hallucinées (âge erroné, statut inventé), réponses longues tronquées. Quatre ajustements : (1) `llm-api/search.py` · `generate()` — `num_ctx=8192` explicite (stoppe la troncature silencieuse du prompt par le défaut Ollama 2048/4096 quand plusieurs notes entières sont injectées), `num_predict` 512 → 1024 (réponses longues complètes), `think: false` top-level (désactive le *thinking mode* de qwen3 qui consommait du budget de sortie). (2) `llm-api/search.py` · `build_prompt()` — troncature conditionnelle des sources rank 2+ via `_truncate_body()` + nouvelles variables `SEARCH_SECONDARY_MAX_CHARS` (1500) / `SEARCH_SECONDARY_KEEP_RATIO` (0.8). Aucune source n'est supprimée, seules celles dont le score est < 0.8 × score(#1) ET dont le body dépasse 1500 chars sont résumées. Loggé dans `prompt_build.metadata.truncation`. (3) Vault — nouvelle note `vault-grasbot/30-Parcours/bio-fernand.md` courte et factuelle (priority 10, aliases biographiques courts), canonique pour les questions du type *« qui est Fernand »*. Renvoie vers le CV complet pour le détail. Correction incohérence d'âge dans le CV (46 → 47 ans dans la section Présentation) qui alimentait les hallucinations. (4) `SYSTEM_PROMPT` — nouveau bloc *Règles de fidélité aux sources* : priorité `type=parcours` pour questions bio, interdiction d'inventer des faits factuels, gestion explicite des contradictions, signalement des notes tronquées. **Bascule Langfuse v4 → v3 dans `requirements.txt`** (`langfuse>=3.0,<4`) : le SDK v4 a supprimé `start_as_current_span`, la v3 reste compatible avec l'instrumentation existante. Dépendances Python ajoutées : `langfuse`, `python-dotenv`. Secrets Langfuse déplacés de `.env.local` Next vers `llm-api/.env` (non committé). Doc mise à jour : [`langfuse-observability.md`](./langfuse-observability.md) (nouvelle section *Tuning du pipeline — 2026-04-23*), `CONFIGURATION_SITE.md` (endpoints `/health` + `/reload-vault`), `etat-actuel.md` (42 notes + mention Langfuse). |
|
||||||
| 2026-04-22 | **GrasBot v3 — bascule RAG vectoriel → retrieval graph + BM25**. Essais d'installation Windows bloqués par `chroma-hnswlib` (compilation C++ requise) et freezes RDP à chaque chargement de `qwen3:8b` + `nomic-embed-text` simultanément. Arbitrage : pour un vault de 40 notes, la RAG vectorielle sur-dimensionne ; on exploite directement la structure Obsidian (frontmatter, wikilinks, MOCs). **Nouveau pipeline** dans `llm-api/search.py` (scoring multi-signaux : aliases / titre-slug / answers / domains / tags / BM25 ; expansion par graphe via `linked`/`related`/wikilinks ; tokenizer FR avec normalisations `c++` → `cpp`, split `-`/`_`). **Déterministe, traçable (champ `reasons` dans les sources), 50 ms de retrieval**. Scoring calibré sur 12 cas (IA, push-swap, LLMs pluriel, hors-sujet clafoutis → `(aucun)`, etc.). **Dépendances allégées** : fini `chromadb`, `chroma-hnswlib`, `nomic-embed-text`. `requirements.txt` = fastapi + uvicorn + requests + pyyaml uniquement. Fichiers supprimés : `llm-api/rag.py`, `llm-api/index_vault.py`, `chroma-index/` (marqué pour suppression, verrouillé par Cursor au moment du cleanup — sera supprimé au reboot). **Vault enrichi** : `build-vault.py` étendu pour générer automatiquement `aliases` (à partir du slug/titre + `DOMAIN_ALIASES`), `answers` (questions-types adaptées au type de note), `priority` (heuristique CV=10, MOCs=7, compétences=7, projets=5). Note CV curatée (`source: manual`) enrichie manuellement avec 12 aliases et 7 answers. Nouvelle `vault-grasbot/TAXONOMIE.md` qui documente le vocabulaire contrôlé. Réécriture de `vault-grasbot/50-Technique/grasbot-rag.md` → `grasbot-retrieval.md` (nouveau pipeline), + `architecture-site.md` + `vault-structure.md` + `MOC-Technique.md`. Nouveau endpoint `POST /reload-vault` pour recharger sans redémarrer uvicorn. Documentation interne refaite : [`04-api-llm-et-chatbot.md`](./04-api-llm-et-chatbot.md), [`06-strapi-extraction.md`](./06-strapi-extraction.md), nouveau [`08-vault-obsidian-retrieval.md`](./08-vault-obsidian-retrieval.md) (remplace `08-vault-obsidian-rag.md`). |
|
| 2026-04-22 | **GrasBot v3 — bascule RAG vectoriel → retrieval graph + BM25**. Essais d'installation Windows bloqués par `chroma-hnswlib` (compilation C++ requise) et freezes RDP à chaque chargement de `qwen3:8b` + `nomic-embed-text` simultanément. Arbitrage : pour un vault de 40 notes, la RAG vectorielle sur-dimensionne ; on exploite directement la structure Obsidian (frontmatter, wikilinks, MOCs). **Nouveau pipeline** dans `llm-api/search.py` (scoring multi-signaux : aliases / titre-slug / answers / domains / tags / BM25 ; expansion par graphe via `linked`/`related`/wikilinks ; tokenizer FR avec normalisations `c++` → `cpp`, split `-`/`_`). **Déterministe, traçable (champ `reasons` dans les sources), 50 ms de retrieval**. Scoring calibré sur 12 cas (IA, push-swap, LLMs pluriel, hors-sujet clafoutis → `(aucun)`, etc.). **Dépendances allégées** : fini `chromadb`, `chroma-hnswlib`, `nomic-embed-text`. `requirements.txt` = fastapi + uvicorn + requests + pyyaml uniquement. Fichiers supprimés : `llm-api/rag.py`, `llm-api/index_vault.py`, `chroma-index/` (marqué pour suppression, verrouillé par Cursor au moment du cleanup — sera supprimé au reboot). **Vault enrichi** : `build-vault.py` étendu pour générer automatiquement `aliases` (à partir du slug/titre + `DOMAIN_ALIASES`), `answers` (questions-types adaptées au type de note), `priority` (heuristique CV=10, MOCs=7, compétences=7, projets=5). Note CV curatée (`source: manual`) enrichie manuellement avec 12 aliases et 7 answers. Nouvelle `vault-grasbot/TAXONOMIE.md` qui documente le vocabulaire contrôlé. Réécriture de `vault-grasbot/50-Technique/grasbot-rag.md` → `grasbot-retrieval.md` (nouveau pipeline), + `architecture-site.md` + `vault-structure.md` + `MOC-Technique.md`. Nouveau endpoint `POST /reload-vault` pour recharger sans redémarrer uvicorn. Documentation interne refaite : [`04-api-llm-et-chatbot.md`](./04-api-llm-et-chatbot.md), [`06-strapi-extraction.md`](./06-strapi-extraction.md), nouveau [`08-vault-obsidian-retrieval.md`](./08-vault-obsidian-retrieval.md) (remplace `08-vault-obsidian-rag.md`). |
|
||||||
|
| 2026-04-24 | **Doc + configuration** : routes `/competences/[slug]/[realisation]`, entité Strapi `realisation-ia`, tri `order` et comportement vignettes/richtext documentés dans `02-frontend-next.md`, `04-api-llm-et-chatbot.md` (parcours public), `etat-actuel.md` ; `CONFIGURATION_SITE.md` : section *Contenu : compétences, réalisations IA et ordre d’affichage*. Décision : **pas de « phase 3 » homelab** sur le site (consignée dans l’état actuel). |
|
||||||
|
|||||||
21
obsidian-site-docs/00 Hub.md
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# Hub — Site portfolio (dépôt my-next-site)
|
||||||
|
|
||||||
|
> Point d’entrée du pack. La doc technique **complète** est **intégrée** (dossier `docs-site-interne/`) : voir [[03 Documentation intégrée (dépôt)]] et [README doc](docs-site-interne/README.md).
|
||||||
|
|
||||||
|
**Site** : [fernandgrascalvet.com](https://fernandgrascalvet.com)
|
||||||
|
|
||||||
|
## Fiches de ce pack
|
||||||
|
|
||||||
|
- [[01 Commandes - Démarrage, arrêt, reload vault]]
|
||||||
|
- [[02 Ports et URLs]]
|
||||||
|
- [[03 Documentation intégrée (dépôt)]]
|
||||||
|
- [[04 GrasBot et API LLM (résumé)]]
|
||||||
|
|
||||||
|
## Rappel express
|
||||||
|
|
||||||
|
| Action | Où ? |
|
||||||
|
|--------|------|
|
||||||
|
| Démarrer les 3 services (Windows) | `start-my-site.ps1` à la racine du dépôt |
|
||||||
|
| Arrêter | `stop-my-site.ps1` |
|
||||||
|
| Recharger le vault sans redémarrer l’API | `POST /reload-vault` sur l’instance FastAPI locale |
|
||||||
|
| Doc opérationnelle (copie dans le coffre) | [CONFIGURATION_SITE.md](CONFIGURATION_SITE.md) |
|
||||||
@ -0,0 +1,83 @@
|
|||||||
|
# Commandes — Démarrage, arrêt, reload vault
|
||||||
|
|
||||||
|
> Chemins d’exemple : adapte le lecteur (ex. `J:`) et le nom du dossier du clone. La racine du dépôt = celui qui contient `package.json` et `start-my-site.ps1`.
|
||||||
|
|
||||||
|
## Démarrage automatique (recommandé)
|
||||||
|
|
||||||
|
Dans un PowerShell, **depuis la racine du dépôt** :
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Set-Location "J:\my-next-site" # à adapter
|
||||||
|
.\start-my-site.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
Lance en général **trois** fenêtres : Strapi (`cmsbackend`), Next (racine), FastAPI (`llm-api`).
|
||||||
|
|
||||||
|
**Arrêt ciblé** (ports 3000, 1337, 8000) :
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Set-Location "J:\my-next-site" # à adapter
|
||||||
|
.\stop-my-site.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
> Les détails (ports déjà pris, UTF-8 BOM, etc.) : voir le fichier `CONFIGURATION_SITE.md` à la racine du dépôt.
|
||||||
|
|
||||||
|
## Démarrage manuel (un terminal par service)
|
||||||
|
|
||||||
|
**Next.js** (port 3000) :
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Set-Location "J:\my-next-site"
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
**Strapi** (port 1337) :
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Set-Location "J:\my-next-site\cmsbackend"
|
||||||
|
npm run develop
|
||||||
|
```
|
||||||
|
|
||||||
|
**FastAPI + GrasBot** (port 8000) :
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Set-Location "J:\my-next-site\llm-api"
|
||||||
|
uvicorn api:app --host 0.0.0.0 --port 8000 --reload
|
||||||
|
```
|
||||||
|
|
||||||
|
- Santé : `GET http://localhost:8000/health`
|
||||||
|
- Question test : `GET http://localhost:8000/ask?q=bonjour`
|
||||||
|
|
||||||
|
## Recharger le vault GrasBot (cache API)
|
||||||
|
|
||||||
|
Après **édition** de fichiers dans `vault-grasbot/`, sans redémarrer uvicorn :
|
||||||
|
|
||||||
|
Sous **Windows PowerShell**, préfère toujours (idempotent, pas d’alias trompeur) :
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Invoke-RestMethod -Method Post -Uri "http://localhost:8000/reload-vault"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Piège :** dans PowerShell, `curl` est un **alias** de `Invoke-WebRequest`, pas le binaire GNU. La forme `curl -X POST …` **échoue** avec *« Impossible de trouver un paramètre correspondant au nom « X » »*.
|
||||||
|
|
||||||
|
Si tu veux le vrai curl (souvent présent sur Windows 10+) :
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
curl.exe -X POST http://localhost:8000/reload-vault
|
||||||
|
```
|
||||||
|
|
||||||
|
Sous **bash** (Git Bash, WSL, Linux, macOS) :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8000/reload-vault
|
||||||
|
```
|
||||||
|
|
||||||
|
Si l’API tourne ailleurs, remplace l’hôte/port.
|
||||||
|
|
||||||
|
**Ollama** (modèle LLM) : service séparé, en pratique sur le **port 11434** en local. Voir [[02 Ports et URLs]].
|
||||||
|
|
||||||
|
## Voir aussi
|
||||||
|
|
||||||
|
- [[00 Hub]]
|
||||||
|
- [[03 Documentation intégrée (dépôt)]]
|
||||||
|
- [04-api-llm-et-chatbot.md](docs-site-interne/04-api-llm-et-chatbot.md) — API GrasBot (implémentation : dossier `llm-api/` du dépôt).
|
||||||
20
obsidian-site-docs/02 Ports et URLs.md
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
# Ports et URLs
|
||||||
|
|
||||||
|
| Service | Port usuel | URL locale typique | Rôle |
|
||||||
|
|---------|------------|--------------------|------|
|
||||||
|
| Next.js | 3000 | http://localhost:3000 | Site public, App Router |
|
||||||
|
| Strapi | 1337 | http://localhost:1337/admin | CMS, API `/api` |
|
||||||
|
| FastAPI (GrasBot) | 8000 | http://localhost:8000 | `/ask`, `/health`, `/reload-vault` |
|
||||||
|
| Ollama | 11434 | http://localhost:11434 | Inférence LLM |
|
||||||
|
|
||||||
|
## Production (rappel)
|
||||||
|
|
||||||
|
- API Strapi publique (ex.) : `https://api.fernandgrascalvet.com`
|
||||||
|
- API LLM hébergée (proxy Next) : `llmapi.fernandgrascalvet.com` — le front en prod appelle souvent celle-là via `app/api/proxy`, pas `localhost:8000`.
|
||||||
|
|
||||||
|
Le navigateur en **dev** pointe le CMS selon `getApiUrl` (souvent `localhost:1337` en local).
|
||||||
|
|
||||||
|
## Voir aussi
|
||||||
|
|
||||||
|
- [[00 Hub]]
|
||||||
|
- [[01 Commandes - Démarrage, arrêt, reload vault]]
|
||||||
27
obsidian-site-docs/03 Documentation intégrée (dépôt).md
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
# Documentation intégrée dans ce coffre
|
||||||
|
|
||||||
|
Toute la **documentation interne** du site est **copiée ici** sous `docs-site-interne/`, avec `CONFIGURATION_SITE.md` et `README-racine-depot.md` à la **racine de ce pack**. Les liens ci-dessous sont relatifs à `obsidian-site-docs/` (ouvert comme coffre Obsidian).
|
||||||
|
|
||||||
|
## Racine du pack
|
||||||
|
|
||||||
|
- [CONFIGURATION_SITE.md](CONFIGURATION_SITE.md) — opérationnel (ports, scripts, `reload-vault`, dépannage)
|
||||||
|
- [README-racine-depot.md](README-racine-depot.md) — entrée du dépôt Git (GitHub)
|
||||||
|
- [SYNC-DOC.md](SYNC-DOC.md) — resynchroniser la copie depuis le dépôt
|
||||||
|
|
||||||
|
## Dossier [docs-site-interne](docs-site-interne/)
|
||||||
|
|
||||||
|
- [README — index](docs-site-interne/README.md) — **commencer ici** pour le détail
|
||||||
|
- [01-architecture](docs-site-interne/01-architecture.md) · [02-frontend](docs-site-interne/02-frontend-next.md) · [03-cms](docs-site-interne/03-cms-strapi.md) · [04-api-llm](docs-site-interne/04-api-llm-et-chatbot.md)
|
||||||
|
- [05-environnement](docs-site-interne/05-environnement-scripts.md) · [06-extraction](docs-site-interne/06-strapi-extraction.md) · [07-captures](docs-site-interne/07-reference-visuelle-captures.md) · [08-vault](docs-site-interne/08-vault-obsidian-retrieval.md)
|
||||||
|
- [etat-actuel](docs-site-interne/etat-actuel.md) · [feuille-de-route](docs-site-interne/feuille-de-route.md) · [REFONTE-VISUELLE](docs-site-interne/REFONTE-VISUELLE.md)
|
||||||
|
- [contact-flow (Brevo)](docs-site-interne/contact-flow.md) · [langfuse-observability](docs-site-interne/langfuse-observability.md)
|
||||||
|
- [captures/INDEX](docs-site-interne/captures/INDEX.md)
|
||||||
|
|
||||||
|
## Ancienne fiche
|
||||||
|
|
||||||
|
L’ancienne note « Lien vers la doc (dépôt) » a été remplacée par celle-ci : tout est **dans** le coffre, plus besoin de remonter au clone Git pour lire le Markdown.
|
||||||
|
|
||||||
|
## Voir aussi
|
||||||
|
|
||||||
|
- [[00 Hub]]
|
||||||
|
- [[01 Commandes - Démarrage, arrêt, reload vault]]
|
||||||
16
obsidian-site-docs/04 GrasBot et API LLM (résumé).md
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
# GrasBot et API LLM (résumé)
|
||||||
|
|
||||||
|
- **Rôle** : chatbot intégré au site (FAB global) ; questions/réponses s’appuient sur `vault-grasbot/` et un modèle local via **Ollama** (ex. Qwen3), orchestré par **FastAPI** (`llm-api/search.py`).
|
||||||
|
|
||||||
|
- **v3 (2026)** : retrieval **graphe + BM25** sur le vault, **sans** embeddings / ChromaDB. Détail : [08-vault-obsidian-retrieval.md](docs-site-interne/08-vault-obsidian-retrieval.md).
|
||||||
|
|
||||||
|
- **Observabilité** : **Langfuse** (optionnel, secrets dans `llm-api/.env` du dépôt). Doc : [langfuse-observability.md](docs-site-interne/langfuse-observability.md).
|
||||||
|
|
||||||
|
- **Rechargement** du contenu textuel du vault côté API : [[01 Commandes - Démarrage, arrêt, reload vault]] (`POST /reload-vault`).
|
||||||
|
|
||||||
|
- **Extraction** Strapi → vault : `strapi_extraction/build-vault.py` (peut écraser les notes `source: strapi` ; ne pas toucher `source: manual`).
|
||||||
|
|
||||||
|
## Voir aussi
|
||||||
|
|
||||||
|
- [[00 Hub]]
|
||||||
|
- [[02 Ports et URLs]]
|
||||||
299
obsidian-site-docs/CONFIGURATION_SITE.md
Normal file
@ -0,0 +1,299 @@
|
|||||||
|
# Configuration du Site Web - Documentation Complète
|
||||||
|
|
||||||
|
## 📋 Vue d'ensemble
|
||||||
|
|
||||||
|
Ce site utilise une architecture full-stack moderne avec :
|
||||||
|
- **Frontend** : Next.js avec TypeScript et Tailwind CSS
|
||||||
|
- **Backend CMS** : Strapi
|
||||||
|
- **API IA** : FastAPI avec intégration Ollama
|
||||||
|
- **Démarrage automatique** : Planificateur de tâches Windows
|
||||||
|
|
||||||
|
## 🏗️ Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
my-next-site/
|
||||||
|
├── app/ # Application Next.js
|
||||||
|
├── cmsbackend/ # Backend Strapi
|
||||||
|
├── llm-api/ # API FastAPI pour IA (+ instrumentation Langfuse)
|
||||||
|
│ ├── .env # Secrets Python (Langfuse, etc.) — non committé
|
||||||
|
│ └── observability.py # Init client Langfuse (no-op safe)
|
||||||
|
├── start-my-site.ps1 # Script de démarrage
|
||||||
|
├── stop-my-site.ps1 # Script d'arrêt propre
|
||||||
|
└── package.json # Dépendances frontend
|
||||||
|
```
|
||||||
|
|
||||||
|
**Observabilité** : le chatbot GrasBot est tracé dans une instance **Langfuse self-hosted** (`langfuse.fernandgrascalvet.com`). Chaque question déclenche une trace `ask` avec spans `retrieval` / `prompt_build` / `ollama-chat`, plus des scores auto (`grounded`, `retrieval_relevance`) et des tags. Voir `docs-site-interne/langfuse-observability.md` pour le détail.
|
||||||
|
|
||||||
|
## Contenu : compétences, réalisations IA et ordre d’affichage
|
||||||
|
|
||||||
|
- **Ordre** de la liste `/competences` : champ `order` sur le content-type compétence. Selon la version de Strapi, l’API renvoie `attributes.order` (v4) ou `order` (v5) — le front unifie cela (voir `getOrder()` dans `app/competences/page.jsx`).
|
||||||
|
- **Fiche** `/competences/[slug]` : affiche des **vignettes** (projets de type `realisation-ia` liés à la compétence) dès qu’il en existe ; sinon, rendu de la fiche richtext « classique ».
|
||||||
|
- **Détail** d’une réalisation : route `/competences/[slug]/[realisation]` (même gabarit de contenu qu’une fiche portfolio).
|
||||||
|
- **Documentation détaillée** (tableau des routes, champs Strapi, lien avec GrasBot) : `docs-site-interne/02-frontend-next.md` et, côté API chatbot, `docs-site-interne/04-api-llm-et-chatbot.md` (section *Parcours public*).
|
||||||
|
|
||||||
|
## 🚀 Démarrage Rapide
|
||||||
|
|
||||||
|
### Script Automatique (Recommandé)
|
||||||
|
```powershell
|
||||||
|
cd J:\my-next-site
|
||||||
|
.\start-my-site.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
Ce script lance automatiquement les 3 services dans des fenêtres PowerShell séparées.
|
||||||
|
|
||||||
|
**Améliorations :**
|
||||||
|
- Configuration centralisée via un tableau `$services` (plus de duplication entre les 3 blocs).
|
||||||
|
- **Détection du port déjà occupé** : si un service tourne déjà, il n'est pas relancé (évite `EADDRINUSE`).
|
||||||
|
- **Portabilité** : chemins résolus via `$PSScriptRoot`, pas de `J:\my-next-site` codé en dur.
|
||||||
|
- **`-NoExit`** sur chaque fenêtre : le message d'erreur reste visible si un service crashe au démarrage.
|
||||||
|
- **Bilan final** : nombre de services lancés / déjà actifs / échecs.
|
||||||
|
|
||||||
|
### Arrêt des services
|
||||||
|
```powershell
|
||||||
|
cd J:\my-next-site
|
||||||
|
.\stop-my-site.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
Termine les processus qui écoutent les ports **1337** (Strapi), **3000** (Next.js) et **8000** (FastAPI). Ne touche pas aux autres processus Node de ta machine. Les fenêtres PowerShell lancées par `start-my-site.ps1` restent ouvertes (à fermer manuellement). En cas d'échec sur certains PIDs → relancer dans un PowerShell admin.
|
||||||
|
|
||||||
|
> Note encodage : les deux scripts PowerShell sont encodés en **UTF-8 avec BOM**. C'est nécessaire pour que Windows PowerShell 5.1 (version par défaut) les lise correctement avec les emojis et accents. PowerShell 7 n'a pas ce souci mais reste compatible avec le BOM.
|
||||||
|
|
||||||
|
## 🔧 Commandes Manuelles
|
||||||
|
|
||||||
|
### 1. Frontend Next.js
|
||||||
|
```powershell
|
||||||
|
cd J:\my-next-site
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
- **URL** : http://localhost:3000
|
||||||
|
- **Mode** : Développement avec Turbopack
|
||||||
|
- **Rechargement** : Automatique
|
||||||
|
|
||||||
|
### 2. Backend Strapi (CMS)
|
||||||
|
```powershell
|
||||||
|
cd J:\my-next-site\cmsbackend
|
||||||
|
npm run develop
|
||||||
|
```
|
||||||
|
- **Interface Admin** : http://localhost:1337/admin
|
||||||
|
- **API** : http://localhost:1337/api
|
||||||
|
- **Mode** : Développement
|
||||||
|
|
||||||
|
### 3. API FastAPI (IA)
|
||||||
|
```powershell
|
||||||
|
cd J:\my-next-site\llm-api
|
||||||
|
uvicorn api:app --host 0.0.0.0 --port 8000 --reload
|
||||||
|
```
|
||||||
|
- **API** : http://localhost:8000
|
||||||
|
- **Endpoint IA** : http://localhost:8000/ask?q=votre_question
|
||||||
|
- **Documentation** : http://localhost:8000/docs
|
||||||
|
- **Santé** : http://localhost:8000/health — renvoie `status`, `ollama_url`, `llm_model`, métadonnées vault, et `observability.langfuse_enabled`.
|
||||||
|
- **Recharger le vault à chaud** : `POST http://localhost:8000/reload-vault` — à appeler après création/modification d'une note dans `vault-grasbot/` (sinon `load_vault()` reste en cache mémoïsé jusqu'au prochain redémarrage d'uvicorn).
|
||||||
|
|
||||||
|
> **Tuning du pipeline LLM** : les paramètres Ollama (`num_ctx`, `num_predict`, `think`), la troncature des sources RAG secondaires (`SEARCH_SECONDARY_MAX_CHARS`, `SEARCH_SECONDARY_KEEP_RATIO`), le system prompt anti-hallucination et la note `bio-fernand` sont documentés en détail dans `docs-site-interne/langfuse-observability.md` (section *Tuning du pipeline — 2026-04-23*).
|
||||||
|
|
||||||
|
## 📊 Ports Utilisés
|
||||||
|
|
||||||
|
| Service | Port | URL |
|
||||||
|
|---------|------|-----|
|
||||||
|
| Next.js | 3000 | http://localhost:3000 |
|
||||||
|
| Strapi | 1337 | http://localhost:1337 |
|
||||||
|
| FastAPI | 8000 | http://localhost:8000 |
|
||||||
|
| Ollama | 11434 | http://localhost:11434 |
|
||||||
|
|
||||||
|
## 🔄 Démarrage Automatique
|
||||||
|
|
||||||
|
### Configuration Actuelle
|
||||||
|
- **Méthode** : Planificateur de tâches Windows
|
||||||
|
- **Nom de la tâche** : "Lancement site web dino"
|
||||||
|
- **État** : Ready (Prêt)
|
||||||
|
|
||||||
|
### Gestion de la Tâche Planifiée
|
||||||
|
|
||||||
|
#### Voir les détails
|
||||||
|
```powershell
|
||||||
|
Get-ScheduledTask -TaskName "Lancement site web dino" | Get-ScheduledTaskInfo
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Démarrer manuellement
|
||||||
|
```powershell
|
||||||
|
Start-ScheduledTask -TaskName "Lancement site web dino"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Arrêter la tâche
|
||||||
|
```powershell
|
||||||
|
Stop-ScheduledTask -TaskName "Lancement site web dino"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Désactiver/Réactiver
|
||||||
|
```powershell
|
||||||
|
# Désactiver
|
||||||
|
Disable-ScheduledTask -TaskName "Lancement site web dino"
|
||||||
|
|
||||||
|
# Réactiver
|
||||||
|
Enable-ScheduledTask -TaskName "Lancement site web dino"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Interface Graphique
|
||||||
|
```
|
||||||
|
Win + R → taskschd.msc
|
||||||
|
```
|
||||||
|
Cherchez "Lancement site web dino" dans la liste.
|
||||||
|
|
||||||
|
## 🛠️ Dépannage
|
||||||
|
|
||||||
|
### Arrêter Tous les Processus
|
||||||
|
```powershell
|
||||||
|
# Arrêter Node.js
|
||||||
|
taskkill /f /im node.exe
|
||||||
|
|
||||||
|
# Arrêter Python/FastAPI
|
||||||
|
taskkill /f /im python.exe
|
||||||
|
```
|
||||||
|
|
||||||
|
### Nettoyer les Caches
|
||||||
|
```powershell
|
||||||
|
# Cache Next.js
|
||||||
|
cd J:\my-next-site
|
||||||
|
Remove-Item .next -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
|
|
||||||
|
# Cache Strapi
|
||||||
|
cd J:\my-next-site\cmsbackend
|
||||||
|
Remove-Item .cache -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
|
```
|
||||||
|
|
||||||
|
### Réinstaller les Dépendances
|
||||||
|
```powershell
|
||||||
|
# Frontend
|
||||||
|
cd J:\my-next-site
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# Backend Strapi
|
||||||
|
cd J:\my-next-site\cmsbackend
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# API Python (si environnement virtuel)
|
||||||
|
cd J:\my-next-site\llm-api
|
||||||
|
pip install fastapi uvicorn[standard] requests
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🐍 Configuration Python/FastAPI
|
||||||
|
|
||||||
|
### Dépendances Requises
|
||||||
|
```txt
|
||||||
|
fastapi==0.115.8
|
||||||
|
uvicorn[standard]==0.38.0
|
||||||
|
requests==2.32.3
|
||||||
|
```
|
||||||
|
|
||||||
|
### Problèmes Courants
|
||||||
|
- **Erreur uvicorn** : Vérifiez l'installation avec `pip show uvicorn`
|
||||||
|
- **Port occupé** : Changez le port dans le script ou tuez le processus
|
||||||
|
- **Ollama non disponible** : Vérifiez que Ollama fonctionne sur le port 11434
|
||||||
|
|
||||||
|
### Installation Propre (Recommandée)
|
||||||
|
```powershell
|
||||||
|
cd J:\my-next-site\llm-api
|
||||||
|
python -m venv venv
|
||||||
|
venv\Scripts\activate
|
||||||
|
pip install fastapi uvicorn[standard] requests
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📝 Scripts Disponibles
|
||||||
|
|
||||||
|
### Frontend (package.json)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"dev": "next dev --turbopack",
|
||||||
|
"build": "next build",
|
||||||
|
"start": "next start",
|
||||||
|
"lint": "next lint"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Backend Strapi (cmsbackend/package.json)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"develop": "strapi develop",
|
||||||
|
"build": "strapi build",
|
||||||
|
"start": "strapi start"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔍 Vérifications de Santé
|
||||||
|
|
||||||
|
### Vérifier que tous les services fonctionnent
|
||||||
|
```powershell
|
||||||
|
# Next.js
|
||||||
|
curl http://localhost:3000
|
||||||
|
|
||||||
|
# Strapi
|
||||||
|
curl http://localhost:1337/admin
|
||||||
|
|
||||||
|
# FastAPI
|
||||||
|
curl http://localhost:8000/docs
|
||||||
|
|
||||||
|
# Ollama
|
||||||
|
curl http://localhost:11434/api/generate
|
||||||
|
```
|
||||||
|
|
||||||
|
### Vérifier les processus actifs
|
||||||
|
```powershell
|
||||||
|
# Processus Node.js
|
||||||
|
Get-Process node -ErrorAction SilentlyContinue
|
||||||
|
|
||||||
|
# Processus Python
|
||||||
|
Get-Process python -ErrorAction SilentlyContinue
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🌐 Configuration Réseau
|
||||||
|
|
||||||
|
### Accès Externe
|
||||||
|
Si vous voulez accéder au site depuis d'autres machines :
|
||||||
|
- **Next.js** : Modifier `next.config.ts` pour accepter les connexions externes
|
||||||
|
- **Strapi** : Configurer `config/server.ts`
|
||||||
|
- **FastAPI** : Déjà configuré avec `--host 0.0.0.0`
|
||||||
|
|
||||||
|
### Pare-feu Windows
|
||||||
|
Assurez-vous que les ports sont ouverts :
|
||||||
|
```powershell
|
||||||
|
# Ouvrir les ports dans le pare-feu
|
||||||
|
New-NetFirewallRule -DisplayName "Next.js" -Direction Inbound -Port 3000 -Protocol TCP -Action Allow
|
||||||
|
New-NetFirewallRule -DisplayName "Strapi" -Direction Inbound -Port 1337 -Protocol TCP -Action Allow
|
||||||
|
New-NetFirewallRule -DisplayName "FastAPI" -Direction Inbound -Port 8000 -Protocol TCP -Action Allow
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📚 Ressources Utiles
|
||||||
|
|
||||||
|
- **Next.js Documentation** : https://nextjs.org/docs
|
||||||
|
- **Strapi Documentation** : https://docs.strapi.io
|
||||||
|
- **FastAPI Documentation** : https://fastapi.tiangolo.com
|
||||||
|
- **Ollama Documentation** : https://ollama.ai/docs
|
||||||
|
|
||||||
|
## 🔧 Maintenance
|
||||||
|
|
||||||
|
### Mise à jour des dépendances
|
||||||
|
```powershell
|
||||||
|
# Frontend
|
||||||
|
cd J:\my-next-site
|
||||||
|
npm update
|
||||||
|
|
||||||
|
# Backend
|
||||||
|
cd J:\my-next-site\cmsbackend
|
||||||
|
npm update
|
||||||
|
|
||||||
|
# Python packages
|
||||||
|
pip list --outdated
|
||||||
|
pip install --upgrade package_name
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sauvegarde
|
||||||
|
Pensez à sauvegarder régulièrement :
|
||||||
|
- Base de données Strapi (`cmsbackend/database/`)
|
||||||
|
- Configuration (`cmsbackend/config/`)
|
||||||
|
- Assets (`app/assets/`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Dernière mise à jour** : 2026-04-24
|
||||||
|
**Version Cursor** : 2.0.77
|
||||||
|
**OS** : Windows Server 2025
|
||||||
15
obsidian-site-docs/README - utiliser ce dossier.md
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
# Comment utiliser ce dossier dans Obsidian
|
||||||
|
|
||||||
|
Ce répertoire (`obsidian-site-docs/`) est une **aide-mémoire** alignée sur la vraie documentation du dépôt (`docs-site-interne/`, `CONFIGURATION_SITE.md`). Il sert surtout aux **commandes** et **liens** pour le travail sur le site.
|
||||||
|
|
||||||
|
## Option A — Ouvrir comme coffre secondaire
|
||||||
|
|
||||||
|
Dans Obsidian : **Fichier → Ouvrir un coffre** → choisir le dossier `obsidian-site-docs` (situé à la racine du clone `my-next-site`). Tu obtiens un mini-coffre avec le [[00 Hub|hub]] en point d’entrée.
|
||||||
|
|
||||||
|
## Option B — Copier dans ton coffre perso
|
||||||
|
|
||||||
|
Copie tout le contenu de `obsidian-site-docs/` dans un dossier de ton choix (ex. `20-Technique/Site-fernandgrascalvet/`). Les wikilinks `[[...]]` entre les notes de ce pack restent valides. La documentation complète est **dans** `docs-site-interne/` (fichiers Markdown) ; [SYNC-DOC](SYNC-DOC.md) explique la resynchro avec le dépôt Git. Les chemins relatifs (ex. `docs-site-interne/01-…`) restent bons **si** tu exportes le dossier entier.
|
||||||
|
|
||||||
|
## Source de vérité
|
||||||
|
|
||||||
|
Les fichiers longs, schémas mermaid et captures restent dans le dépôt : préférer la lecture sur GitHub ou dans l’IDE pour l’**intégralité** de la spec.
|
||||||
75
obsidian-site-docs/README-racine-depot.md
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
# Portfolio fernandgrascalvet.com
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Site portfolio **Next.js 15** + **Strapi 5** + **GrasBot** (FastAPI, Ollama, vault `vault-grasbot/`). UI *Digital Atelier* (Manrope, Newsreader, Tailwind). Hébergement typique : Windows Server, IIS, HTTPS (Win-ACME).
|
||||||
|
|
||||||
|
**Site en ligne :** [fernandgrascalvet.com](https://fernandgrascalvet.com)
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
| Ressource | Rôle |
|
||||||
|
|-----------|------|
|
||||||
|
| **[docs-site-interne](docs-site-interne/README.md)** | Architecture, CMS, front, API LLM, feuille de route, état actuel, captures, refonte UI. *À lire en priorité pour le contexte technique.* |
|
||||||
|
| [`CONFIGURATION_SITE.md`](CONFIGURATION_SITE.md) | Opérationnel : ports, commandes, démarrage automatique, dépannage, pare-feu. |
|
||||||
|
| [`vault-grasbot/README.md`](vault-grasbot/README.md) | Base de connaissances GrasBot (retrieval graph + BM25 v3). |
|
||||||
|
|
||||||
|
**Obsidian :** le dossier [`obsidian-site-docs/`](obsidian-site-docs/) regroupe un hub et des fiches prêtes à copier ou à ouvrir comme coffre secondaire (commandes, ports, liens vers la doc du dépôt).
|
||||||
|
|
||||||
|
## Démarrage rapide (Windows)
|
||||||
|
|
||||||
|
Depuis la racine du dépôt (adapter le lecteur / chemin si besoin) :
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Les trois services : Next, Strapi, FastAPI (fenêtres séparées)
|
||||||
|
.\start-my-site.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Arrêt propre (ports 3000, 1337, 8000)
|
||||||
|
.\stop-my-site.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
Détail, ports et commandes manuelles : **[`CONFIGURATION_SITE.md`](CONFIGURATION_SITE.md)**.
|
||||||
|
|
||||||
|
## Stack (résumé)
|
||||||
|
|
||||||
|
- **Front** : Next.js (App Router), TypeScript/JS, Tailwind, Swiper, chatbot global (FAB).
|
||||||
|
- **CMS** : Strapi — `homepage`, `project`, `competence`, `realisation-ia`, `glossaire`.
|
||||||
|
- **Contact** : e-mail via **Brevo** (route Next `POST /api/contact`), pas de stockage Strapi des messages. Voir [`docs-site-interne/contact-flow.md`](docs-site-interne/contact-flow.md).
|
||||||
|
- **IA** : `llm-api/` (FastAPI) → Ollama (ex. Qwen3), base `vault-grasbot/`, observabilité **Langfuse** optionnelle. Voir [`docs-site-interne/04-api-llm-et-chatbot.md`](docs-site-interne/04-api-llm-et-chatbot.md).
|
||||||
|
|
||||||
|
## Rechargement du vault GrasBot (API locale)
|
||||||
|
|
||||||
|
Après modification des fichiers dans `vault-grasbot/`, recharger le cache côté API sans redémarrer uvicorn :
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Exemple (Invoke-RestMethod)
|
||||||
|
Invoke-RestMethod -Method Post -Uri "http://localhost:8000/reload-vault"
|
||||||
|
```
|
||||||
|
|
||||||
|
Ou : `POST http://localhost:8000/reload-vault` (HTTP client de votre choix). Voir aussi [`CONFIGURATION_SITE.md`](CONFIGURATION_SITE.md) (santé `/health`, endpoint `/ask`).
|
||||||
|
|
||||||
|
## Dépôts et répertoires utiles
|
||||||
|
|
||||||
|
```
|
||||||
|
my-next-site/
|
||||||
|
├── app/ # Next.js
|
||||||
|
├── cmsbackend/ # Strapi
|
||||||
|
├── llm-api/ # FastAPI + GrasBot
|
||||||
|
├── vault-grasbot/ # Connaissance (Obsidian) pour le retrieval
|
||||||
|
├── strapi_extraction/ # Extraction / build vault
|
||||||
|
├── docs-site-interne/ # Doc technique détaillée
|
||||||
|
├── obsidian-site-docs/ # Pack Obsidian (résumés + commandes)
|
||||||
|
├── start-my-site.ps1
|
||||||
|
├── stop-my-site.ps1
|
||||||
|
└── CONFIGURATION_SITE.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Licence et usage
|
||||||
|
|
||||||
|
Projet personnel ; contenu et code sont fournis tels quels pour illustration du portfolio.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Dernière révision du README : 2026-04 — aligné sur `docs-site-interne` et `CONFIGURATION_SITE.md`.*
|
||||||
9
obsidian-site-docs/README.md
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
# Pack Obsidian — site my-next-site
|
||||||
|
|
||||||
|
Contenu : **aide-mémoire** (commandes, ports, hub) + **copie intégrée** de `docs-site-interne/` (toute la doc technique), plus [CONFIGURATION_SITE.md](CONFIGURATION_SITE.md) et [README-racine-depot.md](README-racine-depot.md) à la racine de ce pack.
|
||||||
|
|
||||||
|
- **Entrée** : [00 Hub.md](00%20Hub.md) — ou [docs-site-interne/README.md](docs-site-interne/README.md) pour l’index détaillé
|
||||||
|
- **Resynchronisation** avec le dépôt Git : [SYNC-DOC.md](SYNC-DOC.md)
|
||||||
|
- **Mode d’emploi** (coffre dédié vs copie dans un autre vault) : [README - utiliser ce dossier.md](README%20-%20utiliser%20ce%20dossier.md)
|
||||||
|
|
||||||
|
Sur **GitHub**, le dossier `obsidian-site-docs/` est parcourable ; les liens relatifs pointent vers les fichiers **dans ce même dossier** (export autonome).
|
||||||
31
obsidian-site-docs/SYNC-DOC.md
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
# Resynchroniser la documentation intégrée
|
||||||
|
|
||||||
|
Le dossier `docs-site-interne/` dans **ce coffre** est une **copie** de `my-next-site/docs-site-interne/` (dépôt Git). Les fichiers `CONFIGURATION_SITE.md` et `README-racine-depot.md` à la racine de ce pack sont des copies de la racine du dépôt.
|
||||||
|
|
||||||
|
## Quand lancer une resynchro
|
||||||
|
|
||||||
|
Après toute **édition** des fichiers dans le dépôt (`docs-site-interne/`, `CONFIGURATION_SITE.md`, `README.md` racine) que tu veux voir **dans l’export Obsidian**.
|
||||||
|
|
||||||
|
## Procédure (PowerShell, depuis le clone)
|
||||||
|
|
||||||
|
À adapter : lecteur `J:` et chemin du dépôt.
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
$root = "J:\my-next-site" # racine du dépôt
|
||||||
|
$obs = Join-Path $root "obsidian-site-docs"
|
||||||
|
$target = Join-Path $obs "docs-site-interne"
|
||||||
|
|
||||||
|
Remove-Item -Path $target -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
|
Copy-Item -Path (Join-Path $root "docs-site-interne") -Destination $target -Recurse -Force
|
||||||
|
Copy-Item (Join-Path $root "CONFIGURATION_SITE.md") (Join-Path $obs "CONFIGURATION_SITE.md") -Force
|
||||||
|
Copy-Item (Join-Path $root "README.md") (Join-Path $obs "README-racine-depot.md") -Force
|
||||||
|
|
||||||
|
# Corriger les liens `docs-site-interne/...` pour ce coffre
|
||||||
|
& (Join-Path $obs "_fix-links-in-docs-copy.ps1")
|
||||||
|
```
|
||||||
|
|
||||||
|
Puis **réapplique** manuellement les ajustements spécifiques du pack (fichier `docs-site-interne/README.md` dans *ce* coffre, section *Relation* / arborescence, liens `SYNC-DOC` et `etat-actuel` si ton script a écrasé des retouches locales) — le plus sûr est de **commiter d’abord** ce dépôt et de refaire seulement les 2–3 liens listés en haut de `docs-site-interne/README.md` d’ici.
|
||||||
|
|
||||||
|
## Fichier utilitaire
|
||||||
|
|
||||||
|
- `_fix-links-in-docs-copy.ps1` : adapte les chemins après un `Copy-Item` (ne pas lancer sur le dépôt source `docs-site-interne` à la racine du repo, uniquement sur la copie sous `obsidian-site-docs`).
|
||||||
26
obsidian-site-docs/_fix-links-in-docs-copy.ps1
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
# One-shot: adapte les chemins docs-site-interne/ dans la copie Obsidian
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
$base = Join-Path $PSScriptRoot "docs-site-interne"
|
||||||
|
if (-not (Test-Path $base)) { throw "Dossier introuvable: $base" }
|
||||||
|
|
||||||
|
Get-ChildItem -Path $base -Recurse -Filter *.md | ForEach-Object {
|
||||||
|
# README.md (racine de docs-site-interne) : contient une arborescence en bloc de code
|
||||||
|
# avec le nom du dossier — ne pas y appliquer le remplacement global.
|
||||||
|
if ($_.Name -eq "README.md" -and $_.DirectoryName -eq (Resolve-Path $base).Path) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
$isUnderCaptures = $_.FullName -like "*\captures\*" -or $_.DirectoryName -like "*\captures"
|
||||||
|
$c = [System.IO.File]::ReadAllText($_.FullName)
|
||||||
|
if ($isUnderCaptures) {
|
||||||
|
$c = $c -replace "docs-site-interne/REFONTE-VISUELLE", "../REFONTE-VISUELLE"
|
||||||
|
$c = $c -replace "docs-site-interne/contact-flow", "../contact-flow"
|
||||||
|
$c = $c -replace "docs-site-interne/08-vault-obsidian-retrieval", "../08-vault-obsidian-retrieval"
|
||||||
|
$c = $c -replace "docs-site-interne/captures/", ""
|
||||||
|
$c = $c -replace "docs-site-interne/", "../"
|
||||||
|
} else {
|
||||||
|
$c = $c -replace "docs-site-interne/", ""
|
||||||
|
}
|
||||||
|
$utf8 = New-Object System.Text.UTF8Encoding $false
|
||||||
|
[System.IO.File]::WriteAllText($_.FullName, $c, $utf8)
|
||||||
|
}
|
||||||
|
Write-Host "OK: liens ajustes dans $base"
|
||||||
46
obsidian-site-docs/docs-site-interne/01-architecture.md
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
# Architecture globale
|
||||||
|
|
||||||
|
**Dernière mise à jour :** 2026-04-01
|
||||||
|
|
||||||
|
## Schéma logique
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
Browser[Navigateur]
|
||||||
|
Next[Next.js :3000]
|
||||||
|
Strapi[Strapi :1337]
|
||||||
|
FastAPI[FastAPI :8000]
|
||||||
|
Ollama[Ollama :11434]
|
||||||
|
RemoteAPI[api.fernandgrascalvet.com]
|
||||||
|
|
||||||
|
Browser --> Next
|
||||||
|
Next -->|"fetch REST (getApiUrl)"| Strapi
|
||||||
|
Next -->|"fetch REST"| RemoteAPI
|
||||||
|
Next -->|"/api/proxy → llmapi.*"| FastAPI
|
||||||
|
FastAPI --> Ollama
|
||||||
|
```
|
||||||
|
|
||||||
|
En **développement local**, le front appelle souvent **Strapi sur `localhost:1337`** (voir `getApiUrl`). En **production**, les appels client pointent vers **`https://api.fernandgrascalvet.com`**.
|
||||||
|
|
||||||
|
Le **chatbot** ne parle pas à FastAPI en local par défaut : la route `app/api/proxy/route.js` appelle **`https://llmapi.fernandgrascalvet.com/ask`** (URL absolue).
|
||||||
|
|
||||||
|
## Services et ports
|
||||||
|
|
||||||
|
| Service | Port local typique | Rôle |
|
||||||
|
|---------|-------------------|------|
|
||||||
|
| Next.js | 3000 | Site public, rewrites `/api/*` → Strapi distant (voir `next.config.ts`) |
|
||||||
|
| Strapi | 1337 | CMS, REST `/api/*` |
|
||||||
|
| FastAPI (`llm-api/api.py`) | 8000 | Pont HTTP vers Ollama `/api/generate` |
|
||||||
|
| Ollama | 11434 | Inférence LLM (modèle `mistral` dans le code actuel) |
|
||||||
|
|
||||||
|
## Fichiers clés
|
||||||
|
|
||||||
|
- `next.config.ts` — rewrites, `NEXT_PUBLIC_API_URL`, domaines images.
|
||||||
|
- `app/utils/getApiUrl.ts` — choix de l’URL Strapi (local vs prod, client vs serveur).
|
||||||
|
- `app/api/proxy/route.js` — proxy vers l’API LLM **hébergée** (`llmapi.fernandgrascalvet.com`).
|
||||||
|
- `llm-api/api.py` — implémentation FastAPI locale (Ollama).
|
||||||
|
|
||||||
|
## Points d’attention
|
||||||
|
|
||||||
|
1. **`next.config` rewrites** : les requêtes vers `/api/*` côté Next sont renvoyées vers l’URL Strapi configurée — **conflit sémantique** avec la route Next `app/api/proxy` (chemin différent : le proxy est sous `/api/proxy`, pas sous le rewrite générique de la même façon ; à vérifier selon l’ordre de matching Next).
|
||||||
|
2. **`app/utils/config.ts`** expose `API_URL` avec défaut `localhost:1337`, alors que `next.config.ts` utilise par défaut le domaine de prod : usages distincts, ne pas les confondre.
|
||||||
65
obsidian-site-docs/docs-site-interne/02-frontend-next.md
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
# Frontend Next.js
|
||||||
|
|
||||||
|
**Dernière mise à jour :** 2026-04-24
|
||||||
|
|
||||||
|
## Stack
|
||||||
|
|
||||||
|
- **Next.js** 15.x, **React** 18, **TypeScript** (fichiers `.tsx`/`.ts`) avec fichiers **`.jsx`/`.js`** encore présents.
|
||||||
|
- **Tailwind CSS** + `@tailwindcss/typography`.
|
||||||
|
- Rendu riche Strapi : `@strapi/blocks-react-renderer`, **react-markdown** + rehype/remark.
|
||||||
|
|
||||||
|
## Routes (App Router)
|
||||||
|
|
||||||
|
| Chemin | Fichier | Notes |
|
||||||
|
|--------|---------|--------|
|
||||||
|
| `/` | `app/page.tsx` | Accueil : `GET /api/homepages?populate=*` |
|
||||||
|
| `/portfolio` | `app/portfolio/page.jsx` | Liste projets |
|
||||||
|
| `/portfolio/[slug]` | `app/portfolio/[slug]/page.tsx` | Détail projet : `ContentSection` + `fetchData('projects', slug)` |
|
||||||
|
| `/competences` | `app/competences/page.jsx` | Liste compétences — tri par champ Strapi `order` (v4 : `attributes.order`, v5 : `order` à la racine) |
|
||||||
|
| `/competences/[slug]` | `app/competences/[slug]/page.tsx` | **Rendu conditionnel** : si au moins une entrée `realisation-ia` est liée à la compétence (filtre API sur le slug), **grille de vignettes** (même rythme visuel que le portfolio) ; sinon fiche richtext historique via `ContentSectionCompetencesContainer` |
|
||||||
|
| `/competences/[slug]/[realisation]` | `app/competences/[slug]/[realisation]/page.tsx` | Fiche d'une **réalisation** (collection Strapi `realisation-ia`) : réutilise `ContentSection` comme les projets (carousel, Markdown `resum` ou `Resum`, CTA `link` externe) |
|
||||||
|
| `/contact` | `app/contact/page.js` | Formulaire → `/api/contact` (Brevo, voir `contact-flow.md`) |
|
||||||
|
| `/api/contact` | `app/api/contact/route.ts` | Endpoint serveur : envoie un email via Brevo, honeypot + rate-limit |
|
||||||
|
| `/api/proxy` | `app/api/proxy/route.js` | Proxy GET vers API LLM distante |
|
||||||
|
|
||||||
|
**Strapi — content-types concernés :**
|
||||||
|
|
||||||
|
- `competence` : `name`, `content` (richtext), `picture`, `slug`, `order`
|
||||||
|
- `realisation-ia` : `name`, `description`, `picture`, `slug`, `resum` (richtext, alias accepté côté front : `Resum` pour les `project` uniquement), `link`, `order`, relation `competences` (plusieurs)
|
||||||
|
- Vignette → toujours navigation vers la **fiche détail** interne ; le champ `link` sert de bouton *Voir plus* en bas de fiche (comme sur les fiches `project`).
|
||||||
|
|
||||||
|
## Layout
|
||||||
|
|
||||||
|
- `app/layout.tsx` — **Client Component** (`"use client"`). Header fixe, menu burger mobile, fond décoratif, **Footer**, compteur de visites **localStorage** (`visitCount`).
|
||||||
|
|
||||||
|
## Données Strapi
|
||||||
|
|
||||||
|
- **`getApiUrl()`** (`app/utils/getApiUrl.ts`) :
|
||||||
|
- Côté **navigateur** : si hostname est local / LAN → `http://localhost:1337`, sinon → `https://api.fernandgrascalvet.com`.
|
||||||
|
- Côté **serveur** : `process.env.NEXT_PUBLIC_API_URL` ou défaut `https://api.fernandgrascalvet.com`.
|
||||||
|
- **`fetchData`** (`app/utils/fetchData.ts`) : collection + `slug`, populate `picture`, `cache: "no-store"`.
|
||||||
|
- **Accueil** (`app/page.tsx`) : `homepages`, retry 3×, timeout 10 s.
|
||||||
|
|
||||||
|
## Configuration Next
|
||||||
|
|
||||||
|
- `next.config.ts` : `rewrites` de `/api/:path*` vers `${API_URL}/api/:path*` où `API_URL` vient de `NEXT_PUBLIC_API_URL` ou défaut production.
|
||||||
|
- `images.domains` : `localhost`, `api.fernandgrascalvet.com`.
|
||||||
|
|
||||||
|
## Composants notables
|
||||||
|
|
||||||
|
- Carrousels : `Carousel.tsx`, `CarouselCompetences.tsx` (swiper / react-responsive-carousel).
|
||||||
|
- Sections : `ContentSection.tsx`, `ContentSectionCompetences*.tsx`.
|
||||||
|
- `ContactForm.tsx` → `POST /api/contact` → Brevo API (voir `contact-flow.md`).
|
||||||
|
- `GrasBotFab` + `ChatBot.js` → `askAI.js` → `/api/proxy` → FastAPI `/ask` avec `session_id` + `user_id` (UUID anonymes via `app/utils/grasbotIds.js`, voir `langfuse-observability.md`).
|
||||||
|
- `ModalGlossaire.tsx` — glossaire (données Strapi selon usage dans les pages).
|
||||||
|
|
||||||
|
## Fichiers clés (liste courte)
|
||||||
|
|
||||||
|
```
|
||||||
|
app/layout.tsx
|
||||||
|
app/page.tsx
|
||||||
|
app/utils/getApiUrl.ts
|
||||||
|
app/utils/fetchData.ts
|
||||||
|
app/api/contact/route.ts
|
||||||
|
next.config.ts
|
||||||
|
```
|
||||||
72
obsidian-site-docs/docs-site-interne/03-cms-strapi.md
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
# CMS Strapi
|
||||||
|
|
||||||
|
**Dernière mise à jour :** 2026-04-01
|
||||||
|
|
||||||
|
## Emplacement
|
||||||
|
|
||||||
|
- Code : `cmsbackend/`
|
||||||
|
- Schémas : `cmsbackend/src/api/<nom>/content-types/<nom>/schema.json`
|
||||||
|
|
||||||
|
Tous les types listés ci-dessous ont **`draftAndPublish: true`** : penser à **publier** les entrées dans l’admin.
|
||||||
|
|
||||||
|
## Content-types
|
||||||
|
|
||||||
|
### `homepage` (collection `homepages`)
|
||||||
|
|
||||||
|
| Champ | Type | Notes |
|
||||||
|
|-------|------|--------|
|
||||||
|
| `title` | string | requis |
|
||||||
|
| `cv` | richtext | requis |
|
||||||
|
| `photo` | media (single) | requis |
|
||||||
|
|
||||||
|
Utilisation front : `app/page.tsx` — premier enregistrement `populate=*`, image : `${apiUrl}${photo.url}`.
|
||||||
|
|
||||||
|
### `project` (collection `projects`)
|
||||||
|
|
||||||
|
| Champ | Type | Notes |
|
||||||
|
|-------|------|--------|
|
||||||
|
| `name` | string | requis |
|
||||||
|
| `description` | text | requis |
|
||||||
|
| `picture` | media (multiple) | requis |
|
||||||
|
| `slug` | uid ← `name` | requis |
|
||||||
|
| `Resum` | richtext | requis (nom du champ avec majuscule) |
|
||||||
|
| `link` | string (URL) | requis |
|
||||||
|
| `order` | integer | optionnel |
|
||||||
|
|
||||||
|
### `competence` (collection `competences`)
|
||||||
|
|
||||||
|
| Champ | Type | Notes |
|
||||||
|
|-------|------|--------|
|
||||||
|
| `name` | string | requis |
|
||||||
|
| `content` | richtext | requis |
|
||||||
|
| `picture` | media (multiple) | requis |
|
||||||
|
| `slug` | uid ← `name` | requis |
|
||||||
|
| `order` | integer | optionnel |
|
||||||
|
|
||||||
|
### `message` (supprimé le 2026-04-23)
|
||||||
|
|
||||||
|
Ancien content-type pour stocker les soumissions du formulaire de contact. Supprimé car le formulaire envoie désormais une notification email via **Brevo** (voir `contact-flow.md`) — plus besoin de stockage Strapi. Les 4 fichiers `cmsbackend/src/api/message/**` ont été supprimés ; la table SQLite `messages` reste orpheline (inoffensive, peut être droppée manuellement).
|
||||||
|
|
||||||
|
### `glossaire` (collection `glossaires`)
|
||||||
|
|
||||||
|
| Champ | Type | Notes |
|
||||||
|
|-------|------|--------|
|
||||||
|
| `mot_clef` | string | requis |
|
||||||
|
| `slug` | uid ← `mot_clef` | requis |
|
||||||
|
| `variantes` | json | requis |
|
||||||
|
| `description` | richtext | requis |
|
||||||
|
| `images` | media (multiple) | requis |
|
||||||
|
|
||||||
|
## API REST
|
||||||
|
|
||||||
|
- Base : `http://localhost:1337` (dev) ou `https://api.fernandgrascalvet.com` (prod).
|
||||||
|
- Préfixe : `/api/<pluralName>` (ex. `/api/projects`, `/api/homepages`).
|
||||||
|
|
||||||
|
## Fichiers de config Strapi (référence)
|
||||||
|
|
||||||
|
```
|
||||||
|
cmsbackend/config/database.ts
|
||||||
|
cmsbackend/config/server.ts
|
||||||
|
cmsbackend/config/middlewares.ts
|
||||||
|
cmsbackend/config/api.ts
|
||||||
|
```
|
||||||
157
obsidian-site-docs/docs-site-interne/04-api-llm-et-chatbot.md
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
# API LLM et chatbot (GrasBot)
|
||||||
|
|
||||||
|
**Dernière mise à jour :** 2026-04-24 (v3 + alignement parcours site)
|
||||||
|
|
||||||
|
## Vue d'ensemble
|
||||||
|
|
||||||
|
GrasBot répond aux visiteurs en s'appuyant sur un **pipeline de retrieval
|
||||||
|
local**, sans embeddings ni base vectorielle :
|
||||||
|
|
||||||
|
- Vault Obsidian `vault-grasbot/` lu directement en mémoire par `search.py`.
|
||||||
|
- Scoring déterministe multi-signaux (aliases, titre/slug, answers,
|
||||||
|
domains, tags, BM25 sur le body).
|
||||||
|
- Expansion par graphe via les wikilinks (`linked`, `related`, `[[...]]`
|
||||||
|
dans le corps).
|
||||||
|
- Prompt construit avec top-5 notes entières, envoyé à Qwen3 8B via Ollama.
|
||||||
|
|
||||||
|
Détails architecturaux dans
|
||||||
|
[`08-vault-obsidian-retrieval.md`](./08-vault-obsidian-retrieval.md).
|
||||||
|
|
||||||
|
## Chaîne côté navigateur
|
||||||
|
|
||||||
|
1. FAB `GrasBotFab` (monté dans `app/layout.tsx`) affiche `ChatBot.js`.
|
||||||
|
2. `ChatBot.js` appelle `askAI(question)` (`app/utils/askAI.js`).
|
||||||
|
3. `askAI` envoie un **GET** vers `/api/proxy?q=...` (route Next.js App Router).
|
||||||
|
4. `app/api/proxy/route.js` appelle
|
||||||
|
`https://llmapi.fernandgrascalvet.com/ask?q=...` (URL figée en dur
|
||||||
|
pour l'instant) et renvoie le corps JSON tel quel.
|
||||||
|
|
||||||
|
Le champ consommé par le front reste **`data.response`**. Les champs ajoutés
|
||||||
|
par la refonte (`sources`, `grounded`, `model`, `vault_size`) passent dans
|
||||||
|
la réponse JSON et pourront être affichés dans une itération suivante
|
||||||
|
(voir [pistes d'évolution](#pistes-dévolution)).
|
||||||
|
|
||||||
|
## Parcours public (hors moteur Python) — cohérence contenu
|
||||||
|
|
||||||
|
Le visiteur découvre les **projets** sur `/portfolio/[slug]` et, pour la
|
||||||
|
compétence **IA** (et toute compétence à laquelle on lie des
|
||||||
|
**`realisation-ia`** dans Strapi), un **parallèle** sur `/competences/[slug]`
|
||||||
|
(vignettes) puis `/competences/[slug]/[realisation]` (fiche identique en
|
||||||
|
gabarit à une fiche projet). Rien n'est servi ici par FastAPI : c'est
|
||||||
|
du **Strapi + Next** uniquement. Le chatbot, lui, interroge toujours
|
||||||
|
**`vault-grasbot/`** via `llm-api/search.py` — mettre à jour le vault
|
||||||
|
(ou l'extraction Strapi → vault) quand on veut que GrasBot **reflète** des
|
||||||
|
faits nouveaux présentés sur le site. Détail des routes : [`02-frontend-next.md`](./02-frontend-next.md).
|
||||||
|
|
||||||
|
## FastAPI — `llm-api/`
|
||||||
|
|
||||||
|
| Fichier | Rôle |
|
||||||
|
|---------|------|
|
||||||
|
| `api.py` | Endpoints `GET /ask?q=...`, `GET /health`, `POST /reload-vault`. |
|
||||||
|
| `search.py` | `load_vault`, `tokenize_fr`, `score_note`, `expand_by_graph`, `search`, `build_prompt`, `generate`, `answer`. |
|
||||||
|
| `requirements.txt` | `fastapi`, `uvicorn`, `requests`, `pyyaml` ; + `langfuse` (SDK 3.x, plafond strict inférieur à la v4) + `python-dotenv` pour l'observabilité optionnelle. Voir `llm-api/requirements.txt`. **Plus besoin** de `chromadb` / `chroma-hnswlib` (supprimés v3). |
|
||||||
|
|
||||||
|
Modules supprimés en v3 :
|
||||||
|
|
||||||
|
- `rag.py` → remplacé par `search.py`.
|
||||||
|
- `index_vault.py` → plus d'étape d'indexation (lecture directe du vault).
|
||||||
|
|
||||||
|
## Modèle Ollama
|
||||||
|
|
||||||
|
| Rôle | Modèle | VRAM | Commande |
|
||||||
|
|------|--------|------|----------|
|
||||||
|
| Chat | `qwen3:8b` | ~5 Go (Q4_K_M) | `ollama pull qwen3:8b` |
|
||||||
|
|
||||||
|
**Plus d'embeddings.** Le modèle `nomic-embed-text` n'est plus nécessaire.
|
||||||
|
Tu peux libérer de la place avec `ollama rm nomic-embed-text` si jamais
|
||||||
|
il reste installé.
|
||||||
|
|
||||||
|
## Variables d'environnement (facultatives)
|
||||||
|
|
||||||
|
Toutes définies dans `search.py`, surchargeables via env sans toucher au code :
|
||||||
|
|
||||||
|
- `OLLAMA_URL` (default `http://localhost:11434`)
|
||||||
|
- `LLM_MODEL` (default `qwen3:8b`)
|
||||||
|
- `VAULT_DIR` (default `<repo>/vault-grasbot`)
|
||||||
|
- `SEARCH_TOP_K` (default `5`)
|
||||||
|
- `SEARCH_MIN_SCORE` (default `1.0`) — seuil en-dessous duquel le chatbot
|
||||||
|
bascule en mode *« pas de contexte pertinent »* (évite les réponses
|
||||||
|
inventées sur des questions hors sujet).
|
||||||
|
|
||||||
|
## Mise en service
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# 1. Installer les dépendances Python (pure Python, pas de compilation C++)
|
||||||
|
cd llm-api
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
# 2. Pull le modèle Ollama (Ollama doit tourner)
|
||||||
|
ollama pull qwen3:8b
|
||||||
|
|
||||||
|
# 3. Lancer l'API
|
||||||
|
uvicorn api:app --host 0.0.0.0 --port 8000
|
||||||
|
```
|
||||||
|
|
||||||
|
Plus besoin d'étape d'indexation : l'API lit le vault au démarrage.
|
||||||
|
|
||||||
|
Health-check : `curl http://localhost:8000/health` retourne la config active,
|
||||||
|
la taille du vault et le nombre de notes par type.
|
||||||
|
|
||||||
|
Après édition du vault (ajout/modification d'une note) :
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Force la relecture sans redémarrer uvicorn
|
||||||
|
curl -X POST http://localhost:8000/reload-vault
|
||||||
|
```
|
||||||
|
|
||||||
|
## Réponse du backend
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"response": "Push Swap est un projet 42 qui explore les algorithmes de tri sur piles…",
|
||||||
|
"sources": [
|
||||||
|
{
|
||||||
|
"slug": "push-swap",
|
||||||
|
"title": "push_swap",
|
||||||
|
"type": "projet",
|
||||||
|
"score": 32.27,
|
||||||
|
"reasons": ["alias:push-swap", "slug", "answers-partial", "bm25:2.12"],
|
||||||
|
"url": "/portfolio/push-swap"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "cpp-partie1",
|
||||||
|
"title": "cpp_module_00 à 04",
|
||||||
|
"type": "projet",
|
||||||
|
"score": 20.62,
|
||||||
|
"reasons": ["graph-from:push-swap", "graph-reinforce"],
|
||||||
|
"url": "/portfolio/cpp-partie1"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"grounded": true,
|
||||||
|
"model": "qwen3:8b",
|
||||||
|
"vault_size": 41
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`askAI.js` ne lit que `data.response` → rétrocompatibilité assurée.
|
||||||
|
|
||||||
|
Le champ `reasons` sert à **tracer** pourquoi une note a été remontée : très
|
||||||
|
utile pour ajuster aliases / answers quand une question renvoie de mauvais
|
||||||
|
résultats.
|
||||||
|
|
||||||
|
## Pistes d'évolution
|
||||||
|
|
||||||
|
- **Variable d'environnement côté proxy Next** pour pointer vers
|
||||||
|
`http://localhost:8000` en dev et vers la prod en déploiement (au lieu
|
||||||
|
de l'URL figée dans `app/api/proxy/route.js`).
|
||||||
|
- **Affichage des sources** côté front : vignettes cliquables sous la
|
||||||
|
réponse, utilisant le champ `url` renvoyé par l'API.
|
||||||
|
- **Badge `grounded`** : afficher *« Réponse basée sur les notes »* vs
|
||||||
|
*« Réponse générale »* pour informer le visiteur de la confiance.
|
||||||
|
- **Historique court** (3-4 derniers tours) pour la continuité conversationnelle.
|
||||||
|
- **Streaming** des réponses pour l'UX temps réel (Qwen3 supporte `stream: true`).
|
||||||
|
- **Reload automatique** via file watcher sur `vault-grasbot/` quand on
|
||||||
|
édite dans Obsidian.
|
||||||
|
- **Filtre `visibility`** déjà en place dans `load_vault()` (les notes
|
||||||
|
`private` sont exclues). Le vault perso pourra être fusionné sans
|
||||||
|
exposer ses notes privées au chatbot public.
|
||||||
@ -0,0 +1,29 @@
|
|||||||
|
# Environnement et scripts
|
||||||
|
|
||||||
|
**Dernière mise à jour :** 2026-04-01
|
||||||
|
|
||||||
|
## Variables d’environnement (Next)
|
||||||
|
|
||||||
|
| Variable | Usage |
|
||||||
|
|----------|--------|
|
||||||
|
| `NEXT_PUBLIC_API_URL` | Base URL Strapi pour le build et le rendu serveur ; chargée dans `next.config.ts` (dotenv) pour rewrites et images. |
|
||||||
|
|
||||||
|
Défauts dans le code si absent :
|
||||||
|
|
||||||
|
- `next.config.ts` : `https://api.fernandgrascalvet.com`
|
||||||
|
- `app/utils/getApiUrl.ts` (navigateur local) : `http://localhost:1337`
|
||||||
|
- `app/utils/config.ts` : `http://localhost:1337` (usage ponctuel selon imports)
|
||||||
|
|
||||||
|
## Script `start-my-site.ps1`
|
||||||
|
|
||||||
|
- Définit `NEXT_PUBLIC_API_URL` et `PUBLIC_URL` vers `https://api.fernandgrascalvet.com`.
|
||||||
|
- Lance trois fenêtres PowerShell : Strapi (`cmsbackend`), Next (racine), FastAPI (`llm-api` sur port 8000).
|
||||||
|
- Chemins en dur : `J:\my-next-site\...` — à adapter si le dépôt est déplacé.
|
||||||
|
|
||||||
|
## Démarrage manuel
|
||||||
|
|
||||||
|
Voir `CONFIGURATION_SITE.md` (ports 3000, 1337, 8000, 11434).
|
||||||
|
|
||||||
|
## CORS / proxy
|
||||||
|
|
||||||
|
- La route `app/api/proxy/route.js` renvoie `Access-Control-Allow-Origin: *` sur la réponse proxy.
|
||||||
91
obsidian-site-docs/docs-site-interne/06-strapi-extraction.md
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
# Outils `strapi_extraction/`
|
||||||
|
|
||||||
|
**Dernière mise à jour :** 2026-04-22
|
||||||
|
|
||||||
|
Dossier de **scripts Node + Python** pour extraire, nettoyer et convertir les
|
||||||
|
données issues de l'API Strapi en base de connaissance chatbot (hors runtime
|
||||||
|
du site).
|
||||||
|
|
||||||
|
## Pipeline complet
|
||||||
|
|
||||||
|
```
|
||||||
|
API Strapi
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
extract-api-data.js → extract/raw/*.json
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
clean-api-data.js → extract/clean-data/*.json
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
generate-docs.js → docs/*.md
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
build-vault.py → vault-grasbot/ (Obsidian structuré + aliases/answers/priority)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
GrasBot le lit directement (plus d'étape d'indexation)
|
||||||
|
```
|
||||||
|
|
||||||
|
Depuis avril 2026 (v3 du pipeline GrasBot), **il n'y a plus d'étape
|
||||||
|
`index_vault.py`**. Le vault Obsidian est la seule source de vérité : il
|
||||||
|
est lu directement par `llm-api/search.py` au démarrage de l'API.
|
||||||
|
|
||||||
|
## Scripts
|
||||||
|
|
||||||
|
| Fichier | Rôle |
|
||||||
|
|---------|------|
|
||||||
|
| `extract-api-data.js` | **Node**. Fetch des endpoints Strapi → JSON brut (`extract/raw/`). |
|
||||||
|
| `clean-api-data.js` | **Node**. Nettoyage / normalisation (`extract/clean-data/`). |
|
||||||
|
| `generate-docs.js` | **Node**. Génération de `.md` par entrée Strapi (`docs/`). |
|
||||||
|
| `build-vault.py` | **Python**. Lit `docs/` + PDF CV → vault Obsidian (`vault-grasbot/`) avec frontmatter enrichi (aliases, answers, priority). |
|
||||||
|
| `update-documentation.js` | **Node**. MAJ incrémentale de la doc. |
|
||||||
|
| `analyse-site-architecture.js` | **Node**. Analyse d'architecture du site. |
|
||||||
|
|
||||||
|
## Commande type
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Depuis la racine du repo
|
||||||
|
node strapi_extraction/extract-api-data.js
|
||||||
|
node strapi_extraction/clean-api-data.js
|
||||||
|
node strapi_extraction/generate-docs.js
|
||||||
|
python strapi_extraction/build-vault.py
|
||||||
|
# (plus d'étape d'indexation — GrasBot lit le vault directement)
|
||||||
|
|
||||||
|
# Si GrasBot tourne déjà, recharger le vault sans redémarrer uvicorn :
|
||||||
|
curl -X POST http://localhost:8000/reload-vault
|
||||||
|
```
|
||||||
|
|
||||||
|
## Données générées
|
||||||
|
|
||||||
|
- `strapi_extraction/extract/raw/*.json` — données Strapi brutes.
|
||||||
|
- `strapi_extraction/extract/clean-data/*.json` — données nettoyées.
|
||||||
|
- `strapi_extraction/docs/*.md` — documentation Markdown.
|
||||||
|
- `strapi_extraction/docs/generation-summary.json` — résumé de génération.
|
||||||
|
- `vault-grasbot/**/*.md` — vault Obsidian consommé par le retrieval GrasBot.
|
||||||
|
|
||||||
|
Ces fichiers peuvent être **régénérés** à tout moment ; ne pas les considérer
|
||||||
|
comme source de vérité sans comparer au CMS.
|
||||||
|
|
||||||
|
## Fragilités actuelles
|
||||||
|
|
||||||
|
1. **`clean-api-data.js` n'a pas de cleaner `homepages`** : du coup
|
||||||
|
`generate-docs.js` ne produit jamais `00-homepage.md`. Conséquence :
|
||||||
|
le CV de la page d'accueil n'arrive pas dans `vault-grasbot/30-Parcours/`
|
||||||
|
via la chaîne automatique (il y est aujourd'hui via le PDF séparé
|
||||||
|
`nouveauCV_grascalvet.pdf`).
|
||||||
|
2. **`glossaire` n'est extrait ni nettoyé** : endpoint absent de la liste
|
||||||
|
`ENDPOINTS` dans `extract-api-data.js`, cleaner absent aussi. Le dossier
|
||||||
|
`vault-grasbot/40-Glossaire/` reste vide tant que ce n'est pas réparé.
|
||||||
|
3. **Accès Strapi v5 flat** : les scripts accèdent en direct à `project.name`,
|
||||||
|
`project.Resum`, etc. Si on repasse à une configuration v4 ou "populated
|
||||||
|
with wrapper", il faudra rebrancher en `project.attributes.name`.
|
||||||
|
|
||||||
|
Ces points seront corrigés en même temps que l'enrichissement du vault
|
||||||
|
(glossaire + homepage Strapi → notes `40-Glossaire/` et `30-Parcours/`).
|
||||||
|
|
||||||
|
## Liens complémentaires
|
||||||
|
|
||||||
|
- Vault + retrieval : [`08-vault-obsidian-retrieval.md`](./08-vault-obsidian-retrieval.md)
|
||||||
|
- API LLM : [`04-api-llm-et-chatbot.md`](./04-api-llm-et-chatbot.md)
|
||||||
|
- Schémas Strapi : [`03-cms-strapi.md`](./03-cms-strapi.md)
|
||||||
@ -0,0 +1,24 @@
|
|||||||
|
# Référence visuelle (captures)
|
||||||
|
|
||||||
|
**Dernière mise à jour :** 2026-04-01 — jeu de captures WebP déposé (ID 01–05, 07–21 ; ID 06 optionnel ignoré).
|
||||||
|
|
||||||
|
## Objectif
|
||||||
|
|
||||||
|
Conserver un **contexte visuel** aligné sur le code. Les images validées sont dans `captures/`.
|
||||||
|
|
||||||
|
## Documents
|
||||||
|
|
||||||
|
- [captures/INDEX.md](./captures/INDEX.md) — liste numérotée des captures, noms de fichiers, priorités, cases « Présent ».
|
||||||
|
- [captures/README.md](./captures/README.md) — conventions de nommage, mise à jour, confidentialité.
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
Voir `.cursor/skills/site-portfolio-evolution/SKILL.md` : réflexion → choix → modification → validation → si échec retour Git → si OK mise à jour doc et captures concernées.
|
||||||
|
|
||||||
|
## Activation du skill dans Cursor
|
||||||
|
|
||||||
|
Le fichier est versionné dans le dépôt. Si besoin, ajoutez le dossier aux **Agent Skills** de Cursor ou copiez le skill dans votre répertoire skills personnel pour qu’il soit proposé automatiquement.
|
||||||
|
|
||||||
|
## Pour l’assistant
|
||||||
|
|
||||||
|
Lors d’un chantier UI, **@mentionner** les fichiers dans `captures/` ou citer l’**ID** de l’index (ex. ID 13 — fiche compétence desktop).
|
||||||
@ -0,0 +1,230 @@
|
|||||||
|
# Vault Obsidian + retrieval GrasBot (v3 — graph + BM25)
|
||||||
|
|
||||||
|
**Créé :** 2026-04-22 (v1 RAG vectoriel)
|
||||||
|
**Refondu :** 2026-04-22 (v3 — graph + BM25, sans embeddings)
|
||||||
|
**Statut :** opérationnel (17 projets + 4 compétences + CV + 3 notes techniques + 15 MOCs)
|
||||||
|
|
||||||
|
## Raison d'être
|
||||||
|
|
||||||
|
Avant ce pipeline, GrasBot interrogeait `mistral:7b` sans aucun contexte —
|
||||||
|
il répondait de manière générique sur n'importe quoi. Depuis :
|
||||||
|
|
||||||
|
- **Modèle chat** : `qwen3:8b` (meilleur en FR, reasoning solide).
|
||||||
|
- **Base de connaissance** structurée comme vault Obsidian.
|
||||||
|
- **Pipeline de retrieval** branché : chaque question récupère les notes
|
||||||
|
pertinentes avant génération.
|
||||||
|
|
||||||
|
## Pourquoi `graph + BM25` plutôt que RAG vectoriel ?
|
||||||
|
|
||||||
|
La première version (avril 2026, v2) utilisait **ChromaDB** + embeddings
|
||||||
|
**nomic-embed-text**. Ça marchait, mais :
|
||||||
|
|
||||||
|
- **Vault de taille modeste** (~40 notes, ~100 Ko) : la sémantique vectorielle
|
||||||
|
sur-dimensionne le problème.
|
||||||
|
- **Retrieval imprévisible** sur vocabulaire précis (une question *« compétences
|
||||||
|
en IA »* pouvait ne pas remonter la note `ia.md` si son embedding était
|
||||||
|
dominé par d'autres concepts).
|
||||||
|
- **Chaîne d'installation lourde** : `chromadb` dépend de `chroma-hnswlib`,
|
||||||
|
qui nécessite un compilateur C++ sous Windows → blocage fréquent.
|
||||||
|
- **Coût en VRAM** : `nomic-embed-text` mobilisait ~500 Mo et ~1 s par
|
||||||
|
requête, inutile à cette échelle.
|
||||||
|
- **Désynchronisation vault / index** : étape `index_vault.py` oubliable.
|
||||||
|
|
||||||
|
En v3, on exploite directement la **structure** du vault : frontmatter YAML
|
||||||
|
(aliases, answers, domains, tags, priority), wikilinks, MOCs. Le retrieval
|
||||||
|
est **déterministe**, **traçable** (on sait *pourquoi* une note est remontée),
|
||||||
|
**instantané** (~50 ms), et ne demande qu'une dépendance : `pyyaml`.
|
||||||
|
|
||||||
|
Résultat : GrasBot cite toujours ses sources, et le top-5 est beaucoup
|
||||||
|
plus prévisible pour une question précise.
|
||||||
|
|
||||||
|
## Vault — `vault-grasbot/`
|
||||||
|
|
||||||
|
Arborescence :
|
||||||
|
|
||||||
|
```
|
||||||
|
vault-grasbot/
|
||||||
|
├── 00-MOC/ # hubs thématiques (MOC-Projets, MOC-Ia, MOC-Technique, ...)
|
||||||
|
├── 10-Projets/ # 17 projets Strapi (push-swap, minishell, ft-transcendence, ...)
|
||||||
|
├── 20-Competences/ # 4 compétences Strapi (IA, domotique, web, 3D)
|
||||||
|
├── 30-Parcours/ # CV curaté manuellement (source: manual)
|
||||||
|
├── 40-Glossaire/ # (vide, prévu pour le content-type glossaire Strapi)
|
||||||
|
├── 50-Technique/ # auto-doc : architecture-site, grasbot-retrieval, vault-structure
|
||||||
|
├── README.md # résumé utilisateur (généré)
|
||||||
|
└── TAXONOMIE.md # vocabulaire contrôlé (domaines, tags, aliases, answers, priority)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Frontmatter YAML
|
||||||
|
|
||||||
|
Chaque note porte une en-tête enrichie :
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
title: "push_swap"
|
||||||
|
slug: push-swap
|
||||||
|
type: projet # projet | competence | parcours | moc | technique
|
||||||
|
source: strapi/projects # strapi/... | pdf/... | manual | vault/generated
|
||||||
|
domains: [algorithmique, c, ecole-42]
|
||||||
|
tags: [42-commun, tri, makefile]
|
||||||
|
aliases:
|
||||||
|
- push swap
|
||||||
|
- push_swap
|
||||||
|
- algo de tri 42
|
||||||
|
answers:
|
||||||
|
- "Parle-moi de push-swap"
|
||||||
|
- "Comment fonctionne push-swap ?"
|
||||||
|
priority: 5 # 1..10, boost léger au scoring
|
||||||
|
linked: ["[[MOC-Projets]]"]
|
||||||
|
related: ["[[minishell]]"]
|
||||||
|
updated: 2026-04-22
|
||||||
|
visibility: public
|
||||||
|
---
|
||||||
|
```
|
||||||
|
|
||||||
|
Détail des champs et leur usage exact par le retrieval : voir
|
||||||
|
`vault-grasbot/TAXONOMIE.md` et la note interne
|
||||||
|
[[vault-structure]] du vault.
|
||||||
|
|
||||||
|
### Règle de régénération
|
||||||
|
|
||||||
|
`strapi_extraction/build-vault.py` **écrase** les notes dont `source:
|
||||||
|
strapi/*` ou `source: pdf/*`. Il **ne touche jamais** aux notes
|
||||||
|
`source: manual`.
|
||||||
|
|
||||||
|
Le drapeau `--clean` supprime tout le vault avant régénération : à utiliser
|
||||||
|
uniquement si on veut repartir de zéro (attention aux notes `manual`).
|
||||||
|
|
||||||
|
## Génération — `strapi_extraction/build-vault.py`
|
||||||
|
|
||||||
|
Pipeline :
|
||||||
|
|
||||||
|
1. Lit les `project-*.md` et `competence-*.md` de `strapi_extraction/docs/`
|
||||||
|
(eux-mêmes produits par `generate-docs.js` à partir de l'API Strapi).
|
||||||
|
2. Parse titre, slug, description, détails.
|
||||||
|
3. Infère `domains` / `tags` via `DOMAIN_KEYWORDS` / `TAG_KEYWORDS`
|
||||||
|
(ajustables dans le script).
|
||||||
|
4. **Génère automatiquement** :
|
||||||
|
- `aliases` à partir du slug + titre + `DOMAIN_ALIASES` (synonymes
|
||||||
|
courants par domaine).
|
||||||
|
- `answers` selon le type (projet → *« Parle-moi de X »*, compétence →
|
||||||
|
*« Quelles sont ses compétences en X ? »*, etc.).
|
||||||
|
- `priority` heuristique (CV=10, MOCs=7, compétences=7, projets=5).
|
||||||
|
5. Calcule les `related` par intersection de domaines (top 3).
|
||||||
|
6. Écrit chaque note avec frontmatter + corps + section *« Liens »* en pied.
|
||||||
|
7. Génère les MOCs (un par type + un par domaine significatif).
|
||||||
|
8. Optionnel : convertit le CV PDF via `pypdf` si installé (mais la version
|
||||||
|
manuelle `cv-grascalvet-fernand.md` avec `source: manual` est
|
||||||
|
**toujours préservée**).
|
||||||
|
|
||||||
|
Commandes :
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python strapi_extraction/build-vault.py # régénère tout
|
||||||
|
python strapi_extraction/build-vault.py --dry-run # liste sans écrire
|
||||||
|
python strapi_extraction/build-vault.py --clean # supprime puis regénère
|
||||||
|
```
|
||||||
|
|
||||||
|
## Retrieval — `llm-api/search.py`
|
||||||
|
|
||||||
|
Module lu par `api.py`. Fournit :
|
||||||
|
|
||||||
|
- `load_vault()` — lecture mémoïsée du vault (frontmatter YAML + body +
|
||||||
|
wikilinks). Filtre `visibility: private`.
|
||||||
|
- `tokenize_fr(text)` — tokenisation FR + normalisations
|
||||||
|
(`c++` → `cpp`, split sur `-`/`_`, stop-words).
|
||||||
|
- `score_note(note, query, tokens, stats)` — score déterministe
|
||||||
|
multi-signaux. Retourne un `ScoredNote(score, reasons[])`.
|
||||||
|
- `expand_by_graph(seeds, vault)` — ajoute les voisins (`linked`,
|
||||||
|
`related`, wikilinks du body) avec un score dérivé de 60 %.
|
||||||
|
- `search(query, top_k)` — orchestration : score + expansion + dedupe +
|
||||||
|
top-K.
|
||||||
|
- `build_prompt(query, notes)` — couple `(system, user)` pour `/api/chat`.
|
||||||
|
- `generate(system, user)` — appel Ollama `/api/chat`, retourne le texte.
|
||||||
|
- `answer(query)` — pipeline complet, retourne un dict
|
||||||
|
`{response, sources, grounded, model, vault_size}`.
|
||||||
|
|
||||||
|
### Barème de scoring (documentation opérationnelle)
|
||||||
|
|
||||||
|
| Signal | Points | Détails |
|
||||||
|
|---|---|---|
|
||||||
|
| Alias match | +10 | 1+ aliases de la note apparaissent dans la question |
|
||||||
|
| Title exact | +8 | Titre complet dans la query (len ≥ 4) |
|
||||||
|
| Title tokens | +4 | Au moins 2 tokens du titre dans la query |
|
||||||
|
| Slug | +8 | Tous les tokens du slug sont dans la query |
|
||||||
|
| Answers full | +12 | ≥ 3 tokens communs avec une question-type |
|
||||||
|
| Answers partial | +5 | 2 tokens communs |
|
||||||
|
| Domains | +5 × n | Par domaine strictement matché |
|
||||||
|
| Tags | +3 × n | Par tag strictement matché |
|
||||||
|
| BM25 body | 0..5 | Normalisé |
|
||||||
|
| Priority | (p-5) × 0.3 | Boost léger si déjà scoré |
|
||||||
|
| MOC-hub | +1.0 | Si note de type `moc` ET déjà scorée |
|
||||||
|
| Graph neighbor | 60 % du parent | Via `expand_by_graph` |
|
||||||
|
|
||||||
|
Seuil `SEARCH_MIN_SCORE` (défaut 1.0) : en-dessous, le mode *« sans
|
||||||
|
contexte pertinent »* se déclenche et Qwen3 est invité à ne pas inventer
|
||||||
|
de faits sur Fernand.
|
||||||
|
|
||||||
|
## Compatibilité rétro
|
||||||
|
|
||||||
|
L'API garde la signature `GET /ask?q=...`. Le JSON renvoyé a :
|
||||||
|
|
||||||
|
- `response` (conservé, consommé par `askAI.js`)
|
||||||
|
- `sources[]` (enrichi : `slug`, `title`, `type`, `score`, `reasons`, `url`)
|
||||||
|
- `grounded` (bool — nouveau)
|
||||||
|
- `model` (conservé)
|
||||||
|
- `vault_size` (nouveau)
|
||||||
|
|
||||||
|
Le champ `rag` de la v2 est remplacé par `grounded` (plus explicite).
|
||||||
|
|
||||||
|
## Commandes utiles
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Régénérer le vault depuis strapi_extraction/docs/
|
||||||
|
python strapi_extraction\build-vault.py
|
||||||
|
|
||||||
|
# Démarrer l'API locale (pas d'indexation préalable à faire)
|
||||||
|
cd llm-api ; uvicorn api:app --host 0.0.0.0 --port 8000
|
||||||
|
|
||||||
|
# Vérifier la config active et la taille du vault
|
||||||
|
curl http://localhost:8000/health
|
||||||
|
|
||||||
|
# Forcer la relecture du vault sans redémarrer uvicorn
|
||||||
|
curl -X POST http://localhost:8000/reload-vault
|
||||||
|
|
||||||
|
# Tester une question en direct
|
||||||
|
curl "http://localhost:8000/ask?q=parle-moi+de+push-swap"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fusion avec un vault Obsidian perso
|
||||||
|
|
||||||
|
Deux voies :
|
||||||
|
|
||||||
|
- **Vault séparé** (recommandé au début) : on ouvre `vault-grasbot/` comme
|
||||||
|
vault Obsidian indépendant.
|
||||||
|
- **Fusion** : on copie `vault-grasbot/` comme sous-dossier d'un vault
|
||||||
|
existant. Les wikilinks restent valides tant que les noms sont uniques.
|
||||||
|
Les notes persos doivent porter `source: manual` (évite l'écrasement par
|
||||||
|
`build-vault.py`) et `visibility: private` (exclues automatiquement du
|
||||||
|
retrieval par `load_vault()`).
|
||||||
|
|
||||||
|
## Limites actuelles
|
||||||
|
|
||||||
|
- **Pas de mémoire conversationnelle** : chaque question est indépendante.
|
||||||
|
- **Pas de streaming** : la réponse arrive en un bloc après 2-10 s.
|
||||||
|
- **Aliases / answers auto-générés** : c'est une base. Les notes
|
||||||
|
stratégiques (CV, IA, MOCs) méritent un enrichissement manuel en
|
||||||
|
passant `source: manual`.
|
||||||
|
- **`clean-api-data.js` n'extrait pas les `homepages` ni les `glossaires`** :
|
||||||
|
bug préexistant, à corriger pour enrichir `40-Glossaire/` et la home.
|
||||||
|
- **Re-chargement manuel** via `POST /reload-vault` (pas encore automatisé
|
||||||
|
via file watcher).
|
||||||
|
|
||||||
|
## Évolutions priorisables
|
||||||
|
|
||||||
|
1. Corriger `clean-api-data.js` (homepages + glossaires).
|
||||||
|
2. Afficher les `sources` citées sous la réponse dans `ChatBot.js`.
|
||||||
|
3. Ajouter un badge `grounded` pour informer le visiteur de la confiance.
|
||||||
|
4. Historique conversationnel court (3-4 tours).
|
||||||
|
5. Streaming Ollama `stream: true` (Server-Sent Events côté API).
|
||||||
|
6. File watcher sur `vault-grasbot/` qui appelle `POST /reload-vault`
|
||||||
|
automatiquement.
|
||||||
61
obsidian-site-docs/docs-site-interne/README.md
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
# Documentation interne du site
|
||||||
|
|
||||||
|
**Dernière mise à jour :** 2026-04-24
|
||||||
|
|
||||||
|
> **Dans le pack Obsidian** — Même contenu que le dépôt `my-next-site/docs-site-interne/`. Fichiers à la racine de ce coffre : [CONFIGURATION_SITE.md](../CONFIGURATION_SITE.md) · [00 Hub.md](../00%20Hub.md) · [SYNC-DOC.md](../SYNC-DOC.md) · [README-racine-depot.md](../README-racine-depot.md).
|
||||||
|
|
||||||
|
Ce dossier décrit l'architecture, le fonctionnement et les décisions du projet (Next.js + Strapi + FastAPI/Ollama). Il est destiné à l'équipe et à l'assistant IA pour retrouver vite le contexte.
|
||||||
|
|
||||||
|
## Relation avec les autres fichiers
|
||||||
|
|
||||||
|
| Fichier / zone | Rôle |
|
||||||
|
|----------------|------|
|
||||||
|
| [README-racine-depot.md](../README-racine-depot.md) | Panorama GitHub (copie du `README` racine du dépôt). |
|
||||||
|
| [CONFIGURATION_SITE.md](../CONFIGURATION_SITE.md) | Guide opérationnel : ports, commandes, dépannage, planificateur de tâches Windows. |
|
||||||
|
| Dossier parent [obsidian-site-docs/](../) | Pack Obsidian (commandes, hub, ce module doc). |
|
||||||
|
| Ce dossier (`docs-site-interne/`) | Conception : flux de données, schémas CMS, incohérences connues, feuille de route. |
|
||||||
|
|
||||||
|
**Règle de maintenance :** éditer d’abord le **dépôt** Git, puis [resynchroniser ce dossier](../SYNC-DOC.md) ; si les ports changent, mettre à jour [CONFIGURATION_SITE.md](../CONFIGURATION_SITE.md).
|
||||||
|
|
||||||
|
## Index des documents
|
||||||
|
|
||||||
|
| Fichier | Contenu |
|
||||||
|
|---------|---------|
|
||||||
|
| [01-architecture.md](./01-architecture.md) | Services, ports, flux. |
|
||||||
|
| [02-frontend-next.md](./02-frontend-next.md) | App Router, routes, fetch Strapi. |
|
||||||
|
| [03-cms-strapi.md](./03-cms-strapi.md) | Content-types Strapi. |
|
||||||
|
| [04-api-llm-et-chatbot.md](./04-api-llm-et-chatbot.md) | FastAPI, Ollama, GrasBot. |
|
||||||
|
| [05-environnement-scripts.md](./05-environnement-scripts.md) | Env, scripts PowerShell. |
|
||||||
|
| [06-strapi-extraction.md](./06-strapi-extraction.md) | Outils `strapi_extraction/`. |
|
||||||
|
| [07-reference-visuelle-captures.md](./07-reference-visuelle-captures.md) | Référence visuelle ; dossier `captures/`. |
|
||||||
|
| [08-vault-obsidian-retrieval.md](./08-vault-obsidian-retrieval.md) | Vault GrasBot + pipeline de retrieval graph + BM25 (v3, sans embeddings). |
|
||||||
|
| [captures/INDEX.md](./captures/INDEX.md) | Inventaire des captures WebP (noms réels, slugs, priorités). |
|
||||||
|
| [etat-actuel.md](./etat-actuel.md) | État et dette technique. |
|
||||||
|
| [feuille-de-route.md](./feuille-de-route.md) | Backlog priorisé. |
|
||||||
|
| [REFONTE-VISUELLE.md](./REFONTE-VISUELLE.md) | Journal de bord de la refonte UI Stitch. |
|
||||||
|
| [contact-flow.md](./contact-flow.md) | Contact : e-mail Brevo (remplacement du stockage Strapi). |
|
||||||
|
| [langfuse-observability.md](./langfuse-observability.md) | Langfuse : traces GrasBot, tuning pipeline. |
|
||||||
|
|
||||||
|
## Arborescence utile
|
||||||
|
|
||||||
|
```
|
||||||
|
my-next-site/
|
||||||
|
├── app/
|
||||||
|
├── cmsbackend/
|
||||||
|
├── llm-api/
|
||||||
|
├── strapi_extraction/
|
||||||
|
├── start-my-site.ps1
|
||||||
|
├── stop-my-site.ps1
|
||||||
|
├── next.config.ts
|
||||||
|
├── CONFIGURATION_SITE.md
|
||||||
|
├── obsidian-site-docs/ # pack Obsidian (optionnel)
|
||||||
|
└── docs-site-interne/
|
||||||
|
├── captures/ # screenshots de référence (voir INDEX.md)
|
||||||
|
└── ...
|
||||||
|
```
|
||||||
|
|
||||||
|
**Workflow d’évolution (doc + captures + Git) :** skill Cursor `.cursor/skills/site-portfolio-evolution/SKILL.md`.
|
||||||
|
|
||||||
|
## Reprise d’une session de travail
|
||||||
|
|
||||||
|
Pour enchaîner après une pause : lire `feuille-de-route.md` (priorités), `etat-actuel.md`, puis `captures/INDEX.md` si le travail touche l’UI. Le skill `site-portfolio-evolution` rappelle la boucle modification → validation → mise à jour doc.
|
||||||
419
obsidian-site-docs/docs-site-interne/REFONTE-VISUELLE.md
Normal file
@ -0,0 +1,419 @@
|
|||||||
|
# Refonte visuelle — Direction "Digital Atelier"
|
||||||
|
|
||||||
|
**Créé :** 2026-04-22
|
||||||
|
**Statut :** terminé — 8/8 étapes (2026-04-22)
|
||||||
|
**Source d'inspiration :** `stitch_V1/` (design newsletter Stitch — `DESIGN.md` et `code.html`).
|
||||||
|
**Audit préalable :** [`captures/AUDIT-VISUEL.md`](./captures/AUDIT-VISUEL.md).
|
||||||
|
|
||||||
|
## 1. Règle de garde-fou (validée utilisateur)
|
||||||
|
|
||||||
|
> **On emprunte au Stitch la direction artistique** (palette, typographie, layering tonal, radius, ombres ambient) **et deux ou trois composants signatures** (frame image, pull-quote, bouton jewel). **On n'emprunte ni la mise en page en colonne unique, ni la bottom nav, ni le rythme vertical newsletter.** Le **wallpaper** et le couple **header / drawer mobile** du site restent la fondation.
|
||||||
|
|
||||||
|
Concrètement, `stitch_V1/DESIGN.md` fait foi (système). `stitch_V1/code.html` sert d'illustration : il ne fait pas foi quand il contredit `DESIGN.md` (ex. il utilise des `border border-outline-variant` que DESIGN.md interdit — règle "No-Line").
|
||||||
|
|
||||||
|
Chaque commit de refonte doit être relisable à l'aune de cette règle : s'il introduit une colonne unique `max-w-xl` globale, une bottom nav ou des bordures 1px opaques, il est à corriger.
|
||||||
|
|
||||||
|
## 2. Arbitrages actés
|
||||||
|
|
||||||
|
| Sujet | Décision |
|
||||||
|
|-------|----------|
|
||||||
|
| Photo de profil home | Portrait carré arrondi `rounded-sheet` (1.5 rem) + frame `bg-primary p-1` |
|
||||||
|
| Listes portfolio / compétences | Grille asymétrique 2/3 + 1/3 ; carousel réservé aux galeries intra-fiche |
|
||||||
|
| Orbitron | Retiré partout, remplacé par `Manrope` (titres) + `Newsreader` (corps) |
|
||||||
|
| Opacité cartes sur wallpaper | 85 % + `backdrop-blur-vellum` (≈ 20 px) pour la "sheet of vellum" |
|
||||||
|
| Icônes | `Material Symbols Outlined` (déjà utilisées dans la newsletter Stitch et Listmonk) |
|
||||||
|
| Mode sombre | Light-only pour cette refonte |
|
||||||
|
| Cercles animés `circle-one` / `circle-two` | Repalette vers `primary` / `primary-container` (au lieu de rose/indigo) |
|
||||||
|
| Compteur de visites | Migré dans le footer, en `text-[10px] uppercase tracking-[0.3em] text-outline` |
|
||||||
|
|
||||||
|
## 3. Design tokens portés dans `tailwind.config.ts`
|
||||||
|
|
||||||
|
Voir le fichier pour la liste exhaustive. Rappel des plus utilisés :
|
||||||
|
|
||||||
|
| Token | Hex | Usage |
|
||||||
|
|-------|-----|-------|
|
||||||
|
| `primary` | `#26445d` | CTAs, headlines, frames image |
|
||||||
|
| `primary-container` | `#3e5c76` | dégradé CTA, drawer mobile |
|
||||||
|
| `primary-fixed` | `#cce5ff` | pastilles, badges, barres de citation |
|
||||||
|
| `secondary` | `#516169` | sous-titres, méta |
|
||||||
|
| `surface` | `#f8fafa` | base de page alternative au wallpaper |
|
||||||
|
| `surface-container-low` | `#f2f4f4` | sections secondaires |
|
||||||
|
| `surface-container-lowest` | `#ffffff` | cartes principales (posées à 85 % sur wallpaper) |
|
||||||
|
| `on-surface` | `#191c1d` | texte principal (jamais `#000`) |
|
||||||
|
| `outline-variant` | `#c3c7cd` | ghost-border à 15 % d'opacité max |
|
||||||
|
|
||||||
|
Radius additifs : `rounded-sheet` (1.5 rem) pour cartes principales, `rounded-tile` (1 rem) pour éléments imbriqués. Les radius Tailwind (`rounded-xl`, etc.) ne sont pas écrasés pour ne pas casser les composants existants.
|
||||||
|
|
||||||
|
Ombres : `shadow-ambient` (40 px / 6 %) pour les cartes flottantes, `shadow-jewel` (4 px offset) pour CTAs primaires.
|
||||||
|
|
||||||
|
Polices : `font-headline` (Manrope) et `font-body` (Newsreader), importées via `app/globals.css`.
|
||||||
|
|
||||||
|
## 4. Plan d'exécution (8 étapes)
|
||||||
|
|
||||||
|
Chaque étape = un lot cohérent + éventuelle mise à jour de `captures/AUDIT-VISUEL.md` et nouvelles captures.
|
||||||
|
|
||||||
|
| # | Étape | Fichiers principaux | Statut |
|
||||||
|
|---|-------|---------------------|--------|
|
||||||
|
| 1 | Fondations : tokens Tailwind + import polices + icônes | `tailwind.config.ts`, `app/globals.css` | **fait** (2026-04-22) |
|
||||||
|
| 2 | Garde-fou doc + mise à jour feuille de route | `REFONTE-VISUELLE.md`, `feuille-de-route.md` | **fait** (2026-04-22) |
|
||||||
|
| 3 | Migration typographique globale (Orbitron → Manrope / Newsreader) | `app/**/*.{tsx,jsx,js}`, `app/assets/main.css` | **fait** (2026-04-22) |
|
||||||
|
| 4 | Layout racine : header No-Line, burger ghost, palette cercles, compteur migré, drawer | `app/layout.tsx`, `app/components/NavLink.jsx`, `app/components/Footer.jsx` | **fait** (2026-04-22) |
|
||||||
|
| 5 | Home : hero vellum, portrait frame, takeaways, pull-quote, CTAs | `app/page.tsx` | **fait** (2026-04-22) |
|
||||||
|
| 6 | Listes portfolio + compétences : grille asymétrique, cartes éditoriales | `app/portfolio/page.jsx`, `app/competences/page.jsx`, composants `Carousel*` | **fait** (2026-04-22) |
|
||||||
|
| 7 | Fiches détail + modale glossaire + GrasBot (jewel flottant) | `app/portfolio/[slug]/page.tsx`, `app/competences/[slug]/page.tsx`, `app/components/ModalGlossaire.tsx`, `app/components/ChatBot.js` | **fait** (2026-04-22) |
|
||||||
|
| 8 | Contact + Footer éditorial | `app/contact/page.js`, `app/components/ContactForm.tsx`, `app/components/Footer.jsx` | **fait** (2026-04-22) |
|
||||||
|
|
||||||
|
## 4 bis. Correctif post-étape 3 (2026-04-22) — cohérence desktop/mobile
|
||||||
|
|
||||||
|
Après l'étape 3, retour utilisateur : **couleurs de texte différentes** entre desktop et mobile.
|
||||||
|
|
||||||
|
**Cause** : le template Next de base définissait dans `globals.css` un bloc `@media (prefers-color-scheme: dark)` qui basculait `--foreground` à `#ededed` (texte clair) selon le **thème système** de chaque appareil. Avant l'étape 3, les classes `.font-orbitron-*` forçaient `color: #333333` partout et masquaient ce mode sombre. En les retirant, la variable `--foreground` a pris effet et le rendu est devenu dépendant du thème OS (Windows clair → texte foncé ; mobile sombre → texte clair quasi invisible sur wallpaper clair).
|
||||||
|
|
||||||
|
**Fix** :
|
||||||
|
|
||||||
|
- Retrait du bloc `@media (prefers-color-scheme: dark)` dans `app/globals.css` (incohérent avec l'arbitrage "light-only").
|
||||||
|
- `--foreground` figé à `#191c1d` (= `on-surface` Stitch, jamais `#000`).
|
||||||
|
- `body.color` fixé à `#191c1d` en dur pour ne plus dépendre d'aucune variable conditionnelle.
|
||||||
|
- Classes Tailwind invalides `text-black-500` / `text-black-700` (qui n'existent pas et ne rendaient donc aucune couleur) remplacées par `text-gray-700` dans `app/layout.tsx`, `app/page.tsx`, `app/components/ContentSectionCompetences.tsx`.
|
||||||
|
|
||||||
|
**Leçon retenue (à appliquer aux étapes suivantes)** : quand on supprime un "masque" CSS (comme la couleur forcée d'Orbitron), toujours vérifier que la valeur qui va ré-émerger par héritage est bien la valeur attendue, pas une variable dépendante du contexte d'exécution.
|
||||||
|
|
||||||
|
## 4 ter. Correctif urgent modale glossaire (2026-04-22) — blocage mobile
|
||||||
|
|
||||||
|
Après l'étape 4, retour utilisateur sur Samsung S25 Ultra : les mots-clés du glossaire (compétences) ouvrent bien la modale mais **la modale déborde de l'écran**, **la croix de fermeture est hors champ**, impossible de refermer sans recharger la page.
|
||||||
|
|
||||||
|
**Causes identifiées** dans `app/components/ModalGlossaire.tsx` (pré-existantes avant la refonte) :
|
||||||
|
|
||||||
|
- Carte interne en `w-[114vw] max-w-6xl` : force une largeur > viewport sur mobile (114 % de 400 px = 456 px dans une fenêtre de 400 px), et sur desktop la contrainte est masquée par `max-w-6xl`.
|
||||||
|
- Hauteur figée `h-[72vh]` sans scroll interne : le contenu est simplement tronqué quand il dépasse.
|
||||||
|
- Aucune fermeture au tap sur le voile, ni à Esc. Seule issue = bouton `✖` en haut à droite, hors champ sur mobile.
|
||||||
|
- Bouton de fermeture en `text-sm p-1` : zone tactile < 44 px, sous le seuil Material Design pour le tactile.
|
||||||
|
|
||||||
|
**Fix** (anticipe les besoins de l'étape 7) :
|
||||||
|
|
||||||
|
- Carte interne : `w-full max-w-4xl max-h-[90vh]` + padding 4 sur le voile pour la marge latérale sur mobile.
|
||||||
|
- Contenu intérieur en `overflow-y-auto` pour scroll interne si nécessaire.
|
||||||
|
- Voile cliquable pour fermer, `stopPropagation` sur la carte pour ne pas fermer en interagissant avec.
|
||||||
|
- Fermeture `Escape` via `keydown` global.
|
||||||
|
- Bouton de fermeture rond `h-10 w-10` avec Material Symbol `close`, focus-visible, position `absolute top-3 right-3`.
|
||||||
|
- Alignement palette Stitch : voile `bg-on-surface/75 backdrop-blur-sm`, carte `bg-surface-container-lowest/95 backdrop-blur-vellum shadow-ambient rounded-sheet`, titre `text-primary`, description en `font-body` serif (Newsreader) pour lisibilité, texte `text-on-surface-variant`.
|
||||||
|
- Ajout de `"use client"` (manquant).
|
||||||
|
- `role="dialog" aria-modal="true" aria-label={...}` sur le conteneur, `aria-label` explicite sur le bouton de fermeture.
|
||||||
|
|
||||||
|
Ce correctif concerne uniquement le composant `ModalGlossaire`. L'étape 7 reprendra la refonte globale de cette zone (cohérence visuelle avec les fiches détail) mais le blocage UX mobile est levé dès maintenant.
|
||||||
|
|
||||||
|
## 4 quater. Correctifs post-étape 5 (2026-04-22) — home
|
||||||
|
|
||||||
|
Retour utilisateur sur la home fraichement refaite. Trois points, trois causes distinctes :
|
||||||
|
|
||||||
|
### Icônes Material Symbols affichées comme texte littéral
|
||||||
|
|
||||||
|
Les `<span class="material-symbols-outlined">psychology</span>` affichaient **le mot "psychology"** dans la font par défaut au lieu du glyphe, rendant les takeaways illisibles (texte blanc sur fond bleu = juste du texte). La règle `.material-symbols-outlined` de `app/globals.css` déclarait bien `font-variation-settings`, `display`, `line-height`… mais pas `font-family: 'Material Symbols Outlined'`. L'import Google Fonts pose le `@font-face`, il ne pose pas automatiquement la `font-family` sur la classe — c'est au site de le faire.
|
||||||
|
|
||||||
|
**Fix** : ajout de la ligne `font-family: 'Material Symbols Outlined';` dans la règle. Impact : toutes les icônes du site (takeaways, burger, modale glossaire, CTAs hero, icônes CTAs des futures étapes) s'affichent désormais comme icônes.
|
||||||
|
|
||||||
|
### Pull-quote "Démarche" peu lisible sur wallpaper
|
||||||
|
|
||||||
|
La règle DESIGN.md §5 "Editorial Pull-Quote" dit *"no background card, let the typography breathe on the surface"*. Valide quand la surface de base est un `bg-surface #f8fafa` uni (Stitch newsletter). Chez nous la surface de base est un wallpaper photographique, donc *respirer dessus = se fondre dedans*.
|
||||||
|
|
||||||
|
**Fix** : adaptation contextuelle — carte vellum **légère** (`bg-surface-container-lowest/65 backdrop-blur-vellum rounded-tile`, padding réduit, pas de `shadow-ambient`) pour rester lisible sans uniformiser les 3 sections en cartes identiques. La barre gauche `border-l-4 border-primary` et la typo Newsreader italique sont conservées.
|
||||||
|
|
||||||
|
**Leçon** : les règles DESIGN.md sont un langage, pas un dogme. Elles supposent une surface de base uniforme. Chaque fois qu'on est sur wallpaper, vérifier si la règle reste applicable telle quelle ou si elle demande une adaptation (ici : carte légère plutôt que zéro carte).
|
||||||
|
|
||||||
|
### Espace excessif entre les 3 sections de la home
|
||||||
|
|
||||||
|
`gap-8` (32 px) entre les sections + `py-6 md:py-8` sur la pull-quote donnaient ~80 px d'air vertical entre "Trois axes" et "Démarche".
|
||||||
|
|
||||||
|
**Fix** : `gap-8` → `gap-5` sur le container racine (20 px), `py-6 md:py-8` retiré sur la pull-quote (désormais remplacé par le padding interne de sa nouvelle carte). Les paddings internes des cartes (hero `p-6 sm:p-8 md:p-10`, takeaways `p-6 sm:p-8`) sont conservés — l'espace de contenu n'était pas le problème.
|
||||||
|
|
||||||
|
## 4 sexies. Séparateurs `<hr>` invisibles dans le hero (2026-04-22)
|
||||||
|
|
||||||
|
Le CV rendu par `ReactMarkdown` contient des `---` Markdown convertis en `<hr>`. Par défaut Tailwind Typography les stylise en bordure 1 px `border-gray-300` + `my-8` (32 px). Sur notre carte vellum semi-transparente, cette bordure grise est quasi invisible sur le wallpaper, mais les 64 px de marge verticale (my-8 en haut **et** en bas) restent et donnent l'illusion d'un espace excessif entre les paragraphes du hero.
|
||||||
|
|
||||||
|
**Fix (Option B — barre décorative)** : on surcharge `prose-hr` pour transformer la règle en **petite pastille Stitch** centrée. Classes ajoutées sur le wrapper `ReactMarkdown` :
|
||||||
|
|
||||||
|
```
|
||||||
|
prose-hr:border-0 prose-hr:w-16 prose-hr:mx-auto
|
||||||
|
prose-hr:bg-primary/30 prose-hr:h-0.5 prose-hr:rounded-full
|
||||||
|
prose-hr:my-6
|
||||||
|
```
|
||||||
|
|
||||||
|
Résultat : une barre 64 × 2 px, couleur primaire à 30 % d'opacité, arrondie, avec 24 px de marge au lieu de 32 px. Le séparateur redevient un **signal visuel intentionnel** cohérent avec la palette Stitch, et l'espace perçu entre les paragraphes tombe à un niveau confortable sans perdre la structure éditoriale du CV.
|
||||||
|
|
||||||
|
**Alternatives considérées** : Option A (`prose-hr:hidden`, perd la structure), Option C (`prose-hr:my-4` seul, garde la bordure grise invisible — n'adresse pas la cause).
|
||||||
|
|
||||||
|
## 4 quinquies. Compatibilité Chrome Auto-Translate (2026-04-22)
|
||||||
|
|
||||||
|
Les icônes Material Symbols Outlined fonctionnent via **ligatures de font** : un `<span class="material-symbols-outlined">psychology</span>` n'affiche « psychology » qu'en fallback — si la font est chargée, la ligature transforme ce texte en glyphe « cerveau ». Google Chrome propose à l'utilisateur mobile de traduire automatiquement une page dès que sa langue par défaut n'est pas celle du document. Lorsque la traduction s'active, **Chrome réécrit le `textContent`** (« psychology » → « psychologie ») : la ligature ne correspond plus à aucun glyphe dans la font, l'icône redevient du texte brut, et les layouts se décalent.
|
||||||
|
|
||||||
|
**Règle permanente pour la refonte** : chaque `<span class="material-symbols-outlined">` doit porter **`translate="no"`** (attribut HTML). Pareil pour les éléments contenant un nom propre qui ne doit pas être déformé (titre du site, nom d'école « 42 », nom de ville, etc.). Le reste du contenu éditorial (CV, descriptions de projets, fiches compétences) reste traductible — la traduction automatique est un vrai plus pour un portfolio qu'on veut accessible à l'international.
|
||||||
|
|
||||||
|
Composant wrapper `<Icon>` qui pose automatiquement `translate="no"` envisagé comme DRY à long terme (hors scope actuel).
|
||||||
|
|
||||||
|
## 6. Étape 6 — Listes portfolio + compétences (2026-04-22)
|
||||||
|
|
||||||
|
Les deux pages liste étaient héritées du design avant refonte : cartes `bg-white/80 rounded-lg` à taille **fixe** (`w-80 h-96` sur portfolio, `max-w-xs…2xl` en cascade sur compétences), `hover:scale-105` qui débordait sous le header, **chaque vignette embarquait un `Swiper` autoplay** (cf. `Carousel.tsx` et `CarouselCompetences.tsx`) — bruit visuel constant, coût réseau (3-5 images × N cartes chargées d'emblée), et incohérence avec l'arbitrage acté § 2 *"carousel réservé aux galeries intra-fiche"*. Sur mobile, la largeur fixe 320 px de la carte portfolio débordait un viewport 360 px + padding.
|
||||||
|
|
||||||
|
### Direction Stitch appliquée
|
||||||
|
|
||||||
|
**Règle DESIGN.md §6 "No-Grid-Lock"** interdit la grille 3 colonnes symétrique. On adopte une grille **asymétrique 2/3 + 1/3** qui donne un rythme éditorial plutôt qu'un catalogue :
|
||||||
|
|
||||||
|
```
|
||||||
|
md:grid-cols-6, pattern de spans par index modulo 4 :
|
||||||
|
idx 0 → md:col-span-4 (vedette, 2/3)
|
||||||
|
idx 1 → md:col-span-2 (1/3)
|
||||||
|
idx 2 → md:col-span-2 (1/3)
|
||||||
|
idx 3 → md:col-span-4 (vedette, 2/3)
|
||||||
|
```
|
||||||
|
|
||||||
|
Sur `sm` on bascule en `grid-cols-2` classique (pas de `col-span` tablette pour garder 2 cartes par ligne), sur mobile `grid-cols-1` pleine largeur. Le même pattern est répliqué pour les skeletons de chargement → l'empreinte visuelle est stable pendant le fetch.
|
||||||
|
|
||||||
|
### Anatomie de carte "feuillet de vellum"
|
||||||
|
|
||||||
|
Toutes les cartes sont des `Link` pleine-carte (plus de `Link` imbriqué ambigu) avec :
|
||||||
|
|
||||||
|
- Wrapper : `rounded-sheet bg-surface-container-lowest/85 backdrop-blur-vellum shadow-ambient`, `group` pour propager le hover.
|
||||||
|
- Hover : `hover:-translate-y-0.5 hover:shadow-jewel` (lift subtil + empreinte tactile Stitch) remplace l'ancien `scale-105` qui débordait et cassait l'alignement de la grille.
|
||||||
|
- Média : `aspect-[4/3]` fixe (plus de hauteurs variables) + `overflow-hidden` + `object-cover` + `group-hover:scale-[1.03]` sur l'image (sensation vitrine, discret).
|
||||||
|
- Placeholder `material-symbols-outlined image` centré si pas d'image — `translate="no"` en place (§ 4 quinquies).
|
||||||
|
- Corps : kicker uppercase tracking-[0.3em] (« Projet » / « Compétence ») + titre Manrope extrabold `text-primary` + description Newsreader `text-on-surface-variant` clampée à 3 lignes (`line-clamp-3`, core Tailwind 3.4) pour homogénéiser les hauteurs.
|
||||||
|
- CTA tertiaire « Découvrir → » / « Explorer → » Manrope uppercase `text-primary`, avec flèche Material Symbols `arrow_forward` qui se décale à droite au hover (`group-hover:translate-x-1`). Icône `translate="no"`.
|
||||||
|
|
||||||
|
### États
|
||||||
|
|
||||||
|
- **Chargement** : 4 skeletons animés (`animate-pulse bg-surface-container-low/80`) suivant le même pattern de spans que la grille réelle → pas de saut de layout.
|
||||||
|
- **Vide** : carte centrée avec Material Symbol (`inbox` pour portfolio, `school` pour compétences) + message Newsreader italique. Remplace l'ancien `text-gray-500` orphelin.
|
||||||
|
|
||||||
|
### Ce que ça règle
|
||||||
|
|
||||||
|
- **Régression mobile** : `w-80 h-96` retiré, la carte prend la largeur de la colonne → plus de débordement S25 Ultra.
|
||||||
|
- **Bruit visuel** : `Swiper` autoplay retiré des listes, le scroll n'est plus concurrencé par 3-5 carousels qui tournent simultanément.
|
||||||
|
- **Poids réseau** : une image `loading="lazy"` par carte au lieu de toutes les images de toutes les galeries au chargement initial.
|
||||||
|
- **Hiérarchie** : les pages liste ont désormais un **en-tête éditorial** (kicker + titre + pitch) cohérent avec le hero de la home.
|
||||||
|
- **Cohérence Stitch** : palette `primary` / `on-surface-variant`, radius `rounded-sheet`, ombres `shadow-ambient` / `shadow-jewel`, typographie Manrope + Newsreader → alignement 1:1 avec la home.
|
||||||
|
|
||||||
|
### Correctif post-étape 6 — wallpaper sur-zoomé sur pages longues
|
||||||
|
|
||||||
|
Retour utilisateur une fois `/portfolio` en ligne : le wallpaper apparaît **beaucoup plus zoomé** sur les listes que sur la home, ce qui casse la cohérence visuelle entre les rubriques.
|
||||||
|
|
||||||
|
**Cause** : dans `app/layout.tsx`, la div `.bg-wallpaper` était posée en `absolute inset-0` **à l'intérieur** du conteneur grid `min-h-[100dvh]`. Sur la home, le contenu tient en ≈ 1 viewport → le conteneur fait ≈ 1 viewport de haut → `background-size: cover` cadre l'image à sa taille naturelle. Sur les listes portfolio / compétences (en-tête + grille 4+ cartes + footer), le conteneur atteint 2 à 3 viewports de haut → `cover` redimensionne l'image pour couvrir **toute cette hauteur**, ce qui la fait apparaître zoomée et décalée. Effet amplifié au scroll car le wallpaper défile avec la page.
|
||||||
|
|
||||||
|
**Fix** : sortir le wallpaper du conteneur grid et le passer en `fixed inset-0 z-0 pointer-events-none`. Il est désormais calé sur le **viewport**, garde ses dimensions naturelles indépendamment de la longueur de la page, et reste stable au scroll. Les cercles animés `circle-one` / `circle-two` restent en `absolute` dans le grid pour conserver le comportement de parallax léger au scroll.
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<div className="fixed inset-0 z-0 bg-wallpaper pointer-events-none" aria-hidden="true"></div>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Impact transversal** : corrige au passage le même problème latent sur toutes les autres pages longues (futures fiches détail, page contact si elle s'allonge, etc.) — plus besoin d'y repenser page par page.
|
||||||
|
|
||||||
|
### Points laissés pour l'étape 7
|
||||||
|
|
||||||
|
- Les composants `Carousel.tsx` et `CarouselCompetences.tsx` **ne sont pas touchés** (ils restent utilisés par les pages détail `[slug]/page.tsx`). La refonte visuelle de ces carousels (pagination, flèches, lightbox) se fera dans le lot 7 avec les fiches détail et la modale glossaire.
|
||||||
|
- Pas de filtre / tri côté liste pour l'instant (les items sont peu nombreux, `order` de Strapi suffit). À ré-évaluer si le catalogue grossit.
|
||||||
|
|
||||||
|
### Correctif post-étape 6 — réintroduction du défilement automatique en vignette
|
||||||
|
|
||||||
|
Premier retour utilisateur après l'étape 6 : *« j'ai perdu ma fonctionnalité précédente du carousel où les images des vignettes chargées depuis Strapi défilaient »*. L'arbitrage initial *"carousel réservé aux galeries intra-fiche"* (§ 2 — tableau d'arbitrages) était motivé par le bruit visuel et le poids réseau de **plusieurs `Swiper` autoplay** qui tournaient simultanément. Mais le défilement auto des images en vignette faisait partie intégrante de l'expérience de découverte du portfolio pour l'auteur. **L'arbitrage est donc révisé** : on conserve le défilement en vignette, mais via un composant **allégé et cadré** plutôt que le `Carousel.tsx` complet.
|
||||||
|
|
||||||
|
**Nouveau composant `app/components/VignetteCarousel.tsx`** — différences délibérées avec `Carousel.tsx` / `CarouselCompetences.tsx` :
|
||||||
|
|
||||||
|
- **Pas de flèches de navigation** (`Navigation` module non chargé). Les flèches créaient une **zone de clic ambiguë** avec le `<Link>` englobant la vignette : cliquer sur une flèche déclenchait la navigation vers la fiche détail au lieu de faire défiler le carousel. L'autoplay + le swipe tactile suffisent à l'échelle d'une vignette.
|
||||||
|
- **Pas de lightbox** (pas de `createPortal` ni de `selectedImage`). L'ouverture plein écran reste une signature de la **fiche détail**, pas de la liste.
|
||||||
|
- **Pagination bullets Stitch** : `--swiper-pagination-color: #26445d` (primary) et bullets inactifs blancs à 55 % d'opacité, taille 6 px. Surcharge inline via `style={...}` pour éviter de polluer `globals.css` avec un sélecteur `.swiper-pagination-bullet` global qui risquerait de toucher aussi les carousels de la fiche détail.
|
||||||
|
- **Autoplay 3500 ms** (vs 3000 ms historique) pour laisser plus de temps à la lecture sur les cartes vedette 2/3.
|
||||||
|
- **`loop` conditionnel** (`images.length > 1`) : sans ça Swiper loggait un warning quand une entrée Strapi n'avait qu'une seule image.
|
||||||
|
|
||||||
|
**Intégration dans les listes** : dans `app/portfolio/page.jsx` et `app/competences/page.jsx`, la logique est `length > 1 ? <VignetteCarousel /> : <img statique />` — identique à la version pré-refonte pour les entrées mono-image, plus performante pour les entrées multi-images. Les `alt` sont générés à partir de `img.name` Strapi avec fallback sur le nom du projet / compétence.
|
||||||
|
|
||||||
|
**Pourquoi ne pas avoir réutilisé `Carousel.tsx` tel quel** : il embarque flèches + lightbox + CSS de navigation. Dans le contexte d'un `<Link>` englobant, les flèches auraient conflit, et la lightbox serait inaccessible (capturée par le lien). Ajouter des `stopPropagation` sur ces zones nuirait à l'UX "clic n'importe où sur la carte = ouverture de la fiche". Un composant dédié aux vignettes, avec moins de surface d'interaction, est plus clair à maintenir. Les deux composants `Carousel*.tsx` restent intacts pour la fiche détail (étape 7).
|
||||||
|
|
||||||
|
Les composants `app/components/Carousel.tsx` et `app/components/CarouselCompetences.tsx` deviennent donc formellement **"carousels de fiche détail"** dans la nomenclature interne ; `VignetteCarousel` est leur petit frère "liste". Un éventuel refactor plus tard pourra fusionner les deux premiers (quasi-doublons) — hors scope actuel.
|
||||||
|
|
||||||
|
## 7. Étape 7 — Fiches détail + glossaire + GrasBot flottant (2026-04-22)
|
||||||
|
|
||||||
|
L'étape 7 touche cinq composants et introduit un sixième (le FAB). Elle est découpée en cinq sous-lots décrits ci-dessous.
|
||||||
|
|
||||||
|
### 7.a Carousels fiche détail (`Carousel.tsx` + `CarouselCompetences.tsx`)
|
||||||
|
|
||||||
|
Les deux composants sont quasi-doublons historiques ; on les refait **à l'identique** pour ne pas risquer de régression sur la modale glossaire qui consomme `CarouselCompetences`. Un futur refactor pourra les fusionner une fois le périmètre de la refonte clos (pas dans le scope étape 7).
|
||||||
|
|
||||||
|
Changements appliqués :
|
||||||
|
- **Pagination bullets `primary`** via surcharge inline des variables Swiper (`--swiper-pagination-color: #26445d`, bullets 8 px). Même approche que `VignetteCarousel` pour ne pas polluer `globals.css` avec un sélecteur `.swiper-pagination-bullet` global.
|
||||||
|
- **Flèches** : on conserve les chevrons natifs Swiper, recolorés via `--swiper-navigation-color: #26445d`, taille 28 px. Remplacement par Material Symbols écarté (demande un override complet du markup via slots Swiper, sans bénéfice visuel proportionnel).
|
||||||
|
- **Conteneur** : `rounded-tile overflow-hidden shadow-ambient-sm` (vs `rounded-md shadow-md` pré-refonte).
|
||||||
|
- **`autoplay: 3500` + `disableOnInteraction: false`** : l'autoplay reprend après un swipe manuel plutôt que de rester figé.
|
||||||
|
- **`loop` conditionnel** (`images.length > 1`) : évite le warning Swiper sur les entrées mono-image.
|
||||||
|
- **Lightbox Stitch** : voile `bg-on-surface/80 backdrop-blur-sm` (vs `bg-black/10 backdrop-blur-2xl` qui noyait l'image dans un flou 2xl contre-productif), image en `object-contain max-h-[92vh] max-w-[92vw] rounded-sheet shadow-ambient` (ne déforme plus les portraits), bouton close rond 40 px Material Symbol `close` (`translate="no"`), verrouillage du scroll body + fermeture `Escape` + fermeture sur clic voile avec `stopPropagation` sur l'image.
|
||||||
|
|
||||||
|
### 7.b Fiche portfolio (`ContentSection.tsx`)
|
||||||
|
|
||||||
|
Avant : cartes `bg-white/50 text-blue-700 font-headline font-extrabold` hardcodées, lien externe `bg-white/65 text-red-700 hover:text-blue-700`, pas de retour vers la liste, pas de hiérarchie éditoriale. Contenu Markdown sans `prose`.
|
||||||
|
|
||||||
|
Après :
|
||||||
|
- **Gabarit aligné** sur les listes (étape 6) : wrapper `max-w-3xl` centré, fil d'Ariane minimaliste (pastille ronde `bg-surface-container-lowest/70 backdrop-blur-vellum` avec Material Symbol `arrow_back` + label « Portfolio »), carte vellum principale (`rounded-sheet bg-surface-container-lowest/85 shadow-ambient backdrop-blur-vellum`).
|
||||||
|
- **En-tête éditorial** : kicker `Projet · Portfolio` uppercase tracking-[0.3em] + titre Manrope extrabold `text-on-surface`.
|
||||||
|
- **Carousel détail** : `<Carousel>` plein cadre (`h-64 sm:h-80 md:h-96`) réutilise la version 7.a.
|
||||||
|
- **Corps Markdown en `prose` Stitch** : mêmes overrides que la home (`prose-headings:text-primary`, `prose-p:text-on-surface-variant`, `prose-hr:` transformés en pastille primary via le fix § 4 sexies). Le CV et les fiches partagent désormais la **même charte typographique**.
|
||||||
|
- **CTA externe jewel** : `bg-primary text-white shadow-jewel` avec Material Symbol `open_in_new` (`translate="no"`) et hover `-translate-y-0.5`. Le `linkText` Strapi reste utilisé, fallback « Voir plus » (au lieu de « Voir plus/lien externe » qui mélangeait 2 intentions).
|
||||||
|
- **États loading + not-found** en vellum plutôt qu'en ligne `text-gray-500` orpheline. Le not-found propose un retour explicite vers `/portfolio`.
|
||||||
|
|
||||||
|
### 7.c Fiche compétences (`ContentSectionCompetences.tsx` + `Container`)
|
||||||
|
|
||||||
|
Trois chantiers en un :
|
||||||
|
|
||||||
|
**Style** : gabarit identique à 7.b (pastille retour, en-tête vellum, carousel 16/9, prose Stitch). Les `titleClass` / `contentClass` historiques ne sont plus consommés mais on les garde dans l'interface TS pour ne pas casser le call-site.
|
||||||
|
|
||||||
|
**Keywords glossaire/chatbot sans styles inline** : avant, `transformMarkdownWithKeywords` injectait `<span style="color: blue">...</span>` — visuellement criard et non thématisable. Après, on injecte les classes `.glossary-keyword` et `.chatbot-keyword` (définies dans `globals.css`), stylées en `color: #26445d` (primary), `text-decoration: underline dotted`, `text-underline-offset: 3px`. Le soulignement pointillé signale l'interactivité sans rompre le flux de lecture, la couleur cohérente avec toute la charte Stitch. Attributs `role="button" tabindex="0"` ajoutés au passage pour l'accessibilité clavier (touche Entrée peut être câblée plus tard si besoin).
|
||||||
|
|
||||||
|
**Event listeners scopés** : avant, `document.body.addEventListener("click", ...)` × 2. Après, un seul listener attaché à `contentRef` (ref sur le wrapper prose). Les clics remontent en bubbling depuis les spans Markdown jusqu'au wrapper ; gain en clarté, pas de fuite, pas de risque de conflit avec d'autres zones du DOM. Le handler `keyword` → ouvre `ModalGlossaire`. Le handler `chatbot` → dispatch `CustomEvent("grasbot:open")` sur `window` pour réveiller le FAB global (7.e) au lieu d'instancier un `<ChatBot />` local.
|
||||||
|
|
||||||
|
**Container** (`ContentSectionCompetencesContainer.tsx`) : l'état de chargement `⏳ Chargement des compétences...` remplacé par un skeleton vellum (kicker + titre + carousel + 3 lignes de texte en `animate-pulse`), cohérent avec les listes et `ContentSection`.
|
||||||
|
|
||||||
|
### 7.d ChatBot (`ChatBot.js`)
|
||||||
|
|
||||||
|
Le composant garde son API (`onClose`) mais tout le shell visuel est refait :
|
||||||
|
|
||||||
|
| Zone | Avant | Après |
|
||||||
|
|------|-------|-------|
|
||||||
|
| Carte | `bg-white/70 shadow-lg rounded-lg border border-gray-300` | `bg-surface-container-lowest/95 backdrop-blur-vellum rounded-sheet shadow-ambient` (= gabarit vellum partagé) |
|
||||||
|
| Header | `bg-blue-600 text-white` + 💬 emoji + ❌ | `bg-primary text-white` + Material Symbol `smart_toy` + sous-titre Manrope uppercase « Assistant IA locale » + bouton close rond Material Symbol |
|
||||||
|
| Bulle user | `bg-blue-500 ml-auto` | `bg-primary text-white rounded-sheet` |
|
||||||
|
| Bulle bot | `bg-gray-500 mr-auto` | `bg-surface-container text-on-surface rounded-sheet` |
|
||||||
|
| Indicateur attente | `wait...` sur bulle grise | « GrasBot réfléchit... » en italique atténué avec 3 points animés `.dot-1/2/3` existants |
|
||||||
|
| Input | `border border-gray-300 rounded-l-lg` | `bg-surface-container-low rounded-tile focus-visible:ring-2 focus-visible:ring-primary` |
|
||||||
|
| Envoyer | `bg-blue-500 text-white ➤` | Bouton rond Material Symbol `send` jewel `bg-primary shadow-jewel hover:-translate-y-0.5`, disabled si vide ou en attente |
|
||||||
|
|
||||||
|
Ajouts fonctionnels : auto-scroll en bas à chaque nouveau message (ref `scrollRef`), focus auto sur l'input à l'ouverture, envoi à Entrée (`handleKeyDown`), input désactivé pendant l'attente (plus de double envoi possible), message d'accueil éditorial quand la conversation est vide.
|
||||||
|
|
||||||
|
### 7.e GrasBotFab (`GrasBotFab.tsx`)
|
||||||
|
|
||||||
|
Nouveau composant monté une seule fois dans `app/layout.tsx` → le chatbot est désormais accessible **depuis toutes les pages**, plus seulement les fiches compétences.
|
||||||
|
|
||||||
|
**Anatomie** :
|
||||||
|
- **Bouton** : `fixed bottom-6 right-6 z-30`, rond 56 px (64 px md), `bg-primary text-white shadow-jewel` avec Material Symbol `smart_toy` (→ `close` quand ouvert). Hover `-translate-y-0.5 hover:bg-primary-container`. `aria-expanded` tenu à jour.
|
||||||
|
- **Panneau** : `fixed inset-x-4 bottom-24` mobile (plein largeur - 16 px de chaque côté), `sm:inset-auto sm:bottom-24 sm:right-6 sm:w-96 sm:h-[560px]` desktop. `role="dialog"` avec `aria-label`. Monte le `<ChatBot>` refait en 7.d avec `onClose` qui ferme le panneau.
|
||||||
|
- **Fermeture Esc** globale dès que le panneau est ouvert.
|
||||||
|
|
||||||
|
**Flux d'entrée** :
|
||||||
|
- Clic direct sur le FAB → ouvre le panneau.
|
||||||
|
- Clic sur `.chatbot-keyword` (« IA locale ») dans une fiche compétence → `ContentSectionCompetences` dispatch `window.dispatchEvent(new CustomEvent("grasbot:open"))`, le FAB écoute et ouvre. Pas de Context, pas de store : un `CustomEvent` suffit pour un besoin one-shot et garde les composants découplés.
|
||||||
|
|
||||||
|
**Z-index** : le FAB est en `z-30`. Header en `z-20`, drawer mobile en `z-40`. On passe donc le FAB **devant** le header (sinon invisible sous la bande fixe) mais **derrière** le drawer (pour ne pas masquer la navigation). Si le drawer est ouvert, le FAB est recouvert ; acceptable, le drawer étant un mode modal de navigation.
|
||||||
|
|
||||||
|
### Points laissés pour l'étape 8
|
||||||
|
|
||||||
|
- Fusion de `Carousel.tsx` et `CarouselCompetences.tsx` (doublons) : hors scope étape 7 (pas de gain visuel, risque de régression non justifié). À reprendre en lot "dette technique" après l'étape 8.
|
||||||
|
- `ModalGlossaire` déjà aligné Stitch au correctif § 4 ter ; aucun changement nécessaire en 7, juste une vérification que son `CarouselCompetences` hérite bien de la lightbox 7.a — oui, c'est automatique.
|
||||||
|
- Persistance du fil de conversation GrasBot (refresh = historique perdu) : pas demandé, pas introduit. Ajouter un `localStorage` si besoin plus tard.
|
||||||
|
|
||||||
|
## 8. Étape 8 — Contact + Footer éditorial (2026-04-22)
|
||||||
|
|
||||||
|
Dernière page héritée d'avant refonte. Avant : `app/contact/page.js` utilisait `bg-white/50 rounded-md`, `border-b-4 border-blue-500 pb-2` sous les titres, et `ContactForm` affichait un formulaire `bg-white shadow-lg rounded-lg` bleu Tailwind (`bg-blue-500`). Incohérent avec le reste du site depuis l'étape 5 (tokens Stitch `primary = #26445d`, radius `sheet` / `tile`, ombres `shadow-ambient` / `shadow-jewel`).
|
||||||
|
|
||||||
|
### 8.a Page contact (`app/contact/page.js`)
|
||||||
|
|
||||||
|
Gabarit aligné sur les listes (étape 6) et le hero home (étape 5) :
|
||||||
|
|
||||||
|
- **Colonne utile `max-w-3xl`** (format "lettre", plus intime que les `max-w-6xl` des listes).
|
||||||
|
- **Hero éditorial vellum** : kicker uppercase tracking-[0.3em] « Contact · Prendre la parole » + titre Manrope extrabold `text-on-surface` « Correspondance » + pitch Newsreader `text-on-surface-variant`. Plus d'emoji `📬` dans le titre (cohérence avec les autres pages qui utilisent Material Symbols, pas des emojis).
|
||||||
|
- **Section « canaux directs »** : carte vellum principale avec titre secondaire (`text-primary`) puis grille 3 tuiles imbriquées `rounded-tile bg-surface-container-low/80 hover:bg-surface-container/80`. Chaque tuile = pastille primaire ronde avec Material Symbol (`link` pour LinkedIn, `public` pour Facebook, `alternate_email` pour email) + label kicker + handle tronqué + chevron `arrow_forward` qui se décale au hover. Mêmes codes visuels que les cartes vignette portfolio / compétences, sans l'aspect 2/3+1/3 (3 canaux = symétrie assumée).
|
||||||
|
- **Carte vellum principale pour le formulaire** : `rounded-sheet bg-surface-container-lowest/85 p-5 shadow-ambient backdrop-blur-vellum sm:p-7 md:p-8`, titre secondaire « Écrire un message » `text-primary` + pitch italique Newsreader « Temps de réponse habituel : 48 h ». Le form est rendu **sans** sa propre carte blanche (la carte parente suffit).
|
||||||
|
- Les **liens externes** (LinkedIn, Facebook) ont `target="_blank" rel="noopener noreferrer"` ; l'email ouvre un `mailto:` standard.
|
||||||
|
|
||||||
|
### 8.b ContactForm (`app/components/ContactForm.tsx`)
|
||||||
|
|
||||||
|
Refonte interne complète :
|
||||||
|
|
||||||
|
- **Suppression** du wrapper `bg-white shadow-lg rounded-lg` : le formulaire vit dans la carte vellum parente (page 8.a). Empêche le "double carton" incohérent avec la charte.
|
||||||
|
- **Labels visibles** en Manrope uppercase tracking-[0.3em] (au-dessus de chaque champ) — améliore l'accessibilité et aligne sur les kickers de la page. Les placeholders restent là à titre indicatif.
|
||||||
|
- **Champs** : `bg-surface-container-low/90`, `rounded-tile`, padding `px-4 py-3`, focus `focus-visible:ring-2 focus-visible:ring-primary`, placeholder en `text-on-surface-variant/70`. Le `textarea` gagne `min-h-[9rem] resize-y` (contrôle vertical par l'utilisateur, pas de hauteur figée gênante).
|
||||||
|
- **Bouton CTA jewel** : `bg-primary text-on-primary shadow-jewel hover:-translate-y-0.5 rounded-tile px-6 py-3 font-headline uppercase tracking-widest` + Material Symbol `send` (`translate="no"`). État `disabled` en `bg-outline-variant/60 text-on-surface-variant cursor-not-allowed` avec icône `hourglass_top`.
|
||||||
|
- **Feedback status** : plus de chaîne emoji `❌`/`✅`/`⏳` — un petit bandeau `rounded-tile` avec Material Symbol + texte, couleur selon l'état :
|
||||||
|
- `success` → `bg-primary-fixed/70 text-on-primary-fixed` + `check_circle`
|
||||||
|
- `error` → `bg-error-container text-on-error-container` + `error`
|
||||||
|
- `loading` → `bg-surface-container text-on-surface-variant` + `hourglass_top`
|
||||||
|
- **Accessibilité** : `role="status" aria-live="polite"` sur le bandeau, `autoComplete` (`name`, `email`), `noValidate` sur le form (on fait la validation en JS pour maîtriser les messages FR). L'ancien `isSuccess: boolean | null` à double état est remplacé par un `statusKind: "idle" | "loading" | "success" | "error"` unique, plus lisible.
|
||||||
|
|
||||||
|
### 8.c Footer (`app/components/Footer.jsx`)
|
||||||
|
|
||||||
|
Avant : `bg-white/50 rounded-lg backdrop-blur` + `text-gray-700`. Déjà partiellement migré (font-headline, `visite n°` en kicker) mais surface + radius hors charte.
|
||||||
|
|
||||||
|
Après : **carte vellum légère** centrée, sans ombre ambient (le footer ne doit pas flotter autant que le contenu principal) :
|
||||||
|
|
||||||
|
- Conteneur : `rounded-tile bg-surface-container-lowest/70 backdrop-blur-vellum px-6 py-5 text-center`.
|
||||||
|
- Trois lignes éditoriales :
|
||||||
|
1. **Signature** Manrope `text-primary` « Fernand Gras-Calvet » (identité).
|
||||||
|
2. **Pitch** Newsreader italic `text-on-surface-variant` « Portfolio — Étudiant 42 Perpignan · © {year} » (ton éditorial cohérent avec le hero home).
|
||||||
|
3. **Compteur** de visites Manrope `text-[10px] uppercase tracking-[0.3em] text-outline` (méta discrète).
|
||||||
|
- **SSR-safe** : `new Date().getFullYear()` est calculé côté client (via `useState` init + `useEffect`) pour éviter un mismatch SSR / CSR si l'année bascule pile à minuit.
|
||||||
|
|
||||||
|
### Ce que ça règle
|
||||||
|
|
||||||
|
- **Dernière page hors charte migrée** : le site est désormais 100 % « Digital Atelier » (home, layout, listes, fiches, glossaire, chatbot, contact, footer).
|
||||||
|
- **Cohérence typo** : plus aucune référence à `font-headline font-extrabold border-b-4 border-blue-500 pb-2` (motif ancien).
|
||||||
|
- **Cohérence iconographique** : plus aucun emoji `📬 📩 🚀` résiduel dans les titres de page contact / form ; tout est passé en Material Symbols (seuls emojis acceptés = message utilisateur dans le chatbot, et l'emoji `📅` dans `sendMessage` qui reste un détail de payload côté Strapi, pas d'affichage direct).
|
||||||
|
- **Accessibilité contact** : labels visibles, `role="status"`, `aria-live="polite"`, `autoComplete` — améliore l'usage clavier / lecteur d'écran.
|
||||||
|
- **Footer** : plus de double lecture (`text-gray-700` sur `bg-white/50` contrastait mal sur wallpaper clair) — `text-on-surface-variant` sur vellum reste lisible partout.
|
||||||
|
|
||||||
|
### Points laissés en dehors de l'étape 8
|
||||||
|
|
||||||
|
- **Persistance du compteur de visites** côté serveur (Strapi) : hors scope refonte visuelle. Reste en `localStorage` comme avant.
|
||||||
|
- **Validation serveur des champs du form** (anti-spam, honeypot, reCAPTCHA) : hors scope refonte visuelle. Strapi ne filtre pour l'instant que sur la structure JSON attendue.
|
||||||
|
- **Fusion Carousel.tsx / CarouselCompetences.tsx** : reste en dette technique (déjà noté §7).
|
||||||
|
|
||||||
|
## 9. Post-refonte — Contact effectif via Brevo (2026-04-23)
|
||||||
|
|
||||||
|
La refonte visuelle a laissé le formulaire de contact en état "joli mais non fonctionnel" : il stockait les messages dans Strapi (content-type `message`), à consulter via `/admin/messages` (page publique, non protégée). Aucune notification.
|
||||||
|
|
||||||
|
**Décision** (discutée avec l'utilisateur) : supprimer le passage par Strapi, passer à une **notification email directe** via l'**API HTTP Brevo** (compte existant pour la newsletter). Plus simple, moins de surface d'attaque, notification immédiate.
|
||||||
|
|
||||||
|
### Changements
|
||||||
|
|
||||||
|
- **Nouveau** : `app/api/contact/route.ts` (Next.js App Router, runtime Node) — reçoit le form, valide, applique honeypot + rate-limit, appelle `POST https://api.brevo.com/v3/smtp/email`, retourne `{ ok: true | false, error }`.
|
||||||
|
- **Modifié** : `app/components/ContactForm.tsx` — appel vers `/api/contact` au lieu de `sendMessage(...)`. Ajout d'un **champ honeypot** `website` caché (position absolue hors écran + `aria-hidden` + `tabindex=-1`). Codes d'erreur serveur mappés en messages FR.
|
||||||
|
- **Supprimé** : `app/utils/sendMessage.ts` (plus utilisé), `app/admin/messages/page.tsx` (plus de consultation Strapi nécessaire, et cette page était exposée publiquement sans auth).
|
||||||
|
- **Supprimé côté Strapi** : `cmsbackend/src/api/message/` (content-type + routes + services + controllers). La table SQLite `messages` est laissée en place (orpheline, inoffensive).
|
||||||
|
- **Nouveau** : `.env.example` en racine pour documenter les variables requises, `contact-flow.md` pour l'architecture complète.
|
||||||
|
|
||||||
|
### Variables d'env (.env.local)
|
||||||
|
|
||||||
|
```env
|
||||||
|
BREVO_API_KEY=xkeysib-...
|
||||||
|
CONTACT_FROM_EMAIL=<expéditeur vérifié Brevo>
|
||||||
|
CONTACT_FROM_NAME=Portfolio — nouveau message
|
||||||
|
CONTACT_TO_EMAIL=grascalvet.fernand@gmail.com
|
||||||
|
CONTACT_TO_NAME=Fernand Gras-Calvet
|
||||||
|
```
|
||||||
|
|
||||||
|
### Anti-abus
|
||||||
|
|
||||||
|
- **Honeypot** : champ caché `website`. Si rempli → 200 silencieux + log warning (pas d'erreur pour ne pas signaler aux bots).
|
||||||
|
- **Rate-limit** : 3 envois / IP / 10 min (Map en mémoire). Limité en serverless multi-instance — acceptable pour un portfolio.
|
||||||
|
- **Validation serveur** : longueurs (120 / 160 / 5000), regex email, codes d'erreur en `UPPER_SNAKE_CASE` stables.
|
||||||
|
|
||||||
|
### Email reçu
|
||||||
|
|
||||||
|
- Sujet : `Nouveau message portfolio — {Nom}`
|
||||||
|
- Expéditeur : `CONTACT_FROM_NAME <CONTACT_FROM_EMAIL>`
|
||||||
|
- Reply-To : email du visiteur (un clic "Répondre" dans Gmail et le mail part au visiteur).
|
||||||
|
- Corps HTML : carte Stitch simplifiée (primary/secondary/surface-container-low) avec table nom/email/date/IP + zone message `pre-wrap`.
|
||||||
|
|
||||||
|
### Leçons retenues
|
||||||
|
|
||||||
|
1. **Surveiller les pages admin "dev"** qui fuitent en prod : `/admin/messages` listait les emails de tous les visiteurs en clair, sans auth. Avec la page supprimée, plus d'exposition.
|
||||||
|
2. **Clé API fuitant en clair dans un chat** : considérer comme compromise et régénérer. Documenté dans `contact-flow.md` § "Sécurité — rappels".
|
||||||
|
3. **Pour un besoin "1 seul destinataire" avec faible volume**, une route Next + API transactionnelle bat clairement une solution SMTP + plugin Strapi : moins de pièces, moins de config, même garantie de délivrabilité.
|
||||||
|
|
||||||
|
Voir `contact-flow.md` pour la procédure de test, les codes d'erreur, et le rollback éventuel.
|
||||||
|
|
||||||
|
## 5. Checklist relecture (à passer à la fin de chaque étape)
|
||||||
|
|
||||||
|
- [ ] Aucune colonne unique globale `max-w-xl` (c'est le format newsletter).
|
||||||
|
- [ ] Aucune bottom nav fixe (déjà couvert par header + drawer).
|
||||||
|
- [ ] Aucune bordure 1px pleine (`border border-*`) sur un composant de contenu — sauf ghost-border à 15 % max.
|
||||||
|
- [ ] Aucune utilisation de `#000` pur — toujours `on-surface` / `on-background`.
|
||||||
|
- [ ] Le wallpaper reste perceptible entre / autour des cartes.
|
||||||
|
- [ ] La hiérarchie Manrope / Newsreader est respectée (pas de Orbitron résiduel).
|
||||||
|
- [ ] Les CTAs principaux ont `shadow-jewel`.
|
||||||
|
- [ ] Radius Stitch (`rounded-sheet` / `rounded-tile`) utilisés sur les cartes de la refonte.
|
||||||
|
- [ ] Chaque nouvelle icône Material Symbols Outlined ajoutée porte `translate="no"` (voir §4 quinquies).
|
||||||
|
After Width: | Height: | Size: 57 KiB |
|
After Width: | Height: | Size: 101 KiB |
|
After Width: | Height: | Size: 287 KiB |
|
After Width: | Height: | Size: 175 KiB |
|
After Width: | Height: | Size: 286 KiB |
|
After Width: | Height: | Size: 352 KiB |
|
After Width: | Height: | Size: 288 KiB |
|
After Width: | Height: | Size: 310 KiB |
|
After Width: | Height: | Size: 297 KiB |
|
After Width: | Height: | Size: 319 KiB |
|
After Width: | Height: | Size: 309 KiB |
|
After Width: | Height: | Size: 316 KiB |
|
After Width: | Height: | Size: 198 KiB |
|
After Width: | Height: | Size: 343 KiB |
|
After Width: | Height: | Size: 497 KiB |
|
After Width: | Height: | Size: 198 KiB |
|
After Width: | Height: | Size: 278 KiB |
|
After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 194 KiB |
116
obsidian-site-docs/docs-site-interne/captures/AUDIT-VISUEL.md
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
# Audit visuel et mise en forme
|
||||||
|
|
||||||
|
**Dernière mise à jour :** 2026-04-22
|
||||||
|
**Captures :** [INDEX.md](./INDEX.md)
|
||||||
|
|
||||||
|
## Objectif
|
||||||
|
|
||||||
|
Recenser les **problèmes visuels** et de **mise en forme** pour les **corriger** avec traçabilité (code : `app/layout.tsx`, `app/page.tsx`, `app/components/Footer.jsx`, `app/globals.css`).
|
||||||
|
|
||||||
|
## Légende
|
||||||
|
|
||||||
|
| Champ | Sens |
|
||||||
|
|--------|------|
|
||||||
|
| **ID** | Numéro dans [INDEX.md](./INDEX.md) |
|
||||||
|
| **Constats** | Points validés ensemble |
|
||||||
|
| **Piste / correctif** | Idée ou implémentation |
|
||||||
|
| **Statut** | `OK` / `à traiter` / `fait` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ID 01 — Header et navigation desktop
|
||||||
|
|
||||||
|
| Fichier | `01-layout-header-nav-desktop.webp` |
|
||||||
|
| **Navigateur** | Google Chrome |
|
||||||
|
| **Constats** | Header OK, mais depuis la normalisation des conteneurs (option 1 zoom) le texte de plusieurs pages se retrouvait **collé** au header : le `<header>` étant `position: fixed`, il est **hors du flux** et ne prend pas de place dans la grille racine. Seule la home compensait, via un `mt-12` local. |
|
||||||
|
| **Piste / correctif appliqué (2026-04-22)** | Compensation **centralisée** dans `app/layout.tsx` : `<main>` en `pt-20 md:pt-24` (couvre la hauteur du header `h-16` + un peu d’air). Suppression du `mt-12` dupliqué sur la home (`app/page.tsx`) pour ne pas cumuler. Toutes les pages héritent désormais du même espace sous le header, plus besoin de compenser page par page. |
|
||||||
|
| **Statut** | `fait` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ID 02 — Menu mobile + header (et retour UX burger)
|
||||||
|
|
||||||
|
| Fichier | `02-layout-nav-mobile-ouverte.webp` |
|
||||||
|
| **Route** | `/` |
|
||||||
|
| **Code** | `app/layout.tsx` (header, overlay menu) |
|
||||||
|
|
||||||
|
### Constats (validés)
|
||||||
|
|
||||||
|
- Bouton **menu burger** trop imposant et « gris » (gros bloc) — à rendre plus discret.
|
||||||
|
- Côté **dézoom** (pinch) sur mobile : le **texte Strapi** + **header** se décalent à gauche, effet d’**incohérence** visuelle.
|
||||||
|
|
||||||
|
### Piste / correctif appliqué (2026-04-28) — option 1 (CSS / layout seulement)
|
||||||
|
|
||||||
|
- **Burger** : à affiner (taille, contraste) si besoin — voir capture actuelle.
|
||||||
|
- **Décalage au zoom (sans bloquer le zoom)** : pas de `maximum-scale` ni de meta viewport qui impose le zoom. Renfort uniquement côté **CSS** :
|
||||||
|
- `html` / `body` : `overflow-x: hidden`, `min-width: 0`, `max-width: 100%` dans `app/globals.css` ;
|
||||||
|
- grille racine, `<header>`, `<main>`, footer : `min-w-0`, `w-full` / `max-w-full` là où c’est utile (`app/layout.tsx`, `app/components/Footer.jsx`) ;
|
||||||
|
- **`.bg-wallpaper`** : `width: 100%` (plus de `200vw` sur mobile) pour éviter le double de largeur de page et le débordement au pinch-zoom ;
|
||||||
|
- page d’**accueil** : conteneur principal + bloc texte en `min-w-0` / `max-w-full` (`app/page.tsx`).
|
||||||
|
|
||||||
|
**Accessibilité** : le **verrouillage total du zoom** n’a **pas** été retenu (hors demande) ; on privilégie la mise en page. Si le dézoom reste gênant, on pourra itérer (contraintes sur le markdown, etc.).
|
||||||
|
|
||||||
|
### Refonte du menu mobile en drawer latéral (2026-04-22)
|
||||||
|
|
||||||
|
- **Ancien** : panneau mi-hauteur côté gauche + grosse croix `✖` → peu esthétique.
|
||||||
|
- **Nouveau** (`app/layout.tsx`) : **tiroir gauche** `role="dialog" aria-modal="true"`, **largeur 70 %** (capée à `max-w-sm`), hauteur pleine, fond **sombre translucide** (`bg-gray-900/70 backdrop-blur-md`), bordure droite fine.
|
||||||
|
- **Animation** : `transition-transform` + `-translate-x-full` ↔ `translate-x-0`, 300 ms, easing `ease-out` ; voile en fondu (`transition-opacity`). Respect de `prefers-reduced-motion` (voir `.mobile-drawer-*` dans `app/globals.css`).
|
||||||
|
- **Fermeture** : **pas de croix** ; tap sur le **voile**, **Échap**, **clic sur un lien**, **re-tap sur le burger** (qui affiche `✕` le temps de l’ouverture), et **auto-fermeture après 4 s** d’inactivité (timer remis à zéro à chaque interaction / touch dans le tiroir, nettoyé à la fermeture, et relancé à l’ouverture).
|
||||||
|
- **Accessibilité** : `aria-label` / `aria-expanded` / `aria-controls` sur le burger ; le focus revient sur le burger après fermeture ; fermeture automatique si l’écran passe en ≥ md (resize).
|
||||||
|
|
||||||
|
| **Statut** | `fait` — drawer latéral, auto-close 4 s, fond sombre translucide, 70 % |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ID 03 — Accueil hero desktop
|
||||||
|
|
||||||
|
| Fichier | `03-accueil-hero-desktop.webp` |
|
||||||
|
| **Route** | `/` |
|
||||||
|
| **Code** | `app/page.tsx`, `app/layout.tsx` |
|
||||||
|
| **Constats** | Rien à corriger spécifiquement : les ajustements déjà faits côté **layout** (compensation du header `pt-20 md:pt-24`, conteneur `min-w-0 max-w-full`) et côté **home** (suppression du `mt-12` dupliqué, `min-w-0 max-w-full` sur le `<main>` et la zone markdown) couvrent la mise en forme. Hero, photo, titre et CV s’affichent correctement au-dessus du pli. |
|
||||||
|
| **Statut** | `OK` (pris en charge par les correctifs ID 01 / option 1) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ID 04 — Accueil hero mobile
|
||||||
|
|
||||||
|
| Fichier | `04-accueil-hero-mobile.webp` |
|
||||||
|
| **Route** | `/` |
|
||||||
|
| **Code** | `app/page.tsx`, `app/layout.tsx` |
|
||||||
|
| **Constats** | Couvert par les correctifs globaux : **drawer** latéral (ID 02), **compensation header** (`pt-20 md:pt-24`), conteneurs en `min-w-0 / max-w-full`, `.bg-wallpaper` ramené à `width: 100%` sur mobile. Hero lisible, sans débordement au zoom, texte non collé au header. |
|
||||||
|
| **Statut** | `OK` (pris en charge par les correctifs ID 01 / ID 02 / option 1) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ID 05 à 21 — Passage global en `OK`
|
||||||
|
|
||||||
|
Les captures suivantes **n’ont pas révélé de problème spécifique** après les correctifs **option 1** (CSS / layout — overflow, `min-w-0`, `.bg-wallpaper`), **ID 01** (compensation du header `pt-20 md:pt-24`) et **ID 02** (refonte du menu mobile en drawer latéral). Elles sont donc marquées **`OK`** à ce stade. Elles restent sujettes à la **refonte visuelle plus large** à venir, qui pourra les reprendre une par une.
|
||||||
|
|
||||||
|
| ID | Section | Route | Fichier | Statut |
|
||||||
|
|----|---------|-------|---------|--------|
|
||||||
|
| 05 | Accueil page longue | `/` | `05-accueil-page-pleine-desktop.webp` | `OK` |
|
||||||
|
| 06 | Accueil chargement | `/` | *(non fourni — optionnel)* | `ignoré` |
|
||||||
|
| 07 | Portfolio liste desktop | `/portfolio` | `07-portfolio-liste-desktop.webp` | `OK` |
|
||||||
|
| 08 | Portfolio liste mobile | `/portfolio` | `08-portfolio-liste-mobile.webp` | `OK` |
|
||||||
|
| 09 | Portfolio fiche desktop | `/portfolio/slug` | `09-portfolio-detail-desktop-presentation-ecole-42.webp` | `OK` |
|
||||||
|
| 10 | Portfolio fiche mobile | `/portfolio/slug` | `10-portfolio-detail-mobile-presentation-ecole-42.webp` | `OK` |
|
||||||
|
| 11 | Compétences liste desktop | `/competences` | `11-competences-liste-desktop.webp` | `OK` |
|
||||||
|
| 12 | Compétences liste mobile | `/competences` | `12-competences-liste-mobile.webp` | `OK` |
|
||||||
|
| 13 | Compétences fiche desktop | `/competences/slug` | `13-competences-detail-ia-desktop.webp` | `OK` |
|
||||||
|
| 14 | Compétences fiche mobile | `/competences/slug` | `14-competences-detail-ia-mobile.webp` | `OK` |
|
||||||
|
| 15 | GrasBot ouvert desktop | `/competences/slug` | `15-competences-grasbot-ouvert-desktop.webp` | `OK` |
|
||||||
|
| 16 | Glossaire modal desktop | `/competences/slug` | `16-competences-glossaire-ouvert-desktop.webp` | `OK` |
|
||||||
|
| 17 | Contact formulaire desktop | `/contact` | `17-contact-formulaire-desktop.webp` | `fait` (étape 8, 2026-04-22) |
|
||||||
|
| 18 | Contact formulaire mobile | `/contact` | `18-contact-formulaire-mobile.webp` | `fait` (étape 8, 2026-04-22) |
|
||||||
|
| 19 | Footer desktop | `/` | `19-layout-footer-desktop.webp` | `fait` (étape 8, 2026-04-22) |
|
||||||
|
| 20 | Compteur visites desktop | `/` | `20-layout-compteur-visites-desktop.webp` | `OK` |
|
||||||
|
| 21 | ~~Admin messages desktop~~ | ~~`/admin/messages`~~ | `21-admin-messages-desktop.webp` | `obsolète` (route supprimée le 2026-04-23, voir `contact-flow.md`) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Suite
|
||||||
|
|
||||||
|
- **Étape 8 Digital Atelier bouclée** (2026-04-22) : contact + formulaire + footer migrés à la charte Stitch (voir `../REFONTE-VISUELLE.md §8`).
|
||||||
|
- **Formulaire de contact rendu effectif** (2026-04-23) : envoi via **Brevo** au lieu de Strapi. Content-type `message` supprimé, page `/admin/messages` supprimée, honeypot + rate-limit ajoutés. Voir `../contact-flow.md` et `REFONTE-VISUELLE.md §9`.
|
||||||
|
- Prendre de **nouvelles captures** 17 / 18 / 19 pour figer le rendu post-refonte (remplacement des WebP existants dans ce dossier `captures/`). La capture 21 est désormais **obsolète** (route supprimée).
|
||||||
|
- Dette technique identifiée : fusion `Carousel.tsx` / `CarouselCompetences.tsx` (doublons), persistance serveur du compteur de visites. Anti-spam formulaire : **fait** (honeypot + rate-limit, 2026-04-23).
|
||||||
63
obsidian-site-docs/docs-site-interne/captures/INDEX.md
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
# Index des captures — référence visuelle
|
||||||
|
|
||||||
|
**Dernière mise à jour :** 2026-04-01 — jeu de captures WebP déposé dans ce dossier (sauf ID 06, optionnel).
|
||||||
|
|
||||||
|
**Priorité** : **P0** = contexte global ; **P1** = refonte UI ; **P2** = optionnel.
|
||||||
|
|
||||||
|
## Fichiers réels dans le dépôt (état actuel)
|
||||||
|
|
||||||
|
Les noms suivants sont ceux présents sur disque. Les routes **dynamiques** utilisent deux conventions de slug (voir section Conventions slug).
|
||||||
|
|
||||||
|
| ID | Section | Route | Fichier dans le dépôt | P | Statut |
|
||||||
|
|----|---------|-------|------------------------|---|--------|
|
||||||
|
| 01 | Header + nav desktop | / | `01-layout-header-nav-desktop.webp` | P0 | OK |
|
||||||
|
| 02 | Menu mobile ouvert | / | `02-layout-nav-mobile-ouverte.webp` | P0 | OK |
|
||||||
|
| 03 | Accueil above the fold | / | `03-accueil-hero-desktop.webp` | P0 | OK |
|
||||||
|
| 04 | Accueil above the fold | / | `04-accueil-hero-mobile.webp` | P0 | OK |
|
||||||
|
| 05 | Accueil page longue | / | `05-accueil-page-pleine-desktop.webp` | P1 | OK |
|
||||||
|
| 06 | Accueil chargement | / | *(non fourni — optionnel)* | P2 | Ignoré |
|
||||||
|
| 07 | Portfolio liste | /portfolio | `07-portfolio-liste-desktop.webp` | P0 | OK |
|
||||||
|
| 08 | Portfolio liste | /portfolio | `08-portfolio-liste-mobile.webp` | P0 | OK |
|
||||||
|
| 09 | Portfolio fiche | /portfolio/slug | `09-portfolio-detail-desktop-presentation-ecole-42.webp` | P0 | OK |
|
||||||
|
| 10 | Portfolio fiche | /portfolio/slug | `10-portfolio-detail-mobile-presentation-ecole-42.webp` | P1 | OK |
|
||||||
|
| 11 | Compétences liste | /competences | `11-competences-liste-desktop.webp` | P0 | OK |
|
||||||
|
| 12 | Compétences liste | /competences | `12-competences-liste-mobile.webp` | P0 | OK |
|
||||||
|
| 13 | Compétences fiche | /competences/slug | `13-competences-detail-ia-desktop.webp` | P0 | OK |
|
||||||
|
| 14 | Compétences fiche | /competences/slug | `14-competences-detail-ia-mobile.webp` | P1 | OK |
|
||||||
|
| 15 | GrasBot ouvert | /competences/slug | `15-competences-grasbot-ouvert-desktop.webp` | P1 | OK |
|
||||||
|
| 16 | Glossaire modal | /competences/slug | `16-competences-glossaire-ouvert-desktop.webp` | P1 | OK |
|
||||||
|
| 17 | Contact formulaire | /contact | `17-contact-formulaire-desktop.webp` | P0 | OK |
|
||||||
|
| 18 | Contact formulaire | /contact | `18-contact-formulaire-mobile.webp` | P0 | OK |
|
||||||
|
| 19 | Footer | / | `19-layout-footer-desktop.webp` | P1 | OK |
|
||||||
|
| 20 | Compteur visites | / | `20-layout-compteur-visites-desktop.webp` | P2 | OK |
|
||||||
|
| 21 | Admin messages | /admin/messages | `21-admin-messages-desktop.webp` | P2 | OK |
|
||||||
|
|
||||||
|
**Slugs illustrés :** projet `presentation-ecole-42`, compétence `ia` (Strapi).
|
||||||
|
|
||||||
|
## Conventions slug (nouvelles captures)
|
||||||
|
|
||||||
|
| Zone | Motif |
|
||||||
|
|------|--------|
|
||||||
|
| Portfolio desktop | `09-portfolio-detail-desktop-{slug}.webp` |
|
||||||
|
| Portfolio mobile | `10-portfolio-detail-mobile-{slug}.webp` |
|
||||||
|
| Compétences desktop | `13-competences-detail-{slug}-desktop.webp` |
|
||||||
|
| Compétences mobile | `14-competences-detail-{slug}-mobile.webp` |
|
||||||
|
|
||||||
|
## Ordre conseillé (prise de vue)
|
||||||
|
|
||||||
|
01–02 chrome, 03–06 accueil, 07–10 portfolio, 11–16 compétences, 17–18 contact, puis 19–21.
|
||||||
|
|
||||||
|
## Repères code
|
||||||
|
|
||||||
|
| ID | Fichiers |
|
||||||
|
|----|----------|
|
||||||
|
| 01–02 | `app/layout.tsx` |
|
||||||
|
| 03–06 | `app/page.tsx` |
|
||||||
|
| 07–08 | `app/portfolio/page.jsx` |
|
||||||
|
| 09–10 | `app/portfolio/[slug]/page.tsx`, `ContentSection.tsx` |
|
||||||
|
| 11–12 | `app/competences/page.jsx` |
|
||||||
|
| 13–16 | `ContentSectionCompetences*.tsx`, `ChatBot.js`, `ModalGlossaire.tsx` |
|
||||||
|
| 17–18 | `app/contact/page.js`, `ContactForm.tsx` |
|
||||||
|
| 19 | `app/components/Footer.jsx` |
|
||||||
|
| 20 | `app/layout.tsx` |
|
||||||
|
| 21 | `app/admin/messages/page.tsx` |
|
||||||
17
obsidian-site-docs/docs-site-interne/captures/README.md
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
# Captures d'écran — référence visuelle
|
||||||
|
|
||||||
|
**Dernière mise à jour :** 2026-04-01
|
||||||
|
|
||||||
|
Ce dossier contient les **images WebP de référence** du site (état validé). La liste à jour : **[INDEX.md](./INDEX.md)**.
|
||||||
|
|
||||||
|
## Convention générale
|
||||||
|
|
||||||
|
Format : `{numero}-{zone}-{detail}-{viewport}.webp` — détails et **deux motifs pour les fiches dynamiques** (portfolio vs compétences) dans INDEX.md.
|
||||||
|
|
||||||
|
## Mise à jour
|
||||||
|
|
||||||
|
Après changement visuel validé : remplacer les WebP concernés et ajuster INDEX.md si besoin.
|
||||||
|
|
||||||
|
## Données sensibles
|
||||||
|
|
||||||
|
Sur `/admin/messages`, flouter ou données fictives sur les captures.
|
||||||
186
obsidian-site-docs/docs-site-interne/contact-flow.md
Normal file
@ -0,0 +1,186 @@
|
|||||||
|
# Flux "Contact" — notification par email (Brevo)
|
||||||
|
|
||||||
|
**Créé :** 2026-04-23
|
||||||
|
**Statut :** en production
|
||||||
|
|
||||||
|
## Vue d'ensemble
|
||||||
|
|
||||||
|
Le formulaire de contact du site envoie un **email de notification** sur la boîte Gmail personnelle (`grascalvet.fernand@gmail.com`), via l'**API HTTP transactionnelle de Brevo**.
|
||||||
|
|
||||||
|
**Plus aucun passage par Strapi pour les messages.** Le content-type `message` a été supprimé (ainsi que la page admin associée) le 2026-04-23. La table SQLite `messages` reste présente en base pour l'historique, mais n'est plus écrite ni lue.
|
||||||
|
|
||||||
|
```
|
||||||
|
Visiteur → /contact (Next.js front)
|
||||||
|
↓
|
||||||
|
POST /api/contact (Next.js App Router, server runtime)
|
||||||
|
↓
|
||||||
|
Validation + honeypot + rate-limit
|
||||||
|
↓
|
||||||
|
POST https://api.brevo.com/v3/smtp/email
|
||||||
|
↓
|
||||||
|
Gmail : CONTACT_TO_EMAIL
|
||||||
|
```
|
||||||
|
|
||||||
|
## Pourquoi cette architecture
|
||||||
|
|
||||||
|
### Simplification par rapport à l'ancien flux
|
||||||
|
|
||||||
|
Avant : `Front → Strapi POST /api/messages → SQLite` + consultation via `/admin/messages` (page publique, non protégée).
|
||||||
|
|
||||||
|
Problèmes :
|
||||||
|
- **Pas de notification** : il fallait penser à consulter l'admin pour voir les nouveaux messages.
|
||||||
|
- **Page admin exposée** : `/admin/messages` accessible à n'importe qui en clair (pas d'authentification). Fuite potentielle d'emails privés des visiteurs.
|
||||||
|
- **Content-type dédié** pour un usage limité (3 champs, pas d'historique utile).
|
||||||
|
|
||||||
|
Après : notification directe sur Gmail, zéro stockage intermédiaire, zéro page à sécuriser.
|
||||||
|
|
||||||
|
### Pourquoi l'API HTTP Brevo plutôt que SMTP
|
||||||
|
|
||||||
|
Discussion menée le 2026-04-23. Résumé :
|
||||||
|
|
||||||
|
| Critère | SMTP | API HTTP |
|
||||||
|
|---------|------|----------|
|
||||||
|
| Simplicité plugin Strapi | ✓ | (custom) |
|
||||||
|
| Port potentiellement bloqué | oui | non (HTTPS 443) |
|
||||||
|
| Débogage JSON | non | ✓ |
|
||||||
|
| Notre besoin (1 endroit, 1 mail) | ok | ok |
|
||||||
|
|
||||||
|
Comme on ne passe plus par Strapi, l'avantage "plugin Strapi" disparaît. On a pris **l'API HTTP** pour rester 100 % en HTTPS (aucun souci de port) et avoir des erreurs JSON lisibles.
|
||||||
|
|
||||||
|
## Fichiers concernés
|
||||||
|
|
||||||
|
| Fichier | Rôle |
|
||||||
|
|---------|------|
|
||||||
|
| `app/api/contact/route.ts` | Endpoint serveur Next.js qui reçoit le form et appelle Brevo |
|
||||||
|
| `app/components/ContactForm.tsx` | UI côté client, honeypot, gestion du status |
|
||||||
|
| `.env.local` | Secrets + config (non committé) |
|
||||||
|
| `.env.example` | Template documentaire des variables à remplir |
|
||||||
|
|
||||||
|
## Variables d'environnement
|
||||||
|
|
||||||
|
À définir dans `.env.local` (voir `.env.example` pour la structure) :
|
||||||
|
|
||||||
|
| Variable | Description | Où l'obtenir |
|
||||||
|
|----------|-------------|--------------|
|
||||||
|
| `BREVO_API_KEY` | Clé API Brevo | [app.brevo.com → SMTP & API → API Keys](https://app.brevo.com/settings/keys/api) |
|
||||||
|
| `CONTACT_FROM_EMAIL` | Expéditeur **vérifié** | Senders, Domains & IPs → Senders (pastille SPF/DKIM verte) |
|
||||||
|
| `CONTACT_FROM_NAME` | Nom affiché dans Gmail | Texte libre, ex. "Portfolio — nouveau message" |
|
||||||
|
| `CONTACT_TO_EMAIL` | Destinataire final | L'adresse Gmail personnelle |
|
||||||
|
| `CONTACT_TO_NAME` | Nom du destinataire | Texte libre |
|
||||||
|
|
||||||
|
**L'expéditeur doit être vérifié dans Brevo** : sinon Brevo renvoie une erreur au `POST`. On peut réutiliser l'expéditeur newsletter (pas de cloisonnement côté Brevo) et surcharger le nom d'affichage via `CONTACT_FROM_NAME`.
|
||||||
|
|
||||||
|
## Anti-abus
|
||||||
|
|
||||||
|
### Honeypot
|
||||||
|
|
||||||
|
Un champ caché `website` est ajouté au formulaire :
|
||||||
|
|
||||||
|
- Invisible aux humains (`position: absolute; left: -10000px; width: 1px`).
|
||||||
|
- Masqué aux lecteurs d'écran (`aria-hidden="true"`).
|
||||||
|
- Ignoré par le clavier (`tabindex="-1"`, `autocomplete="off"`).
|
||||||
|
|
||||||
|
Les bots qui parsent le DOM remplissent systématiquement tous les champs texte. Côté serveur, si `website` est non vide :
|
||||||
|
|
||||||
|
- On **renvoie un 200 silencieux** (pas de signal d'erreur aux bots → ils ne retentent pas).
|
||||||
|
- On **n'envoie pas d'email**.
|
||||||
|
- On **logue** un warning avec l'IP pour monitoring.
|
||||||
|
|
||||||
|
### Rate-limit
|
||||||
|
|
||||||
|
`app/api/contact/route.ts` limite à **3 envois par IP par fenêtre de 10 minutes**, via une `Map` en mémoire :
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const RATE_LIMIT_MAX = 3;
|
||||||
|
const RATE_LIMIT_WINDOW_MS = 10 * 60 * 1000;
|
||||||
|
```
|
||||||
|
|
||||||
|
Au-delà : réponse `429 Too Many Requests` avec header `Retry-After`.
|
||||||
|
|
||||||
|
**Limite connue** : en déploiement serverless avec plusieurs instances (Vercel free, Cloudflare Workers), la `Map` n'est pas partagée — le rate-limit est "best effort". Pour un portfolio, c'est acceptable. Pour du vrai ship : passer à Redis ou [Upstash](https://upstash.com/) (wrapper compatible serverless).
|
||||||
|
|
||||||
|
### Validation
|
||||||
|
|
||||||
|
Côté serveur (défense en profondeur, jamais faire confiance au client) :
|
||||||
|
|
||||||
|
- Champs requis non vides : `name`, `email`, `message`.
|
||||||
|
- Longueurs max : `name ≤ 120`, `email ≤ 160`, `message ≤ 5000` caractères (retourne `413`).
|
||||||
|
- Format email : regex `^[^\s@]+@[^\s@]+\.[^\s@]+$` (pragmatique, pas RFC 5322 stricte).
|
||||||
|
|
||||||
|
Les codes d'erreur renvoyés au client sont en `UPPER_SNAKE_CASE` (stables pour l'i18n future) : `MISSING_FIELDS`, `INVALID_EMAIL`, `TOO_LONG`, `RATE_LIMITED`, `BREVO_ERROR`, `BREVO_UNREACHABLE`, `SERVER_MISCONFIGURED`, `INVALID_JSON`.
|
||||||
|
|
||||||
|
## Format de l'email reçu
|
||||||
|
|
||||||
|
- **Sujet** : `Nouveau message portfolio — {Nom du visiteur}`
|
||||||
|
- **Expéditeur** : `{CONTACT_FROM_NAME} <{CONTACT_FROM_EMAIL}>`
|
||||||
|
- **Reply-To** : email du visiteur → cliquer "Répondre" dans Gmail écrit directement au visiteur.
|
||||||
|
- **Corps HTML** : carte Stitch simplifiée (palette primary / secondary) avec table récap (nom, email, date, IP) + zone message `pre-wrap`.
|
||||||
|
- **Corps texte** (fallback) : version plain text équivalente.
|
||||||
|
- **Tags Brevo** : `["portfolio", "contact-form"]` pour filtrer dans les statistiques Brevo si besoin.
|
||||||
|
|
||||||
|
## Procédure de test
|
||||||
|
|
||||||
|
### Local (développement)
|
||||||
|
|
||||||
|
1. S'assurer que `.env.local` contient les 5 variables requises + une clé Brevo valide.
|
||||||
|
2. `npm run dev` dans le repo Next (port 3000).
|
||||||
|
3. `/contact` → remplir le formulaire avec une **adresse email qu'on contrôle** (pour vérifier le Reply-To).
|
||||||
|
4. Vérifier :
|
||||||
|
- Feedback visuel "Message envoyé" en succès Stitch vert.
|
||||||
|
- Réception dans Gmail avec expéditeur = `CONTACT_FROM_NAME`.
|
||||||
|
- Clic "Répondre" → destinataire = l'email saisi dans le formulaire.
|
||||||
|
|
||||||
|
### Honeypot
|
||||||
|
|
||||||
|
1. Ouvrir la console DevTools, onglet Elements.
|
||||||
|
2. Trouver l'input caché `<input name="website">` (dans un div `position: absolute; left: -10000px`).
|
||||||
|
3. Y saisir une valeur (ex. "bot").
|
||||||
|
4. Soumettre le formulaire normalement.
|
||||||
|
5. Résultat attendu : UI affiche "Message envoyé" (succès silencieux) MAIS **aucun email n'arrive** dans Gmail.
|
||||||
|
6. Vérifier le warning serveur dans les logs Next (`[/api/contact] Honeypot déclenché`).
|
||||||
|
|
||||||
|
### Rate-limit
|
||||||
|
|
||||||
|
1. Envoyer un formulaire valide 4 fois de suite (modifier juste le message à chaque fois).
|
||||||
|
2. Au 4ᵉ envoi : réponse `429`, UI affiche "Trop d'envois depuis votre IP. Réessayez dans quelques minutes."
|
||||||
|
3. Attendre 10 min puis réessayer → doit repasser.
|
||||||
|
|
||||||
|
## Nettoyage de l'ancienne table SQLite (optionnel)
|
||||||
|
|
||||||
|
La table `messages` existe toujours dans `cmsbackend/.tmp/data.db` (ou le fichier configuré). Strapi ne la touche plus. Pour la supprimer proprement :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Arrêter Strapi d'abord !
|
||||||
|
cd cmsbackend
|
||||||
|
sqlite3 .tmp/data.db "DROP TABLE IF EXISTS messages;"
|
||||||
|
sqlite3 .tmp/data.db "DROP TABLE IF EXISTS messages_cmps;"
|
||||||
|
# Reprise normale
|
||||||
|
npm run develop
|
||||||
|
```
|
||||||
|
|
||||||
|
Non fait par défaut : la table reste orpheline mais inoffensive. Ça évite toute régression en cas de rollback éventuel.
|
||||||
|
|
||||||
|
## Quotas Brevo
|
||||||
|
|
||||||
|
Le **plan gratuit Brevo** inclut traditionnellement **300 emails transactionnels par jour**, partagés avec la newsletter. Pour un portfolio qui reçoit 0 à 5 messages par semaine, c'est largement suffisant.
|
||||||
|
|
||||||
|
Si le site reçoit un jour beaucoup plus de trafic (ou si Brevo change ses quotas gratuits), surveiller :
|
||||||
|
|
||||||
|
- **Brevo → Transactional → Statistics** : nombre d'envois, taux d'erreur.
|
||||||
|
- **Brevo → SMTP & API → Statistics** : quota quotidien consommé.
|
||||||
|
|
||||||
|
## Rollback
|
||||||
|
|
||||||
|
Si le flux Brevo tombe en panne (compte suspendu, quota dépassé, clé compromise) et qu'on veut **temporairement** retomber sur le flux Strapi ancien :
|
||||||
|
|
||||||
|
1. `git checkout <commit-pre-refonte-contact>` sur les fichiers : `app/api/contact/route.ts` (supprimer), `app/components/ContactForm.tsx` (restaurer `sendMessage`), `app/utils/sendMessage.ts` (restaurer), `cmsbackend/src/api/message/` (restaurer les 4 fichiers), et `app/admin/messages/page.tsx` si on veut la page de consultation.
|
||||||
|
2. Re-démarrer Strapi pour qu'il régénère le content-type en mémoire.
|
||||||
|
3. Vérifier les **permissions publiques** sur `POST /api/messages` dans l'admin Strapi (peut avoir besoin d'être recoché).
|
||||||
|
|
||||||
|
Préférable : **renouveler la clé Brevo** plutôt que rollback.
|
||||||
|
|
||||||
|
## Sécurité — rappels
|
||||||
|
|
||||||
|
- **Ne jamais committer `.env.local`**. Déjà dans `.gitignore` (`.env*`).
|
||||||
|
- **Si la clé Brevo fuit** (exposée dans un commit, un chat, un screenshot) : la **supprimer immédiatement** sur Brevo et en générer une nouvelle. Une clé compromise permet d'envoyer des emails frauduleux depuis l'expéditeur vérifié, ce qui peut faire blacklister le domaine.
|
||||||
|
- L'**IP du visiteur** est loguée dans l'email (pour abus). À supprimer des emails si on veut un template plus "commercial".
|
||||||
33
obsidian-site-docs/docs-site-interne/etat-actuel.md
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
# État actuel du site
|
||||||
|
|
||||||
|
**Dernière mise à jour :** 2026-04-24 (doc compétences / realisation-ia + CONFIGURATION)
|
||||||
|
|
||||||
|
## Ce qui est en place
|
||||||
|
|
||||||
|
- **Next.js 15** avec App Router, Tailwind, pages accueil / portfolio / compétences / contact, layout responsive avec menu burger. Design system "Digital Atelier" (Manrope + Newsreader, palette primary indigo-ardoise, vellum cards).
|
||||||
|
- **Strapi** avec content-types : homepage, projects, competences, **realisation-ia** (rattachées aux compétences), glossaire ; médias et texte riche (l’ancien type `message` a été retiré au profit de Brevo).
|
||||||
|
- **Compétences côté Next** : liste `/competences` (tri `order`) ; fiche `/competences/[slug]` (vignettes des `realisation-ia` liées quand il y en a, sinon fiche richtext) ; détail `/competences/[slug]/[realisation]`. Même logique d’enrichissement que le portfolio (Markdown, galerie, CTA) pour les fiches liées.
|
||||||
|
- **Formulaire contact** : e-mail via **Brevo** (route Next `POST /api/contact`). Voir [contact-flow.md](./contact-flow.md).
|
||||||
|
- **Chatbot GrasBot v3** : FAB global (`GrasBotFab.tsx`) → proxy Next → API LLM hébergée (`llmapi.fernandgrascalvet.com`).
|
||||||
|
- **FastAPI + Ollama** dans `llm-api/` : modèle `qwen3:8b`, pipeline `search.py` (graph + BM25 sur vault Obsidian `vault-grasbot/`, sans embeddings).
|
||||||
|
- **Vault de connaissance `vault-grasbot/`** : ~46 notes Markdown, dont 2 fiches projet manuelles (GrasBot, site portfolio) et compétences IA/Web mises à jour (2026-04) — recharger l’API après déploiement si besoin : `POST /reload-vault` (aliases, answers, priority) — source de vérité du chatbot, régénéré depuis Strapi par `strapi_extraction/build-vault.py`. Inclut une note `bio-fernand` courte (priority 10) dédiée aux questions biographiques et un CV complet complémentaire.
|
||||||
|
- **Observabilité Langfuse** : instance self-hosted `langfuse.fernandgrascalvet.com`, instrumentation Python (`llm-api/observability.py`) traçant chaque requête `/ask` (retrieval / prompt_build / ollama-chat) avec session/user IDs anonymes côté front. Mode no-op automatique si les clés sont absentes. Voir [`langfuse-observability.md`](./langfuse-observability.md).
|
||||||
|
- **Scripts** d'extraction et de doc dans `strapi_extraction/`.
|
||||||
|
- Documentation opérationnelle : [`CONFIGURATION_SITE.md`](../CONFIGURATION_SITE.md) à la racine du dépôt (incl. ordre des compétences et routes dédiées, renvoi vers [02-frontend-next.md](./02-frontend-next.md)).
|
||||||
|
- **Captures d'écran** de référence (WebP) : [captures/](./captures/) — voir [INDEX.md](./captures/INDEX.md).
|
||||||
|
- **Décision produit** : une **rubrique homelab / serveur** (souvent évoquée en « phase 3 ») n’est **pas retenue** — pas d’évolution planifiée sur ce thème ; le parcours public reste portfolio, compétences (dont IA + réalisations) et contact.
|
||||||
|
|
||||||
|
## Dette technique / incohérences connues
|
||||||
|
|
||||||
|
- Mélange **TypeScript** et **JavaScript** (`.jsx`, `.js`) dans `app/`.
|
||||||
|
- **`RootLayout` en client component** : tout le layout est côté client ; pas de Server Component racine pour le shell.
|
||||||
|
- **URLs Strapi** : logique répartie entre `getApiUrl`, `next.config.ts`, `config.ts` — risque de confusion ; à documenter dans les changements futurs.
|
||||||
|
- **Proxy LLM** : URL de production codée en dur dans `app/api/proxy/route.js` ; pas d’alignement automatique avec `llm-api` local.
|
||||||
|
- Champ Strapi **`Resum`** sur `project` : casse atypique ; attention dans le mapping front.
|
||||||
|
- **`start-my-site.ps1`** : chemins absolus `J:\my-next-site` — non portables.
|
||||||
|
|
||||||
|
## Non vérifié dans cette passe
|
||||||
|
|
||||||
|
- Permissions Strapi (public create sur `messages`, etc.).
|
||||||
|
- Comportement exact des rewrites Next vs route `app/api/proxy` (ordre de résolution).
|
||||||
|
- Tests automatisés : présence à confirmer.
|
||||||
61
obsidian-site-docs/docs-site-interne/feuille-de-route.md
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
# Feuille de route
|
||||||
|
|
||||||
|
**Dernière mise à jour :** 2026-04-24
|
||||||
|
|
||||||
|
Document vivant : ajuster les statuts et dates au fil du travail.
|
||||||
|
|
||||||
|
## Court terme (prochaines itérations)
|
||||||
|
|
||||||
|
| ID | Sujet | Statut | Notes |
|
||||||
|
|----|--------|--------|--------|
|
||||||
|
| R1 | Moderniser l’UI (design system, cohérence typo/couleurs) | En cours | Direction "Digital Atelier" inspirée de `stitch_V1/` ; cadrage et plan dans [`REFONTE-VISUELLE.md`](./REFONTE-VISUELLE.md). Étapes 1-7 (tokens + garde-fou + migration typo globale + layout racine + home + listes portfolio/compétences + fiches détail & GrasBot) faites le 2026-04-22. Reste l'étape 8 (contact + footer éditorial). |
|
||||||
|
| R2 | Homogénéiser TS vs JS dans `app/` | À faire | Migrer progressivement les `.jsx`/`.js` |
|
||||||
|
| R3 | Centraliser config API (Strapi + LLM) via `.env` | À faire | Remplacer URLs en dur où pertinent |
|
||||||
|
| R4 | Revoir `layout.tsx` (server vs client, perf SEO) | À faire | Évaluer extraction header/footer |
|
||||||
|
| R5 | Chatbot GrasBot — retrieval local (Qwen3 + vault Obsidian) | v3 en place | Pipeline **graph + BM25** (`llm-api/search.py`), plus de RAG vectoriel ni de dépendance ChromaDB. Vault `vault-grasbot/` enrichi automatiquement (aliases/answers/priority). Lecture directe depuis `build-vault.py`, plus d'étape d'indexation. Doc : [`08-vault-obsidian-retrieval.md`](./08-vault-obsidian-retrieval.md). Reste : fix `clean-api-data.js` (homepages + glossaires), affichage sources côté front, badge `grounded`, historique conversationnel, streaming. |
|
||||||
|
|
||||||
|
## Moyen terme
|
||||||
|
|
||||||
|
| ID | Sujet | Statut | Notes |
|
||||||
|
|----|--------|--------|--------|
|
||||||
|
| M1 | Tests (e2e ou smoke sur routes critiques) | À faire | |
|
||||||
|
| M2 | Accessibilité (navigation, contrastes, focus) | À faire | |
|
||||||
|
| M3 | Performance (images Next/Image, bundle) | À faire | |
|
||||||
|
|
||||||
|
## Long terme / idées
|
||||||
|
|
||||||
|
| ID | Sujet | Statut | Notes |
|
||||||
|
|----|--------|--------|--------|
|
||||||
|
| L1 | CI/CD ou script de déploiement documenté | À faire | |
|
||||||
|
| L2 | Monitoring / logs centralisés | À faire | |
|
||||||
|
|
||||||
|
## Historique des jalons
|
||||||
|
|
||||||
|
| Date | Jalon |
|
||||||
|
|------|--------|
|
||||||
|
| 2026-04-01 | Création du dossier `docs-site-interne` (dépôt Git) et première rédaction basée sur le code. |
|
||||||
|
| 2026-04-01 | Reprise après coupure : vérification complétude ; enrichissement de `04-api-llm-et-chatbot.md` ; suppression de `test.txt`. |
|
||||||
|
| 2026-04-01 | Index captures (`captures/INDEX.md`), `captures/README.md`, `07-reference-visuelle-captures.md`, skill `.cursor/skills/site-portfolio-evolution/SKILL.md`. |
|
||||||
|
| 2026-04-01 | Captures WebP intégrées au dépôt ; INDEX et README `captures/` alignés sur les noms réels (slug portfolio / compétence documentés). |
|
||||||
|
| 2026-04-22 | Audit visuel complet (`captures/AUDIT-VISUEL.md`), correctifs layout (option 1 + compensation header + drawer mobile). |
|
||||||
|
| 2026-04-22 | Refonte visuelle "Digital Atelier" — étape 1 (tokens Tailwind : palette Stitch, `font-headline` Manrope + `font-body` Newsreader, `rounded-sheet/tile`, `shadow-ambient/jewel`) et étape 2 (garde-fou + plan dans [`REFONTE-VISUELLE.md`](./REFONTE-VISUELLE.md)). |
|
||||||
|
| 2026-04-22 | Refonte visuelle — étape 3 : migration typographique globale. Toutes les classes `font-orbitron-*` (12 définitions CSS, 29 occurrences dans 11 fichiers) remplacées par `font-headline` Manrope + tailles/poids Tailwind explicites. Import Google Fonts Orbitron supprimé de `app/assets/main.css`. |
|
||||||
|
| 2026-04-22 | Refonte visuelle — correctif post-étape 3 : régression de couleurs texte entre desktop/mobile. Retrait du `@media (prefers-color-scheme: dark)` hérité du template Next (incohérent avec l'arbitrage "light-only"), `--foreground` fixé à `#191c1d` (on-surface Stitch), `body` avec couleur non-dépendante du thème système. 3 classes Tailwind invalides `text-black-500/700` remplacées par `text-gray-700` (`app/layout.tsx`, `app/page.tsx`, `app/components/ContentSectionCompetences.tsx`). |
|
||||||
|
| 2026-04-22 | Refonte visuelle — étape 4 : layout racine. Header "No-Line" (bordure pleine supprimée, `shadow-ambient-sm` + `backdrop-blur-vellum`, titre en `text-primary`). Burger refait en ghost button (Material Symbols `menu`/`close` au lieu des caractères `☰`/`✕`). Cercles animés repeints en `bg-primary/40` + `bg-primary-container/30`. Drawer mobile en `bg-primary/90 backdrop-blur-vellum` + liens éditoriaux (`bg-primary-container/60` → hover `bg-primary-fixed text-primary`). Bug préexistant **corrigé** : `NavLink` ignorait `className` et `onClick` fournis par le drawer mobile → refait avec support `className` / `onClick` / `activeClassName` / `inactiveClassName`, comportement desktop historique préservé. Compteur de visites migré de `layout.tsx` (bloc orphelin `absolute bottom-0 right-0`) vers `Footer.jsx` (ligne discrète `text-[10px] uppercase tracking-[0.3em]`). Nettoyage : state `visitCount` + useEffect déplacés, `div.max-w-5xl` vide retirée, state `count` inutilisé retiré de `Footer.jsx`. |
|
||||||
|
| 2026-04-22 | Refonte visuelle — correctif urgent `ModalGlossaire` (blocage mobile signalé sur Samsung S25 Ultra). `w-[114vw] max-w-6xl h-[72vh]` → `w-full max-w-4xl max-h-[90vh]` + `overflow-y-auto`. Fermeture ajoutée sur tap-voile et `Escape`. Bouton fermeture rond 40 px Material Symbol `close`. Alignement palette Stitch (`bg-on-surface/75`, `bg-surface-container-lowest/95`, `rounded-sheet`, `text-primary`, description `font-body` Newsreader). `"use client"` + `role="dialog" aria-modal` ajoutés. |
|
||||||
|
| 2026-04-22 | Refonte visuelle — étape 5 : home. Hero "feuillet de vellum" (`bg-surface-container-lowest/85 backdrop-blur-vellum shadow-ambient rounded-sheet`) avec grille `auto_1fr` portrait + texte. Portrait en frame primary (`bg-primary p-1 rounded-sheet`) qui remplace le cercle `rounded-full border-4`. Kicker `Portfolio · Étudiant 42 Perpignan`. Titre Manrope extrabold. `cv` Strapi rendu via ReactMarkdown + plugin typography (`prose prose-stone` custom). CTAs jewel `/portfolio` (primary shadow-jewel) + ghost `/contact` (hover `bg-primary-fixed/40`). Nouvelle section "Trois axes de travail" (3 cartes takeaway avec icônes Material Symbols `psychology`, `terminal`, `school`, contenu hardcodé dans `takeaways[]`). Pull-quote éditoriale `border-l-4 border-primary` en Newsreader italique. Double `<main>` supprimé (layout racine fournit déjà le `<main>`). |
|
||||||
|
| 2026-04-22 | Refonte visuelle — correctifs post-étape 5 (retour utilisateur). **Icônes Material Symbols affichées comme texte littéral** (ex. "psychology" visible dans le rond bleu des takeaways) : oubli de `font-family: 'Material Symbols Outlined'` dans la règle `.material-symbols-outlined` de `app/globals.css` — Google Fonts pose le `@font-face` mais pas la règle de classe. Fix : ajout de la ligne manquante, toutes les icônes (takeaways, burger, modale, CTAs) s'affichent désormais correctement. **Pull-quote "Démarche" qui se fondait dans le wallpaper** : passée en carte vellum légère (`bg-surface-container-lowest/65 backdrop-blur-vellum rounded-tile`, sans `shadow-ambient`) pour rester lisible sans écraser la variation éditoriale. **Espace trop grand entre les 3 sections** : `gap-8` → `gap-5`, `py-6 md:py-8` retiré sur la pull-quote (remplacé par le padding interne de sa carte). |
|
||||||
|
| 2026-04-22 | Refonte visuelle — correctifs post-étape 5 (2e passe). **Icônes toujours en texte littéral après le fix font-family** : URL Google Fonts `@24,400,0,0` (valeur fixe) potentiellement non résolue côté navigateur. Passée en syntaxe ranges `@20..48,100..700,0..1,-50..200` (alignée sur `stitch_V1/code.html`), fiable et documentée. **Densité verticale encore trop aérée** : `gap-5` → `gap-3` sur le container racine de la home. Paddings internes cartes resserrés : hero `p-6 sm:p-8 md:p-10` → `p-5 sm:p-7 md:p-8` ; takeaways `p-6 sm:p-8` → `p-5 sm:p-6`. Grille intérieure takeaways `gap-4` → `gap-3`. Hero texte `gap-4` → `gap-3`. En-tête de section takeaways `mb-6` → `mb-4`. |
|
||||||
|
| 2026-04-22 | **Diagnostic critique via DevTools Network (Chrome desktop + mobile)** : aucune des trois Google Fonts (Manrope, Newsreader, Material Symbols) n'était réellement chargée par le navigateur — les `@import url('https://fonts.googleapis.com/...')` dans `app/globals.css` étaient strippés par la chaîne PostCSS + Tailwind de Next 15 en production. Conséquence : tout le site tournait en fallback Arial / Georgia depuis l'étape 3, la typo éditoriale Stitch n'était jamais réellement visible. **Fix phase 1 (fonts textuelles)** : création de `app/fonts.ts` qui exporte Manrope et Newsreader via `next/font/google` (téléchargement au build, service depuis le domaine du site, plus de dépendance CDN externe). `app/layout.tsx` importe ces fonts et pose `${manrope.variable} ${newsreader.variable}` sur le `<html>`. `tailwind.config.ts` : `font-headline` et `font-body` repointés vers `var(--font-manrope)` et `var(--font-newsreader)`. `app/globals.css` : 2 `@import` inopérants supprimés ; l'`@import` Material Symbols reste temporairement en attendant la phase 2. Validation DevTools Computed : `font-family: Manrope, "Manrope Fallback", system-ui, sans-serif` confirmé sur le h1 de la home. |
|
||||||
|
| 2026-04-22 | **Fix phase 1b** : font par défaut du `body` dans `app/globals.css` passée de `Arial, Helvetica, sans-serif` à `var(--font-newsreader), Georgia, serif`. Tout élément sans classe typo explicite hérite désormais de Newsreader (corps éditorial Stitch) au lieu d'Arial. **Fix phase 2 (Material Symbols)** : `@import url(...)` Material Symbols retiré de `app/globals.css` (strippé), remplacé par un triplet `<link rel="preconnect"> + <link rel="stylesheet">` injecté dans le `<head>` de `app/layout.tsx` — contourne le pipeline PostCSS tout en conservant le CDN Google pour la font-icon (usage très ponctuel : 5 icônes sur la home, 2 sur le burger, 1 sur la modale). Validé en production : icônes + fonts OK sur Chrome desktop. |
|
||||||
|
| 2026-04-22 | **Correctif Chrome Auto-Translate mobile** (retour utilisateur : layout décalé + icônes redevenues du texte littéral sur Chrome mobile quand la traduction auto s'active). Cause : les icônes Material Symbols fonctionnent via ligatures de font (`<span>psychology</span>` → glyphe cerveau) ; Chrome traduit le texte `psychology` en `psychologie`, la ligature ne match plus, l'icône redevient texte. Fix : `translate="no"` ajouté sur les 8 `<span className="material-symbols-outlined">` (5 dans `app/page.tsx`, 2 dans `app/layout.tsx`, 1 dans `app/components/ModalGlossaire.tsx`) + sur le titre header (nom propre `Portfolio Gras-Calvet Fernand`). La traduction automatique reste globalement active pour le contenu éditorial (CV, compétences, projets) — seuls les éléments que la traduction casse sont protégés. |
|
||||||
|
| 2026-04-22 | **Correctif séparateurs `<hr>` du hero** (retour utilisateur : « trop d'espace entre les sections » en fait dû à des `<hr>` quasi invisibles générés depuis les `---` Markdown du CV Strapi). Option B retenue : surcharge `prose-hr` sur le wrapper `ReactMarkdown` (`app/page.tsx`) pour transformer la règle en pastille Stitch centrée (`prose-hr:border-0 prose-hr:w-16 prose-hr:mx-auto prose-hr:bg-primary/30 prose-hr:h-0.5 prose-hr:rounded-full prose-hr:my-6`). Le séparateur redevient un signal visuel intentionnel et cohérent avec la palette, marges verticales réduites de 32 px à 24 px. Détail dans `REFONTE-VISUELLE.md` §4 sexies. |
|
||||||
|
| 2026-04-22 | **Correctif post-étape 6 : réintroduction du défilement auto en vignette**. L'arbitrage initial "carousel retiré des listes" retiré après retour utilisateur (*« j'ai perdu ma fonctionnalité précédente »*). Nouveau composant `app/components/VignetteCarousel.tsx` : Swiper allégé (modules `Autoplay` + `Pagination` uniquement), **sans flèches** (conflit de clic avec le `<Link>` englobant) et **sans lightbox** (réservée à la fiche détail). Pagination bullets teintée `primary` via surcharge inline des variables CSS Swiper (`--swiper-pagination-color: #26445d`) pour ne pas polluer `globals.css`. Autoplay 3500 ms, `loop` conditionnel (`length > 1` pour éviter le warning Swiper sur les entrées mono-image). Intégré dans `app/portfolio/page.jsx` et `app/competences/page.jsx` via `images.length > 1 ? <VignetteCarousel /> : <img statique />` — comportement identique à l'ancien pour les mono-image, défilement retrouvé pour les multi-image. Les composants `Carousel.tsx` et `CarouselCompetences.tsx` existants restent intacts pour la fiche détail (étape 7). Détail dans `REFONTE-VISUELLE.md` §6 *"Correctif post-étape 6 — réintroduction du défilement automatique en vignette"*. |
|
||||||
|
| 2026-04-22 | **Correctif wallpaper sur-zoomé sur pages longues** (retour utilisateur post-étape 6 : incohérence visuelle entre la home et `/portfolio`). Cause : `.bg-wallpaper` en `absolute inset-0` à l'intérieur du conteneur grid `min-h-[100dvh]` — le conteneur s'étirant à la hauteur du contenu (≈ 2-3 viewports sur les listes), `background-size: cover` zoomait l'image pour couvrir toute cette hauteur. Fix : wallpaper sorti du grid, passé en `fixed inset-0 z-0 pointer-events-none` (calé sur le viewport, plus la page entière). Les cercles animés restent en `absolute` dans le grid. Impact transversal sur toutes les pages longues (fiches détail à venir, etc.). Détail dans `REFONTE-VISUELLE.md` §6. |
|
||||||
|
| 2026-04-22 | **Vault GrasBot — correctif note CV + protection `source: manual` effective**. La note `vault-grasbot/30-Parcours/cv-grascalvet-fernand.md` était produite par `pypdf` avec des espaces entre chaque caractère (mise en page Canva du PDF source) → illisible. Réécriture manuelle en Markdown structuré à partir du contenu PDF : sections Identité / Contact / Présentation / Objectifs alternance / Expérience pro (42 → Infirmier → Ostréiculture) / Compétences (langages, IA/LLM, systèmes) / Langues / Intérêts (hardware, 3D, domotique, IA). Wikilinks vers les projets 42 et les compétences (`[[libft]]`, `[[get-next-line]]`, `[[inception]]`, `[[ia]]`, `[[impression-3d]]`, etc.). Frontmatter passé en `source: manual` + `domains: [parcours, ecole-42, ia, 3d, domotique]`. **Correctif de fond dans `strapi_extraction/build-vault.py`** : la règle « ne pas écraser les notes `source: manual` » était documentée mais pas implémentée — `write_notes()` écrasait systématiquement. Ajout de `_existing_source()` qui lit le frontmatter existant + skip avec log `⏭` si `source: manual`. La règle est désormais **effective** (vérifié en `--dry-run` : la note CV curatée est bien préservée). |
|
||||||
|
| 2026-04-22 | **Chatbot GrasBot — migration Mistral → Qwen3 + RAG sur vault Obsidian local**. Passage du modèle `mistral` à `qwen3:8b` dans `llm-api/api.py` (Q4_K_M, ~5 Go VRAM RTX 2080 Ti). Embeddings via `nomic-embed-text` (~500 Mo VRAM, multilingue FR). Nouveau pipeline RAG : `llm-api/rag.py` (embed / retrieve / build_prompt / generate / answer), `llm-api/index_vault.py` (parse frontmatter YAML, chunking par h2 au-delà de 3000 chars, upsert ChromaDB batch 32), `llm-api/requirements.txt` (fastapi, uvicorn, requests, chromadb, pyyaml). Nouveau script `strapi_extraction/build-vault.py` qui convertit `strapi_extraction/docs/*.md` + CV PDF (via `pypdf`) en vault Obsidian structuré `vault-grasbot/` : frontmatter YAML (type, source, domains, tags, linked, related, visibility), wikilinks vers les MOCs, MOCs auto-générés par type et par domaine (15 MOCs). Bootstrap v1 du vault : 17 projets, 4 compétences, 1 CV, 15 MOCs auto + 1 manuel (Technique), 3 notes auto-doc dans `50-Technique/` (architecture-site, grasbot-rag, vault-structure) pour que GrasBot puisse se présenter lui-même. Compatibilité ascendante `askAI.js`/`ChatBot.js` via le champ `response` conservé ; les `sources`, `rag`, `model` ajoutés sont non destructifs. Endpoint `/health` ajouté pour debug. Doc : nouveau [`08-vault-obsidian-rag.md`](./docs-site-interne/08-vault-obsidian-rag.md), mise à jour de [`04-api-llm-et-chatbot.md`](./docs-site-interne/04-api-llm-et-chatbot.md) et [`06-strapi-extraction.md`](./docs-site-interne/06-strapi-extraction.md). Fragilités préexistantes repérées (cleaner `homepages` absent, content-type `glossaire` non extrait) consignées mais non corrigées dans ce lot — à traiter lors du prochain enrichissement vault. |
|
||||||
|
| 2026-04-22 | Refonte visuelle — **étape 7 : fiches détail + glossaire + GrasBot flottant**. Cinq sous-lots. **7.a** : `Carousel.tsx` + `CarouselCompetences.tsx` harmonisés (pagination bullets primary via variables CSS Swiper, flèches primary, `rounded-tile shadow-ambient-sm`, autoplay 3500 ms + `loop` conditionnel, lightbox Stitch refaite avec voile `bg-on-surface/80`, image `object-contain rounded-sheet`, bouton close rond Material Symbol + Escape + verrouillage scroll body). **7.b** : `ContentSection.tsx` (fiche portfolio) — gabarit vellum cohérent avec la home/listes, pastille retour `arrow_back`, kicker `Projet · Portfolio`, titre Manrope, carousel détail plein cadre, corps Markdown en `prose` Stitch (mêmes overrides que la home, y compris pastille `prose-hr`), CTA externe jewel avec `open_in_new`, états loading/404 en vellum. **7.c** : `ContentSectionCompetences.tsx` + container — même gabarit. Refactor glossaire : styles inline `style="color:red/blue"` remplacés par les classes `.glossary-keyword` / `.chatbot-keyword` (ajoutées à `globals.css`, couleur primary + underline dotted offset 3 px). Event listeners `document.body.addEventListener` remplacés par un listener unique scopé au wrapper `contentRef`. Le clic sur « IA locale » ne monte plus un `<ChatBot>` local mais dispatch `window.dispatchEvent(new CustomEvent("grasbot:open"))` capté par le FAB global (7.e). Container refait avec skeleton vellum à la place de `⏳ Chargement...`. **7.d** : `ChatBot.js` entièrement restylé (carte vellum `rounded-sheet shadow-ambient backdrop-blur-vellum`, header primary avec Material Symbol `smart_toy` + sous-titre « Assistant IA locale », bulles user `bg-primary text-white` et bot `bg-surface-container`, input Stitch avec `focus-visible:ring-primary`, bouton envoyer rond jewel Material Symbol `send`, auto-scroll, focus auto, envoi Enter, disabled pendant attente, message d'accueil vide éditorial). **7.e** : nouveau composant `app/components/GrasBotFab.tsx` — FAB jewel `fixed bottom-6 right-6 z-30` rond 56/64 px, `bg-primary shadow-jewel` Material Symbol `smart_toy`/`close`, monté dans `app/layout.tsx` → chatbot accessible depuis **toutes les pages** (plus seulement fiches compétences). Écoute `CustomEvent("grasbot:open")` dispatché depuis le keyword « IA locale ». Fermeture Escape globale, panneau responsive plein largeur mobile / 384 px desktop. Détails dans `REFONTE-VISUELLE.md` §7. |
|
||||||
|
| 2026-04-22 | Refonte visuelle — **étape 6 : listes portfolio + compétences**. `app/portfolio/page.jsx` et `app/competences/page.jsx` entièrement réécrits. En-tête éditorial (kicker + titre Manrope extrabold + pitch Newsreader) cohérent avec le hero de la home. Grille **asymétrique 2/3 + 1/3** alternée (`md:grid-cols-6` + pattern de `col-span-4`/`col-span-2` sur modulo 4, `sm:grid-cols-2`, `grid-cols-1` mobile) — conforme DESIGN.md §6 "No-Grid-Lock". Cartes « feuillet vellum » alignées home : `rounded-sheet bg-surface-container-lowest/85 backdrop-blur-vellum shadow-ambient`, image `aspect-[4/3]` fixe avec `group-hover:scale-[1.03]`, titre `text-primary`, description `line-clamp-3` en Newsreader, CTA tertiaire « Découvrir → » / « Explorer → » avec Material Symbol `arrow_forward` qui se décale au hover (`translate="no"` appliqué). Hover : `hover:-translate-y-0.5 hover:shadow-jewel` (remplace le `scale-105` qui débordait). **`Swiper` retiré des vignettes de liste** (arbitrage acté § 2 : carousel réservé aux galeries intra-fiche) — une seule image par carte, `loading="lazy"`. États ajoutés : skeletons animés respectant la grille + état vide avec Material Symbol. Régressions corrigées au passage : largeur fixe `w-80` qui débordait sur S25 Ultra, `hover:scale-105` qui tapait sous le header, classes `bg-white/80 rounded-lg` remplacées par les tokens Stitch. Les composants `Carousel.tsx` et `CarouselCompetences.tsx` restent en place pour les fiches détail (étape 7). Détail dans `REFONTE-VISUELLE.md` §6. |
|
||||||
|
| 2026-04-23 | **GrasBot — tuning pipeline LLM + anti-hallucinations**. Audit des premières traces Langfuse : questions biographiques hallucinées (âge erroné, statut inventé), réponses longues tronquées. Quatre ajustements : (1) `llm-api/search.py` · `generate()` — `num_ctx=8192` explicite (stoppe la troncature silencieuse du prompt par le défaut Ollama 2048/4096 quand plusieurs notes entières sont injectées), `num_predict` 512 → 1024 (réponses longues complètes), `think: false` top-level (désactive le *thinking mode* de qwen3 qui consommait du budget de sortie). (2) `llm-api/search.py` · `build_prompt()` — troncature conditionnelle des sources rank 2+ via `_truncate_body()` + nouvelles variables `SEARCH_SECONDARY_MAX_CHARS` (1500) / `SEARCH_SECONDARY_KEEP_RATIO` (0.8). Aucune source n'est supprimée, seules celles dont le score est < 0.8 × score(#1) ET dont le body dépasse 1500 chars sont résumées. Loggé dans `prompt_build.metadata.truncation`. (3) Vault — nouvelle note `vault-grasbot/30-Parcours/bio-fernand.md` courte et factuelle (priority 10, aliases biographiques courts), canonique pour les questions du type *« qui est Fernand »*. Renvoie vers le CV complet pour le détail. Correction incohérence d'âge dans le CV (46 → 47 ans dans la section Présentation) qui alimentait les hallucinations. (4) `SYSTEM_PROMPT` — nouveau bloc *Règles de fidélité aux sources* : priorité `type=parcours` pour questions bio, interdiction d'inventer des faits factuels, gestion explicite des contradictions, signalement des notes tronquées. **Bascule Langfuse v4 → v3 dans `requirements.txt`** (`langfuse>=3.0,<4`) : le SDK v4 a supprimé `start_as_current_span`, la v3 reste compatible avec l'instrumentation existante. Dépendances Python ajoutées : `langfuse`, `python-dotenv`. Secrets Langfuse déplacés de `.env.local` Next vers `llm-api/.env` (non committé). Doc mise à jour : [`langfuse-observability.md`](./langfuse-observability.md) (nouvelle section *Tuning du pipeline — 2026-04-23*), `CONFIGURATION_SITE.md` (endpoints `/health` + `/reload-vault`), `etat-actuel.md` (42 notes + mention Langfuse). |
|
||||||
|
| 2026-04-22 | **GrasBot v3 — bascule RAG vectoriel → retrieval graph + BM25**. Essais d'installation Windows bloqués par `chroma-hnswlib` (compilation C++ requise) et freezes RDP à chaque chargement de `qwen3:8b` + `nomic-embed-text` simultanément. Arbitrage : pour un vault de 40 notes, la RAG vectorielle sur-dimensionne ; on exploite directement la structure Obsidian (frontmatter, wikilinks, MOCs). **Nouveau pipeline** dans `llm-api/search.py` (scoring multi-signaux : aliases / titre-slug / answers / domains / tags / BM25 ; expansion par graphe via `linked`/`related`/wikilinks ; tokenizer FR avec normalisations `c++` → `cpp`, split `-`/`_`). **Déterministe, traçable (champ `reasons` dans les sources), 50 ms de retrieval**. Scoring calibré sur 12 cas (IA, push-swap, LLMs pluriel, hors-sujet clafoutis → `(aucun)`, etc.). **Dépendances allégées** : fini `chromadb`, `chroma-hnswlib`, `nomic-embed-text`. `requirements.txt` = fastapi + uvicorn + requests + pyyaml uniquement. Fichiers supprimés : `llm-api/rag.py`, `llm-api/index_vault.py`, `chroma-index/` (marqué pour suppression, verrouillé par Cursor au moment du cleanup — sera supprimé au reboot). **Vault enrichi** : `build-vault.py` étendu pour générer automatiquement `aliases` (à partir du slug/titre + `DOMAIN_ALIASES`), `answers` (questions-types adaptées au type de note), `priority` (heuristique CV=10, MOCs=7, compétences=7, projets=5). Note CV curatée (`source: manual`) enrichie manuellement avec 12 aliases et 7 answers. Nouvelle `vault-grasbot/TAXONOMIE.md` qui documente le vocabulaire contrôlé. Réécriture de `vault-grasbot/50-Technique/grasbot-rag.md` → `grasbot-retrieval.md` (nouveau pipeline), + `architecture-site.md` + `vault-structure.md` + `MOC-Technique.md`. Nouveau endpoint `POST /reload-vault` pour recharger sans redémarrer uvicorn. Documentation interne refaite : [`04-api-llm-et-chatbot.md`](./04-api-llm-et-chatbot.md), [`06-strapi-extraction.md`](./06-strapi-extraction.md), nouveau [`08-vault-obsidian-retrieval.md`](./08-vault-obsidian-retrieval.md) (remplace `08-vault-obsidian-rag.md`). |
|
||||||
|
| 2026-04-24 | **Doc + configuration** : routes `/competences/[slug]/[realisation]`, entité Strapi `realisation-ia`, tri `order` et comportement vignettes/richtext documentés dans `02-frontend-next.md`, `04-api-llm-et-chatbot.md` (parcours public), `etat-actuel.md` ; `CONFIGURATION_SITE.md` : section *Contenu : compétences, réalisations IA et ordre d’affichage*. Décision : **pas de « phase 3 » homelab** sur le site (consignée dans l’état actuel). |
|
||||||
227
obsidian-site-docs/docs-site-interne/langfuse-observability.md
Normal file
@ -0,0 +1,227 @@
|
|||||||
|
# Observabilité GrasBot via Langfuse
|
||||||
|
|
||||||
|
**Créé :** 2026-04-23
|
||||||
|
**Statut :** en production
|
||||||
|
**Pré-requis lecture :** `08-vault-obsidian-retrieval.md` (architecture du pipeline graph + BM25).
|
||||||
|
|
||||||
|
## Vue d'ensemble
|
||||||
|
|
||||||
|
Le chatbot GrasBot est instrumenté avec **Langfuse** (instance self-hosted : `langfuse.fernandgrascalvet.com`) pour tracer **chaque requête visiteur** bout en bout :
|
||||||
|
|
||||||
|
- **Retrieval** : quelles notes du vault ont été remontées, avec quels scores, pour quelles raisons.
|
||||||
|
- **Prompt** : le system + user effectivement envoyés à Qwen3.
|
||||||
|
- **Génération** : latence, tokens, paramètres du modèle.
|
||||||
|
- **Trace globale** : question, réponse, sources, scores dérivés (grounded, retrieval_relevance), tags.
|
||||||
|
|
||||||
|
But : **debug**, **monitoring** (qualité/latence dans le temps), et **itération** sur le pipeline retrieval en voyant directement l'effet d'un changement de règle de scoring.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────┐ ┌──────────────────────────┐
|
||||||
|
│ ChatBot.js (front) │ │ Langfuse self-hosted │
|
||||||
|
│ - grasbotIds.js │ │ langfuse.fernandgrasc… │
|
||||||
|
│ - user_id localStorage│───▶│ │
|
||||||
|
│ - session_id sessionSt │ │ (ingestion HTTPS) │
|
||||||
|
└──────┬──────────────────┘ │ │
|
||||||
|
│ │ │
|
||||||
|
▼ │ ▲ │
|
||||||
|
┌─────────────────────────┐ │ │ SDK Python │
|
||||||
|
│ app/api/proxy/route.js │ │ │ (observability │
|
||||||
|
│ whitelist + GET fwd │ │ │ .py) │
|
||||||
|
└──────┬──────────────────┘ └─────────┼────────────────┘
|
||||||
|
│ │
|
||||||
|
▼ │
|
||||||
|
┌─────────────────────────┐ │
|
||||||
|
│ FastAPI /ask │ │
|
||||||
|
│ (llm-api/api.py) │ │
|
||||||
|
│ → @observe via Langfuse─────────────┘
|
||||||
|
│ → search.answer() │
|
||||||
|
└──────┬──────────────────┘
|
||||||
|
│
|
||||||
|
├── retrieval (span)
|
||||||
|
├── prompt_build (span)
|
||||||
|
└── ollama-chat (generation)
|
||||||
|
```
|
||||||
|
|
||||||
|
L'instrumentation **vit côté Python** (couche où on a accès aux détails du retrieval et du prompt). Le proxy Next ne fait que relayer le `session_id` / `user_id` depuis le front jusqu'à l'API Python.
|
||||||
|
|
||||||
|
## Fichiers concernés
|
||||||
|
|
||||||
|
| Fichier | Rôle |
|
||||||
|
|---------|------|
|
||||||
|
| `llm-api/observability.py` | Init client Langfuse (no-op safe si clés absentes) + `flush()` au shutdown |
|
||||||
|
| `llm-api/api.py` | FastAPI `/ask` — query params `session_id`/`user_id` + `lifespan` pour flush |
|
||||||
|
| `llm-api/search.py` | Spans `retrieval` + `prompt_build` + `generation`, trace racine `ask`, scores auto |
|
||||||
|
| `llm-api/.env` | Secrets Langfuse (non committé) |
|
||||||
|
| `llm-api/.env.example` | Template documentaire |
|
||||||
|
| `app/utils/grasbotIds.js` | Génération UUID v4 anonymes (localStorage + sessionStorage) |
|
||||||
|
| `app/utils/askAI.js` | Passe `session_id`/`user_id` en query params |
|
||||||
|
| `app/api/proxy/route.js` | Whitelist `q`, `session_id`, `user_id` → forward vers API Python |
|
||||||
|
|
||||||
|
## Variables d'environnement (côté Python uniquement)
|
||||||
|
|
||||||
|
Dans **`llm-api/.env`** (chargé automatiquement par `observability.py` via `python-dotenv`) :
|
||||||
|
|
||||||
|
| Variable | Obligatoire | Notes |
|
||||||
|
|----------|-------------|-------|
|
||||||
|
| `LANGFUSE_PUBLIC_KEY` | oui | Format `pk-lf-…` |
|
||||||
|
| `LANGFUSE_SECRET_KEY` | oui | Format `sk-lf-…` — **JAMAIS dans un log/commit/chat** |
|
||||||
|
| `LANGFUSE_BASE_URL` | oui | URL du self-hosted (ex. `https://langfuse.fernandgrascalvet.com`) |
|
||||||
|
| `LANGFUSE_HOST` | fallback | Alternative à `BASE_URL` si jamais on passe sur le cloud Langfuse |
|
||||||
|
|
||||||
|
Si **l'une des 3** est absente → `observability.py` instancie un **client no-op** : l'API fonctionne normalement, aucune trace n'est envoyée, aucune erreur. Pratique pour dev local / contributeurs externes.
|
||||||
|
|
||||||
|
Les variables Langfuse **ne sont pas** dans `.env.local` de Next.js — elles ne servent qu'au backend Python.
|
||||||
|
|
||||||
|
## Structure d'une trace
|
||||||
|
|
||||||
|
### Trace racine : `ask`
|
||||||
|
- **input** : `{ query: "..." }`
|
||||||
|
- **output** : `{ response, sources_count, grounded }`
|
||||||
|
- **metadata** : `{ top_k, min_score }`
|
||||||
|
- **session_id**, **user_id** (propagés depuis le front)
|
||||||
|
- **tags** : `grounded`|`ungrounded`, `model:qwen3:8b`, `vault-miss` (si aucune note scorée)
|
||||||
|
- **scores** auto :
|
||||||
|
- `grounded` (BOOLEAN, 0/1) : au moins 1 note ≥ `MIN_SCORE`
|
||||||
|
- `retrieval_relevance` (NUMERIC, 0-1) : `min(max_score / 15, 1)`
|
||||||
|
|
||||||
|
### Span `retrieval`
|
||||||
|
- **input** : `{ query, top_k }`
|
||||||
|
- **output** : `[{slug, title, type, score, reasons}, …]` — top-K final après expansion
|
||||||
|
- **metadata** :
|
||||||
|
- `query_tokens` : tokens extraits par `tokenize_fr`
|
||||||
|
- `vault_size` : nombre de notes publiques chargées
|
||||||
|
- `candidates_with_signal` : combien de notes ont eu un score > 0
|
||||||
|
- `seeds_before_graph` : top-3 avant expansion par graphe
|
||||||
|
- `bm25_stats` : `{N, avgdl, idf_terms}` (pour debug de régressions BM25)
|
||||||
|
- `elapsed_ms` : durée du retrieval seul
|
||||||
|
|
||||||
|
### Span `prompt_build`
|
||||||
|
- **input** : `{ query, scored_count }`
|
||||||
|
- **output** : `{ system, user }` — le **prompt complet** envoyé à Qwen
|
||||||
|
- **metadata** :
|
||||||
|
- `grounded` : bool (= au moins 1 note ≥ MIN_SCORE)
|
||||||
|
- `relevant_notes` : notes effectivement incluses dans le contexte
|
||||||
|
- `system_chars`, `user_chars` : tailles utiles pour debug de fenêtre de contexte
|
||||||
|
- `min_score_threshold` : valeur du `MIN_SCORE` au moment de l'appel
|
||||||
|
- `truncation` : `{ secondary_max_chars, secondary_keep_ratio, truncated_notes: [...] }` —
|
||||||
|
liste des sources rank 2+ résumées automatiquement (avec leur slug, score,
|
||||||
|
taille d'origine et taille tronquée). Vide s'il n'y a eu aucune troncature.
|
||||||
|
|
||||||
|
### Span `ollama-chat` (type **generation**)
|
||||||
|
- **input** : `[{role: "system", content}, {role: "user", content}]`
|
||||||
|
- **output** : réponse brute du modèle
|
||||||
|
- **model** : `LLM_MODEL` (ex. `qwen3:8b`)
|
||||||
|
- **model_parameters** : `{temperature: 0.4, num_ctx: 8192, num_predict: 1024, think: false}`
|
||||||
|
(voir section "Tuning 2026-04-23" ci-dessous pour le rationnel).
|
||||||
|
- **usage** : `{input, output, total}` — extraits de `prompt_eval_count` / `eval_count` si Ollama les renvoie
|
||||||
|
- Si réponse vide → span `level: ERROR` avec le payload Ollama brut en metadata.
|
||||||
|
|
||||||
|
## Session / User IDs (côté front)
|
||||||
|
|
||||||
|
**Pas de PII**, **pas d'authentification**. Deux UUID v4 anonymes générés automatiquement à la première interaction :
|
||||||
|
|
||||||
|
- **`grasbot_user_id`** → `localStorage` → stable par device, sert à mesurer les utilisateurs uniques et à regrouper l'historique d'un visiteur récurrent.
|
||||||
|
- **`grasbot_session_id`** → `sessionStorage` → expire à la fermeture de l'onglet, regroupe une conversation.
|
||||||
|
|
||||||
|
Générés par `app/utils/grasbotIds.js`, propagés par `askAI.js` → `/api/proxy` (whitelist) → `/ask` (query params) → `search.answer()` (`update_current_trace(session_id=…, user_id=…)`).
|
||||||
|
|
||||||
|
**Impact RGPD** : aucun identifiant déductible de l'utilisateur, aucune donnée persistante côté serveur autre que ce que Langfuse stocke de lui-même. L'utilisateur peut vider son storage pour "réinitialiser" son identité côté observabilité.
|
||||||
|
|
||||||
|
## Procédure de test
|
||||||
|
|
||||||
|
### Local
|
||||||
|
|
||||||
|
1. `cd llm-api && pip install -r requirements.txt` (ajoute `langfuse` + `python-dotenv`).
|
||||||
|
2. Remplir `llm-api/.env` avec les 3 clés (ou laisser vide pour tester le mode no-op).
|
||||||
|
3. `.\start-my-site.ps1` (ou démarrer uvicorn manuellement).
|
||||||
|
4. Aller sur `http://localhost:3000` → ouvrir le chatbot (FAB en bas à droite) → poser une question.
|
||||||
|
5. Dans Langfuse → **Traces** → voir apparaître une trace `ask` en temps réel (quelques secondes après la réponse, le temps du flush).
|
||||||
|
|
||||||
|
### Vérifier le no-op silencieux
|
||||||
|
|
||||||
|
1. Commenter les 3 variables `LANGFUSE_*` dans `llm-api/.env`.
|
||||||
|
2. Redémarrer uvicorn → les logs affichent `ℹ️ Langfuse désactivé — variables manquantes : …`.
|
||||||
|
3. Poser une question au chatbot → réponse normale, aucun crash.
|
||||||
|
4. `GET /health` renvoie `{"observability": {"langfuse_enabled": false}}`.
|
||||||
|
|
||||||
|
### Scénarios utiles à reproduire dans Langfuse
|
||||||
|
|
||||||
|
- **Question grounded classique** : "Parle-moi de push-swap" → tags `grounded`, retrieval_relevance ~0.7-0.9.
|
||||||
|
- **Question hors-sujet** : "Quel temps fait-il demain ?" → tags `ungrounded`, grounded=0, sources_count=0 ou voisins faibles.
|
||||||
|
- **Question sur mot-clé ambigu** : "C" (langage C vs lettre C) → voir dans le span `retrieval` comment `_keyword_matches` filtre ou non.
|
||||||
|
|
||||||
|
## Dashboards Langfuse utiles
|
||||||
|
|
||||||
|
### Qualité du retrieval dans le temps
|
||||||
|
Dashboard → filtrer `score: grounded` → voir le **taux de grounded** par jour. Une chute = problème de vault ou de scoring.
|
||||||
|
|
||||||
|
### Latence p95
|
||||||
|
Dashboard → `latency` sur trace `ask` ou span `ollama-chat`. La génération est **la source de latence majoritaire** (≥ 90%), le retrieval reste sous ~100ms.
|
||||||
|
|
||||||
|
### Questions sans contexte pertinent
|
||||||
|
Filtrer tags = `ungrounded` → voir les questions posées mais non couvertes par le vault → **source d'insights pour enrichir le vault** (nouveaux alias, nouvelles notes).
|
||||||
|
|
||||||
|
### Sessions longues
|
||||||
|
Filtrer par `session_id` → enchaînement des questions d'un visiteur → voir si GrasBot garde la cohérence (pas de mémoire entre requêtes, attendu).
|
||||||
|
|
||||||
|
## Conventions de nommage
|
||||||
|
|
||||||
|
- **Spans** : kebab-case en anglais (`retrieval`, `prompt-build`, `ollama-chat`). Ici `prompt_build` a été laissé en snake pour rappeler la fonction Python, à remplacer par `prompt-build` si on refait un coup de ménage.
|
||||||
|
- **Tags** : kebab-case, préfixés par concept (`model:qwen3:8b`, `vault-miss`).
|
||||||
|
- **Scores** : snake_case nom simple (`grounded`, `retrieval_relevance`), + ajoutera plus tard `user_feedback` si on branche un 👍/👎.
|
||||||
|
|
||||||
|
## Rollback
|
||||||
|
|
||||||
|
Si Langfuse tombe en panne ou si l'instrumentation pose un souci :
|
||||||
|
|
||||||
|
1. **Soft rollback** : vider / commenter les variables `LANGFUSE_*` dans `llm-api/.env` et redémarrer uvicorn. Le client passe en no-op, aucun autre changement nécessaire.
|
||||||
|
2. **Hard rollback** : `git revert` du commit d'intégration Langfuse. Les fichiers `observability.py` / `.env` / `grasbotIds.js` disparaîtront ; le pipeline revient exactement à la v3.0.
|
||||||
|
|
||||||
|
## Sécurité — rappels
|
||||||
|
|
||||||
|
- **`LANGFUSE_SECRET_KEY`** permet d'écrire dans toutes les traces du projet → équivaut à un droit d'admin partiel. Jamais en clair dans un chat, un log, un screenshot, un commit.
|
||||||
|
- **Rotation** : en cas de doute, **Project Settings → API Keys → Delete** puis recréer. Les traces déjà ingérées ne sont pas affectées.
|
||||||
|
- Le client Langfuse envoie les traces **en asynchrone** avec un buffer → bien appeler `flush()` au shutdown pour ne rien perdre (déjà fait via le `lifespan` FastAPI).
|
||||||
|
- **Contenu sensible** : les prompts complets passent dans Langfuse. Vérifier que **le vault ne contient pas d'infos privées** (`visibility: private` est filtré côté search, mais si tu ajoutais un jour un vault mixte public/privé, il faudrait un filtre supplémentaire avant l'envoi à Langfuse).
|
||||||
|
|
||||||
|
## Tuning du pipeline — 2026-04-23
|
||||||
|
|
||||||
|
Audit des premières traces après mise en production : les réponses sur les
|
||||||
|
questions biographiques ("qui est Fernand ?") étaient parfois **hallucinées**
|
||||||
|
(âge erroné, statut inventé) et les réponses longues **tronquées** en plein
|
||||||
|
milieu. Quatre ajustements ciblés ont stabilisé le comportement :
|
||||||
|
|
||||||
|
| # | Fichier | Changement | Effet attendu |
|
||||||
|
|---|---------|------------|---------------|
|
||||||
|
| 1 | `search.py` · `generate()` | `num_ctx` explicite à **8192** | Fin de la troncature silencieuse du prompt (le défaut Ollama à 2048/4096 coupait le début du contexte quand plusieurs notes entières étaient injectées). |
|
||||||
|
| 1 | `search.py` · `generate()` | `num_predict` **512 → 1024** | Réponses longues (descriptions de projet, explications) ne sont plus coupées en plein milieu. |
|
||||||
|
| 1 | `search.py` · `generate()` | `think: false` **top-level** | Désactive le mode *thinking* de qwen3. Le modèle n'utilise plus de budget de sortie pour du raisonnement interne. |
|
||||||
|
| 2 | `search.py` · `build_prompt()` | Troncature conditionnelle des sources **rank 2+** | Les notes secondaires (ex. `inception` sur une question bio) sont résumées à `SEARCH_SECONDARY_MAX_CHARS` chars quand leur score est < `SEARCH_SECONDARY_KEEP_RATIO` × score(#1). Réduit le bruit sans supprimer de source. |
|
||||||
|
| 3 | `vault-grasbot/30-Parcours/bio-fernand.md` | **Nouvelle note** dédiée à la présentation courte | Source canonique pour les questions du type *"qui est Fernand"*. Priorité 10, aliases biographiques courts. Renvoie vers le CV complet pour le détail. |
|
||||||
|
| 3 | CV (`cv-grascalvet-fernand.md`) | Incohérence d'âge corrigée (46 → 47 ans) | Supprime la contradiction interne qui alimentait les hallucinations sur l'âge. |
|
||||||
|
| 4 | `search.py` · `SYSTEM_PROMPT` | Section "Règles de fidélité aux sources" | Force le modèle à (a) s'appuyer en priorité sur `type=parcours` pour les questions bio, (b) ne jamais inventer un fait factuel, (c) écrire *« non précisé dans les notes »* si l'info manque, (d) gérer les contradictions, (e) signaler les notes tronquées. |
|
||||||
|
|
||||||
|
Observabilité : dans les spans Langfuse, `prompt_build.metadata.truncation`
|
||||||
|
liste chaque source tronquée automatiquement → sert de point de vigilance pour
|
||||||
|
vérifier que la troncature reste pertinente (et n'écrase pas une source qu'on
|
||||||
|
aurait dû garder entière).
|
||||||
|
|
||||||
|
Variables d'environnement associées (dans `llm-api/.env` ou shell) :
|
||||||
|
|
||||||
|
| Variable | Défaut | Effet |
|
||||||
|
|----------|--------|-------|
|
||||||
|
| `SEARCH_SECONDARY_MAX_CHARS` | `1500` | Taille max des sources secondaires dans le prompt |
|
||||||
|
| `SEARCH_SECONDARY_KEEP_RATIO` | `0.8` | Tant que score(rank≥2) ≥ ratio × score(#1) → source gardée entière |
|
||||||
|
|
||||||
|
Rappel : `load_vault()` est mémoïsé. Après création/modification d'une note du
|
||||||
|
vault, appeler `POST /reload-vault` pour recharger le cache sans redémarrer
|
||||||
|
uvicorn (voir `api.py`).
|
||||||
|
|
||||||
|
## Évolutions futures possibles
|
||||||
|
|
||||||
|
- **Feedback utilisateur** : ajouter un 👍/👎 sur chaque réponse bot dans `ChatBot.js`, relayé à `/api/feedback` qui appellerait `langfuse.score(trace_id, name="user_feedback", value=1|0)`. Le `trace_id` serait retourné par `/ask` (actuellement omis).
|
||||||
|
- **Prompt versioning** : stocker `SYSTEM_PROMPT` dans Langfuse Prompts pour versionner et A/B tester sans redéploiement.
|
||||||
|
- **Coût / token pricing** : si on branche un provider payant (OpenAI / Anthropic) à la place d'Ollama, Langfuse calcule automatiquement le coût à partir de l'`usage`.
|
||||||
|
- **Dataset d'évaluation** : capturer les meilleures traces comme dataset, puis relancer le pipeline sur ces mêmes questions après modif du scoring pour comparer les sorties.
|
||||||
@ -59,7 +59,12 @@ DOMAIN_KEYWORDS: dict[str, list[str]] = {
|
|||||||
"web": ["next.js", "nextjs", "react", "django", "api rest", "websocket", "strapi"],
|
"web": ["next.js", "nextjs", "react", "django", "api rest", "websocket", "strapi"],
|
||||||
"devops": ["docker", "nginx", "mariadb", "wordpress", "inception", "conteneur"],
|
"devops": ["docker", "nginx", "mariadb", "wordpress", "inception", "conteneur"],
|
||||||
"securite": ["born2beroot", "ssh", "fail2ban", "ufw", "lvm", "cybersécurité"],
|
"securite": ["born2beroot", "ssh", "fail2ban", "ufw", "lvm", "cybersécurité"],
|
||||||
"ia": ["llm", "ollama", "ia locale", "intelligence artificielle", "chatbot", "embedding"],
|
"ia": [
|
||||||
|
"llm", "ollama", "ia locale", "intelligence artificielle", "chatbot", "embedding",
|
||||||
|
# Spécialisation Data / IA (ft_linear_regression, piscine Python data, etc.)
|
||||||
|
"machine learning", "régression", "régression linéaire", "descente de gradient",
|
||||||
|
"numpy", "pandas", "scikit-learn", "data science", "dataframe",
|
||||||
|
],
|
||||||
"graphique": ["minilibx", "raycasting", "cub3d", "fract-ol", "wolfenstein"],
|
"graphique": ["minilibx", "raycasting", "cub3d", "fract-ol", "wolfenstein"],
|
||||||
"3d": ["impression 3d", "3d printing", "prusa", "slicer", "filament"],
|
"3d": ["impression 3d", "3d printing", "prusa", "slicer", "filament"],
|
||||||
"domotique": ["domotique", "home assistant", "zigbee", "iot"],
|
"domotique": ["domotique", "home assistant", "zigbee", "iot"],
|
||||||
@ -74,6 +79,11 @@ TAG_KEYWORDS: dict[str, list[str]] = {
|
|||||||
"concurrence": ["thread", "mutex", "philosopher"],
|
"concurrence": ["thread", "mutex", "philosopher"],
|
||||||
"docker": ["docker", "inception"],
|
"docker": ["docker", "inception"],
|
||||||
"makefile": ["makefile"],
|
"makefile": ["makefile"],
|
||||||
|
"data-ia": [
|
||||||
|
"ft_linear_regression", "ft-linear-regression",
|
||||||
|
"régression linéaire", "descente de gradient",
|
||||||
|
"piscine python", "numpy", "pandas", "scikit-learn",
|
||||||
|
],
|
||||||
"projet-perso": [], # drapeau manuel (futur)
|
"projet-perso": [], # drapeau manuel (futur)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
title: MOC — Algorithmique
|
title: MOC — Algorithmique
|
||||||
slug: MOC-Algorithmique
|
slug: MOC-Algorithmique
|
||||||
type: moc
|
type: moc
|
||||||
source: vault/generated
|
source: manual
|
||||||
domains: [algorithmique]
|
domains: [algorithmique]
|
||||||
tags: [moc]
|
tags: [moc]
|
||||||
aliases:
|
aliases:
|
||||||
@ -19,12 +19,12 @@ answers:
|
|||||||
- Que fait-il en algorithmique ?
|
- Que fait-il en algorithmique ?
|
||||||
priority: 7
|
priority: 7
|
||||||
linked:
|
linked:
|
||||||
updated: 2026-04-22
|
updated: 2026-04-23
|
||||||
visibility: public
|
visibility: public
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
||||||
Notes du domaine *algorithmique* (13 au total).
|
Notes du domaine *algorithmique* (15 au total).
|
||||||
|
|
||||||
## Notes liées
|
## Notes liées
|
||||||
|
|
||||||
@ -32,10 +32,12 @@ Notes du domaine *algorithmique* (13 au total).
|
|||||||
- [[cpp-partie1|cpp-partie1]] — _algorithmique, c, cpp, ecole-42, reseau_
|
- [[cpp-partie1|cpp-partie1]] — _algorithmique, c, cpp, ecole-42, reseau_
|
||||||
- [[cpp-partie2|cpp-partie2]] — _algorithmique, c, cpp, devops, domotique, ecole-42, reseau_
|
- [[cpp-partie2|cpp-partie2]] — _algorithmique, c, cpp, devops, domotique, ecole-42, reseau_
|
||||||
- [[fract-ol|fract-ol]] — _algorithmique, domotique, ecole-42, graphique, reseau, systeme_
|
- [[fract-ol|fract-ol]] — _algorithmique, domotique, ecole-42, graphique, reseau, systeme_
|
||||||
|
- [[ft-linear-regression|ft_linear_regression]] — _algorithmique, ecole-42, ia_
|
||||||
- [[ft-transcendence|ft_transcendence]] — _algorithmique, devops, ecole-42, reseau, web_
|
- [[ft-transcendence|ft_transcendence]] — _algorithmique, devops, ecole-42, reseau, web_
|
||||||
- [[inception|inception]] — _algorithmique, devops, ecole-42, reseau, systeme_
|
- [[inception|inception]] — _algorithmique, devops, ecole-42, reseau, systeme_
|
||||||
- [[libft|libft]] — _algorithmique, c, domotique, ecole-42, reseau_
|
- [[libft|libft]] — _algorithmique, c, domotique, ecole-42, reseau_
|
||||||
- [[minishell|minishell]] — _algorithmique, domotique, ecole-42, reseau, systeme_
|
- [[minishell|minishell]] — _algorithmique, domotique, ecole-42, reseau, systeme_
|
||||||
|
- [[piscine-python-data-science|Piscine Python — Data Science]] — _algorithmique, ecole-42, ia_
|
||||||
- [[ia|Mon Exploration et Maîtrise de l’Intelligence Artificielle]] — _algorithmique, ecole-42, ia_
|
- [[ia|Mon Exploration et Maîtrise de l’Intelligence Artificielle]] — _algorithmique, ecole-42, ia_
|
||||||
- [[competence|Mon expérience dans la domotique]] — _algorithmique, domotique, ia, reseau_
|
- [[competence|Mon expérience dans la domotique]] — _algorithmique, domotique, ia, reseau_
|
||||||
- [[impression-3d|Mon parcours dans l’impression 3D]] — _3d, algorithmique, reseau_
|
- [[impression-3d|Mon parcours dans l’impression 3D]] — _3d, algorithmique, reseau_
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
title: MOC — Devops
|
title: MOC — Devops
|
||||||
slug: MOC-Devops
|
slug: MOC-Devops
|
||||||
type: moc
|
type: moc
|
||||||
source: vault/generated
|
source: manual
|
||||||
domains: [devops]
|
domains: [devops]
|
||||||
tags: [moc]
|
tags: [moc]
|
||||||
aliases:
|
aliases:
|
||||||
@ -19,15 +19,19 @@ answers:
|
|||||||
- Que fait-il en devops ?
|
- Que fait-il en devops ?
|
||||||
priority: 7
|
priority: 7
|
||||||
linked:
|
linked:
|
||||||
updated: 2026-04-22
|
updated: 2026-04-23
|
||||||
visibility: public
|
visibility: public
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
||||||
Notes du domaine *devops* (3 au total).
|
Notes du domaine *devops* (7 au total).
|
||||||
|
|
||||||
## Notes liées
|
## Notes liées
|
||||||
|
|
||||||
- [[cpp-partie2|cpp-partie2]] — _algorithmique, c, cpp, devops, domotique, ecole-42, reseau_
|
- [[cpp-partie2|cpp-partie2]] — _algorithmique, c, cpp, devops, domotique, ecole-42, reseau_
|
||||||
|
- [[fernandgrascalvet-com|fernandgrascalvet.com (portfolio)]] — _devops, ia, web_
|
||||||
- [[ft-transcendence|ft_transcendence]] — _algorithmique, devops, ecole-42, reseau, web_
|
- [[ft-transcendence|ft_transcendence]] — _algorithmique, devops, ecole-42, reseau, web_
|
||||||
|
- [[grasbot|GrasBot — chatbot IA du portfolio]] — _devops, ia, web_
|
||||||
- [[inception|inception]] — _algorithmique, devops, ecole-42, reseau, systeme_
|
- [[inception|inception]] — _algorithmique, devops, ecole-42, reseau, systeme_
|
||||||
|
- [[newsletter-ia|Newsletter IA — Ollama + Listmonk + Directus]] — _devops, ia, web_
|
||||||
|
- [[transcription-video|Transcription vidéo automatique]] — _devops, ia_
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
title: MOC — École 42
|
title: MOC — École 42
|
||||||
slug: MOC-Ecole-42
|
slug: MOC-Ecole-42
|
||||||
type: moc
|
type: moc
|
||||||
source: vault/generated
|
source: manual
|
||||||
domains: [ecole-42]
|
domains: [ecole-42]
|
||||||
tags: [moc]
|
tags: [moc]
|
||||||
aliases:
|
aliases:
|
||||||
@ -21,7 +21,7 @@ answers:
|
|||||||
- Que fait-il en ecole-42 ?
|
- Que fait-il en ecole-42 ?
|
||||||
priority: 7
|
priority: 7
|
||||||
linked:
|
linked:
|
||||||
updated: 2026-04-22
|
updated: 2026-04-23
|
||||||
visibility: public
|
visibility: public
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -36,6 +36,7 @@ Tout ce qui est rattaché à la formation 42 Perpignan.
|
|||||||
- [[cub3d|cub3d]] — _domotique, ecole-42, graphique, reseau_
|
- [[cub3d|cub3d]] — _domotique, ecole-42, graphique, reseau_
|
||||||
- [[fract-ol|fract-ol]] — _algorithmique, domotique, ecole-42, graphique, reseau, systeme_
|
- [[fract-ol|fract-ol]] — _algorithmique, domotique, ecole-42, graphique, reseau, systeme_
|
||||||
- [[ft-irc|ft-irc]] — _cpp, ecole-42, reseau, systeme_
|
- [[ft-irc|ft-irc]] — _cpp, ecole-42, reseau, systeme_
|
||||||
|
- [[ft-linear-regression|ft_linear_regression]] — _algorithmique, ecole-42, ia_
|
||||||
- [[ft-printf|Ft-printf]] — _c, ecole-42, reseau_
|
- [[ft-printf|Ft-printf]] — _c, ecole-42, reseau_
|
||||||
- [[ft-transcendence|ft_transcendence]] — _algorithmique, devops, ecole-42, reseau, web_
|
- [[ft-transcendence|ft_transcendence]] — _algorithmique, devops, ecole-42, reseau, web_
|
||||||
- [[get-next-line|Get_next_line]] — _c, ecole-42, reseau_
|
- [[get-next-line|Get_next_line]] — _c, ecole-42, reseau_
|
||||||
@ -46,5 +47,6 @@ Tout ce qui est rattaché à la formation 42 Perpignan.
|
|||||||
- [[ia|Mon Exploration et Maîtrise de l’Intelligence Artificielle]] — _algorithmique, ecole-42, ia_
|
- [[ia|Mon Exploration et Maîtrise de l’Intelligence Artificielle]] — _algorithmique, ecole-42, ia_
|
||||||
- [[netpractice|netpractice]] — _algorithmique, ecole-42, reseau_
|
- [[netpractice|netpractice]] — _algorithmique, ecole-42, reseau_
|
||||||
- [[philosopher|philosopher]] — _c, ecole-42, reseau, systeme_
|
- [[philosopher|philosopher]] — _c, ecole-42, reseau, systeme_
|
||||||
|
- [[piscine-python-data-science|Piscine Python — Data Science]] — _algorithmique, ecole-42, ia_
|
||||||
- [[presentation-ecole-42|Présentation école 42]] — _ecole-42, reseau, systeme_
|
- [[presentation-ecole-42|Présentation école 42]] — _ecole-42, reseau, systeme_
|
||||||
- [[push-swap|push_swap]] — _algorithmique, ecole-42, reseau_
|
- [[push-swap|push_swap]] — _algorithmique, ecole-42, reseau_
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
title: MOC — Ia
|
title: MOC — Ia
|
||||||
slug: MOC-Ia
|
slug: MOC-Ia
|
||||||
type: moc
|
type: moc
|
||||||
source: vault/generated
|
source: manual
|
||||||
domains: [ia]
|
domains: [ia]
|
||||||
tags: [moc]
|
tags: [moc]
|
||||||
aliases:
|
aliases:
|
||||||
@ -23,14 +23,20 @@ answers:
|
|||||||
- Que fait-il en ia ?
|
- Que fait-il en ia ?
|
||||||
priority: 7
|
priority: 7
|
||||||
linked:
|
linked:
|
||||||
updated: 2026-04-22
|
updated: 2026-04-23
|
||||||
visibility: public
|
visibility: public
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
||||||
Notes du domaine *ia* (2 au total).
|
Notes du domaine *ia* (8 au total : 2 compétences + 6 projets liés).
|
||||||
|
|
||||||
## Notes liées
|
## Notes liées
|
||||||
|
|
||||||
|
- [[grasbot|GrasBot — chatbot IA du portfolio]] — _devops, ia, web_
|
||||||
|
- [[newsletter-ia|Newsletter IA — Ollama + Listmonk + Directus]] — _devops, ia, web_
|
||||||
|
- [[transcription-video|Transcription vidéo automatique]] — _devops, ia_
|
||||||
|
- [[ft-linear-regression|ft_linear_regression]] — _algorithmique, ecole-42, ia_
|
||||||
|
- [[piscine-python-data-science|Piscine Python — Data Science]] — _algorithmique, ecole-42, ia_
|
||||||
- [[ia|Mon Exploration et Maîtrise de l’Intelligence Artificielle]] — _algorithmique, ecole-42, ia_
|
- [[ia|Mon Exploration et Maîtrise de l’Intelligence Artificielle]] — _algorithmique, ecole-42, ia_
|
||||||
- [[competence|Mon expérience dans la domotique]] — _algorithmique, domotique, ia, reseau_
|
- [[competence|Mon expérience dans la domotique]] — _algorithmique, domotique, ia, reseau_
|
||||||
|
- [[fernandgrascalvet-com|fernandgrascalvet.com (portfolio)]] — _devops, ia, web_ (couverture transversale du site)
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
title: MOC — Projets
|
title: MOC — Projets
|
||||||
slug: MOC-Projets
|
slug: MOC-Projets
|
||||||
type: moc
|
type: moc
|
||||||
source: vault/generated
|
source: manual
|
||||||
domains: []
|
domains: []
|
||||||
tags: [moc]
|
tags: [moc]
|
||||||
aliases:
|
aliases:
|
||||||
@ -15,7 +15,7 @@ answers:
|
|||||||
- Que fait-il en Projets ?
|
- Que fait-il en Projets ?
|
||||||
priority: 7
|
priority: 7
|
||||||
linked:
|
linked:
|
||||||
updated: 2026-04-22
|
updated: 2026-04-23
|
||||||
visibility: public
|
visibility: public
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -28,16 +28,22 @@ Hub des projets de Fernand Gras-Calvet, triés par titre.
|
|||||||
- [[cpp-partie1|cpp-partie1]] — _algorithmique, c, cpp, ecole-42, reseau_
|
- [[cpp-partie1|cpp-partie1]] — _algorithmique, c, cpp, ecole-42, reseau_
|
||||||
- [[cpp-partie2|cpp-partie2]] — _algorithmique, c, cpp, devops, domotique, ecole-42, reseau_
|
- [[cpp-partie2|cpp-partie2]] — _algorithmique, c, cpp, devops, domotique, ecole-42, reseau_
|
||||||
- [[cub3d|cub3d]] — _domotique, ecole-42, graphique, reseau_
|
- [[cub3d|cub3d]] — _domotique, ecole-42, graphique, reseau_
|
||||||
|
- [[fernandgrascalvet-com|fernandgrascalvet.com (portfolio)]] — _devops, ia, web_
|
||||||
- [[fract-ol|fract-ol]] — _algorithmique, domotique, ecole-42, graphique, reseau, systeme_
|
- [[fract-ol|fract-ol]] — _algorithmique, domotique, ecole-42, graphique, reseau, systeme_
|
||||||
- [[ft-irc|ft-irc]] — _cpp, ecole-42, reseau, systeme_
|
- [[ft-irc|ft-irc]] — _cpp, ecole-42, reseau, systeme_
|
||||||
|
- [[ft-linear-regression|ft_linear_regression]] — _algorithmique, ecole-42, ia_
|
||||||
- [[ft-printf|Ft-printf]] — _c, ecole-42, reseau_
|
- [[ft-printf|Ft-printf]] — _c, ecole-42, reseau_
|
||||||
- [[ft-transcendence|ft_transcendence]] — _algorithmique, devops, ecole-42, reseau, web_
|
- [[ft-transcendence|ft_transcendence]] — _algorithmique, devops, ecole-42, reseau, web_
|
||||||
- [[get-next-line|Get_next_line]] — _c, ecole-42, reseau_
|
- [[get-next-line|Get_next_line]] — _c, ecole-42, reseau_
|
||||||
|
- [[grasbot|GrasBot — chatbot IA du portfolio]] — _devops, ia, web_
|
||||||
- [[inception|inception]] — _algorithmique, devops, ecole-42, reseau, systeme_
|
- [[inception|inception]] — _algorithmique, devops, ecole-42, reseau, systeme_
|
||||||
- [[libft|libft]] — _algorithmique, c, domotique, ecole-42, reseau_
|
- [[libft|libft]] — _algorithmique, c, domotique, ecole-42, reseau_
|
||||||
- [[minishell|minishell]] — _algorithmique, domotique, ecole-42, reseau, systeme_
|
- [[minishell|minishell]] — _algorithmique, domotique, ecole-42, reseau, systeme_
|
||||||
- [[minitalk|minitalk]] — _c, ecole-42, reseau, systeme_
|
- [[minitalk|minitalk]] — _c, ecole-42, reseau, systeme_
|
||||||
- [[netpractice|netpractice]] — _algorithmique, ecole-42, reseau_
|
- [[netpractice|netpractice]] — _algorithmique, ecole-42, reseau_
|
||||||
|
- [[newsletter-ia|Newsletter IA — Ollama + Listmonk + Directus]] — _devops, ia, web_
|
||||||
- [[philosopher|philosopher]] — _c, ecole-42, reseau, systeme_
|
- [[philosopher|philosopher]] — _c, ecole-42, reseau, systeme_
|
||||||
|
- [[piscine-python-data-science|Piscine Python — Data Science]] — _algorithmique, ecole-42, ia_
|
||||||
- [[presentation-ecole-42|Présentation école 42]] — _ecole-42, reseau, systeme_
|
- [[presentation-ecole-42|Présentation école 42]] — _ecole-42, reseau, systeme_
|
||||||
- [[push-swap|push_swap]] — _algorithmique, ecole-42, reseau_
|
- [[push-swap|push_swap]] — _algorithmique, ecole-42, reseau_
|
||||||
|
- [[transcription-video|Transcription vidéo automatique]] — _devops, ia_
|
||||||
|
|||||||
@ -18,9 +18,11 @@ answers:
|
|||||||
priority: 7
|
priority: 7
|
||||||
linked:
|
linked:
|
||||||
- "[[architecture-site]]"
|
- "[[architecture-site]]"
|
||||||
|
- "[[grasbot]]"
|
||||||
|
- "[[fernandgrascalvet-com]]"
|
||||||
- "[[grasbot-retrieval]]"
|
- "[[grasbot-retrieval]]"
|
||||||
- "[[vault-structure]]"
|
- "[[vault-structure]]"
|
||||||
updated: 2026-04-22
|
updated: 2026-04-23
|
||||||
visibility: public
|
visibility: public
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -34,6 +36,8 @@ utilises-tu ? »*).
|
|||||||
## Notes liées
|
## Notes liées
|
||||||
|
|
||||||
- [[architecture-site]] — vue d'ensemble Next.js + Strapi + FastAPI/Ollama.
|
- [[architecture-site]] — vue d'ensemble Next.js + Strapi + FastAPI/Ollama.
|
||||||
|
- [[grasbot]] — fiche produit de l'assistant (chaîne d'appel, stack).
|
||||||
|
- [[fernandgrascalvet-com]] — périmètre site public (refonte, `realisation-ia`, Brevo).
|
||||||
- [[grasbot-retrieval]] — pipeline de recherche (graph + BM25, sans
|
- [[grasbot-retrieval]] — pipeline de recherche (graph + BM25, sans
|
||||||
embeddings).
|
embeddings).
|
||||||
- [[vault-structure]] — organisation du vault Obsidian, frontmatter,
|
- [[vault-structure]] — organisation du vault Obsidian, frontmatter,
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
title: MOC — Web
|
title: MOC — Web
|
||||||
slug: MOC-Web
|
slug: MOC-Web
|
||||||
type: moc
|
type: moc
|
||||||
source: vault/generated
|
source: manual
|
||||||
domains: [web]
|
domains: [web]
|
||||||
tags: [moc]
|
tags: [moc]
|
||||||
aliases:
|
aliases:
|
||||||
@ -20,14 +20,17 @@ answers:
|
|||||||
- Que fait-il en web ?
|
- Que fait-il en web ?
|
||||||
priority: 7
|
priority: 7
|
||||||
linked:
|
linked:
|
||||||
updated: 2026-04-22
|
updated: 2026-04-23
|
||||||
visibility: public
|
visibility: public
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
||||||
Notes du domaine *web* (2 au total).
|
Notes du domaine *web* (5 au total).
|
||||||
|
|
||||||
## Notes liées
|
## Notes liées
|
||||||
|
|
||||||
- [[developpement-web-and-hebergement-sur-serveur-windows|Développement Web & Hébergement sur serveur Windows]] — _reseau, securite, web_
|
- [[developpement-web-and-hebergement-sur-serveur-windows|Développement Web & Hébergement sur serveur Windows]] — _reseau, securite, web_
|
||||||
|
- [[fernandgrascalvet-com|fernandgrascalvet.com (portfolio)]] — _devops, ia, web_
|
||||||
|
- [[grasbot|GrasBot — chatbot IA du portfolio]] — _devops, ia, web_
|
||||||
|
- [[newsletter-ia|Newsletter IA — Ollama + Listmonk + Directus]] — _devops, ia, web_
|
||||||
- [[ft-transcendence|ft_transcendence]] — _algorithmique, devops, ecole-42, reseau, web_
|
- [[ft-transcendence|ft_transcendence]] — _algorithmique, devops, ecole-42, reseau, web_
|
||||||
|
|||||||
71
vault-grasbot/10-Projets/fernandgrascalvet-com.md
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
---
|
||||||
|
title: "fernandgrascalvet.com — site portfolio"
|
||||||
|
slug: fernandgrascalvet-com
|
||||||
|
type: projet
|
||||||
|
source: manual
|
||||||
|
domains: [web, devops, ia]
|
||||||
|
tags: [nextjs, strapi, portfolio]
|
||||||
|
aliases:
|
||||||
|
- fernandgrascalvet.com
|
||||||
|
- site portfolio
|
||||||
|
- portfolio en ligne
|
||||||
|
- son site
|
||||||
|
- ton site
|
||||||
|
- site perso
|
||||||
|
- site web de fernand
|
||||||
|
answers:
|
||||||
|
- "Quel est le site de Fernand ?"
|
||||||
|
- "Parle-moi de son site portfolio."
|
||||||
|
- "Où peut-on voir ses projets en ligne ?"
|
||||||
|
- "Sur quel site est hébergé GrasBot ?"
|
||||||
|
priority: 6
|
||||||
|
linked:
|
||||||
|
- "[[MOC-Projets]]"
|
||||||
|
- "[[MOC-Web]]"
|
||||||
|
related:
|
||||||
|
- "[[developpement-web-and-hebergement-sur-serveur-windows]]"
|
||||||
|
- "[[grasbot]]"
|
||||||
|
- "[[architecture-site]]"
|
||||||
|
link: "https://fernandgrascalvet.com"
|
||||||
|
updated: 2026-04-23
|
||||||
|
visibility: public
|
||||||
|
---
|
||||||
|
|
||||||
|
# fernandgrascalvet.com — site portfolio
|
||||||
|
|
||||||
|
> [!info] Rôle de cette note
|
||||||
|
> Fiche **projet** dédiée au site public lui-même. Pour le *comment* technique
|
||||||
|
> (stack détaillée, pipeline de build, ports) voir [[architecture-site]] ;
|
||||||
|
> pour la *posture métier* (hébergement IIS, sécurité, SSL) voir la fiche
|
||||||
|
> compétence [[developpement-web-and-hebergement-sur-serveur-windows]].
|
||||||
|
|
||||||
|
## En une phrase
|
||||||
|
|
||||||
|
Portfolio public de Fernand Gras-Calvet, **auto-hébergé** sur un serveur
|
||||||
|
personnel (Windows Server + IIS), construit en **Next.js 15** et alimenté
|
||||||
|
par **Strapi 5**, avec un assistant IA intégré ([[grasbot|GrasBot]]).
|
||||||
|
|
||||||
|
## Ce que le visiteur y trouve
|
||||||
|
|
||||||
|
- **Portfolio** : fiches projets (école 42 et perso) avec Markdown riche
|
||||||
|
et galeries.
|
||||||
|
- **Compétences** : fiches richtext ou **vignettes** de *réalisations IA*
|
||||||
|
quand le Strapi contient des entrées `realisation-ia` liées — route fille
|
||||||
|
`/competences/[slug]/[realisation]` pour le détail.
|
||||||
|
- **Contact** : e-mail transactionnel via **Brevo** (plus de stockage
|
||||||
|
Strapi des messages).
|
||||||
|
- **GrasBot** : bouton flottant disponible sur toutes les pages.
|
||||||
|
|
||||||
|
## Différentiation avec les notes voisines
|
||||||
|
|
||||||
|
| Note | Angle |
|
||||||
|
|------|-------|
|
||||||
|
| `fernandgrascalvet-com` (cette note) | **Projet public** (ce qu'on *voit*) |
|
||||||
|
| [[architecture-site]] | Schéma technique interne (stack, flux, ports) |
|
||||||
|
| [[developpement-web-and-hebergement-sur-serveur-windows]] | Compétence / savoir-faire web + hébergement |
|
||||||
|
| [[grasbot]] | Fonctionnalité IA du site |
|
||||||
|
|
||||||
|
## Liens
|
||||||
|
|
||||||
|
- [[MOC-Projets]] — hub projets
|
||||||
|
- [[MOC-Web]] — hub domaine *web*
|
||||||
131
vault-grasbot/10-Projets/ft-linear-regression.md
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
---
|
||||||
|
title: "ft_linear_regression"
|
||||||
|
slug: ft-linear-regression
|
||||||
|
type: projet
|
||||||
|
source: manual
|
||||||
|
domains: [ia, ecole-42, algorithmique]
|
||||||
|
tags: [data-ia]
|
||||||
|
aliases:
|
||||||
|
- ft linear regression
|
||||||
|
- ft_linear_regression
|
||||||
|
- ft-linear-regression
|
||||||
|
- régression linéaire
|
||||||
|
- linear regression
|
||||||
|
- descente de gradient
|
||||||
|
- régression 42
|
||||||
|
- premier projet ia 42
|
||||||
|
- régression linéaire from scratch
|
||||||
|
answers:
|
||||||
|
- "Qu'est-ce que ft_linear_regression ?"
|
||||||
|
- "Parle-moi de ft_linear_regression."
|
||||||
|
- "Comment a-t-il implémenté une régression linéaire ?"
|
||||||
|
- "Quel est le premier projet Data / IA à l'École 42 ?"
|
||||||
|
- "A-t-il codé une descente de gradient ?"
|
||||||
|
priority: 6
|
||||||
|
linked:
|
||||||
|
- "[[MOC-Projets]]"
|
||||||
|
- "[[MOC-Ecole-42]]"
|
||||||
|
- "[[MOC-Ia]]"
|
||||||
|
related:
|
||||||
|
- "[[piscine-python-data-science]]"
|
||||||
|
- "[[ia]]"
|
||||||
|
- "[[grasbot]]"
|
||||||
|
updated: 2026-04-23
|
||||||
|
visibility: public
|
||||||
|
---
|
||||||
|
|
||||||
|
# ft_linear_regression
|
||||||
|
|
||||||
|
> [!info] Contexte
|
||||||
|
> **Premier projet de la branche Data Science / IA de l'École 42**, enchaîné
|
||||||
|
> après la [[piscine-python-data-science|Piscine Python — Data Science]].
|
||||||
|
> Socle mathématique pour les travaux IA côté [[ia]] et [[grasbot]].
|
||||||
|
|
||||||
|
## Résumé
|
||||||
|
|
||||||
|
Implémenter **from scratch** un algorithme de **régression linéaire simple**
|
||||||
|
entraînée par **descente de gradient**, sans utiliser de fonction toute
|
||||||
|
faite (`numpy.polyfit`, `sklearn.LinearRegression`, etc.). L'exercice
|
||||||
|
force à comprendre les **fondations mathématiques du machine learning**
|
||||||
|
avant de manipuler les bibliothèques de haut niveau.
|
||||||
|
|
||||||
|
## Objectifs pédagogiques
|
||||||
|
|
||||||
|
- Découvrir les concepts fondamentaux du ML : **hypothèse**, **fonction de
|
||||||
|
coût**, **paramètres**, **convergence**.
|
||||||
|
- Implémenter la **descente de gradient** à la main, avec mise à jour
|
||||||
|
simultanée de θ₀ (biais) et θ₁ (pente) via des variables temporaires
|
||||||
|
pour éviter un couplage entre les deux mises à jour.
|
||||||
|
- Comprendre le rôle du **learning rate** : un pas trop grand fait osciller
|
||||||
|
l'algorithme, un pas trop petit le ralentit artificiellement.
|
||||||
|
- Travailler la **préparation des données** : détection des valeurs
|
||||||
|
aberrantes via l'**IQR** (*Interquartile Range*), gestion des valeurs
|
||||||
|
manquantes, **normalisation min-max** pour que la descente converge
|
||||||
|
malgré des échelles différentes (kilométrages en milliers, prix en
|
||||||
|
dizaines de milliers).
|
||||||
|
|
||||||
|
## Stack technique
|
||||||
|
|
||||||
|
| Outil | Rôle |
|
||||||
|
|-------|------|
|
||||||
|
| **Python 3** | Langage principal |
|
||||||
|
| **pandas** | Chargement et exploration du CSV (`describe()`, quartiles, détection d'aberrants) |
|
||||||
|
| **NumPy** | Calculs vectoriels pour la descente de gradient |
|
||||||
|
| **Matplotlib + Seaborn** | Visualisation du nuage de points et de la droite de régression |
|
||||||
|
| **scikit-learn** | **Uniquement** pour les métriques d'évaluation (`mean_squared_error`, `r2_score`), pas pour l'entraînement |
|
||||||
|
| **Tkinter** | Interface graphique native : saisie d'un kilométrage, affichage du prix estimé en temps réel |
|
||||||
|
|
||||||
|
## Architecture du code
|
||||||
|
|
||||||
|
Modules thématiques dans `src/` :
|
||||||
|
|
||||||
|
- `data_process.py` — chargement, nettoyage, normalisation du dataset.
|
||||||
|
- `cost_function.py` — fonction de coût *Mean Squared Error* :
|
||||||
|
`J(θ₀, θ₁) = (1 / 2m) · Σ(hθ(x) − y)²`.
|
||||||
|
- `gradient_descent.py` — boucle d'entraînement (mise à jour simultanée
|
||||||
|
des paramètres).
|
||||||
|
- `training_model.py` — orchestration de l'entraînement + sauvegarde des
|
||||||
|
paramètres optimaux.
|
||||||
|
- `prediction.py` — rechargement des paramètres pour prédire sur une
|
||||||
|
nouvelle entrée.
|
||||||
|
- `evaluate_model.py` — calcul MSE + R² pour mesurer la qualité du modèle.
|
||||||
|
- `visualization.py` — tracé du nuage de points + droite de régression.
|
||||||
|
- `gui.py` — interface Tkinter (bonus).
|
||||||
|
|
||||||
|
## Parties bonus réalisées
|
||||||
|
|
||||||
|
- **Graphe** du nuage de points avec droite de régression superposée.
|
||||||
|
- **Mesure de précision** via MSE et **coefficient de détermination R²**
|
||||||
|
(proche de 1 → modèle pertinent, proche de 0 → modèle inutile, négatif
|
||||||
|
→ pire qu'une moyenne).
|
||||||
|
- **Interface graphique utilisateur** (Tkinter).
|
||||||
|
|
||||||
|
## Compétences mobilisées
|
||||||
|
|
||||||
|
- **Mathématiques appliquées** : dérivées partielles, minimisation d'une
|
||||||
|
fonction de coût, interprétation géométrique d'une régression.
|
||||||
|
- **Programmation scientifique Python** : vectorisation NumPy, DataFrames
|
||||||
|
pandas, patterns de préparation de données.
|
||||||
|
- **Visualisation de données** : IQR, distribution, scatter plot —
|
||||||
|
comprendre ses données avant de modéliser.
|
||||||
|
- **Évaluation de modèle** : différence entre *loss* d'entraînement et
|
||||||
|
métriques de qualité, interprétation du R².
|
||||||
|
|
||||||
|
## Valeur pour la suite
|
||||||
|
|
||||||
|
Ce projet constitue la **base intellectuelle** de tout ce qui suit en IA.
|
||||||
|
Comprendre ce qui se passe sous `model.fit(X, y)` change radicalement la
|
||||||
|
manière d'aborder les bibliothèques de haut niveau (scikit-learn,
|
||||||
|
PyTorch, TensorFlow) et d'interpréter les comportements inattendus d'un
|
||||||
|
modèle (non-convergence, explosion du learning rate, overfitting).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Liens
|
||||||
|
|
||||||
|
- [[MOC-Projets]] — hub projets
|
||||||
|
- [[MOC-Ecole-42]] — contexte pédagogique
|
||||||
|
- [[MOC-Ia]] — domaine *ia*
|
||||||
|
- [[piscine-python-data-science]] — pré-requis pratique
|
||||||
|
- [[ia]] — compétence IA (fiche)
|
||||||
|
- [[grasbot]] — projet IA appliqué au portfolio
|
||||||
144
vault-grasbot/10-Projets/grasbot.md
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
---
|
||||||
|
title: "GrasBot — chatbot IA du portfolio"
|
||||||
|
slug: grasbot
|
||||||
|
type: projet
|
||||||
|
source: manual
|
||||||
|
domains: [ia, web, devops]
|
||||||
|
tags: [ollama, fastapi, langfuse, chatbot, qwen3]
|
||||||
|
aliases:
|
||||||
|
- grasbot
|
||||||
|
- assistant grasbot
|
||||||
|
- chatbot du site
|
||||||
|
- chatbot fernandgrascalvet
|
||||||
|
- chatbot ia du portfolio
|
||||||
|
- assistant du site
|
||||||
|
- assistant ia locale
|
||||||
|
- ia locale test
|
||||||
|
- bot du site
|
||||||
|
- bouton flottant
|
||||||
|
answers:
|
||||||
|
- "Qu'est-ce que GrasBot ?"
|
||||||
|
- "Parle-moi de GrasBot."
|
||||||
|
- "Quel est l'assistant IA sur le site ?"
|
||||||
|
- "Comment fonctionne le chatbot du site ?"
|
||||||
|
- "Sur quelles sources GrasBot s'appuie-t-il ?"
|
||||||
|
- "GrasBot hallucine-t-il ?"
|
||||||
|
priority: 7
|
||||||
|
linked:
|
||||||
|
- "[[MOC-Projets]]"
|
||||||
|
- "[[MOC-Ia]]"
|
||||||
|
related:
|
||||||
|
- "[[ia]]"
|
||||||
|
- "[[grasbot-retrieval]]"
|
||||||
|
- "[[architecture-site]]"
|
||||||
|
- "[[fernandgrascalvet-com]]"
|
||||||
|
- "[[newsletter-ia]]"
|
||||||
|
- "[[transcription-video]]"
|
||||||
|
link: "https://fernandgrascalvet.com"
|
||||||
|
updated: 2026-04-23
|
||||||
|
visibility: public
|
||||||
|
---
|
||||||
|
|
||||||
|
# GrasBot — chatbot IA du portfolio
|
||||||
|
|
||||||
|
> [!info] Rôle de cette note
|
||||||
|
> Fiche **projet** (réalisation IA exposée sur `/competences/ia`). Pour
|
||||||
|
> l'algorithme de retrieval détaillé voir [[grasbot-retrieval]] ; pour
|
||||||
|
> l'architecture globale du site voir [[architecture-site]].
|
||||||
|
|
||||||
|
## Le défi
|
||||||
|
|
||||||
|
Permettre à un visiteur de **poser des questions libres** sur le parcours,
|
||||||
|
les projets et les compétences de Fernand, **sans qu'il doive naviguer**
|
||||||
|
dans dix pages, et **sans que le chatbot invente** des informations qui
|
||||||
|
ne sont pas dans les notes.
|
||||||
|
|
||||||
|
## Pipeline technique
|
||||||
|
|
||||||
|
GrasBot n'est pas un simple wrapper ChatGPT : c'est un vrai pipeline **RAG
|
||||||
|
(Retrieval-Augmented Generation)** construit de bout en bout.
|
||||||
|
|
||||||
|
### 1. Vault Obsidian comme source de vérité
|
||||||
|
|
||||||
|
Un coffre Obsidian structuré (`vault-grasbot/`) contient les notes de
|
||||||
|
parcours, projets École 42, compétences et glossaires. Chaque note porte
|
||||||
|
un **frontmatter YAML** avec `aliases`, `answers`, `priority`, `domains`
|
||||||
|
— autant de **signaux** pour le scoring.
|
||||||
|
|
||||||
|
### 2. Retrieval déterministe par graphe + BM25
|
||||||
|
|
||||||
|
Contrairement à un RAG classique à **embeddings vectoriels**, GrasBot
|
||||||
|
exploite directement la **structure du vault** : score multi-signaux
|
||||||
|
(aliases / titre / answers / domaines / BM25 sur le body), puis
|
||||||
|
**expansion par graphe** via les wikilinks `[[...]]`. Résultat : retrieval
|
||||||
|
en **~50 ms**, **100 % traçable**, sans dépendance à ChromaDB ni à des
|
||||||
|
embeddings. Détail : [[grasbot-retrieval]].
|
||||||
|
|
||||||
|
### 3. Génération locale avec Qwen3 + Ollama
|
||||||
|
|
||||||
|
Un modèle **Qwen3 8B** (quantization Q4_K_M) tourne **en local** sur le
|
||||||
|
serveur, servi par **Ollama**. Paramètres tunés après audit des premières
|
||||||
|
traces :
|
||||||
|
|
||||||
|
- `num_ctx = 8192` pour éviter la **troncature silencieuse** du contexte
|
||||||
|
(défaut Ollama trop bas quand plusieurs notes entières sont injectées).
|
||||||
|
- `num_predict = 1024` pour les réponses longues complètes.
|
||||||
|
- `think = false` pour **désactiver** le mode raisonnement interne qui
|
||||||
|
consommait du budget de sortie.
|
||||||
|
|
||||||
|
### 4. Observabilité Langfuse de bout en bout
|
||||||
|
|
||||||
|
Chaque conversation déclenche une trace Langfuse avec **3 spans** :
|
||||||
|
|
||||||
|
- `retrieval` (notes remontées, scores, *reasons*),
|
||||||
|
- `prompt_build` (system + user complets, sources tronquées, flag
|
||||||
|
*grounded*),
|
||||||
|
- `ollama-chat` (tokens, latence, paramètres).
|
||||||
|
|
||||||
|
Scores automatiques `grounded` et `retrieval_relevance` pour suivre la
|
||||||
|
qualité dans le temps.
|
||||||
|
|
||||||
|
### 5. Règles anti-hallucination
|
||||||
|
|
||||||
|
System prompt strict : **interdiction** d'inventer un fait absent des
|
||||||
|
notes, **priorité** aux sources `type=parcours` pour les questions
|
||||||
|
biographiques, mention explicite *« non précisé dans les notes »* quand
|
||||||
|
l'info manque. La source canonique [[bio-fernand]] (priority 10) couvre
|
||||||
|
les *« qui est Fernand ? »*.
|
||||||
|
|
||||||
|
## Stack complète
|
||||||
|
|
||||||
|
| Couche | Outils |
|
||||||
|
|--------|--------|
|
||||||
|
| **Front** | Next.js 15 + composant React `ChatBot.js` ; session / user IDs anonymes (UUID v4 en `sessionStorage` / `localStorage`). |
|
||||||
|
| **Proxy** | Route API Next qui valide une whitelist de paramètres (`q`, `session_id`, `user_id`) et forward vers l'API Python. |
|
||||||
|
| **Backend** | FastAPI + Ollama ; pipeline `search.py` (graph + BM25) + `observability.py` (Langfuse). |
|
||||||
|
| **Infra** | Windows Server 2025 + IIS reverse proxy + HTTPS Let's Encrypt + VPN pour l'admin. |
|
||||||
|
|
||||||
|
## Ce que ce projet démontre
|
||||||
|
|
||||||
|
- **Chaîne RAG complète** maîtrisée (ingestion, retrieval, génération,
|
||||||
|
observabilité).
|
||||||
|
- Choisir la **bonne brique** selon le contexte : un vault de quelques
|
||||||
|
dizaines de notes ne justifie pas des embeddings vectoriels — **BM25 +
|
||||||
|
graphe** suffit largement.
|
||||||
|
- **Tuning des modèles** plutôt que subir leurs défauts (audit via
|
||||||
|
Langfuse → correctifs ciblés).
|
||||||
|
- Déploiement en **production** sur une infra maîtrisée **bout en bout**.
|
||||||
|
|
||||||
|
## Différentiation avec les notes voisines
|
||||||
|
|
||||||
|
| Note | Angle | Quand elle remonte en premier |
|
||||||
|
|------|-------|-------------------------------|
|
||||||
|
| `grasbot` (cette note) | Fiche **produit / vignette réalisation** IA | *« Qu'est-ce que GrasBot ? »*, *« Pipeline technique ? »* |
|
||||||
|
| [[grasbot-retrieval]] | Technique interne (scoring, BM25, graphe) | *« Comment le chatbot trouve-t-il ses réponses ? »* |
|
||||||
|
| [[architecture-site]] | Stack globale Next + Strapi + FastAPI | *« Comment est fait le site ? »* |
|
||||||
|
| [[ia]] | Compétence (parcours IA de Fernand) | *« Quelles sont ses compétences en IA ? »* |
|
||||||
|
|
||||||
|
## Liens
|
||||||
|
|
||||||
|
- [[MOC-Projets]] — hub projets
|
||||||
|
- [[MOC-Ia]] — hub domaine *ia*
|
||||||
|
- [[ia]] — compétence IA (fiche) ; GrasBot y apparaît en réalisation
|
||||||
|
- [[fernandgrascalvet-com]] — le site qui héberge GrasBot
|
||||||
|
- [[newsletter-ia]] · [[transcription-video]] — autres réalisations IA
|
||||||
86
vault-grasbot/10-Projets/newsletter-ia.md
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
---
|
||||||
|
title: "Newsletter IA — Ollama + Listmonk + Directus"
|
||||||
|
slug: newsletter-ia
|
||||||
|
type: projet
|
||||||
|
source: manual
|
||||||
|
domains: [ia, web, devops]
|
||||||
|
tags: [ollama, langfuse, listmonk, directus, newsletter, docker, chatbot]
|
||||||
|
aliases:
|
||||||
|
- newsletter ia
|
||||||
|
- newsletter automatisée
|
||||||
|
- newsletter ollama listmonk
|
||||||
|
- listmonk directus ollama
|
||||||
|
- génération de newsletter
|
||||||
|
- newsletter homelab
|
||||||
|
- diffusion newsletter auto-hébergée
|
||||||
|
answers:
|
||||||
|
- "Qu'est-ce que la newsletter IA de Fernand ?"
|
||||||
|
- "Parle-moi du projet newsletter automatisée."
|
||||||
|
- "Comment génère-t-il une newsletter avec un LLM local ?"
|
||||||
|
- "Utilise-t-il Listmonk pour ses envois ?"
|
||||||
|
- "Sur quelles briques repose sa newsletter auto-hébergée ?"
|
||||||
|
priority: 6
|
||||||
|
linked:
|
||||||
|
- "[[MOC-Projets]]"
|
||||||
|
- "[[MOC-Ia]]"
|
||||||
|
related:
|
||||||
|
- "[[ia]]"
|
||||||
|
- "[[grasbot]]"
|
||||||
|
- "[[architecture-site]]"
|
||||||
|
updated: 2026-04-23
|
||||||
|
visibility: public
|
||||||
|
---
|
||||||
|
|
||||||
|
# Newsletter IA — Ollama + Listmonk + Directus
|
||||||
|
|
||||||
|
> [!info] Rôle de cette note
|
||||||
|
> Fiche **projet** *Newsletter IA*, exposée sur le site en `realisation-ia`
|
||||||
|
> rattachée à la compétence [[ia]]. Démontre la capacité à orchestrer une
|
||||||
|
> chaîne de publication **auto-hébergée** avec de la génération LLM locale.
|
||||||
|
|
||||||
|
## L'objectif
|
||||||
|
|
||||||
|
Construire un système de newsletter **complètement auto-hébergé**, capable
|
||||||
|
de **générer automatiquement** le contenu éditorial (intro, sélection des
|
||||||
|
articles) à partir d'un corpus d'information, puis de le **diffuser** à une
|
||||||
|
liste d'abonnés — sans dépendre d'un service SaaS propriétaire (Substack,
|
||||||
|
Mailchimp, Beehiiv…).
|
||||||
|
|
||||||
|
## Les briques
|
||||||
|
|
||||||
|
| Brique | Rôle |
|
||||||
|
|--------|------|
|
||||||
|
| **Listmonk** | Diffusion — open-source (Go), API REST complète, segmentation, stats, templates HTML. Dans un conteneur Docker derrière IIS reverse proxy pour HTTPS. |
|
||||||
|
| **Directus** | Édition — CMS headless : chaque édition est un document avec ses articles, sa thématique, sa date. Permet de **réviser** le contenu généré par l'IA avant envoi. |
|
||||||
|
| **Ollama** | Génération LLM locale (Qwen3, Mistral). Deux usages : **(1) sélection bornée** des 3-5 articles les plus pertinents par thématique, **(2) rédaction d'introduction** éditoriale à partir des résumés. |
|
||||||
|
| **Langfuse** | Observabilité — chaque génération est tracée (prompt, sortie, tokens, latence) pour suivre la qualité dans le temps et itérer sur les prompts **sans régression**. |
|
||||||
|
|
||||||
|
## Ce que ce projet démontre
|
||||||
|
|
||||||
|
- **Self-hosting complet** d'une chaîne de publication (serveur, DNS, HTTPS,
|
||||||
|
VM, Docker, reverse proxy, SMTP).
|
||||||
|
- **Orchestration** de plusieurs services (Listmonk + Directus + Ollama)
|
||||||
|
plutôt que coller des briques opaques.
|
||||||
|
- **Usage ciblé de l'IA** là où elle apporte de la valeur (sélection +
|
||||||
|
rédaction d'intro), **sans basculer** dans un 100 % automatique qui
|
||||||
|
perdrait en qualité.
|
||||||
|
- **Observabilité dès le départ** : Langfuse connecté **avant** le moindre
|
||||||
|
envoi en production.
|
||||||
|
|
||||||
|
## Roadmap
|
||||||
|
|
||||||
|
- Enrichissement LLM multi-passes (extraction de citations, tags
|
||||||
|
automatiques).
|
||||||
|
- Interface d'arbitrage humain dans Directus pour valider l'intro avant
|
||||||
|
envoi.
|
||||||
|
- Dashboard Langfuse dédié à la newsletter (taux d'ouverture ↔ ton de
|
||||||
|
l'intro).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Liens
|
||||||
|
|
||||||
|
- [[MOC-Projets]] — hub projets
|
||||||
|
- [[MOC-Ia]] — hub domaine *ia*
|
||||||
|
- [[ia]] — compétence IA (fiche)
|
||||||
|
- [[grasbot]] — autre réalisation IA, même esprit *self-hosted + observable*
|
||||||
118
vault-grasbot/10-Projets/piscine-python-data-science.md
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
---
|
||||||
|
title: "Piscine Python — Data Science"
|
||||||
|
slug: piscine-python-data-science
|
||||||
|
type: projet
|
||||||
|
source: manual
|
||||||
|
domains: [ia, ecole-42, algorithmique]
|
||||||
|
tags: [42-piscine, data-ia]
|
||||||
|
aliases:
|
||||||
|
- piscine python
|
||||||
|
- python piscine
|
||||||
|
- piscine data science
|
||||||
|
- piscine python 42
|
||||||
|
- piscine data
|
||||||
|
- python 42
|
||||||
|
- data science 42
|
||||||
|
- numpy pandas 42
|
||||||
|
answers:
|
||||||
|
- "Qu'est-ce que la Piscine Python de 42 ?"
|
||||||
|
- "Parle-moi de la Piscine Python Data Science."
|
||||||
|
- "Comment s'est-il formé à Python et à la data ?"
|
||||||
|
- "Qu'a-t-il appris en Python à l'École 42 ?"
|
||||||
|
- "A-t-il travaillé avec NumPy et pandas ?"
|
||||||
|
priority: 6
|
||||||
|
linked:
|
||||||
|
- "[[MOC-Projets]]"
|
||||||
|
- "[[MOC-Ecole-42]]"
|
||||||
|
- "[[MOC-Ia]]"
|
||||||
|
related:
|
||||||
|
- "[[ft-linear-regression]]"
|
||||||
|
- "[[ia]]"
|
||||||
|
updated: 2026-04-23
|
||||||
|
visibility: public
|
||||||
|
---
|
||||||
|
|
||||||
|
# Piscine Python — Data Science
|
||||||
|
|
||||||
|
> [!info] Contexte
|
||||||
|
> Sprint d'apprentissage intensif de la branche **Data Science / IA** à l'École 42,
|
||||||
|
> structuré en **5 modules (Python 0 à 4)**. Complément naturel du projet
|
||||||
|
> [[ft-linear-regression]] et socle pratique pour les travaux IA décrits dans
|
||||||
|
> [[ia]] et [[grasbot]].
|
||||||
|
|
||||||
|
## Objectif
|
||||||
|
|
||||||
|
Bâtir la **boîte à outils Python** d'un futur spécialiste Data / IA, en
|
||||||
|
privilégiant la compréhension des **paradigmes** plutôt que la simple
|
||||||
|
collection de syntaxes. Chaque module est une piscine courte : exercices
|
||||||
|
quotidiens, correction par les pairs, rythme soutenu.
|
||||||
|
|
||||||
|
## Les 5 modules
|
||||||
|
|
||||||
|
### Python 0 — Starting
|
||||||
|
|
||||||
|
Bases du langage : variables, structures de contrôle, fonctions, listes /
|
||||||
|
tuples / sets / dictionnaires, compréhensions, fichiers, exceptions,
|
||||||
|
`argparse`. Règles strictes de la Piscine : **Python 3.10 obligatoire**,
|
||||||
|
imports explicites, pas de variables globales, fonctions qui ne plantent
|
||||||
|
jamais, tests écrits par l'étudiant.
|
||||||
|
|
||||||
|
### Python 1 — Array
|
||||||
|
|
||||||
|
Programmation vectorisée avec **NumPy** : création et manipulation
|
||||||
|
d'arrays multidimensionnels, slicing avancé, **broadcasting**, fonctions
|
||||||
|
universelles (`ufunc`), opérations matricielles. Apprentissage du réflexe
|
||||||
|
*« remplacer une boucle Python par une opération vectorielle »* — fondamental
|
||||||
|
pour la performance en ML.
|
||||||
|
|
||||||
|
### Python 2 — DataTable
|
||||||
|
|
||||||
|
Manipulation de **DataFrames** avec **pandas** : chargement CSV / Excel,
|
||||||
|
sélection par index / label, `groupby`, `merge` / `join`, pivots,
|
||||||
|
nettoyage de données, détection de valeurs manquantes et aberrantes,
|
||||||
|
statistiques descriptives (`describe()`, quantiles). Le module charnière
|
||||||
|
pour les projets de régression et d'analyse.
|
||||||
|
|
||||||
|
### Python 3 — Object-Oriented Programming
|
||||||
|
|
||||||
|
Programmation orientée objet : classes, héritage, composition,
|
||||||
|
encapsulation, méthodes spéciales (`__init__`, `__repr__`, `__eq__`…),
|
||||||
|
polymorphisme. Introduction aux design patterns utiles en data science :
|
||||||
|
modélisation de datasets comme objets, classes de modèles avec méthodes
|
||||||
|
`fit` / `predict` (préparation à **scikit-learn**).
|
||||||
|
|
||||||
|
### Python 4 — Data Oriented Design
|
||||||
|
|
||||||
|
Changement de perspective : **DOD** (*Data Oriented Design*) — organiser
|
||||||
|
le code autour des **données** et de leur accès mémoire plutôt qu'autour
|
||||||
|
d'objets. *Cache-friendliness*, *structure of arrays* vs *array of
|
||||||
|
structures*, profilage basique. Un pont entre Python de haut niveau et les
|
||||||
|
contraintes de performance qu'on retrouvera en deep learning (PyTorch /
|
||||||
|
TensorFlow) ou en traitement de gros volumes.
|
||||||
|
|
||||||
|
## Compétences mobilisées
|
||||||
|
|
||||||
|
- **Python moderne** : idiomes 3.10+, typage, gestion d'erreurs robuste.
|
||||||
|
- **NumPy** : vectorisation, broadcasting, algèbre linéaire appliquée.
|
||||||
|
- **pandas** : nettoyage, agrégation, transformation de données tabulaires.
|
||||||
|
- **POO Python** : architecture de classes, héritage, méthodes spéciales.
|
||||||
|
- **Rigueur École 42** : tests écrits, code sans crash, peer-evaluation,
|
||||||
|
respect strict des contraintes de rendu.
|
||||||
|
|
||||||
|
## Valeur pour la suite
|
||||||
|
|
||||||
|
Cette Piscine est le **socle pratique** de tous les projets Data / IA qui
|
||||||
|
suivent. [[ft-linear-regression]] (régression linéaire par descente de
|
||||||
|
gradient), les expérimentations LLM / agents locaux et la manipulation des
|
||||||
|
jeux de données côté chatbot ([[grasbot]]) reposent **directement** sur
|
||||||
|
les acquis de ces 5 modules.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Liens
|
||||||
|
|
||||||
|
- [[MOC-Projets]] — hub projets
|
||||||
|
- [[MOC-Ecole-42]] — contexte pédagogique
|
||||||
|
- [[MOC-Ia]] — domaine *ia*
|
||||||
|
- [[ft-linear-regression]] — premier projet Data / IA 42 après cette piscine
|
||||||
|
- [[ia]] — compétence IA (fiche)
|
||||||
100
vault-grasbot/10-Projets/transcription-video.md
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
---
|
||||||
|
title: "Transcription vidéo automatique"
|
||||||
|
slug: transcription-video
|
||||||
|
type: projet
|
||||||
|
source: manual
|
||||||
|
domains: [ia, devops]
|
||||||
|
tags: [ollama, openwebui, ovh, transcription, multimedia]
|
||||||
|
aliases:
|
||||||
|
- transcription vidéo
|
||||||
|
- transcription automatique
|
||||||
|
- transcription auto
|
||||||
|
- speech to text
|
||||||
|
- sous-titres automatiques
|
||||||
|
- résumé de vidéo
|
||||||
|
- pipeline transcription
|
||||||
|
answers:
|
||||||
|
- "Comment transcrit-il des vidéos automatiquement ?"
|
||||||
|
- "Parle-moi du projet de transcription vidéo."
|
||||||
|
- "Quel pipeline utilise-t-il pour la transcription ?"
|
||||||
|
- "A-t-il testé OVHcloud AI Endpoints ?"
|
||||||
|
priority: 6
|
||||||
|
linked:
|
||||||
|
- "[[MOC-Projets]]"
|
||||||
|
- "[[MOC-Ia]]"
|
||||||
|
related:
|
||||||
|
- "[[ia]]"
|
||||||
|
- "[[newsletter-ia]]"
|
||||||
|
- "[[grasbot]]"
|
||||||
|
updated: 2026-04-23
|
||||||
|
visibility: public
|
||||||
|
---
|
||||||
|
|
||||||
|
# Transcription vidéo automatique
|
||||||
|
|
||||||
|
> [!info] Rôle de cette note
|
||||||
|
> Fiche **projet** *Transcription vidéo*, exposée sur le site en
|
||||||
|
> `realisation-ia` rattachée à la compétence [[ia]]. Montre la maîtrise
|
||||||
|
> d'un pipeline **multimédia** (décodage → segmentation → transcription →
|
||||||
|
> post-processing) et la capacité à **comparer** des fournisseurs de
|
||||||
|
> modèles (OVHcloud AI Endpoints vs modèles locaux Ollama).
|
||||||
|
|
||||||
|
## Contexte
|
||||||
|
|
||||||
|
Pouvoir **transcrire automatiquement** une vidéo longue (conférence,
|
||||||
|
tutoriel, cours), **segmenter les moments clés**, et **post-traiter** le
|
||||||
|
résultat pour obtenir un document éditable directement utilisable.
|
||||||
|
|
||||||
|
Exploration en **sept parties**, de l'intégration d'OVHcloud dans
|
||||||
|
Open WebUI jusqu'au calcul fin des **intervalles d'images** selon le mode
|
||||||
|
retenu.
|
||||||
|
|
||||||
|
## Pipeline
|
||||||
|
|
||||||
|
### 1. Préparation
|
||||||
|
|
||||||
|
- Intégration d'**OVHcloud AI Endpoints** dans **Open WebUI** comme
|
||||||
|
fournisseur complémentaire aux modèles locaux Ollama.
|
||||||
|
- Mise en place d'une pipeline vidéo sur la **VM dédiée** (calculs,
|
||||||
|
décodage, extraction audio).
|
||||||
|
|
||||||
|
### 2. Traitement vidéo
|
||||||
|
|
||||||
|
- Extraction de l'**audio** de la vidéo source.
|
||||||
|
- **Segmentation temporelle** avec calcul d'intervalles d'images adaptés
|
||||||
|
au mode choisi (transcription pure, analyse d'images-clés, ou les deux).
|
||||||
|
- **Transcription** par modèle *speech-to-text*.
|
||||||
|
|
||||||
|
### 3. Post-processing
|
||||||
|
|
||||||
|
- Nettoyage des hésitations, **reformulation légère** par LLM local.
|
||||||
|
- **Structuration en chapitres** avec timestamps.
|
||||||
|
- Export au format exploitable.
|
||||||
|
|
||||||
|
### 4. Analyse et corrections
|
||||||
|
|
||||||
|
Phase d'analyse des résultats sur des vidéos tests, qui a permis
|
||||||
|
d'identifier une liste de correctifs (**segmentation**, **gestion des
|
||||||
|
silences**, **précision des timestamps**) **avant** d'industrialiser le
|
||||||
|
workflow — démarche itérative et mesurée.
|
||||||
|
|
||||||
|
## Ce que ce projet démontre
|
||||||
|
|
||||||
|
- Maîtrise d'un **pipeline multimédia complet** (décodage, segmentation,
|
||||||
|
transcription, post-processing).
|
||||||
|
- Capacité à **comparer** des fournisseurs de modèles (**OVHcloud** vs
|
||||||
|
**Ollama local**) et à choisir selon le contexte.
|
||||||
|
- Intégration de plusieurs briques (**Open WebUI**, scripts Python,
|
||||||
|
modèles distants + locaux) dans un workflow cohérent.
|
||||||
|
- **Démarche itérative** : identification des régressions **avant** la mise
|
||||||
|
en production.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Liens
|
||||||
|
|
||||||
|
- [[MOC-Projets]] — hub projets
|
||||||
|
- [[MOC-Ia]] — hub domaine *ia*
|
||||||
|
- [[ia]] — compétence IA (fiche)
|
||||||
|
- [[newsletter-ia]] — autre réalisation IA orchestrée (Listmonk + Directus + Ollama)
|
||||||
|
- [[grasbot]] — réalisation IA : assistant du portfolio
|
||||||
@ -2,7 +2,7 @@
|
|||||||
title: "Développement Web & Hébergement sur serveur Windows"
|
title: "Développement Web & Hébergement sur serveur Windows"
|
||||||
slug: developpement-web-and-hebergement-sur-serveur-windows
|
slug: developpement-web-and-hebergement-sur-serveur-windows
|
||||||
type: competence
|
type: competence
|
||||||
source: strapi/competences
|
source: manual
|
||||||
domains: [reseau, securite, web]
|
domains: [reseau, securite, web]
|
||||||
tags: []
|
tags: []
|
||||||
aliases:
|
aliases:
|
||||||
@ -26,10 +26,12 @@ priority: 7
|
|||||||
linked:
|
linked:
|
||||||
- "[[MOC-Competences]]"
|
- "[[MOC-Competences]]"
|
||||||
related:
|
related:
|
||||||
|
- "[[fernandgrascalvet-com]]"
|
||||||
|
- "[[grasbot]]"
|
||||||
- "[[born2beroot]]"
|
- "[[born2beroot]]"
|
||||||
- "[[ft-transcendence]]"
|
- "[[ft-transcendence]]"
|
||||||
- "[[cpp-partie1]]"
|
- "[[cpp-partie1]]"
|
||||||
updated: 2026-04-22
|
updated: 2026-04-23
|
||||||
visibility: public
|
visibility: public
|
||||||
---
|
---
|
||||||
**Slug :** `developpement-web-and-hebergement-sur-serveur-windows`
|
**Slug :** `developpement-web-and-hebergement-sur-serveur-windows`
|
||||||
@ -39,6 +41,12 @@ visibility: public
|
|||||||
|
|
||||||
J'ai réalisé ce projet afin d'étendre mes compétences en développement Web.
|
J'ai réalisé ce projet afin d'étendre mes compétences en développement Web.
|
||||||
|
|
||||||
|
### Le site en production
|
||||||
|
|
||||||
|
Le site **fernandgrascalvet.com** est la mise en pratique courante de cette compétence. La fiche projet [[fernandgrascalvet-com]] décrit ce que le visiteur voit (portfolio, compétences avec `realisation-ia`, contact via Brevo, assistant [[grasbot|GrasBot]]).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
Ce projet est un site web basé sur Next.js pour le frontend et Strapi pour le backend, hébergé sur un serveur Windows Server 2025 avec IIS comme serveur web. Il repose sur une architecture Headless CMS, où le contenu est géré via une API REST et affiché dynamiquement sur le frontend.
|
Ce projet est un site web basé sur Next.js pour le frontend et Strapi pour le backend, hébergé sur un serveur Windows Server 2025 avec IIS comme serveur web. Il repose sur une architecture Headless CMS, où le contenu est géré via une API REST et affiché dynamiquement sur le frontend.
|
||||||
|
|
||||||
🔹 Technologies utilisées
|
🔹 Technologies utilisées
|
||||||
|
|||||||
@ -2,13 +2,16 @@
|
|||||||
title: Mon Exploration et Maîtrise de l’Intelligence Artificielle
|
title: Mon Exploration et Maîtrise de l’Intelligence Artificielle
|
||||||
slug: ia
|
slug: ia
|
||||||
type: competence
|
type: competence
|
||||||
source: strapi/competences
|
source: manual
|
||||||
domains: [algorithmique, ecole-42, ia]
|
domains: [ia, ecole-42, algorithmique]
|
||||||
tags: [tri]
|
tags: [chatbot, ollama, langfuse, data-ia]
|
||||||
aliases:
|
aliases:
|
||||||
- mon exploration et maîtrise de l’intelligence artificielle
|
- mon exploration et maîtrise de l’intelligence artificielle
|
||||||
- ia
|
- ia
|
||||||
- intelligence artificielle
|
- intelligence artificielle
|
||||||
|
- ia locale
|
||||||
|
- ia self-hosted
|
||||||
|
- ia auto-hébergée
|
||||||
- llm
|
- llm
|
||||||
- llms
|
- llms
|
||||||
- modèles de langage
|
- modèles de langage
|
||||||
@ -18,18 +21,25 @@ aliases:
|
|||||||
- deep learning
|
- deep learning
|
||||||
- data science
|
- data science
|
||||||
- ollama
|
- ollama
|
||||||
|
- langfuse
|
||||||
answers:
|
answers:
|
||||||
- Quelles sont ses compétences en IA ?
|
- "Quelles sont ses compétences en IA ?"
|
||||||
- "A-t-il de l'expérience en IA ?"
|
- "A-t-il de l'expérience en IA ?"
|
||||||
- Parle-moi de son expérience en IA
|
- "Parle-moi de son expérience en IA."
|
||||||
|
- "Utilise-t-il des LLMs locaux ?"
|
||||||
|
- "Qu'est-ce qui l'intéresse dans l'IA ?"
|
||||||
priority: 7
|
priority: 7
|
||||||
linked:
|
linked:
|
||||||
- "[[MOC-Competences]]"
|
- "[[MOC-Competences]]"
|
||||||
|
- "[[MOC-Ia]]"
|
||||||
related:
|
related:
|
||||||
- "[[born2beroot]]"
|
- "[[grasbot]]"
|
||||||
- "[[cpp-partie1]]"
|
- "[[newsletter-ia]]"
|
||||||
- "[[cpp-partie2]]"
|
- "[[transcription-video]]"
|
||||||
updated: 2026-04-22
|
- "[[ft-linear-regression]]"
|
||||||
|
- "[[piscine-python-data-science]]"
|
||||||
|
- "[[fernandgrascalvet-com]]"
|
||||||
|
updated: 2026-04-23
|
||||||
visibility: public
|
visibility: public
|
||||||
---
|
---
|
||||||
**Slug :** `ia`
|
**Slug :** `ia`
|
||||||
@ -37,15 +47,42 @@ visibility: public
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
Comme beaucoup, j’ai découvert l’intelligence artificielle grand public avec l’arrivée de ChatGPT, qui a marqué un tournant décisif dans l’accessibilité et la démocratisation de cette technologie. En l’espace de quelques mois, le domaine a connu une expansion fulgurante, avec l’émergence d’une multitude de solutions exploitant l’IA sous diverses formes. Fasciné par ces avancées, j’ai rapidement développé un vif intérêt pour plusieurs applications, notamment la génération d’images, les chatbots intelligents et plus largement les modèles de langage avancés (LLMs).
|
## Mon exploration des LLM et de l'IA : un intérêt devenu spécialisation
|
||||||
|
|
||||||
Dans cette quête d’exploration, j’ai expérimenté des solutions d’IA locale, notamment avec Ollama, LLM Studio, et d’autres outils permettant une plus grande maîtrise et personnalisation des modèles. Mon objectif a été de comprendre en profondeur les capacités d’intégration de ces intelligences artificielles locales, en explorant l’entraînement de modèles personnalisés, l’optimisation des performances et l’affinement des interactions par l’ingénierie des prompts (cliquez sur IA locale test mistral 7b sur mon serveur).
|
Comme beaucoup, j'ai découvert l'intelligence artificielle grand public avec l'arrivée de ChatGPT, qui a marqué un tournant décisif dans l'accessibilité et la démocratisation de cette technologie. Fasciné par la vitesse d'évolution du domaine, j'ai rapidement développé un intérêt pour plusieurs applications : **génération d'images**, **chatbots**, et plus largement les **modèles de langage (LLMs)**.
|
||||||
|
|
||||||
Actuellement, je suis en phase d’installation et de déploiement de solutions d’IA locale sur mon propre serveur, un projet en cours de développement qui me permet d’expérimenter les configurations avancées et d’adapter ces modèles à des cas d’usage spécifiques. Cette démarche s’inscrit dans une volonté de maîtriser l’IA en environnement auto-hébergé, offrant ainsi une meilleure compréhension de la gestion des ressources, du fine-tuning des modèles et des défis liés à l’infrastructure.
|
## De l'expérimentation à l'auto-hébergement
|
||||||
|
|
||||||
Parallèlement, j’ai entrepris une spécialisation en Data Science et Intelligence Artificielle au sein de l’École 42, afin d’approfondir mes connaissances théoriques et pratiques dans ce domaine en perpétuelle évolution. Cette formation me permet d’aller encore plus loin dans l’analyse des algorithmes de machine learning et deep learning, d’explorer des approches avancées en traitement des données massives, et de perfectionner mes compétences en développement et intégration de solutions IA appliquées.
|
Rapidement, j'ai voulu sortir de la dépendance aux API propriétaires et comprendre ce qui se passe sous le capot. J'ai expérimenté des solutions d'**IA locale** avec **Ollama**, **LM Studio**, **Open WebUI**, en explorant le fine-tuning léger, l'ingénierie de prompts, et l'intégration de modèles dans des workflows métier.
|
||||||
|
|
||||||
Animé par une passion pour l’intelligence artificielle et ses innombrables possibilités, je continue de m’informer, d’expérimenter et d’appliquer ces technologies à des projets concrets. Mon objectif est d’acquérir une expertise approfondie pour concevoir des systèmes intelligents performants, adaptables et innovants, tout en restant à la pointe des avancées technologiques.
|
Aujourd'hui, toute ma pile IA tourne sur **mon serveur personnel** :
|
||||||
|
|
||||||
|
- **LLM locaux** (Qwen3, Llama, Mistral, modèles d'embedding),
|
||||||
|
- **observabilité** via **Langfuse** self-hosted,
|
||||||
|
- **recherche web privée** via **SearxNG** et **Firecrawl** pour alimenter des agents.
|
||||||
|
|
||||||
|
Cette infrastructure me permet d'expérimenter **sans plafond de tokens**, **sans fuite de données**, et avec une compréhension fine du **coût** et de la **performance** de chaque modèle.
|
||||||
|
|
||||||
|
## Une spécialisation formalisée à l'École 42
|
||||||
|
|
||||||
|
Parallèlement, je me spécialise en **Data Science et Intelligence Artificielle à l'École 42**, ce qui me permet d'approfondir les **fondations mathématiques** (régression, descente de gradient, fonctions de coût, métriques d'évaluation) plutôt que de rester au niveau de l'utilisation des bibliothèques de haut niveau. Les projets [[ft-linear-regression]] (premier modèle **from scratch**) et la [[piscine-python-data-science|Piscine Python for Data Science]] (5 modules) posent les briques essentielles avant les modèles plus complexes.
|
||||||
|
|
||||||
|
## Mon angle
|
||||||
|
|
||||||
|
Je privilégie trois axes :
|
||||||
|
|
||||||
|
1. **Autonomie** — héberger moi-même pour comprendre réellement les contraintes, les coûts et les limites.
|
||||||
|
2. **Observabilité** — chaque appel LLM que je produis est tracé (Langfuse). On ne peut pas améliorer ce qu'on ne mesure pas.
|
||||||
|
3. **Utilité concrète** — pas d'IA pour l'IA : chaque projet résout un problème réel (newsletter automatisée, chatbot de portfolio, transcription vidéo…).
|
||||||
|
|
||||||
|
## Réalisations IA exposées sur le site
|
||||||
|
|
||||||
|
La rubrique `/competences/ia` présente ces travaux en **vignettes navigables** (entité Strapi `realisation-ia`) :
|
||||||
|
|
||||||
|
- [[grasbot|GrasBot — chatbot IA du portfolio]] — RAG maison (graph + BM25) + Qwen3 local + Langfuse.
|
||||||
|
- [[newsletter-ia|Newsletter IA — Ollama + Listmonk + Directus]] — chaîne de publication auto-hébergée avec génération LLM ciblée.
|
||||||
|
- [[transcription-video|Transcription vidéo automatique]] — pipeline multimédia (décodage → segmentation → STT → post-processing).
|
||||||
|
- *Parcours / formation* : voir [[ft-linear-regression]] et [[piscine-python-data-science]].
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -56,6 +93,6 @@ Animé par une passion pour l’intelligence artificielle et ses innombrables po
|
|||||||
## Liens
|
## Liens
|
||||||
|
|
||||||
- [[MOC-Competences]] — vue d'ensemble des compétences
|
- [[MOC-Competences]] — vue d'ensemble des compétences
|
||||||
|
- [[MOC-Ia]] — domaine *ia*
|
||||||
- [[MOC-Algorithmique]] — domaine *algorithmique*
|
- [[MOC-Algorithmique]] — domaine *algorithmique*
|
||||||
- [[MOC-Ecole-42]] — domaine *ecole-42*
|
- [[MOC-Ecole-42]] — domaine *ecole-42*
|
||||||
- [[MOC-Ia]] — domaine *ia*
|
|
||||||
|
|||||||
@ -20,9 +20,11 @@ linked:
|
|||||||
- "[[MOC-Technique]]"
|
- "[[MOC-Technique]]"
|
||||||
- "[[developpement-web-and-hebergement-sur-serveur-windows]]"
|
- "[[developpement-web-and-hebergement-sur-serveur-windows]]"
|
||||||
- "[[ia]]"
|
- "[[ia]]"
|
||||||
|
- "[[grasbot]]"
|
||||||
|
- "[[fernandgrascalvet-com]]"
|
||||||
- "[[grasbot-retrieval]]"
|
- "[[grasbot-retrieval]]"
|
||||||
- "[[vault-structure]]"
|
- "[[vault-structure]]"
|
||||||
updated: 2026-04-22
|
updated: 2026-04-23
|
||||||
visibility: public
|
visibility: public
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -53,15 +55,16 @@ indépendantes, reliées par des API HTTP.
|
|||||||
Manrope + Newsreader, radius `sheet` / `tile`, ombres `ambient` / `jewel`.
|
Manrope + Newsreader, radius `sheet` / `tile`, ombres `ambient` / `jewel`.
|
||||||
- **Pages principales** : `/` (hero + takeaways + démarche), `/portfolio`
|
- **Pages principales** : `/` (hero + takeaways + démarche), `/portfolio`
|
||||||
(grille asymétrique 2/3 + 1/3), `/competences` (même pattern), fiches
|
(grille asymétrique 2/3 + 1/3), `/competences` (même pattern), fiches
|
||||||
détail `[slug]`, `/contact`.
|
détail (`/portfolio/[slug]`, `/competences/[slug]`), réalisations liées
|
||||||
|
`/competences/[slug]/[realisation]`, `/contact` (Brevo).
|
||||||
- **Composant chat** : `ChatBot.js` + FAB flottant `GrasBotFab.tsx` monté
|
- **Composant chat** : `ChatBot.js` + FAB flottant `GrasBotFab.tsx` monté
|
||||||
dans `layout.tsx` → accessible depuis toutes les pages.
|
dans `layout.tsx` → accessible depuis toutes les pages.
|
||||||
|
|
||||||
## CMS — `cmsbackend/` (Strapi 5)
|
## CMS — `cmsbackend/` (Strapi 5)
|
||||||
|
|
||||||
- Content-types : `homepage`, `project`, `competence`, `glossaire`, `message`.
|
- Content-types : `homepage`, `project`, `competence`, `realisation-ia`, `glossaire` (le type `message` a été retiré — contact via Brevo, voir `docs-site-interne/contact-flow.md`).
|
||||||
- API REST : `https://api.fernandgrascalvet.com/api/<pluralName>`.
|
- API REST : `https://api.fernandgrascalvet.com/api/<pluralName>`.
|
||||||
- Permissions `find` publique sur tous les types `draftAndPublish: true`.
|
- Permissions `find` publique sur les types publics en `draftAndPublish: true`.
|
||||||
|
|
||||||
## Chatbot — `llm-api/` + `vault-grasbot/` + Ollama
|
## Chatbot — `llm-api/` + `vault-grasbot/` + Ollama
|
||||||
|
|
||||||
|
|||||||
@ -6,7 +6,8 @@ source: manual
|
|||||||
domains: [ia, web]
|
domains: [ia, web]
|
||||||
tags: [graph, bm25, ollama, qwen3, retrieval]
|
tags: [graph, bm25, ollama, qwen3, retrieval]
|
||||||
aliases:
|
aliases:
|
||||||
- grasbot
|
- pipeline grasbot
|
||||||
|
- moteur grasbot
|
||||||
- chatbot du site
|
- chatbot du site
|
||||||
- moteur de recherche
|
- moteur de recherche
|
||||||
- retrieval
|
- retrieval
|
||||||
@ -18,11 +19,12 @@ answers:
|
|||||||
- "Quel modèle utilise GrasBot ?"
|
- "Quel modèle utilise GrasBot ?"
|
||||||
priority: 6
|
priority: 6
|
||||||
linked:
|
linked:
|
||||||
|
- "[[grasbot]]"
|
||||||
- "[[MOC-Ia]]"
|
- "[[MOC-Ia]]"
|
||||||
- "[[architecture-site]]"
|
- "[[architecture-site]]"
|
||||||
- "[[vault-structure]]"
|
- "[[vault-structure]]"
|
||||||
- "[[ia]]"
|
- "[[ia]]"
|
||||||
updated: 2026-04-22
|
updated: 2026-04-23
|
||||||
visibility: public
|
visibility: public
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@ -5,12 +5,12 @@ contenus Strapi du site (projets + compétences) et du CV PDF. Alimente
|
|||||||
directement le pipeline de recherche de GrasBot (`llm-api/search.py`) :
|
directement le pipeline de recherche de GrasBot (`llm-api/search.py`) :
|
||||||
graph + BM25, sans embeddings.
|
graph + BM25, sans embeddings.
|
||||||
|
|
||||||
**Dernière génération :** 2026-04-22
|
**Dernière génération :** 2026-04-23 (complété manuellement : +2 projets `source: manual`, maj compétences IA/Web)
|
||||||
|
|
||||||
## Structure
|
## Structure
|
||||||
|
|
||||||
- `00-MOC/` — Maps of Content (hubs thématiques)
|
- `00-MOC/` — Maps of Content (hubs thématiques)
|
||||||
- `10-Projets/` — 17 projets extraits de Strapi
|
- `10-Projets/` — 17 projets Strapi + **6 notes manuelles** (`grasbot.md`, `newsletter-ia.md`, `transcription-video.md`, `fernandgrascalvet-com.md`, `ft-linear-regression.md`, `piscine-python-data-science.md`)
|
||||||
- `20-Competences/` — 4 compétences extraites de Strapi
|
- `20-Competences/` — 4 compétences extraites de Strapi
|
||||||
- `30-Parcours/` — Parcours personnel, CV, bio (version curatée `source: manual`)
|
- `30-Parcours/` — Parcours personnel, CV, bio (version curatée `source: manual`)
|
||||||
- `40-Glossaire/` — Termes techniques (vide, à remplir manuellement ou depuis Strapi plus tard)
|
- `40-Glossaire/` — Termes techniques (vide, à remplir manuellement ou depuis Strapi plus tard)
|
||||||
|
|||||||
@ -52,6 +52,21 @@ actuellement utilisés :
|
|||||||
| `concurrence` | Threads, mutex, synchronisation |
|
| `concurrence` | Threads, mutex, synchronisation |
|
||||||
| `docker` | Conteneurisation |
|
| `docker` | Conteneurisation |
|
||||||
| `makefile` | Build system GNU Make |
|
| `makefile` | Build system GNU Make |
|
||||||
|
| `chatbot` | Assistants conversationnels |
|
||||||
|
| `ollama` | Serveur de LLM local |
|
||||||
|
| `fastapi` | API Python FastAPI |
|
||||||
|
| `langfuse` | Observabilité LLM |
|
||||||
|
| `nextjs` | Framework React Next.js |
|
||||||
|
| `strapi` | CMS headless Strapi |
|
||||||
|
| `portfolio` | Site portfolio personnel |
|
||||||
|
| `qwen3` | Modèle LLM Qwen3 |
|
||||||
|
| `listmonk` | Diffusion de newsletters open-source |
|
||||||
|
| `directus` | CMS headless Directus |
|
||||||
|
| `newsletter` | Chaîne de publication newsletter |
|
||||||
|
| `openwebui` | Open WebUI (front LLM) |
|
||||||
|
| `ovh` | OVHcloud AI Endpoints |
|
||||||
|
| `transcription` | Speech-to-text / sous-titres |
|
||||||
|
| `multimedia` | Traitement audio / vidéo |
|
||||||
|
|
||||||
## Aliases (`aliases:`)
|
## Aliases (`aliases:`)
|
||||||
|
|
||||||
|
|||||||