mirror of
https://github.com/Ladebeze66/devsite.git
synced 2026-05-11 16:56:26 +02:00
maj_grasbot
This commit is contained in:
parent
9ee64f04f2
commit
a69a8f57ed
@ -11,6 +11,7 @@ import {
|
|||||||
newChatMessageId,
|
newChatMessageId,
|
||||||
saveGrasbotChatMessages,
|
saveGrasbotChatMessages,
|
||||||
} from "../utils/grasbotChatStorage";
|
} from "../utils/grasbotChatStorage";
|
||||||
|
import { getGrasbotSourceIconName, resolveGrasbotSourceHref } from "../utils/grasbotSourceUrl";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GrasBot — UI du chatbot (Stitch).
|
* GrasBot — UI du chatbot (Stitch).
|
||||||
@ -20,8 +21,9 @@ import {
|
|||||||
* `app/layout.tsx`. Ce composant se concentre sur le panneau de conversation.
|
* `app/layout.tsx`. Ce composant se concentre sur le panneau de conversation.
|
||||||
*
|
*
|
||||||
* v3 (2026-04-22) — bascule retrieval graph + BM25 :
|
* v3 (2026-04-22) — bascule retrieval graph + BM25 :
|
||||||
* - Affichage des `sources` renvoyées par l'API (pill par source, cliquable
|
* - Affichage des `sources` renvoyées par l'API (pill par source). L'URL est
|
||||||
* vers `/portfolio/<slug>` ou `/competences/<slug>` si dispo).
|
* résolue via `resolveGrasbotSourceHref` (API `url` / `route_parent` + fallback
|
||||||
|
* pour l'historique localStorage sans métadonnées à jour).
|
||||||
* - Badge `grounded` sous chaque réponse (paperclip si sources exploitées,
|
* - Badge `grounded` sous chaque réponse (paperclip si sources exploitées,
|
||||||
* 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.
|
||||||
@ -292,7 +294,7 @@ function BotFooter({ sources, grounded }) {
|
|||||||
seen.add(s.slug);
|
seen.add(s.slug);
|
||||||
uniqueSources.push(s);
|
uniqueSources.push(s);
|
||||||
}
|
}
|
||||||
const clickable = uniqueSources.filter((s) => s.url);
|
const clickable = uniqueSources.filter((s) => resolveGrasbotSourceHref(s) !== "#");
|
||||||
const displayed = clickable.slice(0, 4);
|
const displayed = clickable.slice(0, 4);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -309,19 +311,22 @@ function BotFooter({ sources, grounded }) {
|
|||||||
</span>
|
</span>
|
||||||
{grounded ? "Basé sur le vault" : "Réponse générale"}
|
{grounded ? "Basé sur le vault" : "Réponse générale"}
|
||||||
</span>
|
</span>
|
||||||
{displayed.map((s) => (
|
{displayed.map((s) => {
|
||||||
<Link
|
const href = resolveGrasbotSourceHref(s);
|
||||||
key={s.slug}
|
return (
|
||||||
href={s.url}
|
<Link
|
||||||
className="inline-flex items-center gap-1 rounded-full bg-surface-container-low px-2 py-0.5 font-headline text-[10px] text-primary transition-colors hover:bg-primary/10 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary"
|
key={s.slug}
|
||||||
title={s.title}
|
href={href}
|
||||||
>
|
className="inline-flex items-center gap-1 rounded-full bg-surface-container-low px-2 py-0.5 font-headline text-[10px] text-primary transition-colors hover:bg-primary/10 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary"
|
||||||
<span className="material-symbols-outlined text-[12px]" aria-hidden="true" translate="no">
|
title={s.title}
|
||||||
{s.type === "competence" ? "psychology" : "folder"}
|
>
|
||||||
</span>
|
<span className="material-symbols-outlined text-[12px]" aria-hidden="true" translate="no">
|
||||||
{s.slug}
|
{getGrasbotSourceIconName(s, href)}
|
||||||
</Link>
|
</span>
|
||||||
))}
|
{s.slug}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,7 +16,16 @@ import { getGrasbotSessionId, getGrasbotUserId } from "./grasbotIds";
|
|||||||
* @param {string} question
|
* @param {string} question
|
||||||
* @returns {Promise<{
|
* @returns {Promise<{
|
||||||
* response: string,
|
* response: string,
|
||||||
* sources?: Array<{slug: string, title: string, type: string, score: number, url?: string}>,
|
* sources?: Array<{
|
||||||
|
* slug: string,
|
||||||
|
* title: string,
|
||||||
|
* type: string,
|
||||||
|
* score: number,
|
||||||
|
* url?: string,
|
||||||
|
* route_parent?: string,
|
||||||
|
* path_slug?: string,
|
||||||
|
* site_slug?: string,
|
||||||
|
* }>,
|
||||||
* grounded?: boolean,
|
* grounded?: boolean,
|
||||||
* model?: string,
|
* model?: string,
|
||||||
* vault_size?: number,
|
* vault_size?: number,
|
||||||
|
|||||||
@ -1,13 +1,13 @@
|
|||||||
/**
|
/**
|
||||||
* Persistance locale du fil GrasBot (sans envoi à Ollama).
|
* Persistance locale du fil GrasBot (sans envoi à Ollama).
|
||||||
*
|
*
|
||||||
* Clé : `grasbot_chat_v1:<user_id>` où `user_id` est celui de `grasbotIds.js`.
|
* Clé : `grasbot_chat_v{VERSION}:<user_id>` où `user_id` est celui de `grasbotIds.js`.
|
||||||
* Limite : les derniers messages uniquement pour éviter quota localStorage (~5 Mo).
|
* Limite : les derniers messages uniquement pour éviter quota localStorage (~5 Mo).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { getGrasbotUserId } from "./grasbotIds";
|
import { getGrasbotUserId } from "./grasbotIds";
|
||||||
|
|
||||||
export const GRASBOT_CHAT_STORAGE_VERSION = 1;
|
export const GRASBOT_CHAT_STORAGE_VERSION = 2;
|
||||||
export const GRASBOT_CHAT_MAX_MESSAGES = 80;
|
export const GRASBOT_CHAT_MAX_MESSAGES = 80;
|
||||||
|
|
||||||
function storageKey(userId) {
|
function storageKey(userId) {
|
||||||
|
|||||||
135
app/utils/grasbotSourceUrl.js
Normal file
135
app/utils/grasbotSourceUrl.js
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
/**
|
||||||
|
* URL publique pour les pilules « sources » GrasBot (aligné sur `llm-api/search.py`).
|
||||||
|
*
|
||||||
|
* Ne pas faire confiance à `source.url` seul : l’API ou l’historique localStorage
|
||||||
|
* peut contenir d’anciennes URLs (`/competences/ia/grasbot`). On reconstruit
|
||||||
|
* toujours à partir de `path_slug` / `site_slug` / fallbacks quand un segment
|
||||||
|
* parent Strapi est connu.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** Slug vault → segment parent dans l’URL */
|
||||||
|
export const GRASBOT_ROUTE_PARENT_FALLBACK = {
|
||||||
|
grasbot: "ia",
|
||||||
|
"newsletter-ia": "ia",
|
||||||
|
"transcription-video": "ia",
|
||||||
|
"transcription-audio-fgc-transcription": "ia",
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Slug vault → dernier segment Strapi / Next (cf. Admin Strapi → realisation-ia) */
|
||||||
|
export const GRASBOT_SITE_SLUG_FALLBACK = {
|
||||||
|
grasbot: "gras-bot-chatbot-ia-du-portfolio",
|
||||||
|
"transcription-video": "transcription-video-automatique",
|
||||||
|
"newsletter-ia": "newsletter-ia-generation-automatisee-avec-ollama-and-listmonk",
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {{
|
||||||
|
* slug?: string,
|
||||||
|
* type?: string,
|
||||||
|
* url?: string,
|
||||||
|
* route_parent?: string,
|
||||||
|
* site_slug?: string,
|
||||||
|
* path_slug?: string,
|
||||||
|
* }} source
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function resolveGrasbotSourceHref(source) {
|
||||||
|
if (!source?.slug) return "#";
|
||||||
|
|
||||||
|
const vaultSlug = source.slug;
|
||||||
|
|
||||||
|
/* Dernier segment d’URL Strapi : souvent `site_slug`, sinon slug vault.
|
||||||
|
* Piège : l’API peut n’exposer que le slug vault (`grasbot`) alors que la page
|
||||||
|
* Next/Strapi attend l’UID titre (`gras-bot-chatbot-ia-du-portfolio`). Une
|
||||||
|
* compétence comme transcription audio n’a pas ce décalage : path_slug = slug
|
||||||
|
* vault = slug Strapi → ça marche. Les projets IA oui : on corrige quand
|
||||||
|
* path_slug vaut encore le seul identifiant vault mais un fallback existe. */
|
||||||
|
let pathSlug =
|
||||||
|
(source.path_slug && String(source.path_slug).trim()) ||
|
||||||
|
(source.site_slug && String(source.site_slug).trim()) ||
|
||||||
|
vaultSlug;
|
||||||
|
|
||||||
|
const siteSlugMapped = GRASBOT_SITE_SLUG_FALLBACK[vaultSlug];
|
||||||
|
if (siteSlugMapped && pathSlug === vaultSlug) {
|
||||||
|
pathSlug = siteSlugMapped;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parentRaw =
|
||||||
|
source.route_parent !== undefined && source.route_parent !== null
|
||||||
|
? String(source.route_parent).trim()
|
||||||
|
: "";
|
||||||
|
const parent = parentRaw || GRASBOT_ROUTE_PARENT_FALLBACK[vaultSlug] || "";
|
||||||
|
/** Route imbriquée /competences/[parent]/[pathSlug] (réalisation IA, compétence sous ia, etc.) */
|
||||||
|
const nested = Boolean(parent && parent !== vaultSlug);
|
||||||
|
|
||||||
|
if (nested) {
|
||||||
|
return `/competences/${parent}/${pathSlug}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const type = String(source.type || "").toLowerCase();
|
||||||
|
|
||||||
|
if (type === "projet") {
|
||||||
|
return `/portfolio/${pathSlug}`;
|
||||||
|
}
|
||||||
|
if (type === "competence") {
|
||||||
|
return `/competences/${pathSlug}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* moc, parcours, technique, … — garder l’URL serveur si fournie */
|
||||||
|
if (typeof source.url === "string" && source.url.startsWith("/")) {
|
||||||
|
return source.url;
|
||||||
|
}
|
||||||
|
return "#";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Icône Material Symbols pour la pilule : alignée sur l’URL réelle, pas seulement `source.type`.
|
||||||
|
* Les réalisations IA sont des `type: projet` dans le vault mais vivent sous `/competences/.../...`
|
||||||
|
* (comme une section compétence) : l’ancien test `type === "competence" ? psychology : folder`
|
||||||
|
* leur assignait à tort l’icône « dossier » réservée au portfolio.
|
||||||
|
*
|
||||||
|
* @param {{
|
||||||
|
* slug?: string,
|
||||||
|
* type?: string,
|
||||||
|
* url?: string,
|
||||||
|
* route_parent?: string,
|
||||||
|
* site_slug?: string,
|
||||||
|
* path_slug?: string,
|
||||||
|
* }} source
|
||||||
|
* @param {string} [resolvedHref] — résultat de `resolveGrasbotSourceHref(source)` pour éviter un double calcul
|
||||||
|
* @returns {string} nom d’icône Material Symbols
|
||||||
|
*/
|
||||||
|
export function getGrasbotSourceIconName(source, resolvedHref) {
|
||||||
|
const href =
|
||||||
|
typeof resolvedHref === "string" ? resolvedHref : resolveGrasbotSourceHref(source);
|
||||||
|
|
||||||
|
if (href === "#") return "link";
|
||||||
|
|
||||||
|
if (href.startsWith("/portfolio/")) {
|
||||||
|
return "folder";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (href.startsWith("/competences/")) {
|
||||||
|
const parts = href.split("/").filter(Boolean);
|
||||||
|
const nested = parts.length >= 3;
|
||||||
|
|
||||||
|
const type = String(source?.type || "").toLowerCase();
|
||||||
|
|
||||||
|
if (nested) {
|
||||||
|
if (type === "competence") {
|
||||||
|
return "psychology";
|
||||||
|
}
|
||||||
|
if (type === "projet") {
|
||||||
|
return "deployed_code";
|
||||||
|
}
|
||||||
|
if (GRASBOT_SITE_SLUG_FALLBACK[source.slug]) {
|
||||||
|
return "deployed_code";
|
||||||
|
}
|
||||||
|
return "psychology";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "psychology";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "link";
|
||||||
|
}
|
||||||
@ -28,10 +28,9 @@ Détails architecturaux dans
|
|||||||
|
|
||||||
Le champ consommé par le front reste **`data.response`**. Les champs ajoutés
|
Le champ consommé par le front reste **`data.response`**. Les champs ajoutés
|
||||||
par la refonte (`sources`, `grounded`, `model`, `vault_size`) passent dans
|
par la refonte (`sources`, `grounded`, `model`, `vault_size`) passent dans
|
||||||
la réponse JSON ; les **`sources`** incluent une **`url`** relative pour les types
|
la réponse JSON ; les **`sources`** incluent **`url`**, **`route_parent`**, **`path_slug`**
|
||||||
`projet` et `compétence` (ex. `/portfolio/[slug]`, `/competences/[slug]` ou
|
(dernier segment d’URL), et **`site_slug`** lorsque le vault le définit (alias Strapi).
|
||||||
`/competences/[route_parent]/[slug]` si la note compétence définit `route_parent`
|
Le front résout l’hyperlien via `app/utils/grasbotSourceUrl.js`.
|
||||||
dans le frontmatter du vault — utilisé pour les fiches sous `/competences/ia/…`).
|
|
||||||
|
|
||||||
## Parcours public (hors moteur Python) — cohérence contenu
|
## Parcours public (hors moteur Python) — cohérence contenu
|
||||||
|
|
||||||
|
|||||||
@ -249,6 +249,11 @@ def parse_note(path: Path) -> Note | None:
|
|||||||
if rp is not None and str(rp).strip():
|
if rp is not None and str(rp).strip():
|
||||||
extra["route_parent"] = str(rp).strip()
|
extra["route_parent"] = str(rp).strip()
|
||||||
|
|
||||||
|
# Slug public Strapi / Next (dernier segment d'URL) si différent du slug vault (ex. realisation-ia).
|
||||||
|
ss = fm.get("site_slug")
|
||||||
|
if ss is not None and str(ss).strip():
|
||||||
|
extra["site_slug"] = str(ss).strip()
|
||||||
|
|
||||||
return Note(
|
return Note(
|
||||||
slug=slug,
|
slug=slug,
|
||||||
title=title,
|
title=title,
|
||||||
@ -804,6 +809,35 @@ def generate(system: str, user: str) -> str:
|
|||||||
return content
|
return content
|
||||||
|
|
||||||
|
|
||||||
|
def _path_slug(note: Note) -> str:
|
||||||
|
"""Dernier segment d'URL site : `site_slug` Strapi si renseigné, sinon slug vault."""
|
||||||
|
s = str(note.extra.get("site_slug") or "").strip()
|
||||||
|
return s if s else note.slug
|
||||||
|
|
||||||
|
|
||||||
|
def _source_public_url(note: Note) -> tuple[str | None, str]:
|
||||||
|
"""URL relative pour les pilules GrasBot + segment `route_parent` si imbriqué.
|
||||||
|
|
||||||
|
Retourne ``(url_ou_none, route_parent_expose)`` où `route_parent_expose` est
|
||||||
|
le segment parent (ex. ``ia``) uniquement lorsqu'il participe à l'URL
|
||||||
|
``/competences/{parent}/{path_slug}``. Le dernier segment utilise
|
||||||
|
:func:`_path_slug` (alias Strapi ``site_slug`` dans le frontmatter).
|
||||||
|
"""
|
||||||
|
raw = str(note.extra.get("route_parent") or "").strip()
|
||||||
|
seg = raw if raw and raw != note.slug else ""
|
||||||
|
last = _path_slug(note)
|
||||||
|
|
||||||
|
if note.type == "projet":
|
||||||
|
if seg:
|
||||||
|
return f"/competences/{seg}/{last}", seg
|
||||||
|
return f"/portfolio/{last}", ""
|
||||||
|
if note.type == "competence":
|
||||||
|
if seg:
|
||||||
|
return f"/competences/{seg}/{last}", seg
|
||||||
|
return f"/competences/{last}", ""
|
||||||
|
return None, ""
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Façade haut-niveau — trace racine Langfuse
|
# Façade haut-niveau — trace racine Langfuse
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@ -818,7 +852,7 @@ def answer(
|
|||||||
Retourne :
|
Retourne :
|
||||||
{
|
{
|
||||||
"response": str, # texte LLM (consommé par askAI.js → ChatBot.js)
|
"response": str, # texte LLM (consommé par askAI.js → ChatBot.js)
|
||||||
"sources": list[{slug, title, type, score, reasons, url?}],
|
"sources": list[{slug, title, type, score, reasons, url?, route_parent?, path_slug?}],
|
||||||
"model": str,
|
"model": str,
|
||||||
"grounded": bool, # True si au moins 1 note a dépassé MIN_SCORE
|
"grounded": bool, # True si au moins 1 note a dépassé MIN_SCORE
|
||||||
"vault_size": int,
|
"vault_size": int,
|
||||||
@ -861,23 +895,23 @@ def answer(
|
|||||||
# --- Construction de la réponse API ---
|
# --- Construction de la réponse API ---
|
||||||
sources = []
|
sources = []
|
||||||
for s in scored:
|
for s in scored:
|
||||||
url = None
|
url, rp_out = _source_public_url(s.note)
|
||||||
if s.note.type == "projet":
|
entry: dict[str, Any] = {
|
||||||
url = f"/portfolio/{s.note.slug}"
|
|
||||||
elif s.note.type == "competence":
|
|
||||||
parent = str(s.note.extra.get("route_parent") or "").strip()
|
|
||||||
if parent:
|
|
||||||
url = f"/competences/{parent}/{s.note.slug}"
|
|
||||||
else:
|
|
||||||
url = f"/competences/{s.note.slug}"
|
|
||||||
sources.append({
|
|
||||||
"slug": s.note.slug,
|
"slug": s.note.slug,
|
||||||
"title": s.note.title,
|
"title": s.note.title,
|
||||||
"type": s.note.type,
|
"type": s.note.type,
|
||||||
"score": round(s.score, 2),
|
"score": round(s.score, 2),
|
||||||
"reasons": s.reasons,
|
"reasons": s.reasons,
|
||||||
**({"url": url} if url else {}),
|
"path_slug": _path_slug(s.note),
|
||||||
})
|
}
|
||||||
|
if url:
|
||||||
|
entry["url"] = url
|
||||||
|
if rp_out:
|
||||||
|
entry["route_parent"] = rp_out
|
||||||
|
site_val = str(s.note.extra.get("site_slug") or "").strip()
|
||||||
|
if site_val:
|
||||||
|
entry["site_slug"] = site_val
|
||||||
|
sources.append(entry)
|
||||||
|
|
||||||
grounded = any(s.score >= MIN_SCORE for s in scored)
|
grounded = any(s.score >= MIN_SCORE for s in scored)
|
||||||
max_score = max((s.score for s in scored), default=0.0)
|
max_score = max((s.score for s in scored), default=0.0)
|
||||||
|
|||||||
@ -20,11 +20,11 @@ $uri = ($BaseUrl.TrimEnd("/") + "/reload-vault")
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
$response = Invoke-RestMethod -Method Post -Uri $uri -TimeoutSec 60
|
$response = Invoke-RestMethod -Method Post -Uri $uri -TimeoutSec 60
|
||||||
Write-Host ("Vault rechargé : {0} notes — {1}" -f $response.notes_total, $uri) -ForegroundColor Green
|
Write-Host ("Vault reloaded: {0} note(s) -> {1}" -f $response.notes_total, $uri) -ForegroundColor Green
|
||||||
$response | ConvertTo-Json -Compress
|
$response | ConvertTo-Json -Compress
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
Write-Host ("Échec : {0}" -f $_.Exception.Message) -ForegroundColor Red
|
Write-Host ("Failed: {0}" -f $_.Exception.Message) -ForegroundColor Red
|
||||||
Write-Host ("URI : {0}" -f $uri)
|
Write-Host ("POST {0}" -f $uri)
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|||||||
@ -35,6 +35,9 @@ related:
|
|||||||
- "[[newsletter-ia]]"
|
- "[[newsletter-ia]]"
|
||||||
- "[[transcription-video]]"
|
- "[[transcription-video]]"
|
||||||
link: "https://fernandgrascalvet.com"
|
link: "https://fernandgrascalvet.com"
|
||||||
|
route_parent: ia
|
||||||
|
# Slug public Strapi (realisation-ia, UID depuis le titre) — dernier segment de l’URL site.
|
||||||
|
site_slug: gras-bot-chatbot-ia-du-portfolio
|
||||||
updated: 2026-04-23
|
updated: 2026-04-23
|
||||||
visibility: public
|
visibility: public
|
||||||
---
|
---
|
||||||
|
|||||||
@ -27,6 +27,8 @@ related:
|
|||||||
- "[[ia]]"
|
- "[[ia]]"
|
||||||
- "[[grasbot]]"
|
- "[[grasbot]]"
|
||||||
- "[[architecture-site]]"
|
- "[[architecture-site]]"
|
||||||
|
route_parent: ia
|
||||||
|
site_slug: newsletter-ia-generation-automatisee-avec-ollama-and-listmonk
|
||||||
updated: 2026-04-23
|
updated: 2026-04-23
|
||||||
visibility: public
|
visibility: public
|
||||||
---
|
---
|
||||||
|
|||||||
@ -27,6 +27,8 @@ related:
|
|||||||
- "[[newsletter-ia]]"
|
- "[[newsletter-ia]]"
|
||||||
- "[[grasbot]]"
|
- "[[grasbot]]"
|
||||||
- "[[transcription-audio-fgc-transcription]]"
|
- "[[transcription-audio-fgc-transcription]]"
|
||||||
|
route_parent: ia
|
||||||
|
site_slug: transcription-video-automatique
|
||||||
updated: 2026-05-10
|
updated: 2026-05-10
|
||||||
visibility: public
|
visibility: public
|
||||||
---
|
---
|
||||||
|
|||||||
@ -37,12 +37,38 @@ answers: # questions-types auxquelles répond la
|
|||||||
priority: 5 # 1..10, boost léger au scoring
|
priority: 5 # 1..10, boost léger au scoring
|
||||||
linked: ["[[MOC-...]]"] # voisins du graphe (sortants)
|
linked: ["[[MOC-...]]"] # voisins du graphe (sortants)
|
||||||
related: ["[[autre-note]]"]
|
related: ["[[autre-note]]"]
|
||||||
route_parent: ia # optionnel (compétence) : lien source `/competences/ia/{slug}`
|
route_parent: ia # optionnel — voir § URLs des tags GrasBot
|
||||||
|
site_slug: autre-slug-strapi # optionnel : dernier segment URL si différent du slug vault (realisation-ia)
|
||||||
updated: YYYY-MM-DD
|
updated: YYYY-MM-DD
|
||||||
visibility: public | private # `private` exclu du retrieval
|
visibility: public | private # `private` exclu du retrieval
|
||||||
---
|
---
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### URLs des tags GrasBot (`route_parent`)
|
||||||
|
|
||||||
|
L’API (`llm-api/search.py`) ajoute aux **`sources`** une URL relative pour les pilules sous la réponse. Défaut :
|
||||||
|
|
||||||
|
| `type` | URL si pas de `route_parent` |
|
||||||
|
|--------|------------------------------|
|
||||||
|
| `projet` | `/portfolio/{slug}` |
|
||||||
|
| `competence` | `/competences/{slug}` |
|
||||||
|
|
||||||
|
Le site peut aussi servir **`/competences/[parent]/[slug]`** (réalisations IA sous la compétence **IA**, fiches compétence imbriquées, etc.). Dans ce cas, ajouter dans le frontmatter :
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
route_parent: ia # segment parent dans l’URL Next (ex. ia → /competences/ia/{slug})
|
||||||
|
```
|
||||||
|
|
||||||
|
Règles : si `route_parent` est défini et **différent** de `slug` → lien **`/competences/{route_parent}/{slug}`** (pour `projet` et `competence`). Si `route_parent == slug`, on évite le doublon et on utilise `/competences/{slug}`.
|
||||||
|
|
||||||
|
**Slug Strapi ≠ slug vault** — Les entrées `realisation-ia` utilisent un UID dérivé du **titre** dans Strapi (`slug` admin), souvent différent du fichier vault (`grasbot.md`, slug court pour les wikilinks). Dans ce cas, renseigner **`site_slug`** avec la valeur exacte du champ *slug* Strapi (voir Admin ou `GET /api/realisation-ias`). L’API GrasBot expose alors **`path_slug`** et l’URL utilise ce segment pour le dernier morceau du chemin.
|
||||||
|
|
||||||
|
**Audit** — repérer les notes à corriger : corps ou bloc info qui mentionne `realisation-ia` et `[[ia]]`, ou une route documentée sous `/competences/ia/`. Dans `10-Projets/`, les réalisations IA typiques ont `route_parent: ia` (GrasBot, newsletter IA, transcription vidéo). Les projets 42 restent en `/portfolio/...` sans `route_parent`.
|
||||||
|
|
||||||
|
Après édition du vault : **`POST /reload-vault`** ou **`.\reload-vault.ps1`** (racine du dépôt).
|
||||||
|
|
||||||
|
**Front Next** : `app/utils/grasbotSourceUrl.js` — fallbacks `GRASBOT_ROUTE_PARENT_FALLBACK` et **`GRASBOT_SITE_SLUG_FALLBACK`** (à synchroniser avec Strapi si tu ajoutes des réalisations IA).
|
||||||
|
|
||||||
Voir `TAXONOMIE.md` pour le vocabulaire contrôlé des domaines/tags et les
|
Voir `TAXONOMIE.md` pour le vocabulaire contrôlé des domaines/tags et les
|
||||||
règles de rédaction des aliases/answers.
|
règles de rédaction des aliases/answers.
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user