Compare commits

...

4 Commits

Author SHA1 Message Date
ba34820ffc justify 2026-05-01 12:39:26 +02:00
ee79d56d66 history_chatbot 2026-05-01 12:30:39 +02:00
5021251dbf ajout_cv 2026-05-01 12:17:48 +02:00
5c4abb2118 makdown_chatbot 2026-05-01 12:05:10 +02:00
9 changed files with 210 additions and 23 deletions

View File

@ -1,8 +1,16 @@
"use client"; "use client";
import Link from "next/link"; import Link from "next/link";
import { useEffect, useRef, useState } from "react"; import { useEffect, useLayoutEffect, useRef, useState } from "react";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import { askAI } from "../utils/askAI"; import { askAI } from "../utils/askAI";
import {
clearGrasbotChatMessages,
loadGrasbotChatMessages,
newChatMessageId,
saveGrasbotChatMessages,
} from "../utils/grasbotChatStorage";
/** /**
* GrasBot UI du chatbot (Stitch). * GrasBot UI du chatbot (Stitch).
@ -18,6 +26,17 @@ import { askAI } from "../utils/askAI";
* info si réponse générale faute de contexte pertinent). * 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. * - Timeout 45 s côté fetch (géré dans `askAI.js`) avec message éditorial.
* *
* v3.2 (2026-04-26) :
* - Réponses bot rendues en Markdown (`ReactMarkdown` + `remark-gfm`) : gras,
* listes, liens cliquables. Texte justifié dans la bulle. Les messages
* utilisateur restent en texte brut.
*
* v3.3 (2026-04-26) :
* - Historique persisté en **localStorage** par `grasbot_user_id` (voir
* `grasbotChatStorage.js`) : même navigateur / effacement cookies selon usage,
* jusqu'à 80 messages récents. Aucune réinjection dans Ollama.
* - Bouton pour effacer la conversation locale.
*
* Design : * Design :
* - Fond `surface-container-lowest/95 backdrop-blur-vellum rounded-sheet shadow-ambient`. * - Fond `surface-container-lowest/95 backdrop-blur-vellum rounded-sheet shadow-ambient`.
* - Bulles : user = `bg-primary text-white` à droite, bot = `bg-surface-container` à gauche. * - Bulles : user = `bg-primary text-white` à droite, bot = `bg-surface-container` à gauche.
@ -27,10 +46,27 @@ import { askAI } from "../utils/askAI";
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 [hydrated, setHydrated] = useState(false);
const [isWaiting, setIsWaiting] = useState(false); const [isWaiting, setIsWaiting] = useState(false);
const scrollRef = useRef(null); const scrollRef = useRef(null);
const inputRef = useRef(null); const inputRef = useRef(null);
useLayoutEffect(() => {
setMessages(loadGrasbotChatMessages());
setHydrated(true);
}, []);
useEffect(() => {
if (!hydrated) return;
saveGrasbotChatMessages(messages);
}, [messages, hydrated]);
const handleClearHistory = () => {
setMessages([]);
clearGrasbotChatMessages();
inputRef.current?.focus();
};
useEffect(() => { useEffect(() => {
if (scrollRef.current) { if (scrollRef.current) {
scrollRef.current.scrollTop = scrollRef.current.scrollHeight; scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
@ -44,7 +80,7 @@ export default function ChatBot({ onClose }) {
const handleAsk = async () => { const handleAsk = async () => {
if (!question.trim() || isWaiting) return; if (!question.trim() || isWaiting) return;
const userMessage = { sender: "user", text: question }; const userMessage = { sender: "user", text: question, id: newChatMessageId() };
setMessages((prev) => [...prev, userMessage]); setMessages((prev) => [...prev, userMessage]);
setQuestion(""); setQuestion("");
setIsWaiting(true); setIsWaiting(true);
@ -59,6 +95,7 @@ export default function ChatBot({ onClose }) {
sources: payload.sources || [], sources: payload.sources || [],
grounded: Boolean(payload.grounded), grounded: Boolean(payload.grounded),
timeout: Boolean(payload._timeout), timeout: Boolean(payload._timeout),
id: newChatMessageId(),
}, },
]); ]);
} catch (_error) { } catch (_error) {
@ -70,6 +107,7 @@ export default function ChatBot({ onClose }) {
sources: [], sources: [],
grounded: false, grounded: false,
error: true, error: true,
id: newChatMessageId(),
}, },
]); ]);
} finally { } finally {
@ -102,20 +140,34 @@ export default function ChatBot({ onClose }) {
</p> </p>
</div> </div>
</div> </div>
<button <div className="flex shrink-0 items-center gap-1">
type="button" <button
onClick={onClose} type="button"
aria-label="Fermer le chat" onClick={handleClearHistory}
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" disabled={messages.length === 0}
> aria-label="Effacer l'historique de conversation"
<span title="Effacer l'historique"
className="material-symbols-outlined" 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 disabled:pointer-events-none disabled:opacity-40"
aria-hidden="true"
translate="no"
> >
close <span className="material-symbols-outlined text-xl" aria-hidden="true" translate="no">
</span> delete_sweep
</button> </span>
</button>
<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> </div>
<div <div
@ -129,10 +181,11 @@ export default function ChatBot({ onClose }) {
)} )}
{messages.map((msg, index) => { {messages.map((msg, index) => {
const msgKey = msg.id ?? `legacy-${index}`;
if (msg.sender === "user") { if (msg.sender === "user") {
return ( return (
<div <div
key={index} key={msgKey}
className="ml-auto max-w-[80%] rounded-sheet bg-primary px-3 py-2 font-headline text-xs leading-relaxed text-white" className="ml-auto max-w-[80%] rounded-sheet bg-primary px-3 py-2 font-headline text-xs leading-relaxed text-white"
> >
{msg.text} {msg.text}
@ -140,9 +193,39 @@ export default function ChatBot({ onClose }) {
); );
} }
return ( return (
<div key={index} className="mr-auto flex max-w-[85%] flex-col gap-1.5"> <div key={msgKey} 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"> <div
{msg.text} className="rounded-sheet bg-surface-container px-3 py-2 text-on-surface [&>*:first-child]:mt-0 [&>*:last-child]:mb-0"
>
<ReactMarkdown
remarkPlugins={[remarkGfm]}
className="prose prose-sm max-w-none text-justify font-body text-xs leading-relaxed text-on-surface
prose-p:my-2 prose-p:text-xs prose-p:leading-relaxed
prose-strong:font-semibold prose-strong:text-on-surface
prose-ul:my-2 prose-ol:my-2 prose-li:my-0.5 prose-li:text-xs
prose-a:break-words prose-a:text-primary prose-a:no-underline hover:prose-a:underline
prose-headings:font-headline prose-headings:text-sm prose-headings:text-on-surface prose-headings:my-2
prose-code:rounded prose-code:bg-surface-container-low prose-code:px-1 prose-code:text-[11px]
prose-pre:my-2 prose-pre:bg-surface-container-low prose-pre:text-[11px]"
components={{
a({ href, children, ...props }) {
const external =
typeof href === "string" && /^https?:\/\//i.test(href);
return (
<a
href={href}
{...props}
target={external ? "_blank" : undefined}
rel={external ? "noopener noreferrer" : undefined}
>
{children}
</a>
);
},
}}
>
{msg.text}
</ReactMarkdown>
</div> </div>
{(msg.sources?.length > 0 || msg.grounded !== undefined) && !msg.error && !msg.timeout && ( {(msg.sources?.length > 0 || msg.grounded !== undefined) && !msg.error && !msg.timeout && (
<BotFooter sources={msg.sources} grounded={msg.grounded} /> <BotFooter sources={msg.sources} grounded={msg.grounded} />

View File

@ -212,7 +212,7 @@ export default function ContentSection({
<div <div
className="prose prose-sm mt-5 max-w-none font-body text-on-surface-variant sm:prose-base 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-headings:font-headline prose-headings:text-primary
prose-p:font-body prose-p:text-on-surface-variant prose-p:font-body prose-p:text-left prose-p:text-on-surface-variant md:prose-p:text-justify
prose-strong:text-on-surface prose-strong:text-on-surface
prose-a:text-primary prose-a:no-underline hover:prose-a:underline prose-a:text-primary prose-a:no-underline hover:prose-a:underline
prose-li:marker:text-primary prose-li:marker:text-primary

View File

@ -219,7 +219,7 @@ export default function ContentSectionCompetences({
ref={contentRef} ref={contentRef}
className="prose prose-sm mt-5 max-w-none font-body text-on-surface-variant sm:prose-base 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-headings:font-headline prose-headings:text-primary
prose-p:font-body prose-p:text-on-surface-variant prose-p:font-body prose-p:text-left prose-p:text-on-surface-variant md:prose-p:text-justify
prose-strong:text-on-surface prose-strong:text-on-surface
prose-a:text-primary prose-a:no-underline hover:prose-a:underline prose-a:text-primary prose-a:no-underline hover:prose-a:underline
prose-li:marker:text-primary prose-li:marker:text-primary

View File

@ -170,6 +170,19 @@ export default function HomePage() {
</div> </div>
)} )}
<p className="mt-1 text-center md:text-left">
<a
href="/cv/cv-fernand-grascalvet.pdf"
download="cv-fernand-grascalvet.pdf"
className="inline-flex items-center gap-1.5 font-headline text-xs font-semibold text-primary underline decoration-primary/40 underline-offset-2 transition-colors hover:text-primary-container hover:decoration-primary"
>
<span className="material-symbols-outlined text-base" aria-hidden="true" translate="no">
picture_as_pdf
</span>
Télécharger le CV (PDF)
</a>
</p>
<div className="mt-2 flex flex-col items-stretch gap-3 sm:flex-row sm:justify-center sm:items-center md:justify-start"> <div className="mt-2 flex flex-col items-stretch gap-3 sm:flex-row sm:justify-center sm:items-center md:justify-start">
<Link <Link
href="/portfolio" href="/portfolio"

View File

@ -0,0 +1,86 @@
/**
* Persistance locale du fil GrasBot (sans envoi à Ollama).
*
* Clé : `grasbot_chat_v1:<user_id>` `user_id` est celui de `grasbotIds.js`.
* Limite : les derniers messages uniquement pour éviter quota localStorage (~5 Mo).
*/
import { getGrasbotUserId } from "./grasbotIds";
export const GRASBOT_CHAT_STORAGE_VERSION = 1;
export const GRASBOT_CHAT_MAX_MESSAGES = 80;
function storageKey(userId) {
return `grasbot_chat_v${GRASBOT_CHAT_STORAGE_VERSION}:${userId}`;
}
function safeRandomId() {
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
return crypto.randomUUID();
}
return `m_${Date.now()}_${Math.random().toString(16).slice(2)}`;
}
/**
* @param {unknown} m
* @returns {object | null}
*/
function sanitizeMessage(m) {
if (!m || typeof m !== "object") return null;
if (m.sender !== "user" && m.sender !== "bot") return null;
if (typeof m.text !== "string") return null;
const out = { sender: m.sender, text: m.text };
if (typeof m.id === "string") out.id = m.id;
if (m.sender === "bot") {
if (Array.isArray(m.sources)) out.sources = m.sources;
if (typeof m.grounded === "boolean") out.grounded = m.grounded;
if (typeof m.timeout === "boolean") out.timeout = m.timeout;
if (typeof m.error === "boolean") out.error = m.error;
}
return out;
}
/** @returns {object[]} */
export function loadGrasbotChatMessages() {
if (typeof window === "undefined") return [];
try {
const uid = getGrasbotUserId();
if (!uid) return [];
const raw = window.localStorage.getItem(storageKey(uid));
if (!raw) return [];
const parsed = JSON.parse(raw);
if (!Array.isArray(parsed)) return [];
return parsed.map(sanitizeMessage).filter(Boolean);
} catch {
return [];
}
}
/** @param {object[]} messages */
export function saveGrasbotChatMessages(messages) {
if (typeof window === "undefined") return;
try {
const uid = getGrasbotUserId();
if (!uid) return;
const trimmed = messages.slice(-GRASBOT_CHAT_MAX_MESSAGES);
const serialized = trimmed.map(sanitizeMessage).filter(Boolean);
window.localStorage.setItem(storageKey(uid), JSON.stringify(serialized));
} catch {
// quota / mode privé strict
}
}
export function clearGrasbotChatMessages() {
if (typeof window === "undefined") return;
try {
const uid = getGrasbotUserId();
if (!uid) return;
window.localStorage.removeItem(storageKey(uid));
} catch {
/* ignore */
}
}
export function newChatMessageId() {
return safeRandomId();
}

View File

@ -593,13 +593,15 @@ Ton rôle :
Règles de ton : Règles de ton :
- Réponds en français, ton sobre et précis, sans emojis. - 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]). - N'inclus **pas** de références entre crochets du type `[slug-de-la-note]` dans ta réponse : l'interface liste déjà les fiches du vault sous le message (liens cliquables). Tu peux nommer un projet ou une compétence dans la phrase si utile, sans notation `[slug]`.
- Pour les **liens externes** (site personnel, GitHub, LinkedIn, etc.), utilise le Markdown `[libellé court](url)`. Ne répète pas l'URL en texte brut après le lien. Les listes à puces conviennent pour les coordonnées (téléphone, email, adresse).
- **CV PDF** : le fichier officiel est servi sous `/cv/cv-fernand-grascalvet.pdf` (toujours ce chemin). Si le visiteur demande le CV, un PDF, ou « télécharger », inclus dans ta réponse un lien Markdown `[Télécharger le CV (PDF)](/cv/cv-fernand-grascalvet.pdf)`. Pour une question sur **alternance**, **embauche** ou **parcours** un CV est naturellement utile, tu peux proposer ce lien une fois en fin de réponse (sans insister si hors contexte).
- Reste concis (3 à 6 phrases en général), sauf demande explicite de détail. - 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. - 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.
Règles de fidélité aux sources (important) : Règles de fidélité aux sources (important) :
- Chaque source fournie est annotée `type=parcours | projet | moc | competence | glossaire`. - Chaque source fournie est annotée `type=parcours | projet | moc | competence | glossaire`.
- Pour toute question biographique (qui est Fernand, âge, situation, école, objectif, contact, localisation), appuie-toi **en priorité** sur les sources de `type=parcours` (ex. [bio-fernand], [cv-grascalvet-fernand]). Ne **déduis jamais** d'informations biographiques depuis une source `type=projet` ou `type=moc`. - Pour toute question biographique (qui est Fernand, âge, situation, école, objectif, contact, localisation), appuie-toi **en priorité** sur les sources de `type=parcours` visibles dans le contexte (bio, CV). Ne **déduis jamais** d'informations biographiques depuis une source `type=projet` ou `type=moc`.
- Ne **jamais inventer** un fait factuel (âge, date, diplôme, école, entreprise, technologie utilisée) qui n'apparaît pas littéralement dans les sources. Si l'info n'est pas présente, écris « non précisé dans les notes » et oriente vers /portfolio, /competences ou /contact. - Ne **jamais inventer** un fait factuel (âge, date, diplôme, école, entreprise, technologie utilisée) qui n'apparaît pas littéralement dans les sources. Si l'info n'est pas présente, écris « non précisé dans les notes » et oriente vers /portfolio, /competences ou /contact.
- En cas de contradiction entre deux sources, privilégie la source de plus haut score, mentionne brièvement la divergence, et ne choisis jamais une valeur absente des deux. - En cas de contradiction entre deux sources, privilégie la source de plus haut score, mentionne brièvement la divergence, et ne choisis jamais une valeur absente des deux.
- Une note dont le body se termine par « note tronquée » a été résumée : signale-le si tu t'appuies dessus pour un point précis, ou invite à consulter la note complète.""" - Une note dont le body se termine par « note tronquée » a été résumée : signale-le si tu t'appuies dessus pour un point précis, ou invite à consulter la note complète."""

View File

@ -1,60 +1,68 @@
Gras-Calvet Fernand Gras-Calvet Fernand
Etudiant Informatique / 04/11/1978he
Recherche alternance Data-IA
Expérience pro Étudiant Informatique Recherche
alternance Data / IA
Ostréiculture 1999-2012 (Automatisation & LLM)
Présentation Pendant plus de 12 ans, jai travaillé dans lostréiculture à Leucate, Expérience pro
assurant la gestion complète de lentreprise familiale (Port-Leucate).
Ancien infirmier de 46 ans, actuellement
étudiant en informatique à lÉcole 42 Infirmier 2014-2023
Perpignan, je recherche une alternance de
2 ans pour me spécialiser dans Suite à une reconversion professionnelle, jai exercé la profession
lautomatisation agentique au sein des
entreprises, en y apportant mon dinfirmier pendant près de 10 ans. Dernier poste clinique Supervaltech
expérience sur le traitement de Data et les
nouveaux process basés sur les LLM. Mon à St Estève en gériatrie de 2018-2023.
parcours atypique reflète ma capacité
dadaptation, ma rigueur et mon esprit Etudiant Ecole 42 2023- 2025
déquipe.
Après des problèmes de santé, bénéficiant dune RQTH, jai entamé une
Contact
nouvelle reconversion professionnelle dans le domaine de
06.12.01.01.72
grascalvet.fernand@gmail.com linformatique. Durant cette période jai validé le tronc commun de
fernandgrascalvet.com
https://github.com/Ladebeze66 lécole. Réalisant un stage dans un entreprise logiciel métier spécialisée
13 rue de Belfort 66600 dans le béton. Lobjectif du stage étant basé sur lélaboration dun Etudiant Ecole 42 / Stage 2023- 2025
Rivesaltes chatbot, multi-agent, automatisation (interfaçage) entre les différents Après des problèmes de santé, bénéficiant dune RQTH, jai entamé une
Expérience outils de lentreprise (support, commercial, CRM ...). Présentation nouvelle reconversion professionnelle dans le domaine de
Objectifs Alternance Ancien infirmier en reconversion vers linformatique. Lors de mon stage en entreprise logicielle spécialisée
lingénierie informatique et lintelligence
artificielle, je me spécialise aujourdhui dans dans le béton, jai conçu une architecture de chatbot multi-agent
la conception de systèmes basés sur des
LLM, lautomatisation de processus métier intégrant analyse de tickets support, traitement dimages techniques
et le traitement de données techniques.
Mon parcours atypique mapporte rigueur, et pipeline RAG.
adaptabilité et une forte capacité
dapprentissage autonome. Infirmier 2014-2023
Mes objectifs de stage sont dapprofondir mon expertise en Contact Suite à une reconversion professionnelle, jai exercé la profession
programmation, tout en renforçant mes compétences en travail dinfirmier pendant près de 10 ans. Dernier poste clinique Supervaltech
collaboratif et en gestion de projet. Je souhaite également maîtriser 06.12.01.01.72 à St Estève en gériatrie de 2018-2023.
des outils et technologies avancés afin dapporter une réelle valeur grascalvet.fernand@gmail.com
ajoutée aux équipes avec lesquelles je travaillerai. Ostréiculture 1999-2012
https://fernandgrascalvet.com
Intérêts techniques et ambitions https://github.com/Ladebeze66 Pendant plus de 12 ans, jai travaillé dans lostréiculture à Leucate,
professionnelles assurant la gestion complète de lentreprise familiale (Port-Leucate).
13 rue de Belfort 66600 Rivesaltes
Python, PHP, HTML, C, C++ Passionné dinformatique depuis mon plus jeune âge, jai suivi Objectifs Alternance
Windows, Linux, VM, Server , Shell ... lévolution des générations de PC et acquis une solide expertise en Compétences techniques
Docker assemblage, maintenance et dépannage matériel. Intégrer une alternance de 2 ans orientée Data / IA appliquée afin de
Ollama, LLMs , VLMs open-source, API Également passionné par limpression 3D, je possède plusieurs Python, PHP, HTML, C, C++ concevoir des outils intelligents pour lautomatisation des processus
LangChain, Serveurs MCP ... machines et maîtrise des logiciels spécialisés tels que Fusion 360, ainsi bases Node.js / FastAPI métier :
que des slicers (PrusaSlicer, Orca) et firmwares (Klipper, Marlin). Next, Strapi
Hardware informatique, Impression 3d En parallèle, jai des notions en domotique, un domaine que jaimerais Windows, Linux, VM, Git agents IA et workflows LLM
approfondir. Enfin, je mintéresse particulièrement à lintelligence Server , Shell, Docker analyse documentaire et RAG
Language artificielle, dans laquelle je souhaite me spécialiser après mon stage à Architecture serveur et self-hosting IA interfaçage avec systèmes existants (CRM, support, ERP)
lÉcole 42. Ollama, LLMs , VLMs open-source, API
Anglais LangChain, MCP Servers Projets personnels et environnement
Espagnol Ragflow technique
Obsidian
Hardware Passionné dinformatique et dinnovation technologique, je développe
Impression 3D un environnement personnel orienté intelligence artificielle,
automatisation et auto-hébergement. Je conçois et maintiens mes
Language propres infrastructures (serveurs Windows/Linux, virtualisation,
services cloud personnels, dépôt Git auto-hébergé, observabilité avec
Anglais Langfuse, expérimentation doutils IA comme Searxng, Firecrawl et
Espagnol Ollama).
Jexplore activement les architectures RAG, en combinant LLM open-
source, bases vectorielles et pipelines documentaires afin daméliorer
la recherche dinformation et lanalyse technique.
Jutilise également Obsidian comme un environnement de gestion de
connaissances moderne et évolutif. Au-delà de la simple prise de notes,
il me permet de structurer, relier et tracer linformation technique dans
le temps. Couplé à des LLM et à des bases vectorielles, cet outil devient
un véritable support dingénierie personnelle.
En parallèle, je pratique limpression 3D (Fusion 360, PrusaSlicer, Orca,
Klipper, Marlin), ce qui renforce mon approche concrète de la
conception et du prototypage technique.

View File

@ -0,0 +1,68 @@
Gras-Calvet Fernand
04/11/1978he
Étudiant Informatique Recherche
alternance Data / IA
(Automatisation & LLM)
Expérience pro
Etudiant Ecole 42 / Stage 2023- 2025
Après des problèmes de santé, bénéficiant dune RQTH, jai entamé une
Présentation nouvelle reconversion professionnelle dans le domaine de
Ancien infirmier en reconversion vers linformatique. Lors de mon stage en entreprise logicielle spécialisée
lingénierie informatique et lintelligence
artificielle, je me spécialise aujourdhui dans dans le béton, jai conçu une architecture de chatbot multi-agent
la conception de systèmes basés sur des
LLM, lautomatisation de processus métier intégrant analyse de tickets support, traitement dimages techniques
et le traitement de données techniques.
Mon parcours atypique mapporte rigueur, et pipeline RAG.
adaptabilité et une forte capacité
dapprentissage autonome. Infirmier 2014-2023
Contact Suite à une reconversion professionnelle, jai exercé la profession
dinfirmier pendant près de 10 ans. Dernier poste clinique Supervaltech
06.12.01.01.72 à St Estève en gériatrie de 2018-2023.
grascalvet.fernand@gmail.com
Ostréiculture 1999-2012
https://fernandgrascalvet.com
https://github.com/Ladebeze66 Pendant plus de 12 ans, jai travaillé dans lostréiculture à Leucate,
assurant la gestion complète de lentreprise familiale (Port-Leucate).
13 rue de Belfort 66600 Rivesaltes
Objectifs Alternance
Compétences techniques
Intégrer une alternance de 2 ans orientée Data / IA appliquée afin de
Python, PHP, HTML, C, C++ concevoir des outils intelligents pour lautomatisation des processus
bases Node.js / FastAPI métier :
Next, Strapi
Windows, Linux, VM, Git agents IA et workflows LLM
Server , Shell, Docker analyse documentaire et RAG
Architecture serveur et self-hosting IA interfaçage avec systèmes existants (CRM, support, ERP)
Ollama, LLMs , VLMs open-source, API
LangChain, MCP Servers Projets personnels et environnement
Ragflow technique
Obsidian
Hardware Passionné dinformatique et dinnovation technologique, je développe
Impression 3D un environnement personnel orienté intelligence artificielle,
automatisation et auto-hébergement. Je conçois et maintiens mes
Language propres infrastructures (serveurs Windows/Linux, virtualisation,
services cloud personnels, dépôt Git auto-hébergé, observabilité avec
Anglais Langfuse, expérimentation doutils IA comme Searxng, Firecrawl et
Espagnol Ollama).
Jexplore activement les architectures RAG, en combinant LLM open-
source, bases vectorielles et pipelines documentaires afin daméliorer
la recherche dinformation et lanalyse technique.
Jutilise également Obsidian comme un environnement de gestion de
connaissances moderne et évolutif. Au-delà de la simple prise de notes,
il me permet de structurer, relier et tracer linformation technique dans
le temps. Couplé à des LLM et à des bases vectorielles, cet outil devient
un véritable support dingénierie personnelle.
En parallèle, je pratique limpression 3D (Fusion 360, PrusaSlicer, Orca,
Klipper, Marlin), ce qui renforce mon approche concrète de la
conception et du prototypage technique.

View File

@ -26,6 +26,8 @@ answers:
- "Quel est son profil ?" - "Quel est son profil ?"
- "Cherche-t-il une alternance ?" - "Cherche-t-il une alternance ?"
- "A-t-il de l'expérience professionnelle ?" - "A-t-il de l'expérience professionnelle ?"
- "Où télécharger le CV ?"
- "Je veux ton CV en PDF"
priority: 10 priority: 10
linked: linked:
- "[[bio-fernand]]" - "[[bio-fernand]]"
@ -57,6 +59,7 @@ visibility: public
## Contact ## Contact
- **CV PDF** : téléchargeable sur le site à lURL fixe `/cv/cv-fernand-grascalvet.pdf` (remplacer le fichier sur le serveur sans changer ce chemin pour mettre à jour le PDF).
- **Téléphone** : 06.12.01.01.72 - **Téléphone** : 06.12.01.01.72
- **Email** : grascalvet.fernand@gmail.com - **Email** : grascalvet.fernand@gmail.com
- **Adresse** : 13 rue de Belfort, 66600 Rivesaltes - **Adresse** : 13 rue de Belfort, 66600 Rivesaltes