Compare commits

...

5 Commits

Author SHA1 Message Date
1267d60cf2 contact_visuel_ok 2026-04-22 20:29:00 +02:00
747193ea3c chatbot 2026-04-22 20:11:16 +02:00
ab47a41a37 etape7ok 2026-04-22 16:39:19 +02:00
f824053a31 etape6ok 2026-04-22 16:12:31 +02:00
0a506dbf39 etape5ok 2026-04-22 15:56:35 +02:00
78 changed files with 7906 additions and 464 deletions

9
.gitignore vendored
View File

@ -41,3 +41,12 @@ yarn-error.log*
next-env.d.ts
.vercel
# Python (llm-api)
llm-api/__pycache__/
llm-api/.venv/
llm-api/*.pyc
# Legacy RAG index (ChromaDB) — obsolete depuis bascule graph+BM25
/chroma-index/

View File

@ -3,12 +3,24 @@
import { useEffect, useState } from "react";
import Link from "next/link";
import { getApiUrl } from "../utils/getApiUrl";
import CarouselCompetences from "../components/CarouselCompetences";
import VignetteCarousel from "../components/VignetteCarousel";
import "../globals.css";
import "../assets/main.css";
/**
* Liste des compétences refonte "Digital Atelier" (étape 6).
*
* Même pattern de grille asymétrique 2/3 + 1/3 que `app/portfolio/page.jsx` pour
* garder une cohérence visuelle entre les deux rubriques principales. Le
* `CarouselCompetences` est retiré de la liste (arbitrage REFONTE-VISUELLE.md §2 :
* carousel réservé aux galeries intra-fiche) : seule la première image est
* affichée ici, ce qui allège le rendu et clarifie la lecture.
*/
const spanPattern = ["md:col-span-4", "md:col-span-2", "md:col-span-2", "md:col-span-4"];
export default function Page() {
const [competences, setCompetences] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const apiUrl = getApiUrl();
useEffect(() => {
@ -21,57 +33,141 @@ export default function Page() {
const data = await response.json();
// Tri sécurisé des compétences par `order`
const sortedCompetences = (data.data ?? []).sort((a, b) => ((a.attributes?.order ?? 999) - (b.attributes?.order ?? 999)));
const sortedCompetences = (data.data ?? []).sort(
(a, b) => ((a.attributes?.order ?? 999) - (b.attributes?.order ?? 999))
);
setCompetences(sortedCompetences);
} catch (error) {
console.error("❌ Erreur lors de la récupération des compétences :", error);
} finally {
setIsLoading(false);
}
}
fetchCompetences();
}, [apiUrl]);
return (
<main className="w-full p-3 mt-5 mb-5">
{competences.length === 0 ? (
<p className="text-center text-gray-500">Aucune compétence disponible.</p>
) : (
<div className="flex flex-col gap-7 max-w-7xl mx-auto">
{competences.map((competence) => {
const pictures = competence.picture || [];
const images = pictures.map(picture => ({
url: picture.url ? `${apiUrl}${picture.url}` : "/placeholder.jpg",
alt: picture.name || "Competence image"
}));
return (
<div
key={competence.id}
className="bg-white/70 rounded-lg shadow-md overflow-hidden w-full max-w-xs sm:max-w-md md:max-w-lg lg:max-w-xl xl:max-w-2xl h-auto flex flex-col transform transition-all duration-300 hover:scale-105 hover:shadow-xl p-4"
>
<Link href={`/competences/${competence.slug}`}>
<div className="overflow-hidden w-full h-64 mb-4">
<CarouselCompetences images={images} className="w-full h-full object-cover" />
</div>
<div className="flex-grow overflow-y-auto max-h-32 hide-scrollbar show-scrollbar">
<p className="font-headline font-bold text-xl mb-2">{competence.name}</p>
<p className="text-gray-700 text-sm hover:text-base transition-all duration-200 ease-in-out">
{competence.description}
<div className="mx-auto flex w-full min-w-0 max-w-6xl flex-col gap-5 px-4 pb-10 sm:px-6">
{/* En-tête éditorial cohérent avec le portfolio. */}
<section
className="rounded-sheet bg-surface-container-lowest/85 p-5 shadow-ambient backdrop-blur-vellum sm:p-7 md:p-8"
aria-labelledby="competences-title"
>
<div className="flex flex-col gap-3 text-center md:text-left">
<span className="font-headline text-[11px] font-bold uppercase tracking-[0.3em] text-secondary">
Compétences · Savoir-faire
</span>
<h1
id="competences-title"
className="font-headline text-3xl font-extrabold tracking-tight text-on-surface md:text-4xl lg:text-5xl"
>
Ce que je sais faire, et comment
</h1>
<p className="font-body text-on-surface-variant sm:text-lg">
Chaque fiche détaille une compétence, son contexte dapprentissage et des
exemples concrets ouvrez une carte pour voir les outils mobilisés et les
projets associés.
</p>
</div>
</section>
{isLoading ? (
<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 className="mt-1.5 h-4 w-5/6 animate-pulse rounded-full bg-surface-container-low/60" />
</div>
))}
</div>
) : competences.length === 0 ? (
<section className="rounded-sheet bg-surface-container-lowest/75 p-8 text-center shadow-ambient-sm backdrop-blur-vellum">
<span
className="material-symbols-outlined mb-3 text-4xl text-primary"
aria-hidden="true"
translate="no"
>
school
</span>
<p className="font-body italic text-on-surface-variant">
Aucune compétence disponible pour le moment.
</p>
</section>
) : (
<div className="grid grid-cols-1 gap-5 sm:grid-cols-2 md:grid-cols-6">
{competences.map((competence, idx) => {
const pictures = competence.picture ?? [];
const images = pictures.map((img) => ({
url: img.url ? `${apiUrl}${img.url}` : "/placeholder.jpg",
alt: img.name || `Visuel de la compétence ${competence.name}`,
}));
const firstImage = images[0];
return (
<Link
key={competence.id}
href={`/competences/${competence.slug}`}
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">
Compétence
</span>
<h2 className="font-headline text-xl font-extrabold leading-tight tracking-tight text-primary">
{competence.name}
</h2>
{competence.description && (
<p className="font-body text-sm leading-relaxed text-on-surface-variant line-clamp-3 sm:text-base">
{competence.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">
Explorer
<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>
)}
</main>
</div>
);
}

View File

@ -1,6 +1,6 @@
"use client";
import { useState } from "react";
import { useEffect, useState } from "react";
import { createPortal } from "react-dom";
import { Swiper, SwiperSlide } from "swiper/react";
import { Navigation, Pagination, Autoplay } from "swiper/modules";
@ -10,6 +10,25 @@ import "swiper/css/pagination";
import "../globals.css";
import "../assets/main.css";
/**
* Carousel "fiche détail" refonte Stitch (étape 7.a).
*
* Conserve l'API publique (`images`, `className`) pour ne rien casser côté
* consommateurs (`ContentSection`, `ModalGlossaire`). Changements vs version
* historique :
*
* - Pagination bullets teintée `primary` via surcharge inline des variables
* Swiper (pas de pollution `globals.css`, cf. même approche que `VignetteCarousel`).
* - Flèches Swiper par défaut recolorées `primary` laissées en chevrons
* Swiper pour garder l'empreinte tactile native (le Material Symbol `chevron`
* demandait un remplacement complet du markup via slots).
* - Conteneur en `rounded-tile overflow-hidden shadow-ambient-sm` plutôt que
* `rounded-md shadow-md` cohérence avec les tokens de la refonte.
* - **Lightbox refaite en Stitch** : voile `bg-on-surface/80 backdrop-blur-sm`
* (vs `bg-black/10 backdrop-blur-2xl` qui dépixellisait l'image), image en
* `object-contain` (ne déforme plus les photos verticales), bouton close rond
* Material Symbol, fermeture Esc + clic voile.
*/
interface CarouselProps {
images: Array<{ url: string; alt: string }>;
className?: string;
@ -18,25 +37,53 @@ interface CarouselProps {
export default function Carousel({ images, className }: CarouselProps) {
const [selectedImage, setSelectedImage] = useState<string | null>(null);
useEffect(() => {
if (!selectedImage) return;
document.body.classList.add("overflow-hidden");
const handleKey = (e: KeyboardEvent) => {
if (e.key === "Escape") setSelectedImage(null);
};
window.addEventListener("keydown", handleKey);
return () => {
document.body.classList.remove("overflow-hidden");
window.removeEventListener("keydown", handleKey);
};
}, [selectedImage]);
return (
<>
<div className={`relative w-full ${className || "h-64"} rounded-md shadow-md`}>
<div
className={`relative w-full ${className || "h-64"} overflow-hidden rounded-tile shadow-ambient-sm`}
>
<Swiper
modules={[Navigation, Pagination, Autoplay]}
spaceBetween={10}
slidesPerView={1}
navigation
pagination={{ clickable: true }}
autoplay={{ delay: 3000 }}
autoplay={{ delay: 3500, disableOnInteraction: false }}
loop={images.length > 1}
className={`w-full ${className || "h-64"}`}
style={
{
"--swiper-navigation-color": "#26445d",
"--swiper-navigation-size": "28px",
"--swiper-pagination-color": "#26445d",
"--swiper-pagination-bullet-inactive-color": "#ffffff",
"--swiper-pagination-bullet-inactive-opacity": "0.6",
"--swiper-pagination-bullet-size": "8px",
"--swiper-pagination-bullet-horizontal-gap": "4px",
} as React.CSSProperties
}
>
{images.map((img, index) => (
<SwiperSlide key={index} className="flex items-center justify-center h-full">
<SwiperSlide key={index} className="flex h-full items-center justify-center">
<img
src={img.url}
alt={img.alt}
className="w-full h-full object-cover rounded-md cursor-pointer transition-transform duration-300 hover:scale-105"
className="h-full w-full cursor-zoom-in object-cover transition-transform duration-300 hover:scale-[1.02]"
onClick={() => setSelectedImage(img.url)}
loading="lazy"
/>
</SwiperSlide>
))}
@ -46,20 +93,34 @@ export default function Carousel({ images, className }: CarouselProps) {
{selectedImage &&
createPortal(
<div
className="fixed inset-0 flex items-center justify-center w-screen h-screen bg-black bg-opacity-10 backdrop-blur-2xl transition-opacity duration-300 z-[1000]"
className="fixed inset-0 z-[1000] flex items-center justify-center bg-on-surface/80 p-4 backdrop-blur-sm transition-opacity duration-300"
onClick={() => setSelectedImage(null)}
role="dialog"
aria-modal="true"
aria-label="Image agrandie"
>
<div
className="relative flex max-h-[92vh] max-w-[92vw] items-center justify-center"
onClick={(e) => e.stopPropagation()}
>
<div className="relative w-full max-w-6xl p-6 bg-transparent">
<button
className="absolute top-6 right-6 text-white text-l bg-gray-900/70 p-2 rounded-full"
type="button"
className="absolute -right-1 -top-1 z-10 flex h-10 w-10 items-center justify-center rounded-full bg-surface-container-lowest/95 text-primary shadow-ambient-sm transition-colors hover:bg-primary hover:text-white focus:outline-none focus-visible:ring-2 focus-visible:ring-primary"
onClick={() => setSelectedImage(null)}
aria-label="Fermer l'aperçu"
>
<span
className="material-symbols-outlined"
aria-hidden="true"
translate="no"
>
close
</span>
</button>
<img
src={selectedImage}
alt="Agrandissement"
className="w-full h-full object-cover rounded-md"
alt="Aperçu en taille réelle"
className="max-h-[92vh] max-w-[92vw] rounded-sheet object-contain shadow-ambient"
/>
</div>
</div>,

View File

@ -1,6 +1,6 @@
"use client";
import { useState } from "react";
import { useEffect, useState } from "react";
import { createPortal } from "react-dom";
import { Swiper, SwiperSlide } from "swiper/react";
import { Navigation, Pagination, Autoplay } from "swiper/modules";
@ -10,6 +10,13 @@ import "swiper/css/pagination";
import "../globals.css";
import "../assets/main.css";
/**
* Variante du `Carousel` pour les fiches compétences + la modale glossaire.
* Comportement et style identiques à `Carousel.tsx` (étape 7.a) les deux
* composants sont des quasi-doublons historiques, fusionner proprement demande
* de rationaliser `ContentSection*` et `ModalGlossaire` en même temps : hors
* scope, on garde la même API et les mêmes styles côte à côte pour l'instant.
*/
interface CarouselProps {
images: Array<{ url: string; alt: string }>;
className?: string;
@ -18,25 +25,53 @@ interface CarouselProps {
export default function CarouselCompetences({ images, className }: CarouselProps) {
const [selectedImage, setSelectedImage] = useState<string | null>(null);
useEffect(() => {
if (!selectedImage) return;
document.body.classList.add("overflow-hidden");
const handleKey = (e: KeyboardEvent) => {
if (e.key === "Escape") setSelectedImage(null);
};
window.addEventListener("keydown", handleKey);
return () => {
document.body.classList.remove("overflow-hidden");
window.removeEventListener("keydown", handleKey);
};
}, [selectedImage]);
return (
<>
<div className={`relative w-full ${className || "h-64"} rounded-md shadow-md`}>
<div
className={`relative w-full ${className || "h-64"} overflow-hidden rounded-tile shadow-ambient-sm`}
>
<Swiper
modules={[Navigation, Pagination, Autoplay]}
spaceBetween={10}
slidesPerView={1}
navigation
pagination={{ clickable: true }}
autoplay={{ delay: 3000 }}
autoplay={{ delay: 3500, disableOnInteraction: false }}
loop={images.length > 1}
className={`w-full ${className || "h-64"}`}
style={
{
"--swiper-navigation-color": "#26445d",
"--swiper-navigation-size": "28px",
"--swiper-pagination-color": "#26445d",
"--swiper-pagination-bullet-inactive-color": "#ffffff",
"--swiper-pagination-bullet-inactive-opacity": "0.6",
"--swiper-pagination-bullet-size": "8px",
"--swiper-pagination-bullet-horizontal-gap": "4px",
} as React.CSSProperties
}
>
{images.map((img, index) => (
<SwiperSlide key={index} className="flex items-center justify-center h-full">
<SwiperSlide key={index} className="flex h-full items-center justify-center">
<img
src={img.url}
alt={img.alt}
className="w-full h-full object-cover rounded-md cursor-pointer transition-transform duration-300 hover:scale-105"
className="h-full w-full cursor-zoom-in object-cover transition-transform duration-300 hover:scale-[1.02]"
onClick={() => setSelectedImage(img.url)}
loading="lazy"
/>
</SwiperSlide>
))}
@ -46,20 +81,34 @@ export default function CarouselCompetences({ images, className }: CarouselProps
{selectedImage &&
createPortal(
<div
className="fixed inset-0 flex items-center justify-center w-screen h-screen bg-black bg-opacity-10 backdrop-blur-2xl transition-opacity duration-300 z-[1000]"
className="fixed inset-0 z-[1000] flex items-center justify-center bg-on-surface/80 p-4 backdrop-blur-sm transition-opacity duration-300"
onClick={() => setSelectedImage(null)}
role="dialog"
aria-modal="true"
aria-label="Image agrandie"
>
<div
className="relative flex max-h-[92vh] max-w-[92vw] items-center justify-center"
onClick={(e) => e.stopPropagation()}
>
<div className="relative w-full max-w-6xl p-6 bg-transparent">
<button
className="absolute top-6 right-6 text-white text-l bg-gray-900/70 p-2 rounded-full"
type="button"
className="absolute -right-1 -top-1 z-10 flex h-10 w-10 items-center justify-center rounded-full bg-surface-container-lowest/95 text-primary shadow-ambient-sm transition-colors hover:bg-primary hover:text-white focus:outline-none focus-visible:ring-2 focus-visible:ring-primary"
onClick={() => setSelectedImage(null)}
aria-label="Fermer l'aperçu"
>
<span
className="material-symbols-outlined"
aria-hidden="true"
translate="no"
>
close
</span>
</button>
<img
src={selectedImage}
alt="Agrandissement"
className="w-full h-full object-cover rounded-md"
alt="Aperçu en taille réelle"
className="max-h-[92vh] max-w-[92vw] rounded-sheet object-contain shadow-ambient"
/>
</div>
</div>,

View File

@ -1,70 +1,244 @@
import { useState } from "react";
"use client";
import Link from "next/link";
import { useEffect, useRef, useState } from "react";
import { askAI } from "../utils/askAI";
/**
* GrasBot UI du chatbot (Stitch).
*
* L'API publique ne change pas (`onClose` propagé par le parent). Le bouton
* d'ouverture est porté par le FAB global `GrasBotFab` monté dans
* `app/layout.tsx`. Ce composant se concentre sur le panneau de conversation.
*
* v3 (2026-04-22) bascule retrieval graph + BM25 :
* - Affichage des `sources` renvoyées par l'API (pill par source, cliquable
* vers `/portfolio/<slug>` ou `/competences/<slug>` si dispo).
* - Badge `grounded` sous chaque réponse (paperclip si sources exploitées,
* info si réponse générale faute de contexte pertinent).
* - Timeout 45 s côté fetch (géré dans `askAI.js`) avec message éditorial.
*
* Design :
* - Fond `surface-container-lowest/95 backdrop-blur-vellum rounded-sheet shadow-ambient`.
* - Bulles : user = `bg-primary text-white` à droite, bot = `bg-surface-container` à gauche.
* - Sources : petites pills `bg-surface-container-low text-primary` sous la bulle bot.
* - Auto-scroll en bas, envoi à Enter, focus auto, disabled pendant attente.
*/
export default function ChatBot({ onClose }) {
const [question, setQuestion] = useState("");
const [messages, setMessages] = useState([]);
const [isWaiting, setIsWaiting] = useState(false);
const scrollRef = useRef(null);
const inputRef = useRef(null);
useEffect(() => {
if (scrollRef.current) {
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
}
}, [messages, isWaiting]);
useEffect(() => {
inputRef.current?.focus();
}, []);
const handleAsk = async () => {
if (!question.trim()) return;
if (!question.trim() || isWaiting) return;
const userMessage = { sender: "user", text: question };
setMessages([...messages, userMessage]);
setMessages((prev) => [...prev, userMessage]);
setQuestion("");
setIsWaiting(true);
try {
const botResponse = await askAI(question);
const botMessage = { sender: "bot", text: botResponse };
setMessages((prevMessages) => [...prevMessages, botMessage]);
} catch (error) {
setMessages([...messages, { sender: "bot", text: "❌ Erreur de réponse. Réessayez plus tard." }]);
const payload = await askAI(userMessage.text);
setMessages((prev) => [
...prev,
{
sender: "bot",
text: payload.response,
sources: payload.sources || [],
grounded: Boolean(payload.grounded),
timeout: Boolean(payload._timeout),
},
]);
} catch (_error) {
setMessages((prev) => [
...prev,
{
sender: "bot",
text: "Erreur de réponse. Réessayez plus tard.",
sources: [],
grounded: false,
error: true,
},
]);
} finally {
setIsWaiting(false);
}
};
const handleKeyDown = (e) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
handleAsk();
}
};
return (
<div className="flex flex-col w-96 bg-white/70 shadow-lg rounded-lg border border-gray-300">
<div className="bg-blue-600 text-white p-3 rounded-t-lg flex justify-between items-center">
<span className="font-headline font-bold">💬 GrasBot</span>
<button className="text-white hover:text-red-400 text-xl" onClick={onClose}></button>
<div className="flex h-full w-full flex-col overflow-hidden rounded-sheet bg-surface-container-lowest/95 shadow-ambient backdrop-blur-vellum">
<div className="flex items-center justify-between gap-3 bg-primary px-4 py-3 text-white">
<div className="flex min-w-0 items-center gap-2">
<span
className="material-symbols-outlined text-2xl"
aria-hidden="true"
translate="no"
>
smart_toy
</span>
<div className="min-w-0">
<p className="truncate font-headline text-sm font-bold tracking-tight">GrasBot</p>
<p className="truncate font-headline text-[10px] uppercase tracking-[0.25em] text-primary-fixed">
Assistant IA locale
</p>
</div>
</div>
<button
type="button"
onClick={onClose}
aria-label="Fermer le chat"
className="flex h-9 w-9 items-center justify-center rounded-full text-white transition-colors hover:bg-primary-container focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-fixed"
>
<span
className="material-symbols-outlined"
aria-hidden="true"
translate="no"
>
close
</span>
</button>
</div>
<div className="h-64 overflow-y-auto p-4 space-y-2">
{messages.map((msg, index) => (
<div
ref={scrollRef}
className="flex-1 space-y-2 overflow-y-auto bg-surface-container-lowest/60 p-4"
>
{messages.length === 0 && !isWaiting && (
<p className="mt-6 text-center font-body italic text-sm text-on-surface-variant">
Posez-moi une question sur le site, les projets ou les compétences.
</p>
)}
{messages.map((msg, index) => {
if (msg.sender === "user") {
return (
<div
key={index}
className={`p-2 rounded-lg text-white font-headline text-xs ${msg.sender === "user" ? "bg-blue-500 ml-auto" : "bg-gray-500 mr-auto"}`}
style={{ maxWidth: "80%" }}
className="ml-auto max-w-[80%] rounded-sheet bg-primary px-3 py-2 font-headline text-xs leading-relaxed text-white"
>
{msg.text}
</div>
))}
);
}
return (
<div key={index} className="mr-auto flex max-w-[85%] flex-col gap-1.5">
<div className="rounded-sheet bg-surface-container px-3 py-2 font-headline text-xs leading-relaxed text-on-surface">
{msg.text}
</div>
{(msg.sources?.length > 0 || msg.grounded !== undefined) && !msg.error && !msg.timeout && (
<BotFooter sources={msg.sources} grounded={msg.grounded} />
)}
</div>
);
})}
{isWaiting && (
<div className="p-2 rounded-lg text-white bg-gray-500 mr-auto wait-animation" style={{ maxWidth: "80%" }}>
wait<span className="dot-1">.</span><span className="dot-2">.</span><span className="dot-3">.</span>
<div
className="wait-animation mr-auto inline-flex max-w-[80%] items-end gap-0.5 rounded-sheet bg-surface-container px-3 py-2 font-headline text-xs text-on-surface-variant"
aria-live="polite"
>
GrasBot réfléchit
<span className="dot-1">.</span>
<span className="dot-2">.</span>
<span className="dot-3">.</span>
</div>
)}
</div>
<div className="flex p-3 border-t border-gray-300">
<div className="flex items-center gap-2 border-t border-outline-variant/30 bg-surface-container-lowest/80 p-3">
<input
ref={inputRef}
type="text"
className="flex-1 p-2 border border-gray-300 font-headline text-xs rounded-l-lg focus:outline-none"
placeholder="Posez votre question..."
className="min-w-0 flex-1 rounded-tile bg-surface-container-low px-3 py-2 font-headline text-xs text-on-surface placeholder:text-on-surface-variant focus:outline-none focus-visible:ring-2 focus-visible:ring-primary"
placeholder="Votre question…"
value={question}
onChange={(e) => setQuestion(e.target.value)}
onKeyDown={handleKeyDown}
disabled={isWaiting}
aria-label="Votre question pour GrasBot"
/>
<button
className="bg-blue-500 text-white px-4 py-2 rounded-r-lg hover:bg-blue-700"
type="button"
onClick={handleAsk}
disabled={isWaiting || !question.trim()}
aria-label="Envoyer la question"
className="flex h-10 w-10 items-center justify-center rounded-full bg-primary text-white shadow-jewel transition-transform duration-200 hover:-translate-y-0.5 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary disabled:cursor-not-allowed disabled:opacity-50 disabled:shadow-none disabled:hover:translate-y-0"
>
<span
className="material-symbols-outlined text-lg"
aria-hidden="true"
translate="no"
>
send
</span>
</button>
</div>
</div>
);
}
/**
* Sous-composant : pied d'une réponse bot avec badge grounded + sources.
* Extrait pour alléger la lisibilité et garder les styles groupés.
*/
function BotFooter({ sources, grounded }) {
// Filtrer les sources internes sans url + dédoublonner par slug
const uniqueSources = [];
const seen = new Set();
for (const s of sources || []) {
if (!s?.slug || seen.has(s.slug)) continue;
seen.add(s.slug);
uniqueSources.push(s);
}
const clickable = uniqueSources.filter((s) => s.url);
const displayed = clickable.slice(0, 4);
return (
<div className="flex flex-wrap items-center gap-1.5 pl-1 text-[10px]">
<span
className={`inline-flex items-center gap-1 rounded-full px-2 py-0.5 font-headline uppercase tracking-[0.15em] ${
grounded
? "bg-primary/10 text-primary"
: "bg-surface-container text-on-surface-variant"
}`}
>
<span className="material-symbols-outlined text-[12px]" aria-hidden="true" translate="no">
{grounded ? "verified" : "info"}
</span>
{grounded ? "Basé sur le vault" : "Réponse générale"}
</span>
{displayed.map((s) => (
<Link
key={s.slug}
href={s.url}
className="inline-flex items-center gap-1 rounded-full bg-surface-container-low px-2 py-0.5 font-headline text-[10px] text-primary transition-colors hover:bg-primary/10 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary"
title={s.title}
>
<span className="material-symbols-outlined text-[12px]" aria-hidden="true" translate="no">
{s.type === "competence" ? "psychology" : "folder"}
</span>
{s.slug}
</Link>
))}
</div>
);
}

View File

@ -3,98 +3,158 @@
import { useState } from "react";
import { sendMessage } from "../utils/sendMessage";
/**
* Formulaire de contact refonte "Digital Atelier" (étape 8).
*
* - Plus de `bg-white shadow-lg rounded-lg` sur le form : il est désormais
* monté dans la carte vellum de `app/contact/page.js`.
* - Champs : `bg-surface-container-low`, radius `rounded-tile`, `focus-visible:ring-2 focus-visible:ring-primary`.
* - CTA jewel : `bg-primary text-on-primary shadow-jewel` avec Material Symbol
* `send` + effet `-translate-y-0.5` au hover, état disabled en `bg-outline-variant/60`.
* - Bandeau status Stitch : succès en `primary-fixed`, erreur en `error-container`,
* chargement en `surface-container`. Chaque état porte une Material Symbol.
*/
export default function ContactForm() {
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [message, setMessage] = useState("");
const [status, setStatus] = useState("");
const [isSuccess, setIsSuccess] = useState<boolean | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [statusKind, setStatusKind] = useState<
"idle" | "loading" | "success" | "error"
>("idle");
const isLoading = statusKind === "loading";
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!name.trim() || !email.trim() || !message.trim()) {
setStatus("Tous les champs sont obligatoires.");
setIsSuccess(false);
setStatus("Tous les champs sont obligatoires.");
setStatusKind("error");
return;
}
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
setStatus("Email invalide.");
setIsSuccess(false);
setStatus("Email invalide.");
setStatusKind("error");
return;
}
setStatus("⏳ Envoi en cours...");
setIsSuccess(null);
setIsLoading(true);
setStatus("Envoi en cours…");
setStatusKind("loading");
try {
await sendMessage(name, email, message);
setStatus("✅ Message envoyé avec succès !");
setIsSuccess(true);
setStatus("Message envoyé. Merci, je reviens vers vous rapidement.");
setStatusKind("success");
setName("");
setEmail("");
setMessage("");
} catch (error) {
setStatus("❌ Erreur lors de l'envoi du message.");
setIsSuccess(false);
} finally {
setIsLoading(false);
setStatus("Erreur lors de l'envoi du message.");
setStatusKind("error");
}
};
return (
<form
onSubmit={handleSubmit}
className="max-w-lg mx-auto p-6 bg-white shadow-lg rounded-lg animate-fade-in"
>
<h2 className="text-2xl font-headline font-bold mb-4 text-center">📩 Contactez-moi</h2>
const statusStyles: Record<typeof statusKind, string> = {
idle: "",
loading:
"bg-surface-container text-on-surface-variant",
success:
"bg-primary-fixed/70 text-on-primary-fixed",
error: "bg-error-container text-on-error-container",
};
const statusIcon: Record<typeof statusKind, string> = {
idle: "",
loading: "hourglass_top",
success: "check_circle",
error: "error",
};
const fieldClass =
"w-full rounded-tile bg-surface-container-low/90 px-4 py-3 font-body text-base text-on-surface placeholder:text-on-surface-variant/70 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary";
return (
<form onSubmit={handleSubmit} className="flex flex-col gap-3" noValidate>
<label className="flex flex-col gap-1">
<span className="font-headline text-[11px] font-bold uppercase tracking-[0.3em] text-secondary">
Votre nom
</span>
<input
type="text"
placeholder="Votre nom"
placeholder="Prénom Nom"
value={name}
onChange={(e) => setName(e.target.value)}
className="w-full p-3 border border-gray-300 font-headline font-bold rounded mb-3 focus:outline-none focus:ring-2 focus:ring-blue-400"
className={fieldClass}
required
autoComplete="name"
/>
</label>
<label className="flex flex-col gap-1">
<span className="font-headline text-[11px] font-bold uppercase tracking-[0.3em] text-secondary">
Votre email
</span>
<input
type="email"
placeholder="Votre email"
placeholder="adresse@exemple.com"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="w-full p-3 border border-gray-300 rounded font-headline font-bold mb-3 focus:outline-none focus:ring-2 focus:ring-blue-400"
className={fieldClass}
required
autoComplete="email"
/>
</label>
<label className="flex flex-col gap-1">
<span className="font-headline text-[11px] font-bold uppercase tracking-[0.3em] text-secondary">
Votre message
</span>
<textarea
placeholder="Votre message"
placeholder="Quelques mots sur votre projet, question ou intention…"
value={message}
onChange={(e) => setMessage(e.target.value)}
className="w-full p-3 border border-gray-300 rounded mb-3 font-headline font-bold focus:outline-none focus:ring-2 focus:ring-blue-400"
rows={5}
className={`${fieldClass} min-h-[9rem] resize-y`}
required
/>
</label>
<button
type="submit"
disabled={isLoading}
className={`w-full py-3 rounded transition ${
isLoading ? "bg-gray-400 cursor-not-allowed" : "bg-blue-500 hover:bg-blue-600 text-white font-headline font-bold"
className={`mt-1 inline-flex items-center justify-center gap-2 rounded-tile px-6 py-3 font-headline text-sm font-bold uppercase tracking-widest transition-transform focus:outline-none focus-visible:ring-2 focus-visible:ring-primary ${
isLoading
? "cursor-not-allowed bg-outline-variant/60 text-on-surface-variant"
: "bg-primary text-on-primary shadow-jewel hover:-translate-y-0.5"
}`}
>
{isLoading ? "⏳ Envoi..." : "Envoyer"}
<span
className="material-symbols-outlined text-base"
aria-hidden="true"
translate="no"
>
{isLoading ? "hourglass_top" : "send"}
</span>
{isLoading ? "Envoi…" : "Envoyer"}
</button>
{status && (
<p
className={`mt-4 text-center ${isSuccess ? "text-green-600" : "text-red-600"}`}
{statusKind !== "idle" && status && (
<div
role="status"
aria-live="polite"
className={`mt-2 flex items-center gap-2 rounded-tile px-4 py-3 font-body text-sm ${statusStyles[statusKind]}`}
>
{status}
</p>
<span
className="material-symbols-outlined text-base"
aria-hidden="true"
translate="no"
>
{statusIcon[statusKind]}
</span>
<span className="min-w-0">{status}</span>
</div>
)}
</form>
);

View File

@ -1,5 +1,6 @@
"use client";
import Link from "next/link";
import { useEffect, useState } from "react";
import { fetchData } from "../utils/fetchData";
import { getApiUrl } from "../utils/getApiUrl";
@ -31,39 +32,144 @@ interface ContentSectionProps {
contentClass?: string;
}
export default function ContentSection({ collection, slug, titleClass, contentClass }: ContentSectionProps) {
/**
* Fiche détail portfolio refonte "Digital Atelier" (étape 7.b).
*
* Structure : bouton retour + en-tête vellum (kicker + titre Manrope) + carousel
* détail (Swiper Stitch) + corps Markdown en `prose` Newsreader + CTA jewel
* optionnel vers le lien externe. Les props `titleClass` / `contentClass`
* 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
* pour ne pas casser les consommateurs.
*/
export default function ContentSection({
collection,
slug,
}: ContentSectionProps) {
const [data, setData] = useState<ContentData | null>(null);
const [isLoading, setIsLoading] = useState(true);
const apiUrl = getApiUrl();
useEffect(() => {
async function fetchContent() {
try {
const result = await fetchData(collection, slug);
setData(result);
} finally {
setIsLoading(false);
}
}
fetchContent();
}, [collection, slug, apiUrl]);
if (isLoading) {
return (
<div className="mx-auto flex w-full max-w-3xl flex-col gap-4 px-4 pb-10 sm:px-6">
<div className="rounded-sheet bg-surface-container-lowest/65 p-6 shadow-ambient-sm backdrop-blur-vellum">
<div className="h-4 w-24 animate-pulse rounded-full bg-surface-container-low/80" />
<div className="mt-3 h-8 w-3/4 animate-pulse rounded-full bg-surface-container-low/80" />
<div className="mt-5 aspect-[16/9] w-full animate-pulse rounded-tile bg-surface-container-low/80" />
<div className="mt-5 h-4 w-full animate-pulse rounded-full bg-surface-container-low/60" />
<div className="mt-2 h-4 w-5/6 animate-pulse rounded-full bg-surface-container-low/60" />
<div className="mt-2 h-4 w-4/6 animate-pulse rounded-full bg-surface-container-low/60" />
</div>
</div>
);
}
if (!data) {
return <div className="text-center text-gray-500">Contenu introuvable.</div>;
return (
<div className="mx-auto flex w-full max-w-3xl flex-col gap-4 px-4 pb-10 sm:px-6">
<section className="rounded-sheet bg-surface-container-lowest/85 p-8 text-center shadow-ambient backdrop-blur-vellum">
<span
className="material-symbols-outlined mb-3 text-4xl text-primary"
aria-hidden="true"
translate="no"
>
search_off
</span>
<p className="font-body italic text-on-surface-variant">
Ce projet est introuvable.
</p>
<Link
href="/portfolio"
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
className="material-symbols-outlined text-lg"
aria-hidden="true"
translate="no"
>
arrow_back
</span>
Retour au portfolio
</Link>
</section>
</div>
);
}
const { name, Resum: richText, picture, link, linkText } = data;
const images = picture?.map((img: ImageData) => ({
const images =
picture?.map((img: ImageData) => ({
url: `${apiUrl}${img.formats?.large?.url || img.url}`,
alt: img.name || "Image",
alt: img.name || `Visuel du projet ${name}`,
})) || [];
return (
<div className="max-w-3xl mx-auto p-6">
<h1 className={titleClass || "bg-white/50 rounded-md text-3xl mb-6 font-headline font-extrabold tracking-tight p-2 text-blue-700"}>{name}</h1>
<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. */}
<Link
href="/portfolio"
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
className="material-symbols-outlined text-base"
aria-hidden="true"
translate="no"
>
arrow_back
</span>
Portfolio
</Link>
<Carousel images={images} className="w-full h-64" />
{/* En-tête "feuillet de vellum" aligné sur la home et les listes. */}
<section
className="rounded-sheet bg-surface-container-lowest/85 p-5 shadow-ambient backdrop-blur-vellum sm:p-7 md:p-8"
aria-labelledby="project-title"
>
<div className="flex flex-col gap-3">
<span className="font-headline text-[11px] font-bold uppercase tracking-[0.3em] text-secondary">
Projet · Portfolio
</span>
<h1
id="project-title"
className="font-headline text-3xl font-extrabold tracking-tight text-on-surface md:text-4xl"
>
{name}
</h1>
</div>
<div className={contentClass || "bg-white/80 rounded-md p-4 font-headline font-bold shadow-md mt-6"}>
{images.length > 0 && (
<div className="mt-5">
<Carousel images={images} className="h-64 sm:h-80 md:h-96" />
</div>
)}
{richText && (
<div
className="prose prose-sm mt-5 max-w-none font-body text-on-surface-variant sm:prose-base
prose-headings:font-headline prose-headings:text-primary
prose-p:font-body prose-p:text-on-surface-variant
prose-strong:text-on-surface
prose-a:text-primary prose-a:no-underline hover:prose-a:underline
prose-li:marker:text-primary
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"
>
<ReactMarkdown>{richText}</ReactMarkdown>
</div>
)}
{link && (
<div className="mt-6">
@ -71,12 +177,20 @@ export default function ContentSection({ collection, slug, titleClass, contentCl
href={link}
target="_blank"
rel="noopener noreferrer"
className="bg-white/65 rounded-md p-1 text-red-700 hover:underline transition duration-300 ease-in-out transform hover:scale-105 font-headline font-bold hover:text-blue-700"
className="inline-flex items-center gap-2 rounded-tile bg-primary px-5 py-2.5 font-headline text-sm font-bold uppercase tracking-[0.2em] text-white shadow-jewel transition-transform duration-200 hover:-translate-y-0.5 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary"
>
{linkText || "Voir plus/lien externe"}
{linkText || "Voir plus"}
<span
className="material-symbols-outlined text-lg"
aria-hidden="true"
translate="no"
>
open_in_new
</span>
</a>
</div>
)}
</section>
</div>
);
}

View File

@ -1,12 +1,12 @@
"use client";
import { useState, useEffect } from "react";
import Link from "next/link";
import { useEffect, useRef, useState } from "react";
import { getApiUrl } from "../utils/getApiUrl";
import CarouselCompetences from "./CarouselCompetences";
import ReactMarkdown from "react-markdown";
import rehypeRaw from "rehype-raw";
import ModalGlossaire from "./ModalGlossaire";
import ChatBot from "./ChatBot";
interface ImageData {
url: string;
@ -37,32 +37,96 @@ interface ContentSectionProps {
contentClass?: string;
}
/**
* Fiche détail compétences refonte "Digital Atelier" (étape 7.c).
*
* Trois changements structurants par rapport à la version pré-refonte :
*
* 1. **Style tokenisé** : même gabarit "feuillet de vellum" que le portfolio.
* Les classes hardcodées `bg-white/70 text-blue-700 font-headline font-bold`
* disparaissent. Le corps éditorial est rendu en `prose` Newsreader, les
* titres Markdown en Manrope `text-primary`.
*
* 2. **Keywords glossaire & chatbot sans styles inline** : on retire les
* `style="color: red/blue; cursor: pointer"` injectés dans le HTML. On
* conserve les classes `.keyword` / `.chatbot-keyword` historiques et on
* les stylise via `globals.css` avec la palette Stitch (voir `.glossary-keyword`).
* Pour rester rétro-compatible avec les classes historiques, `keyword` est
* renommée `glossary-keyword` dans la transformation.
*
* 3. **Event listeners scopés au wrapper** (ref `contentRef`) plutôt que
* `document.body.addEventListener`. Avant : risque de fuite + interaction
* avec d'autres parties du DOM. Après : la zone "contenu" capture ses clics
* en bubbling, comportement identique mais sans effet de bord global.
*
* 4. **Chatbot via FAB global** (étape 7.e) : plus de `<ChatBot />` local dans
* cette fiche. Un clic sur "IA locale" dispatch `CustomEvent("grasbot:open")`
* que le FAB monté dans `layout.tsx` écoute pour ouvrir le chatbot partagé.
*/
export default function ContentSectionCompetences({
competenceData,
glossaireData,
titleClass,
contentClass,
}: ContentSectionProps) {
console.log("🔍 [ContentSectionCompetences] Chargement du composant...");
const [selectedMot, setSelectedMot] = useState<GlossaireItem | null>(null);
const [isChatbotOpen, setIsChatbotOpen] = useState(false);
const [loading, setLoading] = useState(competenceData === null);
const contentRef = useRef<HTMLDivElement | null>(null);
const apiUrl = getApiUrl();
// Délégation locale : capte les clics sur les keywords injectés dans le Markdown,
// sans polluer document.body comme avant la refonte.
useEffect(() => {
if (competenceData) {
setLoading(false);
}
}, [competenceData]);
const node = contentRef.current;
if (!node) return;
if (loading) {
return <div className="text-center text-gray-500"> Chargement des détails de la compétence...</div>;
const handleClick = (event: Event) => {
const target = event.target as HTMLElement;
if (target.dataset?.chatbot === "true") {
window.dispatchEvent(new CustomEvent("grasbot:open"));
return;
}
if (target.classList?.contains("glossary-keyword")) {
const mot = target.getAttribute("data-mot");
if (!mot) return;
const glossaireMot = glossaireData.find((g) => g.mot_clef === mot);
setSelectedMot(glossaireMot || null);
}
};
node.addEventListener("click", handleClick);
return () => node.removeEventListener("click", handleClick);
}, [glossaireData]);
if (!competenceData) {
console.error("❌ [ContentSectionCompetences] Compétence introuvable !");
return <div className="text-red-500 text-center"> Compétence introuvable.</div>;
return (
<div className="mx-auto flex w-full max-w-3xl flex-col gap-4 px-4 pb-10 sm:px-6">
<section className="rounded-sheet bg-surface-container-lowest/85 p-8 text-center shadow-ambient backdrop-blur-vellum">
<span
className="material-symbols-outlined mb-3 text-4xl text-primary"
aria-hidden="true"
translate="no"
>
search_off
</span>
<p className="font-body italic text-on-surface-variant">
Cette compétence est introuvable.
</p>
<Link
href="/competences"
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
className="material-symbols-outlined text-lg"
aria-hidden="true"
translate="no"
>
arrow_back
</span>
Retour aux compétences
</Link>
</section>
</div>
);
}
const { name, content, picture } = competenceData;
@ -70,19 +134,24 @@ export default function ContentSectionCompetences({
const images =
picture?.map((img) => ({
url: `${apiUrl}${img.formats?.large?.url || img.url}`,
alt: img.name || "Image de compétence",
alt: img.name || `Visuel de la compétence ${name}`,
})) || [];
/**
* Transforme le Markdown en injectant des spans `.glossary-keyword` / `.chatbot-keyword`
* autour des mots-clés trouvés. Les styles sont définis dans `globals.css`
* (palette Stitch, soulignement pointillé) plutôt qu'inline dans l'attribut style.
*/
function transformMarkdownWithKeywords(text: string) {
if (!glossaireData.length) return text;
if (!text) return "";
let modifiedText = text;
modifiedText = modifiedText.replace(
/\bIA locale\b/g,
`<span class="chatbot-keyword" data-chatbot="true" style="color: red; cursor: pointer;">IA locale</span>`
`<span class="chatbot-keyword" data-chatbot="true" role="button" tabindex="0">IA locale</span>`
);
if (glossaireData.length) {
glossaireData.forEach(({ mot_clef, variantes }) => {
const regexVariants = variantes
.map((v) => v.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
@ -90,58 +159,70 @@ export default function ContentSectionCompetences({
const regex = new RegExp(`\\b(${mot_clef}|${regexVariants})\\b`, "gi");
modifiedText = modifiedText.replace(regex, (match) => {
return `<span class="keyword" data-mot="${mot_clef}" style="color: blue; cursor: pointer;">${match}</span>`;
return `<span class="glossary-keyword" data-mot="${mot_clef}" role="button" tabindex="0">${match}</span>`;
});
});
}
return modifiedText;
}
const contentWithLinks = transformMarkdownWithKeywords(content);
useEffect(() => {
function handleKeywordClick(event: MouseEvent) {
const target = event.target as HTMLElement;
if (target.classList.contains("keyword")) {
const mot = target.getAttribute("data-mot");
if (mot) {
const glossaireMot = glossaireData.find((g) => g.mot_clef === mot);
setSelectedMot(glossaireMot || null);
}
}
}
document.body.addEventListener("click", handleKeywordClick);
return () => document.body.removeEventListener("click", handleKeywordClick);
}, [glossaireData]);
useEffect(() => {
function handleChatbotClick(event: MouseEvent) {
const target = event.target as HTMLElement;
if (target.dataset.chatbot === "true") {
setIsChatbotOpen(true);
}
}
document.body.addEventListener("click", handleChatbotClick);
return () => document.body.removeEventListener("click", handleChatbotClick);
}, []);
return (
<div className="max-w-3xl mx-auto p-6">
<h1 className={titleClass || "bg-white/60 rounded-md p-1 text-2xl mb-6 font-headline font-bold text-blue-700"}>
<div className="mx-auto flex w-full min-w-0 max-w-3xl flex-col gap-5 px-4 pb-10 sm:px-6">
<Link
href="/competences"
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
className="material-symbols-outlined text-base"
aria-hidden="true"
translate="no"
>
arrow_back
</span>
Compétences
</Link>
<section
className="rounded-sheet bg-surface-container-lowest/85 p-5 shadow-ambient backdrop-blur-vellum sm:p-7 md:p-8"
aria-labelledby="competence-title"
>
<div className="flex flex-col gap-3">
<span className="font-headline text-[11px] font-bold uppercase tracking-[0.3em] text-secondary">
Compétence · Savoir-faire
</span>
<h1
id="competence-title"
className="font-headline text-3xl font-extrabold tracking-tight text-on-surface md:text-4xl"
>
{name}
</h1>
<CarouselCompetences images={images} className="w-full h-64" />
<div className={contentClass || "bg-white/70 rounded-md p-4 mt-6 text-lg font-headline font-bold text-gray-700"}>
</div>
{images.length > 0 && (
<div className="mt-5">
<CarouselCompetences images={images} className="h-64 sm:h-80 md:h-96" />
</div>
)}
<div
ref={contentRef}
className="prose prose-sm mt-5 max-w-none font-body text-on-surface-variant sm:prose-base
prose-headings:font-headline prose-headings:text-primary
prose-p:font-body prose-p:text-on-surface-variant
prose-strong:text-on-surface
prose-a:text-primary prose-a:no-underline hover:prose-a:underline
prose-li:marker:text-primary
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"
>
<ReactMarkdown rehypePlugins={[rehypeRaw]}>{contentWithLinks}</ReactMarkdown>
</div>
{selectedMot && <ModalGlossaire mot={selectedMot} onClose={() => setSelectedMot(null)} />}
</section>
{isChatbotOpen && (
<div className="fixed bottom-10 right-10 p-4 w-96">
<ChatBot onClose={() => setIsChatbotOpen(false)} />
</div>
{selectedMot && (
<ModalGlossaire mot={selectedMot} onClose={() => setSelectedMot(null)} />
)}
</div>
);

View File

@ -11,9 +11,16 @@ interface ContentSectionProps {
contentClass?: string;
}
export default function ContentSectionCompetencesContainer({ collection, slug, titleClass, contentClass }: ContentSectionProps) {
console.log("🔍 [ContentSectionCompetencesContainer] Chargement des données...");
/**
* Orchestre le fetch compétence + glossaire et transmet les données au rendu.
* Le rendu (`ContentSectionCompetences`) gère son propre état "non trouvé" en
* feuillet vellum on s'aligne ici sur le même gabarit pour l'état de
* chargement (étape 7.c).
*/
export default function ContentSectionCompetencesContainer({
collection,
slug,
}: ContentSectionProps) {
const [competenceData, setCompetenceData] = useState(null);
const [glossaireData, setGlossaireData] = useState([]);
const [loading, setLoading] = useState(true);
@ -38,15 +45,24 @@ export default function ContentSectionCompetencesContainer({ collection, slug, t
}, [collection, slug]);
if (loading) {
return <div className="text-center text-gray-500"> Chargement des compétences...</div>;
return (
<div className="mx-auto flex w-full max-w-3xl flex-col gap-4 px-4 pb-10 sm:px-6">
<div className="rounded-sheet bg-surface-container-lowest/65 p-6 shadow-ambient-sm backdrop-blur-vellum">
<div className="h-4 w-24 animate-pulse rounded-full bg-surface-container-low/80" />
<div className="mt-3 h-8 w-3/4 animate-pulse rounded-full bg-surface-container-low/80" />
<div className="mt-5 aspect-[16/9] w-full animate-pulse rounded-tile bg-surface-container-low/80" />
<div className="mt-5 h-4 w-full animate-pulse rounded-full bg-surface-container-low/60" />
<div className="mt-2 h-4 w-5/6 animate-pulse rounded-full bg-surface-container-low/60" />
<div className="mt-2 h-4 w-4/6 animate-pulse rounded-full bg-surface-container-low/60" />
</div>
</div>
);
}
return (
<ContentSectionCompetences
competenceData={competenceData}
glossaireData={glossaireData}
titleClass={titleClass}
contentClass={contentClass}
/>
);
}

View File

@ -2,21 +2,41 @@
import { useEffect, useState } from "react";
/**
* Footer éditorial étape 8 "Digital Atelier" (voir docs-site-interne/REFONTE-VISUELLE.md §4).
*
* Avant : `bg-white/50 rounded-lg` + `text-gray-700`. Alignement partiel avec la refonte
* (tracking `visite n°`, font-headline) mais surface et radius hors charte.
*
* Après : carte vellum légère (`bg-surface-container-lowest/70`, `rounded-tile`,
* `backdrop-blur-vellum`) sans ombre ambient le footer ne doit pas flotter autant
* que les cartes de contenu. Trois lignes éditoriales :
* 1. Signature Manrope `text-primary` (identité)
* 2. Pitch Newsreader italic `text-on-surface-variant` (ton éditorial)
* 3. Compteur de visites `text-outline` (méta discrète)
*/
export default function Footer() {
const [visitCount, setVisitCount] = useState(0);
const [year, setYear] = useState(() => new Date().getFullYear());
useEffect(() => {
const visits = localStorage.getItem("visitCount");
const newVisitCount = visits ? parseInt(visits, 10) + 1 : 1;
localStorage.setItem("visitCount", newVisitCount.toString());
setVisitCount(newVisitCount);
setYear(new Date().getFullYear());
}, []);
return (
<footer className="min-h-[80px] w-full min-w-0 rounded-lg bg-white/50 backdrop-blur">
<div className="mx-auto flex max-w-4xl min-w-0 flex-col items-center gap-1 px-4 py-6 font-headline text-sm text-gray-700">
<p>&copy; {new Date().getFullYear()} Gras-Calvet Fernand</p>
<p className="text-[10px] uppercase tracking-[0.3em] text-outline">
<footer className="mx-auto w-full min-w-0 max-w-6xl px-4 pb-6 sm:px-6">
<div className="rounded-tile bg-surface-container-lowest/70 px-6 py-5 text-center backdrop-blur-vellum">
<p className="font-headline text-sm font-bold tracking-tight text-primary">
Fernand Gras-Calvet
</p>
<p className="mt-1 font-body text-xs italic leading-relaxed text-on-surface-variant">
Portfolio Étudiant 42 Perpignan · © {year}
</p>
<p className="mt-3 font-headline text-[10px] uppercase tracking-[0.3em] text-outline">
Visite n° {visitCount}
</p>
</div>

View File

@ -0,0 +1,76 @@
"use client";
import { useEffect, useState } from "react";
import ChatBot from "./ChatBot";
/**
* FAB "GrasBot" global (étape 7.e).
*
* Monté une seule fois dans `app/layout.tsx` pour que le chatbot soit accessible
* **depuis toutes les pages**, pas seulement depuis les fiches compétences comme
* avant la refonte.
*
* Flux d'ouverture :
* - Clic direct sur le bouton flottant ouvre le panneau.
* - Clic sur un mot-clé `.chatbot-keyword` dans une fiche compétence
* `ContentSectionCompetences` dispatche `window.dispatchEvent(new CustomEvent("grasbot:open"))`,
* ce FAB écoute et ouvre le panneau. Pas de Context partagé, pas de store global :
* un événement `window` suffit pour un besoin one-shot et découple proprement
* le ChatBot de ses points d'entrée.
*
* Positionnement :
* - Bouton : `fixed bottom-6 right-6`, z-index 30 (au-dessus du contenu, en
* dessous du header z-20 ? non : au-dessus, pour rester accessible. On
* passe à z-30).
* - Panneau : `fixed inset-x-4 bottom-24 sm:inset-auto sm:bottom-24 sm:right-6`
* plein largeur mobile (avec 16 px de marge), 384 px desktop.
*/
export default function GrasBotFab() {
const [isOpen, setIsOpen] = useState(false);
useEffect(() => {
const handleOpen = () => setIsOpen(true);
window.addEventListener("grasbot:open", handleOpen);
return () => window.removeEventListener("grasbot:open", handleOpen);
}, []);
useEffect(() => {
if (!isOpen) return;
const handleKey = (e: KeyboardEvent) => {
if (e.key === "Escape") setIsOpen(false);
};
window.addEventListener("keydown", handleKey);
return () => window.removeEventListener("keydown", handleKey);
}, [isOpen]);
return (
<>
<button
type="button"
onClick={() => setIsOpen((v) => !v)}
aria-label={isOpen ? "Fermer GrasBot" : "Ouvrir GrasBot"}
aria-expanded={isOpen}
className="fixed bottom-6 right-6 z-30 flex h-14 w-14 items-center justify-center rounded-full bg-primary text-white shadow-jewel transition-transform duration-200 hover:-translate-y-0.5 hover:bg-primary-container focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-fixed md:h-16 md:w-16"
>
<span
className="material-symbols-outlined text-3xl"
aria-hidden="true"
translate="no"
>
{isOpen ? "close" : "smart_toy"}
</span>
</button>
{isOpen && (
<div
className="fixed inset-x-4 bottom-24 z-30 h-[70vh] max-h-[560px] sm:inset-auto sm:bottom-24 sm:right-6 sm:h-[560px] sm:w-96"
role="dialog"
aria-modal="false"
aria-label="Conversation avec GrasBot"
>
<ChatBot onClose={() => setIsOpen(false)} />
</div>
)}
</>
);
}

View File

@ -68,7 +68,11 @@ export default function ModalGlossaire({ mot, onClose }: ModalGlossaireProps) {
onClick={onClose}
aria-label="Fermer la fenêtre du glossaire"
>
<span className="material-symbols-outlined" aria-hidden="true">
<span
className="material-symbols-outlined"
aria-hidden="true"
translate="no"
>
close
</span>
</button>

View File

@ -0,0 +1,68 @@
"use client";
import { Swiper, SwiperSlide } from "swiper/react";
import { Autoplay, Pagination } from "swiper/modules";
import "swiper/css";
import "swiper/css/pagination";
/**
* Carousel allégé réservé aux vignettes des listes (portfolio, compétences).
*
* Différences volontaires par rapport à `Carousel.tsx` / `CarouselCompetences.tsx`
* (qui restent utilisés dans les fiches détail) :
*
* - **Pas de flèches de navigation** : chaque vignette est enveloppée d'un
* `<Link>` qui capture le clic. Les flèches Swiper créaient une zone de clic
* ambiguë (on croit naviguer dans les images, on arrive sur la fiche détail).
* L'autoplay + le swipe tactile suffisent à l'échelle d'une vignette.
* - **Pas de lightbox** (pas de `createPortal`). La lightbox reste la signature
* de la fiche détail ; en vignette on ne propose que la navigation vers la fiche.
* - **Pagination Stitch** : bullets teintés `primary` via des variables CSS
* Swiper surchargées en inline, pour éviter de polluer `globals.css` avec un
* sélecteur global.
*
* Le composant couvre 100 % de son conteneur parent (qui fixe le `aspect-ratio`
* dans les pages liste), avec `object-cover` sur les images.
*/
interface VignetteCarouselProps {
images: Array<{ url: string; alt: string }>;
autoplayDelay?: number;
}
export default function VignetteCarousel({
images,
autoplayDelay = 3500,
}: VignetteCarouselProps) {
return (
<Swiper
modules={[Autoplay, Pagination]}
slidesPerView={1}
loop={images.length > 1}
autoplay={{ delay: autoplayDelay, disableOnInteraction: false }}
pagination={{ clickable: false }}
className="h-full w-full"
style={
{
// Surcharges Swiper : bullets primary Stitch, taille discrète.
"--swiper-pagination-color": "#26445d",
"--swiper-pagination-bullet-inactive-color": "#ffffff",
"--swiper-pagination-bullet-inactive-opacity": "0.55",
"--swiper-pagination-bullet-size": "6px",
"--swiper-pagination-bullet-horizontal-gap": "3px",
"--swiper-pagination-bottom": "8px",
} as React.CSSProperties
}
>
{images.map((img, index) => (
<SwiperSlide key={index} className="flex h-full items-center justify-center">
<img
src={img.url}
alt={img.alt}
className="h-full w-full object-cover"
loading="lazy"
/>
</SwiperSlide>
))}
</Swiper>
);
}

View File

@ -0,0 +1,8 @@
{
"folders": [
{
"path": "../.."
}
],
"settings": {}
}

View File

@ -1,34 +1,151 @@
import Link from "next/link";
import ContactForm from "../components/ContactForm";
import { getApiUrl } from "../utils/getApiUrl";
/**
* Page contact étape 8 "Digital Atelier" (voir docs-site-interne/REFONTE-VISUELLE.md §4).
*
* Gabarit aligné sur les autres pages de la refonte :
* - Colonne utile `max-w-3xl` (format lettre, plus intime que les listes `max-w-6xl`).
* - Hero vellum identique aux pages liste (kicker + titre Manrope + pitch Newsreader).
* - Tuiles "canaux" imbriquées (`rounded-tile bg-surface-container-low/80`) avec
* Material Symbols LinkedIn / Facebook / Email. LinkedIn et Facebook utilisent
* `link` et `public` (pas d'icône Material Symbols dédiée aux marques sociales,
* et on veut rester cohérent avec le reste du site en icon-font).
* - Carte vellum principale pour le formulaire (`rounded-sheet`, `shadow-ambient`,
* `backdrop-blur-vellum`) même empreinte visuelle que le hero home.
*/
const canaux = [
{
icon: "link",
label: "LinkedIn",
handle: "Fernand Gras-Calvet",
href: "https://www.linkedin.com/in/fernand-gras-calvet/",
external: true,
},
{
icon: "public",
label: "Facebook",
handle: "Fernand Gras-Calvet",
href: "https://www.facebook.com/fernand.grascalvet",
external: true,
},
{
icon: "alternate_email",
label: "Email",
handle: "grascalvet.fernand@gmail.com",
href: "mailto:grascalvet.fernand@gmail.com",
external: false,
},
];
export default function ContactPage() {
const apiUrl = getApiUrl();
return (
<div className="max-w-3xl mx-auto p-6 flex flex-col justify-top min-h-screen">
<h1 className="bg-white/50 rounded-md text-3xl font-headline font-extrabold tracking-tight text-center mb-6 border-b-4 border-blue-500 pb-2">
📬 Correspondance
<div className="mx-auto flex w-full min-w-0 max-w-3xl flex-col gap-5 px-4 pb-10 sm:px-6">
{/* Hero éditorial : kicker + titre + pitch. Gabarit identique aux listes. */}
<section
className="rounded-sheet bg-surface-container-lowest/85 p-5 shadow-ambient backdrop-blur-vellum sm:p-7 md:p-8"
aria-labelledby="contact-title"
>
<div className="flex flex-col gap-3 text-center md:text-left">
<span className="font-headline text-[11px] font-bold uppercase tracking-[0.3em] text-secondary">
Contact · Prendre la parole
</span>
<h1
id="contact-title"
className="font-headline text-3xl font-extrabold tracking-tight text-on-surface md:text-4xl"
>
Correspondance
</h1>
<p className="font-body text-base leading-relaxed text-on-surface-variant md:text-lg">
Pour un projet, une question ou une discussion autour de l&apos;IA,
du développement web ou de l&apos;École 42 voici les canaux
ouverts. Le formulaire ci-dessous arrive directement dans mon
back-office.
</p>
</div>
</section>
<p className="bg-white/70 rounded-md font-headline text-lg text-center border-b-4 border-blue-500 pb-2 mb-4">
Vous pouvez me contacter via ce formulaire ou sur mes réseaux sociaux.
</p>
{/* Canaux : 3 tuiles imbriquées, same-tier que les takeaways de la home. */}
<section
className="rounded-sheet bg-surface-container-lowest/85 p-5 shadow-ambient backdrop-blur-vellum sm:p-6"
aria-labelledby="contact-canaux"
>
<div className="mb-4">
<span className="font-headline text-[11px] font-bold uppercase tracking-[0.3em] text-secondary">
Canaux directs
</span>
<h2
id="contact-canaux"
className="mt-1 font-headline text-xl font-extrabold tracking-tight text-primary md:text-2xl"
>
Me joindre ailleurs
</h2>
</div>
<div className="bg-white/80 rounded-md flex flex-col items-center space-y-4 mb-6">
<p className="text-blue-500 font-headline font-bold">
LinkedIn: Fernand Gras-Calvet
</p>
<p className="text-blue-500 font-headline font-bold">
Facebook: Fernand Gras-Calvet
</p>
<p className="text-blue-500 font-headline font-bold">
Email: grascalvet.fernand@gmail.com
<ul className="grid gap-3 md:grid-cols-3">
{canaux.map((c) => (
<li key={c.label}>
<Link
href={c.href}
target={c.external ? "_blank" : undefined}
rel={c.external ? "noopener noreferrer" : undefined}
className="group flex h-full items-center gap-3 rounded-tile bg-surface-container-low/80 px-4 py-3 transition-colors hover:bg-surface-container/80 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary"
>
<span className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-primary text-on-primary">
<span
className="material-symbols-outlined"
aria-hidden="true"
translate="no"
>
{c.icon}
</span>
</span>
<span className="min-w-0 flex-1">
<span className="block font-headline text-[11px] font-bold uppercase tracking-[0.3em] text-secondary">
{c.label}
</span>
<span className="block truncate font-body text-sm text-on-surface">
{c.handle}
</span>
</span>
<span
className="material-symbols-outlined text-base text-primary transition-transform group-hover:translate-x-0.5"
aria-hidden="true"
translate="no"
>
arrow_forward
</span>
</Link>
</li>
))}
</ul>
</section>
{/* Formulaire : carte vellum principale, le form occupe l'intérieur. */}
<section
className="rounded-sheet bg-surface-container-lowest/85 p-5 shadow-ambient backdrop-blur-vellum sm:p-7 md:p-8"
aria-labelledby="contact-form-title"
>
<div className="mb-5">
<span className="font-headline text-[11px] font-bold uppercase tracking-[0.3em] text-secondary">
Formulaire
</span>
<h2
id="contact-form-title"
className="mt-1 font-headline text-xl font-extrabold tracking-tight text-primary md:text-2xl"
>
Écrire un message
</h2>
<p className="mt-1 font-body text-sm italic text-on-surface-variant">
Les trois champs sont obligatoires. Temps de réponse habituel : 48 h.
</p>
</div>
<div className="bg-white/50 p-6 rounded-lg border-b-4 border-blue-500 pb-2 shadow">
<ContactForm apiUrl={apiUrl} />
</div>
</section>
</div>
);
}

35
app/fonts.ts Normal file
View File

@ -0,0 +1,35 @@
import { Manrope, Newsreader } from "next/font/google";
/**
* Fonts du système "Digital Atelier" (voir docs-site-interne/REFONTE-VISUELLE.md).
*
* Chargées via `next/font/google` : Next télécharge les fichiers woff2 au build
* et les sert depuis le domaine du site (plus de dépendance fonts.googleapis.com,
* plus de problème de CDN ou de cache navigateur agressif).
*
* L'ancien chargement via `@import url(...)` dans `app/globals.css` était strippé
* par la chaîne PostCSS + Tailwind en production, les polices n'arrivaient jamais
* au navigateur (diagnostic 2026-04-22 : aucune requête `fonts.googleapis.com`
* visible dans l'onglet Network).
*
* Usage :
* 1. Importer `manrope` / `newsreader` dans le layout racine.
* 2. Poser leurs `variable` sur le `<html>` pour exposer `--font-manrope` /
* `--font-newsreader` à tout le sous-arbre.
* 3. `tailwind.config.ts` mappe `font-headline` / `font-body` vers ces variables.
*/
export const manrope = Manrope({
subsets: ["latin"],
weight: ["400", "500", "600", "700", "800"],
variable: "--font-manrope",
display: "swap",
});
export const newsreader = Newsreader({
subsets: ["latin"],
weight: ["400", "500", "600"],
style: ["normal", "italic"],
variable: "--font-newsreader",
display: "swap",
});

View File

@ -1,8 +1,14 @@
/* Polices Stitch "Digital Atelier" : Manrope (titres/UI) + Newsreader (corps éditorial).
Voir docs-site-interne/REFONTE-VISUELLE.md. Les anciennes classes font-orbitron-*
ont été retirées à l'étape 3 de la refonte (2026-04-22). */
@import url('https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&family=Newsreader:ital,wght@0,400;0,500;0,600;1,400;1,500&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@24,400,0,0&display=swap');
Voir docs-site-interne/REFONTE-VISUELLE.md.
--
Les `@import url(...)` vers Google Fonts étaient strippés en production par la
chaîne PostCSS + Tailwind de Next 15 (diagnostic confirmé 2026-04-22 via DevTools
Network sur Chrome desktop et mobile). Depuis :
- Manrope et Newsreader passent par `next/font/google` (voir `app/fonts.ts`)
avec les variables CSS `--font-manrope` et `--font-newsreader`.
- Material Symbols Outlined est chargée via `<link rel="stylesheet">` injecté
directement dans le `<head>` par `app/layout.tsx` (contourne le pipeline
PostCSS strippant, conserve le CDN Google pour une icon-font). */
@tailwind base;
@tailwind components;
@ -17,8 +23,12 @@
--foreground: #191c1d; /* on-surface Stitch : jamais #000 pur */
}
/* Icônes Material Symbols : utilitaire <span class="material-symbols-outlined">name</span>. */
/* Icônes Material Symbols : utilitaire <span class="material-symbols-outlined">name</span>.
Sans la règle font-family explicite, le span affiche littéralement le nom de l'icône
(ex. "psychology") dans la font par défaut l'import Google Fonts ne la pose pas
automatiquement sur la classe, c'est au site de le faire. */
.material-symbols-outlined {
font-family: 'Material Symbols Outlined';
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
display: inline-block;
line-height: 1;
@ -38,15 +48,42 @@ html {
}
body {
/* Couleur texte et fond fixés en clair, pas de dépendance au thème système. */
/* Couleur et fond fixés en clair, pas de dépendance au thème système.
Font par défaut : Newsreader (corps éditorial Stitch). Les éléments avec
`font-headline` passeront en Manrope, ceux sans classe explicite hériteront
de Newsreader au lieu d'Arial. */
color: #191c1d;
background: var(--background);
font-family: Arial, Helvetica, sans-serif;
font-family: var(--font-newsreader), Georgia, serif;
max-width: 100%;
min-width: 0;
overflow-x: hidden;
}
/* Keywords glossaire & chatbot dans les fiches compétences (étape 7.c).
Les spans sont injectés via `transformMarkdownWithKeywords` + `rehype-raw`.
Avant la refonte : styles inline `style="color:red/blue"` incohérent avec
la palette Stitch et intraçable. Désormais : deux classes utilitaires alignées
sur `primary` (#26445d), soulignement pointillé éditorial (underline dotted
offset 3px) pour signaler l'interactivité sans rompre le flux de lecture. */
.glossary-keyword,
.chatbot-keyword {
color: #26445d;
cursor: pointer;
font-weight: 600;
text-decoration: underline dotted;
text-underline-offset: 3px;
transition: color 0.2s ease;
}
.glossary-keyword:hover,
.chatbot-keyword:hover,
.glossary-keyword:focus-visible,
.chatbot-keyword:focus-visible {
color: #3e5c76;
outline: none;
}
@keyframes fade-in {
from {
opacity: 0;

View File

@ -5,6 +5,8 @@ import Footer from "./components/Footer";
import "./assets/main.css";
import "./globals.css";
import NavLink from "./components/NavLink";
import GrasBotFab from "./components/GrasBotFab";
import { manrope, newsreader } from "./fonts";
export default function RootLayout({ children }: { children: React.ReactNode }) {
const [isMenuOpen, setIsMenuOpen] = useState(false);
@ -72,12 +74,37 @@ export default function RootLayout({ children }: { children: React.ReactNode })
"text-on-surface-variant hover:text-primary transition-colors";
return (
<html lang="fr">
<html lang="fr" className={`${manrope.variable} ${newsreader.variable}`}>
<head>
{/* Material Symbols : chargés via <link> plutôt que via @import CSS qui est
strippé par la chaîne PostCSS + Tailwind de Next 15 (diagnostic 2026-04-22).
Ressource critique côté UI preload pour éviter un flash de texte brut
sur les icônes des CTAs, du burger et des cartes de la home. */}
<link
rel="preconnect"
href="https://fonts.googleapis.com"
/>
<link
rel="preconnect"
href="https://fonts.gstatic.com"
crossOrigin=""
/>
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&display=swap"
/>
</head>
<body className="min-w-0 overflow-x-hidden antialiased">
<div className="relative grid min-h-[100dvh] w-full min-w-0 grid-rows-[auto_1fr_auto]">
{/* Wallpaper plein écran (fondation, ne change pas avec la refonte). */}
<div className="absolute inset-0 bg-wallpaper"></div>
{/* Wallpaper plein écran (fondation).
`fixed inset-0` plutôt qu'`absolute` dans le grid : le wallpaper est
désormais calé sur le viewport, pas sur la hauteur totale de la page.
Sans ça, sur les pages longues (portfolio, compétences, fiches) le
conteneur grid atteignait 2-3 viewports de haut et `background-size: cover`
zoomait l'image pour couvrir cette hauteur — d' le rendu incohérent
entre home (courte) et listes (longues). Corrigé le 2026-04-22. */}
<div className="fixed inset-0 z-0 bg-wallpaper pointer-events-none" aria-hidden="true"></div>
<div className="relative grid min-h-[100dvh] w-full min-w-0 grid-rows-[auto_1fr_auto]">
{/* Cercles animés : repalette en ton indigo-ardoise (Stitch "Digital Atelier"). */}
<div className="absolute z-0 inset-0 overflow-hidden pointer-events-none">
<div className="circle-one blur-3xl w-40 md:w-64 h-40 md:h-64 rounded-full bg-primary/40 top-0 right-10 md:right-28 absolute"></div>
@ -87,7 +114,10 @@ export default function RootLayout({ children }: { children: React.ReactNode })
{/* Header "No-Line" : pas de bordure pleine, juste un shift tonal + ombre ambient diffuse. */}
<header className="fixed left-0 top-0 z-20 h-16 w-full min-w-0 bg-surface/80 px-4 py-2 shadow-ambient-sm backdrop-blur-vellum md:h-16 md:px-6">
<div className="mx-auto flex max-w-4xl min-w-0 items-center justify-between gap-2">
<h2 className="min-w-0 truncate pr-1 text-xl font-headline font-extrabold italic tracking-tight text-primary md:text-2xl">
<h2
className="min-w-0 truncate pr-1 text-xl font-headline font-extrabold italic tracking-tight text-primary md:text-2xl"
translate="no"
>
Portfolio Gras-Calvet Fernand
</h2>
@ -101,7 +131,11 @@ export default function RootLayout({ children }: { children: React.ReactNode })
aria-expanded={isMenuOpen}
aria-controls="mobile-drawer"
>
<span className="material-symbols-outlined" aria-hidden="true">
<span
className="material-symbols-outlined"
aria-hidden="true"
translate="no"
>
{isMenuOpen ? "close" : "menu"}
</span>
</button>
@ -211,6 +245,11 @@ export default function RootLayout({ children }: { children: React.ReactNode })
<Footer />
</div>
</div>
{/* GrasBot : FAB global Stitch (étape 7.e). Accessible depuis toutes les
pages, écoute aussi `CustomEvent("grasbot:open")` dispatché depuis
les fiches compétences quand l'utilisateur clique sur « IA locale ». */}
<GrasBotFab />
</body>
</html>
);

View File

@ -1,5 +1,6 @@
"use client";
import Link from "next/link";
import React, { useEffect, useState } from "react";
import ReactMarkdown from "react-markdown";
import "./assets/main.css";
@ -8,18 +9,17 @@ import { getApiUrl } from "./utils/getApiUrl";
async function getHomepageData() {
const apiUrl = getApiUrl();
// Configuration avec timeout et retry
const fetchWithTimeout = async (url: string, options: RequestInit = {}) => {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 10000); // 10 secondes timeout
const timeoutId = setTimeout(() => controller.abort(), 10000);
try {
const response = await fetch(url, {
...options,
signal: controller.signal,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
"Content-Type": "application/json",
Accept: "application/json",
...options.headers,
},
});
@ -31,12 +31,15 @@ async function getHomepageData() {
}
};
// Tentative avec retry
for (let attempt = 1; attempt <= 3; attempt++) {
try {
console.log(`🔄 [getHomepageData] Tentative ${attempt}/3 - URL: ${apiUrl}/api/homepages?populate=*`);
console.log(
`🔄 [getHomepageData] Tentative ${attempt}/3 - URL: ${apiUrl}/api/homepages?populate=*`
);
const response = await fetchWithTimeout(`${apiUrl}/api/homepages?populate=*`);
const response = await fetchWithTimeout(
`${apiUrl}/api/homepages?populate=*`
);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
@ -45,24 +48,44 @@ async function getHomepageData() {
const data = await response.json();
console.log("✅ [getHomepageData] Données récupérées avec succès");
return data.data?.[0] ?? null;
} catch (error) {
console.error(`❌ [getHomepageData] Erreur tentative ${attempt}:`, error);
if (attempt === 3) {
// Dernière tentative échouée
console.error("🚨 [getHomepageData] Toutes les tentatives ont échoué");
return null;
}
// Attendre avant la prochaine tentative
await new Promise(resolve => setTimeout(resolve, 1000 * attempt));
await new Promise((resolve) => setTimeout(resolve, 1000 * attempt));
}
}
return null;
}
/**
* Trois axes éditoriaux de la home. Contenu hardcodé pour l'instant : simple
* et modifiable ici. À porter vers un content-type Strapi dédié plus tard si
* on veut l'éditer sans déploiement.
*/
const takeaways = [
{
icon: "psychology",
title: "Intelligence artificielle",
body: "Intégration d'IA locale et d'assistants conversationnels en environnement souverain.",
},
{
icon: "terminal",
title: "Développement web",
body: "Next.js, Strapi, FastAPI — stack moderne de bout en bout, du CMS à la diffusion.",
},
{
icon: "school",
title: "École 42",
body: "Formation par projets, pédagogie par les pairs, progression autodidacte continue.",
},
];
export default function HomePage() {
const [homepage, setHomepage] = useState<any>(null);
const apiUrl = getApiUrl();
@ -71,29 +94,170 @@ export default function HomePage() {
getHomepageData().then((data) => setHomepage(data));
}, []);
if (!homepage) return <p className="text-center text-blue-500">Chargement de la page...</p>;
if (!homepage) {
return (
<div className="mx-auto flex min-h-[40vh] w-full max-w-md items-center justify-center px-4">
<p className="font-body italic text-secondary">
Chargement de la page
</p>
</div>
);
}
const title = homepage.title ?? "Titre par défaut";
const cv = homepage.cv ?? "";
const cv: string = homepage.cv ?? "";
const imageUrl = homepage.photo?.url ? `${apiUrl}${homepage.photo.url}` : null;
return (
<main className="mx-auto mb-3 flex w-full min-w-0 max-w-full flex-col items-center justify-center rounded-lg bg-white/55 p-4 sm:max-w-2xl sm:p-6 md:max-w-3xl lg:max-w-4xl xl:max-w-5xl">
<h1 className="text-3xl font-headline font-extrabold italic tracking-tight text-gray-800 mb-4">{title}</h1>
<div className="mx-auto flex w-full min-w-0 max-w-5xl flex-col gap-3 px-4 pb-10 sm:px-6">
{/* Hero "feuillet de vellum" : carte principale à 85 % sur le wallpaper. */}
<section
className="rounded-sheet bg-surface-container-lowest/85 p-5 shadow-ambient backdrop-blur-vellum sm:p-7 md:p-8"
aria-labelledby="home-title"
>
<div className="grid gap-5 md:grid-cols-[auto_1fr] md:items-center md:gap-8">
{/* Portrait avec frame primary (1 px d'air), remplace le cercle historique. */}
<div className="mx-auto md:mx-0">
{imageUrl ? (
<div className="relative w-64 h-64 rounded-full overflow-hidden shadow-lg border-4 border-gray-300 transition-transform duration-300 hover:scale-110 hover:rotate-3">
<img src={imageUrl} alt="Photo de profil" className="w-full h-full object-cover object-center" />
<div className="rounded-sheet bg-primary p-1 shadow-ambient-sm">
<div className="overflow-hidden rounded-[1.25rem]">
<img
src={imageUrl}
alt={`Portrait de ${title}`}
className="h-48 w-48 object-cover object-center sm:h-56 sm:w-56 md:h-64 md:w-64"
/>
</div>
</div>
) : (
<div className="w-64 h-64 flex items-center justify-center bg-gray-500 text-gray-200 rounded-full shadow-md">
<div className="flex h-48 w-48 items-center justify-center rounded-sheet bg-surface-container text-sm text-on-surface-variant sm:h-56 sm:w-56 md:h-64 md:w-64">
Image indisponible
</div>
)}
</div>
<div className="mt-6 w-full min-w-0 max-w-2xl px-4 text-center text-lg font-headline font-bold text-gray-700 sm:px-6">
<div className="flex flex-col gap-3 text-center md:text-left">
<span className="font-headline text-[11px] font-bold uppercase tracking-[0.3em] text-secondary">
Portfolio · Étudiant 42 Perpignan
</span>
<h1
id="home-title"
className="font-headline text-3xl font-extrabold tracking-tight text-on-surface md:text-4xl lg:text-5xl"
>
{title}
</h1>
{cv && (
<div
className="prose prose-sm max-w-none font-body text-on-surface-variant sm:prose-base
prose-headings:font-headline prose-headings:text-primary
prose-p:font-body prose-p:text-on-surface-variant
prose-strong:text-on-surface
prose-a:text-primary prose-a:no-underline hover:prose-a:underline
prose-li:marker:text-primary
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"
>
<ReactMarkdown>{cv}</ReactMarkdown>
</div>
</main>
)}
<div className="mt-2 flex flex-col items-stretch gap-3 sm:flex-row sm:justify-center sm:items-center md:justify-start">
<Link
href="/portfolio"
className="inline-flex items-center justify-center gap-2 rounded-tile bg-primary px-6 py-3 font-headline text-sm font-bold uppercase tracking-widest text-on-primary shadow-jewel transition-transform hover:-translate-y-0.5 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-fixed"
>
Voir mes projets
<span
className="material-symbols-outlined text-base"
aria-hidden="true"
translate="no"
>
arrow_forward
</span>
</Link>
<Link
href="/contact"
className="inline-flex items-center justify-center gap-2 rounded-tile px-6 py-3 font-headline text-sm font-bold uppercase tracking-widest text-primary transition-colors hover:bg-primary-fixed/40 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary"
>
Me contacter
<span
className="material-symbols-outlined text-base"
aria-hidden="true"
translate="no"
>
mail
</span>
</Link>
</div>
</div>
</div>
</section>
{/* Trois axes : cartes éditoriales, grille 3 colonnes desktop, stack mobile. */}
<section
className="rounded-sheet bg-surface-container-lowest/85 p-5 shadow-ambient backdrop-blur-vellum sm:p-6"
aria-labelledby="home-axes"
>
<div className="mb-4 text-center md:text-left">
<span className="font-headline text-[11px] font-bold uppercase tracking-[0.3em] text-secondary">
Ce qui m&apos;anime
</span>
<h2
id="home-axes"
className="mt-1 font-headline text-2xl font-extrabold tracking-tight text-primary md:text-3xl"
>
Trois axes de travail
</h2>
</div>
<div className="grid gap-3 md:grid-cols-3">
{takeaways.map((item) => (
<article
key={item.title}
className="rounded-tile bg-surface-container-low/80 p-5 transition-colors hover:bg-surface-container/80"
>
<div className="mb-3 flex h-10 w-10 items-center justify-center rounded-full bg-primary text-on-primary">
<span
className="material-symbols-outlined"
aria-hidden="true"
translate="no"
>
{item.icon}
</span>
</div>
<h3 className="mb-1 font-headline text-lg font-bold text-primary">
{item.title}
</h3>
<p className="font-body text-sm leading-relaxed text-on-surface-variant">
{item.body}
</p>
</article>
))}
</div>
</section>
{/* Pull-quote "Démarche" : carte vellum légère (opacité 65 %, sans ombre,
radius tile) pour rester lisible sur wallpaper sans écraser la variation
éditoriale voulue par DESIGN.md §5. Barre gauche primaire conservée. */}
<section
className="rounded-tile bg-surface-container-lowest/65 p-5 backdrop-blur-vellum sm:p-6"
aria-labelledby="home-demarche"
>
<blockquote className="border-l-4 border-primary pl-5 md:pl-8">
<span
id="home-demarche"
className="mb-2 block font-headline text-[11px] font-bold uppercase tracking-[0.3em] text-primary"
>
Démarche
</span>
<p className="font-body text-lg italic leading-snug text-on-surface md:text-2xl">
« Apprendre à construire, puis construire pour apprendre chaque
projet est une nouvelle pièce du métier. »
</p>
<cite className="mt-3 block font-headline text-sm font-bold not-italic text-secondary">
Fernand Gras-Calvet
</cite>
</blockquote>
</section>
</div>
);
}

View File

@ -3,76 +3,180 @@
import { useEffect, useState } from "react";
import Link from "next/link";
import { getApiUrl } from "../utils/getApiUrl";
import Carousel from "../components/Carousel";
import VignetteCarousel from "../components/VignetteCarousel";
import "../assets/main.css";
import "../globals.css";
/**
* Liste des projets refonte "Digital Atelier" (étape 6).
*
* Règle DESIGN.md §6 "No-Grid-Lock" : on bannit la grille 3 colonnes symétrique.
* On utilise une grille asymétrique 2/3 + 1/3 alternée par paires, qui crée un
* rythme éditorial plutôt qu'un catalogue. Arbitrage acté dans REFONTE-VISUELLE.md §2 :
* le carousel reste réservé aux fiches détail (étape 7) la liste n'affiche que
* la première image, ce qui allège le rendu et clarifie la hiérarchie.
*
* Pattern de spans (modulo 4 sur desktop 6 colonnes) :
* idx 0 col-span-4 (vedette)
* idx 1 col-span-2
* idx 2 col-span-2
* idx 3 col-span-4
* répète l'alternance pour éviter la monotonie sans dépendre du nombre d'items.
*/
const spanPattern = ["md:col-span-4", "md:col-span-2", "md:col-span-2", "md:col-span-4"];
export default function Page() {
const [projects, setProjects] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const apiUrl = getApiUrl();
useEffect(() => {
async function fetchProjects() {
try {
const response = await fetch(`${apiUrl}/api/projects?populate=picture&sort=order:asc`); // Récupération triée depuis Strapi
const response = await fetch(
`${apiUrl}/api/projects?populate=picture&sort=order:asc`
);
if (!response.ok) {
throw new Error(`Erreur de récupération des projets : ${response.statusText}`);
}
const data = await response.json();
// Tri des projets côté Next.js (au cas où Strapi ne le fait pas)
const sortedProjects = (data.data ?? []).sort((a, b) => (a.order || 999) - (b.order || 999));
const sortedProjects = (data.data ?? []).sort(
(a, b) => (a.order || 999) - (b.order || 999)
);
setProjects(sortedProjects);
} catch (error) {
console.error("❌ Erreur lors de la récupération des projets :", error);
} finally {
setIsLoading(false);
}
}
fetchProjects();
}, [apiUrl]);
return (
<main className="w-full p-3 mt-5 mb-5">
<div className="grid grid-cols-1 sm:grid-cols-[repeat(auto-fit,minmax(300px,1fr))] gap-7 max-w-7xl mx-auto mobile-landscape">
{projects.map((project) => {
<div className="mx-auto flex w-full min-w-0 max-w-6xl flex-col gap-5 px-4 pb-10 sm:px-6">
{/* En-tête éditorial, aligné sur le hero de la home (kicker + titre Manrope). */}
<section
className="rounded-sheet bg-surface-container-lowest/85 p-5 shadow-ambient backdrop-blur-vellum sm:p-7 md:p-8"
aria-labelledby="portfolio-title"
>
<div className="flex flex-col gap-3 text-center md:text-left">
<span className="font-headline text-[11px] font-bold uppercase tracking-[0.3em] text-secondary">
Portfolio · Projets
</span>
<h1
id="portfolio-title"
className="font-headline text-3xl font-extrabold tracking-tight text-on-surface md:text-4xl lg:text-5xl"
>
Les projets qui mont construit
</h1>
<p className="font-body text-on-surface-variant sm:text-lg">
Une sélection de réalisations pédagogiques, personnelles et professionnelles
cliquez sur une carte pour en découvrir la genèse, les choix techniques et les
visuels.
</p>
</div>
</section>
{/* État de chargement : 4 squelettes qui respectent la grille asymétrique. */}
{isLoading ? (
<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 className="mt-1.5 h-4 w-5/6 animate-pulse rounded-full bg-surface-container-low/60" />
</div>
))}
</div>
) : projects.length === 0 ? (
<section className="rounded-sheet bg-surface-container-lowest/75 p-8 text-center shadow-ambient-sm backdrop-blur-vellum">
<span
className="material-symbols-outlined mb-3 text-4xl text-primary"
aria-hidden="true"
translate="no"
>
inbox
</span>
<p className="font-body italic text-on-surface-variant">
Aucun projet à afficher pour le moment.
</p>
</section>
) : (
<div className="grid grid-cols-1 gap-5 sm:grid-cols-2 md:grid-cols-6">
{projects.map((project, idx) => {
const pictures = project.picture ?? [];
const images = pictures.map((img) => ({
url: `${apiUrl}${img.url}`,
alt: img.name || "Project image",
alt: img.name || `Visuel du projet ${project.name}`,
}));
const firstImage = images[0];
return (
<div
<Link
key={project.id}
className="bg-white/80 rounded-lg shadow-md overflow-hidden w-80 h-96 flex flex-col transform transition-all duration-300 hover:scale-105 hover:shadow-xl p-4"
href={`/portfolio/${project.slug}`}
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`}
>
<Link href={`/portfolio/${project.slug}`}>
<div className="overflow-hidden w-full h-48 mb-4">
<div className="relative aspect-[4/3] w-full overflow-hidden bg-surface-container-low">
{images.length > 1 ? (
<Carousel images={images} className="h-48" />
) : (
<VignetteCarousel images={images} />
) : firstImage ? (
<img
src={images[0]?.url || "/placeholder.jpg"}
alt={images[0]?.alt || "Project image"}
className="w-full h-full object-cover"
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-grow overflow-y-auto max-h-32 hide-scrollbar show-scrollbar">
<p className="font-headline font-bold text-xl mb-2">{project.name}</p>
<p className="text-gray-700 text-sm font-headline hover:text-base transition-all duration-200 ease-in-out">
<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">
Projet
</span>
<h2 className="font-headline text-xl font-extrabold leading-tight tracking-tight text-primary">
{project.name}
</h2>
{project.description && (
<p className="font-body text-sm leading-relaxed text-on-surface-variant line-clamp-3 sm:text-base">
{project.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>
</main>
)}
</div>
);
}

View File

@ -1,5 +1,51 @@
/**
* Appelle l'API GrasBot via le proxy Next (/api/proxy).
*
* Historique :
* - v1 : retournait juste `data.response` (string). Compatibilité ascendante
* assurée côté API (le champ `response` existe toujours).
* - v3 (2026-04-22) : retourne maintenant l'objet complet pour que `ChatBot.js`
* puisse afficher les sources citées, le badge `grounded`, etc. Ajoute un
* timeout (45 s) via `AbortController` pour éviter les spinners infinis.
*
* @param {string} question
* @returns {Promise<{
* response: string,
* sources?: Array<{slug: string, title: string, type: string, score: number, url?: string}>,
* grounded?: boolean,
* model?: string,
* vault_size?: number,
* }>}
*/
export async function askAI(question) {
const response = await fetch(`/api/proxy?q=${encodeURIComponent(question)}`);
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 45_000);
try {
const response = await fetch(`/api/proxy?q=${encodeURIComponent(question)}`, {
signal: controller.signal,
});
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
return data.response;
// Rétrocompatibilité : même si l'API ne renvoyait que `response`, on
// retourne l'objet tel quel.
return data;
} catch (error) {
clearTimeout(timeoutId);
if (error.name === "AbortError") {
return {
response:
"La réponse a mis trop de temps. Le modèle est peut-être occupé, réessayez dans un instant.",
sources: [],
grounded: false,
_timeout: true,
};
}
throw error;
}
}

View File

@ -1,34 +1,145 @@
# API LLM et chatbot (GrasBot)
**Dernière mise à jour :** 2026-04-01
**Dernière mise à jour :** 2026-04-22 (v3 — bascule graph + BM25)
## 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. `app/components/ChatBot.js` appelle `askAI(question)` (`app/utils/askAI.js`).
2. `askAI` envoie un **GET** vers **`/api/proxy?q=...`** (route Next.js App Router).
3. `app/api/proxy/route.js` appelle en dur **`https://llmapi.fernandgrascalvet.com/ask?q=...`** et renvoie le corps JSON tel quel.
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.
**Conséquence :** en développement local, le chatbot s'appuie sur l'**API LLM déployée** sur ce domaine, pas sur `uvicorn` local (`http://localhost:8000`) tant que l'URL n'est pas rendue configurable.
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)).
## Réponse attendue par le front
## FastAPI — `llm-api/`
- Dans `askAI.js`, le texte affiché est lu via **`data.response`**. Le flux Ollama (`/api/generate`) renvoie en général un champ `response` dans le JSON ; si l'API distante ou le format change, vérifier ce mapping.
| 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`. **Plus besoin** de `chromadb` / `chroma-hnswlib` (supprimés v3). |
## FastAPI local (`llm-api/api.py`)
Modules supprimés en v3 :
- Endpoint **GET** `/ask`, paramètre query **`q`**.
- Requête **POST** vers Ollama : `http://localhost:11434/api/generate` avec JSON `model: "mistral"`, `prompt`, `stream: false`.
- Dépendances : voir `CONFIGURATION_SITE.md` (FastAPI, uvicorn, requests).
- `rag.py` → remplacé par `search.py`.
- `index_vault.py` → plus d'étape d'indexation (lecture directe du vault).
## Fichiers clés
## Modèle Ollama
```
app/utils/askAI.js
app/api/proxy/route.js
app/components/ChatBot.js
llm-api/api.py
| 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
```
## Piste d'évolution
Plus besoin d'étape d'indexation : l'API lit le vault au démarrage.
- Variable d'environnement (ex. URL LLM côté serveur) pour pointer vers `http://localhost:8000` en dev et vers la prod en déploiement, au lieu d'une URL figée dans `route.js`.
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.

View File

@ -1,26 +1,91 @@
# Outils `strapi_extraction/`
**Dernière mise à jour :** 2026-04-01
**Dernière mise à jour :** 2026-04-22
Dossier de **scripts Node** pour extraire, nettoyer et documenter les données issues de lAPI Strapi (hors runtime du site).
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).
## Scripts repérés
## Pipeline complet
| Fichier | Rôle probable |
|---------|----------------|
| `extract-api-data.js` | Extraction brute vers JSON (`extract/raw/`). |
| `clean-api-data.js` | Nettoyage / normalisation (`extract/clean-data/`). |
| `generate-docs.js` | Génération de documentation à partir des données. |
| `update-documentation.js` | Mise à jour de la doc générée. |
| `analyse-site-architecture.js` | Analyse darchitecture du site. |
```
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)
```
## Données générées (exemples)
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.
- `extract/raw/*.json`, `extract/clean-data/*.json`
- `logs/last-update-summary.json`, `docs/generation-summary.json`
## Scripts
Ces fichiers peuvent être **régénérés** ; ne pas les considérer comme source de vérité sans comparer au CMS.
| 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. |
## Usage
## Commande type
Lancer depuis la racine ou le dossier selon les chemins dans chaque script (à vérifier avant exécution). Détails : lire len-tête de chaque fichier `.js`.
```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)

View File

@ -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.

View File

@ -24,9 +24,11 @@ Ce dossier décrit l'architecture, le fonctionnement et les décisions du projet
| [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. |
## Arborescence utile

View File

@ -1,7 +1,7 @@
# Refonte visuelle — Direction "Digital Atelier"
**Créé :** 2026-04-22
**Statut :** en cours (étapes 1-4/8 terminées)
**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).
@ -58,10 +58,10 @@ Chaque étape = un lot cohérent + éventuelle mise à jour de `captures/AUDIT-V
| 2 | Garde-fou doc + mise à jour feuille de route | `docs-site-interne/REFONTE-VISUELLE.md`, `docs-site-interne/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` | à faire |
| 6 | Listes portfolio + compétences : grille asymétrique, cartes éditoriales | `app/portfolio/page.jsx`, `app/competences/page.jsx`, composants `Carousel*` | à faire |
| 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` | à faire |
| 8 | Contact + Footer éditorial | `app/contact/page.js`, `app/components/ContactForm.tsx`, `app/components/Footer.jsx` | à faire |
| 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
@ -102,6 +102,265 @@ Après l'étape 4, retour utilisateur sur Samsung S25 Ultra : les mots-clés du
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).
## 5. Checklist relecture (à passer à la fin de chaque étape)
- [ ] Aucune colonne unique globale `max-w-xl` (c'est le format newsletter).
@ -112,3 +371,4 @@ Ce correctif concerne uniquement le composant `ModalGlossaire`. L'étape 7 repre
- [ ] 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).

View File

@ -100,9 +100,9 @@ Les captures suivantes **nont pas révélé de problème spécifique** après
| 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` | `OK` |
| 18 | Contact formulaire mobile | `/contact` | `18-contact-formulaire-mobile.webp` | `OK` |
| 19 | Footer desktop | `/` | `19-layout-footer-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` | `OK` |
@ -110,4 +110,6 @@ Les captures suivantes **nont pas révélé de problème spécifique** après
## Suite
Passage à la **refonte visuelle globale** : direction artistique (palette, typographie, rythme vertical), hiérarchie des pages, traitement des cartes portfolio / compétences, header et footer. À cadrer avec lutilisateur avant toute modification.
- **Étape 8 Digital Atelier bouclée** (2026-04-22) : contact + formulaire + footer migrés à la charte Stitch (voir `docs-site-interne/REFONTE-VISUELLE.md §8`).
- Prendre de **nouvelles captures** 17 / 18 / 19 pour figer le rendu post-refonte (remplacement des WebP existants dans `docs-site-interne/captures/`).
- Dette technique identifiée pour une future passe : fusion `Carousel.tsx` / `CarouselCompetences.tsx` (doublons), persistance serveur du compteur de visites, validation anti-spam du formulaire.

View File

@ -1,15 +1,16 @@
# État actuel du site
**Dernière mise à jour :** 2026-04-01
**Dernière mise à jour :** 2026-04-22 (post-refonte GrasBot v3)
## Ce qui est en place
- **Next.js 15** avec App Router, Tailwind, pages accueil / portfolio / compétences / contact, layout responsive avec menu burger.
- **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.
- **Formulaire contact** : POST vers Strapi `messages`.
- **Chatbot GrasBot** : proxy Next vers API LLM hébergée (`llmapi.fernandgrascalvet.com`).
- **FastAPI + Ollama** dans le dépôt pour usage local ou serveur ; modèle `mistral` dans `llm-api/api.py`.
- **Scripts** dextraction et de doc dans `strapi_extraction/`.
- **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/`** : 41 notes enrichies (aliases, answers, priority) — source de vérité du chatbot, régénéré depuis Strapi par `strapi_extraction/build-vault.py`.
- **Scripts** d'extraction et de doc dans `strapi_extraction/`.
- Documentation opérationnelle : `CONFIGURATION_SITE.md`.
- **Captures d'écran** de référence (WebP) : `docs-site-interne/captures/` — voir `captures/INDEX.md`.

View File

@ -8,10 +8,11 @@ Document vivant : ajuster les statuts et dates au fil du travail.
| ID | Sujet | Statut | Notes |
|----|--------|--------|--------|
| R1 | Moderniser lUI (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-4 (tokens + garde-fou + migration typo globale + layout racine) faites le 2026-04-22. |
| R1 | Moderniser lUI (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
@ -42,3 +43,17 @@ Document vivant : ajuster les statuts et dates au fil du travail.
| 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-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`). |

View File

@ -1,16 +1,81 @@
from fastapi import FastAPI
import requests
"""API FastAPI de GrasBot — orchestre le retrieval et Ollama.
app = FastAPI()
Historique :
- 2026-04-01 : version initiale minimaliste (GET /ask Ollama `mistral` sans contexte).
- 2026-04-22 (matin) : refonte RAG (ChromaDB + nomic-embed-text + Qwen3:8b).
- 2026-04-22 (soir) : bascule vers un pipeline **graph + BM25** sans embeddings.
* Vault `vault-grasbot/` lu directement (frontmatter YAML + wikilinks).
* Retrieval déterministe (alias / answers / domains / tags / BM25).
* Expansion par graphe (linked / related / wikilinks du body).
* Plus de dépendance ChromaDB ni hnswlib ni nomic-embed-text.
* Module `rag.py` / `index_vault.py` supprimés.
Voir `docs-site-interne/08-vault-obsidian-retrieval.md` pour l'architecture.
"""
from __future__ import annotations
from fastapi import FastAPI, HTTPException
from search import (
LLM_MODEL,
MIN_SCORE,
OLLAMA_URL,
TOP_K,
VAULT_DIR,
answer,
load_vault,
reload_vault,
)
app = FastAPI(title="GrasBot LLM API", version="3.0.0")
OLLAMA_API_URL = "http://localhost:11434/api/generate"
@app.get("/ask")
async def ask_question(q: str):
data = {
"model": "mistral",
"prompt": q,
"stream": False
"""Endpoint historique consommé par `app/utils/askAI.js`.
Le front lit `data.response` : on conserve cette clé pour la compatibilité.
Les champs `sources` / `grounded` / `model` sont des ajouts non destructifs
utilisés par `ChatBot.js` pour afficher les sources cliquables.
"""
if not q or not q.strip():
raise HTTPException(status_code=400, detail="Paramètre `q` manquant ou vide.")
try:
return answer(q)
except Exception as exc:
print(f"❌ /ask failed ({exc})")
raise HTTPException(status_code=502, detail=str(exc))
@app.get("/health")
async def health():
"""Configuration active + stats du vault — utile pour debug / monitoring."""
vault = load_vault()
# Stats rapides pour vérifier que le vault est bien chargé
by_type: dict[str, int] = {}
for n in vault.values():
by_type[n.type] = by_type.get(n.type, 0) + 1
return {
"status": "ok",
"ollama_url": OLLAMA_URL,
"llm_model": LLM_MODEL,
"vault": {
"path": VAULT_DIR.as_posix(),
"notes_total": len(vault),
"notes_by_type": by_type,
},
"search": {
"top_k": TOP_K,
"min_score": MIN_SCORE,
},
}
response = requests.post(OLLAMA_API_URL, json=data)
return response.json()
@app.post("/reload-vault")
async def reload_vault_endpoint():
"""Force la relecture du vault sans redémarrer l'API (utile après édition)."""
vault = reload_vault()
return {"status": "ok", "notes_total": len(vault)}

13
llm-api/requirements.txt Normal file
View File

@ -0,0 +1,13 @@
# Dépendances Python pour l'API LLM du site (GrasBot v3).
# Installer : pip install -r requirements.txt
#
# Historique :
# - v1 : fastapi + requests (Mistral 7B sans contexte).
# - v2 : ajout chromadb + pyyaml (RAG vectoriel avec nomic-embed-text).
# - v3 : retour à un pipeline graph + BM25, 100 % pure Python, pas de
# compilation C++, pas d'embeddings (lecture directe de vault-grasbot/).
fastapi>=0.110
uvicorn[standard]>=0.27
requests>=2.31
pyyaml>=6.0

637
llm-api/search.py Normal file
View File

@ -0,0 +1,637 @@
"""Pipeline de recherche GrasBot — graph + BM25, sans embeddings (2026-04-22).
Remplace l'ancien `rag.py` (ChromaDB + embeddings Ollama). Rationnel :
- Vault de taille modeste (~36 notes, ~50-100 Ko) : la recherche sémantique
vectorielle est sur-dimensionnée et imprévisible sur vocabulaire précis.
- Le vault est déjà **structuré** (frontmatter YAML, wikilinks, MOCs) : on
exploite directement cette structure comme un graphe de connaissance.
- Résultat : retrieval **déterministe**, **traçable**, instantané (~50 ms),
sans Chroma, sans compilation C++, sans `nomic-embed-text` chargé en VRAM.
Pipeline :
1. `load_vault()` : parcours récursif de `vault-grasbot/`, parsing YAML +
body, extraction des wikilinks. Mémoïsé (chargé une fois par process).
2. `search(query, top_k)` : score chaque note (alias/title/slug/answers/
domains/tags + BM25 sur le body), expansion par graphe (voisins via
`linked`, `related`, wikilinks du body), dédoublonnage, top_k.
3. `build_prompt(query, notes)` : assemble (system, user) avec notes entières.
4. `generate(system, user)` : appel Ollama `/api/chat` (Qwen3 par défaut).
5. `answer(query)` : façade haut-niveau consommée par `api.py`.
Variables d'environnement (toutes optionnelles) :
- `OLLAMA_URL` (default: http://localhost:11434)
- `LLM_MODEL` (default: qwen3:8b)
- `VAULT_DIR` (default: <repo_root>/vault-grasbot)
- `SEARCH_TOP_K` (default: 5)
- `SEARCH_MIN_SCORE` (default: 1.0) seuil en-dessous duquel on considère
qu'aucune note pertinente n'a été trouvée.
"""
from __future__ import annotations
import math
import os
import re
from dataclasses import dataclass, field
from functools import lru_cache
from pathlib import Path
from typing import Any
import requests
import yaml
# ---------------------------------------------------------------------------
# Configuration
# ---------------------------------------------------------------------------
OLLAMA_URL = os.environ.get("OLLAMA_URL", "http://localhost:11434")
LLM_MODEL = os.environ.get("LLM_MODEL", "qwen3:8b")
_DEFAULT_VAULT = (Path(__file__).resolve().parent.parent / "vault-grasbot").as_posix()
VAULT_DIR = Path(os.environ.get("VAULT_DIR", _DEFAULT_VAULT))
TOP_K = int(os.environ.get("SEARCH_TOP_K", "5"))
MIN_SCORE = float(os.environ.get("SEARCH_MIN_SCORE", "1.0"))
# ---------------------------------------------------------------------------
# Tokenisation FR (stop-words minimalistes, suffisants pour 36 notes)
# ---------------------------------------------------------------------------
_FR_STOPWORDS = {
"le", "la", "les", "un", "une", "des", "du", "de", "d", "au", "aux",
"l", "et", "ou", "ni", "mais", "donc", "or", "car", "que", "qui",
"quoi", "dont", "ou", "", "si", "en", "y", "à", "a", "sur", "sous",
"dans", "par", "pour", "avec", "sans", "vers", "chez", "entre",
"est", "sont", "été", "être", "avoir", "il", "elle", "ils", "elles",
"on", "nous", "vous", "je", "tu", "me", "te", "se", "moi", "toi",
"son", "sa", "ses", "ma", "mon", "mes", "ta", "ton", "tes",
"notre", "nos", "votre", "vos", "leur", "leurs",
"ce", "cet", "cette", "ces", "cela", "ça", "celui", "celle",
"ceci", "celles", "ceux", "tout", "tous", "toute", "toutes",
"pas", "ne", "n", "plus", "moins", "très", "bien", "mal",
"peux", "peut", "peuvent", "pouvoir", "fait", "faire", "dit", "dire",
"quel", "quelle", "quels", "quelles", "comment", "pourquoi", "quand",
"parle", "parles", "parlez", "parler",
"moi", "toi", "lui", "eux",
}
_TOKEN_RE = re.compile(r"[A-Za-zÀ-ÖØ-öø-ÿ0-9+#.]+", re.UNICODE)
# Normalisations courantes pour que `c++`, `C#`, `push-swap`, `push_swap`
# tombent tous sur les mêmes tokens que le vault.
_NORMALIZE = {
"c++": "cpp",
"c#": "csharp",
}
def tokenize_fr(text: str) -> list[str]:
"""Tokenisation minimale avec normalisations :
- minuscules
- `-` et `_` éclatés en espaces (ex. `push-swap` `push swap`)
- `c++` `cpp`, `c#` → `csharp`
- stop-words FR retirés, tokens de 1 caractère écartés
"""
if not text:
return []
# Éclate les slugs/identifiants composés
cleaned = text.lower().replace("-", " ").replace("_", " ")
words = _TOKEN_RE.findall(cleaned)
out: list[str] = []
for w in words:
w = _NORMALIZE.get(w, w)
if w in _FR_STOPWORDS or len(w) <= 1:
continue
out.append(w)
return out
# ---------------------------------------------------------------------------
# Structure d'une note
# ---------------------------------------------------------------------------
@dataclass
class Note:
"""Note Obsidian chargée en mémoire, prête à être scorée."""
slug: str
title: str
type: str # projet | competence | moc | parcours | technique
path: Path
body: str
body_tokens: list[str]
# Frontmatter structurant
source: str = ""
visibility: str = "public"
domains: list[str] = field(default_factory=list)
tags: list[str] = field(default_factory=list)
aliases: list[str] = field(default_factory=list)
answers: list[str] = field(default_factory=list)
priority: int = 5 # 1 (rarement pertinent) à 10 (toujours à remonter)
# Graphe
linked: list[str] = field(default_factory=list) # slugs
related: list[str] = field(default_factory=list) # slugs
wikilinks: list[str] = field(default_factory=list) # slugs mentionnés dans le body
# Utile côté UI
extra: dict[str, Any] = field(default_factory=dict)
# ---------------------------------------------------------------------------
# Parsing du vault
# ---------------------------------------------------------------------------
_WIKILINK_RE = re.compile(r"\[\[([^\[\]|]+?)(?:\|[^\]]*?)?\]\]")
_YAML_WIKILINK_RE = re.compile(r'"\[\[([^\[\]|"]+?)(?:\|[^\]"]*?)?\]\]"')
def _extract_slugs_from_list(value: Any) -> list[str]:
"""Extrait des slugs depuis une liste YAML pouvant contenir '[[slug]]'."""
if not value:
return []
if isinstance(value, str):
value = [value]
if not isinstance(value, list):
return []
slugs: list[str] = []
for item in value:
if not isinstance(item, str):
continue
m = re.match(r"\[\[([^\[\]|]+?)(?:\|[^\]]*?)?\]\]", item.strip())
if m:
slugs.append(m.group(1).strip())
elif item.strip():
slugs.append(item.strip())
return slugs
def _extract_wikilinks_from_body(body: str) -> list[str]:
"""Retourne les slugs mentionnés via [[slug]] ou [[slug|alias]] dans le body."""
return sorted({m.group(1).strip() for m in _WIKILINK_RE.finditer(body)})
_FRONTMATTER_RE = re.compile(r"^---\s*\n(.*?)\n---\s*\n(.*)$", re.DOTALL)
def parse_note(path: Path) -> Note | None:
"""Parse une note Markdown avec frontmatter YAML. Retourne None si illisible."""
try:
raw = path.read_text(encoding="utf-8")
except OSError as exc:
print(f"⚠ parse_note: {path} illisible ({exc})")
return None
m = _FRONTMATTER_RE.match(raw)
if not m:
# Note sans frontmatter (rare) : on l'accepte quand même avec défauts.
fm: dict[str, Any] = {}
body = raw.strip()
else:
fm_text, body = m.group(1), m.group(2).strip()
try:
fm = yaml.safe_load(fm_text) or {}
except yaml.YAMLError as exc:
print(f"⚠ parse_note: frontmatter invalide dans {path.name} ({exc})")
fm = {}
slug = str(fm.get("slug") or path.stem).strip()
title = str(fm.get("title") or slug).strip()
type_ = str(fm.get("type") or "note").strip()
domains = [str(d).strip() for d in (fm.get("domains") or []) if str(d).strip()]
tags = [str(t).strip() for t in (fm.get("tags") or []) if str(t).strip()]
aliases = [str(a).strip() for a in (fm.get("aliases") or []) if str(a).strip()]
answers = [str(a).strip() for a in (fm.get("answers") or []) if str(a).strip()]
linked = _extract_slugs_from_list(fm.get("linked"))
related = _extract_slugs_from_list(fm.get("related"))
wikilinks = _extract_wikilinks_from_body(body)
try:
priority = int(fm.get("priority", 5))
except (TypeError, ValueError):
priority = 5
extra: dict[str, Any] = {}
for key in ("link", "updated"):
if key in fm and fm[key] is not None:
extra[key] = fm[key]
return Note(
slug=slug,
title=title,
type=type_,
path=path,
body=body,
body_tokens=tokenize_fr(body),
source=str(fm.get("source") or ""),
visibility=str(fm.get("visibility") or "public"),
domains=domains,
tags=tags,
aliases=aliases,
answers=answers,
priority=priority,
linked=linked,
related=related,
wikilinks=wikilinks,
extra=extra,
)
@lru_cache(maxsize=1)
def load_vault() -> dict[str, Note]:
"""Charge toutes les notes `.md` du vault en mémoire (mémoïsé).
Retourne un dict {slug: Note}. Les notes invisibles (`visibility: private`)
sont **exclues** pour que le chatbot ne puisse jamais les surfaces.
"""
if not VAULT_DIR.exists():
print(f"⚠ load_vault: {VAULT_DIR} introuvable")
return {}
vault: dict[str, Note] = {}
for md_path in sorted(VAULT_DIR.rglob("*.md")):
if md_path.name == "README.md" or md_path.name == "TAXONOMIE.md":
# Méta-docs du vault, pas de contenu à surfacer au chatbot.
continue
note = parse_note(md_path)
if note is None:
continue
if note.visibility != "public":
continue
if note.slug in vault:
print(f"⚠ load_vault: slug dupliqué '{note.slug}' ({md_path.name})")
vault[note.slug] = note
print(f"📚 Vault chargé : {len(vault)} notes ({VAULT_DIR})")
return vault
def reload_vault() -> dict[str, Note]:
"""Force la relecture du vault (utile après édition sans redémarrer l'API)."""
load_vault.cache_clear()
return load_vault()
# ---------------------------------------------------------------------------
# Scoring
# ---------------------------------------------------------------------------
def _contains_any(haystack: str, needles: list[str]) -> bool:
"""True si au moins un `needle` apparaît dans `haystack` (insensible à la casse)."""
if not needles:
return False
lower = haystack.lower()
return any(n.lower() in lower for n in needles if n)
def _token_overlap(tokens_a: list[str], tokens_b: list[str]) -> int:
"""Nombre de tokens communs (intersection simple)."""
if not tokens_a or not tokens_b:
return 0
return len(set(tokens_a) & set(tokens_b))
def _bm25_score(query_tokens: list[str], note: Note, corpus_stats: dict[str, Any]) -> float:
"""Score BM25 simplifié sur le body. Normalisé [0, ~5]."""
if not query_tokens or not note.body_tokens:
return 0.0
k1 = 1.5
b = 0.75
avgdl = corpus_stats["avgdl"]
N = corpus_stats["N"]
idf = corpus_stats["idf"]
doc_len = len(note.body_tokens)
tf_counts: dict[str, int] = {}
for tok in note.body_tokens:
tf_counts[tok] = tf_counts.get(tok, 0) + 1
score = 0.0
for q in query_tokens:
if q not in tf_counts:
continue
f = tf_counts[q]
denom = f + k1 * (1 - b + b * doc_len / avgdl) if avgdl else f
if denom == 0:
continue
score += idf.get(q, 0.0) * (f * (k1 + 1)) / denom
# Normalisation empirique : BM25 sur body court donne des valeurs 0-15,
# on écrase à [0, 5] pour rester comparable aux boosts exacts.
return min(score / 3.0, 5.0)
@lru_cache(maxsize=1)
def _corpus_stats() -> dict[str, Any]:
"""Pré-calcule les stats BM25 globales (IDF, avgdl) une fois."""
vault = load_vault()
N = len(vault) or 1
total_len = 0
df: dict[str, int] = {}
for note in vault.values():
total_len += len(note.body_tokens)
for tok in set(note.body_tokens):
df[tok] = df.get(tok, 0) + 1
avgdl = total_len / N if N else 1
idf = {
tok: math.log((N - d + 0.5) / (d + 0.5) + 1.0)
for tok, d in df.items()
}
return {"N": N, "avgdl": avgdl, "idf": idf}
@dataclass
class ScoredNote:
note: Note
score: float
reasons: list[str] = field(default_factory=list)
def _alias_matches(aliases: list[str], query_tokens: set[str], query_lower: str) -> list[str]:
"""Un alias matche si :
- il est composé de >=2 tokens ET apparaît en substring (ex. "home assistant")
- OU il est un token unique ET ce token apparaît dans les query_tokens.
"""
hits: list[str] = []
for alias in aliases:
if not alias:
continue
al = alias.strip().lower()
al_tokens = tokenize_fr(al)
if len(al_tokens) >= 2:
if al in query_lower:
hits.append(alias)
elif al_tokens:
if al_tokens[0] in query_tokens:
hits.append(alias)
elif al in query_lower:
hits.append(alias)
return hits
def _keyword_matches(keywords: list[str], query_tokens: set[str]) -> list[str]:
"""Match strict par token : évite que 'c' matche 'recette'."""
if not keywords:
return []
kw_lower = {k.lower() for k in keywords if k}
return sorted(kw_lower & query_tokens)
def score_note(note: Note, query: str, query_tokens: list[str],
stats: dict[str, Any]) -> ScoredNote:
"""Score une note selon plusieurs signaux, retourne (score, reasons)."""
score = 0.0
reasons: list[str] = []
query_lower = query.lower()
query_token_set = set(query_tokens)
title_tokens = tokenize_fr(note.title)
# 1. Match d'alias : signal très fort (synonymes explicites)
alias_hits = _alias_matches(note.aliases, query_token_set, query_lower)
if alias_hits:
score += 10.0
reasons.append(f"alias:{','.join(alias_hits[:2])}")
# 2. Match titre / slug (stricts par tokens, pas substring)
if note.title.lower() in query_lower and len(note.title) >= 4:
score += 8.0
reasons.append("title")
elif _token_overlap(title_tokens, query_tokens) >= 2:
score += 4.0
reasons.append("title-tokens")
slug_tokens = tokenize_fr(note.slug.replace("-", " ").replace("_", " "))
if slug_tokens and all(t in query_token_set for t in slug_tokens):
score += 8.0
reasons.append("slug")
# 3. Questions-type : si la requête ressemble à une question-réponse prévue
for ans in note.answers:
if ans:
overlap = _token_overlap(tokenize_fr(ans), query_tokens)
if overlap >= 3:
score += 12.0
reasons.append("answers")
break
elif overlap >= 2:
score += 5.0
reasons.append("answers-partial")
break
# 4. Domaines et tags : match STRICT par token pour éviter les faux positifs
domain_hits = _keyword_matches(note.domains, query_token_set)
if domain_hits:
score += 5.0 * len(domain_hits)
reasons.append(f"domains:{','.join(domain_hits)}")
tag_hits = _keyword_matches(note.tags, query_token_set)
if tag_hits:
score += 3.0 * len(tag_hits)
reasons.append(f"tags:{','.join(tag_hits)}")
# 4. BM25 sur le body
bm25 = _bm25_score(query_tokens, note, stats)
if bm25 > 0:
score += bm25
reasons.append(f"bm25:{bm25:.2f}")
# À ce stade, `score` reflète uniquement des **signaux textuels** (alias,
# titre, slug, answers, domaines, tags, BM25). Les boosts ci-dessous
# (priorité, moc-hub) ne s'appliquent que si l'on a au moins un vrai
# signal — sinon une note MOC au repos bouffererait tout le top à 1.6.
if score > 0:
if note.priority > 5:
score += (note.priority - 5) * 0.3
reasons.append(f"priority:{note.priority}")
if note.type == "moc":
score += 1.0
reasons.append("moc-hub")
return ScoredNote(note=note, score=score, reasons=reasons)
# ---------------------------------------------------------------------------
# Expansion par graphe
# ---------------------------------------------------------------------------
def expand_by_graph(seed: list[ScoredNote], vault: dict[str, Note],
max_extra: int = 3) -> list[ScoredNote]:
"""Ajoute les voisins directs (linked + related + wikilinks) des seeds.
Chaque voisin récupère un score dérivé de son parent (70 %) + éventuellement
boosté s'il est déjà présent dans plusieurs seeds.
"""
if not seed:
return []
result: dict[str, ScoredNote] = {s.note.slug: s for s in seed}
for parent in seed:
neighbors = set(parent.note.linked + parent.note.related + parent.note.wikilinks)
for slug in neighbors:
neighbor = vault.get(slug)
if neighbor is None:
continue
derived = parent.score * 0.6
if slug in result:
# Renforcement : voisin mentionné par plusieurs seeds = plus pertinent
result[slug].score += derived * 0.3
if "graph-reinforce" not in result[slug].reasons:
result[slug].reasons.append("graph-reinforce")
else:
result[slug] = ScoredNote(
note=neighbor,
score=derived,
reasons=[f"graph-from:{parent.note.slug}"],
)
# Limite le total de voisins ajoutés pour ne pas noyer le contexte
extras = [s for s in result.values() if s.note.slug not in {x.note.slug for x in seed}]
extras.sort(key=lambda x: -x.score)
return seed + extras[:max_extra]
# ---------------------------------------------------------------------------
# API haut-niveau : search
# ---------------------------------------------------------------------------
def search(query: str, top_k: int | None = None) -> list[ScoredNote]:
"""Retourne la liste des notes pertinentes pour `query`, triée par score."""
top_k = top_k or TOP_K
vault = load_vault()
if not vault:
return []
stats = _corpus_stats()
query_tokens = tokenize_fr(query)
scored = [score_note(note, query, query_tokens, stats) for note in vault.values()]
scored = [s for s in scored if s.score > 0]
scored.sort(key=lambda x: -x.score)
# Top-N brut avant expansion (garde 3 seeds pour expansion graphe)
seeds = scored[:3]
expanded = expand_by_graph(seeds, vault, max_extra=top_k - len(seeds))
expanded.sort(key=lambda x: -x.score)
return expanded[:top_k]
# ---------------------------------------------------------------------------
# Prompt building
# ---------------------------------------------------------------------------
SYSTEM_PROMPT = """Tu es GrasBot, l'assistant IA du portfolio de Fernand Gras-Calvet, étudiant à l'École 42 Perpignan.
Ton rôle :
- Répondre aux visiteurs du site sur le parcours, les projets, les compétences de Fernand.
- T'appuyer sur les notes du vault personnel fournies dans le contexte.
Règles :
- Réponds en français, ton sobre et précis, sans emojis.
- Cite tes sources entre crochets carrés en utilisant le slug (ex. [push-swap], [ia]).
- Si l'information n'apparaît pas dans les notes fournies, dis-le honnêtement et oriente vers le site (/portfolio, /competences, /contact) sans inventer.
- Reste concis (3 à 6 phrases en général), sauf demande explicite de détail.
- Si la question est hors sujet (ex. question généraliste sans rapport avec Fernand), indique poliment ton rôle et invite à poser une question sur son parcours."""
def build_prompt(query: str, scored_notes: list[ScoredNote]) -> tuple[str, str]:
"""Assemble (system, user) pour Qwen3. Notes **entières** dans le contexte."""
# Seuil : si toutes les notes sont en-dessous, on considère "pas de contexte pertinent"
relevant = [s for s in scored_notes if s.score >= MIN_SCORE]
if relevant:
context_blocks = []
for i, s in enumerate(relevant, 1):
n = s.note
header = f"[SOURCE {i} · slug={n.slug} · type={n.type} · score={s.score:.1f}] {n.title}"
context_blocks.append(f"{header}\n{n.body}")
context = "\n\n---\n\n".join(context_blocks)
user = (
"Voici les notes pertinentes du vault personnel de Fernand :\n\n"
f"{context}\n\n"
"---\n\n"
f"Question du visiteur : {query}\n\n"
"Réponds en t'appuyant sur ces notes. Si la question dépasse leur portée, dis-le."
)
else:
user = (
f"Question du visiteur : {query}\n\n"
"Note : aucune fiche du vault ne correspond clairement à cette question. "
"Réponds sobrement à partir de tes connaissances générales, "
"sans inventer de faits spécifiques sur Fernand. "
"Invite le visiteur à explorer /portfolio, /competences, /contact."
)
return SYSTEM_PROMPT, user
# ---------------------------------------------------------------------------
# Génération via Ollama
# ---------------------------------------------------------------------------
def generate(system: str, user: str) -> str:
"""Appelle Ollama `/api/chat` et renvoie le texte de réponse."""
response = requests.post(
f"{OLLAMA_URL}/api/chat",
json={
"model": LLM_MODEL,
"messages": [
{"role": "system", "content": system},
{"role": "user", "content": user},
],
"stream": False,
"options": {
"temperature": 0.4,
"num_predict": 512,
},
"keep_alive": "30m",
},
timeout=180,
)
response.raise_for_status()
data = response.json()
message = data.get("message") or {}
content = message.get("content", "").strip()
if not content:
raise RuntimeError(
f"generate: réponse vide du modèle '{LLM_MODEL}' — vérifier qu'il est pullé."
)
return content
# ---------------------------------------------------------------------------
# Façade haut-niveau
# ---------------------------------------------------------------------------
def answer(query: str, top_k: int | None = None) -> dict[str, Any]:
"""Entrée principale consommée par `api.py`.
Retourne :
{
"response": str, # texte LLM (consommé par askAI.js → ChatBot.js)
"sources": list[{slug, title, type, score, reasons, url?}],
"model": str,
"grounded": bool, # True si au moins 1 note a dépassé MIN_SCORE
"vault_size": int,
}
"""
scored = search(query, top_k=top_k)
system, user = build_prompt(query, scored)
text = generate(system, user)
sources = []
for s in scored:
url = None
if s.note.type == "projet":
url = f"/portfolio/{s.note.slug}"
elif s.note.type == "competence":
url = f"/competences/{s.note.slug}"
sources.append({
"slug": s.note.slug,
"title": s.note.title,
"type": s.note.type,
"score": round(s.score, 2),
"reasons": s.reasons,
**({"url": url} if url else {}),
})
grounded = any(s.score >= MIN_SCORE for s in scored)
return {
"response": text,
"sources": sources,
"model": LLM_MODEL,
"grounded": grounded,
"vault_size": len(load_vault()),
}

View File

@ -0,0 +1,764 @@
#!/usr/bin/env python3
"""Génère le vault Obsidian `vault-grasbot/` à partir de `strapi_extraction/docs/`.
Lit les `project-*.md` et `competence-*.md` produits par `generate-docs.js`,
et les réécrit sous forme de notes Obsidian structurées :
- Frontmatter YAML (type, source, domains, tags, linked, related, updated, visibility)
- Wikilinks [[...]] vers les MOCs et notes frères
- Section "Liens" en pied de note
Génère aussi les MOCs (00-MOC/) qui servent de hubs thématiques.
Usage :
python build-vault.py # régénère tout le vault
python build-vault.py --dry-run # liste sans écrire
Dépendances : stdlib seule. Optionnel : `pypdf` pour convertir le CV PDF
(absent le PDF est ignoré, conversion manuelle possible après coup).
"""
from __future__ import annotations
import argparse
import re
import shutil
import sys
from dataclasses import dataclass, field
from datetime import date
from pathlib import Path
# ---------------------------------------------------------------------------
# Chemins
# ---------------------------------------------------------------------------
ROOT = Path(__file__).resolve().parent.parent
DOCS_DIR = ROOT / "strapi_extraction" / "docs"
VAULT_DIR = ROOT / "vault-grasbot"
PDF_CV = DOCS_DIR / "nouveauCV_grascalvet.pdf"
SUBDIRS = (
"00-MOC",
"10-Projets",
"20-Competences",
"30-Parcours",
"40-Glossaire",
"50-Technique",
)
# ---------------------------------------------------------------------------
# Inférence de domaines / tags à partir de mots-clés.
# Première version volontairement simple : on cherche des sous-chaînes (case-insensitive)
# dans le titre + corps de la note. Affinable au fil de l'enrichissement du vault.
# ---------------------------------------------------------------------------
DOMAIN_KEYWORDS: dict[str, list[str]] = {
"algorithmique": ["tri", "pile", "algorithm", "complexité", "push_swap", "fractal"],
"c": ["langage c", "printf", "libft", "get_next_line", "minitalk", "philosopher"],
"cpp": ["c++", "cpp", "poo", "polymorphisme", "template", "stl"],
"systeme": ["unix", "signal", "processus", "mutex", "thread", "ipc", "bash"],
"reseau": ["tcp", "ip", "socket", "irc", "netpractice", "routage"],
"web": ["next.js", "nextjs", "react", "django", "api rest", "websocket", "strapi"],
"devops": ["docker", "nginx", "mariadb", "wordpress", "inception", "conteneur"],
"securite": ["born2beroot", "ssh", "fail2ban", "ufw", "lvm", "cybersécurité"],
"ia": ["llm", "ollama", "ia locale", "intelligence artificielle", "chatbot", "embedding"],
"graphique": ["minilibx", "raycasting", "cub3d", "fract-ol", "wolfenstein"],
"3d": ["impression 3d", "3d printing", "prusa", "slicer", "filament"],
"domotique": ["domotique", "home assistant", "zigbee", "iot"],
"ecole-42": ["école 42", "42 perpignan", "42 paris", "projet pédagogique"],
}
TAG_KEYWORDS: dict[str, list[str]] = {
"42-commun": ["libft", "get_next_line", "push_swap", "minitalk", "philosopher"],
"42-piscine": ["piscine"],
"42-tronc": ["minishell", "inception", "cub3d", "netpractice"],
"tri": ["tri", "push_swap"],
"concurrence": ["thread", "mutex", "philosopher"],
"docker": ["docker", "inception"],
"makefile": ["makefile"],
"projet-perso": [], # drapeau manuel (futur)
}
# Aliases par domaine : synonymes / acronymes utilisés par les visiteurs.
# Injectés automatiquement dans les notes du domaine pour booster le retrieval
# (voir llm-api/search.py). Complémentaires aux aliases manuels du frontmatter.
DOMAIN_ALIASES: dict[str, list[str]] = {
"algorithmique": ["algo", "algorithme", "algorithmes", "complexité"],
"c": ["langage c", "ansi c", "c 42"],
"cpp": ["c++", "cpp 42", "poo", "programmation orientée objet"],
"systeme": ["système", "unix", "linux", "processus", "threads"],
"reseau": ["réseau", "tcp", "ip", "sockets", "routage"],
"web": ["développement web", "site web", "frontend", "backend", "full stack"],
"devops": ["devops", "conteneurs", "ci/cd", "infrastructure"],
"securite": ["sécurité", "hardening", "cybersécurité", "audit"],
"ia": ["ia", "intelligence artificielle", "llm", "llms", "modèles de langage",
"chatbot", "chatbots", "machine learning", "deep learning", "data science",
"ollama", "agent", "agents", "rag"],
"graphique": ["rendu", "raycasting", "minilibx", "graphisme", "2d", "game dev"],
"3d": ["impression 3d", "3d printing", "fdm", "slicer", "prusa"],
"domotique": ["domotique", "home assistant", "iot", "smart home", "zigbee"],
"ecole-42": ["42", "école 42", "42 perpignan", "42 paris", "piscine 42", "tronc commun"],
"parcours": ["parcours", "cv", "profil", "carrière", "reconversion", "trajectoire"],
}
def infer(text: str, catalog: dict[str, list[str]]) -> list[str]:
"""Retourne les clés du catalog dont au moins un mot-clé apparaît dans text."""
lowered = text.lower()
return sorted(k for k, keywords in catalog.items() if any(kw in lowered for kw in keywords))
def slug_variants(slug: str, title: str) -> list[str]:
"""Retourne des variantes utiles d'un slug/titre pour les aliases.
Ex. slug="push-swap", title="push_swap" ["push swap", "push-swap", "push_swap"]
"""
variants: set[str] = set()
for base in (slug, title):
if not base:
continue
b = base.strip()
variants.add(b.lower())
variants.add(b.replace("-", " ").lower())
variants.add(b.replace("_", " ").lower())
variants.add(b.replace("-", "_").lower())
variants.add(b.replace("_", "-").lower())
# Retire les vides et les doublons, trie par longueur décroissante (plus spécifiques d'abord)
out = sorted((v for v in variants if v), key=lambda s: (-len(s), s))
return out[:4]
def _order_domains_by_slug(slug: str, domains: list[str]) -> list[str]:
"""Remonte en tête le domaine qui correspond au slug (ou préfixe).
Ex. slug='ia' + domains=['algorithmique','ecole-42','ia'] ['ia','algorithmique','ecole-42'].
"""
s = slug.lower()
if not domains:
return []
exact = [d for d in domains if d.lower() == s]
rest = [d for d in domains if d.lower() != s]
return exact + rest
def build_aliases(title: str, slug: str, domains: list[str]) -> list[str]:
"""Génère une liste d'aliases à partir du titre, du slug et des domaines.
On priorise dans l'ordre : slug-variants > domaine match avec slug > autres domaines.
Coupe à 12 pour éviter de trop disperser le scoring, en gardant les plus
spécifiques (slug du domaine d'abord).
"""
aliases: list[str] = []
aliases.extend(slug_variants(slug, title))
ordered_domains = _order_domains_by_slug(slug, domains)
for d in ordered_domains:
aliases.extend(DOMAIN_ALIASES.get(d, []))
# Dé-doublonne tout en préservant l'ordre
seen: set[str] = set()
out: list[str] = []
for a in aliases:
a_norm = a.lower().strip()
if a_norm and a_norm not in seen:
seen.add(a_norm)
out.append(a)
return out[:12]
# Courts libellés parlants pour les answers de compétences (plutôt que le titre brut).
# Priorité : slug > premier domaine significatif > fallback sur le titre.
COMPETENCE_SHORT_LABELS: dict[str, str] = {
"ia": "IA",
"domotique": "domotique",
"3d": "impression 3D",
"web": "développement web",
"securite": "sécurité",
"reseau": "réseaux",
"systeme": "systèmes",
"devops": "DevOps",
"graphique": "programmation graphique",
"cpp": "C++",
"c": "langage C",
"algorithmique": "algorithmique",
"ecole-42": "l'École 42",
}
def _competence_label(title: str, slug: str, domains: list[str]) -> str:
"""Retourne un libellé court et parlant pour une compétence."""
if slug in COMPETENCE_SHORT_LABELS:
return COMPETENCE_SHORT_LABELS[slug]
for d in domains:
if d in COMPETENCE_SHORT_LABELS and d != "ecole-42":
return COMPETENCE_SHORT_LABELS[d]
# Fallback : titre tronqué aux 5 premiers mots
words = title.split()
return " ".join(words[:5]).rstrip(".?!")
def build_answers(title: str, type_: str, slug: str = "", domains: list[str] | None = None) -> list[str]:
"""Génère 2-3 questions-types auxquelles cette note répond naturellement."""
domains = domains or []
t = title.strip().rstrip(".?!")
if type_ == "projet":
return [
f"Parle-moi de {t}",
f"Qu'est-ce que {t} ?",
f"Comment fonctionne {t} ?",
]
if type_ == "competence":
label = _competence_label(title, slug, domains)
return [
f"Quelles sont ses compétences en {label} ?",
f"A-t-il de l'expérience en {label} ?",
f"Parle-moi de son expérience en {label}",
]
if type_ == "moc":
domain_name = t.replace("MOC —", "").replace("MOC -", "").strip()
return [
f"Que fait-il en {domain_name} ?",
f"Quels projets en {domain_name} ?",
]
if type_ == "parcours":
return [
f"Quel est son parcours ?",
f"Que peux-tu me dire sur Fernand ?",
f"Cherche-t-il une alternance ?",
]
return []
def compute_priority(type_: str, domains: list[str]) -> int:
"""Priorité heuristique : MOCs > compétences > projets emblématiques > autres."""
if type_ == "parcours":
return 10
if type_ == "moc":
return 7
if type_ == "competence":
return 7
# Projets : boost léger si domaine "ia" (stratégique pour l'alternance visée)
if type_ == "projet" and "ia" in domains:
return 6
return 5
# ---------------------------------------------------------------------------
# Structures
# ---------------------------------------------------------------------------
@dataclass
class Note:
"""Note Obsidian prête à être sérialisée."""
filename: str # "push-swap.md"
title: str # "push_swap"
type: str # "projet" | "competence" | "parcours" | ...
slug: str
source: str # "strapi/projects" ou similaire
domains: list[str] = field(default_factory=list)
tags: list[str] = field(default_factory=list)
aliases: list[str] = field(default_factory=list)
answers: list[str] = field(default_factory=list)
priority: int = 5
linked: list[str] = field(default_factory=list) # wikilinks (sans les [[ ]])
related: list[str] = field(default_factory=list)
extra: dict[str, str] = field(default_factory=dict) # champs spécifiques (link, etc.)
body: str = ""
def serialize(self) -> str:
"""Retourne le contenu complet de la note Obsidian, frontmatter + corps."""
yaml_lines = ["---"]
yaml_lines.append(f"title: {self._yaml_str(self.title)}")
yaml_lines.append(f"slug: {self.slug}")
yaml_lines.append(f"type: {self.type}")
yaml_lines.append(f"source: {self.source}")
yaml_lines.append(f"domains: {self._yaml_list(self.domains)}")
yaml_lines.append(f"tags: {self._yaml_list(self.tags)}")
if self.aliases:
yaml_lines.append("aliases:")
for alias in self.aliases:
yaml_lines.append(f" - {self._yaml_str(alias)}")
if self.answers:
yaml_lines.append("answers:")
for answer in self.answers:
yaml_lines.append(f" - {self._yaml_str(answer)}")
yaml_lines.append(f"priority: {self.priority}")
yaml_lines.append("linked:")
for link in self.linked:
yaml_lines.append(f" - \"[[{link}]]\"")
if self.related:
yaml_lines.append("related:")
for rel in self.related:
yaml_lines.append(f" - \"[[{rel}]]\"")
for key, val in self.extra.items():
yaml_lines.append(f"{key}: {self._yaml_str(val)}")
yaml_lines.append(f"updated: {date.today().isoformat()}")
yaml_lines.append("visibility: public")
yaml_lines.append("---")
yaml_lines.append("")
return "\n".join(yaml_lines) + self.body
@staticmethod
def _yaml_str(value: str) -> str:
if value is None:
return '""'
if any(c in value for c in ":#&*!|>'\"%@`"):
escaped = value.replace('"', '\\"')
return f'"{escaped}"'
return value
@staticmethod
def _yaml_list(values: list[str]) -> str:
if not values:
return "[]"
return "[" + ", ".join(values) + "]"
# ---------------------------------------------------------------------------
# Parsing des .md sources
# ---------------------------------------------------------------------------
SLUG_RE = re.compile(r"^\*\*Slug :\*\*\s*`([^`]+)`", re.MULTILINE)
LINK_RE = re.compile(r"^\*\*Lien GitHub :\*\*\s*\[.+?\]\((.+?)\)", re.MULTILINE)
H1_RE = re.compile(r"^# (.+)$", re.MULTILINE)
H2_RE = re.compile(r"^## (.+)$", re.MULTILINE)
def parse_project(filepath: Path) -> Note | None:
"""Transforme un project-*.md en Note projet."""
raw = filepath.read_text(encoding="utf-8")
title_match = H1_RE.search(raw)
slug_match = SLUG_RE.search(raw)
if not title_match or not slug_match:
print(f"{filepath.name} : titre ou slug introuvable, ignoré", file=sys.stderr)
return None
title = title_match.group(1).strip()
slug = slug_match.group(1).strip()
link_match = LINK_RE.search(raw)
body_start = title_match.end()
body = raw[body_start:].strip()
# Retire la section "Informations techniques" dupliquée que le générateur ajoute
# (on la reconstruit dans le footer).
body = re.sub(r"\n## Informations techniques\n[\s\S]*$", "", body).strip()
extra: dict[str, str] = {}
if link_match:
extra["link"] = link_match.group(1).strip()
domains = infer(raw, DOMAIN_KEYWORDS)
tags = infer(raw, TAG_KEYWORDS)
if "ecole-42" not in domains:
domains.append("ecole-42")
domains.sort()
linked = ["MOC-Projets", "MOC-Ecole-42"]
footer = "\n\n---\n\n## Liens\n\n"
footer += "- [[MOC-Projets]] — vue d'ensemble des projets\n"
footer += "- [[MOC-Ecole-42]] — contexte pédagogique\n"
for d in domains:
if d != "ecole-42":
footer += f"- [[MOC-{d.capitalize()}]] — domaine *{d}*\n"
return Note(
filename=f"{slug}.md",
title=title,
type="projet",
slug=slug,
source="strapi/projects",
domains=domains,
tags=tags,
aliases=build_aliases(title, slug, domains),
answers=build_answers(title, "projet", slug, domains),
priority=compute_priority("projet", domains),
linked=linked,
extra=extra,
body=body + footer,
)
def parse_competence(filepath: Path) -> Note | None:
"""Transforme un competence-*.md en Note compétence."""
raw = filepath.read_text(encoding="utf-8")
title_match = H1_RE.search(raw)
slug_match = SLUG_RE.search(raw)
if not title_match or not slug_match:
print(f"{filepath.name} : titre ou slug introuvable, ignoré", file=sys.stderr)
return None
title = title_match.group(1).strip()
slug = slug_match.group(1).strip()
body_start = title_match.end()
body = raw[body_start:].strip()
domains = infer(raw, DOMAIN_KEYWORDS)
tags = infer(raw, TAG_KEYWORDS)
linked = ["MOC-Competences"]
footer = "\n\n---\n\n## Liens\n\n"
footer += "- [[MOC-Competences]] — vue d'ensemble des compétences\n"
for d in domains:
footer += f"- [[MOC-{d.capitalize()}]] — domaine *{d}*\n"
return Note(
filename=f"{slug}.md",
title=title,
type="competence",
slug=slug,
source="strapi/competences",
domains=domains,
tags=tags,
aliases=build_aliases(title, slug, domains),
answers=build_answers(title, "competence", slug, domains),
priority=compute_priority("competence", domains),
linked=linked,
body=body + footer,
)
# ---------------------------------------------------------------------------
# Génération des MOCs
# ---------------------------------------------------------------------------
def build_moc(
title: str,
description: str,
notes: list[Note],
*,
moc_slug: str,
type_filter: str | None = None,
domain_filter: str | None = None,
) -> Note:
selected = [
n for n in notes
if (type_filter is None or n.type == type_filter)
and (domain_filter is None or domain_filter in n.domains)
]
selected.sort(key=lambda n: n.title.lower())
body = f"\n\n{description}\n\n## Notes liées\n\n"
if not selected:
body += "*Aucune note pour l'instant.*\n"
else:
for n in selected:
body += f"- [[{n.slug}|{n.title}]]"
if n.domains:
body += f" — _{', '.join(n.domains)}_"
body += "\n"
moc_domains = [domain_filter] if domain_filter else []
moc_aliases = build_aliases(title, moc_slug, moc_domains)
# Un MOC répond bien aux questions "quels projets en X", "domaine Y"
domain_label = domain_filter or title.replace("MOC —", "").replace("MOC -", "").strip()
moc_answers = [
f"Quels projets en {domain_label} ?",
f"Que fait-il en {domain_label} ?",
]
return Note(
filename=f"{moc_slug}.md",
title=title,
type="moc",
slug=moc_slug,
source="vault/generated",
domains=moc_domains,
tags=["moc"],
aliases=moc_aliases,
answers=moc_answers,
priority=7,
linked=[],
body=body,
)
def build_mocs(projects: list[Note], competences: list[Note]) -> list[tuple[str, Note]]:
"""Construit la liste des MOCs à écrire. Chaque item = (sous-dossier, Note)."""
all_notes = projects + competences
mocs: list[Note] = []
mocs.append(build_moc(
"MOC — Projets",
"Hub des projets de Fernand Gras-Calvet, triés par titre.",
all_notes,
moc_slug="MOC-Projets",
type_filter="projet",
))
mocs.append(build_moc(
"MOC — Compétences",
"Hub des domaines de compétences.",
all_notes,
moc_slug="MOC-Competences",
type_filter="competence",
))
mocs.append(build_moc(
"MOC — École 42",
"Tout ce qui est rattaché à la formation 42 Perpignan.",
all_notes,
moc_slug="MOC-Ecole-42",
domain_filter="ecole-42",
))
# MOCs par domaine significatif (s'il y a au moins 2 notes).
domain_counts: dict[str, int] = {}
for n in all_notes:
for d in n.domains:
domain_counts[d] = domain_counts.get(d, 0) + 1
for d, count in sorted(domain_counts.items()):
if count < 2 or d == "ecole-42":
continue
mocs.append(build_moc(
f"MOC — {d.capitalize()}",
f"Notes du domaine *{d}* ({count} au total).",
all_notes,
moc_slug=f"MOC-{d.capitalize()}",
domain_filter=d,
))
mocs.append(build_moc(
"MOC — Parcours",
"Parcours atypique de Fernand Gras-Calvet, du CV aux projets.",
all_notes,
moc_slug="MOC-Parcours",
type_filter="parcours",
))
return [("00-MOC", m) for m in mocs]
# ---------------------------------------------------------------------------
# PDF CV → Markdown (optionnel, requiert pypdf)
# ---------------------------------------------------------------------------
def try_build_cv(vault_dir: Path, dry_run: bool = False) -> Note | None:
if not PDF_CV.exists():
print(f" PDF CV absent ({PDF_CV}), étape ignorée")
return None
try:
from pypdf import PdfReader
except ImportError:
print(
" ⚠ `pypdf` non installé — la conversion du CV est ignorée.\n"
" Installer : pip install pypdf\n"
" Ou fournir une version déjà convertie dans 30-Parcours/"
)
return None
print(f" 🔄 Conversion PDF → MD : {PDF_CV.name}")
reader = PdfReader(str(PDF_CV))
pages_text = [page.extract_text() or "" for page in reader.pages]
raw = "\n\n".join(pages_text).strip()
body = "\n\n> [!info] Source\n> Extrait automatiquement depuis `nouveauCV_grascalvet.pdf`.\n> Structurer manuellement dans Obsidian si besoin.\n\n"
body += "## Contenu brut\n\n"
body += raw + "\n"
note = Note(
filename="cv-grascalvet-fernand.md",
title="CV — Fernand Gras-Calvet",
type="parcours",
slug="cv-grascalvet-fernand",
source="pdf/nouveauCV_grascalvet",
domains=["ecole-42"],
tags=["cv", "parcours"],
linked=["MOC-Parcours"],
body=body,
)
return note
# ---------------------------------------------------------------------------
# Écriture du vault
# ---------------------------------------------------------------------------
_FRONTMATTER_SOURCE_RE = re.compile(r"^---\s*\n(?:.*?\n)*?source:\s*([^\n]+)\n(?:.*?\n)*?---\s*\n", re.DOTALL)
def _existing_source(path: Path) -> str | None:
"""Retourne la valeur `source:` du frontmatter existant, ou None."""
if not path.exists():
return None
try:
head = path.read_text(encoding="utf-8")[:2000]
except OSError:
return None
m = _FRONTMATTER_SOURCE_RE.match(head)
if not m:
return None
return m.group(1).strip().strip('"').strip("'")
def write_notes(pairs: list[tuple[str, Note]], dry_run: bool) -> None:
"""Écrit chaque note sur disque, sauf celles dont le frontmatter local a
`source: manual` dans ce cas on préserve la version curatée humainement.
"""
skipped = 0
for subdir, note in pairs:
target = VAULT_DIR / subdir / note.filename
existing = _existing_source(target)
if existing == "manual":
print(f"{target.relative_to(ROOT)} (source: manual, préservé)")
skipped += 1
continue
if dry_run:
print(f" [dry] {target.relative_to(ROOT)}")
continue
target.parent.mkdir(parents=True, exist_ok=True)
target.write_text(note.serialize(), encoding="utf-8")
if skipped:
print(f" {skipped} note(s) préservée(s) (source: manual).")
def write_readme(projects: list[Note], competences: list[Note], dry_run: bool) -> None:
readme = f"""# Vault GrasBot — Base de connaissances
Vault Obsidian généré par `strapi_extraction/build-vault.py` à partir des
contenus Strapi du site (projets + compétences) et du CV PDF. Alimente
directement le pipeline de recherche de GrasBot (`llm-api/search.py`) :
graph + BM25, sans embeddings.
**Dernière génération :** {date.today().isoformat()}
## Structure
- `00-MOC/` Maps of Content (hubs thématiques)
- `10-Projets/` {len(projects)} projets extraits de Strapi
- `20-Competences/` {len(competences)} compétences extraites de Strapi
- `30-Parcours/` Parcours personnel, CV, bio (version curatée `source: manual`)
- `40-Glossaire/` Termes techniques (vide, à remplir manuellement ou depuis Strapi plus tard)
- `50-Technique/` Auto-documentation (architecture, retrieval, vault)
- `TAXONOMIE.md` Vocabulaire contrôlé (domaines, tags, aliases, answers, priority)
## Conventions
Chaque note porte un frontmatter YAML enrichi :
```yaml
---
title: ...
slug: ...
type: projet | competence | parcours | glossaire | moc | technique
source: strapi/... | pdf/... | manual | vault/generated
domains: [ia, web, systeme, ...] # taxonomie contrôlée
tags: [tag-1, tag-2]
aliases: # synonymes pour le retrieval
- "alias court"
- "autre formulation"
answers: # questions-types auxquelles répond la note
- "Question formulée naturellement ?"
priority: 5 # 1..10, boost léger au scoring
linked: ["[[MOC-...]]"] # voisins du graphe (sortants)
related: ["[[autre-note]]"]
updated: YYYY-MM-DD
visibility: public | private # `private` exclu du retrieval
---
```
Voir `TAXONOMIE.md` pour le vocabulaire contrôlé des domaines/tags et les
règles de rédaction des aliases/answers.
**Règle de régénération** : le script `build-vault.py` **écrase** sans prévenir
les notes dont le frontmatter a `source: strapi/*` ou `source: pdf/*`. Il ne
touche **jamais** aux notes `source: manual` que tu ajoutes toi-même. Les
aliases, answers et priority des notes générées sont calculés automatiquement
à partir du titre, du slug et des domaines ; les notes stratégiques méritent
un enrichissement manuel en passant `source: manual`.
## Fusion avec un vault personnel
Pour agrémenter ce vault avec ton vault Obsidian perso :
1. Copier `vault-grasbot/` dans ton vault existant comme sous-dossier, ou
2. Ouvrir `vault-grasbot/` comme vault séparé dans Obsidian (plus simple pour démarrer).
Les wikilinks `[[nom]]` restent valides tant que les noms de notes sont uniques
dans le vault courant. Les notes `source: manual` que tu crées ne seront jamais
écrasées par une régénération. Pour une note privée qui ne doit pas apparaître
côté chatbot, ajouter `visibility: private` : elle sera exclue de `load_vault()`.
"""
target = VAULT_DIR / "README.md"
if dry_run:
print(f" [dry] {target.relative_to(ROOT)}")
return
target.write_text(readme, encoding="utf-8")
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--dry-run", action="store_true", help="N'écrit rien, affiche juste ce qui serait fait.")
parser.add_argument("--clean", action="store_true", help="Supprime le vault avant de le régénérer (les notes manuelles seront perdues !).")
args = parser.parse_args()
print(f"🏗 Vault → {VAULT_DIR.relative_to(ROOT)}")
if args.clean and VAULT_DIR.exists() and not args.dry_run:
print(f" 🧹 Suppression du vault existant (--clean)")
shutil.rmtree(VAULT_DIR)
if not args.dry_run:
for subdir in SUBDIRS:
(VAULT_DIR / subdir).mkdir(parents=True, exist_ok=True)
# Parsing
print("\n📦 Parsing des projets…")
project_notes = []
for fp in sorted(DOCS_DIR.glob("project-*.md")):
note = parse_project(fp)
if note:
project_notes.append(note)
print(f"{note.slug}")
print(f"\n📦 Parsing des compétences…")
competence_notes = []
for fp in sorted(DOCS_DIR.glob("competence-*.md")):
note = parse_competence(fp)
if note:
competence_notes.append(note)
print(f"{note.slug}")
# Related : pour chaque note, trouve les 3 notes les plus similaires (intersection domains)
print("\n🔗 Calcul des notes connexes…")
for note in project_notes + competence_notes:
note.related = _find_related(note, project_notes + competence_notes, limit=3)
# Réinjecte les related dans le footer : on réécrit la section Liens.
# (Le footer est déjà inclus dans note.body — on le laisse tel quel, les
# wikilinks related apparaîtront via le frontmatter. Simplicité >.)
# MOCs
print("\n🗺 Génération des MOCs…")
moc_pairs = build_mocs(project_notes, competence_notes)
for _, m in moc_pairs:
print(f"{m.slug}")
# Assemble l'ensemble à écrire
pairs: list[tuple[str, Note]] = []
pairs += [("10-Projets", n) for n in project_notes]
pairs += [("20-Competences", n) for n in competence_notes]
pairs += moc_pairs
# PDF CV
print("\n📄 CV PDF…")
cv_note = try_build_cv(VAULT_DIR, dry_run=args.dry_run)
if cv_note:
pairs.append(("30-Parcours", cv_note))
# Écriture
print(f"\n✍ Écriture ({len(pairs)} notes)…")
write_notes(pairs, dry_run=args.dry_run)
write_readme(project_notes, competence_notes, dry_run=args.dry_run)
print(f"\n🎯 Terminé — {len(project_notes)} projets, {len(competence_notes)} compétences, {len(moc_pairs)} MOCs" + (", 1 CV" if cv_note else ""))
print(f"📁 {VAULT_DIR.relative_to(ROOT)}")
return 0
def _find_related(note: Note, all_notes: list[Note], limit: int = 3) -> list[str]:
"""Ordre simple : notes qui partagent le plus de domains avec `note`."""
scored: list[tuple[int, Note]] = []
for other in all_notes:
if other.slug == note.slug:
continue
shared = len(set(note.domains) & set(other.domains))
if shared >= 1:
scored.append((shared, other))
scored.sort(key=lambda x: (-x[0], x[1].title.lower()))
return [n.slug for _, n in scored[:limit]]
if __name__ == "__main__":
sys.exit(main())

View File

@ -89,9 +89,10 @@ export default {
bebas: ['Bebas Neue', 'sans-serif'],
// Stitch "Digital Atelier" : Manrope pour titres/UI, Newsreader pour corps éditorial.
headline: ['Manrope', 'system-ui', 'sans-serif'],
body: ['Newsreader', 'Georgia', 'serif'],
label: ['Manrope', 'system-ui', 'sans-serif'],
// Variables CSS posées par next/font/google (voir app/fonts.ts + app/layout.tsx).
headline: ['var(--font-manrope)', 'system-ui', 'sans-serif'],
body: ['var(--font-newsreader)', 'Georgia', 'serif'],
label: ['var(--font-manrope)', 'system-ui', 'sans-serif'],
},
borderRadius: {
// Additifs : ne remplacent pas les radius Tailwind (xl, 2xl, etc.) pour ne rien casser.

View File

@ -0,0 +1,43 @@
---
title: MOC — Algorithmique
slug: MOC-Algorithmique
type: moc
source: vault/generated
domains: [algorithmique]
tags: [moc]
aliases:
- moc — algorithmique
- moc algorithmique
- moc-algorithmique
- moc_algorithmique
- algo
- algorithme
- algorithmes
- complexité
answers:
- Quels projets en algorithmique ?
- Que fait-il en algorithmique ?
priority: 7
linked:
updated: 2026-04-22
visibility: public
---
Notes du domaine *algorithmique* (13 au total).
## Notes liées
- [[born2beroot|Born2beroot]] — _algorithmique, ecole-42, reseau, securite_
- [[cpp-partie1|cpp-partie1]] — _algorithmique, c, cpp, 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_
- [[ft-transcendence|ft_transcendence]] — _algorithmique, devops, ecole-42, reseau, web_
- [[inception|inception]] — _algorithmique, devops, ecole-42, reseau, systeme_
- [[libft|libft]] — _algorithmique, c, domotique, ecole-42, reseau_
- [[minishell|minishell]] — _algorithmique, domotique, ecole-42, reseau, systeme_
- [[ia|Mon Exploration et Maîtrise de lIntelligence Artificielle]] — _algorithmique, ecole-42, ia_
- [[competence|Mon expérience dans la domotique]] — _algorithmique, domotique, ia, reseau_
- [[impression-3d|Mon parcours dans limpression 3D]] — _3d, algorithmique, reseau_
- [[netpractice|netpractice]] — _algorithmique, ecole-42, reseau_
- [[push-swap|push_swap]] — _algorithmique, ecole-42, reseau_

View File

@ -0,0 +1,36 @@
---
title: MOC — C
slug: MOC-C
type: moc
source: vault/generated
domains: [c]
tags: [moc]
aliases:
- moc — c
- moc c
- moc-c
- moc_c
- langage c
- ansi c
- c 42
answers:
- Quels projets en c ?
- Que fait-il en c ?
priority: 7
linked:
updated: 2026-04-22
visibility: public
---
Notes du domaine *c* (7 au total).
## Notes liées
- [[cpp-partie1|cpp-partie1]] — _algorithmique, c, cpp, ecole-42, reseau_
- [[cpp-partie2|cpp-partie2]] — _algorithmique, c, cpp, devops, domotique, ecole-42, reseau_
- [[ft-printf|Ft-printf]] — _c, ecole-42, reseau_
- [[get-next-line|Get_next_line]] — _c, ecole-42, reseau_
- [[libft|libft]] — _algorithmique, c, domotique, ecole-42, reseau_
- [[minitalk|minitalk]] — _c, ecole-42, reseau, systeme_
- [[philosopher|philosopher]] — _c, ecole-42, reseau, systeme_

View File

@ -0,0 +1,30 @@
---
title: MOC — Compétences
slug: MOC-Competences
type: moc
source: vault/generated
domains: []
tags: [moc]
aliases:
- moc — compétences
- moc competences
- moc-competences
- moc_competences
answers:
- Quels projets en Compétences ?
- Que fait-il en Compétences ?
priority: 7
linked:
updated: 2026-04-22
visibility: public
---
Hub des domaines de compétences.
## Notes liées
- [[developpement-web-and-hebergement-sur-serveur-windows|Développement Web & Hébergement sur serveur Windows]] — _reseau, securite, web_
- [[ia|Mon Exploration et Maîtrise de lIntelligence Artificielle]] — _algorithmique, ecole-42, ia_
- [[competence|Mon expérience dans la domotique]] — _algorithmique, domotique, ia, reseau_
- [[impression-3d|Mon parcours dans limpression 3D]] — _3d, algorithmique, reseau_

View File

@ -0,0 +1,33 @@
---
title: MOC — Cpp
slug: MOC-Cpp
type: moc
source: vault/generated
domains: [cpp]
tags: [moc]
aliases:
- moc — cpp
- moc cpp
- moc-cpp
- moc_cpp
- c++
- cpp 42
- poo
- programmation orientée objet
answers:
- Quels projets en cpp ?
- Que fait-il en cpp ?
priority: 7
linked:
updated: 2026-04-22
visibility: public
---
Notes du domaine *cpp* (3 au total).
## Notes liées
- [[cpp-partie1|cpp-partie1]] — _algorithmique, c, cpp, ecole-42, reseau_
- [[cpp-partie2|cpp-partie2]] — _algorithmique, c, cpp, devops, domotique, ecole-42, reseau_
- [[ft-irc|ft-irc]] — _cpp, ecole-42, reseau, systeme_

View File

@ -0,0 +1,33 @@
---
title: MOC — Devops
slug: MOC-Devops
type: moc
source: vault/generated
domains: [devops]
tags: [moc]
aliases:
- moc — devops
- moc devops
- moc-devops
- moc_devops
- devops
- conteneurs
- ci/cd
- infrastructure
answers:
- Quels projets en devops ?
- Que fait-il en devops ?
priority: 7
linked:
updated: 2026-04-22
visibility: public
---
Notes du domaine *devops* (3 au total).
## Notes liées
- [[cpp-partie2|cpp-partie2]] — _algorithmique, c, cpp, devops, domotique, ecole-42, reseau_
- [[ft-transcendence|ft_transcendence]] — _algorithmique, devops, ecole-42, reseau, web_
- [[inception|inception]] — _algorithmique, devops, ecole-42, reseau, systeme_

View File

@ -0,0 +1,37 @@
---
title: MOC — Domotique
slug: MOC-Domotique
type: moc
source: vault/generated
domains: [domotique]
tags: [moc]
aliases:
- moc — domotique
- moc domotique
- moc-domotique
- moc_domotique
- domotique
- home assistant
- iot
- smart home
- zigbee
answers:
- Quels projets en domotique ?
- Que fait-il en domotique ?
priority: 7
linked:
updated: 2026-04-22
visibility: public
---
Notes du domaine *domotique* (6 au total).
## Notes liées
- [[cpp-partie2|cpp-partie2]] — _algorithmique, c, cpp, devops, domotique, ecole-42, reseau_
- [[cub3d|cub3d]] — _domotique, ecole-42, graphique, reseau_
- [[fract-ol|fract-ol]] — _algorithmique, domotique, ecole-42, graphique, reseau, systeme_
- [[libft|libft]] — _algorithmique, c, domotique, ecole-42, reseau_
- [[minishell|minishell]] — _algorithmique, domotique, ecole-42, reseau, systeme_
- [[competence|Mon expérience dans la domotique]] — _algorithmique, domotique, ia, reseau_

View File

@ -0,0 +1,50 @@
---
title: MOC — École 42
slug: MOC-Ecole-42
type: moc
source: vault/generated
domains: [ecole-42]
tags: [moc]
aliases:
- moc — école 42
- moc ecole 42
- moc-ecole-42
- moc_ecole_42
- 42
- école 42
- 42 perpignan
- 42 paris
- piscine 42
- tronc commun
answers:
- Quels projets en ecole-42 ?
- Que fait-il en ecole-42 ?
priority: 7
linked:
updated: 2026-04-22
visibility: public
---
Tout ce qui est rattaché à la formation 42 Perpignan.
## Notes liées
- [[born2beroot|Born2beroot]] — _algorithmique, ecole-42, reseau, securite_
- [[cpp-partie1|cpp-partie1]] — _algorithmique, c, cpp, ecole-42, reseau_
- [[cpp-partie2|cpp-partie2]] — _algorithmique, c, cpp, devops, domotique, ecole-42, reseau_
- [[cub3d|cub3d]] — _domotique, ecole-42, graphique, reseau_
- [[fract-ol|fract-ol]] — _algorithmique, domotique, ecole-42, graphique, reseau, systeme_
- [[ft-irc|ft-irc]] — _cpp, ecole-42, reseau, systeme_
- [[ft-printf|Ft-printf]] — _c, ecole-42, reseau_
- [[ft-transcendence|ft_transcendence]] — _algorithmique, devops, ecole-42, reseau, web_
- [[get-next-line|Get_next_line]] — _c, ecole-42, reseau_
- [[inception|inception]] — _algorithmique, devops, ecole-42, reseau, systeme_
- [[libft|libft]] — _algorithmique, c, domotique, ecole-42, reseau_
- [[minishell|minishell]] — _algorithmique, domotique, ecole-42, reseau, systeme_
- [[minitalk|minitalk]] — _c, ecole-42, reseau, systeme_
- [[ia|Mon Exploration et Maîtrise de lIntelligence Artificielle]] — _algorithmique, ecole-42, ia_
- [[netpractice|netpractice]] — _algorithmique, ecole-42, reseau_
- [[philosopher|philosopher]] — _c, ecole-42, reseau, systeme_
- [[presentation-ecole-42|Présentation école 42]] — _ecole-42, reseau, systeme_
- [[push-swap|push_swap]] — _algorithmique, ecole-42, reseau_

View File

@ -0,0 +1,34 @@
---
title: MOC — Graphique
slug: MOC-Graphique
type: moc
source: vault/generated
domains: [graphique]
tags: [moc]
aliases:
- moc — graphique
- moc graphique
- moc-graphique
- moc_graphique
- rendu
- raycasting
- minilibx
- graphisme
- 2d
- game dev
answers:
- Quels projets en graphique ?
- Que fait-il en graphique ?
priority: 7
linked:
updated: 2026-04-22
visibility: public
---
Notes du domaine *graphique* (2 au total).
## Notes liées
- [[cub3d|cub3d]] — _domotique, ecole-42, graphique, reseau_
- [[fract-ol|fract-ol]] — _algorithmique, domotique, ecole-42, graphique, reseau, systeme_

View File

@ -0,0 +1,36 @@
---
title: MOC — Ia
slug: MOC-Ia
type: moc
source: vault/generated
domains: [ia]
tags: [moc]
aliases:
- moc — ia
- moc ia
- moc-ia
- moc_ia
- ia
- intelligence artificielle
- llm
- llms
- modèles de langage
- chatbot
- chatbots
- machine learning
answers:
- Quels projets en ia ?
- Que fait-il en ia ?
priority: 7
linked:
updated: 2026-04-22
visibility: public
---
Notes du domaine *ia* (2 au total).
## Notes liées
- [[ia|Mon Exploration et Maîtrise de lIntelligence Artificielle]] — _algorithmique, ecole-42, ia_
- [[competence|Mon expérience dans la domotique]] — _algorithmique, domotique, ia, reseau_

View File

@ -0,0 +1,27 @@
---
title: MOC — Parcours
slug: MOC-Parcours
type: moc
source: vault/generated
domains: []
tags: [moc]
aliases:
- moc — parcours
- moc parcours
- moc-parcours
- moc_parcours
answers:
- Quels projets en Parcours ?
- Que fait-il en Parcours ?
priority: 7
linked:
updated: 2026-04-22
visibility: public
---
Parcours atypique de Fernand Gras-Calvet, du CV aux projets.
## Notes liées
*Aucune note pour l'instant.*

View File

@ -0,0 +1,43 @@
---
title: MOC — Projets
slug: MOC-Projets
type: moc
source: vault/generated
domains: []
tags: [moc]
aliases:
- moc — projets
- moc projets
- moc-projets
- moc_projets
answers:
- Quels projets en Projets ?
- Que fait-il en Projets ?
priority: 7
linked:
updated: 2026-04-22
visibility: public
---
Hub des projets de Fernand Gras-Calvet, triés par titre.
## Notes liées
- [[born2beroot|Born2beroot]] — _algorithmique, ecole-42, reseau, securite_
- [[cpp-partie1|cpp-partie1]] — _algorithmique, c, cpp, ecole-42, reseau_
- [[cpp-partie2|cpp-partie2]] — _algorithmique, c, cpp, devops, domotique, ecole-42, reseau_
- [[cub3d|cub3d]] — _domotique, ecole-42, graphique, reseau_
- [[fract-ol|fract-ol]] — _algorithmique, domotique, ecole-42, graphique, reseau, systeme_
- [[ft-irc|ft-irc]] — _cpp, ecole-42, reseau, systeme_
- [[ft-printf|Ft-printf]] — _c, ecole-42, reseau_
- [[ft-transcendence|ft_transcendence]] — _algorithmique, devops, ecole-42, reseau, web_
- [[get-next-line|Get_next_line]] — _c, ecole-42, reseau_
- [[inception|inception]] — _algorithmique, devops, ecole-42, reseau, systeme_
- [[libft|libft]] — _algorithmique, c, domotique, ecole-42, reseau_
- [[minishell|minishell]] — _algorithmique, domotique, ecole-42, reseau, systeme_
- [[minitalk|minitalk]] — _c, ecole-42, reseau, systeme_
- [[netpractice|netpractice]] — _algorithmique, ecole-42, reseau_
- [[philosopher|philosopher]] — _c, ecole-42, reseau, systeme_
- [[presentation-ecole-42|Présentation école 42]] — _ecole-42, reseau, systeme_
- [[push-swap|push_swap]] — _algorithmique, ecole-42, reseau_

View File

@ -0,0 +1,51 @@
---
title: MOC — Reseau
slug: MOC-Reseau
type: moc
source: vault/generated
domains: [reseau]
tags: [moc]
aliases:
- moc — reseau
- moc reseau
- moc-reseau
- moc_reseau
- réseau
- tcp
- ip
- sockets
- routage
answers:
- Quels projets en reseau ?
- Que fait-il en reseau ?
priority: 7
linked:
updated: 2026-04-22
visibility: public
---
Notes du domaine *reseau* (20 au total).
## Notes liées
- [[born2beroot|Born2beroot]] — _algorithmique, ecole-42, reseau, securite_
- [[cpp-partie1|cpp-partie1]] — _algorithmique, c, cpp, ecole-42, reseau_
- [[cpp-partie2|cpp-partie2]] — _algorithmique, c, cpp, devops, domotique, ecole-42, reseau_
- [[cub3d|cub3d]] — _domotique, ecole-42, graphique, reseau_
- [[developpement-web-and-hebergement-sur-serveur-windows|Développement Web & Hébergement sur serveur Windows]] — _reseau, securite, web_
- [[fract-ol|fract-ol]] — _algorithmique, domotique, ecole-42, graphique, reseau, systeme_
- [[ft-irc|ft-irc]] — _cpp, ecole-42, reseau, systeme_
- [[ft-printf|Ft-printf]] — _c, ecole-42, reseau_
- [[ft-transcendence|ft_transcendence]] — _algorithmique, devops, ecole-42, reseau, web_
- [[get-next-line|Get_next_line]] — _c, ecole-42, reseau_
- [[inception|inception]] — _algorithmique, devops, ecole-42, reseau, systeme_
- [[libft|libft]] — _algorithmique, c, domotique, ecole-42, reseau_
- [[minishell|minishell]] — _algorithmique, domotique, ecole-42, reseau, systeme_
- [[minitalk|minitalk]] — _c, ecole-42, reseau, systeme_
- [[competence|Mon expérience dans la domotique]] — _algorithmique, domotique, ia, reseau_
- [[impression-3d|Mon parcours dans limpression 3D]] — _3d, algorithmique, reseau_
- [[netpractice|netpractice]] — _algorithmique, ecole-42, reseau_
- [[philosopher|philosopher]] — _c, ecole-42, reseau, systeme_
- [[presentation-ecole-42|Présentation école 42]] — _ecole-42, reseau, systeme_
- [[push-swap|push_swap]] — _algorithmique, ecole-42, reseau_

View File

@ -0,0 +1,32 @@
---
title: MOC — Securite
slug: MOC-Securite
type: moc
source: vault/generated
domains: [securite]
tags: [moc]
aliases:
- moc — securite
- moc securite
- moc-securite
- moc_securite
- sécurité
- hardening
- cybersécurité
- audit
answers:
- Quels projets en securite ?
- Que fait-il en securite ?
priority: 7
linked:
updated: 2026-04-22
visibility: public
---
Notes du domaine *securite* (2 au total).
## Notes liées
- [[born2beroot|Born2beroot]] — _algorithmique, ecole-42, reseau, securite_
- [[developpement-web-and-hebergement-sur-serveur-windows|Développement Web & Hébergement sur serveur Windows]] — _reseau, securite, web_

View File

@ -0,0 +1,38 @@
---
title: MOC — Systeme
slug: MOC-Systeme
type: moc
source: vault/generated
domains: [systeme]
tags: [moc]
aliases:
- moc — systeme
- moc systeme
- moc-systeme
- moc_systeme
- système
- unix
- linux
- processus
- threads
answers:
- Quels projets en systeme ?
- Que fait-il en systeme ?
priority: 7
linked:
updated: 2026-04-22
visibility: public
---
Notes du domaine *systeme* (7 au total).
## Notes liées
- [[fract-ol|fract-ol]] — _algorithmique, domotique, ecole-42, graphique, reseau, systeme_
- [[ft-irc|ft-irc]] — _cpp, ecole-42, reseau, systeme_
- [[inception|inception]] — _algorithmique, devops, ecole-42, reseau, systeme_
- [[minishell|minishell]] — _algorithmique, domotique, ecole-42, reseau, systeme_
- [[minitalk|minitalk]] — _c, ecole-42, reseau, systeme_
- [[philosopher|philosopher]] — _c, ecole-42, reseau, systeme_
- [[presentation-ecole-42|Présentation école 42]] — _ecole-42, reseau, systeme_

View File

@ -0,0 +1,40 @@
---
title: "MOC — Technique"
slug: MOC-Technique
type: moc
source: manual
domains: [ia, web, devops]
tags: [moc]
aliases:
- technique
- architecture
- stack
- fonctionnement interne
- comment ca marche
answers:
- "Comment fonctionne ce site techniquement ?"
- "Quelle est l'architecture ?"
- "Comment est fait GrasBot ?"
priority: 7
linked:
- "[[architecture-site]]"
- "[[grasbot-retrieval]]"
- "[[vault-structure]]"
updated: 2026-04-22
visibility: public
---
# MOC — Technique
Hub de l'auto-documentation du chatbot GrasBot et de l'architecture du
site. Ces notes permettent au chatbot de répondre aux questions sur son
propre fonctionnement (*« comment es-tu fait ? »*, *« quel modèle
utilises-tu ? »*).
## Notes liées
- [[architecture-site]] — vue d'ensemble Next.js + Strapi + FastAPI/Ollama.
- [[grasbot-retrieval]] — pipeline de recherche (graph + BM25, sans
embeddings).
- [[vault-structure]] — organisation du vault Obsidian, frontmatter,
taxonomie, règles de régénération.

View File

@ -0,0 +1,33 @@
---
title: MOC — Web
slug: MOC-Web
type: moc
source: vault/generated
domains: [web]
tags: [moc]
aliases:
- moc — web
- moc web
- moc-web
- moc_web
- développement web
- site web
- frontend
- backend
- full stack
answers:
- Quels projets en web ?
- Que fait-il en web ?
priority: 7
linked:
updated: 2026-04-22
visibility: public
---
Notes du domaine *web* (2 au total).
## Notes liées
- [[developpement-web-and-hebergement-sur-serveur-windows|Développement Web & Hébergement sur serveur Windows]] — _reseau, securite, web_
- [[ft-transcendence|ft_transcendence]] — _algorithmique, devops, ecole-42, reseau, web_

View File

@ -0,0 +1,143 @@
---
title: Born2beroot
slug: born2beroot
type: projet
source: strapi/projects
domains: [algorithmique, ecole-42, reseau, securite]
tags: [tri]
aliases:
- born2beroot
- algo
- algorithme
- algorithmes
- complexité
- 42
- école 42
- 42 perpignan
- 42 paris
- piscine 42
- tronc commun
- réseau
answers:
- Parle-moi de Born2beroot
- "Qu'est-ce que Born2beroot ?"
- Comment fonctionne Born2beroot ?
priority: 5
linked:
- "[[MOC-Projets]]"
- "[[MOC-Ecole-42]]"
related:
- "[[cpp-partie1]]"
- "[[cpp-partie2]]"
- "[[fract-ol]]"
link: "https://github.com/Ladebeze66/born2beroot"
updated: 2026-04-22
visibility: public
---
**Slug :** `born2beroot`
**Lien GitHub :** [https://github.com/Ladebeze66/born2beroot](https://github.com/Ladebeze66/born2beroot)
---
## Description
Le projet Born2beroot de l'école 42 est une initiation à ladministration système sous Linux. L'objectif est de configurer un serveur sécurisé en installant une machine virtuelle sous Debian ou AlmaLinux, en mettant en place des politiques de sécurité strictes (gestion des utilisateurs, restrictions SSH, pare-feu UFW, Fail2ban) et en utilisant LVM pour la gestion des volumes de stockage. Les étudiants doivent également automatiser la surveillance du système avec un script monitoring.sh. Ce projet développe des compétences en sécurité informatique, gestion des serveurs et DevOps, essentielles pour les métiers dadministrateur système ou de cybersécurité.
## Détails du projet
Le projet Born2beroot de lécole 42 est un projet dinitiation à ladministration système, conçu pour familiariser les étudiants avec la gestion des serveurs Linux, la sécurisation du système, et les bonnes pratiques DevOps. Lobjectif est de comprendre comment un système fonctionne, dadopter les bonnes pratiques de sécurité et dautomatiser certaines tâches essentielles.
🏆 Objectifs Principaux du Projet
Ce projet vise à initier les étudiants à plusieurs concepts fondamentaux du sysadmin à travers une configuration minimale mais sécurisée d'un serveur basé sur Debian ou AlmaLinux.
1⃣ Création et Configuration dune Machine Virtuelle
Installation dun serveur sur une machine virtuelle (VirtualBox ou UTM selon lOS utilisé).
Utilisation dune image Debian (par défaut) ou AlmaLinux.
Apprentissage de la gestion dun serveur sans interface graphique.
2⃣ Gestion des Utilisateurs et Sécurisation du Système
Création et gestion des utilisateurs avec une structure bien définie.
Mise en place de règles de mot de passe strictes :
Expiration des mots de passe après un certain temps.
Interdiction de mots de passe trop faibles.
Rotation obligatoire des mots de passe.
Configuration de sudo et groupes restreints pour limiter les accès root.
Restriction des connexions SSH (pas de connexion en tant que root, utilisation de clés SSH).
3⃣ Renforcement de la Sécurité
Mise en place de UFW (Uncomplicated Firewall) pour filtrer le trafic réseau.
Installation et configuration de Fail2ban pour bloquer les tentatives de connexion frauduleuses.
Activation et gestion de SELinux ou AppArmor pour renforcer la sécurité du noyau.
Restriction des permissions et droits daccès pour éviter des failles potentielles.
4⃣ Gestion du Stockage avec LVM (Logical Volume Manager)
Partitionnement intelligent du disque avec LVM pour une gestion flexible de lespace disque.
Création et gestion de volumes logiques, permettant détendre le stockage facilement.
5⃣ Automatisation et Surveillance du Système
Écriture dun script de monitoring (monitoring.sh) affichant des informations essentielles :
Charge CPU
Utilisation mémoire et disque
Nombre dutilisateurs connectés
Journal des connexions SSH
Configuration de cron pour exécuter automatiquement des tâches répétitives.
Gestion des logs et journalisation des événements pour surveiller lactivité du serveur.
🚀 Livrables et Validation du Projet
Une machine virtuelle prête à lemploi avec tous les éléments configurés.
Un script monitoring.sh fonctionnel.
Une documentation claire expliquant les choix techniques et sécuritaires.
Une défense orale où létudiant devra expliquer et démontrer les configurations mises en place.
🎯 Compétences Développées
✔ Gestion des utilisateurs et permissions sur un système Linux.
✔ Configuration et administration dun serveur Debian.
✔ Mise en place de protocoles de sécurité pour un serveur en production.
✔ Automatisation et surveillance des services via des scripts shell.
✔ Maîtrise de LVM pour gérer dynamiquement lespace disque.
✔ Apprentissage des bases de DevOps et des bonnes pratiques dadministration système.
🔥 Pourquoi ce projet est important ?
Le projet Born2beroot prépare les étudiants à des postes en administration système et en cybersécurité. Il permet aussi de se familiariser avec les bases du DevOps, un domaine clé dans lindustrie informatique.
🎯 Conclusion
Born2beroot est un projet incontournable de l'école 42 qui permet aux étudiants de plonger dans l'administration système et la sécurisation dun serveur Linux. C'est une première étape essentielle pour ceux qui souhaitent s'orienter vers les métiers du DevOps, de la cybersécurité ou de l'administration système.
---
## Liens
- [[MOC-Projets]] — vue d'ensemble des projets
- [[MOC-Ecole-42]] — contexte pédagogique
- [[MOC-Algorithmique]] — domaine *algorithmique*
- [[MOC-Reseau]] — domaine *reseau*
- [[MOC-Securite]] — domaine *securite*

View File

@ -0,0 +1,83 @@
---
title: cpp-partie1
slug: cpp-partie1
type: projet
source: strapi/projects
domains: [algorithmique, c, cpp, ecole-42, reseau]
tags: [tri]
aliases:
- cpp partie1
- cpp-partie1
- cpp_partie1
- algo
- algorithme
- algorithmes
- complexité
- langage c
- ansi c
- c 42
- c++
- cpp 42
answers:
- Parle-moi de cpp-partie1
- "Qu'est-ce que cpp-partie1 ?"
- Comment fonctionne cpp-partie1 ?
priority: 5
linked:
- "[[MOC-Projets]]"
- "[[MOC-Ecole-42]]"
related:
- "[[cpp-partie2]]"
- "[[libft]]"
- "[[born2beroot]]"
link: "https://github.com/Ladebeze66/cpp-partie-1"
updated: 2026-04-22
visibility: public
---
**Slug :** `cpp-partie1`
**Lien GitHub :** [https://github.com/Ladebeze66/cpp-partie-1](https://github.com/Ladebeze66/cpp-partie-1)
---
## Description
Les modules CPP 00 à 04 de l'école 42 sont une introduction progressive au langage C++ et à la programmation orientée objet (POO). Ils couvrent les bases du C++ (classes, objets, fonctions membres, références, mémoire), la surcharge d'opérateurs, l'héritage, et le polymorphisme. Ces modules permettent d'acquérir une compréhension solide du modèle objet du C++, essentielle pour le développement logiciel et les projets avancés en POO.
## Détails du projet
Les modules CPP 00 à 04 de l'école 42 constituent une introduction progressive au langage C++ et à la programmation orientée objet (POO). Chaque module aborde des concepts clés du C++ pour fournir une compréhension solide des spécificités du langage par rapport au C.
🎯 Objectifs des Modules
Module 00 : Découverte des bases du C++, y compris les espaces de noms (namespaces), les classes, les fonctions membres, les flux d'entrée/sortie (stdio streams), les listes d'initialisation, ainsi que les mots-clés static et const.
Module 01 : Approfondissement de la gestion de la mémoire, des références, des pointeurs sur membres et de l'utilisation de l'instruction switch.
Module 02 : Introduction au polymorphisme ad hoc, à la surcharge des opérateurs et aux classes canoniques orthodoxes.
Module 03 : Étude de l'héritage en C++, permettant la création de hiérarchies de classes et la réutilisation du code.
Module 04 : Exploration du polymorphisme de sous-type, des classes abstraites et des interfaces, fondamentaux pour la conception de systèmes modulaires et extensibles.
🛠️ Approche Pédagogique
Chaque module est structuré pour introduire progressivement des concepts clés du C++ :
Lecture et Compréhension : Étudier les notions théoriques présentées dans le module.
Exercices Pratiques : Réaliser des exercices pour appliquer les concepts appris, tels que la création de classes, la gestion de la mémoire et l'implémentation de polymorphisme.
Projets d'Application : Développer des projets concrets qui intègrent plusieurs concepts, renforçant ainsi la compréhension et la maîtrise du langage.
Ces modules sont conçus pour fournir une base solide en C++ et en programmation orientée objet, préparant les étudiants à des projets plus complexes et à une compréhension approfondie du développement logiciel moderne.
---
## Liens
- [[MOC-Projets]] — vue d'ensemble des projets
- [[MOC-Ecole-42]] — contexte pédagogique
- [[MOC-Algorithmique]] — domaine *algorithmique*
- [[MOC-C]] — domaine *c*
- [[MOC-Cpp]] — domaine *cpp*
- [[MOC-Reseau]] — domaine *reseau*

View File

@ -0,0 +1,113 @@
---
title: cpp-partie2
slug: cpp-partie2
type: projet
source: strapi/projects
domains: [algorithmique, c, cpp, devops, domotique, ecole-42, reseau]
tags: [tri]
aliases:
- cpp partie2
- cpp-partie2
- cpp_partie2
- algo
- algorithme
- algorithmes
- complexité
- langage c
- ansi c
- c 42
- c++
- cpp 42
answers:
- Parle-moi de cpp-partie2
- "Qu'est-ce que cpp-partie2 ?"
- Comment fonctionne cpp-partie2 ?
priority: 5
linked:
- "[[MOC-Projets]]"
- "[[MOC-Ecole-42]]"
related:
- "[[cpp-partie1]]"
- "[[libft]]"
- "[[fract-ol]]"
link: "https://github.com/Ladebeze66/cpp-partie-2"
updated: 2026-04-22
visibility: public
---
**Slug :** `cpp-partie2`
**Lien GitHub :** [https://github.com/Ladebeze66/cpp-partie-2](https://github.com/Ladebeze66/cpp-partie-2)
---
## Description
Les modules CPP 04 à 09 de l'école 42 approfondissent les concepts avancés du C++, notamment le polymorphisme, la gestion des exceptions, les casts, les templates génériques, et l'utilisation de la STL (Standard Template Library). Ils permettent d'acquérir des compétences essentielles en programmation orientée objet, conception modulaire et optimisation du code, préparant les étudiants à des projets logiciels complexes en C++.
## Détails du projet
Les modules CPP 04 à 09 de l'école 42 approfondissent les concepts avancés du langage C++ et de la programmation orientée objet (POO). Chaque module est conçu pour renforcer la compréhension et la maîtrise des aspects spécifiques du C++, préparant les étudiants à des projets complexes et à une utilisation efficace du langage dans des applications réelles.
🎯 Objectifs des Modules
Module 04 : Approfondir le polymorphisme de sous-type, les classes abstraites et les interfaces, permettant une conception modulaire et extensible des applications.
Module 05 : Maîtriser la gestion des exceptions en C++, en utilisant les blocs try et catch pour gérer les erreurs de manière élégante et robuste.
Module 06 : Comprendre les différents types de cast en C++, tels que static_cast, dynamic_cast, const_cast et reinterpret_cast, pour effectuer des conversions de types en toute sécurité.
Module 07 : Explorer les templates en C++, permettant la création de fonctions et de classes génériques pour une réutilisabilité accrue du code.
Module 08 : Se familiariser avec les conteneurs de la Standard Template Library (STL), les itérateurs et les algorithmes, essentiels pour une manipulation efficace des collections de données.
Module 09 : Intégrer les connaissances précédemment acquises pour résoudre des problèmes complexes, en mettant l'accent sur l'utilisation avancée de la STL et des concepts modernes du C++.
🛠️ Spécifications Techniques
Langage de Programmation : C++.
Concepts Clés :
Polymorphisme et héritage.
Gestion des exceptions.
Conversions de types sécurisées.
Programmation générique avec les templates.
Utilisation avancée de la STL.
Prérequis : Connaissance des bases du C++ et des principes de la programmation orientée objet.
🔧 Approche d'Implémentation
Étude Théorique :
Lire et comprendre les concepts avancés du C++ présentés dans chaque module.
Exercices Pratiques :
Réaliser des exercices ciblés pour appliquer les concepts appris, tels que l'implémentation de classes abstraites, la gestion des exceptions, et l'utilisation des templates.
Projets d'Application :
Développer des projets concrets intégrant plusieurs concepts, comme la création de conteneurs personnalisés ou l'implémentation d'algorithmes génériques.
Revue de Code :
Analyser et optimiser le code écrit, en mettant l'accent sur les bonnes pratiques de programmation et l'efficacité.
Ces modules sont conçus pour fournir une compréhension approfondie des aspects avancés du C++, préparant les étudiants à des défis de programmation complexes et à une utilisation efficace du langage dans des projets réels. Ils mettent l'accent sur la conception modulaire, la gestion des erreurs, la programmation générique et l'utilisation efficace des bibliothèques standard, des compétences essentielles pour tout développeur C++ moderne. 🚀
---
## Liens
- [[MOC-Projets]] — vue d'ensemble des projets
- [[MOC-Ecole-42]] — contexte pédagogique
- [[MOC-Algorithmique]] — domaine *algorithmique*
- [[MOC-C]] — domaine *c*
- [[MOC-Cpp]] — domaine *cpp*
- [[MOC-Devops]] — domaine *devops*
- [[MOC-Domotique]] — domaine *domotique*
- [[MOC-Reseau]] — domaine *reseau*

View File

@ -0,0 +1,137 @@
---
title: cub3d
slug: cub3d
type: projet
source: strapi/projects
domains: [domotique, ecole-42, graphique, reseau]
tags: [42-tronc]
aliases:
- cub3d
- domotique
- home assistant
- iot
- smart home
- zigbee
- 42
- école 42
- 42 perpignan
- 42 paris
- piscine 42
- tronc commun
answers:
- Parle-moi de cub3d
- "Qu'est-ce que cub3d ?"
- Comment fonctionne cub3d ?
priority: 5
linked:
- "[[MOC-Projets]]"
- "[[MOC-Ecole-42]]"
related:
- "[[fract-ol]]"
- "[[cpp-partie2]]"
- "[[libft]]"
link: "https://github.com/Ladebeze66/cub3D"
updated: 2026-04-22
visibility: public
---
**Slug :** `cub3d`
**Lien GitHub :** [https://github.com/Ladebeze66/cub3D](https://github.com/Ladebeze66/cub3D)
---
## Description
Le projet Cub3D de l'école 42 consiste à coder un moteur graphique en C, inspiré de Wolfenstein 3D, utilisant la technique du raycasting pour afficher une vue en pseudo-3D à partir d'une carte 2D. Il intègre la gestion des mouvements du joueur, des textures, des redirections de caméra, et des événements clavier/souris via MiniLibX. Ce projet développe des compétences en programmation graphique, manipulation des fichiers de configuration et gestion des collisions
## Détails du projet
Le projet Cub3D de l'École 42 consiste à développer un moteur graphique en C, inspiré du jeu classique Wolfenstein 3D. L'objectif principal est d'implémenter une vue en trois dimensions à partir d'une carte en deux dimensions en utilisant la technique du raycasting. Ce projet permet aux étudiants de se familiariser avec les concepts fondamentaux de la programmation graphique et de la gestion des événements en temps réel.
🎯 Objectifs du Projet
Compréhension du Raycasting : Apprendre et implémenter la technique du raycasting pour simuler une perspective 3D à partir d'une carte 2D.
Manipulation de la MiniLibX : Utiliser la bibliothèque graphique minimaliste MiniLibX pour gérer l'affichage, les événements clavier et souris, ainsi que le rendu des images.
Gestion des Textures et des Couleurs : Appliquer des textures aux surfaces rendues et gérer les couleurs pour améliorer le réalisme de la scène.
Gestion des Collisions : Implémenter la détection des collisions pour empêcher le joueur de traverser les murs ou les obstacles.
Parsage de Fichiers de Configuration : Lire et interpréter des fichiers de configuration pour définir la carte du jeu, les positions de départ, les textures, etc.
🛠️ Spécifications Techniques
Langage de Programmation : C.
Bibliothèque Graphique : MiniLibX, une bibliothèque graphique simple fournie par l'École 42.
Fonctionnalités à Implémenter :
Affichage en 3D : Rendu en temps réel d'une scène 3D en utilisant le raycasting.
Mouvements du Joueur : Gestion des déplacements avant, arrière et latéraux, ainsi que la rotation de la vue.
Gestion des Textures : Application de textures sur les murs et autres surfaces.
Minicarte : Affichage d'une minicarte 2D pour représenter la position du joueur et la disposition de la carte.
Gestion des Événements : Réponse aux entrées clavier et souris pour le contrôle du joueur.
Parsage de la Carte : Lecture de fichiers de configuration pour générer la carte du jeu.
🔧 Approche d'Implémentation
Initialisation de la MiniLibX :
Configurer la fenêtre d'affichage et initialiser les paramètres nécessaires.
Parsage de la Carte :
Lire le fichier de configuration pour créer une représentation en mémoire de la carte, incluant les positions des murs, des espaces vides, et la position initiale du joueur.
Implémentation du Raycasting :
Calculer les intersections des rayons avec les murs pour déterminer les distances et les angles, afin de rendre la scène en 3D.
Gestion des Mouvements et des Collisions :
Mettre en place la logique pour déplacer le joueur tout en détectant et en empêchant les collisions avec les murs.
Application des Textures :
Charger les images des textures et les appliquer aux surfaces correspondantes lors du rendu.
Gestion des Événements :
Configurer les callbacks pour les entrées clavier et souris afin de permettre le contrôle du joueur.
Affichage de la Minicarte :
Dessiner une représentation 2D de la carte et de la position du joueur pour faciliter la navigation.
🧪 Tests et Validation
Tests Fonctionnels :
Vérifier que le rendu 3D est correct et que les murs apparaissent aux bonnes positions.
Tester les mouvements du joueur pour s'assurer qu'ils sont fluides et que les collisions sont correctement détectées.
Confirmer que les textures sont correctement appliquées aux surfaces correspondantes.
Tests de Performance :
Évaluer le taux de rafraîchissement pour s'assurer que le jeu fonctionne de manière fluide.
Optimiser le code pour réduire la charge processeur et améliorer les performances.
Tests d'Intégration :
S'assurer que tous les composants (parsing, rendu, gestion des entrées) fonctionnent ensemble de manière cohérente.
---
## Liens
- [[MOC-Projets]] — vue d'ensemble des projets
- [[MOC-Ecole-42]] — contexte pédagogique
- [[MOC-Domotique]] — domaine *domotique*
- [[MOC-Graphique]] — domaine *graphique*
- [[MOC-Reseau]] — domaine *reseau*

View File

@ -0,0 +1,140 @@
---
title: fract-ol
slug: fract-ol
type: projet
source: strapi/projects
domains: [algorithmique, domotique, ecole-42, graphique, reseau, systeme]
tags: [concurrence, tri]
aliases:
- fract ol
- fract-ol
- fract_ol
- algo
- algorithme
- algorithmes
- complexité
- domotique
- home assistant
- iot
- smart home
- zigbee
answers:
- Parle-moi de fract-ol
- "Qu'est-ce que fract-ol ?"
- Comment fonctionne fract-ol ?
priority: 5
linked:
- "[[MOC-Projets]]"
- "[[MOC-Ecole-42]]"
related:
- "[[minishell]]"
- "[[cpp-partie2]]"
- "[[cub3d]]"
link: "https://github.com/Ladebeze66/fractol"
updated: 2026-04-22
visibility: public
---
**Slug :** `fract-ol`
**Lien GitHub :** [https://github.com/Ladebeze66/fractol](https://github.com/Ladebeze66/fractol)
---
## Description
Le projet fract-ol de l'école 42 consiste à générer et afficher des fractales (Mandelbrot, Julia, etc.) en utilisant la bibliothèque graphique MiniLibX. Il permet dexplorer la programmation graphique en C, la manipulation des nombres complexes, et loptimisation des calculs pour le rendu dimages. Les utilisateurs peuvent interagir avec les fractales via le clavier et la souris pour zoomer, naviguer et modifier les paramètres en temps réel.
## Détails du projet
Le projet fract-ol de l'École 42 est l'un des premiers projets graphiques du cursus, conçu pour initier les étudiants à la programmation 2D en générant des fractales. Une fractale est une figure géométrique fragmentée qui se répète infiniment à différentes échelles. Ce projet utilise la bibliothèque graphique MiniLibX fournie par l'école.
🎯 Objectifs du Projet
Manipulation d'une Bibliothèque Graphique Bas-Niveau : Apprendre à utiliser MiniLibX pour créer des fenêtres, gérer les événements clavier et souris, et dessiner des images.
Compréhension des Nombres Complexes : Utiliser les nombres complexes pour calculer et représenter des ensembles fractals tels que Mandelbrot et Julia.
Optimisation des Performances : Améliorer l'efficacité du rendu graphique, notamment en explorant l'utilisation de threads pour le calcul parallèle.
🛠️ Spécifications Techniques
Fractales à Générer :
Ensemble de Mandelbrot : Défini par l'itération de la fonction
𝑧𝑛+1=𝑧𝑛2+𝑐z n+1=z n2+c, où 𝑧z et 𝑐c sont des nombres complexes.
Ensemble de Julia : Similaire à Mandelbrot, mais avec une constante 𝑐c fixe et des valeurs initiales 𝑧z variant selon les pixels.
Ensemble Burning Ship : Variante de Mandelbrot utilisant la valeur absolue des parties réelle et imaginaire de 𝑧z à chaque itération.
Fonctionnalités du Programme :
Zoom et Déplacement : Permettre à l'utilisateur de zoomer et de se déplacer dans la fractale pour explorer différents niveaux de détail.
Modification des Paramètres : Changer dynamiquement les paramètres de la fractale, comme les constantes complexes pour l'ensemble de Julia.
Changement de Palette de Couleurs : Offrir différentes palettes pour améliorer la visualisation des fractales.
Contrôles Utilisateur :
Souris : Zoom avant/arrière avec la molette, déplacement en cliquant et en faisant glisser.
Clavier : Touches pour déplacer la vue, ajuster le niveau de zoom, modifier les paramètres de la fractale et changer les couleurs.
🔧 Approche d'Implémentation
Initialisation de MiniLibX :
Créer une fenêtre et initialiser une image pour le rendu des fractales.
Calcul des Fractales :
Pour chaque pixel de l'image, convertir les coordonnées en un nombre complexe.
Appliquer l'itération de la fonction fractale correspondante.
Déterminer la couleur du pixel en fonction du nombre d'itérations avant que la valeur ne diverge au-delà d'un seuil fixé.
Gestion des Entrées Utilisateur :
Implémenter des gestionnaires d'événements pour les entrées clavier et souris afin de permettre l'interaction en temps réel avec la fractale.
Optimisation :
Utiliser des techniques telles que le calcul en parallèle avec des threads pour améliorer la performance du rendu, surtout lors de zooms profonds nécessitant plus d'itérations.
Bibliothèques :
MiniLibX : Bibliothèque graphique utilisée pour le rendu et la gestion des entrées.
pthread : Bibliothèque pour la gestion des threads, si l'optimisation parallèle est implémentée.
🧪 Tests et Validation
Tests Fonctionnels :
Vérifier que chaque type de fractale est correctement généré et affiché.
Tester les fonctionnalités d'interaction utilisateur, comme le zoom, le déplacement et la modification des paramètres.
Tests de Performance :
Évaluer le temps de rendu pour différentes tailles de fenêtre et profondeurs de zoom.
Tester l'efficacité des optimisations, notamment l'utilisation de threads pour le calcul parallèle.
Tests de Robustesse :
Assurer la stabilité du programme lors d'entrées utilisateur rapides ou inattendues.
Vérifier la gestion appropriée des erreurs, comme des valeurs de paramètres invalides.
---
## Liens
- [[MOC-Projets]] — vue d'ensemble des projets
- [[MOC-Ecole-42]] — contexte pédagogique
- [[MOC-Algorithmique]] — domaine *algorithmique*
- [[MOC-Domotique]] — domaine *domotique*
- [[MOC-Graphique]] — domaine *graphique*
- [[MOC-Reseau]] — domaine *reseau*
- [[MOC-Systeme]] — domaine *systeme*

View File

@ -0,0 +1,120 @@
---
title: ft-irc
slug: ft-irc
type: projet
source: strapi/projects
domains: [cpp, ecole-42, reseau, systeme]
tags: [concurrence]
aliases:
- ft irc
- ft-irc
- ft_irc
- c++
- cpp 42
- poo
- programmation orientée objet
- 42
- école 42
- 42 perpignan
- 42 paris
- piscine 42
answers:
- Parle-moi de ft-irc
- "Qu'est-ce que ft-irc ?"
- Comment fonctionne ft-irc ?
priority: 5
linked:
- "[[MOC-Projets]]"
- "[[MOC-Ecole-42]]"
related:
- "[[cpp-partie1]]"
- "[[cpp-partie2]]"
- "[[fract-ol]]"
link: "https://github.com/Ladebeze66/ft_irc"
updated: 2026-04-22
visibility: public
---
**Slug :** `ft-irc`
**Lien GitHub :** [https://github.com/Ladebeze66/ft_irc](https://github.com/Ladebeze66/ft_irc)
---
## Description
Le projet ft_irc de l'école 42 consiste à coder un serveur IRC (Internet Relay Chat) en C++, conforme à la RFC 2812. Il permet de gérer plusieurs connexions clients, d'implémenter des commandes IRC (JOIN, PART, PRIVMSG, NICK, etc.), de gérer des canaux de discussion, et dassurer lauthentification des utilisateurs. Ce projet développe des compétences en programmation réseau, gestion des sockets et protocoles de communication.
## Détails du projet
Le projet ft_irc de l'école 42 consiste à développer un serveur IRC (Internet Relay Chat) en C++, conforme à la spécification RFC 2812. L'objectif est de comprendre les mécanismes des sockets et de la communication réseau en temps réel.
🎯 Objectifs du Projet
Gestion des Connexions Client : Permettre à plusieurs clients de se connecter simultanément au serveur via des sockets.
Implémentation des Commandes IRC : Supporter les commandes essentielles telles que JOIN, PART, PRIVMSG, NICK, et USER.
Gestion des Canaux : Permettre la création, la gestion et la suppression de canaux de discussion, avec des fonctionnalités comme les modes de canal et la liste des utilisateurs.
Authentification des Utilisateurs : Gérer l'enregistrement et l'authentification des utilisateurs, y compris la gestion des pseudonymes (nicks) et des mots de passe.
Gestion des Messages Privés et de Groupe : Assurer la transmission de messages privés entre utilisateurs et de messages de groupe au sein des canaux.
🛠️ Spécifications Techniques
Langage de Programmation : C++.
Protocoles : Utilisation du protocole TCP pour les communications réseau, conformément à la spécification IRC.
Conformité RFC : Le serveur doit être conforme à la RFC 2812, qui définit le protocole IRC.
Gestion des Sockets : Utilisation des sockets pour gérer les connexions réseau entrantes et sortantes.
Multi-threading : Gestion des connexions multiples, soit par multi-threading, soit par une approche asynchrone, pour permettre à plusieurs clients de se connecter simultanément.
🔧 Approche d'Implémentation
Initialisation du Serveur :
Création d'un socket serveur et liaison à un port spécifié.
Mise en place de l'écoute des connexions entrantes.
Gestion des Connexions Client :
Acceptation des nouvelles connexions et création de structures pour gérer chaque client connecté.
Gestion des entrées/sorties pour chaque client, en assurant la réception et l'envoi de messages.
Parsage et Traitement des Commandes :
Analyse des messages reçus des clients pour identifier les commandes IRC.
Exécution des commandes appropriées, telles que la connexion à un canal (JOIN), l'envoi de messages (PRIVMSG), ou le changement de pseudonyme (NICK).
Gestion des Canaux :
Création et suppression de canaux en fonction des besoins.
Gestion des listes d'utilisateurs pour chaque canal et des modes de canal (par exemple, canaux privés, protégés par mot de passe).
Authentification et Gestion des Utilisateurs :
Vérification des informations d'identification des utilisateurs lors de la connexion.
Gestion des conflits de pseudonymes et assurance de l'unicité des noms d'utilisateur sur le serveur.
Envoi de Messages :
Routage des messages privés directement aux destinataires concernés.
Diffusion des messages de canal à tous les membres du canal concerné.
---
## Liens
- [[MOC-Projets]] — vue d'ensemble des projets
- [[MOC-Ecole-42]] — contexte pédagogique
- [[MOC-Cpp]] — domaine *cpp*
- [[MOC-Reseau]] — domaine *reseau*
- [[MOC-Systeme]] — domaine *systeme*

View File

@ -0,0 +1,130 @@
---
title: Ft-printf
slug: ft-printf
type: projet
source: strapi/projects
domains: [c, ecole-42, reseau]
tags: [makefile]
aliases:
- ft printf
- ft-printf
- ft_printf
- langage c
- ansi c
- c 42
- 42
- école 42
- 42 perpignan
- 42 paris
- piscine 42
- tronc commun
answers:
- Parle-moi de Ft-printf
- "Qu'est-ce que Ft-printf ?"
- Comment fonctionne Ft-printf ?
priority: 5
linked:
- "[[MOC-Projets]]"
- "[[MOC-Ecole-42]]"
related:
- "[[cpp-partie1]]"
- "[[cpp-partie2]]"
- "[[get-next-line]]"
link: "https://github.com/Ladebeze66/printf"
updated: 2026-04-22
visibility: public
---
**Slug :** `ft-printf`
**Lien GitHub :** [https://github.com/Ladebeze66/printf](https://github.com/Ladebeze66/printf)
---
## Description
Le projet ft_printf de l'école 42 consiste à reproduire la fonction printf du langage C. Il permet aux étudiants de comprendre la manipulation des chaînes de formatage, et l'affichage de différents types de données (%d, %s, %p, %x…). Lobjectif est dimplémenter une fonction efficace, sans utiliser printf, en travaillant directement avec write. Ce projet développe des compétences essentielles en programmation bas niveau, gestion mémoire et optimisation du code C.
## Détails du projet
Le projet ft_printf de l'École 42 consiste à recréer la fonction printf du langage C. Cette fonction permet d'afficher des chaînes de caractères formatées et est essentielle en programmation système et développement logiciel. Ce projet développe des compétences avancées en C, notamment la gestion des arguments variables, la manipulation des chaînes de formatage et lutilisation de fonctions bas niveau comme write.
🏆 Objectifs du Projet
Comprendre le fonctionnement de printf et ses spécificateurs.
Travailler avec les bases numériques (décimal, hexadécimal, etc.).
Optimiser la gestion de la mémoire et laffichage de caractères en C.
🛠️ Spécifications Techniques
Fonctionnalités Requises : ft_printf doit gérer les conversions suivantes :
%c → Caractère unique
%s → Chaîne de caractères
%p → Pointeur (adresse mémoire)
%d / %i → Entier signé
%u → Entier non signé
%x / %X → Hexadécimal (minuscule/majuscule)
%% → Affichage du symbole %
Gestion des Paramètres Variables :
Retour de la Fonction :
ft_printf doit retourner le nombre total de caractères affichés, comme la version standard.
🔧 Approche dImplémentation
Lecture de la Chaîne de Formatage → Identifier les spécificateurs (%).
Affichage des Caractères avec write → Pas de printf autorisé.
Retour du Nombre de Caractères Affichés → Compteur à incrémenter.
📂 Structure du Projet
ft_printf.c → Fonction principale et parsing des arguments.
ft_printf.h → Prototypes et #include nécessaires.
Fichiers auxiliaires :
ft_putchar_pf.c → Affiche un caractère.
ft_putstr_pf.c → Affiche une chaîne.
ft_putnbr_pf.c → Affiche un entier.
ft_puthex_pf.c → Affiche un nombre en hexadécimal.
ft_putptr_pf.c → Affiche une adresse mémoire.
Makefile → Automatisation de la compilation.
🧪 Tests et Validation
Comparaison avec printf standard.
Tests unitaires pour chaque spécificateur.
Gestion des cas limites : valeurs nulles, chaînes vides, grands nombres, etc.
🚀 Pourquoi ce projet est important ?
ft_printf permet de développer des compétences clés en C, en apprenant à manipuler des arguments
variadiques et en travaillant sur un projet bas niveau essentiel en programmation système et logicielle.
---
## Liens
- [[MOC-Projets]] — vue d'ensemble des projets
- [[MOC-Ecole-42]] — contexte pédagogique
- [[MOC-C]] — domaine *c*
- [[MOC-Reseau]] — domaine *reseau*

View File

@ -0,0 +1,173 @@
---
title: ft_transcendence
slug: ft-transcendence
type: projet
source: strapi/projects
domains: [algorithmique, devops, ecole-42, reseau, web]
tags: [docker, tri]
aliases:
- ft transcendence
- ft-transcendence
- ft_transcendence
- algo
- algorithme
- algorithmes
- complexité
- devops
- conteneurs
- ci/cd
- infrastructure
- 42
answers:
- Parle-moi de ft_transcendence
- "Qu'est-ce que ft_transcendence ?"
- Comment fonctionne ft_transcendence ?
priority: 5
linked:
- "[[MOC-Projets]]"
- "[[MOC-Ecole-42]]"
related:
- "[[cpp-partie2]]"
- "[[inception]]"
- "[[born2beroot]]"
link: "https://github.com/Ladebeze66/ft_transcendence"
updated: 2026-04-22
visibility: public
---
**Slug :** `ft-transcendence`
**Lien GitHub :** [https://github.com/Ladebeze66/ft_transcendence](https://github.com/Ladebeze66/ft_transcendence)
---
## Description
Le projet ft_transcendence de l'école 42 est une application web full-stack développée en équipe, combinant un jeu de Pong multijoueur et un chat en temps réel. Le frontend est conçu avec HTML, CSS et JavaScript, tandis que le backend repose sur Django et WebSockets pour gérer la logique du jeu et des interactions en temps réel. L'application est conteneurisée avec Docker, et utilise PostgreSQL pour la gestion des données. Vous avez principalement travaillé sur l'implémentation du chat en jeu, permettant aux joueurs de communiquer en direct, et l'infrastructure est surveillée via Elasticsearch et Kibana pour le monitoring.
## Détails du projet
Le projet ft_transcendence est un projet full-stack web développé en groupe, combinant un jeu multijoueur de Pong et un chat en temps réel. L'objectif est dimplémenter une application web interactive, en utilisant HTML, CSS et JavaScript pour le frontend, et Django avec WebSockets pour le backend. Le projet est conteneurisé avec Docker, permettant une gestion efficace des services et une infrastructure modulaire.
🎯 Objectifs du Projet
Développement Collaboratif : Travailler en groupe pour concevoir une application web robuste et modulaire.
Architecture Full-Stack : Séparer le backend (Django) et le frontend (HTML/CSS/JS) pour une meilleure organisation du code.
Chat en Temps Réel (votre contribution) : Implémenter un chat intégré au jeu, permettant aux joueurs de communiquer en direct via WebSockets.
Déploiement Conteneurisé : Utiliser Docker et Docker Compose pour faciliter lexécution et le déploiement du projet.
Sécurisation des Connexions : Intégrer une authentification OAuth et gérer laccès des utilisateurs.
🛠️ Technologies Utilisées
Frontend (Interface Utilisateur - HTML/CSS/JS) :
HTML5 : Structure des pages et affichage du jeu.
CSS3 : Design et mise en page pour une interface utilisateur fluide.
JavaScript : Interactions dynamiques, gestion des WebSockets pour le chat.
Backend (Serveur & APIs - Django) :
Django : Framework backend pour gérer les utilisateurs et la logique métier.
Django Channels & WebSockets : Gestion de la communication en temps réel pour le chat et le jeu.
PostgreSQL : Base de données pour stocker les informations des joueurs et du chat.
Infrastructure & Déploiement :
Docker & Docker Compose : Isolation des services et déploiement facilité.
Elasticsearch, Logstash, Kibana (ELK Stack) : Monitoring des logs du serveur et des connexions.
OAuth : Authentification sécurisée des utilisateurs via un service externe (Google, GitHub…).
🏓 Fonctionnalités Principales
✅ Jeu de Pong Multijoueur :
Match en temps réel entre joueurs.
Gestion des scores et classement des joueurs.
✅ Chat en Jeu (ma contribution) :
Communication instantanée entre joueurs via WebSockets.
Interface dynamique mise à jour sans rechargement de la page.
Gestion des utilisateurs connectés et des messages persistants.
✅ Authentification OAuth :
Connexion des utilisateurs via Google, GitHub ou une autre plateforme OAuth.
Gestion des profils et des permissions d'accès.
✅ Dashboard Joueurs & Matchs :
Interface affichant les statistiques des joueurs.
Historique des matchs et leaderboard.
✅ Déploiement & Monitoring :
Gestion des logs système via ELK Stack.
Conteneurisation avec Docker pour un environnement de développement homogène.
🔧 Approche dImplémentation
1⃣ Déploiement de lInfrastructure
Configuration des conteneurs Docker pour PostgreSQL, Django et les services de monitoring.
Création de la base de données avec PostgreSQL.
2⃣ Développement du Backend (Django)
Implémentation des modèles dutilisateurs et des scores.
Création des routes API pour gérer les connexions des joueurs et les parties de Pong.
Mise en place du système de chat en temps réel via Django Channels et WebSockets.
3⃣ Développement du Frontend (HTML/CSS/JS)
Création des fichiers HTML pour structurer linterface du jeu et du chat.
Intégration du CSS pour améliorer le design et rendre l'interface utilisateur attrayante.
Ajout de JavaScript pour :
Gérer le chat en direct via WebSockets.
Mettre à jour dynamiquement l'interface du jeu.
Afficher les scores et létat des joueurs connectés.
4⃣ Mise en Place du Système de Logs et Monitoring
Intégration de Logstash pour la collecte des logs.
Visualisation des événements système avec Kibana.
Surveillance des connexions utilisateurs via Elasticsearch.
🚀 Pourquoi ce projet est important ?
Le projet ft_transcendence est une expérience complète de développement web full-stack. Il permet dacquérir des compétences en travail collaboratif, en gestion des conteneurs avec Docker, en développement backend avec Django, et en interaction utilisateur en temps réel via WebSockets.
---
## Liens
- [[MOC-Projets]] — vue d'ensemble des projets
- [[MOC-Ecole-42]] — contexte pédagogique
- [[MOC-Algorithmique]] — domaine *algorithmique*
- [[MOC-Devops]] — domaine *devops*
- [[MOC-Reseau]] — domaine *reseau*
- [[MOC-Web]] — domaine *web*

View File

@ -0,0 +1,146 @@
---
title: Get_next_line
slug: get-next-line
type: projet
source: strapi/projects
domains: [c, ecole-42, reseau]
tags: [42-commun, makefile]
aliases:
- get next line
- get-next-line
- get_next_line
- langage c
- ansi c
- c 42
- 42
- école 42
- 42 perpignan
- 42 paris
- piscine 42
- tronc commun
answers:
- Parle-moi de Get_next_line
- "Qu'est-ce que Get_next_line ?"
- Comment fonctionne Get_next_line ?
priority: 5
linked:
- "[[MOC-Projets]]"
- "[[MOC-Ecole-42]]"
related:
- "[[cpp-partie1]]"
- "[[cpp-partie2]]"
- "[[ft-printf]]"
link: "https://github.com/Ladebeze66/getnextline"
updated: 2026-04-22
visibility: public
---
**Slug :** `get-next-line`
**Lien GitHub :** [https://github.com/Ladebeze66/getnextline](https://github.com/Ladebeze66/getnextline)
---
## Description
Le projet get_next_line de l'école 42 consiste à implémenter une fonction en C capable de lire une ligne à la fois depuis un descripteur de fichier, sans recharger tout le fichier en mémoire. Pour cela, il utilise une lecture par blocs (BUFFER_SIZE), des variables statiques pour conserver les données non traitées entre les appels, et une gestion efficace des descripteurs de fichiers multiples. Ce projet est essentiel pour apprendre la manipulation des fichiers en C, la gestion dynamique de la mémoire et l'optimisation des entrées/sorties
## Détails du projet
Le projet get_next_line de l'École 42 vise à développer une fonction en C capable de lire et de retourner une ligne complète depuis un descripteur de fichier, à chaque appel. Ce projet est essentiel pour comprendre la gestion des entrées/sorties en C, la manipulation des descripteurs de fichiers, et l'utilisation des variables statiques.
🎯 Objectifs du Projet
Lecture Ligne par Ligne : Implémenter une fonction get_next_line qui lit une ligne complète depuis un descripteur de fichier donné.
Gestion des Descripteurs de Fichiers : Apprendre à manipuler les descripteurs de fichiers pour lire des données depuis différentes sources, telles que des fichiers ou l'entrée standard.
Utilisation des Variables Statiques : Comprendre et utiliser les variables statiques pour conserver l'état entre les appels de fonction, notamment pour gérer les données restantes entre les lectures.
🛠️ Spécifications Techniques
Prototype de la Fonction :
char *get_next_line(int fd);
Comportement Attendu :
La fonction doit lire une ligne complète depuis le descripteur de fichier fd et la retourner.
Une ligne est définie par une séquence de caractères se terminant par un saut de ligne ('\n') ou par la fin du fichier (EOF).
La fonction doit gérer les descripteurs de fichiers multiples, en conservant l'état de lecture pour chacun.
Gestion de la Mémoire :
Allouer dynamiquement la mémoire nécessaire pour chaque ligne lue.
Assurer la libération appropriée de la mémoire allouée pour éviter les fuites de mémoire.
Variables Statiques :
Utiliser des variables statiques pour stocker les données restantes entre les appels de la fonction, permettant ainsi de gérer correctement les lectures partielles.
🔧 Approche d'Implémentation
Lecture par Blocs :
Lire le contenu du descripteur de fichier par blocs de taille définie (BUFFER_SIZE).
Concaténer les blocs lus jusqu'à ce qu'une ligne complète soit obtenue.
Gestion des Lignes :
Identifier la position du caractère de saut de ligne ('\n') pour délimiter la fin de la ligne.
Extraire la ligne complète et conserver le reste des données pour les appels suivants.
Utilisation des Variables Statiques :
Stocker les données restantes après chaque lecture dans une variable statique, afin de les utiliser lors des appels ultérieurs de la fonction pour le même descripteur de fichier.
Gestion des Erreurs :
Gérer les cas où la lecture échoue, où la mémoire ne peut pas être allouée, ou où le descripteur de fichier est invalide.
📂 Structure du Projet
Fichiers Principaux :
get_next_line.c : Contient l'implémentation de la fonction principale get_next_line.
get_next_line.h : Déclare le prototype de la fonction et les inclusions nécessaires.
get_next_line_utils.c : Contient les fonctions utilitaires utilisées par get_next_line (par exemple, fonctions de manipulation de chaînes).
Compilation :
Utiliser un Makefile pour automatiser la compilation du projet.
Définir la macro BUFFER_SIZE lors de la compilation pour spécifier la taille des blocs de lecture.
🧪 Tests et Validation
Cas de Test :
Lire des fichiers de différentes tailles, y compris des fichiers vides et de très grands fichiers.
Tester la lecture depuis l'entrée standard (stdin).
Gérer les fichiers contenant des lignes sans saut de ligne final.
Gestion des Descripteurs Multiples :
Assurer que la fonction peut gérer plusieurs descripteurs de fichiers simultanément, en maintenant l'état de lecture pour chacun.
Vérification des Fuites de Mémoire :
Utiliser des outils tels que Valgrind pour détecter et corriger les fuites de mémoire potentielles.
En réalisant le projet get_next_line, les étudiants de l'École 42 acquièrent une compréhension approfondie de la gestion des entrées/sorties en C, de la manipulation des descripteurs de fichiers, et de l'utilisation des variables statiques pour conserver l'état entre les appels de fonction. Ce projet est une étape cruciale pour développer des compétences en programmation système et en gestion efficace de la mémoire en C.
---
## Liens
- [[MOC-Projets]] — vue d'ensemble des projets
- [[MOC-Ecole-42]] — contexte pédagogique
- [[MOC-C]] — domaine *c*
- [[MOC-Reseau]] — domaine *reseau*

View File

@ -0,0 +1,147 @@
---
title: inception
slug: inception
type: projet
source: strapi/projects
domains: [algorithmique, devops, ecole-42, reseau, systeme]
tags: [42-tronc, docker, tri]
aliases:
- inception
- algo
- algorithme
- algorithmes
- complexité
- devops
- conteneurs
- ci/cd
- infrastructure
- 42
- école 42
- 42 perpignan
answers:
- Parle-moi de inception
- "Qu'est-ce que inception ?"
- Comment fonctionne inception ?
priority: 5
linked:
- "[[MOC-Projets]]"
- "[[MOC-Ecole-42]]"
related:
- "[[cpp-partie2]]"
- "[[fract-ol]]"
- "[[ft-transcendence]]"
link: "https://github.com/Ladebeze66/Inception"
updated: 2026-04-22
visibility: public
---
**Slug :** `inception`
**Lien GitHub :** [https://github.com/Ladebeze66/Inception](https://github.com/Ladebeze66/Inception)
---
## Description
Le projet Inception de l'école 42 consiste à déployer une infrastructure basée sur Docker et Docker Compose, en isolant plusieurs services dans des conteneurs distincts. Il inclut la configuration d'un serveur web (Nginx), d'une base de données (MariaDB/MySQL) et d'une application web (WordPress), tout en assurant la sécurisation, la persistance des données et lautomatisation du déploiement. Ce projet permet dacquérir des compétences en virtualisation, gestion de conteneurs et administration système.
## Détails du projet
Le projet Inception de l'école 42 vise à approfondir les compétences en administration système et en virtualisation en utilisant Docker et Docker Compose. L'objectif est de configurer une infrastructure de conteneurs pour héberger plusieurs services, en respectant les bonnes pratiques de sécurité et d'architecture.
🎯 Objectifs du Projet
Maîtrise de Docker : Apprendre à créer et gérer des conteneurs pour isoler des applications et leurs dépendances.
Utilisation de Docker Compose : Définir et orchestrer des applications multi-conteneurs pour faciliter le déploiement et la gestion des services.
Sécurisation des Services : Mettre en place des mesures de sécurité pour protéger les services hébergés, notamment en configurant correctement les pare-feux, les utilisateurs et les permissions.
Automatisation du Déploiement : Automatiser le déploiement des services pour assurer une infrastructure reproductible et évolutive.
🛠️ Spécifications Techniques
Services à Héberger :
Un serveur web (par exemple, Nginx) pour servir du contenu statique et dynamique.
Une base de données relationnelle (par exemple, MySQL ou PostgreSQL) pour stocker les données des applications.
Une application web (par exemple, WordPress) connectée à la base de données.
Configuration des Conteneurs :
Chaque service doit être isolé dans son propre conteneur Docker.
Les conteneurs doivent pouvoir communiquer entre eux via un réseau Docker dédié.
Les données persistantes doivent être stockées dans des volumes Docker pour assurer la persistance des données entre les redémarrages.
Fichier Docker Compose :
Définir un fichier docker-compose.yml pour décrire les services, les réseaux et les volumes nécessaires à l'application.
Configurer les dépendances entre les services pour assurer un démarrage correct de l'application.
Sécurité :
Mettre en place des règles de pare-feu pour limiter l'accès aux services sensibles.
Utiliser des variables d'environnement pour gérer les informations sensibles, telles que les mots de passe de la base de données.
Assurer la mise à jour régulière des images Docker pour inclure les derniers correctifs de sécurité.
🔧 Approche d'Implémentation
Installation de Docker et Docker Compose :
Installer Docker et Docker Compose sur le système hôte.
Vérifier que les installations fonctionnent correctement en exécutant des conteneurs de test.
Création des Dockerfiles :
Pour chaque service, créer un Dockerfile définissant l'environnement nécessaire et les étapes d'installation de l'application.
Optimiser les Dockerfile pour réduire la taille des images et améliorer les performances.
Définition du Fichier Docker Compose :
Écrire le fichier docker-compose.yml en spécifiant les services, les images à utiliser, les ports exposés, les volumes et les réseaux.
Configurer les dépendances entre les services pour assurer un ordre de démarrage correct.
Configuration des Réseaux et Volumes :
Définir des réseaux Docker pour permettre la communication sécurisée entre les conteneurs.
Configurer des volumes pour la persistance des données, notamment pour la base de données et les fichiers de l'application web.
Mise en Place des Mesures de Sécurité :
Restreindre les ports exposés aux seuls nécessaires et configurer des règles de pare-feu appropriées.
Mettre en place des utilisateurs non-root dans les conteneurs lorsque cela est possible.
Gérer les secrets et les variables d'environnement de manière sécurisée.
Tests et Validation :
Démarrer l'ensemble des services à l'aide de Docker Compose et vérifier leur bon fonctionnement.
Tester la communication entre les services, par exemple, vérifier que l'application web peut interagir avec la base de données.
Assurer la persistance des données en redémarrant les conteneurs et en vérifiant l'intégrité des données.
Documentation :
Documenter le processus d'installation, de configuration et de déploiement des services.
Fournir des instructions pour la maintenance et la mise à jour de l'infrastructure.
---
## Liens
- [[MOC-Projets]] — vue d'ensemble des projets
- [[MOC-Ecole-42]] — contexte pédagogique
- [[MOC-Algorithmique]] — domaine *algorithmique*
- [[MOC-Devops]] — domaine *devops*
- [[MOC-Reseau]] — domaine *reseau*
- [[MOC-Systeme]] — domaine *systeme*

View File

@ -0,0 +1,100 @@
---
title: libft
slug: libft
type: projet
source: strapi/projects
domains: [algorithmique, c, domotique, ecole-42, reseau]
tags: [42-commun, tri]
aliases:
- libft
- algo
- algorithme
- algorithmes
- complexité
- langage c
- ansi c
- c 42
- domotique
- home assistant
- iot
- smart home
answers:
- Parle-moi de libft
- "Qu'est-ce que libft ?"
- Comment fonctionne libft ?
priority: 5
linked:
- "[[MOC-Projets]]"
- "[[MOC-Ecole-42]]"
related:
- "[[cpp-partie2]]"
- "[[cpp-partie1]]"
- "[[fract-ol]]"
link: "https://github.com/Ladebeze66/libft"
updated: 2026-04-22
visibility: public
---
**Slug :** `libft`
**Lien GitHub :** [https://github.com/Ladebeze66/libft](https://github.com/Ladebeze66/libft)
---
## Description
Le projet Libft de l'École 42 consiste à recréer une bibliothèque standard en C, comprenant des fonctions essentielles de manipulation de chaînes, de gestion de mémoire et de structures de données comme les listes chaînées. Ce projet vise à renforcer la maîtrise du langage C, à développer des compétences en gestion de mémoire dynamique, et à produire une bibliothèque modulaire et réutilisable pour des projets futurs.
## Détails du projet
Le **projet Libft** de l'École 42 consiste à recréer une bibliothèque standard en langage C, en implémentant un ensemble de fonctions essentielles utilisées couramment en programmation. Ce projet a pour but de consolider les bases du langage C, dapprofondir la compréhension des mécanismes bas niveau, et de développer des compétences en gestion de mémoire, en manipulation de pointeurs, et en création de structures de données personnalisées.
**Objectifs pédagogiques :**
**. Reproduire des fonctions standard de la bibliothèque C** (<stdlib.h>, <string.h>, etc.).
**. Comprendre les mécanismes internes du langage C**(allocation dynamique, manipulation de chaînes de caractères, gestion des tableaux).
**. Développer une approche rigoureuse** pour écrire un code modulaire, lisible et bien documenté.
**.Apprendre à gérer des projets complexes** avec une attention particulière au debugging et aux tests unitaires.
****Compétences acquises :****
**.Programmation en C :** Implémentation de fonctions basiques comme strlen, strcpy, atoi, etc.
**.Création et manipulation de structures de données comme les listes chaînées** (linked lists).
**.Gestion de mémoire :** Utilisation de fonctions telles que malloc, free, pour la gestion dynamique.
**.Prévention des fuites de mémoire** grâce à des tests rigoureux.
**.Écriture d'une bibliothèque réutilisable :** Organisation et modularité du code source pour faciliter la réutilisation.
**.Compilation et création dun fichier binaire** (libft.a) utilisable dans dautres projets.
**Debugging et tests unitaires :**
Identification et résolution des erreurs de segmentation ou de comportement inattendu.
Mise en place de tests pour valider le bon fonctionnement de chaque fonction.
**Points forts à valoriser :**
**.Approche méthodique :** La rigueur dans la mise en œuvre des fonctions standard permet de garantir un code robuste et performant.
**.Code réutilisable :** La bibliothèque libft.a constitue une base solide qui peut être intégrée dans de nombreux projets futurs.
**.Polyvalence :** Ce projet démontre une capacité à travailler sur des fonctions diverses allant de la manipulation de chaînes à la gestion des structures de données.
**Impact professionnel :**
La réalisation du projet Libft atteste dune maîtrise des fondamentaux en développement logiciel, dune capacité à écrire du code performant et maintenable, et dun intérêt marqué pour les bases techniques nécessaires à tout projet informatique avancé. Cette expérience est un atout clé pour des postes impliquant du développement bas niveau, de loptimisation logicielle ou encore des systèmes embarqués.
---
## Liens
- [[MOC-Projets]] — vue d'ensemble des projets
- [[MOC-Ecole-42]] — contexte pédagogique
- [[MOC-Algorithmique]] — domaine *algorithmique*
- [[MOC-C]] — domaine *c*
- [[MOC-Domotique]] — domaine *domotique*
- [[MOC-Reseau]] — domaine *reseau*

View File

@ -0,0 +1,191 @@
---
title: minishell
slug: minishell
type: projet
source: strapi/projects
domains: [algorithmique, domotique, ecole-42, reseau, systeme]
tags: [42-tronc, tri]
aliases:
- minishell
- algo
- algorithme
- algorithmes
- complexité
- domotique
- home assistant
- iot
- smart home
- zigbee
- 42
- école 42
answers:
- Parle-moi de minishell
- "Qu'est-ce que minishell ?"
- Comment fonctionne minishell ?
priority: 5
linked:
- "[[MOC-Projets]]"
- "[[MOC-Ecole-42]]"
related:
- "[[fract-ol]]"
- "[[cpp-partie2]]"
- "[[inception]]"
link: "https://github.com/Ladebeze66/minishell"
updated: 2026-04-22
visibility: public
---
**Slug :** `minishell`
**Lien GitHub :** [https://github.com/Ladebeze66/minishell](https://github.com/Ladebeze66/minishell)
---
## Description
Le projet Minishell de l'école 42 consiste à coder un interpréteur de commandes minimaliste, inspiré de bash. Il permet dexécuter des commandes via un prompt interactif, en gérant les processus, les pipes (|), les redirections (<, >, >>, <<), et les signaux (Ctrl+C, Ctrl+D). Il inclut aussi des built-ins (echo, cd, pwd, export, env, unset, exit). Ce projet développe des compétences essentielles en programmation système, gestion de la mémoire et manipulation des processus sous Unix.
## Détails du projet
Le projet Minishell de l'École 42 consiste à développer un interpréteur de commandes minimaliste, inspiré de bash. Ce projet vise à familiariser les étudiants avec le fonctionnement interne des shells, en mettant l'accent sur le parsing, la gestion des processus, la synchronisation et la gestion des signaux.
🎯 Objectifs du Projet
Compréhension des Shells Unix : Apprendre le fonctionnement des shells, qui fournissent une interface en ligne de commande pour interagir avec le système.
Gestion des Processus : Mettre en œuvre la création, la synchronisation et la terminaison des processus pour exécuter des commandes utilisateur.
Gestion des Signaux : Manipuler les signaux pour gérer les interruptions et les commandes intégrées, telles que Ctrl+C pour interrompre un processus.
Implémentation des Redirections et des Pipes : Gérer les redirections d'entrée/sortie (<, >, >>) et les pipes (|) pour permettre la communication entre processus.
🛠️ Spécifications Techniques
Fonctionnalités à Implémenter :
Affichage d'un Prompt : Afficher un prompt personnalisé en attente des commandes de l'utilisateur.
Historique des Commandes : Maintenir un historique des commandes exécutées pour permettre la navigation et la réexécution.
Exécution des Commandes : Localiser et exécuter les exécutables en se basant sur la variable d'environnement PATH ou via un chemin absolu.
Gestion des Citations Simples et Doubles : Gérer les guillemets simples (') et doubles (") pour empêcher ou permettre l'interprétation des métacaractères.
Redirections :
Entrée (<) : Rediriger l'entrée standard depuis un fichier.
**Sortie (>) : Rediriger la sortie standard vers un fichier, en écrasant le contenu existant.
**Append (>>) : Rediriger la sortie standard vers un fichier, en ajoutant au contenu existant.
**Heredoc (<<) : Lire l'entrée jusqu'à un délimiteur spécifié, sans mettre à jour l'historique.
Pipes (|) : Connecter la sortie d'une commande à l'entrée d'une autre, permettant la création de pipelines.
Variables d'Environnement : Gérer l'expansion des variables d'environnement ($VARIABLE) et de la variable $? pour le statut de sortie de la dernière commande exécutée.
Gestion des Signaux :
Ctrl+C : Afficher un nouveau prompt sur une nouvelle ligne.
Ctrl+D : Quitter le shell.
Ctrl+\ : Ne rien faire.
Built-ins à Implémenter :
echo : Avec l'option -n pour supprimer le saut de ligne final.
cd : Changer le répertoire de travail actuel.
pwd : Afficher le répertoire de travail actuel.
export : Définir des variables d'environnement.
unset : Supprimer des variables d'environnement.
env : Afficher les variables d'environnement actuelles.
exit : Quitter le shell.
🔧 Approche d'Implémentation
Lecture de l'Entrée :
Utiliser la fonction readline pour afficher le prompt et lire l'entrée de l'utilisateur.
Ajouter les commandes saisies à l'historique à l'aide de add_history.
Analyse Lexicale (Lexer) :
Diviser l'entrée en tokens pour identifier les commandes, arguments, opérateurs, etc.
Analyse Syntaxique (Parser) :
Construire une structure de données représentant la commande et ses composants, en tenant compte de la priorité des opérateurs et des parenthèses.
Expansion :
Gérer l'expansion des variables d'environnement et le traitement des guillemets.
Exécution :
Implémenter les built-ins directement dans le shell.
Pour les autres commandes, utiliser fork pour créer un processus enfant et execve pour exécuter la commande.
Gérer les redirections et les pipes en ajustant les descripteurs de fichiers à l'aide de dup2.
Gestion des Signaux :
Configurer des gestionnaires de signaux pour intercepter Ctrl+C, Ctrl+D et Ctrl+\ et appliquer le comportement approprié.
Bibliothèques Utilisées :
readline : Pour la gestion du prompt et de lhistorique des commandes.
unistd.h : Pour les appels système (fork, execve, dup2).
signal.h : Pour la gestion des signaux.
stdlib.h et string.h : Pour la manipulation des chaînes et allocation dynamique.
🧪 Tests et Validation
Tests Fonctionnels :
Vérifier que chaque commande interne (cd, pwd, etc.) fonctionne correctement.
Vérifier la gestion des redirections (<, >, >>) et des pipes (|).
Vérifier lexpansion des variables ($USER, $HOME, etc.).
Assurer la bonne gestion des erreurs (commandes inconnues, fichiers inexistants, etc.).
Tests de Robustesse :
Exécuter le shell avec des entrées non valides pour observer le comportement.
Tester la gestion des signaux (Ctrl+C, Ctrl+D) pour éviter les comportements indésirables.
Vérifier la gestion de la mémoire avec valgrind pour éviter les fuites.
Tests de Performance :
Exécuter un grand nombre de commandes en boucle pour évaluer la stabilité.
Tester lexécution simultanée de plusieurs processus avec des pipes.
🚀 Pourquoi ce projet est important ?
Le projet Minishell est un exercice clé pour comprendre comment fonctionne un shell Unix. Il permet d'acquérir des compétences avancées en gestion des processus, redirections dentrée/sortie, gestion de la mémoire, et synchronisation des tâches. Ces compétences sont essentielles pour les développeurs systèmes, DevOps et ingénieurs en logiciels bas niveau. 🔥
---
## Liens
- [[MOC-Projets]] — vue d'ensemble des projets
- [[MOC-Ecole-42]] — contexte pédagogique
- [[MOC-Algorithmique]] — domaine *algorithmique*
- [[MOC-Domotique]] — domaine *domotique*
- [[MOC-Reseau]] — domaine *reseau*
- [[MOC-Systeme]] — domaine *systeme*

View File

@ -0,0 +1,141 @@
---
title: minitalk
slug: minitalk
type: projet
source: strapi/projects
domains: [c, ecole-42, reseau, systeme]
tags: [42-commun, makefile]
aliases:
- minitalk
- langage c
- ansi c
- c 42
- 42
- école 42
- 42 perpignan
- 42 paris
- piscine 42
- tronc commun
- réseau
- tcp
answers:
- Parle-moi de minitalk
- "Qu'est-ce que minitalk ?"
- Comment fonctionne minitalk ?
priority: 5
linked:
- "[[MOC-Projets]]"
- "[[MOC-Ecole-42]]"
related:
- "[[philosopher]]"
- "[[cpp-partie1]]"
- "[[cpp-partie2]]"
link: "https://github.com/Ladebeze66/minitalk"
updated: 2026-04-22
visibility: public
---
**Slug :** `minitalk`
**Lien GitHub :** [https://github.com/Ladebeze66/minitalk](https://github.com/Ladebeze66/minitalk)
---
## Description
Le projet Minitalk de l'école 42 consiste à établir une communication inter-processus (IPC) entre un serveur et un client en utilisant uniquement les signaux UNIX (SIGUSR1 et SIGUSR2). Le client envoie un message caractère par caractère sous forme binaire, tandis que le serveur le reçoit, le reconstruit et l'affiche. Ce projet permet dapprendre la gestion des signaux, la conversion binaire et la synchronisation des processus en C.
## Détails du projet
Le projet Minitalk de l'École 42 consiste à développer un programme de communication entre processus en utilisant les signaux UNIX. L'objectif est de créer un serveur capable de recevoir et d'afficher des messages envoyés par un client, en se servant exclusivement des signaux SIGUSR1 et SIGUSR2 pour transmettre les données.
🎯 Objectifs du Projet
Communication Inter-Processus (IPC) : Mettre en place une communication efficace entre deux processus distincts en utilisant les signaux UNIX.
Gestion des Signaux : Apprendre à manipuler et à gérer les signaux SIGUSR1 et SIGUSR2 pour transmettre des informations entre le client et le serveur.
Conversion des Données : Convertir les messages en une forme binaire afin de les transmettre bit par bit via les signaux, puis les reconstruire correctement du côté du serveur.
🛠️ Spécifications Techniques
Programmes à Développer :
Serveur :
Doit afficher son PID (Process ID) au lancement.
Attend de recevoir des messages du client et les affiche dès réception.
Client :
Prend en paramètres le PID du serveur et le message à envoyer.
Envoie le message au serveur en utilisant uniquement les signaux SIGUSR1 et SIGUSR2.
Contraintes :
Utilisation exclusive des signaux SIGUSR1 et SIGUSR2 pour la communication.
Gestion des erreurs, notamment la validation des PID et la vérification de la bonne réception des messages.
Respect des normes de codage de l'École 42.
🔧 Approche d'Implémentation
Initialisation du Serveur :
Le serveur démarre et affiche son PID, permettant au client de le cibler pour la communication.
Mise en place d'un gestionnaire de signaux pour traiter SIGUSR1 et SIGUSR2.
Envoi du Message par le Client :
Le client convertit chaque caractère du message en sa représentation binaire.
Pour chaque bit, le client envoie SIGUSR1 pour un bit à 0 et SIGUSR2 pour un bit à 1 au PID du serveur.
Réception et Reconstruction du Message par le Serveur :
Le serveur reçoit les signaux et reconstruit les caractères en assemblant les bits reçus.
Une fois le message complet, il l'affiche à l'écran.
Gestion des Cas Particuliers :
Assurer la synchronisation entre le client et le serveur pour éviter les pertes de données.
Gérer les interruptions et les erreurs potentielles lors de la transmission.
📂 Structure du Projet
Fichiers Principaux :
server.c : Contient le code du serveur, y compris l'initialisation, la gestion des signaux et l'affichage des messages reçus.
client.c : Contient le code du client, responsable de la conversion du message en signaux et de leur envoi au serveur.
Makefile : Automatise la compilation des programmes client et serveur.
Fonctions Autorisées :
malloc, free, write, getpid, signal, sigemptyset, sigaddset, sigaction, pause, kill, sleep, usleep, exit.
🧪 Tests et Validation
Tests Fonctionnels :
Vérifier que le client peut envoyer des messages de différentes longueurs au serveur.
Confirmer que le serveur affiche correctement les messages reçus.
Tests de Robustesse :
Tester la gestion des erreurs, comme l'envoi d'un message à un PID invalide.
Évaluer le comportement du système lors de l'envoi simultané de messages par plusieurs clients.
Tests de Performance :
Mesurer le temps de transmission pour des messages de grande taille.
Analyser l'utilisation des ressources système pendant la communication.
---
## Liens
- [[MOC-Projets]] — vue d'ensemble des projets
- [[MOC-Ecole-42]] — contexte pédagogique
- [[MOC-C]] — domaine *c*
- [[MOC-Reseau]] — domaine *reseau*
- [[MOC-Systeme]] — domaine *systeme*

View File

@ -0,0 +1,101 @@
---
title: netpractice
slug: netpractice
type: projet
source: strapi/projects
domains: [algorithmique, ecole-42, reseau]
tags: [42-tronc, tri]
aliases:
- netpractice
- algo
- algorithme
- algorithmes
- complexité
- 42
- école 42
- 42 perpignan
- 42 paris
- piscine 42
- tronc commun
- réseau
answers:
- Parle-moi de netpractice
- "Qu'est-ce que netpractice ?"
- Comment fonctionne netpractice ?
priority: 5
linked:
- "[[MOC-Projets]]"
- "[[MOC-Ecole-42]]"
related:
- "[[born2beroot]]"
- "[[cpp-partie1]]"
- "[[cpp-partie2]]"
link: "https://github.com/Ladebeze66/netpractice"
updated: 2026-04-22
visibility: public
---
**Slug :** `netpractice`
**Lien GitHub :** [https://github.com/Ladebeze66/netpractice](https://github.com/Ladebeze66/netpractice)
---
## Description
Le projet NetPractice est essentiel pour acquérir une compréhension pratique des réseaux informatiques, une compétence cruciale pour les administrateurs système et les ingénieurs réseau. Il offre une base solide pour des projets plus avancés impliquant la communication réseau et la gestion des infrastructures.
## Détails du projet
Le projet NetPractice de l'école 42 est conçu pour initier les étudiants aux concepts fondamentaux des réseaux informatiques, en particulier l'adressage TCP/IP. Il se compose de 10 exercices pratiques où les étudiants doivent configurer de petits réseaux pour assurer la communication entre différentes machines. L'objectif principal est de comprendre comment les adresses IP et les masques de sous-réseau déterminent la connectivité entre les dispositifs.
🎯 Objectifs du Projet
Compréhension de l'adressage IP : Apprendre à attribuer des adresses IP correctes aux appareils pour assurer une communication efficace.
Masques de sous-réseau (Subnet Masks) : Comprendre comment les masques de sous-réseau définissent les parties réseau et hôte d'une adresse IP.
Routage de base : Apprendre à configurer des tables de routage pour permettre la communication entre différents sous-réseaux.
Reconnaissance des adresses privées et publiques : Identifier les plages d'adresses IP réservées aux réseaux privés et comprendre leurs limitations en matière d'accès à Internet.
🛠️ Spécifications Techniques
Exercices Progressifs : Le projet est structuré en 10 niveaux, chacun présentant des défis croissants en complexité.
Configuration des Appareils : Les étudiants doivent attribuer des adresses IP, des masques de sous-réseau et configurer des tables de routage pour assurer la connectivité.
Outils Simulés : Utilisation d'environnements simulés pour pratiquer la configuration réseau sans matériel physique.
🔧 Approche d'Implémentation
Analyse du Réseau :
Pour chaque exercice, examiner la topologie du réseau fourni.
Identifier les segments de réseau et les appareils impliqués.
Attribution des Adresses IP :
Assigner des adresses IP uniques à chaque appareil, en veillant à ce qu'elles appartiennent au même sous-réseau pour les appareils devant communiquer directement.
Configuration des Masques de Sous-Réseau :
Déterminer le masque de sous-réseau approprié pour chaque segment de réseau afin de définir correctement les parties réseau et hôte des adresses IP.
Mise en Place des Tables de Routage :
Configurer les tables de routage sur les routeurs pour permettre la communication entre différents sous-réseaux.
Vérification de la Connectivité :
Tester la configuration en s'assurant que toutes les machines peuvent communiquer selon les spécifications de l'exercice.
Le projet NetPractice est essentiel pour acquérir une compréhension pratique des réseaux informatiques, une compétence cruciale pour les administrateurs système et les ingénieurs réseau. Il offre une base solide pour des projets plus avancés impliquant la communication réseau et la gestion des infrastructures.
---
## Liens
- [[MOC-Projets]] — vue d'ensemble des projets
- [[MOC-Ecole-42]] — contexte pédagogique
- [[MOC-Algorithmique]] — domaine *algorithmique*
- [[MOC-Reseau]] — domaine *reseau*

View File

@ -0,0 +1,123 @@
---
title: philosopher
slug: philosopher
type: projet
source: strapi/projects
domains: [c, ecole-42, reseau, systeme]
tags: [42-commun, concurrence]
aliases:
- philosopher
- langage c
- ansi c
- c 42
- 42
- école 42
- 42 perpignan
- 42 paris
- piscine 42
- tronc commun
- réseau
- tcp
answers:
- Parle-moi de philosopher
- "Qu'est-ce que philosopher ?"
- Comment fonctionne philosopher ?
priority: 5
linked:
- "[[MOC-Projets]]"
- "[[MOC-Ecole-42]]"
related:
- "[[minitalk]]"
- "[[cpp-partie1]]"
- "[[cpp-partie2]]"
link: "https://github.com/Ladebeze66/philosophers"
updated: 2026-04-22
visibility: public
---
**Slug :** `philosopher`
**Lien GitHub :** [https://github.com/Ladebeze66/philosophers](https://github.com/Ladebeze66/philosophers)
---
## Description
Le projet Philosopher de l'école 42 consiste à résoudre le problème des philosophes mangeurs, un exercice classique de programmation concurrente. Il met en œuvre des threads ou processus pour simuler des philosophes partageant des fourchettes et alternant entre les états manger, penser et dormir, tout en évitant les problèmes de deadlock et starvation. Ce projet permet dapprendre la gestion des threads (pthread), lutilisation des mutex et sémaphores, ainsi que la synchronisation des ressources partagées.
## Détails du projet
Le projet Philosopher de l'École 42 est une implémentation du célèbre problème des philosophes mangeurs (ou Dining Philosophers Problem), conçu pour introduire les étudiants aux concepts fondamentaux de la programmation concurrente. Ce problème illustre les défis liés à la synchronisation et à la gestion des ressources partagées entre processus ou threads.
🎯 Objectifs du Projet
Compréhension de la Programmation Concurrente : Apprendre à gérer l'exécution simultanée de plusieurs threads ou processus au sein d'un programme.
Gestion des Ressources Partagées : Mettre en place des mécanismes pour synchroniser l'accès à des ressources communes, évitant ainsi les conditions de course et les blocages.
Utilisation des Mutex et Sémaphores : Implémenter des solutions utilisant des mutex et des sémaphores pour contrôler l'accès aux ressources partagées et assurer la synchronisation entre threads ou processus.
🛠️ Spécifications Techniques (Suite)
Contraintes (Suite) :
Le programme doit éviter les situations de deadlock (blocage mutuel) où aucun philosophe ne peut progresser.
Il doit également prévenir les situations de starvation où un philosophe ne peut jamais accéder aux fourchettes nécessaires pour manger.
Les actions des philosophes (prendre une fourchette, manger, dormir, penser) doivent être affichées avec un horodatage pour suivre l'évolution de la simulation.
🔧 Approche d'Implémentation
1⃣ Gestion des Threads et Synchronisation
Chaque philosophe est représenté par un thread.
Les fourchettes sont partagées et représentées par des mutex (pour la version multi-threads) ou sémaphores (pour la version multi-processus).
Chaque philosophe tente dacquérir les deux fourchettes adjacentes avant de commencer à manger.
2⃣ Éviter les Problèmes de Concurrence
Pour éviter un deadlock, une approche classique consiste à :
Faire en sorte que le dernier philosophe prenne dabord la fourchette droite, puis la gauche (contrairement aux autres).
Utiliser un sémaphore global pour limiter le nombre de philosophes mangeant simultanément.
Pour éviter la starvation, on sassure quaucun philosophe ne reste bloqué indéfiniment sans accès aux fourchettes.
3⃣ Gestion des États et Horodatage
Chaque action est enregistrée avec un timestamp.
Un thread de surveillance peut être utilisé pour vérifier si un philosophe na pas mangé depuis trop longtemps et signaler sa mort si nécessaire.
🧪 Tests et Validation
Tests Fonctionnels :
Vérifier que les philosophes prennent correctement les fourchettes et alternent entre les états.
Observer si la simulation empêche les deadlocks et starvation.
Tests de Performance :
Exécuter avec un nombre élevé de philosophes pour tester la stabilité et lefficacité du programme.
Cas Limites :
Philosophe unique (peut-il manger ?).
Temps à mourir très court.
Vérification des performances avec un grand nombre de philosophes.
🚀 Pourquoi ce projet est important ?
Le projet Philosopher est une introduction essentielle aux problèmes de concurrence en informatique. Il enseigne la gestion des threads et processus, la synchronisation avec mutex et sémaphores, et loptimisation des ressources partagées. Ces concepts sont fondamentaux pour le développement système, les bases de données et le multithreading en programmation avancée.
---
## Liens
- [[MOC-Projets]] — vue d'ensemble des projets
- [[MOC-Ecole-42]] — contexte pédagogique
- [[MOC-C]] — domaine *c*
- [[MOC-Reseau]] — domaine *reseau*
- [[MOC-Systeme]] — domaine *systeme*

View File

@ -0,0 +1,74 @@
---
title: Présentation école 42
slug: presentation-ecole-42
type: projet
source: strapi/projects
domains: [ecole-42, reseau, systeme]
tags: [42-piscine]
aliases:
- presentation ecole 42
- presentation-ecole-42
- presentation_ecole_42
- présentation école 42
- 42
- école 42
- 42 perpignan
- 42 paris
- piscine 42
- tronc commun
- réseau
- tcp
answers:
- Parle-moi de Présentation école 42
- "Qu'est-ce que Présentation école 42 ?"
- Comment fonctionne Présentation école 42 ?
priority: 5
linked:
- "[[MOC-Projets]]"
- "[[MOC-Ecole-42]]"
related:
- "[[fract-ol]]"
- "[[ft-irc]]"
- "[[inception]]"
link: "https://42perpignan.fr/"
updated: 2026-04-22
visibility: public
---
**Slug :** `presentation-ecole-42`
**Lien GitHub :** [https://42perpignan.fr/](https://42perpignan.fr/)
---
## Description
L'École 42 Perpignan est un établissement d'enseignement en informatique, gratuit et ouvert à tous, basé sur une pédagogie innovante sans cours ni professeurs. Les étudiants y apprennent de manière collaborative et autonome à travers des projets pratiques, dans un environnement moderne et accessible 24h/24.
## Détails du projet
L'École 42 Perpignan est un établissement d'enseignement supérieur en informatique, reconnu pour son approche pédagogique innovante et collaborative.
Située dans le bâtiment emblématique des Dames de France, rue Pierre Curie à Perpignan, elle offre une formation gratuite, ouverte à tous dès 18 ans, sans exigence de diplôme préalable.
L'apprentissage y est basé sur des projets pratiques, favorisant l'autonomie et l'entraide entre étudiants. Le campus est accessible 24h/24 et 7j/7, disposant d'installations modernes pour soutenir les apprenants dans leur parcours.
Fondée en 2013, l'École 42 a rapidement gagné une réputation internationale pour sa méthode d'enseignement révolutionnaire, sans cours magistraux ni professeurs.
En février 2021, elle a inauguré son 46e campus à Perpignan, devenant ainsi l'un des six campus français aux côtés de Paris, Lyon, Nice, Mulhouse et Angoulême. Cette expansion vise à répondre à la demande croissante de professionnels qualifiés dans le domaine du numérique.
Le processus d'admission débute par une épreuve intensive appelée "la piscine", une immersion de 26 jours durant laquelle les candidats apprennent les bases du codage. Cette méthode permet d'évaluer leur motivation, leur capacité d'adaptation et leur esprit d'équipe. Le campus de Perpignan a accueilli sa deuxième promotion en octobre 2023, avec 113 étudiants âgés de 17 à 54 ans, issus de divers horizons.
Cependant, la parité reste un défi, avec seulement 10% de femmes dans cette cohorte. L'école s'efforce d'attirer davantage de profils féminins pour les prochaines sessions. L'École 42 Perpignan s'intègre dans un réseau mondial de campus, offrant aux étudiants des opportunités d'échanges et de collaborations internationales. Son modèle éducatif unique prépare les apprenants à relever les défis du secteur technologique, en mettant l'accent sur l'innovation, la créativité et la résolution de problèmes concrets.
Avec un taux d'employabilité de 100%, les diplômés sont très recherchés par les entreprises du numérique.
En choisissant l'École 42 Perpignan, les étudiants intègrent une communauté dynamique et engagée, prête à façonner l'avenir de la technologie.
---
## Liens
- [[MOC-Projets]] — vue d'ensemble des projets
- [[MOC-Ecole-42]] — contexte pédagogique
- [[MOC-Reseau]] — domaine *reseau*
- [[MOC-Systeme]] — domaine *systeme*

View File

@ -0,0 +1,160 @@
---
title: push_swap
slug: push-swap
type: projet
source: strapi/projects
domains: [algorithmique, ecole-42, reseau]
tags: [42-commun, makefile, tri]
aliases:
- push swap
- push-swap
- push_swap
- algo
- algorithme
- algorithmes
- complexité
- 42
- école 42
- 42 perpignan
- 42 paris
- piscine 42
answers:
- Parle-moi de push_swap
- "Qu'est-ce que push_swap ?"
- Comment fonctionne push_swap ?
priority: 5
linked:
- "[[MOC-Projets]]"
- "[[MOC-Ecole-42]]"
related:
- "[[born2beroot]]"
- "[[cpp-partie1]]"
- "[[cpp-partie2]]"
link: "https://github.com/Ladebeze66/pushswap"
updated: 2026-04-22
visibility: public
---
**Slug :** `push-swap`
**Lien GitHub :** [https://github.com/Ladebeze66/pushswap](https://github.com/Ladebeze66/pushswap)
---
## Description
Le projet push_swap de l'école 42 consiste à trier une liste d'entiers en utilisant uniquement un ensemble limité d'opérations sur deux piles (A et B). Lobjectif est de trouver lalgorithme de tri le plus efficace pour minimiser le nombre de mouvements. Ce projet permet dexplorer la gestion des structures de données (piles), loptimisation des algorithmes de tri, et la complexité algorithmique, tout en respectant des contraintes strictes dexécution.
## Détails du projet
Le projet push_swap de l'École 42 est conçu pour approfondir la compréhension des algorithmes de tri et des structures de données, en particulier les piles (stacks). Les étudiants doivent développer un programme en C capable de trier une liste d'entiers en utilisant un ensemble limité d'opérations sur deux piles, tout en minimisant le nombre de mouvements effectués.
🎯 Objectifs du Projet
Implémentation d'Algorithmes de Tri : Concevoir et implémenter des algorithmes efficaces pour trier des nombres en utilisant des piles.
Gestion des Piles : Manipuler deux piles nommées a et b pour réaliser le tri, en appliquant des opérations spécifiques.
Optimisation des Opérations : Minimiser le nombre total d'opérations nécessaires pour trier la liste initiale.
🛠️ Spécifications Techniques
Programme Principal : Un exécutable nommé push_swap qui prend en entrée une liste d'entiers non triés et affiche les opérations nécessaires pour les trier.
Opérations Autorisées :
sa (swap a): Échange les deux premiers éléments de la pile a.
sb (swap b): Échange les deux premiers éléments de la pile b.
ss: Effectue sa et sb simultanément.
pa (push a): Prend le premier élément de b et le place sur a.
pb (push b): Prend le premier élément de a et le place sur b.
ra (rotate a): Fait pivoter tous les éléments de a vers le haut (le premier devient le dernier).
rb (rotate b): Fait pivoter tous les éléments de b vers le haut.
rr: Effectue ra et rb simultanément.
rra (reverse rotate a): Fait pivoter tous les éléments de a vers le bas (le dernier devient le premier).
rrb (reverse rotate b): Fait pivoter tous les éléments de b vers le bas.
rrr: Effectue rra et rrb simultanément.
Contraintes :
Le programme doit gérer les erreurs d'entrée, telles que les arguments non numériques ou les doublons.
Aucune fonction de tri prédéfinie n'est autorisée.
Le nombre d'opérations doit être optimisé pour obtenir le meilleur score possible lors de l'évaluation.
🔧 Approche d'Implémentation
Analyse des Entrées :
Vérifier la validité des arguments fournis (nombres entiers, absence de doublons).
Initialiser les piles a et b en conséquence.
Choix de l'Algorithme de Tri :
Pour un petit nombre d'éléments (par exemple, 3 ou 5), utiliser des algorithmes simples comme le tri par sélection ou le tri à bulles.
Pour un plus grand nombre d'éléments, implémenter des algorithmes plus complexes, tels que le tri par insertion ou des variantes du tri rapide adaptées aux piles.
Optimisation des Opérations :
Analyser les séquences d'opérations pour identifier les redondances ou les mouvements inutiles.
Combiner des opérations lorsque cela est possible (par exemple, utiliser ss au lieu de sa suivi de sb).
Gestion de la Mémoire :
Assurer une allocation et une libération appropriées de la mémoire pour éviter les fuites.
Utiliser des structures de données appropriées pour représenter les piles et faciliter les opérations.
📂 Structure du Projet
Fichiers Principaux :
push_swap.c : Contient la fonction main et la logique générale du programme.
operations.c : Implémente les fonctions correspondant aux opérations autorisées (sa, pb, etc.).
sorting_algorithms.c : Contient les différentes stratégies de tri en fonction de la taille de la pile.
utils.c : Fonctions utilitaires pour la gestion des piles et la validation des entrées.
Fichiers d'En-tête :
push_swap.h : Déclare les prototypes de fonctions et les structures de données utilisées.
Compilation :
Utilisation d'un Makefile pour automatiser la compilation et gérer les dépendances.
🧪 Tests et Validation
Cas de Test :
Listes déjà triées, inversées, ou avec des motifs spécifiques.
Grandes listes générées aléatoirement pour évaluer les performances.
Outils de Test :
Scripts pour automatiser les tests et comparer les résultats avec des solutions de référence.
Utilisation d'outils de débogage et de profilage pour identifier les goulots d'étranglement et optimiser le code.
---
## Liens
- [[MOC-Projets]] — vue d'ensemble des projets
- [[MOC-Ecole-42]] — contexte pédagogique
- [[MOC-Algorithmique]] — domaine *algorithmique*
- [[MOC-Reseau]] — domaine *reseau*

View File

@ -0,0 +1,62 @@
---
title: Mon expérience dans la domotique
slug: competence
type: competence
source: strapi/competences
domains: [algorithmique, domotique, ia, reseau]
tags: [tri]
aliases:
- mon expérience dans la domotique
- competence
- algo
- algorithme
- algorithmes
- complexité
- domotique
- home assistant
- iot
- smart home
- zigbee
- ia
answers:
- Quelles sont ses compétences en algorithmique ?
- "A-t-il de l'expérience en algorithmique ?"
- Parle-moi de son expérience en algorithmique
priority: 7
linked:
- "[[MOC-Competences]]"
related:
- "[[cpp-partie2]]"
- "[[fract-ol]]"
- "[[libft]]"
updated: 2026-04-22
visibility: public
---
**Slug :** `competence`
**Ordre d'affichage :** 4
---
Depuis plusieurs années, la domotique a connu une évolution fulgurante, rendant accessible à tous des solutions permettant d'automatiser et d'optimiser son environnement quotidien. Fasciné par cet univers, jai entrepris dexplorer les différentes technologies permettant de connecter, centraliser et contrôler efficacement les objets intelligents au sein dun écosystème domestique unifié.
Convaincu que la domotique doit rester abordable et accessible, jai recherché des solutions qui allient simplicité dinstallation, compatibilité et flexibilité. Très rapidement, je me suis intéressé à lécosystème Tuya, qui offre une large gamme dappareils connectés tout en permettant une gestion centralisée. Bien que Tuya repose sur une architecture cloud, jai exploré des solutions open source permettant dintégrer et dinterconnecter ces appareils tout en optimisant lautonomie et la confidentialité des données.
Dans cette démarche, jai approfondi lutilisation de Home Assistant, une plateforme open-source puissante qui permet dagréger et de gérer un nombre considérable de dispositifs connectés. Lun de mes principaux objectifs a été de développer une solution de contrôle unifiée, permettant d'automatiser différents appareils et scénarios en fonction des besoins du quotidien (éclairage intelligent, gestion thermique, sécurité, capteurs environnementaux, etc.).
Jai également expérimenté les possibilités offertes par lauto-hébergement des serveurs domotiques, réduisant ainsi la dépendance aux plateformes cloud et garantissant une meilleure maîtrise des données personnelles. Lintégration de protocoles de communication ouverts tels que Zigbee, MQTT ou Matter ma permis dexplorer des alternatives plus résilientes, évolutives et personnalisables pour bâtir un environnement domotique intelligent, modulable et sécurisé.
Aujourdhui, je continue dapprofondir mes connaissances dans ce domaine en explorant les nouvelles générations dobjets connectés, loptimisation des scénarios dautomatisation avancés, ainsi que lintégration de solutions basées sur lintelligence artificielle afin daméliorer ladaptabilité et lefficacité des systèmes domotiques. Mon objectif est datteindre un équilibre entre simplicité dusage, interopérabilité et autonomie, tout en offrant une expérience fluide et intuitive pour un habitat véritablement intelligent et connecté.
---
*Cette compétence est illustrée par 12 images sur le site.*
---
## Liens
- [[MOC-Competences]] — vue d'ensemble des compétences
- [[MOC-Algorithmique]] — domaine *algorithmique*
- [[MOC-Domotique]] — domaine *domotique*
- [[MOC-Ia]] — domaine *ia*
- [[MOC-Reseau]] — domaine *reseau*

View File

@ -0,0 +1,155 @@
---
title: "Développement Web & Hébergement sur serveur Windows"
slug: developpement-web-and-hebergement-sur-serveur-windows
type: competence
source: strapi/competences
domains: [reseau, securite, web]
tags: []
aliases:
- developpement web and hebergement sur serveur windows
- developpement-web-and-hebergement-sur-serveur-windows
- developpement_web_and_hebergement_sur_serveur_windows
- "développement web & hébergement sur serveur windows"
- réseau
- tcp
- ip
- sockets
- routage
- sécurité
- hardening
- cybersécurité
answers:
- Quelles sont ses compétences en réseaux ?
- "A-t-il de l'expérience en réseaux ?"
- Parle-moi de son expérience en réseaux
priority: 7
linked:
- "[[MOC-Competences]]"
related:
- "[[born2beroot]]"
- "[[ft-transcendence]]"
- "[[cpp-partie1]]"
updated: 2026-04-22
visibility: public
---
**Slug :** `developpement-web-and-hebergement-sur-serveur-windows`
**Ordre d'affichage :** 2
---
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
Frontend (Client) :
Framework : Next.js (React, TypeScript, Server-Side Rendering & Static Generation)
Styling : Tailwind CSS
Gestion des requêtes API : Fetch API (avec qs pour structurer les requêtes)
SEO & Performance : Optimisation des images, pré-rendu des pages
Backend (Serveur) :
CMS : Strapi (Node.js, API REST)
Base de données : PostgreSQL ou MySQL
Hébergement : IIS sur Windows Server 2025
Sécurité : HTTPS activé via Win-ACME (Lets Encrypt)
Déploiement & Infrastructure :
Système dexploitation : Windows Server 2025
Serveur Web : IIS 10 (gestion des proxys et reverse proxy pour Next.js & Strapi)
Gestion des certificats SSL : Win-ACME pour le renouvellement automatique des certificats HTTPS
Monitoring : Logs IIS + Console Next.js & Strapi
🔹 Fonctionnalités du site
✅ Affichage dynamique des compétences (compétences récupérées via API Strapi)
✅ Glossaire interactif avec mots-clés détectés dynamiquement
✅ Carousel d'images pour présenter les compétences
✅ Navigation rapide et fluide grâce à Next.js
✅ SEO optimisé via les pages statiques et le rendu dynamique
Ce projet est toujours en développement, je l'agrémenterai de contenu au fil du temps.
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 dun 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 nutilise pas un service cloud public (Azure, AWS, GCP), il comporte des éléments liés à lautomatisation et à la gestion des déploiements.
Déploiement dune application Next.js & Strapi sur un serveur dédié
Gestion des certificats SSL automatisée (Win-ACME, Let's Encrypt)
Possibilité dextensions avec CI/CD pour automatiser les mises à jour
4⃣ Sécurité Informatique 🔒
Avec limplémentation du HTTPS, de lauthentification 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
---
*Cette compétence est illustrée par 3 images sur le site.*
---
## Liens
- [[MOC-Competences]] — vue d'ensemble des compétences
- [[MOC-Reseau]] — domaine *reseau*
- [[MOC-Securite]] — domaine *securite*
- [[MOC-Web]] — domaine *web*

View File

@ -0,0 +1,61 @@
---
title: Mon Exploration et Maîtrise de lIntelligence Artificielle
slug: ia
type: competence
source: strapi/competences
domains: [algorithmique, ecole-42, ia]
tags: [tri]
aliases:
- mon exploration et maîtrise de lintelligence artificielle
- ia
- intelligence artificielle
- llm
- llms
- modèles de langage
- chatbot
- chatbots
- machine learning
- deep learning
- data science
- ollama
answers:
- Quelles sont ses compétences en IA ?
- "A-t-il de l'expérience en IA ?"
- Parle-moi de son expérience en IA
priority: 7
linked:
- "[[MOC-Competences]]"
related:
- "[[born2beroot]]"
- "[[cpp-partie1]]"
- "[[cpp-partie2]]"
updated: 2026-04-22
visibility: public
---
**Slug :** `ia`
**Ordre d'affichage :** 1
---
Comme beaucoup, jai découvert lintelligence artificielle grand public avec larrivée de ChatGPT, qui a marqué un tournant décisif dans laccessibilité et la démocratisation de cette technologie. En lespace de quelques mois, le domaine a connu une expansion fulgurante, avec lémergence dune multitude de solutions exploitant lIA sous diverses formes. Fasciné par ces avancées, jai rapidement développé un vif intérêt pour plusieurs applications, notamment la génération dimages, les chatbots intelligents et plus largement les modèles de langage avancés (LLMs).
Dans cette quête dexploration, jai expérimenté des solutions dIA locale, notamment avec Ollama, LLM Studio, et dautres outils permettant une plus grande maîtrise et personnalisation des modèles. Mon objectif a été de comprendre en profondeur les capacités dintégration de ces intelligences artificielles locales, en explorant lentraînement de modèles personnalisés, loptimisation des performances et laffinement des interactions par lingénierie des prompts (cliquez sur IA locale test mistral 7b sur mon serveur).
Actuellement, je suis en phase dinstallation et de déploiement de solutions dIA locale sur mon propre serveur, un projet en cours de développement qui me permet dexpérimenter les configurations avancées et dadapter ces modèles à des cas dusage spécifiques. Cette démarche sinscrit dans une volonté de maîtriser lIA 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 à linfrastructure.
Parallèlement, jai entrepris une spécialisation en Data Science et Intelligence Artificielle au sein de lÉcole 42, afin dapprofondir mes connaissances théoriques et pratiques dans ce domaine en perpétuelle évolution. Cette formation me permet daller encore plus loin dans lanalyse des algorithmes de machine learning et deep learning, dexplorer 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.
Animé par une passion pour lintelligence artificielle et ses innombrables possibilités, je continue de minformer, dexpérimenter et dappliquer ces technologies à des projets concrets. Mon objectif est dacquérir une expertise approfondie pour concevoir des systèmes intelligents performants, adaptables et innovants, tout en restant à la pointe des avancées technologiques.
---
*Cette compétence est illustrée par 9 images sur le site.*
---
## Liens
- [[MOC-Competences]] — vue d'ensemble des compétences
- [[MOC-Algorithmique]] — domaine *algorithmique*
- [[MOC-Ecole-42]] — domaine *ecole-42*
- [[MOC-Ia]] — domaine *ia*

View File

@ -0,0 +1,63 @@
---
title: Mon parcours dans limpression 3D
slug: impression-3d
type: competence
source: strapi/competences
domains: [3d, algorithmique, reseau]
tags: [tri]
aliases:
- mon parcours dans limpression 3d
- impression 3d
- impression-3d
- impression_3d
- 3d printing
- fdm
- slicer
- prusa
- algo
- algorithme
- algorithmes
- complexité
answers:
- Quelles sont ses compétences en impression 3D ?
- "A-t-il de l'expérience en impression 3D ?"
- Parle-moi de son expérience en impression 3D
priority: 7
linked:
- "[[MOC-Competences]]"
related:
- "[[born2beroot]]"
- "[[cpp-partie1]]"
- "[[cpp-partie2]]"
updated: 2026-04-22
visibility: public
---
**Slug :** `impression-3d`
**Ordre d'affichage :** 3
---
Jai découvert lunivers fascinant de limpression 3D en 2018, ce qui a immédiatement éveillé ma curiosité pour cette technologie en pleine expansion. Désireux den apprendre davantage, jai entrepris mes premières expérimentations en impression FDM (Fused Deposition Modeling) en utilisant une imprimante Alfawise U30. Cette première immersion ma permis de me familiariser avec les fondamentaux de limpression 3D, notamment la calibration de la machine, la compréhension des paramètres dimpression et loptimisation des premiers prototypes.
En 2020, jai enrichi mon expérience en intégrant à mon parc une Sidewinder X2, une imprimante plus performante qui ma offert la possibilité dexplorer davantage les subtilités des différents slicers disponibles sur le marché, tels que Ultimaker Cura, PrusaSlicer et OrcaSlicer. Parallèlement, jai approfondi mes compétences en modélisation 3D en me formant à lutilisation de logiciels spécialisés, en particulier Fusion 360, qui constitue aujourdhui un outil incontournable dans mon flux de travail.
Lapprentissage des différents firmwares, notamment Marlin et Klipper, a constitué une étape essentielle de mon évolution. Jai ainsi acquis des compétences approfondies dans la configuration et loptimisation des paramètres machines, ce qui ma permis de mieux comprendre leur fonctionnement, dassurer leur maintenance et dintervenir efficacement en cas de dysfonctionnement.
Aujourdhui, mon expertise sest consolidée grâce à lutilisation dimprimantes plus avancées telles que la Bambu Lab X1C et la P1P, qui me permettent de réaliser des impressions de haute précision avec une grande diversité de matériaux. En effet, je maîtrise désormais limpression avec divers filaments techniques et composites, notamment le PLA, ABS, ASA, Nylon, TPU, ainsi que des impressions en multi-matériaux nécessitant des paramètres dimpression spécifiques.
Grâce à ces expériences, jai développé une parfaite connaissance des conditions optimales requises pour chaque matériau, telles que la gestion des températures, ladhérence au plateau, lhygrométrie, la ventilation ou encore la gestion du warping. Mon parcours ma ainsi permis dacquérir une solide autonomie dans lexploitation des imprimantes 3D, tant sur le plan technique que logiciel, et dapprofondir ma compréhension des défis liés à limpression de pièces complexes ou fonctionnelles.
Aujourdhui passionné par limpression 3D, je continue dexplorer les innovations du secteur, de perfectionner mes compétences et dexpérimenter de nouvelles approches afin doptimiser la qualité et la fiabilité des impressions.
---
*Cette compétence est illustrée par 8 images sur le site.*
---
## Liens
- [[MOC-Competences]] — vue d'ensemble des compétences
- [[MOC-3d]] — domaine *3d*
- [[MOC-Algorithmique]] — domaine *algorithmique*
- [[MOC-Reseau]] — domaine *reseau*

View File

@ -0,0 +1,165 @@
---
title: "CV — Fernand Gras-Calvet"
slug: cv-grascalvet-fernand
type: parcours
source: manual
domains: [parcours, ecole-42, ia, 3d, domotique]
tags: [cv, parcours, alternance, reconversion, data-ia]
aliases:
- Fernand
- Fernand Gras-Calvet
- Gras-Calvet
- Grascalvet
- cv
- curriculum vitae
- profil
- bio
- biographie
- qui est il
- alternance
- reconversion
answers:
- "Qui est Fernand Gras-Calvet ?"
- "Qui est Fernand ?"
- "Que peux-tu me dire sur Fernand ?"
- "Quel est son parcours ?"
- "Quel est son profil ?"
- "Cherche-t-il une alternance ?"
- "A-t-il de l'expérience professionnelle ?"
priority: 10
linked:
- "[[MOC-Parcours]]"
- "[[MOC-Ecole-42]]"
- "[[MOC-Ia]]"
related:
- "[[ia]]"
- "[[impression-3d]]"
- "[[competence]]"
- "[[developpement-web-and-hebergement-sur-serveur-windows]]"
updated: 2026-04-22
visibility: public
---
# CV — Fernand Gras-Calvet
> [!info] Source
> Version curatée du CV, structurée à la main depuis `nouveauCV_grascalvet.pdf`.
> Marquée `source: manual` : **ne sera pas écrasée** par un rebuild du vault.
## Identité
- **Nom** : Gras-Calvet Fernand
- **Âge** : 46 ans
- **Situation** : Étudiant en informatique, École 42 Perpignan
- **Objectif** : Alternance **Data / IA** (2 ans)
- **RQTH** : reconversion professionnelle suite à problèmes de santé
## Contact
- **Téléphone** : 06.12.01.01.72
- **Email** : grascalvet.fernand@gmail.com
- **Adresse** : 13 rue de Belfort, 66600 Rivesaltes
- **Site** : [fernandgrascalvet.com](https://fernandgrascalvet.com)
- **GitHub** : [Ladebeze66](https://github.com/Ladebeze66)
## Présentation
Ancien infirmier de 46 ans, actuellement étudiant en informatique à l'École 42
Perpignan. Je recherche une alternance de 2 ans pour me spécialiser dans
**l'automatisation agentique au sein des entreprises**, en y apportant mon
expérience sur le traitement de Data et les nouveaux process basés sur les LLM.
Mon parcours atypique reflète ma capacité d'adaptation, ma rigueur et mon
esprit d'équipe.
## Objectifs d'alternance
Mes objectifs de stage sont d'**approfondir mon expertise en programmation**,
tout en renforçant mes compétences en **travail collaboratif** et en
**gestion de projet**. Je souhaite également maîtriser des outils et
technologies avancés afin d'apporter une réelle valeur ajoutée aux équipes
avec lesquelles je travaillerai.
## Expérience professionnelle
### 2023 2025 · Étudiant École 42 Perpignan
Reconversion dans l'informatique, bénéficiant d'une RQTH. Validation du
tronc commun (voir [[MOC-Projets]] pour le détail des projets 42).
Stage en entreprise **logiciel métier spécialisée dans le béton** :
conception d'un **chatbot multi-agent** et automatisation (interfaçage)
entre les différents outils internes (support, commercial, CRM, etc.).
Cette expérience a consolidé mon intérêt pour l'[[ia|IA locale]] et
l'agentique.
### 2014 2023 · Infirmier Diplômé d'État
10 ans d'exercice en soins. Dernier poste : clinique **Supervaltech**
(Saint-Estève), service de **gériatrie**, de 2018 à 2023.
Compétences transférables au contexte informatique : rigueur clinique,
gestion du stress, documentation précise, travail en équipe pluridisciplinaire,
relation de service.
### 1999 2012 · Ostréiculture (Port-Leucate)
Plus de 12 ans dans l'**entreprise familiale à Leucate**, gestion
complète de l'exploitation. Expérience entrepreneuriale : gestion,
logistique, relations clients, saisonnalité.
## Compétences techniques
### Langages de programmation
- **Python** — expérience (principal pour l'IA et l'automatisation)
- **PHP** — expérience
- **HTML / CSS** — expérience
- **C** — École 42 (cf. [[libft]], [[get-next-line]], [[ft-printf]])
- **C++** — École 42 (cf. [[cpp-partie1]], [[cpp-partie2]], [[ft-irc]])
### IA & LLM
- **Ollama** — exécution de modèles en local
- **LLMs & VLMs open-source** — intégration, fine-tuning léger
- **LangChain** — orchestration d'agents
- **Serveurs MCP** — Model Context Protocol pour outillage agentique
- **API** — intégration d'APIs tierces et conception d'endpoints
### Systèmes & infrastructure
- **Windows, Linux** — administration quotidienne
- **VM, Server, Shell** — virtualisation et scripting
- **Docker** — conteneurisation (cf. [[inception]])
## Langues
- **Français** — langue maternelle
- **Anglais** — expérience
- **Espagnol** — expérience
## Intérêts techniques et ambitions professionnelles
### Hardware informatique
Passionné d'informatique depuis mon plus jeune âge, j'ai suivi l'évolution
des générations de PC et acquis une solide expertise en **assemblage,
maintenance et dépannage matériel**.
### Impression 3D
Également passionné par l'[[impression-3d|impression 3D]] : je possède
plusieurs machines et maîtrise des logiciels spécialisés :
- **Modélisation** : Fusion 360
- **Slicers** : PrusaSlicer, OrcaSlicer
- **Firmwares** : Klipper, Marlin
### Domotique
Notions en [[competence|domotique]], domaine que j'aimerais approfondir.
### Intelligence artificielle
Intérêt particulier pour l'[[ia|IA]], dans laquelle je souhaite me
spécialiser après mon stage à l'École 42.

View File

@ -0,0 +1,88 @@
---
title: "Architecture du site fernandgrascalvet.com"
slug: architecture-site
type: technique
source: manual
domains: [web, devops, ia]
tags: [architecture, nextjs, strapi, ollama]
aliases:
- architecture du site
- architecture technique
- stack technique
- comment est fait le site
- next.js strapi fastapi
answers:
- "Comment est fait ce site ?"
- "Quelle est la stack technique ?"
- "Avec quelles technologies le site est-il construit ?"
priority: 6
linked:
- "[[MOC-Technique]]"
- "[[developpement-web-and-hebergement-sur-serveur-windows]]"
- "[[ia]]"
- "[[grasbot-retrieval]]"
- "[[vault-structure]]"
updated: 2026-04-22
visibility: public
---
# Architecture du site fernandgrascalvet.com
Le site portfolio de Fernand Gras-Calvet est composé de trois briques
indépendantes, reliées par des API HTTP.
## Vue d'ensemble
```
┌─────────────────────┐ ┌────────────────────┐ ┌───────────────────────┐
│ Next.js 15 (front) │◄───▶│ Strapi 5 (CMS) │ │ FastAPI + Ollama │
│ App Router, TS │ │ Projets, Compét., │ │ GrasBot (graph+BM25) │
│ Tailwind + Stitch │ │ Homepage, Glossaire│ │ vault-grasbot/ │
└──────────┬──────────┘ └────────────────────┘ └──────────┬────────────┘
│ ▲
│ /api/proxy?q=... │
└──────────────────────────────────────────────────────┘
```
## Frontend — `app/`
- **Next.js 15** en mode App Router, Turbopack en dev.
- **TypeScript + JSX** mélangés (migration progressive vers TS).
- **Tailwind CSS 3.4** avec tokens Stitch « Digital Atelier » (voir
`tailwind.config.ts`) : palette `primary` indigo-ardoise, typographie
Manrope + Newsreader, radius `sheet` / `tile`, ombres `ambient` / `jewel`.
- **Pages principales** : `/` (hero + takeaways + démarche), `/portfolio`
(grille asymétrique 2/3 + 1/3), `/competences` (même pattern), fiches
détail `[slug]`, `/contact`.
- **Composant chat** : `ChatBot.js` + FAB flottant `GrasBotFab.tsx` monté
dans `layout.tsx` → accessible depuis toutes les pages.
## CMS — `cmsbackend/` (Strapi 5)
- Content-types : `homepage`, `project`, `competence`, `glossaire`, `message`.
- API REST : `https://api.fernandgrascalvet.com/api/<pluralName>`.
- Permissions `find` publique sur tous les types `draftAndPublish: true`.
## Chatbot — `llm-api/` + `vault-grasbot/` + Ollama
- Modèle chat : **Qwen3 8B** (quantif Q4_K_M) via Ollama local.
- **Pas d'embeddings** depuis la v3 (avril 2026) : la recherche se fait
sur le vault Obsidian directement, en **pur Python**.
- Pipeline : question → tokenisation → scoring multi-signaux (aliases,
titre/slug, answers, domaines, tags, BM25) → expansion par graphe
(wikilinks) → top-5 notes → prompt Qwen3 → réponse.
Voir [[grasbot-retrieval]] pour le détail complet.
## Extraction Strapi → vault — `strapi_extraction/`
Scripts Node + Python qui génèrent la base de connaissance du chatbot :
1. `extract-api-data.js` : fetch des endpoints Strapi → JSON brut (`extract/raw/`).
2. `clean-api-data.js` : nettoyage → JSON minimal (`extract/clean-data/`).
3. `generate-docs.js` : génération de Markdown (`docs/`).
4. `build-vault.py` : conversion `docs/` → vault Obsidian (`vault-grasbot/`)
avec frontmatter YAML enrichi (aliases, answers, priority), MOCs et
wikilinks.
Voir [[vault-structure]] pour la structure du vault.

View File

@ -0,0 +1,137 @@
---
title: "GrasBot — pipeline de retrieval (graph + BM25)"
slug: grasbot-retrieval
type: technique
source: manual
domains: [ia, web]
tags: [graph, bm25, ollama, qwen3, retrieval]
aliases:
- grasbot
- chatbot du site
- moteur de recherche
- retrieval
- graph + bm25
- comment fonctionne grasbot
answers:
- "Comment fonctionne GrasBot ?"
- "Comment le chatbot trouve-t-il ses réponses ?"
- "Quel modèle utilise GrasBot ?"
priority: 6
linked:
- "[[MOC-Ia]]"
- "[[architecture-site]]"
- "[[vault-structure]]"
- "[[ia]]"
updated: 2026-04-22
visibility: public
---
# GrasBot — pipeline de retrieval (graph + BM25)
GrasBot est l'assistant conversationnel du site fernandgrascalvet.com. Il
répond aux visiteurs en s'appuyant sur le vault Obsidian personnel
[[vault-structure]] et sur un LLM local qui tourne sur la machine de
Fernand.
**Version actuelle : 3.0** (avril 2026). Remplace l'ancien pipeline RAG
vectoriel (ChromaDB + embeddings Ollama) par une recherche **déterministe**
sur graphe de notes + BM25. Plus simple, plus précis pour un vault de cette
taille, zéro dépendance lourde.
## Matériel
- **GPU** : NVIDIA RTX 2080 Ti (11 Go VRAM).
- **Modèle chat** : Qwen3 8B en quantification Q4_K_M via Ollama
(≈ 5 Go VRAM), laissant une marge confortable pour le contexte.
- **Pas d'embeddings** : plus besoin de `nomic-embed-text` en VRAM. Le
retrieval tourne sur CPU en pur Python, ~50 ms par requête.
## Pipeline d'une question
```
question utilisateur
[ tokenize_fr ] ◄── stop-words FR, c++→cpp, split sur -/_
[ score_note pour chaque note ]
- alias match (+10)
- title / slug match (+8)
- answers overlap (+5 à +12)
- domains / tags match (+3 à +5 par hit)
- BM25 sur body (+0 à +5)
- priorité + MOC-hub (+0.3 à +1.3 si déjà scoré)
[ expand_by_graph ] ◄── voisins via linked / related / wikilinks
[ build_prompt ] ◄── notes entières en contexte (top-5)
[ Ollama /api/chat ] ◄── Qwen3 8B, temperature 0.4, keep_alive 30m
{ response: "...",
sources: [{slug, title, type, score, reasons, url}],
grounded: true|false,
model: "qwen3:8b",
vault_size: 41 }
```
## Implémentation
Deux modules Python dans `llm-api/` :
- **`api.py`** — FastAPI. Endpoints `GET /ask?q=...`, `GET /health`,
`POST /reload-vault`.
- **`search.py`** — primitives `load_vault`, `tokenize_fr`, `score_note`,
`expand_by_graph`, `search`, `build_prompt`, `generate`, `answer`.
Configuration via variables d'environnement (`LLM_MODEL`, `VAULT_DIR`,
`SEARCH_TOP_K`, `SEARCH_MIN_SCORE`…).
Le vault est chargé **une seule fois** par process FastAPI via
`@lru_cache` sur `load_vault()`. Après édition d'une note, l'endpoint
`POST /reload-vault` force la relecture sans redémarrer l'API.
## Prompt système
Qwen3 reçoit un prompt qui le force à :
1. Répondre en français, ton sobre, sans emojis.
2. Citer ses sources entre crochets (par slug : `[push-swap]`, `[ia]`).
3. Avouer quand l'info n'est pas dans les notes, orienter vers le site
(/portfolio, /competences, /contact).
4. Rester concis (3 à 6 phrases).
5. Réorienter poliment si la question est totalement hors-sujet.
Quand `grounded=false` (score max < `SEARCH_MIN_SCORE`), le prompt user
bascule en mode **sans contexte** : le LLM sait qu'il ne doit pas inventer
de faits sur Fernand.
## Compatibilité front
Le champ `response` de la réponse JSON est conservé. `ChatBot.js` et
`askAI.js` peuvent exploiter en plus :
- `sources[]` → vignettes cliquables (url `/portfolio/<slug>` ou
`/competences/<slug>` selon `type`).
- `grounded` → afficher un badge « basé sur X notes » ou « réponse
généraliste » selon la valeur.
## Limites connues
- **Pas de mémoire conversationnelle** : chaque question est indépendante.
Ajouter un historique court (3-4 derniers tours) passerait par
`ChatBot.js` (envoi du contexte) et `search.answer()` (intégration).
- **Pas de streaming** : la réponse arrive en un bloc après 2-10 s selon
la question. Passage à `stream: true` possible (modifier `generate()`
et exposer un endpoint SSE).
- **Taxonomie à entretenir à la main** : voir [[vault-structure]] et
`vault-grasbot/TAXONOMIE.md`. Les aliases/answers/priority générés
par `build-vault.py` sont une base, mais les notes stratégiques méritent
un enrichissement manuel (voir le CV [[cv-grascalvet-fernand]]).
- **Pas encore de hybrid scoring** : on pourrait composer avec un embedding
optionnel si le vault grossit > ~500 notes. Sur le périmètre actuel,
superflu.

View File

@ -0,0 +1,132 @@
---
title: "Structure du vault GrasBot"
slug: vault-structure
type: technique
source: manual
domains: [ia]
tags: [obsidian, vault, moc, taxonomie]
aliases:
- structure du vault
- organisation du vault
- vault obsidian
- structure de la base de connaissance
answers:
- "Comment est organisé le vault ?"
- "Quelle est la structure de la base de connaissance ?"
- "Où sont stockées les notes ?"
priority: 6
linked:
- "[[MOC-Ia]]"
- "[[architecture-site]]"
- "[[grasbot-retrieval]]"
updated: 2026-04-22
visibility: public
---
# Structure du vault GrasBot
Le vault `vault-grasbot/` est la base de connaissance du chatbot. Il est
conçu pour être ouvert directement dans Obsidian (comme vault séparé, ou
fusionné avec un vault perso existant) et pour alimenter le pipeline de
retrieval décrit dans [[grasbot-retrieval]].
## Arborescence
- **`00-MOC/`** — *Maps of Content*, hubs thématiques qui recensent les
notes d'un domaine. Générés automatiquement par `build-vault.py`.
- **`10-Projets/`** — une note par projet Strapi (École 42 +
projets perso). Source : `source: strapi/projects`.
- **`20-Competences/`** — une note par compétence Strapi (IA, Web,
Impression 3D, Domotique). Source : `source: strapi/competences`.
- **`30-Parcours/`** — CV, bio, étapes du parcours. La version curatée
`cv-grascalvet-fernand.md` est protégée (`source: manual`).
- **`40-Glossaire/`** — termes techniques référencés par les fiches
compétences (réservé pour quand le content-type `glossaire` sera
ajouté au script d'extraction).
- **`50-Technique/`** — auto-documentation : [[architecture-site]],
[[grasbot-retrieval]], [[vault-structure]] (cette note). Permet à
GrasBot de répondre aux questions *« comment es-tu fait ? »*.
- **`README.md`** — résumé utilisateur (généré).
- **`TAXONOMIE.md`** — vocabulaire contrôlé pour domaines, tags, aliases,
answers, priority. **À éditer manuellement** avant tout changement de
vocabulaire structurant.
## Frontmatter YAML
Chaque note porte une en-tête structurée :
```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 # public | private
---
```
### Signification des champs pour le retrieval
Ces champs sont **exploités directement** par `llm-api/search.py` :
| Champ | Usage |
|---|---|
| `aliases` | Synonymes forts (matchés avant tout). **+10 au score** par hit. |
| `answers` | Questions-types ; si overlap ≥ 3 tokens, **+12 au score**. |
| `domains` | Domaines contrôlés. **+5 par match strict** de token. |
| `tags` | Tags libres. **+3 par match strict**. |
| `priority` | 1-10. Boost léger (0.3 × (priority 5)) si déjà scoré. |
| `linked` / `related` | Voisins du graphe, activés par `expand_by_graph()`. |
| `visibility` | Si `private`, la note est **exclue** du retrieval. |
## Règles de régénération
`strapi_extraction/build-vault.py` **écrase** les notes dont le frontmatter
a `source: strapi/*` ou `source: pdf/*`. Il **ne touche jamais** aux notes
`source: manual`. Sécurité simple pour pouvoir enrichir le vault à la main
sans craindre qu'un rebuild efface le travail.
Le script génère automatiquement :
- `aliases` depuis le slug + le titre + les domaines (via `DOMAIN_ALIASES`).
- `answers` adaptés au type de note (projet, compétence, MOC, parcours).
- `priority` par défaut (5 pour projets, 7 pour MOCs/compétences, 10 pour CV).
Pour **enrichir davantage** une note stratégique (comme le CV ou la fiche IA),
passer son frontmatter en `source: manual` et ajouter à la main des aliases /
answers plus précis.
## Agrémentation avec un vault perso
Deux approches :
1. **Vault séparé (recommandé)** : on ouvre `vault-grasbot/` comme vault
Obsidian indépendant. Le chatbot n'indexe que ce qui s'y trouve ;
le vault perso reste privé.
2. **Fusion** : on copie `vault-grasbot/` comme sous-dossier d'un vault
existant. Les wikilinks restent valides tant que les noms sont uniques.
Attention à placer les notes perso avec `source: manual` (évite
l'écrasement) et `visibility: private` (exclue du retrieval automatique).
## Évolutions prévues
- Extraction du content-type `glossaire` Strapi dans `40-Glossaire/`.
- Fix du cleaner `homepages` dans `clean-api-data.js`.
- Enrichissement manuel progressif des aliases/answers pour les compétences
phares (IA, domotique, 3D) — la génération auto est une base, pas un
optimum.

65
vault-grasbot/README.md Normal file
View File

@ -0,0 +1,65 @@
# Vault GrasBot — Base de connaissances
Vault Obsidian généré par `strapi_extraction/build-vault.py` à partir des
contenus Strapi du site (projets + compétences) et du CV PDF. Alimente
directement le pipeline de recherche de GrasBot (`llm-api/search.py`) :
graph + BM25, sans embeddings.
**Dernière génération :** 2026-04-22
## Structure
- `00-MOC/` — Maps of Content (hubs thématiques)
- `10-Projets/` — 17 projets extraits de Strapi
- `20-Competences/` — 4 compétences extraites de Strapi
- `30-Parcours/` — Parcours personnel, CV, bio (version curatée `source: manual`)
- `40-Glossaire/` — Termes techniques (vide, à remplir manuellement ou depuis Strapi plus tard)
- `50-Technique/` — Auto-documentation (architecture, retrieval, vault)
- `TAXONOMIE.md` — Vocabulaire contrôlé (domaines, tags, aliases, answers, priority)
## Conventions
Chaque note porte un frontmatter YAML enrichi :
```yaml
---
title: ...
slug: ...
type: projet | competence | parcours | glossaire | moc | technique
source: strapi/... | pdf/... | manual | vault/generated
domains: [ia, web, systeme, ...] # taxonomie contrôlée
tags: [tag-1, tag-2]
aliases: # synonymes pour le retrieval
- "alias court"
- "autre formulation"
answers: # questions-types auxquelles répond la note
- "Question formulée naturellement ?"
priority: 5 # 1..10, boost léger au scoring
linked: ["[[MOC-...]]"] # voisins du graphe (sortants)
related: ["[[autre-note]]"]
updated: YYYY-MM-DD
visibility: public | private # `private` exclu du retrieval
---
```
Voir `TAXONOMIE.md` pour le vocabulaire contrôlé des domaines/tags et les
règles de rédaction des aliases/answers.
**Règle de régénération** : le script `build-vault.py` **écrase** sans prévenir
les notes dont le frontmatter a `source: strapi/*` ou `source: pdf/*`. Il ne
touche **jamais** aux notes `source: manual` que tu ajoutes toi-même. Les
aliases, answers et priority des notes générées sont calculés automatiquement
à partir du titre, du slug et des domaines ; les notes stratégiques méritent
un enrichissement manuel en passant `source: manual`.
## Fusion avec un vault personnel
Pour agrémenter ce vault avec ton vault Obsidian perso :
1. Copier `vault-grasbot/` dans ton vault existant comme sous-dossier, ou
2. Ouvrir `vault-grasbot/` comme vault séparé dans Obsidian (plus simple pour démarrer).
Les wikilinks `[[nom]]` restent valides tant que les noms de notes sont uniques
dans le vault courant. Les notes `source: manual` que tu crées ne seront jamais
écrasées par une régénération. Pour une note privée qui ne doit pas apparaître
côté chatbot, ajouter `visibility: private` : elle sera exclue de `load_vault()`.

152
vault-grasbot/TAXONOMIE.md Normal file
View File

@ -0,0 +1,152 @@
# Taxonomie du vault GrasBot
Vocabulaire contrôlé pour les champs `domains`, `tags`, `aliases`, `answers`
du frontmatter YAML. Garantit que le pipeline de retrieval (graph + BM25 de
`llm-api/search.py`) trouve des correspondances cohérentes entre notes.
**Règle d'or** : si tu inventes un nouveau domaine ou tag, **ajoute-le ici
avant** de l'utiliser dans une note. Pour `aliases`, libre à toi d'en
ajouter autant que nécessaire par note.
---
## Domaines (`domains:`)
Liste fermée. Une note doit avoir entre 1 et 4 domaines, du plus spécifique
au plus général.
| Slug | Libellé | Description |
|---|---|---|
| `algorithmique` | Algorithmique | Structures de données, tri, complexité |
| `c` | Langage C | Projets 42 en C (libft, printf, GNL, …) |
| `cpp` | C++ | POO, templates, STL, projets C++ 42 |
| `systeme` | Systèmes | Unix, processus, threads, mutex, IPC |
| `reseau` | Réseaux | TCP/IP, sockets, IRC, routage |
| `web` | Web | Next.js, React, Strapi, APIs |
| `devops` | DevOps | Docker, Nginx, conteneurs, CI/CD |
| `securite` | Sécurité | Hardening, SSH, firewall, cybersécurité |
| `ia` | Intelligence artificielle | LLMs, embeddings, agents, chatbots |
| `graphique` | Graphique | Raycasting, MinilibX, rendu 2D/3D |
| `3d` | Impression 3D | Slicers, firmwares, conception |
| `domotique` | Domotique | Home Assistant, Zigbee, IoT |
| `ecole-42` | École 42 | Tout projet ou tranche de formation 42 |
| `parcours` | Parcours | CV, bio, reconversion, trajectoire |
## Tags (`tags:`)
Plus libres que les domaines, mais garder une cohérence. Liste des tags
actuellement utilisés :
| Tag | Usage |
|---|---|
| `moc` | Note qui est un Map of Content |
| `cv` | Note liée au CV personnel |
| `parcours` | Bio, reconversion, trajectoire |
| `alternance` | Recherche d'alternance / stage |
| `reconversion` | Changement de carrière |
| `data-ia` | Stream data / IA pour alternance |
| `42-commun` | Projets du tronc commun 42 |
| `42-piscine` | Projets de la piscine 42 |
| `42-tronc` | Projets du tronc commun (post-piscine) |
| `tri` | Algorithmes de tri |
| `concurrence` | Threads, mutex, synchronisation |
| `docker` | Conteneurisation |
| `makefile` | Build system GNU Make |
## Aliases (`aliases:`)
Libre par note. **Objectif** : capter toutes les formulations naturelles que
pourrait employer un visiteur pour désigner le sujet de la note.
Règles :
- **Entre 3 et 12 aliases par note** (ni trop peu pour la couverture, ni
trop pour ne pas bruiter le scoring).
- Formes **courtes** (un à trois mots, en minuscules).
- Inclure : **variantes orthographiques**, **acronymes**, **synonymes FR/EN**,
**formulations utilisateur** probables.
- Ne **pas** inclure de phrases entières (c'est le rôle d'`answers:`).
**Exemples** :
```yaml
# Note ia.md
aliases:
- IA
- intelligence artificielle
- LLM
- modèles de langage
- chatbot
- Ollama
- Qwen
- Mistral
- machine learning
- deep learning
- data science
# Note push-swap.md
aliases:
- push swap
- push_swap
- tri par piles
- algorithme de tri 42
- push
- swap
```
## Answers (`answers:`)
Questions-types **formulées à la 1re personne du visiteur** auxquelles cette
note apporte une réponse directe. Fort signal pour le retrieval.
Règles :
- **2 à 6 answers par note**.
- Rédigées comme un visiteur **naturel** : *« Quelles sont ses compétences
en IA ? »*, pas *« compétences IA »*.
- Ciblées : une answer doit **vraiment** appeler cette note comme source
principale (pas une note périphérique).
**Exemples** :
```yaml
# Note cv-grascalvet-fernand.md
answers:
- "Qui est Fernand Gras-Calvet ?"
- "Quel est son parcours ?"
- "Que peux-tu me dire sur Fernand ?"
- "Quel est son profil ?"
- "Cherche-t-il une alternance ?"
# Note ia.md
answers:
- "Quelles sont ses compétences en IA ?"
- "Utilise-t-il des LLMs locaux ?"
- "Qu'est-ce qui l'intéresse dans l'IA ?"
```
## Priority (`priority:`)
Entier de 1 à 10. Boost léger appliqué au scoring.
| Valeur | Usage |
|---|---|
| 910 | Notes stratégiques (CV, MOCs principaux) |
| 68 | Notes riches (compétences, projets emblématiques) |
| 5 | Défaut (la plupart des projets 42) |
| 34 | Notes techniques internes (auto-doc) |
| 12 | Notes archivées, peu pertinentes pour le visiteur |
## Visibility (`visibility:`)
- `public` (défaut) : accessible au chatbot et indexable
- `private` : **exclue** du retrieval par `load_vault()` (usage personnel)
---
## Quand régénérer ?
- Après ajout d'une note Strapi : `python strapi_extraction/build-vault.py`
- Après édition manuelle dans Obsidian : rien, l'API lit le vault au vol
- Après changement de taxonomie : éditer ce fichier puis les notes concernées
- Après chargement de l'API : `POST /reload-vault` pour recharger sans redémarrer