mirror of
https://github.com/Ladebeze66/devsite.git
synced 2026-05-11 16:56:26 +02:00
Compare commits
4 Commits
517eed915d
...
ba34820ffc
| Author | SHA1 | Date | |
|---|---|---|---|
| ba34820ffc | |||
| ee79d56d66 | |||
| 5021251dbf | |||
| 5c4abb2118 |
@ -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,6 +140,19 @@ export default function ChatBot({ onClose }) {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex shrink-0 items-center gap-1">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleClearHistory}
|
||||||
|
disabled={messages.length === 0}
|
||||||
|
aria-label="Effacer l'historique de conversation"
|
||||||
|
title="Effacer l'historique"
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
<span className="material-symbols-outlined text-xl" aria-hidden="true" translate="no">
|
||||||
|
delete_sweep
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
@ -117,6 +168,7 @@ export default function ChatBot({ onClose }) {
|
|||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
ref={scrollRef}
|
ref={scrollRef}
|
||||||
@ -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
|
||||||
|
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}
|
{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} />
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
13
app/page.tsx
13
app/page.tsx
@ -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"
|
||||||
|
|||||||
86
app/utils/grasbotChatStorage.js
Normal file
86
app/utils/grasbotChatStorage.js
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
/**
|
||||||
|
* Persistance locale du fil GrasBot (sans envoi à Ollama).
|
||||||
|
*
|
||||||
|
* Clé : `grasbot_chat_v1:<user_id>` où `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();
|
||||||
|
}
|
||||||
@ -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** où 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."""
|
||||||
|
|||||||
@ -1,60 +1,68 @@
|
|||||||
Gras-Calvet Fernand
|
Gras-Calvet Fernand
|
||||||
|
|
||||||
Etudiant Informatique /
|
04/11/1978he
|
||||||
Recherche alternance Data-IA
|
|
||||||
|
Étudiant Informatique – Recherche
|
||||||
|
alternance Data / IA
|
||||||
|
|
||||||
|
(Automatisation & LLM)
|
||||||
|
|
||||||
Expérience pro
|
Expérience pro
|
||||||
|
|
||||||
Ostréiculture 1999-2012
|
Etudiant Ecole 42 / Stage 2023- 2025
|
||||||
|
|
||||||
Présentation Pendant plus de 12 ans, j’ai travaillé dans l’ostréiculture à Leucate,
|
|
||||||
assurant la gestion complète de l’entreprise 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, j’ai exercé la profession
|
|
||||||
l’automatisation agentique au sein des
|
|
||||||
entreprises, en y apportant mon d’infirmier 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é
|
|
||||||
d’adaptation, ma rigueur et mon esprit Etudiant Ecole 42 2023- 2025
|
|
||||||
d’équipe.
|
|
||||||
Après des problèmes de santé, bénéficiant d’une RQTH, j’ai entamé une
|
Après des problèmes de santé, bénéficiant d’une RQTH, j’ai entamé une
|
||||||
Contact
|
|
||||||
nouvelle reconversion professionnelle dans le domaine de
|
|
||||||
06.12.01.01.72
|
|
||||||
grascalvet.fernand@gmail.com l’informatique. Durant cette période j’ai 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. L’objectif du stage étant basé sur l’élaboration d’un
|
Présentation nouvelle reconversion professionnelle dans le domaine de
|
||||||
|
|
||||||
Rivesaltes chatbot, multi-agent, automatisation (interfaçage) entre les différents
|
Ancien infirmier en reconversion vers l’informatique. Lors de mon stage en entreprise logicielle spécialisée
|
||||||
|
l’ingénierie informatique et l’intelligence
|
||||||
Expérience outils de l’entreprise (support, commercial, CRM ...).
|
artificielle, je me spécialise aujourd’hui dans dans le béton, j’ai conçu une architecture de chatbot multi-agent
|
||||||
|
la conception de systèmes basés sur des
|
||||||
|
LLM, l’automatisation de processus métier intégrant analyse de tickets support, traitement d’images techniques
|
||||||
|
et le traitement de données techniques.
|
||||||
|
Mon parcours atypique m’apporte rigueur, et pipeline RAG.
|
||||||
|
adaptabilité et une forte capacité
|
||||||
|
d’apprentissage autonome. Infirmier 2014-2023
|
||||||
|
|
||||||
|
Contact Suite à une reconversion professionnelle, j’ai exercé la profession
|
||||||
|
d’infirmier 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, j’ai travaillé dans l’ostréiculture à Leucate,
|
||||||
|
assurant la gestion complète de l’entreprise familiale (Port-Leucate).
|
||||||
|
13 rue de Belfort 66600 Rivesaltes
|
||||||
Objectifs Alternance
|
Objectifs Alternance
|
||||||
|
Compétences techniques
|
||||||
Mes objectifs de stage sont d’approfondir mon expertise en
|
Intégrer une alternance de 2 ans orientée Data / IA appliquée afin de
|
||||||
programmation, tout en renforçant mes compétences en travail
|
Python, PHP, HTML, C, C++ concevoir des outils intelligents pour l’automatisation des processus
|
||||||
collaboratif et en gestion de projet. Je souhaite également maîtriser
|
bases Node.js / FastAPI métier :
|
||||||
des outils et technologies avancés afin d’apporter une réelle valeur
|
Next, Strapi
|
||||||
ajoutée aux équipes avec lesquelles je travaillerai.
|
Windows, Linux, VM, Git agents IA et workflows LLM
|
||||||
|
Server , Shell, Docker analyse documentaire et RAG
|
||||||
Intérêts techniques et ambitions
|
Architecture serveur et self-hosting IA interfaçage avec systèmes existants (CRM, support, ERP)
|
||||||
professionnelles
|
Ollama, LLMs , VLMs open-source, API
|
||||||
|
LangChain, MCP Servers Projets personnels et environnement
|
||||||
Python, PHP, HTML, C, C++ Passionné d’informatique depuis mon plus jeune âge, j’ai suivi
|
Ragflow technique
|
||||||
Windows, Linux, VM, Server , Shell ... l’évolution des générations de PC et acquis une solide expertise en
|
Obsidian
|
||||||
Docker assemblage, maintenance et dépannage matériel.
|
Hardware Passionné d’informatique et d’innovation technologique, je développe
|
||||||
Ollama, LLMs , VLMs open-source, API Également passionné par l’impression 3D, je possède plusieurs
|
Impression 3D un environnement personnel orienté intelligence artificielle,
|
||||||
LangChain, Serveurs MCP ... machines et maîtrise des logiciels spécialisés tels que Fusion 360, ainsi
|
automatisation et auto-hébergement. Je conçois et maintiens mes
|
||||||
que des slicers (PrusaSlicer, Orca) et firmwares (Klipper, Marlin).
|
Language propres infrastructures (serveurs Windows/Linux, virtualisation,
|
||||||
Hardware informatique, Impression 3d En parallèle, j’ai des notions en domotique, un domaine que j’aimerais
|
services cloud personnels, dépôt Git auto-hébergé, observabilité avec
|
||||||
approfondir. Enfin, je m’intéresse particulièrement à l’intelligence
|
Anglais Langfuse, expérimentation d’outils IA comme Searxng, Firecrawl et
|
||||||
Language artificielle, dans laquelle je souhaite me spécialiser après mon stage à
|
Espagnol Ollama).
|
||||||
l’École 42.
|
J’explore activement les architectures RAG, en combinant LLM open-
|
||||||
Anglais
|
source, bases vectorielles et pipelines documentaires afin d’améliorer
|
||||||
Espagnol
|
la recherche d’information et l’analyse technique.
|
||||||
|
J’utilise é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 l’information technique dans
|
||||||
|
le temps. Couplé à des LLM et à des bases vectorielles, cet outil devient
|
||||||
|
un véritable support d’ingénierie personnelle.
|
||||||
|
En parallèle, je pratique l’impression 3D (Fusion 360, PrusaSlicer, Orca,
|
||||||
|
Klipper, Marlin), ce qui renforce mon approche concrète de la
|
||||||
|
conception et du prototypage technique.
|
||||||
|
|
||||||
68
strapi_extraction/docs/CV/Gras_Calvet_CV.pdf
Normal file
68
strapi_extraction/docs/CV/Gras_Calvet_CV.pdf
Normal 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 d’une RQTH, j’ai entamé une
|
||||||
|
|
||||||
|
Présentation nouvelle reconversion professionnelle dans le domaine de
|
||||||
|
|
||||||
|
Ancien infirmier en reconversion vers l’informatique. Lors de mon stage en entreprise logicielle spécialisée
|
||||||
|
l’ingénierie informatique et l’intelligence
|
||||||
|
artificielle, je me spécialise aujourd’hui dans dans le béton, j’ai conçu une architecture de chatbot multi-agent
|
||||||
|
la conception de systèmes basés sur des
|
||||||
|
LLM, l’automatisation de processus métier intégrant analyse de tickets support, traitement d’images techniques
|
||||||
|
et le traitement de données techniques.
|
||||||
|
Mon parcours atypique m’apporte rigueur, et pipeline RAG.
|
||||||
|
adaptabilité et une forte capacité
|
||||||
|
d’apprentissage autonome. Infirmier 2014-2023
|
||||||
|
|
||||||
|
Contact Suite à une reconversion professionnelle, j’ai exercé la profession
|
||||||
|
d’infirmier 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, j’ai travaillé dans l’ostréiculture à Leucate,
|
||||||
|
assurant la gestion complète de l’entreprise 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 l’automatisation 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é d’informatique et d’innovation 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 d’outils IA comme Searxng, Firecrawl et
|
||||||
|
Espagnol Ollama).
|
||||||
|
J’explore activement les architectures RAG, en combinant LLM open-
|
||||||
|
source, bases vectorielles et pipelines documentaires afin d’améliorer
|
||||||
|
la recherche d’information et l’analyse technique.
|
||||||
|
J’utilise é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 l’information technique dans
|
||||||
|
le temps. Couplé à des LLM et à des bases vectorielles, cet outil devient
|
||||||
|
un véritable support d’ingénierie personnelle.
|
||||||
|
En parallèle, je pratique l’impression 3D (Fusion 360, PrusaSlicer, Orca,
|
||||||
|
Klipper, Marlin), ce qui renforce mon approche concrète de la
|
||||||
|
conception et du prototypage technique.
|
||||||
|
|
||||||
@ -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 à l’URL 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
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user