mirror of
https://github.com/Ladebeze66/devsite.git
synced 2026-05-11 16:56:26 +02:00
Compare commits
5 Commits
20d4c78df8
...
1267d60cf2
| Author | SHA1 | Date | |
|---|---|---|---|
| 1267d60cf2 | |||
| 747193ea3c | |||
| ab47a41a37 | |||
| f824053a31 | |||
| 0a506dbf39 |
9
.gitignore
vendored
9
.gitignore
vendored
@ -41,3 +41,12 @@ yarn-error.log*
|
|||||||
next-env.d.ts
|
next-env.d.ts
|
||||||
|
|
||||||
.vercel
|
.vercel
|
||||||
|
|
||||||
|
# Python (llm-api)
|
||||||
|
llm-api/__pycache__/
|
||||||
|
llm-api/.venv/
|
||||||
|
llm-api/*.pyc
|
||||||
|
|
||||||
|
# Legacy RAG index (ChromaDB) — obsolete depuis bascule graph+BM25
|
||||||
|
/chroma-index/
|
||||||
|
|
||||||
|
|||||||
@ -3,12 +3,24 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { getApiUrl } from "../utils/getApiUrl";
|
import { getApiUrl } from "../utils/getApiUrl";
|
||||||
import CarouselCompetences from "../components/CarouselCompetences";
|
import VignetteCarousel from "../components/VignetteCarousel";
|
||||||
import "../globals.css";
|
import "../globals.css";
|
||||||
import "../assets/main.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() {
|
export default function Page() {
|
||||||
const [competences, setCompetences] = useState([]);
|
const [competences, setCompetences] = useState([]);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const apiUrl = getApiUrl();
|
const apiUrl = getApiUrl();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -21,57 +33,141 @@ export default function Page() {
|
|||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
// Tri sécurisé des compétences par `order`
|
const sortedCompetences = (data.data ?? []).sort(
|
||||||
const sortedCompetences = (data.data ?? []).sort((a, b) => ((a.attributes?.order ?? 999) - (b.attributes?.order ?? 999)));
|
(a, b) => ((a.attributes?.order ?? 999) - (b.attributes?.order ?? 999))
|
||||||
|
);
|
||||||
|
|
||||||
setCompetences(sortedCompetences);
|
setCompetences(sortedCompetences);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("❌ Erreur lors de la récupération des compétences :", error);
|
console.error("❌ Erreur lors de la récupération des compétences :", error);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchCompetences();
|
fetchCompetences();
|
||||||
}, [apiUrl]);
|
}, [apiUrl]);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="w-full p-3 mt-5 mb-5">
|
<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 d’apprentissage et des
|
||||||
|
exemples concrets — ouvrez une carte pour voir les outils mobilisés et les
|
||||||
|
projets associés.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{isLoading ? (
|
||||||
{competences.length === 0 ? (
|
<div className="grid grid-cols-1 gap-5 sm:grid-cols-2 md:grid-cols-6">
|
||||||
<p className="text-center text-gray-500">Aucune compétence disponible.</p>
|
{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="flex flex-col gap-7 max-w-7xl mx-auto">
|
<div className="grid grid-cols-1 gap-5 sm:grid-cols-2 md:grid-cols-6">
|
||||||
{competences.map((competence) => {
|
{competences.map((competence, idx) => {
|
||||||
const pictures = competence.picture || [];
|
const pictures = competence.picture ?? [];
|
||||||
const images = pictures.map(picture => ({
|
const images = pictures.map((img) => ({
|
||||||
url: picture.url ? `${apiUrl}${picture.url}` : "/placeholder.jpg",
|
url: img.url ? `${apiUrl}${img.url}` : "/placeholder.jpg",
|
||||||
alt: picture.name || "Competence image"
|
alt: img.name || `Visuel de la compétence ${competence.name}`,
|
||||||
}));
|
}));
|
||||||
|
const firstImage = images[0];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<Link
|
||||||
key={competence.id}
|
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"
|
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`}
|
||||||
<Link href={`/competences/${competence.slug}`}>
|
>
|
||||||
<div className="overflow-hidden w-full h-64 mb-4">
|
<div className="relative aspect-[4/3] w-full overflow-hidden bg-surface-container-low">
|
||||||
<CarouselCompetences images={images} className="w-full h-full object-cover" />
|
{images.length > 1 ? (
|
||||||
</div>
|
<VignetteCarousel images={images} />
|
||||||
<div className="flex-grow overflow-y-auto max-h-32 hide-scrollbar show-scrollbar">
|
) : firstImage ? (
|
||||||
<p className="font-headline font-bold text-xl mb-2">{competence.name}</p>
|
<img
|
||||||
<p className="text-gray-700 text-sm hover:text-base transition-all duration-200 ease-in-out">
|
src={firstImage.url}
|
||||||
{competence.description}
|
alt={firstImage.alt}
|
||||||
</p>
|
className="h-full w-full object-cover transition-transform duration-500 group-hover:scale-[1.03]"
|
||||||
</div>
|
loading="lazy"
|
||||||
</Link>
|
/>
|
||||||
</div>
|
) : (
|
||||||
|
<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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { createPortal } from "react-dom";
|
import { createPortal } from "react-dom";
|
||||||
import { Swiper, SwiperSlide } from "swiper/react";
|
import { Swiper, SwiperSlide } from "swiper/react";
|
||||||
import { Navigation, Pagination, Autoplay } from "swiper/modules";
|
import { Navigation, Pagination, Autoplay } from "swiper/modules";
|
||||||
@ -10,6 +10,25 @@ import "swiper/css/pagination";
|
|||||||
import "../globals.css";
|
import "../globals.css";
|
||||||
import "../assets/main.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 {
|
interface CarouselProps {
|
||||||
images: Array<{ url: string; alt: string }>;
|
images: Array<{ url: string; alt: string }>;
|
||||||
className?: string;
|
className?: string;
|
||||||
@ -18,25 +37,53 @@ interface CarouselProps {
|
|||||||
export default function Carousel({ images, className }: CarouselProps) {
|
export default function Carousel({ images, className }: CarouselProps) {
|
||||||
const [selectedImage, setSelectedImage] = useState<string | null>(null);
|
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 (
|
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
|
<Swiper
|
||||||
modules={[Navigation, Pagination, Autoplay]}
|
modules={[Navigation, Pagination, Autoplay]}
|
||||||
spaceBetween={10}
|
spaceBetween={10}
|
||||||
slidesPerView={1}
|
slidesPerView={1}
|
||||||
navigation
|
navigation
|
||||||
pagination={{ clickable: true }}
|
pagination={{ clickable: true }}
|
||||||
autoplay={{ delay: 3000 }}
|
autoplay={{ delay: 3500, disableOnInteraction: false }}
|
||||||
|
loop={images.length > 1}
|
||||||
className={`w-full ${className || "h-64"}`}
|
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) => (
|
{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
|
<img
|
||||||
src={img.url}
|
src={img.url}
|
||||||
alt={img.alt}
|
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)}
|
onClick={() => setSelectedImage(img.url)}
|
||||||
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
</SwiperSlide>
|
</SwiperSlide>
|
||||||
))}
|
))}
|
||||||
@ -46,20 +93,34 @@ export default function Carousel({ images, className }: CarouselProps) {
|
|||||||
{selectedImage &&
|
{selectedImage &&
|
||||||
createPortal(
|
createPortal(
|
||||||
<div
|
<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)}
|
onClick={() => setSelectedImage(null)}
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-label="Image agrandie"
|
||||||
>
|
>
|
||||||
<div className="relative w-full max-w-6xl p-6 bg-transparent">
|
<div
|
||||||
|
className="relative flex max-h-[92vh] max-w-[92vw] items-center justify-center"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
<button
|
<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)}
|
onClick={() => setSelectedImage(null)}
|
||||||
|
aria-label="Fermer l'aperçu"
|
||||||
>
|
>
|
||||||
✖
|
<span
|
||||||
|
className="material-symbols-outlined"
|
||||||
|
aria-hidden="true"
|
||||||
|
translate="no"
|
||||||
|
>
|
||||||
|
close
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
<img
|
<img
|
||||||
src={selectedImage}
|
src={selectedImage}
|
||||||
alt="Agrandissement"
|
alt="Aperçu en taille réelle"
|
||||||
className="w-full h-full object-cover rounded-md"
|
className="max-h-[92vh] max-w-[92vw] rounded-sheet object-contain shadow-ambient"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>,
|
</div>,
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { createPortal } from "react-dom";
|
import { createPortal } from "react-dom";
|
||||||
import { Swiper, SwiperSlide } from "swiper/react";
|
import { Swiper, SwiperSlide } from "swiper/react";
|
||||||
import { Navigation, Pagination, Autoplay } from "swiper/modules";
|
import { Navigation, Pagination, Autoplay } from "swiper/modules";
|
||||||
@ -10,6 +10,13 @@ import "swiper/css/pagination";
|
|||||||
import "../globals.css";
|
import "../globals.css";
|
||||||
import "../assets/main.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 {
|
interface CarouselProps {
|
||||||
images: Array<{ url: string; alt: string }>;
|
images: Array<{ url: string; alt: string }>;
|
||||||
className?: string;
|
className?: string;
|
||||||
@ -18,25 +25,53 @@ interface CarouselProps {
|
|||||||
export default function CarouselCompetences({ images, className }: CarouselProps) {
|
export default function CarouselCompetences({ images, className }: CarouselProps) {
|
||||||
const [selectedImage, setSelectedImage] = useState<string | null>(null);
|
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 (
|
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
|
<Swiper
|
||||||
modules={[Navigation, Pagination, Autoplay]}
|
modules={[Navigation, Pagination, Autoplay]}
|
||||||
spaceBetween={10}
|
spaceBetween={10}
|
||||||
slidesPerView={1}
|
slidesPerView={1}
|
||||||
navigation
|
navigation
|
||||||
pagination={{ clickable: true }}
|
pagination={{ clickable: true }}
|
||||||
autoplay={{ delay: 3000 }}
|
autoplay={{ delay: 3500, disableOnInteraction: false }}
|
||||||
|
loop={images.length > 1}
|
||||||
className={`w-full ${className || "h-64"}`}
|
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) => (
|
{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
|
<img
|
||||||
src={img.url}
|
src={img.url}
|
||||||
alt={img.alt}
|
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)}
|
onClick={() => setSelectedImage(img.url)}
|
||||||
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
</SwiperSlide>
|
</SwiperSlide>
|
||||||
))}
|
))}
|
||||||
@ -46,20 +81,34 @@ export default function CarouselCompetences({ images, className }: CarouselProps
|
|||||||
{selectedImage &&
|
{selectedImage &&
|
||||||
createPortal(
|
createPortal(
|
||||||
<div
|
<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)}
|
onClick={() => setSelectedImage(null)}
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-label="Image agrandie"
|
||||||
>
|
>
|
||||||
<div className="relative w-full max-w-6xl p-6 bg-transparent">
|
<div
|
||||||
|
className="relative flex max-h-[92vh] max-w-[92vw] items-center justify-center"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
<button
|
<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)}
|
onClick={() => setSelectedImage(null)}
|
||||||
|
aria-label="Fermer l'aperçu"
|
||||||
>
|
>
|
||||||
✖
|
<span
|
||||||
|
className="material-symbols-outlined"
|
||||||
|
aria-hidden="true"
|
||||||
|
translate="no"
|
||||||
|
>
|
||||||
|
close
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
<img
|
<img
|
||||||
src={selectedImage}
|
src={selectedImage}
|
||||||
alt="Agrandissement"
|
alt="Aperçu en taille réelle"
|
||||||
className="w-full h-full object-cover rounded-md"
|
className="max-h-[92vh] max-w-[92vw] rounded-sheet object-contain shadow-ambient"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>,
|
</div>,
|
||||||
|
|||||||
@ -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";
|
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 }) {
|
export default function ChatBot({ onClose }) {
|
||||||
const [question, setQuestion] = useState("");
|
const [question, setQuestion] = useState("");
|
||||||
const [messages, setMessages] = useState([]);
|
const [messages, setMessages] = useState([]);
|
||||||
const [isWaiting, setIsWaiting] = useState(false);
|
const [isWaiting, setIsWaiting] = useState(false);
|
||||||
|
const scrollRef = useRef(null);
|
||||||
|
const inputRef = useRef(null);
|
||||||
|
|
||||||
const handleAsk = async () => {
|
useEffect(() => {
|
||||||
if (!question.trim()) return;
|
if (scrollRef.current) {
|
||||||
|
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
||||||
|
}
|
||||||
|
}, [messages, isWaiting]);
|
||||||
|
|
||||||
const userMessage = { sender: "user", text: question };
|
useEffect(() => {
|
||||||
setMessages([...messages, userMessage]);
|
inputRef.current?.focus();
|
||||||
|
}, []);
|
||||||
|
|
||||||
setQuestion("");
|
const handleAsk = async () => {
|
||||||
setIsWaiting(true);
|
if (!question.trim() || isWaiting) return;
|
||||||
|
|
||||||
try {
|
const userMessage = { sender: "user", text: question };
|
||||||
const botResponse = await askAI(question);
|
setMessages((prev) => [...prev, userMessage]);
|
||||||
const botMessage = { sender: "bot", text: botResponse };
|
setQuestion("");
|
||||||
setMessages((prevMessages) => [...prevMessages, botMessage]);
|
setIsWaiting(true);
|
||||||
} catch (error) {
|
|
||||||
setMessages([...messages, { sender: "bot", text: "❌ Erreur de réponse. Réessayez plus tard." }]);
|
|
||||||
} finally {
|
|
||||||
setIsWaiting(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
try {
|
||||||
<div className="flex flex-col w-96 bg-white/70 shadow-lg rounded-lg border border-gray-300">
|
const payload = await askAI(userMessage.text);
|
||||||
<div className="bg-blue-600 text-white p-3 rounded-t-lg flex justify-between items-center">
|
setMessages((prev) => [
|
||||||
<span className="font-headline font-bold">💬 GrasBot</span>
|
...prev,
|
||||||
<button className="text-white hover:text-red-400 text-xl" onClick={onClose}>❌</button>
|
{
|
||||||
</div>
|
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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
<div className="h-64 overflow-y-auto p-4 space-y-2">
|
const handleKeyDown = (e) => {
|
||||||
{messages.map((msg, index) => (
|
if (e.key === "Enter" && !e.shiftKey) {
|
||||||
<div
|
e.preventDefault();
|
||||||
key={index}
|
handleAsk();
|
||||||
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%" }}
|
};
|
||||||
>
|
|
||||||
{msg.text}
|
|
||||||
</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>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex p-3 border-t border-gray-300">
|
return (
|
||||||
<input
|
<div className="flex h-full w-full flex-col overflow-hidden rounded-sheet bg-surface-container-lowest/95 shadow-ambient backdrop-blur-vellum">
|
||||||
type="text"
|
<div className="flex items-center justify-between gap-3 bg-primary px-4 py-3 text-white">
|
||||||
className="flex-1 p-2 border border-gray-300 font-headline text-xs rounded-l-lg focus:outline-none"
|
<div className="flex min-w-0 items-center gap-2">
|
||||||
placeholder="Posez votre question..."
|
<span
|
||||||
value={question}
|
className="material-symbols-outlined text-2xl"
|
||||||
onChange={(e) => setQuestion(e.target.value)}
|
aria-hidden="true"
|
||||||
/>
|
translate="no"
|
||||||
<button
|
>
|
||||||
className="bg-blue-500 text-white px-4 py-2 rounded-r-lg hover:bg-blue-700"
|
smart_toy
|
||||||
onClick={handleAsk}
|
</span>
|
||||||
>
|
<div className="min-w-0">
|
||||||
➤
|
<p className="truncate font-headline text-sm font-bold tracking-tight">GrasBot</p>
|
||||||
</button>
|
<p className="truncate font-headline text-[10px] uppercase tracking-[0.25em] text-primary-fixed">
|
||||||
</div>
|
Assistant IA locale
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</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
|
||||||
|
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="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="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 items-center gap-2 border-t border-outline-variant/30 bg-surface-container-lowest/80 p-3">
|
||||||
|
<input
|
||||||
|
ref={inputRef}
|
||||||
|
type="text"
|
||||||
|
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
|
||||||
|
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>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
@ -3,98 +3,158 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { sendMessage } from "../utils/sendMessage";
|
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() {
|
export default function ContactForm() {
|
||||||
const [name, setName] = useState("");
|
const [name, setName] = useState("");
|
||||||
const [email, setEmail] = useState("");
|
const [email, setEmail] = useState("");
|
||||||
const [message, setMessage] = useState("");
|
const [message, setMessage] = useState("");
|
||||||
const [status, setStatus] = useState("");
|
const [status, setStatus] = useState("");
|
||||||
const [isSuccess, setIsSuccess] = useState<boolean | null>(null);
|
const [statusKind, setStatusKind] = useState<
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
"idle" | "loading" | "success" | "error"
|
||||||
|
>("idle");
|
||||||
|
|
||||||
|
const isLoading = statusKind === "loading";
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
if (!name.trim() || !email.trim() || !message.trim()) {
|
if (!name.trim() || !email.trim() || !message.trim()) {
|
||||||
setStatus("❌ Tous les champs sont obligatoires.");
|
setStatus("Tous les champs sont obligatoires.");
|
||||||
setIsSuccess(false);
|
setStatusKind("error");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
||||||
setStatus("❌ Email invalide.");
|
setStatus("Email invalide.");
|
||||||
setIsSuccess(false);
|
setStatusKind("error");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setStatus("⏳ Envoi en cours...");
|
setStatus("Envoi en cours…");
|
||||||
setIsSuccess(null);
|
setStatusKind("loading");
|
||||||
setIsLoading(true);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await sendMessage(name, email, message);
|
await sendMessage(name, email, message);
|
||||||
setStatus("✅ Message envoyé avec succès !");
|
setStatus("Message envoyé. Merci, je reviens vers vous rapidement.");
|
||||||
setIsSuccess(true);
|
setStatusKind("success");
|
||||||
setName("");
|
setName("");
|
||||||
setEmail("");
|
setEmail("");
|
||||||
setMessage("");
|
setMessage("");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setStatus("❌ Erreur lors de l'envoi du message.");
|
setStatus("Erreur lors de l'envoi du message.");
|
||||||
setIsSuccess(false);
|
setStatusKind("error");
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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 (
|
return (
|
||||||
<form
|
<form onSubmit={handleSubmit} className="flex flex-col gap-3" noValidate>
|
||||||
onSubmit={handleSubmit}
|
<label className="flex flex-col gap-1">
|
||||||
className="max-w-lg mx-auto p-6 bg-white shadow-lg rounded-lg animate-fade-in"
|
<span className="font-headline text-[11px] font-bold uppercase tracking-[0.3em] text-secondary">
|
||||||
>
|
Votre nom
|
||||||
<h2 className="text-2xl font-headline font-bold mb-4 text-center">📩 Contactez-moi</h2>
|
</span>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Prénom Nom"
|
||||||
|
value={name}
|
||||||
|
onChange={(e) => setName(e.target.value)}
|
||||||
|
className={fieldClass}
|
||||||
|
required
|
||||||
|
autoComplete="name"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
<input
|
<label className="flex flex-col gap-1">
|
||||||
type="text"
|
<span className="font-headline text-[11px] font-bold uppercase tracking-[0.3em] text-secondary">
|
||||||
placeholder="Votre nom"
|
Votre email
|
||||||
value={name}
|
</span>
|
||||||
onChange={(e) => setName(e.target.value)}
|
<input
|
||||||
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"
|
type="email"
|
||||||
required
|
placeholder="adresse@exemple.com"
|
||||||
/>
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
className={fieldClass}
|
||||||
|
required
|
||||||
|
autoComplete="email"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
<input
|
<label className="flex flex-col gap-1">
|
||||||
type="email"
|
<span className="font-headline text-[11px] font-bold uppercase tracking-[0.3em] text-secondary">
|
||||||
placeholder="Votre email"
|
Votre message
|
||||||
value={email}
|
</span>
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
<textarea
|
||||||
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"
|
placeholder="Quelques mots sur votre projet, question ou intention…"
|
||||||
required
|
value={message}
|
||||||
/>
|
onChange={(e) => setMessage(e.target.value)}
|
||||||
|
rows={5}
|
||||||
<textarea
|
className={`${fieldClass} min-h-[9rem] resize-y`}
|
||||||
placeholder="Votre message"
|
required
|
||||||
value={message}
|
/>
|
||||||
onChange={(e) => setMessage(e.target.value)}
|
</label>
|
||||||
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"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
className={`w-full py-3 rounded transition ${
|
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 ? "bg-gray-400 cursor-not-allowed" : "bg-blue-500 hover:bg-blue-600 text-white font-headline font-bold"
|
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>
|
</button>
|
||||||
|
|
||||||
{status && (
|
{statusKind !== "idle" && status && (
|
||||||
<p
|
<div
|
||||||
className={`mt-4 text-center ${isSuccess ? "text-green-600" : "text-red-600"}`}
|
role="status"
|
||||||
aria-live="polite"
|
aria-live="polite"
|
||||||
|
className={`mt-2 flex items-center gap-2 rounded-tile px-4 py-3 font-body text-sm ${statusStyles[statusKind]}`}
|
||||||
>
|
>
|
||||||
{status}
|
<span
|
||||||
</p>
|
className="material-symbols-outlined text-base"
|
||||||
|
aria-hidden="true"
|
||||||
|
translate="no"
|
||||||
|
>
|
||||||
|
{statusIcon[statusKind]}
|
||||||
|
</span>
|
||||||
|
<span className="min-w-0">{status}</span>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import Link from "next/link";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { fetchData } from "../utils/fetchData";
|
import { fetchData } from "../utils/fetchData";
|
||||||
import { getApiUrl } from "../utils/getApiUrl";
|
import { getApiUrl } from "../utils/getApiUrl";
|
||||||
@ -31,52 +32,165 @@ interface ContentSectionProps {
|
|||||||
contentClass?: string;
|
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 [data, setData] = useState<ContentData | null>(null);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const apiUrl = getApiUrl();
|
const apiUrl = getApiUrl();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function fetchContent() {
|
async function fetchContent() {
|
||||||
|
try {
|
||||||
const result = await fetchData(collection, slug);
|
const result = await fetchData(collection, slug);
|
||||||
setData(result);
|
setData(result);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchContent();
|
fetchContent();
|
||||||
}, [collection, slug, apiUrl]);
|
}, [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) {
|
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 { name, Resum: richText, picture, link, linkText } = data;
|
||||||
|
|
||||||
const images = picture?.map((img: ImageData) => ({
|
const images =
|
||||||
url: `${apiUrl}${img.formats?.large?.url || img.url}`,
|
picture?.map((img: ImageData) => ({
|
||||||
alt: img.name || "Image",
|
url: `${apiUrl}${img.formats?.large?.url || img.url}`,
|
||||||
})) || [];
|
alt: img.name || `Visuel du projet ${name}`,
|
||||||
|
})) || [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-3xl mx-auto p-6">
|
<div className="mx-auto flex w-full min-w-0 max-w-3xl flex-col gap-5 px-4 pb-10 sm:px-6">
|
||||||
<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>
|
{/* 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
|
||||||
<div className={contentClass || "bg-white/80 rounded-md p-4 font-headline font-bold shadow-md mt-6"}>
|
className="rounded-sheet bg-surface-container-lowest/85 p-5 shadow-ambient backdrop-blur-vellum sm:p-7 md:p-8"
|
||||||
<ReactMarkdown>{richText}</ReactMarkdown>
|
aria-labelledby="project-title"
|
||||||
</div>
|
>
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
{link && (
|
<span className="font-headline text-[11px] font-bold uppercase tracking-[0.3em] text-secondary">
|
||||||
<div className="mt-6">
|
Projet · Portfolio
|
||||||
<a
|
</span>
|
||||||
href={link}
|
<h1
|
||||||
target="_blank"
|
id="project-title"
|
||||||
rel="noopener noreferrer"
|
className="font-headline text-3xl font-extrabold tracking-tight text-on-surface md:text-4xl"
|
||||||
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"
|
|
||||||
>
|
>
|
||||||
{linkText || "Voir plus/lien externe"}
|
{name}
|
||||||
</a>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
{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">
|
||||||
|
<a
|
||||||
|
href={link}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
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"}
|
||||||
|
<span
|
||||||
|
className="material-symbols-outlined text-lg"
|
||||||
|
aria-hidden="true"
|
||||||
|
translate="no"
|
||||||
|
>
|
||||||
|
open_in_new
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect } from "react";
|
import Link from "next/link";
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { getApiUrl } from "../utils/getApiUrl";
|
import { getApiUrl } from "../utils/getApiUrl";
|
||||||
import CarouselCompetences from "./CarouselCompetences";
|
import CarouselCompetences from "./CarouselCompetences";
|
||||||
import ReactMarkdown from "react-markdown";
|
import ReactMarkdown from "react-markdown";
|
||||||
import rehypeRaw from "rehype-raw";
|
import rehypeRaw from "rehype-raw";
|
||||||
import ModalGlossaire from "./ModalGlossaire";
|
import ModalGlossaire from "./ModalGlossaire";
|
||||||
import ChatBot from "./ChatBot";
|
|
||||||
|
|
||||||
interface ImageData {
|
interface ImageData {
|
||||||
url: string;
|
url: string;
|
||||||
@ -37,32 +37,96 @@ interface ContentSectionProps {
|
|||||||
contentClass?: string;
|
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({
|
export default function ContentSectionCompetences({
|
||||||
competenceData,
|
competenceData,
|
||||||
glossaireData,
|
glossaireData,
|
||||||
titleClass,
|
|
||||||
contentClass,
|
|
||||||
}: ContentSectionProps) {
|
}: ContentSectionProps) {
|
||||||
console.log("🔍 [ContentSectionCompetences] Chargement du composant...");
|
|
||||||
|
|
||||||
const [selectedMot, setSelectedMot] = useState<GlossaireItem | null>(null);
|
const [selectedMot, setSelectedMot] = useState<GlossaireItem | null>(null);
|
||||||
const [isChatbotOpen, setIsChatbotOpen] = useState(false);
|
const contentRef = useRef<HTMLDivElement | null>(null);
|
||||||
const [loading, setLoading] = useState(competenceData === null);
|
|
||||||
const apiUrl = getApiUrl();
|
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(() => {
|
useEffect(() => {
|
||||||
if (competenceData) {
|
const node = contentRef.current;
|
||||||
setLoading(false);
|
if (!node) return;
|
||||||
}
|
|
||||||
}, [competenceData]);
|
|
||||||
|
|
||||||
if (loading) {
|
const handleClick = (event: Event) => {
|
||||||
return <div className="text-center text-gray-500">⏳ Chargement des détails de la compétence...</div>;
|
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) {
|
if (!competenceData) {
|
||||||
console.error("❌ [ContentSectionCompetences] Compétence introuvable !");
|
return (
|
||||||
return <div className="text-red-500 text-center">❌ Compétence introuvable.</div>;
|
<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;
|
const { name, content, picture } = competenceData;
|
||||||
@ -70,78 +134,95 @@ export default function ContentSectionCompetences({
|
|||||||
const images =
|
const images =
|
||||||
picture?.map((img) => ({
|
picture?.map((img) => ({
|
||||||
url: `${apiUrl}${img.formats?.large?.url || img.url}`,
|
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) {
|
function transformMarkdownWithKeywords(text: string) {
|
||||||
if (!glossaireData.length) return text;
|
if (!text) return "";
|
||||||
|
|
||||||
let modifiedText = text;
|
let modifiedText = text;
|
||||||
|
|
||||||
modifiedText = modifiedText.replace(
|
modifiedText = modifiedText.replace(
|
||||||
/\bIA locale\b/g,
|
/\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>`
|
||||||
);
|
);
|
||||||
|
|
||||||
glossaireData.forEach(({ mot_clef, variantes }) => {
|
if (glossaireData.length) {
|
||||||
const regexVariants = variantes
|
glossaireData.forEach(({ mot_clef, variantes }) => {
|
||||||
.map((v) => v.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
|
const regexVariants = variantes
|
||||||
.join("|");
|
.map((v) => v.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
|
||||||
const regex = new RegExp(`\\b(${mot_clef}|${regexVariants})\\b`, "gi");
|
.join("|");
|
||||||
|
const regex = new RegExp(`\\b(${mot_clef}|${regexVariants})\\b`, "gi");
|
||||||
|
|
||||||
modifiedText = modifiedText.replace(regex, (match) => {
|
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;
|
return modifiedText;
|
||||||
}
|
}
|
||||||
|
|
||||||
const contentWithLinks = transformMarkdownWithKeywords(content);
|
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 (
|
return (
|
||||||
<div className="max-w-3xl mx-auto p-6">
|
<div className="mx-auto flex w-full min-w-0 max-w-3xl flex-col gap-5 px-4 pb-10 sm:px-6">
|
||||||
<h1 className={titleClass || "bg-white/60 rounded-md p-1 text-2xl mb-6 font-headline font-bold text-blue-700"}>
|
<Link
|
||||||
{name}
|
href="/competences"
|
||||||
</h1>
|
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"
|
||||||
<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"}>
|
<span
|
||||||
<ReactMarkdown rehypePlugins={[rehypeRaw]}>{contentWithLinks}</ReactMarkdown>
|
className="material-symbols-outlined text-base"
|
||||||
</div>
|
aria-hidden="true"
|
||||||
{selectedMot && <ModalGlossaire mot={selectedMot} onClose={() => setSelectedMot(null)} />}
|
translate="no"
|
||||||
|
>
|
||||||
|
arrow_back
|
||||||
|
</span>
|
||||||
|
Compétences
|
||||||
|
</Link>
|
||||||
|
|
||||||
{isChatbotOpen && (
|
<section
|
||||||
<div className="fixed bottom-10 right-10 p-4 w-96">
|
className="rounded-sheet bg-surface-container-lowest/85 p-5 shadow-ambient backdrop-blur-vellum sm:p-7 md:p-8"
|
||||||
<ChatBot onClose={() => setIsChatbotOpen(false)} />
|
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>
|
||||||
</div>
|
</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>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{selectedMot && (
|
||||||
|
<ModalGlossaire mot={selectedMot} onClose={() => setSelectedMot(null)} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -11,9 +11,16 @@ interface ContentSectionProps {
|
|||||||
contentClass?: string;
|
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 [competenceData, setCompetenceData] = useState(null);
|
||||||
const [glossaireData, setGlossaireData] = useState([]);
|
const [glossaireData, setGlossaireData] = useState([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
@ -38,15 +45,24 @@ export default function ContentSectionCompetencesContainer({ collection, slug, t
|
|||||||
}, [collection, slug]);
|
}, [collection, slug]);
|
||||||
|
|
||||||
if (loading) {
|
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 (
|
return (
|
||||||
<ContentSectionCompetences
|
<ContentSectionCompetences
|
||||||
competenceData={competenceData}
|
competenceData={competenceData}
|
||||||
glossaireData={glossaireData}
|
glossaireData={glossaireData}
|
||||||
titleClass={titleClass}
|
|
||||||
contentClass={contentClass}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,21 +2,41 @@
|
|||||||
|
|
||||||
import { useEffect, useState } from "react";
|
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() {
|
export default function Footer() {
|
||||||
const [visitCount, setVisitCount] = useState(0);
|
const [visitCount, setVisitCount] = useState(0);
|
||||||
|
const [year, setYear] = useState(() => new Date().getFullYear());
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const visits = localStorage.getItem("visitCount");
|
const visits = localStorage.getItem("visitCount");
|
||||||
const newVisitCount = visits ? parseInt(visits, 10) + 1 : 1;
|
const newVisitCount = visits ? parseInt(visits, 10) + 1 : 1;
|
||||||
localStorage.setItem("visitCount", newVisitCount.toString());
|
localStorage.setItem("visitCount", newVisitCount.toString());
|
||||||
setVisitCount(newVisitCount);
|
setVisitCount(newVisitCount);
|
||||||
|
setYear(new Date().getFullYear());
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<footer className="min-h-[80px] w-full min-w-0 rounded-lg bg-white/50 backdrop-blur">
|
<footer className="mx-auto w-full min-w-0 max-w-6xl px-4 pb-6 sm:px-6">
|
||||||
<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">
|
<div className="rounded-tile bg-surface-container-lowest/70 px-6 py-5 text-center backdrop-blur-vellum">
|
||||||
<p>© {new Date().getFullYear()} Gras-Calvet Fernand</p>
|
<p className="font-headline text-sm font-bold tracking-tight text-primary">
|
||||||
<p className="text-[10px] uppercase tracking-[0.3em] text-outline">
|
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}
|
Visite n° {visitCount}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
76
app/components/GrasBotFab.tsx
Normal file
76
app/components/GrasBotFab.tsx
Normal 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>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -68,7 +68,11 @@ export default function ModalGlossaire({ mot, onClose }: ModalGlossaireProps) {
|
|||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
aria-label="Fermer la fenêtre du glossaire"
|
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
|
close
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
68
app/components/VignetteCarousel.tsx
Normal file
68
app/components/VignetteCarousel.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
8
app/components/my-next-site.code-workspace
Normal file
8
app/components/my-next-site.code-workspace
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"folders": [
|
||||||
|
{
|
||||||
|
"path": "../.."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"settings": {}
|
||||||
|
}
|
||||||
@ -1,34 +1,151 @@
|
|||||||
|
import Link from "next/link";
|
||||||
import ContactForm from "../components/ContactForm";
|
import ContactForm from "../components/ContactForm";
|
||||||
import { getApiUrl } from "../utils/getApiUrl";
|
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() {
|
export default function ContactPage() {
|
||||||
const apiUrl = getApiUrl();
|
const apiUrl = getApiUrl();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-3xl mx-auto p-6 flex flex-col justify-top min-h-screen">
|
<div className="mx-auto flex w-full min-w-0 max-w-3xl flex-col gap-5 px-4 pb-10 sm:px-6">
|
||||||
<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">
|
{/* Hero éditorial : kicker + titre + pitch. Gabarit identique aux listes. */}
|
||||||
📬 Correspondance
|
<section
|
||||||
</h1>
|
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'IA,
|
||||||
|
du développement web ou de l'É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">
|
{/* Canaux : 3 tuiles imbriquées, same-tier que les takeaways de la home. */}
|
||||||
Vous pouvez me contacter via ce formulaire ou sur mes réseaux sociaux.
|
<section
|
||||||
</p>
|
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">
|
<ul className="grid gap-3 md:grid-cols-3">
|
||||||
<p className="text-blue-500 font-headline font-bold">
|
{canaux.map((c) => (
|
||||||
LinkedIn: Fernand Gras-Calvet
|
<li key={c.label}>
|
||||||
</p>
|
<Link
|
||||||
<p className="text-blue-500 font-headline font-bold">
|
href={c.href}
|
||||||
Facebook: Fernand Gras-Calvet
|
target={c.external ? "_blank" : undefined}
|
||||||
</p>
|
rel={c.external ? "noopener noreferrer" : undefined}
|
||||||
<p className="text-blue-500 font-headline font-bold">
|
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"
|
||||||
Email: grascalvet.fernand@gmail.com
|
>
|
||||||
</p>
|
<span className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-primary text-on-primary">
|
||||||
</div>
|
<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} />
|
<ContactForm apiUrl={apiUrl} />
|
||||||
</div>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
35
app/fonts.ts
Normal file
35
app/fonts.ts
Normal 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",
|
||||||
|
});
|
||||||
@ -1,8 +1,14 @@
|
|||||||
/* Polices Stitch "Digital Atelier" : Manrope (titres/UI) + Newsreader (corps éditorial).
|
/* Polices Stitch "Digital Atelier" : Manrope (titres/UI) + Newsreader (corps éditorial).
|
||||||
Voir docs-site-interne/REFONTE-VISUELLE.md. Les anciennes classes font-orbitron-*
|
Voir docs-site-interne/REFONTE-VISUELLE.md.
|
||||||
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');
|
Les `@import url(...)` vers Google Fonts étaient strippés en production par la
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@24,400,0,0&display=swap');
|
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 base;
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@ -17,8 +23,12 @@
|
|||||||
--foreground: #191c1d; /* on-surface Stitch : jamais #000 pur */
|
--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 {
|
.material-symbols-outlined {
|
||||||
|
font-family: 'Material Symbols Outlined';
|
||||||
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
|
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
@ -38,15 +48,42 @@ html {
|
|||||||
}
|
}
|
||||||
|
|
||||||
body {
|
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;
|
color: #191c1d;
|
||||||
background: var(--background);
|
background: var(--background);
|
||||||
font-family: Arial, Helvetica, sans-serif;
|
font-family: var(--font-newsreader), Georgia, serif;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
overflow-x: hidden;
|
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 {
|
@keyframes fade-in {
|
||||||
from {
|
from {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
|||||||
@ -5,6 +5,8 @@ import Footer from "./components/Footer";
|
|||||||
import "./assets/main.css";
|
import "./assets/main.css";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
import NavLink from "./components/NavLink";
|
import NavLink from "./components/NavLink";
|
||||||
|
import GrasBotFab from "./components/GrasBotFab";
|
||||||
|
import { manrope, newsreader } from "./fonts";
|
||||||
|
|
||||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
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";
|
"text-on-surface-variant hover:text-primary transition-colors";
|
||||||
|
|
||||||
return (
|
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">
|
<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).
|
||||||
{/* Wallpaper plein écran (fondation, ne change pas avec la refonte). */}
|
`fixed inset-0` plutôt qu'`absolute` dans le grid : le wallpaper est
|
||||||
<div className="absolute inset-0 bg-wallpaper"></div>
|
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'où 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"). */}
|
{/* 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="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>
|
<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 "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">
|
<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">
|
<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
|
Portfolio Gras-Calvet Fernand
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
@ -101,7 +131,11 @@ export default function RootLayout({ children }: { children: React.ReactNode })
|
|||||||
aria-expanded={isMenuOpen}
|
aria-expanded={isMenuOpen}
|
||||||
aria-controls="mobile-drawer"
|
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"}
|
{isMenuOpen ? "close" : "menu"}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
@ -211,6 +245,11 @@ export default function RootLayout({ children }: { children: React.ReactNode })
|
|||||||
<Footer />
|
<Footer />
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
|||||||
220
app/page.tsx
220
app/page.tsx
@ -1,5 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import Link from "next/link";
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import ReactMarkdown from "react-markdown";
|
import ReactMarkdown from "react-markdown";
|
||||||
import "./assets/main.css";
|
import "./assets/main.css";
|
||||||
@ -8,18 +9,17 @@ import { getApiUrl } from "./utils/getApiUrl";
|
|||||||
async function getHomepageData() {
|
async function getHomepageData() {
|
||||||
const apiUrl = getApiUrl();
|
const apiUrl = getApiUrl();
|
||||||
|
|
||||||
// Configuration avec timeout et retry
|
|
||||||
const fetchWithTimeout = async (url: string, options: RequestInit = {}) => {
|
const fetchWithTimeout = async (url: string, options: RequestInit = {}) => {
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
const timeoutId = setTimeout(() => controller.abort(), 10000); // 10 secondes timeout
|
const timeoutId = setTimeout(() => controller.abort(), 10000);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
...options,
|
...options,
|
||||||
signal: controller.signal,
|
signal: controller.signal,
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
"Content-Type": "application/json",
|
||||||
'Accept': 'application/json',
|
Accept: "application/json",
|
||||||
...options.headers,
|
...options.headers,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -31,12 +31,15 @@ async function getHomepageData() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Tentative avec retry
|
|
||||||
for (let attempt = 1; attempt <= 3; attempt++) {
|
for (let attempt = 1; attempt <= 3; attempt++) {
|
||||||
try {
|
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) {
|
if (!response.ok) {
|
||||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||||
@ -45,24 +48,44 @@ async function getHomepageData() {
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
console.log("✅ [getHomepageData] Données récupérées avec succès");
|
console.log("✅ [getHomepageData] Données récupérées avec succès");
|
||||||
return data.data?.[0] ?? null;
|
return data.data?.[0] ?? null;
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`❌ [getHomepageData] Erreur tentative ${attempt}:`, error);
|
console.error(`❌ [getHomepageData] Erreur tentative ${attempt}:`, error);
|
||||||
|
|
||||||
if (attempt === 3) {
|
if (attempt === 3) {
|
||||||
// Dernière tentative échouée
|
|
||||||
console.error("🚨 [getHomepageData] Toutes les tentatives ont échoué");
|
console.error("🚨 [getHomepageData] Toutes les tentatives ont échoué");
|
||||||
return null;
|
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;
|
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() {
|
export default function HomePage() {
|
||||||
const [homepage, setHomepage] = useState<any>(null);
|
const [homepage, setHomepage] = useState<any>(null);
|
||||||
const apiUrl = getApiUrl();
|
const apiUrl = getApiUrl();
|
||||||
@ -71,29 +94,170 @@ export default function HomePage() {
|
|||||||
getHomepageData().then((data) => setHomepage(data));
|
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 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;
|
const imageUrl = homepage.photo?.url ? `${apiUrl}${homepage.photo.url}` : null;
|
||||||
|
|
||||||
return (
|
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">
|
<div className="mx-auto flex w-full min-w-0 max-w-5xl flex-col gap-3 px-4 pb-10 sm:px-6">
|
||||||
<h1 className="text-3xl font-headline font-extrabold italic tracking-tight text-gray-800 mb-4">{title}</h1>
|
{/* 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="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="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>
|
||||||
|
|
||||||
{imageUrl ? (
|
<div className="flex flex-col gap-3 text-center md:text-left">
|
||||||
<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">
|
<span className="font-headline text-[11px] font-bold uppercase tracking-[0.3em] text-secondary">
|
||||||
<img src={imageUrl} alt="Photo de profil" className="w-full h-full object-cover object-center" />
|
Portfolio · Étudiant 42 Perpignan
|
||||||
</div>
|
</span>
|
||||||
) : (
|
<h1
|
||||||
<div className="w-64 h-64 flex items-center justify-center bg-gray-500 text-gray-200 rounded-full shadow-md">
|
id="home-title"
|
||||||
Image indisponible
|
className="font-headline text-3xl font-extrabold tracking-tight text-on-surface md:text-4xl lg:text-5xl"
|
||||||
</div>
|
>
|
||||||
)}
|
{title}
|
||||||
|
</h1>
|
||||||
|
|
||||||
<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">
|
{cv && (
|
||||||
<ReactMarkdown>{cv}</ReactMarkdown>
|
<div
|
||||||
</div>
|
className="prose prose-sm max-w-none font-body text-on-surface-variant sm:prose-base
|
||||||
</main>
|
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>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<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'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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -3,76 +3,180 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { getApiUrl } from "../utils/getApiUrl";
|
import { getApiUrl } from "../utils/getApiUrl";
|
||||||
import Carousel from "../components/Carousel";
|
import VignetteCarousel from "../components/VignetteCarousel";
|
||||||
import "../assets/main.css";
|
import "../assets/main.css";
|
||||||
import "../globals.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() {
|
export default function Page() {
|
||||||
const [projects, setProjects] = useState([]);
|
const [projects, setProjects] = useState([]);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const apiUrl = getApiUrl();
|
const apiUrl = getApiUrl();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function fetchProjects() {
|
async function fetchProjects() {
|
||||||
try {
|
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) {
|
if (!response.ok) {
|
||||||
throw new Error(`Erreur de récupération des projets : ${response.statusText}`);
|
throw new Error(`Erreur de récupération des projets : ${response.statusText}`);
|
||||||
}
|
}
|
||||||
const data = await response.json();
|
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(
|
||||||
const sortedProjects = (data.data ?? []).sort((a, b) => (a.order || 999) - (b.order || 999));
|
(a, b) => (a.order || 999) - (b.order || 999)
|
||||||
|
);
|
||||||
|
|
||||||
setProjects(sortedProjects);
|
setProjects(sortedProjects);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("❌ Erreur lors de la récupération des projets :", error);
|
console.error("❌ Erreur lors de la récupération des projets :", error);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchProjects();
|
fetchProjects();
|
||||||
}, [apiUrl]);
|
}, [apiUrl]);
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="w-full p-3 mt-5 mb-5">
|
<div className="mx-auto flex w-full min-w-0 max-w-6xl flex-col gap-5 px-4 pb-10 sm:px-6">
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-[repeat(auto-fit,minmax(300px,1fr))] gap-7 max-w-7xl mx-auto mobile-landscape">
|
{/* En-tête éditorial, aligné sur le hero de la home (kicker + titre Manrope). */}
|
||||||
{projects.map((project) => {
|
<section
|
||||||
const pictures = project.picture ?? [];
|
className="rounded-sheet bg-surface-container-lowest/85 p-5 shadow-ambient backdrop-blur-vellum sm:p-7 md:p-8"
|
||||||
const images = pictures.map((img) => ({
|
aria-labelledby="portfolio-title"
|
||||||
url: `${apiUrl}${img.url}`,
|
>
|
||||||
alt: img.name || "Project image",
|
<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
|
||||||
return (
|
</span>
|
||||||
<div
|
<h1
|
||||||
key={project.id}
|
id="portfolio-title"
|
||||||
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"
|
className="font-headline text-3xl font-extrabold tracking-tight text-on-surface md:text-4xl lg:text-5xl"
|
||||||
>
|
>
|
||||||
<Link href={`/portfolio/${project.slug}`}>
|
Les projets qui m’ont construit
|
||||||
<div className="overflow-hidden w-full h-48 mb-4">
|
</h1>
|
||||||
{images.length > 1 ? (
|
<p className="font-body text-on-surface-variant sm:text-lg">
|
||||||
<Carousel images={images} className="h-48" />
|
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
|
||||||
<img
|
visuels.
|
||||||
src={images[0]?.url || "/placeholder.jpg"}
|
</p>
|
||||||
alt={images[0]?.alt || "Project image"}
|
|
||||||
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">{project.name}</p>
|
|
||||||
<p className="text-gray-700 text-sm font-headline hover:text-base transition-all duration-200 ease-in-out">
|
|
||||||
{project.description}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
</section>
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
|
{/* É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 || `Visuel du projet ${project.name}`,
|
||||||
|
}));
|
||||||
|
const firstImage = images[0];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
key={project.id}
|
||||||
|
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`}
|
||||||
|
>
|
||||||
|
<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">
|
||||||
|
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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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) {
|
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();
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,34 +1,145 @@
|
|||||||
# API LLM et chatbot (GrasBot)
|
# 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
|
## Chaîne côté navigateur
|
||||||
|
|
||||||
1. `app/components/ChatBot.js` appelle `askAI(question)` (`app/utils/askAI.js`).
|
1. FAB `GrasBotFab` (monté dans `app/layout.tsx`) affiche `ChatBot.js`.
|
||||||
2. `askAI` envoie un **GET** vers **`/api/proxy?q=...`** (route Next.js App Router).
|
2. `ChatBot.js` appelle `askAI(question)` (`app/utils/askAI.js`).
|
||||||
3. `app/api/proxy/route.js` appelle en dur **`https://llmapi.fernandgrascalvet.com/ask?q=...`** et renvoie le corps JSON tel quel.
|
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`**.
|
- `rag.py` → remplacé par `search.py`.
|
||||||
- Requête **POST** vers Ollama : `http://localhost:11434/api/generate` avec JSON `model: "mistral"`, `prompt`, `stream: false`.
|
- `index_vault.py` → plus d'étape d'indexation (lecture directe du vault).
|
||||||
- Dépendances : voir `CONFIGURATION_SITE.md` (FastAPI, uvicorn, requests).
|
|
||||||
|
|
||||||
## Fichiers clés
|
## Modèle Ollama
|
||||||
|
|
||||||
```
|
| Rôle | Modèle | VRAM | Commande |
|
||||||
app/utils/askAI.js
|
|------|--------|------|----------|
|
||||||
app/api/proxy/route.js
|
| Chat | `qwen3:8b` | ~5 Go (Q4_K_M) | `ollama pull qwen3:8b` |
|
||||||
app/components/ChatBot.js
|
|
||||||
llm-api/api.py
|
**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.
|
||||||
|
|||||||
@ -1,26 +1,91 @@
|
|||||||
# Outils `strapi_extraction/`
|
# 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 l’API 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 |
|
```
|
||||||
|---------|----------------|
|
API Strapi
|
||||||
| `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. |
|
extract-api-data.js → extract/raw/*.json
|
||||||
| `update-documentation.js` | Mise à jour de la doc générée. |
|
│
|
||||||
| `analyse-site-architecture.js` | Analyse d’architecture du site. |
|
▼
|
||||||
|
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`
|
## Scripts
|
||||||
- `logs/last-update-summary.json`, `docs/generation-summary.json`
|
|
||||||
|
|
||||||
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 l’en-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)
|
||||||
|
|||||||
230
docs-site-interne/08-vault-obsidian-retrieval.md
Normal file
230
docs-site-interne/08-vault-obsidian-retrieval.md
Normal 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.
|
||||||
@ -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. |
|
| [05-environnement-scripts.md](./05-environnement-scripts.md) | Env, scripts PowerShell. |
|
||||||
| [06-strapi-extraction.md](./06-strapi-extraction.md) | Outils `strapi_extraction/`. |
|
| [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/`. |
|
| [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). |
|
| [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. |
|
| [etat-actuel.md](./etat-actuel.md) | État et dette technique. |
|
||||||
| [feuille-de-route.md](./feuille-de-route.md) | Backlog priorisé. |
|
| [feuille-de-route.md](./feuille-de-route.md) | Backlog priorisé. |
|
||||||
|
| [REFONTE-VISUELLE.md](./REFONTE-VISUELLE.md) | Journal de bord de la refonte UI Stitch. |
|
||||||
|
|
||||||
## Arborescence utile
|
## Arborescence utile
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
# Refonte visuelle — Direction "Digital Atelier"
|
# Refonte visuelle — Direction "Digital Atelier"
|
||||||
|
|
||||||
**Créé :** 2026-04-22
|
**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`).
|
**Source d'inspiration :** `stitch_V1/` (design newsletter Stitch — `DESIGN.md` et `code.html`).
|
||||||
**Audit préalable :** [`captures/AUDIT-VISUEL.md`](./captures/AUDIT-VISUEL.md).
|
**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) |
|
| 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) |
|
| 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) |
|
| 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 |
|
| 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*` | à faire |
|
| 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` | à 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` | **fait** (2026-04-22) |
|
||||||
| 8 | Contact + Footer éditorial | `app/contact/page.js`, `app/components/ContactForm.tsx`, `app/components/Footer.jsx` | à faire |
|
| 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
|
## 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.
|
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)
|
## 5. Checklist relecture (à passer à la fin de chaque étape)
|
||||||
|
|
||||||
- [ ] Aucune colonne unique globale `max-w-xl` (c'est le format newsletter).
|
- [ ] 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).
|
- [ ] La hiérarchie Manrope / Newsreader est respectée (pas de Orbitron résiduel).
|
||||||
- [ ] Les CTAs principaux ont `shadow-jewel`.
|
- [ ] Les CTAs principaux ont `shadow-jewel`.
|
||||||
- [ ] Radius Stitch (`rounded-sheet` / `rounded-tile`) utilisés sur les cartes de la refonte.
|
- [ ] 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).
|
||||||
|
|||||||
@ -100,9 +100,9 @@ Les captures suivantes **n’ont 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` |
|
| 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` |
|
| 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` |
|
| 16 | Glossaire modal desktop | `/competences/slug` | `16-competences-glossaire-ouvert-desktop.webp` | `OK` |
|
||||||
| 17 | Contact formulaire desktop | `/contact` | `17-contact-formulaire-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` | `OK` |
|
| 18 | Contact formulaire mobile | `/contact` | `18-contact-formulaire-mobile.webp` | `fait` (étape 8, 2026-04-22) |
|
||||||
| 19 | Footer desktop | `/` | `19-layout-footer-desktop.webp` | `OK` |
|
| 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` |
|
| 20 | Compteur visites desktop | `/` | `20-layout-compteur-visites-desktop.webp` | `OK` |
|
||||||
| 21 | Admin messages desktop | `/admin/messages` | `21-admin-messages-desktop.webp` | `OK` |
|
| 21 | Admin messages desktop | `/admin/messages` | `21-admin-messages-desktop.webp` | `OK` |
|
||||||
|
|
||||||
@ -110,4 +110,6 @@ Les captures suivantes **n’ont pas révélé de problème spécifique** après
|
|||||||
|
|
||||||
## Suite
|
## 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 l’utilisateur 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.
|
||||||
|
|||||||
@ -1,15 +1,16 @@
|
|||||||
# État actuel du site
|
# É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
|
## 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.
|
- **Strapi** avec content-types : homepage, projects, competences, messages, glossaire ; médias et texte riche.
|
||||||
- **Formulaire contact** : POST vers Strapi `messages`.
|
- **Formulaire contact** : POST vers Strapi `messages`.
|
||||||
- **Chatbot GrasBot** : proxy Next vers API LLM hébergée (`llmapi.fernandgrascalvet.com`).
|
- **Chatbot GrasBot v3** : FAB global (`GrasBotFab.tsx`) → proxy Next → API LLM hébergée (`llmapi.fernandgrascalvet.com`).
|
||||||
- **FastAPI + Ollama** dans le dépôt pour usage local ou serveur ; modèle `mistral` dans `llm-api/api.py`.
|
- **FastAPI + Ollama** dans `llm-api/` : modèle `qwen3:8b`, pipeline `search.py` (graph + BM25 sur vault Obsidian `vault-grasbot/`, sans embeddings).
|
||||||
- **Scripts** d’extraction et de doc dans `strapi_extraction/`.
|
- **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`.
|
- Documentation opérationnelle : `CONFIGURATION_SITE.md`.
|
||||||
- **Captures d'écran** de référence (WebP) : `docs-site-interne/captures/` — voir `captures/INDEX.md`.
|
- **Captures d'écran** de référence (WebP) : `docs-site-interne/captures/` — voir `captures/INDEX.md`.
|
||||||
|
|
||||||
|
|||||||
@ -8,10 +8,11 @@ Document vivant : ajuster les statuts et dates au fil du travail.
|
|||||||
|
|
||||||
| ID | Sujet | Statut | Notes |
|
| ID | Sujet | Statut | Notes |
|
||||||
|----|--------|--------|--------|
|
|----|--------|--------|--------|
|
||||||
| R1 | Moderniser l’UI (design system, cohérence typo/couleurs) | En cours | Direction "Digital Atelier" inspirée de `stitch_V1/` ; cadrage et plan dans [`REFONTE-VISUELLE.md`](./REFONTE-VISUELLE.md). Étapes 1-4 (tokens + garde-fou + migration typo globale + layout racine) faites le 2026-04-22. |
|
| R1 | Moderniser l’UI (design system, cohérence typo/couleurs) | En cours | Direction "Digital Atelier" inspirée de `stitch_V1/` ; cadrage et plan dans [`REFONTE-VISUELLE.md`](./REFONTE-VISUELLE.md). Étapes 1-7 (tokens + garde-fou + migration typo globale + layout racine + home + listes portfolio/compétences + fiches détail & GrasBot) faites le 2026-04-22. Reste l'étape 8 (contact + footer éditorial). |
|
||||||
| R2 | Homogénéiser TS vs JS dans `app/` | À faire | Migrer progressivement les `.jsx`/`.js` |
|
| 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 |
|
| 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 |
|
| 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
|
## 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 — 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 — é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 — 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`). |
|
||||||
|
|||||||
Binary file not shown.
@ -1,16 +1,81 @@
|
|||||||
from fastapi import FastAPI
|
"""API FastAPI de GrasBot — orchestre le retrieval et Ollama.
|
||||||
import requests
|
|
||||||
|
|
||||||
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")
|
@app.get("/ask")
|
||||||
async def ask_question(q: str):
|
async def ask_question(q: str):
|
||||||
data = {
|
"""Endpoint historique consommé par `app/utils/askAI.js`.
|
||||||
"model": "mistral",
|
|
||||||
"prompt": q,
|
Le front lit `data.response` : on conserve cette clé pour la compatibilité.
|
||||||
"stream": False
|
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
13
llm-api/requirements.txt
Normal 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
637
llm-api/search.py
Normal 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", "où", "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()),
|
||||||
|
}
|
||||||
764
strapi_extraction/build-vault.py
Normal file
764
strapi_extraction/build-vault.py
Normal 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())
|
||||||
@ -89,9 +89,10 @@ export default {
|
|||||||
bebas: ['Bebas Neue', 'sans-serif'],
|
bebas: ['Bebas Neue', 'sans-serif'],
|
||||||
|
|
||||||
// Stitch "Digital Atelier" : Manrope pour titres/UI, Newsreader pour corps éditorial.
|
// Stitch "Digital Atelier" : Manrope pour titres/UI, Newsreader pour corps éditorial.
|
||||||
headline: ['Manrope', 'system-ui', 'sans-serif'],
|
// Variables CSS posées par next/font/google (voir app/fonts.ts + app/layout.tsx).
|
||||||
body: ['Newsreader', 'Georgia', 'serif'],
|
headline: ['var(--font-manrope)', 'system-ui', 'sans-serif'],
|
||||||
label: ['Manrope', 'system-ui', 'sans-serif'],
|
body: ['var(--font-newsreader)', 'Georgia', 'serif'],
|
||||||
|
label: ['var(--font-manrope)', 'system-ui', 'sans-serif'],
|
||||||
},
|
},
|
||||||
borderRadius: {
|
borderRadius: {
|
||||||
// Additifs : ne remplacent pas les radius Tailwind (xl, 2xl, etc.) pour ne rien casser.
|
// Additifs : ne remplacent pas les radius Tailwind (xl, 2xl, etc.) pour ne rien casser.
|
||||||
|
|||||||
43
vault-grasbot/00-MOC/MOC-Algorithmique.md
Normal file
43
vault-grasbot/00-MOC/MOC-Algorithmique.md
Normal 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 l’Intelligence Artificielle]] — _algorithmique, ecole-42, ia_
|
||||||
|
- [[competence|Mon expérience dans la domotique]] — _algorithmique, domotique, ia, reseau_
|
||||||
|
- [[impression-3d|Mon parcours dans l’impression 3D]] — _3d, algorithmique, reseau_
|
||||||
|
- [[netpractice|netpractice]] — _algorithmique, ecole-42, reseau_
|
||||||
|
- [[push-swap|push_swap]] — _algorithmique, ecole-42, reseau_
|
||||||
36
vault-grasbot/00-MOC/MOC-C.md
Normal file
36
vault-grasbot/00-MOC/MOC-C.md
Normal 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_
|
||||||
30
vault-grasbot/00-MOC/MOC-Competences.md
Normal file
30
vault-grasbot/00-MOC/MOC-Competences.md
Normal 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 l’Intelligence Artificielle]] — _algorithmique, ecole-42, ia_
|
||||||
|
- [[competence|Mon expérience dans la domotique]] — _algorithmique, domotique, ia, reseau_
|
||||||
|
- [[impression-3d|Mon parcours dans l’impression 3D]] — _3d, algorithmique, reseau_
|
||||||
33
vault-grasbot/00-MOC/MOC-Cpp.md
Normal file
33
vault-grasbot/00-MOC/MOC-Cpp.md
Normal 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_
|
||||||
33
vault-grasbot/00-MOC/MOC-Devops.md
Normal file
33
vault-grasbot/00-MOC/MOC-Devops.md
Normal 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_
|
||||||
37
vault-grasbot/00-MOC/MOC-Domotique.md
Normal file
37
vault-grasbot/00-MOC/MOC-Domotique.md
Normal 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_
|
||||||
50
vault-grasbot/00-MOC/MOC-Ecole-42.md
Normal file
50
vault-grasbot/00-MOC/MOC-Ecole-42.md
Normal 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 l’Intelligence 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_
|
||||||
34
vault-grasbot/00-MOC/MOC-Graphique.md
Normal file
34
vault-grasbot/00-MOC/MOC-Graphique.md
Normal 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_
|
||||||
36
vault-grasbot/00-MOC/MOC-Ia.md
Normal file
36
vault-grasbot/00-MOC/MOC-Ia.md
Normal 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 l’Intelligence Artificielle]] — _algorithmique, ecole-42, ia_
|
||||||
|
- [[competence|Mon expérience dans la domotique]] — _algorithmique, domotique, ia, reseau_
|
||||||
27
vault-grasbot/00-MOC/MOC-Parcours.md
Normal file
27
vault-grasbot/00-MOC/MOC-Parcours.md
Normal 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.*
|
||||||
43
vault-grasbot/00-MOC/MOC-Projets.md
Normal file
43
vault-grasbot/00-MOC/MOC-Projets.md
Normal 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_
|
||||||
51
vault-grasbot/00-MOC/MOC-Reseau.md
Normal file
51
vault-grasbot/00-MOC/MOC-Reseau.md
Normal 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 l’impression 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_
|
||||||
32
vault-grasbot/00-MOC/MOC-Securite.md
Normal file
32
vault-grasbot/00-MOC/MOC-Securite.md
Normal 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_
|
||||||
38
vault-grasbot/00-MOC/MOC-Systeme.md
Normal file
38
vault-grasbot/00-MOC/MOC-Systeme.md
Normal 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_
|
||||||
40
vault-grasbot/00-MOC/MOC-Technique.md
Normal file
40
vault-grasbot/00-MOC/MOC-Technique.md
Normal 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.
|
||||||
33
vault-grasbot/00-MOC/MOC-Web.md
Normal file
33
vault-grasbot/00-MOC/MOC-Web.md
Normal 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_
|
||||||
143
vault-grasbot/10-Projets/born2beroot.md
Normal file
143
vault-grasbot/10-Projets/born2beroot.md
Normal 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 à l’administration 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 d’administrateur système ou de cybersécurité.
|
||||||
|
|
||||||
|
## Détails du projet
|
||||||
|
|
||||||
|
Le projet Born2beroot de l’école 42 est un projet d’initiation à l’administration 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. L’objectif est de comprendre comment un système fonctionne, d’adopter les bonnes pratiques de sécurité et d’automatiser 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 d’une Machine Virtuelle
|
||||||
|
|
||||||
|
Installation d’un serveur sur une machine virtuelle (VirtualBox ou UTM selon l’OS utilisé).
|
||||||
|
|
||||||
|
Utilisation d’une image Debian (par défaut) ou AlmaLinux.
|
||||||
|
|
||||||
|
Apprentissage de la gestion d’un 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 d’accè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 l’espace disque.
|
||||||
|
Création et gestion de volumes logiques, permettant d’étendre le stockage facilement.
|
||||||
|
|
||||||
|
5️⃣ Automatisation et Surveillance du Système
|
||||||
|
|
||||||
|
Écriture d’un script de monitoring (monitoring.sh) affichant des informations essentielles :
|
||||||
|
|
||||||
|
Charge CPU
|
||||||
|
|
||||||
|
Utilisation mémoire et disque
|
||||||
|
|
||||||
|
Nombre d’utilisateurs 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 l’activité du serveur.
|
||||||
|
|
||||||
|
🚀 Livrables et Validation du Projet
|
||||||
|
|
||||||
|
Une machine virtuelle prête à l’emploi 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 d’un 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 l’espace disque.
|
||||||
|
|
||||||
|
✔ Apprentissage des bases de DevOps et des bonnes pratiques d’administration 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 l’industrie 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 d’un 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*
|
||||||
83
vault-grasbot/10-Projets/cpp-partie1.md
Normal file
83
vault-grasbot/10-Projets/cpp-partie1.md
Normal 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*
|
||||||
113
vault-grasbot/10-Projets/cpp-partie2.md
Normal file
113
vault-grasbot/10-Projets/cpp-partie2.md
Normal 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*
|
||||||
137
vault-grasbot/10-Projets/cub3d.md
Normal file
137
vault-grasbot/10-Projets/cub3d.md
Normal 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*
|
||||||
140
vault-grasbot/10-Projets/fract-ol.md
Normal file
140
vault-grasbot/10-Projets/fract-ol.md
Normal 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 d’explorer la programmation graphique en C, la manipulation des nombres complexes, et l’optimisation des calculs pour le rendu d’images. 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*
|
||||||
120
vault-grasbot/10-Projets/ft-irc.md
Normal file
120
vault-grasbot/10-Projets/ft-irc.md
Normal 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 d’assurer l’authentification 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*
|
||||||
130
vault-grasbot/10-Projets/ft-printf.md
Normal file
130
vault-grasbot/10-Projets/ft-printf.md
Normal 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…). L’objectif est d’implé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 l’utilisation 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 l’affichage 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 d’Implé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*
|
||||||
173
vault-grasbot/10-Projets/ft-transcendence.md
Normal file
173
vault-grasbot/10-Projets/ft-transcendence.md
Normal 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 d’implé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 l’exécution et le déploiement du projet.
|
||||||
|
|
||||||
|
Sécurisation des Connexions : Intégrer une authentification OAuth et gérer l’accè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 d’Implémentation
|
||||||
|
|
||||||
|
1️⃣ Déploiement de l’Infrastructure
|
||||||
|
|
||||||
|
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 d’utilisateurs 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 l’interface 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 d’acqué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*
|
||||||
146
vault-grasbot/10-Projets/get-next-line.md
Normal file
146
vault-grasbot/10-Projets/get-next-line.md
Normal 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*
|
||||||
147
vault-grasbot/10-Projets/inception.md
Normal file
147
vault-grasbot/10-Projets/inception.md
Normal 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 l’automatisation du déploiement. Ce projet permet d’acqué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*
|
||||||
100
vault-grasbot/10-Projets/libft.md
Normal file
100
vault-grasbot/10-Projets/libft.md
Normal 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, d’approfondir 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 d’un fichier binaire** (libft.a) utilisable dans d’autres 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 d’une maîtrise des fondamentaux en développement logiciel, d’une capacité à écrire du code performant et maintenable, et d’un 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 l’optimisation 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*
|
||||||
191
vault-grasbot/10-Projets/minishell.md
Normal file
191
vault-grasbot/10-Projets/minishell.md
Normal 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 d’exé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 l’historique 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 l’expansion 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 l’exé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 d’entré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*
|
||||||
141
vault-grasbot/10-Projets/minitalk.md
Normal file
141
vault-grasbot/10-Projets/minitalk.md
Normal 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 d’apprendre 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*
|
||||||
101
vault-grasbot/10-Projets/netpractice.md
Normal file
101
vault-grasbot/10-Projets/netpractice.md
Normal 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*
|
||||||
123
vault-grasbot/10-Projets/philosopher.md
Normal file
123
vault-grasbot/10-Projets/philosopher.md
Normal 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 d’apprendre la gestion des threads (pthread), l’utilisation 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 d’acqué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 d’abord 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 s’assure qu’aucun 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 n’a 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 l’efficacité 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 l’optimisation 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*
|
||||||
74
vault-grasbot/10-Projets/presentation-ecole-42.md
Normal file
74
vault-grasbot/10-Projets/presentation-ecole-42.md
Normal 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*
|
||||||
160
vault-grasbot/10-Projets/push-swap.md
Normal file
160
vault-grasbot/10-Projets/push-swap.md
Normal 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). L’objectif est de trouver l’algorithme de tri le plus efficace pour minimiser le nombre de mouvements. Ce projet permet d’explorer la gestion des structures de données (piles), l’optimisation des algorithmes de tri, et la complexité algorithmique, tout en respectant des contraintes strictes d’exé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*
|
||||||
62
vault-grasbot/20-Competences/competence.md
Normal file
62
vault-grasbot/20-Competences/competence.md
Normal 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, j’ai entrepris d’explorer les différentes technologies permettant de connecter, centraliser et contrôler efficacement les objets intelligents au sein d’un écosystème domestique unifié.
|
||||||
|
|
||||||
|
Convaincu que la domotique doit rester abordable et accessible, j’ai recherché des solutions qui allient simplicité d’installation, compatibilité et flexibilité. Très rapidement, je me suis intéressé à l’écosystème Tuya, qui offre une large gamme d’appareils connectés tout en permettant une gestion centralisée. Bien que Tuya repose sur une architecture cloud, j’ai exploré des solutions open source permettant d’intégrer et d’interconnecter ces appareils tout en optimisant l’autonomie et la confidentialité des données.
|
||||||
|
|
||||||
|
Dans cette démarche, j’ai approfondi l’utilisation de Home Assistant, une plateforme open-source puissante qui permet d’agréger et de gérer un nombre considérable de dispositifs connectés. L’un 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.).
|
||||||
|
|
||||||
|
J’ai également expérimenté les possibilités offertes par l’auto-hébergement des serveurs domotiques, réduisant ainsi la dépendance aux plateformes cloud et garantissant une meilleure maîtrise des données personnelles. L’intégration de protocoles de communication ouverts tels que Zigbee, MQTT ou Matter m’a permis d’explorer des alternatives plus résilientes, évolutives et personnalisables pour bâtir un environnement domotique intelligent, modulable et sécurisé.
|
||||||
|
|
||||||
|
Aujourd’hui, je continue d’approfondir mes connaissances dans ce domaine en explorant les nouvelles générations d’objets connectés, l’optimisation des scénarios d’automatisation avancés, ainsi que l’intégration de solutions basées sur l’intelligence artificielle afin d’améliorer l’adaptabilité et l’efficacité des systèmes domotiques. Mon objectif est d’atteindre un équilibre entre simplicité d’usage, 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*
|
||||||
@ -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 (Let’s Encrypt)
|
||||||
|
|
||||||
|
Déploiement & Infrastructure :
|
||||||
|
|
||||||
|
Système d’exploitation : 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 d’un serveur web (IIS, Windows Server 2025)
|
||||||
|
|
||||||
|
Gestion des certificats SSL avec Win-ACME (HTTPS, sécurité)
|
||||||
|
|
||||||
|
Base de données (PostgreSQL ou MySQL)
|
||||||
|
|
||||||
|
Surveillance et gestion des performances (logs IIS, monitoring)
|
||||||
|
|
||||||
|
3️⃣ Cloud & DevOps (partiellement) ☁️
|
||||||
|
|
||||||
|
Même si ce projet n’utilise pas un service cloud public (Azure, AWS, GCP), il comporte des éléments liés à l’automatisation et à la gestion des déploiements.
|
||||||
|
|
||||||
|
Déploiement d’une application Next.js & Strapi sur un serveur dédié
|
||||||
|
|
||||||
|
Gestion des certificats SSL automatisée (Win-ACME, Let's Encrypt)
|
||||||
|
|
||||||
|
Possibilité d’extensions avec CI/CD pour automatiser les mises à jour
|
||||||
|
|
||||||
|
4️⃣ Sécurité Informatique 🔒
|
||||||
|
|
||||||
|
Avec l’implémentation du HTTPS, de l’authentification API et de la gestion des accès via Strapi et IIS, ce projet a aussi un aspect cybersécurité.
|
||||||
|
|
||||||
|
Chiffrement des connexions avec SSL/TLS (HTTPS activé)
|
||||||
|
|
||||||
|
Protection des API (Cors, Access-Control-Allow-Origin, JWT si activé dans Strapi)
|
||||||
|
|
||||||
|
Gestion des permissions et authentification des utilisateurs (Strapi)
|
||||||
|
|
||||||
|
5️⃣ Expérience Utilisateur & SEO 📈
|
||||||
|
|
||||||
|
Le projet est conçu pour être rapide, interactif et optimisé pour le référencement.
|
||||||
|
|
||||||
|
SEO optimisé avec Next.js (Static Generation, Server-Side Rendering)
|
||||||
|
|
||||||
|
Performance améliorée grâce au préchargement et à la mise en cache
|
||||||
|
|
||||||
|
Expérience utilisateur fluide avec des animations et une navigation rapide
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*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*
|
||||||
61
vault-grasbot/20-Competences/ia.md
Normal file
61
vault-grasbot/20-Competences/ia.md
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
---
|
||||||
|
title: Mon Exploration et Maîtrise de l’Intelligence Artificielle
|
||||||
|
slug: ia
|
||||||
|
type: competence
|
||||||
|
source: strapi/competences
|
||||||
|
domains: [algorithmique, ecole-42, ia]
|
||||||
|
tags: [tri]
|
||||||
|
aliases:
|
||||||
|
- mon exploration et maîtrise de l’intelligence 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, j’ai découvert l’intelligence artificielle grand public avec l’arrivée de ChatGPT, qui a marqué un tournant décisif dans l’accessibilité et la démocratisation de cette technologie. En l’espace de quelques mois, le domaine a connu une expansion fulgurante, avec l’émergence d’une multitude de solutions exploitant l’IA sous diverses formes. Fasciné par ces avancées, j’ai rapidement développé un vif intérêt pour plusieurs applications, notamment la génération d’images, les chatbots intelligents et plus largement les modèles de langage avancés (LLMs).
|
||||||
|
|
||||||
|
Dans cette quête d’exploration, j’ai expérimenté des solutions d’IA locale, notamment avec Ollama, LLM Studio, et d’autres outils permettant une plus grande maîtrise et personnalisation des modèles. Mon objectif a été de comprendre en profondeur les capacités d’intégration de ces intelligences artificielles locales, en explorant l’entraînement de modèles personnalisés, l’optimisation des performances et l’affinement des interactions par l’ingénierie des prompts (cliquez sur IA locale test mistral 7b sur mon serveur).
|
||||||
|
|
||||||
|
Actuellement, je suis en phase d’installation et de déploiement de solutions d’IA locale sur mon propre serveur, un projet en cours de développement qui me permet d’expérimenter les configurations avancées et d’adapter ces modèles à des cas d’usage spécifiques. Cette démarche s’inscrit dans une volonté de maîtriser l’IA en environnement auto-hébergé, offrant ainsi une meilleure compréhension de la gestion des ressources, du fine-tuning des modèles et des défis liés à l’infrastructure.
|
||||||
|
|
||||||
|
Parallèlement, j’ai entrepris une spécialisation en Data Science et Intelligence Artificielle au sein de l’École 42, afin d’approfondir mes connaissances théoriques et pratiques dans ce domaine en perpétuelle évolution. Cette formation me permet d’aller encore plus loin dans l’analyse des algorithmes de machine learning et deep learning, d’explorer des approches avancées en traitement des données massives, et de perfectionner mes compétences en développement et intégration de solutions IA appliquées.
|
||||||
|
|
||||||
|
Animé par une passion pour l’intelligence artificielle et ses innombrables possibilités, je continue de m’informer, d’expérimenter et d’appliquer ces technologies à des projets concrets. Mon objectif est d’acquérir une expertise approfondie pour concevoir des systèmes intelligents performants, adaptables et innovants, tout en restant à la pointe des avancées technologiques.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*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*
|
||||||
63
vault-grasbot/20-Competences/impression-3d.md
Normal file
63
vault-grasbot/20-Competences/impression-3d.md
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
---
|
||||||
|
title: Mon parcours dans l’impression 3D
|
||||||
|
slug: impression-3d
|
||||||
|
type: competence
|
||||||
|
source: strapi/competences
|
||||||
|
domains: [3d, algorithmique, reseau]
|
||||||
|
tags: [tri]
|
||||||
|
aliases:
|
||||||
|
- mon parcours dans l’impression 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
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
J’ai découvert l’univers fascinant de l’impression 3D en 2018, ce qui a immédiatement éveillé ma curiosité pour cette technologie en pleine expansion. Désireux d’en apprendre davantage, j’ai entrepris mes premières expérimentations en impression FDM (Fused Deposition Modeling) en utilisant une imprimante Alfawise U30. Cette première immersion m’a permis de me familiariser avec les fondamentaux de l’impression 3D, notamment la calibration de la machine, la compréhension des paramètres d’impression et l’optimisation des premiers prototypes.
|
||||||
|
|
||||||
|
En 2020, j’ai enrichi mon expérience en intégrant à mon parc une Sidewinder X2, une imprimante plus performante qui m’a offert la possibilité d’explorer davantage les subtilités des différents slicers disponibles sur le marché, tels que Ultimaker Cura, PrusaSlicer et OrcaSlicer. Parallèlement, j’ai approfondi mes compétences en modélisation 3D en me formant à l’utilisation de logiciels spécialisés, en particulier Fusion 360, qui constitue aujourd’hui un outil incontournable dans mon flux de travail.
|
||||||
|
|
||||||
|
L’apprentissage des différents firmwares, notamment Marlin et Klipper, a constitué une étape essentielle de mon évolution. J’ai ainsi acquis des compétences approfondies dans la configuration et l’optimisation des paramètres machines, ce qui m’a permis de mieux comprendre leur fonctionnement, d’assurer leur maintenance et d’intervenir efficacement en cas de dysfonctionnement.
|
||||||
|
|
||||||
|
Aujourd’hui, mon expertise s’est consolidée grâce à l’utilisation d’imprimantes 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 l’impression 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 d’impression spécifiques.
|
||||||
|
|
||||||
|
Grâce à ces expériences, j’ai développé une parfaite connaissance des conditions optimales requises pour chaque matériau, telles que la gestion des températures, l’adhérence au plateau, l’hygrométrie, la ventilation ou encore la gestion du warping. Mon parcours m’a ainsi permis d’acquérir une solide autonomie dans l’exploitation des imprimantes 3D, tant sur le plan technique que logiciel, et d’approfondir ma compréhension des défis liés à l’impression de pièces complexes ou fonctionnelles.
|
||||||
|
|
||||||
|
Aujourd’hui passionné par l’impression 3D, je continue d’explorer les innovations du secteur, de perfectionner mes compétences et d’expérimenter de nouvelles approches afin d’optimiser 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*
|
||||||
165
vault-grasbot/30-Parcours/cv-grascalvet-fernand.md
Normal file
165
vault-grasbot/30-Parcours/cv-grascalvet-fernand.md
Normal 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.
|
||||||
88
vault-grasbot/50-Technique/architecture-site.md
Normal file
88
vault-grasbot/50-Technique/architecture-site.md
Normal 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.
|
||||||
137
vault-grasbot/50-Technique/grasbot-retrieval.md
Normal file
137
vault-grasbot/50-Technique/grasbot-retrieval.md
Normal 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.
|
||||||
132
vault-grasbot/50-Technique/vault-structure.md
Normal file
132
vault-grasbot/50-Technique/vault-structure.md
Normal 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
65
vault-grasbot/README.md
Normal 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
152
vault-grasbot/TAXONOMIE.md
Normal 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 |
|
||||||
|
|---|---|
|
||||||
|
| 9–10 | Notes stratégiques (CV, MOCs principaux) |
|
||||||
|
| 6–8 | Notes riches (compétences, projets emblématiques) |
|
||||||
|
| 5 | Défaut (la plupart des projets 42) |
|
||||||
|
| 3–4 | Notes techniques internes (auto-doc) |
|
||||||
|
| 1–2 | 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
|
||||||
Loading…
x
Reference in New Issue
Block a user