mirror of
https://github.com/Ladebeze66/devsite.git
synced 2026-05-11 16:56:26 +02:00
ajout_section_ia
This commit is contained in:
parent
a0e59442f4
commit
a2c0c78590
89
app/Competences/[slug]/[realisation]/page.tsx
Normal file
89
app/Competences/[slug]/[realisation]/page.tsx
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useParams } from "next/navigation";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import ContentSection from "../../../components/ContentSection";
|
||||||
|
import { getApiUrl } from "../../../utils/getApiUrl";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Page détail d'une réalisation liée à une compétence.
|
||||||
|
*
|
||||||
|
* Route : `/competences/[slug]/[realisation]`
|
||||||
|
* - `slug` : slug de la compétence parente (ex. `ia`).
|
||||||
|
* - `realisation` : slug de la réalisation (ex. `grasbot`, `newsletter-ia`…).
|
||||||
|
*
|
||||||
|
* Rendu : on réutilise intégralement `ContentSection` (même carousel Swiper,
|
||||||
|
* même prose Markdown Newsreader, même CTA jewel, même skeleton, même état
|
||||||
|
* 404) avec la collection Strapi `realisation-ias` et un bouton retour qui
|
||||||
|
* renvoie vers la page vignettes de la compétence parente plutôt que vers le
|
||||||
|
* portfolio.
|
||||||
|
*
|
||||||
|
* Le nom exact de la compétence parente est fetché pour personnaliser le
|
||||||
|
* kicker (ex. *"Réalisation · Mon exploration et maîtrise de l'IA"*). Si le
|
||||||
|
* fetch échoue, on tombe silencieusement sur un libellé générique
|
||||||
|
* *"Réalisation · Compétence"*.
|
||||||
|
*/
|
||||||
|
export default function RealisationDetailPage() {
|
||||||
|
const params = useParams();
|
||||||
|
const competenceSlug =
|
||||||
|
typeof params?.slug === "string" ? params.slug : null;
|
||||||
|
const realisationSlug =
|
||||||
|
typeof params?.realisation === "string" ? params.realisation : null;
|
||||||
|
|
||||||
|
const [competenceName, setCompetenceName] = useState<string | null>(null);
|
||||||
|
const apiUrl = getApiUrl();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!competenceSlug) return;
|
||||||
|
|
||||||
|
let cancelled = false;
|
||||||
|
|
||||||
|
async function fetchCompetenceName() {
|
||||||
|
try {
|
||||||
|
const res = await fetch(
|
||||||
|
`${apiUrl}/api/competences?filters[slug][$eq]=${encodeURIComponent(
|
||||||
|
competenceSlug!
|
||||||
|
)}`
|
||||||
|
);
|
||||||
|
if (!res.ok) return;
|
||||||
|
const data = await res.json();
|
||||||
|
const name: string | undefined = data?.data?.[0]?.name;
|
||||||
|
if (!cancelled && name) {
|
||||||
|
setCompetenceName(name);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// silencieux : le kicker restera générique
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchCompetenceName();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, [apiUrl, competenceSlug]);
|
||||||
|
|
||||||
|
if (!competenceSlug || !realisationSlug) {
|
||||||
|
return (
|
||||||
|
<div className="mx-auto max-w-6xl px-4 py-10 text-center text-on-surface-variant">
|
||||||
|
<p className="font-body italic">⏳ Chargement...</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const backHref = `/competences/${competenceSlug}`;
|
||||||
|
const kickerLabel = competenceName
|
||||||
|
? `Réalisation · ${competenceName}`
|
||||||
|
: "Réalisation · Compétence";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ContentSection
|
||||||
|
collection="realisation-ias"
|
||||||
|
slug={realisationSlug}
|
||||||
|
backHref={backHref}
|
||||||
|
backLabel="Réalisations"
|
||||||
|
kickerLabel={kickerLabel}
|
||||||
|
notFoundLabel="Cette réalisation est introuvable."
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -2,21 +2,277 @@
|
|||||||
|
|
||||||
import { useParams } from "next/navigation";
|
import { useParams } from "next/navigation";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { getApiUrl } from "../../utils/getApiUrl";
|
||||||
|
import VignetteCarousel from "../../components/VignetteCarousel";
|
||||||
import ContentSectionCompetencesContainer from "../../components/ContentSectionCompetencesContainer";
|
import ContentSectionCompetencesContainer from "../../components/ContentSectionCompetencesContainer";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Page détail d'une compétence — double rendu selon le contenu Strapi.
|
||||||
|
*
|
||||||
|
* Cas 1 — la compétence est liée à une ou plusieurs `realisation-ia` :
|
||||||
|
* on affiche une **grille de vignettes** alignée sur le pattern éditorial
|
||||||
|
* de `app/portfolio/page.jsx` (grille asymétrique 2/3 + 1/3 alternée). Chaque
|
||||||
|
* vignette est cliquable (lien externe si le champ `link` de la réalisation
|
||||||
|
* est renseigné, sinon lien interne vers la future page détail
|
||||||
|
* `/competences/[slug]/[realisation]` — pour l'instant 404 tant que cette
|
||||||
|
* route n'est pas ajoutée, ce qui est attendu pour l'étape de test du listing).
|
||||||
|
*
|
||||||
|
* Cas 2 — aucune réalisation liée :
|
||||||
|
* on conserve le rendu historique `ContentSectionCompetencesContainer` qui
|
||||||
|
* affiche le `content` richtext de la compétence. Les compétences Web / 3D
|
||||||
|
* restent donc strictement inchangées tant qu'on ne leur associe pas de
|
||||||
|
* réalisation côté Strapi.
|
||||||
|
*
|
||||||
|
* Endpoint Strapi utilisé : `/api/realisation-ias` (pluralName par défaut
|
||||||
|
* Strapi 5 pour un singularName `realisation-ia`). Filtre sur le slug de la
|
||||||
|
* compétence parente via la relation `competences` (many-to-many d'après
|
||||||
|
* le content-type créé côté admin).
|
||||||
|
*/
|
||||||
|
const spanPattern = ["md:col-span-4", "md:col-span-2", "md:col-span-2", "md:col-span-4"];
|
||||||
|
|
||||||
|
type Realisation = {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
slug?: string;
|
||||||
|
description?: string;
|
||||||
|
link?: string;
|
||||||
|
order?: number;
|
||||||
|
picture?: Array<{ url?: string; name?: string }>;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Competence = {
|
||||||
|
id: number;
|
||||||
|
name?: string;
|
||||||
|
slug?: string;
|
||||||
|
content?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export default function CompetencePage() {
|
export default function CompetencePage() {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const [slug, setSlug] = useState<string | null>(null);
|
const slug = typeof params?.slug === "string" ? params.slug : null;
|
||||||
|
|
||||||
|
const [competence, setCompetence] = useState<Competence | null>(null);
|
||||||
|
const [realisations, setRealisations] = useState<Realisation[] | null>(null);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const apiUrl = getApiUrl();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (params?.slug) {
|
if (!slug) return;
|
||||||
setSlug(params.slug as string);
|
|
||||||
|
let cancelled = false;
|
||||||
|
|
||||||
|
async function fetchData() {
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. Compétence parente (pour le titre de la page vignettes).
|
||||||
|
const resCompet = await fetch(
|
||||||
|
`${apiUrl}/api/competences?filters[slug][$eq]=${encodeURIComponent(
|
||||||
|
slug!
|
||||||
|
)}&populate=*`
|
||||||
|
);
|
||||||
|
const dataCompet = resCompet.ok ? await resCompet.json() : null;
|
||||||
|
const fetchedCompetence: Competence | null =
|
||||||
|
dataCompet?.data?.[0] ?? null;
|
||||||
|
|
||||||
|
// 2. Réalisations IA liées à cette compétence.
|
||||||
|
// Si l'endpoint n'existe pas encore côté Strapi (404), on bascule
|
||||||
|
// silencieusement sur le rendu historique au lieu de crasher.
|
||||||
|
let fetchedRealisations: Realisation[] = [];
|
||||||
|
const resReal = await fetch(
|
||||||
|
`${apiUrl}/api/realisation-ias?filters[competences][slug][$eq]=${encodeURIComponent(
|
||||||
|
slug!
|
||||||
|
)}&populate=picture&sort=order:asc`
|
||||||
|
);
|
||||||
|
if (resReal.ok) {
|
||||||
|
const dataReal = await resReal.json();
|
||||||
|
fetchedRealisations = (dataReal?.data ?? []).sort(
|
||||||
|
(a: Realisation, b: Realisation) =>
|
||||||
|
(a.order ?? 999) - (b.order ?? 999)
|
||||||
|
);
|
||||||
|
} else if (resReal.status !== 404) {
|
||||||
|
// 404 = content-type absent, cas légitime → on log seulement les
|
||||||
|
// vraies erreurs HTTP (500, 403, etc.).
|
||||||
|
console.warn(
|
||||||
|
`⚠️ [competences/${slug}] realisation-ias HTTP ${resReal.status}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}, [params]);
|
|
||||||
|
if (!cancelled) {
|
||||||
|
setCompetence(fetchedCompetence);
|
||||||
|
setRealisations(fetchedRealisations);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (!cancelled) {
|
||||||
|
console.error("❌ [competences/[slug]] Erreur fetch :", err);
|
||||||
|
setRealisations([]);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (!cancelled) setIsLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchData();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, [apiUrl, slug]);
|
||||||
|
|
||||||
if (!slug) {
|
if (!slug) {
|
||||||
return <div className="text-center text-gray-500">⏳ Chargement...</div>;
|
return (
|
||||||
|
<div className="mx-auto max-w-6xl px-4 py-10 text-center text-on-surface-variant">
|
||||||
|
<p className="font-body italic">⏳ Chargement...</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return <ContentSectionCompetencesContainer collection="competences" slug={slug} />;
|
// Squelette commun pendant le premier fetch (avant de savoir s'il y a des vignettes ou pas).
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="mx-auto flex w-full min-w-0 max-w-6xl flex-col gap-5 px-4 pb-10 sm:px-6">
|
||||||
|
<section className="rounded-sheet bg-surface-container-lowest/60 p-6 shadow-ambient-sm backdrop-blur-vellum sm:p-8">
|
||||||
|
<div className="h-3 w-24 animate-pulse rounded-full bg-surface-container-low/80" />
|
||||||
|
<div className="mt-4 h-8 w-2/3 animate-pulse rounded-full bg-surface-container-low/80" />
|
||||||
|
<div className="mt-3 h-4 w-full animate-pulse rounded-full bg-surface-container-low/60" />
|
||||||
|
</section>
|
||||||
|
<div className="grid grid-cols-1 gap-5 sm:grid-cols-2 md:grid-cols-6">
|
||||||
|
{Array.from({ length: 4 }).map((_, idx) => (
|
||||||
|
<div
|
||||||
|
key={idx}
|
||||||
|
className={`${spanPattern[idx % spanPattern.length]} rounded-sheet bg-surface-container-lowest/60 p-5 shadow-ambient-sm backdrop-blur-vellum`}
|
||||||
|
>
|
||||||
|
<div className="aspect-[4/3] w-full animate-pulse rounded-tile bg-surface-container-low/80" />
|
||||||
|
<div className="mt-4 h-5 w-2/3 animate-pulse rounded-full bg-surface-container-low/80" />
|
||||||
|
<div className="mt-2 h-4 w-full animate-pulse rounded-full bg-surface-container-low/60" />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pas de réalisations associées → rendu historique (toutes les compétences hors IA).
|
||||||
|
if (!realisations || realisations.length === 0) {
|
||||||
|
return (
|
||||||
|
<ContentSectionCompetencesContainer collection="competences" slug={slug} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rendu vignettes.
|
||||||
|
return (
|
||||||
|
<div className="mx-auto flex w-full min-w-0 max-w-6xl flex-col gap-5 px-4 pb-10 sm:px-6">
|
||||||
|
<section
|
||||||
|
className="rounded-sheet bg-surface-container-lowest/85 p-5 shadow-ambient backdrop-blur-vellum sm:p-7 md:p-8"
|
||||||
|
aria-labelledby="realisations-title"
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-3 text-center md:text-left">
|
||||||
|
<Link
|
||||||
|
href="/competences"
|
||||||
|
className="inline-flex items-center gap-1.5 self-center font-headline text-xs font-bold uppercase tracking-[0.3em] text-primary transition hover:text-primary/80 md:self-start"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="material-symbols-outlined text-lg"
|
||||||
|
aria-hidden="true"
|
||||||
|
translate="no"
|
||||||
|
>
|
||||||
|
arrow_back
|
||||||
|
</span>
|
||||||
|
Retour aux compétences
|
||||||
|
</Link>
|
||||||
|
<span className="font-headline text-[11px] font-bold uppercase tracking-[0.3em] text-secondary">
|
||||||
|
Compétence · Réalisations
|
||||||
|
</span>
|
||||||
|
<h1
|
||||||
|
id="realisations-title"
|
||||||
|
className="font-headline text-3xl font-extrabold tracking-tight text-on-surface md:text-4xl lg:text-5xl"
|
||||||
|
>
|
||||||
|
{competence?.name ?? "Compétence"}
|
||||||
|
</h1>
|
||||||
|
<p className="font-body text-on-surface-variant sm:text-lg">
|
||||||
|
Une sélection de réalisations qui illustrent cette compétence en
|
||||||
|
contexte — ouvrez une vignette pour en voir le détail.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 gap-5 sm:grid-cols-2 md:grid-cols-6">
|
||||||
|
{realisations.map((realisation, idx) => {
|
||||||
|
const pictures = realisation.picture ?? [];
|
||||||
|
const images = pictures.map((img) => ({
|
||||||
|
url: img?.url ? `${apiUrl}${img.url}` : "/placeholder.jpg",
|
||||||
|
alt: img?.name || `Visuel de la réalisation ${realisation.name}`,
|
||||||
|
}));
|
||||||
|
const firstImage = images[0];
|
||||||
|
|
||||||
|
// Comportement voulu : la vignette renvoie TOUJOURS vers la fiche
|
||||||
|
// détail interne (`/competences/[slug]/[realisation]`). Le lien
|
||||||
|
// externe `realisation.link` est exposé en tant que CTA jewel en bas
|
||||||
|
// de la fiche détail par `ContentSection`, pas en court-circuit
|
||||||
|
// depuis la vignette. Cohérent avec le comportement des fiches
|
||||||
|
// `project` du portfolio.
|
||||||
|
const href = realisation.slug
|
||||||
|
? `/competences/${slug}/${realisation.slug}`
|
||||||
|
: "#";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
key={realisation.id}
|
||||||
|
href={href}
|
||||||
|
className={`${spanPattern[idx % spanPattern.length]} group flex flex-col overflow-hidden rounded-sheet bg-surface-container-lowest/85 shadow-ambient backdrop-blur-vellum transition duration-300 hover:-translate-y-0.5 hover:shadow-jewel focus:outline-none focus-visible:ring-2 focus-visible:ring-primary`}
|
||||||
|
>
|
||||||
|
<div className="relative aspect-[4/3] w-full overflow-hidden bg-surface-container-low">
|
||||||
|
{images.length > 1 ? (
|
||||||
|
<VignetteCarousel images={images} />
|
||||||
|
) : firstImage ? (
|
||||||
|
<img
|
||||||
|
src={firstImage.url}
|
||||||
|
alt={firstImage.alt}
|
||||||
|
className="h-full w-full object-cover transition-transform duration-500 group-hover:scale-[1.03]"
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="flex h-full w-full items-center justify-center text-sm text-on-surface-variant">
|
||||||
|
<span
|
||||||
|
className="material-symbols-outlined text-3xl"
|
||||||
|
aria-hidden="true"
|
||||||
|
translate="no"
|
||||||
|
>
|
||||||
|
image
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-1 flex-col gap-2 p-5 sm:p-6">
|
||||||
|
<span className="font-headline text-[10px] font-bold uppercase tracking-[0.3em] text-secondary">
|
||||||
|
Réalisation
|
||||||
|
</span>
|
||||||
|
<h2 className="font-headline text-xl font-extrabold leading-tight tracking-tight text-primary">
|
||||||
|
{realisation.name}
|
||||||
|
</h2>
|
||||||
|
{realisation.description && (
|
||||||
|
<p className="font-body line-clamp-3 text-sm leading-relaxed text-on-surface-variant sm:text-base">
|
||||||
|
{realisation.description}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<span className="mt-auto inline-flex items-center gap-1.5 pt-3 font-headline text-sm font-bold uppercase tracking-[0.2em] text-primary">
|
||||||
|
Découvrir
|
||||||
|
<span
|
||||||
|
className="material-symbols-outlined text-lg transition-transform duration-300 group-hover:translate-x-1"
|
||||||
|
aria-hidden="true"
|
||||||
|
translate="no"
|
||||||
|
>
|
||||||
|
arrow_forward
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -33,8 +33,12 @@ export default function Page() {
|
|||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
|
// Strapi v4 : `attributes.order` — Strapi v5 (souvent) : `order` à la racine.
|
||||||
|
const getOrder = (item) =>
|
||||||
|
item?.order ?? item?.attributes?.order ?? 999;
|
||||||
|
|
||||||
const sortedCompetences = (data.data ?? []).sort(
|
const sortedCompetences = (data.data ?? []).sort(
|
||||||
(a, b) => ((a.attributes?.order ?? 999) - (b.attributes?.order ?? 999))
|
(a, b) => getOrder(a) - getOrder(b)
|
||||||
);
|
);
|
||||||
|
|
||||||
setCompetences(sortedCompetences);
|
setCompetences(sortedCompetences);
|
||||||
|
|||||||
@ -19,7 +19,18 @@ interface ImageData {
|
|||||||
|
|
||||||
interface ContentData {
|
interface ContentData {
|
||||||
name: string;
|
name: string;
|
||||||
Resum: string;
|
/**
|
||||||
|
* Champ richtext Markdown de la fiche.
|
||||||
|
*
|
||||||
|
* Dette historique : le content-type Strapi `project` utilise `Resum` avec
|
||||||
|
* majuscule (legacy Strapi 4). Les nouveaux content-types (ex.
|
||||||
|
* `realisation-ia`) utilisent `resum` en minuscule, cohérent avec tous les
|
||||||
|
* autres champs (`name`, `slug`, `link`, `order`…). On tolère les deux
|
||||||
|
* orthographes dans le rendu pour ne pas avoir à renommer `Resum` côté
|
||||||
|
* `project` (ce qui casserait les 15+ fiches projet déjà saisies).
|
||||||
|
*/
|
||||||
|
Resum?: string;
|
||||||
|
resum?: string;
|
||||||
picture?: ImageData[];
|
picture?: ImageData[];
|
||||||
link?: string;
|
link?: string;
|
||||||
linkText?: string;
|
linkText?: string;
|
||||||
@ -30,6 +41,26 @@ interface ContentSectionProps {
|
|||||||
slug: string;
|
slug: string;
|
||||||
titleClass?: string;
|
titleClass?: string;
|
||||||
contentClass?: string;
|
contentClass?: string;
|
||||||
|
/**
|
||||||
|
* Lien du bouton retour discret posé en haut de la page.
|
||||||
|
* Défaut : `/portfolio` (comportement historique pour les fiches projet).
|
||||||
|
*/
|
||||||
|
backHref?: string;
|
||||||
|
/**
|
||||||
|
* Libellé du bouton retour. Défaut : `"Portfolio"`.
|
||||||
|
*/
|
||||||
|
backLabel?: string;
|
||||||
|
/**
|
||||||
|
* Kicker affiché au-dessus du titre dans l'en-tête vellum.
|
||||||
|
* Défaut : `"Projet · Portfolio"` (fiches du portfolio).
|
||||||
|
* Exemple pour une réalisation de compétence : `"Réalisation · Compétence IA"`.
|
||||||
|
*/
|
||||||
|
kickerLabel?: string;
|
||||||
|
/**
|
||||||
|
* Message affiché dans l'état 404 (fiche introuvable).
|
||||||
|
* Défaut : `"Ce projet est introuvable."`
|
||||||
|
*/
|
||||||
|
notFoundLabel?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -41,10 +72,21 @@ interface ContentSectionProps {
|
|||||||
* héritées du composant pré-refonte restent acceptées pour compatibilité mais
|
* héritées du composant pré-refonte restent acceptées pour compatibilité mais
|
||||||
* sont ignorées (styles tokenisés désormais) — on les garde dans l'interface
|
* sont ignorées (styles tokenisés désormais) — on les garde dans l'interface
|
||||||
* pour ne pas casser les consommateurs.
|
* pour ne pas casser les consommateurs.
|
||||||
|
*
|
||||||
|
* 2026-04-23 : composant rendu paramétrable pour être réutilisé par la page
|
||||||
|
* détail des réalisations IA (`/competences/[slug]/[realisation]`) avec un
|
||||||
|
* retour vers la compétence parente au lieu du portfolio. Les 4 props
|
||||||
|
* `backHref` / `backLabel` / `kickerLabel` / `notFoundLabel` ont des défauts
|
||||||
|
* strictement identiques au comportement historique → 100 % rétro-compatible
|
||||||
|
* pour les fiches projet existantes.
|
||||||
*/
|
*/
|
||||||
export default function ContentSection({
|
export default function ContentSection({
|
||||||
collection,
|
collection,
|
||||||
slug,
|
slug,
|
||||||
|
backHref = "/portfolio",
|
||||||
|
backLabel = "Portfolio",
|
||||||
|
kickerLabel = "Projet · Portfolio",
|
||||||
|
notFoundLabel = "Ce projet est introuvable.",
|
||||||
}: ContentSectionProps) {
|
}: ContentSectionProps) {
|
||||||
const [data, setData] = useState<ContentData | null>(null);
|
const [data, setData] = useState<ContentData | null>(null);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
@ -89,10 +131,10 @@ export default function ContentSection({
|
|||||||
search_off
|
search_off
|
||||||
</span>
|
</span>
|
||||||
<p className="font-body italic text-on-surface-variant">
|
<p className="font-body italic text-on-surface-variant">
|
||||||
Ce projet est introuvable.
|
{notFoundLabel}
|
||||||
</p>
|
</p>
|
||||||
<Link
|
<Link
|
||||||
href="/portfolio"
|
href={backHref}
|
||||||
className="mt-5 inline-flex items-center gap-1.5 font-headline text-sm font-bold uppercase tracking-[0.2em] text-primary hover:underline"
|
className="mt-5 inline-flex items-center gap-1.5 font-headline text-sm font-bold uppercase tracking-[0.2em] text-primary hover:underline"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
@ -102,14 +144,16 @@ export default function ContentSection({
|
|||||||
>
|
>
|
||||||
arrow_back
|
arrow_back
|
||||||
</span>
|
</span>
|
||||||
Retour au portfolio
|
Retour
|
||||||
</Link>
|
</Link>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { name, Resum: richText, picture, link, linkText } = data;
|
const { name, picture, link, linkText } = data;
|
||||||
|
// Legacy `Resum` (content-type `project`) OU `resum` moderne (nouveaux content-types).
|
||||||
|
const richText = data.Resum ?? data.resum ?? "";
|
||||||
|
|
||||||
const images =
|
const images =
|
||||||
picture?.map((img: ImageData) => ({
|
picture?.map((img: ImageData) => ({
|
||||||
@ -121,7 +165,7 @@ export default function ContentSection({
|
|||||||
<div className="mx-auto flex w-full min-w-0 max-w-3xl flex-col gap-5 px-4 pb-10 sm:px-6">
|
<div className="mx-auto flex w-full min-w-0 max-w-3xl flex-col gap-5 px-4 pb-10 sm:px-6">
|
||||||
{/* Bouton retour discret, posé sur le wallpaper comme une miette de fil d'Ariane. */}
|
{/* Bouton retour discret, posé sur le wallpaper comme une miette de fil d'Ariane. */}
|
||||||
<Link
|
<Link
|
||||||
href="/portfolio"
|
href={backHref}
|
||||||
className="inline-flex w-fit items-center gap-1.5 rounded-full bg-surface-container-lowest/70 px-3 py-1.5 font-headline text-xs font-bold uppercase tracking-[0.2em] text-primary backdrop-blur-vellum transition-colors hover:bg-surface-container-lowest/95 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary"
|
className="inline-flex w-fit items-center gap-1.5 rounded-full bg-surface-container-lowest/70 px-3 py-1.5 font-headline text-xs font-bold uppercase tracking-[0.2em] text-primary backdrop-blur-vellum transition-colors hover:bg-surface-container-lowest/95 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
@ -131,7 +175,7 @@ export default function ContentSection({
|
|||||||
>
|
>
|
||||||
arrow_back
|
arrow_back
|
||||||
</span>
|
</span>
|
||||||
Portfolio
|
{backLabel}
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
{/* En-tête "feuillet de vellum" aligné sur la home et les listes. */}
|
{/* En-tête "feuillet de vellum" aligné sur la home et les listes. */}
|
||||||
@ -141,7 +185,7 @@ export default function ContentSection({
|
|||||||
>
|
>
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
<span className="font-headline text-[11px] font-bold uppercase tracking-[0.3em] text-secondary">
|
<span className="font-headline text-[11px] font-bold uppercase tracking-[0.3em] text-secondary">
|
||||||
Projet · Portfolio
|
{kickerLabel}
|
||||||
</span>
|
</span>
|
||||||
<h1
|
<h1
|
||||||
id="project-title"
|
id="project-title"
|
||||||
|
|||||||
1
coffreobsidian
Submodule
1
coffreobsidian
Submodule
@ -0,0 +1 @@
|
|||||||
|
Subproject commit d2d513c23de79be2707d1267f5171c49cad55e36
|
||||||
@ -1,6 +1,6 @@
|
|||||||
# État actuel du site
|
# État actuel du site
|
||||||
|
|
||||||
**Dernière mise à jour :** 2026-04-22 (post-refonte GrasBot v3)
|
**Dernière mise à jour :** 2026-04-23 (post-tuning GrasBot + observabilité Langfuse)
|
||||||
|
|
||||||
## Ce qui est en place
|
## Ce qui est en place
|
||||||
|
|
||||||
@ -9,7 +9,8 @@
|
|||||||
- **Formulaire contact** : POST vers Strapi `messages`.
|
- **Formulaire contact** : POST vers Strapi `messages`.
|
||||||
- **Chatbot GrasBot v3** : FAB global (`GrasBotFab.tsx`) → proxy Next → API LLM hébergée (`llmapi.fernandgrascalvet.com`).
|
- **Chatbot GrasBot v3** : FAB global (`GrasBotFab.tsx`) → proxy Next → API LLM hébergée (`llmapi.fernandgrascalvet.com`).
|
||||||
- **FastAPI + Ollama** dans `llm-api/` : modèle `qwen3:8b`, pipeline `search.py` (graph + BM25 sur vault Obsidian `vault-grasbot/`, sans embeddings).
|
- **FastAPI + Ollama** dans `llm-api/` : modèle `qwen3:8b`, pipeline `search.py` (graph + BM25 sur vault Obsidian `vault-grasbot/`, sans embeddings).
|
||||||
- **Vault de connaissance `vault-grasbot/`** : 41 notes enrichies (aliases, answers, priority) — source de vérité du chatbot, régénéré depuis Strapi par `strapi_extraction/build-vault.py`.
|
- **Vault de connaissance `vault-grasbot/`** : 42 notes enrichies (aliases, answers, priority) — source de vérité du chatbot, régénéré depuis Strapi par `strapi_extraction/build-vault.py`. Inclut une note `bio-fernand` courte (priority 10) dédiée aux questions biographiques et un CV complet complémentaire.
|
||||||
|
- **Observabilité Langfuse** : instance self-hosted `langfuse.fernandgrascalvet.com`, instrumentation Python (`llm-api/observability.py`) traçant chaque requête `/ask` (retrieval / prompt_build / ollama-chat) avec session/user IDs anonymes côté front. Mode no-op automatique si les clés sont absentes. Voir [`langfuse-observability.md`](./langfuse-observability.md).
|
||||||
- **Scripts** d'extraction et de doc dans `strapi_extraction/`.
|
- **Scripts** d'extraction et de doc dans `strapi_extraction/`.
|
||||||
- Documentation opérationnelle : `CONFIGURATION_SITE.md`.
|
- Documentation opérationnelle : `CONFIGURATION_SITE.md`.
|
||||||
- **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`.
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
# Feuille de route
|
# Feuille de route
|
||||||
|
|
||||||
**Dernière mise à jour :** 2026-04-22
|
**Dernière mise à jour :** 2026-04-23
|
||||||
|
|
||||||
Document vivant : ajuster les statuts et dates au fil du travail.
|
Document vivant : ajuster les statuts et dates au fil du travail.
|
||||||
|
|
||||||
@ -56,4 +56,5 @@ Document vivant : ajuster les statuts et dates au fil du travail.
|
|||||||
| 2026-04-22 | **Chatbot GrasBot — migration Mistral → Qwen3 + RAG sur vault Obsidian local**. Passage du modèle `mistral` à `qwen3:8b` dans `llm-api/api.py` (Q4_K_M, ~5 Go VRAM RTX 2080 Ti). Embeddings via `nomic-embed-text` (~500 Mo VRAM, multilingue FR). Nouveau pipeline RAG : `llm-api/rag.py` (embed / retrieve / build_prompt / generate / answer), `llm-api/index_vault.py` (parse frontmatter YAML, chunking par h2 au-delà de 3000 chars, upsert ChromaDB batch 32), `llm-api/requirements.txt` (fastapi, uvicorn, requests, chromadb, pyyaml). Nouveau script `strapi_extraction/build-vault.py` qui convertit `strapi_extraction/docs/*.md` + CV PDF (via `pypdf`) en vault Obsidian structuré `vault-grasbot/` : frontmatter YAML (type, source, domains, tags, linked, related, visibility), wikilinks vers les MOCs, MOCs auto-générés par type et par domaine (15 MOCs). Bootstrap v1 du vault : 17 projets, 4 compétences, 1 CV, 15 MOCs auto + 1 manuel (Technique), 3 notes auto-doc dans `50-Technique/` (architecture-site, grasbot-rag, vault-structure) pour que GrasBot puisse se présenter lui-même. Compatibilité ascendante `askAI.js`/`ChatBot.js` via le champ `response` conservé ; les `sources`, `rag`, `model` ajoutés sont non destructifs. Endpoint `/health` ajouté pour debug. Doc : nouveau [`08-vault-obsidian-rag.md`](./docs-site-interne/08-vault-obsidian-rag.md), mise à jour de [`04-api-llm-et-chatbot.md`](./docs-site-interne/04-api-llm-et-chatbot.md) et [`06-strapi-extraction.md`](./docs-site-interne/06-strapi-extraction.md). Fragilités préexistantes repérées (cleaner `homepages` absent, content-type `glossaire` non extrait) consignées mais non corrigées dans ce lot — à traiter lors du prochain enrichissement vault. |
|
| 2026-04-22 | **Chatbot GrasBot — migration Mistral → Qwen3 + RAG sur vault Obsidian local**. Passage du modèle `mistral` à `qwen3:8b` dans `llm-api/api.py` (Q4_K_M, ~5 Go VRAM RTX 2080 Ti). Embeddings via `nomic-embed-text` (~500 Mo VRAM, multilingue FR). Nouveau pipeline RAG : `llm-api/rag.py` (embed / retrieve / build_prompt / generate / answer), `llm-api/index_vault.py` (parse frontmatter YAML, chunking par h2 au-delà de 3000 chars, upsert ChromaDB batch 32), `llm-api/requirements.txt` (fastapi, uvicorn, requests, chromadb, pyyaml). Nouveau script `strapi_extraction/build-vault.py` qui convertit `strapi_extraction/docs/*.md` + CV PDF (via `pypdf`) en vault Obsidian structuré `vault-grasbot/` : frontmatter YAML (type, source, domains, tags, linked, related, visibility), wikilinks vers les MOCs, MOCs auto-générés par type et par domaine (15 MOCs). Bootstrap v1 du vault : 17 projets, 4 compétences, 1 CV, 15 MOCs auto + 1 manuel (Technique), 3 notes auto-doc dans `50-Technique/` (architecture-site, grasbot-rag, vault-structure) pour que GrasBot puisse se présenter lui-même. Compatibilité ascendante `askAI.js`/`ChatBot.js` via le champ `response` conservé ; les `sources`, `rag`, `model` ajoutés sont non destructifs. Endpoint `/health` ajouté pour debug. Doc : nouveau [`08-vault-obsidian-rag.md`](./docs-site-interne/08-vault-obsidian-rag.md), mise à jour de [`04-api-llm-et-chatbot.md`](./docs-site-interne/04-api-llm-et-chatbot.md) et [`06-strapi-extraction.md`](./docs-site-interne/06-strapi-extraction.md). Fragilités préexistantes repérées (cleaner `homepages` absent, content-type `glossaire` non extrait) consignées mais non corrigées dans ce lot — à traiter lors du prochain enrichissement vault. |
|
||||||
| 2026-04-22 | Refonte visuelle — **étape 7 : fiches détail + glossaire + GrasBot flottant**. Cinq sous-lots. **7.a** : `Carousel.tsx` + `CarouselCompetences.tsx` harmonisés (pagination bullets primary via variables CSS Swiper, flèches primary, `rounded-tile shadow-ambient-sm`, autoplay 3500 ms + `loop` conditionnel, lightbox Stitch refaite avec voile `bg-on-surface/80`, image `object-contain rounded-sheet`, bouton close rond Material Symbol + Escape + verrouillage scroll body). **7.b** : `ContentSection.tsx` (fiche portfolio) — gabarit vellum cohérent avec la home/listes, pastille retour `arrow_back`, kicker `Projet · Portfolio`, titre Manrope, carousel détail plein cadre, corps Markdown en `prose` Stitch (mêmes overrides que la home, y compris pastille `prose-hr`), CTA externe jewel avec `open_in_new`, états loading/404 en vellum. **7.c** : `ContentSectionCompetences.tsx` + container — même gabarit. Refactor glossaire : styles inline `style="color:red/blue"` remplacés par les classes `.glossary-keyword` / `.chatbot-keyword` (ajoutées à `globals.css`, couleur primary + underline dotted offset 3 px). Event listeners `document.body.addEventListener` remplacés par un listener unique scopé au wrapper `contentRef`. Le clic sur « IA locale » ne monte plus un `<ChatBot>` local mais dispatch `window.dispatchEvent(new CustomEvent("grasbot:open"))` capté par le FAB global (7.e). Container refait avec skeleton vellum à la place de `⏳ Chargement...`. **7.d** : `ChatBot.js` entièrement restylé (carte vellum `rounded-sheet shadow-ambient backdrop-blur-vellum`, header primary avec Material Symbol `smart_toy` + sous-titre « Assistant IA locale », bulles user `bg-primary text-white` et bot `bg-surface-container`, input Stitch avec `focus-visible:ring-primary`, bouton envoyer rond jewel Material Symbol `send`, auto-scroll, focus auto, envoi Enter, disabled pendant attente, message d'accueil vide éditorial). **7.e** : nouveau composant `app/components/GrasBotFab.tsx` — FAB jewel `fixed bottom-6 right-6 z-30` rond 56/64 px, `bg-primary shadow-jewel` Material Symbol `smart_toy`/`close`, monté dans `app/layout.tsx` → chatbot accessible depuis **toutes les pages** (plus seulement fiches compétences). Écoute `CustomEvent("grasbot:open")` dispatché depuis le keyword « IA locale ». Fermeture Escape globale, panneau responsive plein largeur mobile / 384 px desktop. Détails dans `REFONTE-VISUELLE.md` §7. |
|
| 2026-04-22 | Refonte visuelle — **étape 7 : fiches détail + glossaire + GrasBot flottant**. Cinq sous-lots. **7.a** : `Carousel.tsx` + `CarouselCompetences.tsx` harmonisés (pagination bullets primary via variables CSS Swiper, flèches primary, `rounded-tile shadow-ambient-sm`, autoplay 3500 ms + `loop` conditionnel, lightbox Stitch refaite avec voile `bg-on-surface/80`, image `object-contain rounded-sheet`, bouton close rond Material Symbol + Escape + verrouillage scroll body). **7.b** : `ContentSection.tsx` (fiche portfolio) — gabarit vellum cohérent avec la home/listes, pastille retour `arrow_back`, kicker `Projet · Portfolio`, titre Manrope, carousel détail plein cadre, corps Markdown en `prose` Stitch (mêmes overrides que la home, y compris pastille `prose-hr`), CTA externe jewel avec `open_in_new`, états loading/404 en vellum. **7.c** : `ContentSectionCompetences.tsx` + container — même gabarit. Refactor glossaire : styles inline `style="color:red/blue"` remplacés par les classes `.glossary-keyword` / `.chatbot-keyword` (ajoutées à `globals.css`, couleur primary + underline dotted offset 3 px). Event listeners `document.body.addEventListener` remplacés par un listener unique scopé au wrapper `contentRef`. Le clic sur « IA locale » ne monte plus un `<ChatBot>` local mais dispatch `window.dispatchEvent(new CustomEvent("grasbot:open"))` capté par le FAB global (7.e). Container refait avec skeleton vellum à la place de `⏳ Chargement...`. **7.d** : `ChatBot.js` entièrement restylé (carte vellum `rounded-sheet shadow-ambient backdrop-blur-vellum`, header primary avec Material Symbol `smart_toy` + sous-titre « Assistant IA locale », bulles user `bg-primary text-white` et bot `bg-surface-container`, input Stitch avec `focus-visible:ring-primary`, bouton envoyer rond jewel Material Symbol `send`, auto-scroll, focus auto, envoi Enter, disabled pendant attente, message d'accueil vide éditorial). **7.e** : nouveau composant `app/components/GrasBotFab.tsx` — FAB jewel `fixed bottom-6 right-6 z-30` rond 56/64 px, `bg-primary shadow-jewel` Material Symbol `smart_toy`/`close`, monté dans `app/layout.tsx` → chatbot accessible depuis **toutes les pages** (plus seulement fiches compétences). Écoute `CustomEvent("grasbot:open")` dispatché depuis le keyword « IA locale ». Fermeture Escape globale, panneau responsive plein largeur mobile / 384 px desktop. Détails dans `REFONTE-VISUELLE.md` §7. |
|
||||||
| 2026-04-22 | Refonte visuelle — **étape 6 : listes portfolio + compétences**. `app/portfolio/page.jsx` et `app/competences/page.jsx` entièrement réécrits. En-tête éditorial (kicker + titre Manrope extrabold + pitch Newsreader) cohérent avec le hero de la home. Grille **asymétrique 2/3 + 1/3** alternée (`md:grid-cols-6` + pattern de `col-span-4`/`col-span-2` sur modulo 4, `sm:grid-cols-2`, `grid-cols-1` mobile) — conforme DESIGN.md §6 "No-Grid-Lock". Cartes « feuillet vellum » alignées home : `rounded-sheet bg-surface-container-lowest/85 backdrop-blur-vellum shadow-ambient`, image `aspect-[4/3]` fixe avec `group-hover:scale-[1.03]`, titre `text-primary`, description `line-clamp-3` en Newsreader, CTA tertiaire « Découvrir → » / « Explorer → » avec Material Symbol `arrow_forward` qui se décale au hover (`translate="no"` appliqué). Hover : `hover:-translate-y-0.5 hover:shadow-jewel` (remplace le `scale-105` qui débordait). **`Swiper` retiré des vignettes de liste** (arbitrage acté § 2 : carousel réservé aux galeries intra-fiche) — une seule image par carte, `loading="lazy"`. États ajoutés : skeletons animés respectant la grille + état vide avec Material Symbol. Régressions corrigées au passage : largeur fixe `w-80` qui débordait sur S25 Ultra, `hover:scale-105` qui tapait sous le header, classes `bg-white/80 rounded-lg` remplacées par les tokens Stitch. Les composants `Carousel.tsx` et `CarouselCompetences.tsx` restent en place pour les fiches détail (étape 7). Détail dans `REFONTE-VISUELLE.md` §6. |
|
| 2026-04-22 | Refonte visuelle — **étape 6 : listes portfolio + compétences**. `app/portfolio/page.jsx` et `app/competences/page.jsx` entièrement réécrits. En-tête éditorial (kicker + titre Manrope extrabold + pitch Newsreader) cohérent avec le hero de la home. Grille **asymétrique 2/3 + 1/3** alternée (`md:grid-cols-6` + pattern de `col-span-4`/`col-span-2` sur modulo 4, `sm:grid-cols-2`, `grid-cols-1` mobile) — conforme DESIGN.md §6 "No-Grid-Lock". Cartes « feuillet vellum » alignées home : `rounded-sheet bg-surface-container-lowest/85 backdrop-blur-vellum shadow-ambient`, image `aspect-[4/3]` fixe avec `group-hover:scale-[1.03]`, titre `text-primary`, description `line-clamp-3` en Newsreader, CTA tertiaire « Découvrir → » / « Explorer → » avec Material Symbol `arrow_forward` qui se décale au hover (`translate="no"` appliqué). Hover : `hover:-translate-y-0.5 hover:shadow-jewel` (remplace le `scale-105` qui débordait). **`Swiper` retiré des vignettes de liste** (arbitrage acté § 2 : carousel réservé aux galeries intra-fiche) — une seule image par carte, `loading="lazy"`. États ajoutés : skeletons animés respectant la grille + état vide avec Material Symbol. Régressions corrigées au passage : largeur fixe `w-80` qui débordait sur S25 Ultra, `hover:scale-105` qui tapait sous le header, classes `bg-white/80 rounded-lg` remplacées par les tokens Stitch. Les composants `Carousel.tsx` et `CarouselCompetences.tsx` restent en place pour les fiches détail (étape 7). Détail dans `REFONTE-VISUELLE.md` §6. |
|
||||||
|
| 2026-04-23 | **GrasBot — tuning pipeline LLM + anti-hallucinations**. Audit des premières traces Langfuse : questions biographiques hallucinées (âge erroné, statut inventé), réponses longues tronquées. Quatre ajustements : (1) `llm-api/search.py` · `generate()` — `num_ctx=8192` explicite (stoppe la troncature silencieuse du prompt par le défaut Ollama 2048/4096 quand plusieurs notes entières sont injectées), `num_predict` 512 → 1024 (réponses longues complètes), `think: false` top-level (désactive le *thinking mode* de qwen3 qui consommait du budget de sortie). (2) `llm-api/search.py` · `build_prompt()` — troncature conditionnelle des sources rank 2+ via `_truncate_body()` + nouvelles variables `SEARCH_SECONDARY_MAX_CHARS` (1500) / `SEARCH_SECONDARY_KEEP_RATIO` (0.8). Aucune source n'est supprimée, seules celles dont le score est < 0.8 × score(#1) ET dont le body dépasse 1500 chars sont résumées. Loggé dans `prompt_build.metadata.truncation`. (3) Vault — nouvelle note `vault-grasbot/30-Parcours/bio-fernand.md` courte et factuelle (priority 10, aliases biographiques courts), canonique pour les questions du type *« qui est Fernand »*. Renvoie vers le CV complet pour le détail. Correction incohérence d'âge dans le CV (46 → 47 ans dans la section Présentation) qui alimentait les hallucinations. (4) `SYSTEM_PROMPT` — nouveau bloc *Règles de fidélité aux sources* : priorité `type=parcours` pour questions bio, interdiction d'inventer des faits factuels, gestion explicite des contradictions, signalement des notes tronquées. **Bascule Langfuse v4 → v3 dans `requirements.txt`** (`langfuse>=3.0,<4`) : le SDK v4 a supprimé `start_as_current_span`, la v3 reste compatible avec l'instrumentation existante. Dépendances Python ajoutées : `langfuse`, `python-dotenv`. Secrets Langfuse déplacés de `.env.local` Next vers `llm-api/.env` (non committé). Doc mise à jour : [`langfuse-observability.md`](./langfuse-observability.md) (nouvelle section *Tuning du pipeline — 2026-04-23*), `CONFIGURATION_SITE.md` (endpoints `/health` + `/reload-vault`), `etat-actuel.md` (42 notes + mention Langfuse). |
|
||||||
| 2026-04-22 | **GrasBot v3 — bascule RAG vectoriel → retrieval graph + BM25**. Essais d'installation Windows bloqués par `chroma-hnswlib` (compilation C++ requise) et freezes RDP à chaque chargement de `qwen3:8b` + `nomic-embed-text` simultanément. Arbitrage : pour un vault de 40 notes, la RAG vectorielle sur-dimensionne ; on exploite directement la structure Obsidian (frontmatter, wikilinks, MOCs). **Nouveau pipeline** dans `llm-api/search.py` (scoring multi-signaux : aliases / titre-slug / answers / domains / tags / BM25 ; expansion par graphe via `linked`/`related`/wikilinks ; tokenizer FR avec normalisations `c++` → `cpp`, split `-`/`_`). **Déterministe, traçable (champ `reasons` dans les sources), 50 ms de retrieval**. Scoring calibré sur 12 cas (IA, push-swap, LLMs pluriel, hors-sujet clafoutis → `(aucun)`, etc.). **Dépendances allégées** : fini `chromadb`, `chroma-hnswlib`, `nomic-embed-text`. `requirements.txt` = fastapi + uvicorn + requests + pyyaml uniquement. Fichiers supprimés : `llm-api/rag.py`, `llm-api/index_vault.py`, `chroma-index/` (marqué pour suppression, verrouillé par Cursor au moment du cleanup — sera supprimé au reboot). **Vault enrichi** : `build-vault.py` étendu pour générer automatiquement `aliases` (à partir du slug/titre + `DOMAIN_ALIASES`), `answers` (questions-types adaptées au type de note), `priority` (heuristique CV=10, MOCs=7, compétences=7, projets=5). Note CV curatée (`source: manual`) enrichie manuellement avec 12 aliases et 7 answers. Nouvelle `vault-grasbot/TAXONOMIE.md` qui documente le vocabulaire contrôlé. Réécriture de `vault-grasbot/50-Technique/grasbot-rag.md` → `grasbot-retrieval.md` (nouveau pipeline), + `architecture-site.md` + `vault-structure.md` + `MOC-Technique.md`. Nouveau endpoint `POST /reload-vault` pour recharger sans redémarrer uvicorn. Documentation interne refaite : [`04-api-llm-et-chatbot.md`](./04-api-llm-et-chatbot.md), [`06-strapi-extraction.md`](./06-strapi-extraction.md), nouveau [`08-vault-obsidian-retrieval.md`](./08-vault-obsidian-retrieval.md) (remplace `08-vault-obsidian-rag.md`). |
|
| 2026-04-22 | **GrasBot v3 — bascule RAG vectoriel → retrieval graph + BM25**. Essais d'installation Windows bloqués par `chroma-hnswlib` (compilation C++ requise) et freezes RDP à chaque chargement de `qwen3:8b` + `nomic-embed-text` simultanément. Arbitrage : pour un vault de 40 notes, la RAG vectorielle sur-dimensionne ; on exploite directement la structure Obsidian (frontmatter, wikilinks, MOCs). **Nouveau pipeline** dans `llm-api/search.py` (scoring multi-signaux : aliases / titre-slug / answers / domains / tags / BM25 ; expansion par graphe via `linked`/`related`/wikilinks ; tokenizer FR avec normalisations `c++` → `cpp`, split `-`/`_`). **Déterministe, traçable (champ `reasons` dans les sources), 50 ms de retrieval**. Scoring calibré sur 12 cas (IA, push-swap, LLMs pluriel, hors-sujet clafoutis → `(aucun)`, etc.). **Dépendances allégées** : fini `chromadb`, `chroma-hnswlib`, `nomic-embed-text`. `requirements.txt` = fastapi + uvicorn + requests + pyyaml uniquement. Fichiers supprimés : `llm-api/rag.py`, `llm-api/index_vault.py`, `chroma-index/` (marqué pour suppression, verrouillé par Cursor au moment du cleanup — sera supprimé au reboot). **Vault enrichi** : `build-vault.py` étendu pour générer automatiquement `aliases` (à partir du slug/titre + `DOMAIN_ALIASES`), `answers` (questions-types adaptées au type de note), `priority` (heuristique CV=10, MOCs=7, compétences=7, projets=5). Note CV curatée (`source: manual`) enrichie manuellement avec 12 aliases et 7 answers. Nouvelle `vault-grasbot/TAXONOMIE.md` qui documente le vocabulaire contrôlé. Réécriture de `vault-grasbot/50-Technique/grasbot-rag.md` → `grasbot-retrieval.md` (nouveau pipeline), + `architecture-site.md` + `vault-structure.md` + `MOC-Technique.md`. Nouveau endpoint `POST /reload-vault` pour recharger sans redémarrer uvicorn. Documentation interne refaite : [`04-api-llm-et-chatbot.md`](./04-api-llm-et-chatbot.md), [`06-strapi-extraction.md`](./06-strapi-extraction.md), nouveau [`08-vault-obsidian-retrieval.md`](./08-vault-obsidian-retrieval.md) (remplace `08-vault-obsidian-rag.md`). |
|
||||||
|
|||||||
1
ft_linear_regression
Submodule
1
ft_linear_regression
Submodule
@ -0,0 +1 @@
|
|||||||
|
Subproject commit a5616622a3e4ac6b4b44d57c3b2c150f33476255
|
||||||
Loading…
x
Reference in New Issue
Block a user