@hgdc/docs 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/hgdc-docs.mjs +48 -0
- package/docs.schema.json +294 -0
- package/package.json +51 -4
- package/src/build/auto-import.ts +74 -0
- package/src/build/markdown.ts +114 -0
- package/src/build/outputs.ts +91 -0
- package/src/build/public-files.ts +35 -0
- package/src/components/Accordion.astro +12 -0
- package/src/components/AccordionGroup.astro +4 -0
- package/src/components/Card.astro +15 -0
- package/src/components/CardGroup.astro +8 -0
- package/src/components/Check.astro +10 -0
- package/src/components/CodeGroup.astro +4 -0
- package/src/components/Columns.astro +8 -0
- package/src/components/Danger.astro +10 -0
- package/src/components/Frame.astro +11 -0
- package/src/components/Info.astro +10 -0
- package/src/components/Note.astro +10 -0
- package/src/components/Step.astro +11 -0
- package/src/components/Steps.astro +4 -0
- package/src/components/Tab.astro +9 -0
- package/src/components/Tabs.astro +9 -0
- package/src/components/Tip.astro +10 -0
- package/src/components/Warning.astro +10 -0
- package/src/core/answer-markdown.ts +109 -0
- package/src/core/assistant-api.ts +65 -0
- package/src/core/chunk.ts +134 -0
- package/src/core/config.ts +153 -0
- package/src/core/corpus.ts +42 -0
- package/src/core/json-schema.ts +12 -0
- package/src/core/llms.ts +95 -0
- package/src/core/navigation.ts +88 -0
- package/src/core/theme.ts +111 -0
- package/src/integration.ts +229 -0
- package/src/locals.d.ts +6 -0
- package/src/overrides/Assistant.astro +72 -0
- package/src/overrides/Head.astro +12 -0
- package/src/overrides/PageFrame.astro +15 -0
- package/src/overrides/PageTitle.astro +94 -0
- package/src/overrides/Sidebar.astro +60 -0
- package/src/overrides/route-data.ts +33 -0
- package/src/styles/theme.css +773 -0
- package/src/virtual.d.ts +6 -0
- package/src/widget/client.ts +252 -0
- package/template/.astro/collections/docs.schema.json +651 -0
- package/template/.astro/content-assets.mjs +1 -0
- package/template/.astro/content-modules.mjs +18 -0
- package/template/.astro/content.d.ts +179 -0
- package/template/.astro/types.d.ts +2 -0
- package/template/.build/.well-known/llms.txt +38 -0
- package/template/.build/404.html +42 -0
- package/template/.build/_astro/Assistant.astro_astro_type_script_index_0_lang.DWgmlr85.js +4 -0
- package/template/.build/_astro/MobileTableOfContents.astro_astro_type_script_index_0_lang.C3fM4Kra.js +1 -0
- package/template/.build/_astro/Search.astro_astro_type_script_index_0_lang.g9hoxVJg.js +1 -0
- package/template/.build/_astro/TableOfContents.astro_astro_type_script_index_0_lang.BpJaGz7G.js +1 -0
- package/template/.build/_astro/common.8GHCkW7i.css +3 -0
- package/template/.build/_astro/ec.0vx5m.js +3 -0
- package/template/.build/_astro/ec.8nqtp.css +1 -0
- package/template/.build/_astro/geist-cyrillic-ext-wght-normal.DjL33-gN.woff2 +0 -0
- package/template/.build/_astro/geist-cyrillic-wght-normal.BEAKL7Jp.woff2 +0 -0
- package/template/.build/_astro/geist-latin-ext-wght-normal.DC-KSUi6.woff2 +0 -0
- package/template/.build/_astro/geist-latin-wght-normal.BgDaEnEv.woff2 +0 -0
- package/template/.build/_astro/geist-vietnamese-wght-normal.6IgcOCM7.woff2 +0 -0
- package/template/.build/_astro/inter-cyrillic-ext-wght-normal.BOeWTOD4.woff2 +0 -0
- package/template/.build/_astro/inter-cyrillic-wght-normal.DqGufNeO.woff2 +0 -0
- package/template/.build/_astro/inter-greek-ext-wght-normal.DlzME5K_.woff2 +0 -0
- package/template/.build/_astro/inter-greek-wght-normal.CkhJZR-_.woff2 +0 -0
- package/template/.build/_astro/inter-latin-ext-wght-normal.DO1Apj_S.woff2 +0 -0
- package/template/.build/_astro/inter-latin-wght-normal.Dx4kXJAl.woff2 +0 -0
- package/template/.build/_astro/inter-vietnamese-wght-normal.CBcvBZtf.woff2 +0 -0
- package/template/.build/_astro/logo-clair.BqNh6JNi.svg +1 -0
- package/template/.build/_astro/logo-sombre.BkgsFXxh.svg +1 -0
- package/template/.build/_astro/page.cNngjUgL.js +1 -0
- package/template/.build/_astro/print.ehPL0gv-.css +1 -0
- package/template/.build/_astro/starlight-toc.D4UDIxQL.js +1 -0
- package/template/.build/_astro/ui-core.Clh5jUUO.js +2 -0
- package/template/.build/_hgdc/corpus.json +1 -0
- package/template/.build/demarrer/premiere-doc/index.html +84 -0
- package/template/.build/demarrer/premiere-doc.md +65 -0
- package/template/.build/explications/donnees/index.html +114 -0
- package/template/.build/explications/donnees.md +28 -0
- package/template/.build/explications/fonctionnement/index.html +92 -0
- package/template/.build/explications/fonctionnement.md +32 -0
- package/template/.build/guides/agents/index.html +113 -0
- package/template/.build/guides/agents.md +40 -0
- package/template/.build/guides/assistant/index.html +117 -0
- package/template/.build/guides/assistant.md +42 -0
- package/template/.build/guides/composants/index.html +97 -0
- package/template/.build/guides/composants.md +103 -0
- package/template/.build/guides/ecrire/index.html +115 -0
- package/template/.build/guides/ecrire.md +63 -0
- package/template/.build/guides/navigation/index.html +89 -0
- package/template/.build/guides/navigation.md +63 -0
- package/template/.build/guides/publier/index.html +86 -0
- package/template/.build/guides/publier.md +65 -0
- package/template/.build/guides/theme/index.html +86 -0
- package/template/.build/guides/theme.md +33 -0
- package/template/.build/images/favicon.svg +1 -0
- package/template/.build/images/logo-clair.svg +1 -0
- package/template/.build/images/logo-sombre.svg +1 -0
- package/template/.build/index.html +87 -0
- package/template/.build/index.md +35 -0
- package/template/.build/llms-full.txt +830 -0
- package/template/.build/llms.txt +38 -0
- package/template/.build/pagefind/fragment/fr-ch_148313b.pf_fragment +0 -0
- package/template/.build/pagefind/fragment/fr-ch_424fd16.pf_fragment +0 -0
- package/template/.build/pagefind/fragment/fr-ch_4f8575c.pf_fragment +0 -0
- package/template/.build/pagefind/fragment/fr-ch_5a4d77d.pf_fragment +0 -0
- package/template/.build/pagefind/fragment/fr-ch_6c36edc.pf_fragment +0 -0
- package/template/.build/pagefind/fragment/fr-ch_73d7887.pf_fragment +0 -0
- package/template/.build/pagefind/fragment/fr-ch_8234ed3.pf_fragment +0 -0
- package/template/.build/pagefind/fragment/fr-ch_8f8a19e.pf_fragment +0 -0
- package/template/.build/pagefind/fragment/fr-ch_9f1b981.pf_fragment +0 -0
- package/template/.build/pagefind/fragment/fr-ch_a19039d.pf_fragment +0 -0
- package/template/.build/pagefind/fragment/fr-ch_d72cc1e.pf_fragment +0 -0
- package/template/.build/pagefind/fragment/fr-ch_d732215.pf_fragment +0 -0
- package/template/.build/pagefind/fragment/fr-ch_e2c46e3.pf_fragment +0 -0
- package/template/.build/pagefind/fragment/fr-ch_e78d58b.pf_fragment +0 -0
- package/template/.build/pagefind/fragment/fr-ch_e92f972.pf_fragment +0 -0
- package/template/.build/pagefind/index/fr-ch_f94c95d.pf_index +0 -0
- package/template/.build/pagefind/pagefind-component-ui.css +1509 -0
- package/template/.build/pagefind/pagefind-component-ui.js +56 -0
- package/template/.build/pagefind/pagefind-entry.json +1 -0
- package/template/.build/pagefind/pagefind-highlight.js +1070 -0
- package/template/.build/pagefind/pagefind-modular-ui.css +214 -0
- package/template/.build/pagefind/pagefind-modular-ui.js +8 -0
- package/template/.build/pagefind/pagefind-ui.css +1 -0
- package/template/.build/pagefind/pagefind-ui.js +2 -0
- package/template/.build/pagefind/pagefind-worker.js +6 -0
- package/template/.build/pagefind/pagefind.fr-ch_fb78ab42961f4.pf_meta +0 -0
- package/template/.build/pagefind/pagefind.js +6 -0
- package/template/.build/pagefind/wasm.fr-ch.pagefind +0 -0
- package/template/.build/pagefind/wasm.unknown.pagefind +0 -0
- package/template/.build/reference/api-assistant/index.html +161 -0
- package/template/.build/reference/api-assistant.md +60 -0
- package/template/.build/reference/cli/index.html +153 -0
- package/template/.build/reference/cli.md +31 -0
- package/template/.build/reference/docs-json/index.html +192 -0
- package/template/.build/reference/docs-json.md +43 -0
- package/template/.build/reference/mcp/index.html +151 -0
- package/template/.build/reference/mcp.md +49 -0
- package/template/astro.config.ts +6 -0
- package/template/node_modules/.vite/deps/_metadata.json +74 -0
- package/template/node_modules/.vite/deps/astro-B3JHJyiv.js +434 -0
- package/template/node_modules/.vite/deps/astro-B3JHJyiv.js.map +1 -0
- package/template/node_modules/.vite/deps/astro_n_aria-query.js +6390 -0
- package/template/node_modules/.vite/deps/astro_n_aria-query.js.map +1 -0
- package/template/node_modules/.vite/deps/astro_n_axobject-query.js +2705 -0
- package/template/node_modules/.vite/deps/astro_n_axobject-query.js.map +1 -0
- package/template/node_modules/.vite/deps/astro_n_html-escaper.js +65 -0
- package/template/node_modules/.vite/deps/astro_n_html-escaper.js.map +1 -0
- package/template/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js +455 -0
- package/template/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js.map +1 -0
- package/template/node_modules/.vite/deps/audit-Do8QH17W.js +1543 -0
- package/template/node_modules/.vite/deps/audit-Do8QH17W.js.map +1 -0
- package/template/node_modules/.vite/deps/highlight-BLtvqtzO.js +48 -0
- package/template/node_modules/.vite/deps/highlight-BLtvqtzO.js.map +1 -0
- package/template/node_modules/.vite/deps/icons-C92i61-2.js +42 -0
- package/template/node_modules/.vite/deps/icons-C92i61-2.js.map +1 -0
- package/template/node_modules/.vite/deps/package.json +3 -0
- package/template/node_modules/.vite/deps/rolldown-runtime-C3I60J0A.js +4 -0
- package/template/node_modules/.vite/deps/settings-BYmTKrLi.js +189 -0
- package/template/node_modules/.vite/deps/settings-BYmTKrLi.js.map +1 -0
- package/template/node_modules/.vite/deps/toolbar-CEKhukuK.js +461 -0
- package/template/node_modules/.vite/deps/toolbar-CEKhukuK.js.map +1 -0
- package/template/node_modules/.vite/deps/ui-library-BO4--ANp.js +1015 -0
- package/template/node_modules/.vite/deps/ui-library-BO4--ANp.js.map +1 -0
- package/template/node_modules/.vite/deps/window-vo3A6TMn.js +32 -0
- package/template/node_modules/.vite/deps/window-vo3A6TMn.js.map +1 -0
- package/template/node_modules/.vite/deps/xray-Lf9x6g8P.js +112 -0
- package/template/node_modules/.vite/deps/xray-Lf9x6g8P.js.map +1 -0
- package/template/src/content.config.ts +21 -0
- package/README.md +0 -3
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { existsSync, readdirSync, statSync } from "node:fs"
|
|
2
|
+
import { extname, join, relative } from "node:path"
|
|
3
|
+
|
|
4
|
+
const IGNORED_DIRS = new Set(["node_modules", "dist", ".hgdc-docs", ".git", ".github", ".vercel"])
|
|
5
|
+
|
|
6
|
+
/** Dossiers d'images publiés tels quels ; `public/` est servi à la racine du site. */
|
|
7
|
+
export const ASSET_DIRS = ["images", "img", "logo", "assets", "static"]
|
|
8
|
+
const MEDIA = new Set([".png", ".jpg", ".jpeg", ".gif", ".webp", ".avif", ".svg", ".ico", ".mp4", ".webm", ".woff2"])
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Fichiers statiques publiés, et seulement ceux-là : le logo et le favicon déclarés dans
|
|
12
|
+
* docs.json, les médias des dossiers d'images (`images/`, `logo/`…), et tout `public/`.
|
|
13
|
+
* Un `.txt` ou un `.pdf` oublié ailleurs dans le dépôt n'est jamais mis en ligne.
|
|
14
|
+
*/
|
|
15
|
+
export function publicFiles(contentDir: string, declared: ReadonlyArray<string>): Array<{ from: string; to: string }> {
|
|
16
|
+
const files: Array<{ from: string; to: string }> = []
|
|
17
|
+
const walk = (dir: string, onlyMedia: boolean, base: string) => {
|
|
18
|
+
if (!existsSync(dir)) return
|
|
19
|
+
for (const name of readdirSync(dir)) {
|
|
20
|
+
if (name.startsWith(".") || IGNORED_DIRS.has(name)) continue
|
|
21
|
+
const path = join(dir, name)
|
|
22
|
+
if (statSync(path).isDirectory()) walk(path, onlyMedia, base)
|
|
23
|
+
else if (!onlyMedia || MEDIA.has(extname(name).toLowerCase()))
|
|
24
|
+
files.push({ from: path, to: relative(base, path) })
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
for (const folder of ASSET_DIRS) walk(join(contentDir, folder), true, contentDir)
|
|
28
|
+
walk(join(contentDir, "public"), false, join(contentDir, "public"))
|
|
29
|
+
for (const asset of declared) {
|
|
30
|
+
const from = join(contentDir, asset.replace(/^\/+/, ""))
|
|
31
|
+
if (relative(contentDir, from).startsWith("..") || !existsSync(from)) continue
|
|
32
|
+
files.push({ from, to: relative(contentDir, from) })
|
|
33
|
+
}
|
|
34
|
+
return files
|
|
35
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
---
|
|
2
|
+
interface Props {
|
|
3
|
+
title: string
|
|
4
|
+
defaultOpen?: boolean
|
|
5
|
+
}
|
|
6
|
+
const { title, defaultOpen = false } = Astro.props
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
<details class="hgdc-accordion" open={defaultOpen}>
|
|
10
|
+
<summary>{title}</summary>
|
|
11
|
+
<div class="hgdc-accordion__body"><slot /></div>
|
|
12
|
+
</details>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
interface Props {
|
|
3
|
+
title: string
|
|
4
|
+
href?: string
|
|
5
|
+
/** Accepté pour la compatibilité Mintlify, ignoré : les icônes de Mintlify sont propriétaires. */
|
|
6
|
+
icon?: string
|
|
7
|
+
}
|
|
8
|
+
const { title, href } = Astro.props
|
|
9
|
+
const Tag = href ? "a" : "div"
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
<Tag class:list={["hgdc-card", { "hgdc-card--link": href }]} href={href}>
|
|
13
|
+
<span class="hgdc-card__title">{title}</span>
|
|
14
|
+
<div class="hgdc-card__body"><slot /></div>
|
|
15
|
+
</Tag>
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Markdown restreint des réponses de l'assistant → arbre de nœuds, rendu ensuite en DOM
|
|
3
|
+
* avec `textContent` seulement : aucun HTML de la réponse n'est jamais interprété. Une
|
|
4
|
+
* réponse du modèle reste une donnée non fiable, même quand elle cite la doc.
|
|
5
|
+
*/
|
|
6
|
+
export type Inline =
|
|
7
|
+
| { type: "text"; value: string }
|
|
8
|
+
| { type: "strong" | "em"; children: Array<Inline> }
|
|
9
|
+
| { type: "code"; value: string }
|
|
10
|
+
| { type: "link"; href: string; children: Array<Inline> }
|
|
11
|
+
| { type: "ref"; n: number }
|
|
12
|
+
|
|
13
|
+
export type Block =
|
|
14
|
+
| { type: "paragraph"; children: Array<Inline> }
|
|
15
|
+
| { type: "heading"; children: Array<Inline> }
|
|
16
|
+
| { type: "list"; ordered: boolean; items: Array<Array<Inline>> }
|
|
17
|
+
| { type: "code"; language: string | null; value: string }
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Liens permis : chemins du site, ancres, et adresses des origines données (le site de la
|
|
21
|
+
* doc). Un lien vers ailleurs, même en https, devient du texte : une page injectée ne peut
|
|
22
|
+
* pas faire afficher un lien de hameçonnage par l'assistant.
|
|
23
|
+
*/
|
|
24
|
+
export function safeHref(href: string, origins: ReadonlyArray<string> = []): string | null {
|
|
25
|
+
const value = href.trim()
|
|
26
|
+
if (value.startsWith("#")) return value
|
|
27
|
+
if (value.startsWith("/") && !value.startsWith("//") && !value.startsWith("/\\")) return value
|
|
28
|
+
try {
|
|
29
|
+
const url = new URL(value)
|
|
30
|
+
const web = url.protocol === "https:" || url.protocol === "http:"
|
|
31
|
+
return web && origins.includes(url.origin) ? url.toString() : null
|
|
32
|
+
} catch {
|
|
33
|
+
return null
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const INLINE = /(`[^`]+`)|(\*\*[^*]+\*\*)|(\*[^*\s][^*]*\*|_[^_\s][^_]*_)|(\[\^(\d+)\])|(\[([^\]]+)\]\(([^)\s]+)\))/
|
|
38
|
+
|
|
39
|
+
export function parseInline(source: string, origins: ReadonlyArray<string> = []): Array<Inline> {
|
|
40
|
+
const out: Array<Inline> = []
|
|
41
|
+
let rest = source
|
|
42
|
+
while (rest) {
|
|
43
|
+
const match = INLINE.exec(rest)
|
|
44
|
+
if (!match) {
|
|
45
|
+
out.push({ type: "text", value: rest })
|
|
46
|
+
break
|
|
47
|
+
}
|
|
48
|
+
if (match.index > 0) out.push({ type: "text", value: rest.slice(0, match.index) })
|
|
49
|
+
const [whole, code, strong, em, ref, refN, link, linkText, linkHref] = match
|
|
50
|
+
if (code) out.push({ type: "code", value: code.slice(1, -1) })
|
|
51
|
+
else if (strong) out.push({ type: "strong", children: parseInline(strong.slice(2, -2), origins) })
|
|
52
|
+
else if (em) out.push({ type: "em", children: parseInline(em.slice(1, -1), origins) })
|
|
53
|
+
else if (ref && refN) out.push({ type: "ref", n: Number(refN) })
|
|
54
|
+
else if (link && linkText && linkHref) {
|
|
55
|
+
const href = safeHref(linkHref, origins)
|
|
56
|
+
out.push(
|
|
57
|
+
href ? { type: "link", href, children: parseInline(linkText, origins) } : { type: "text", value: linkText },
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
rest = rest.slice(match.index + whole.length)
|
|
61
|
+
}
|
|
62
|
+
return out
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const LIST = /^\s*(?:([-*])|(\d+)[.)])\s+(.*)$/
|
|
66
|
+
|
|
67
|
+
export function parseMarkdown(source: string, origins: ReadonlyArray<string> = []): Array<Block> {
|
|
68
|
+
const blocks: Array<Block> = []
|
|
69
|
+
const lines = source.replace(/\r\n?/g, "\n").split("\n")
|
|
70
|
+
let paragraph: Array<string> = []
|
|
71
|
+
const flush = () => {
|
|
72
|
+
if (paragraph.length) blocks.push({ type: "paragraph", children: parseInline(paragraph.join(" "), origins) })
|
|
73
|
+
paragraph = []
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
for (let i = 0; i < lines.length; i++) {
|
|
77
|
+
const line = lines[i]!
|
|
78
|
+
const fence = /^\s*```\s*([\w+-]*)\s*$/.exec(line)
|
|
79
|
+
if (fence) {
|
|
80
|
+
flush()
|
|
81
|
+
const body: Array<string> = []
|
|
82
|
+
while (++i < lines.length && !/^\s*```\s*$/.test(lines[i]!)) body.push(lines[i]!)
|
|
83
|
+
blocks.push({ type: "code", language: fence[1] || null, value: body.join("\n") })
|
|
84
|
+
continue
|
|
85
|
+
}
|
|
86
|
+
const heading = /^#{1,6}\s+(.*)$/.exec(line)
|
|
87
|
+
if (heading) {
|
|
88
|
+
flush()
|
|
89
|
+
blocks.push({ type: "heading", children: parseInline(heading[1]!, origins) })
|
|
90
|
+
continue
|
|
91
|
+
}
|
|
92
|
+
const item = LIST.exec(line)
|
|
93
|
+
if (item) {
|
|
94
|
+
flush()
|
|
95
|
+
const ordered = Boolean(item[2])
|
|
96
|
+
const previous = blocks.at(-1)
|
|
97
|
+
const list =
|
|
98
|
+
previous?.type === "list" && previous.ordered === ordered && lines[i - 1]?.trim() !== ""
|
|
99
|
+
? previous
|
|
100
|
+
: (blocks[blocks.push({ type: "list", ordered, items: [] }) - 1] as Extract<Block, { type: "list" }>)
|
|
101
|
+
list.items.push(parseInline(item[3]!, origins))
|
|
102
|
+
continue
|
|
103
|
+
}
|
|
104
|
+
if (line.trim() === "") flush()
|
|
105
|
+
else paragraph.push(line.trim())
|
|
106
|
+
}
|
|
107
|
+
flush()
|
|
108
|
+
return blocks
|
|
109
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { z } from "zod"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Contrat de `POST /api/v1/ask` entre le widget du site et la console. Une seule source,
|
|
5
|
+
* importée des deux côtés : un champ ajouté ici est vu par le build et par l'API.
|
|
6
|
+
*/
|
|
7
|
+
export const MAX_QUESTION = 1000
|
|
8
|
+
export const MAX_PREVIOUS = 3
|
|
9
|
+
|
|
10
|
+
export const askRequestSchema = z.object({
|
|
11
|
+
project: z.string().regex(/^[a-z0-9]{8,64}$/),
|
|
12
|
+
question: z.string().trim().min(2).max(MAX_QUESTION),
|
|
13
|
+
/** Chemin de la page ouverte : l'assistant la privilégie dans sa recherche. */
|
|
14
|
+
page: z.string().startsWith("/").max(512).optional(),
|
|
15
|
+
/**
|
|
16
|
+
* Échanges précédents, par leur identifiant (`id` des réponses) : le serveur relit ce qu'il a
|
|
17
|
+
* lui-même répondu. Un visiteur ne peut pas glisser une fausse réponse de l'assistant.
|
|
18
|
+
*/
|
|
19
|
+
previous: z
|
|
20
|
+
.array(z.string().regex(/^[a-z0-9]{8,64}$/))
|
|
21
|
+
.max(MAX_PREVIOUS)
|
|
22
|
+
.optional(),
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
export type AskRequest = z.infer<typeof askRequestSchema>
|
|
26
|
+
|
|
27
|
+
export const sourceSchema = z.object({
|
|
28
|
+
n: z.number().int().positive(),
|
|
29
|
+
title: z.string(),
|
|
30
|
+
section: z.string().nullable(),
|
|
31
|
+
url: z.string(),
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
export type AnswerSource = z.infer<typeof sourceSchema>
|
|
35
|
+
|
|
36
|
+
export const askResponseSchema = z.object({
|
|
37
|
+
id: z.string(),
|
|
38
|
+
/** `no_answer` : rien dans la doc ne répond, l'assistant le dit au lieu d'inventer. */
|
|
39
|
+
status: z.enum(["answered", "no_answer"]),
|
|
40
|
+
/** Markdown restreint ; `[^n]` renvoie à la source `n`. */
|
|
41
|
+
answer: z.string(),
|
|
42
|
+
sources: z.array(sourceSchema),
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
export type AskResponse = z.infer<typeof askResponseSchema>
|
|
46
|
+
|
|
47
|
+
export const ASK_ERRORS = {
|
|
48
|
+
invalid_request: "La question n'a pas pu être lue.",
|
|
49
|
+
project_not_found: "Cette documentation n'a pas d'assistant.",
|
|
50
|
+
assistant_disabled: "L'assistant est désactivé pour cette documentation.",
|
|
51
|
+
origin_not_allowed: "Cet assistant ne répond que depuis son site de documentation.",
|
|
52
|
+
rate_limited: "Trop de questions d'un coup. Réessaie dans une minute.",
|
|
53
|
+
quota_exceeded: "L'assistant a atteint sa limite de questions pour ce mois.",
|
|
54
|
+
not_indexed: "La documentation n'est pas encore indexée. Réessaie après la prochaine publication.",
|
|
55
|
+
unavailable: "L'assistant ne répond pas pour l'instant. Réessaie dans un moment.",
|
|
56
|
+
} as const
|
|
57
|
+
|
|
58
|
+
export type AskErrorCode = keyof typeof ASK_ERRORS
|
|
59
|
+
|
|
60
|
+
export const askErrorSchema = z.object({
|
|
61
|
+
error: z.object({
|
|
62
|
+
code: z.enum(Object.keys(ASK_ERRORS) as [AskErrorCode, ...Array<AskErrorCode>]),
|
|
63
|
+
message: z.string(),
|
|
64
|
+
}),
|
|
65
|
+
})
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import GithubSlugger from "github-slugger"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Découpe une page en passages pour la recherche de l'assistant : une section par titre
|
|
5
|
+
* (`##` à `######`), coupée en paragraphes si elle dépasse `MAX_CHARS`. Chaque passage
|
|
6
|
+
* garde l'ancre de son titre, calculée comme les identifiants de Starlight (github-slugger),
|
|
7
|
+
* pour que la source citée ouvre la bonne section.
|
|
8
|
+
*/
|
|
9
|
+
export const MAX_CHARS = 1600
|
|
10
|
+
const MIN_CHARS = 120
|
|
11
|
+
|
|
12
|
+
export type Chunk = {
|
|
13
|
+
/** Fil des titres, page comprise : « Publier › Avec Host ». */
|
|
14
|
+
heading: string
|
|
15
|
+
anchor: string | null
|
|
16
|
+
text: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
type Section = { trail: Array<string>; anchor: string | null; lines: Array<string> }
|
|
20
|
+
|
|
21
|
+
/** Au-delà, une ligne n'est pas un titre : la borne garde l'analyse linéaire. */
|
|
22
|
+
const MAX_HEADING_LINE = 500
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Titre ATX (`## Titre`, fermeture `##` facultative), lu caractère par caractère : pas
|
|
26
|
+
* d'expression régulière à quantificateurs chevauchants, donc pas de retour arrière
|
|
27
|
+
* explosif sur une ligne piégée (« ## a » suivi de milliers d'espaces).
|
|
28
|
+
*/
|
|
29
|
+
export function parseHeading(line: string): { level: number; text: string } | null {
|
|
30
|
+
if (line.length > MAX_HEADING_LINE || !line.startsWith("#")) return null
|
|
31
|
+
let level = 0
|
|
32
|
+
while (line[level] === "#") level++
|
|
33
|
+
if (level > 6 || (line[level] !== " " && line[level] !== "\t")) return null
|
|
34
|
+
let text = line.slice(level).trim()
|
|
35
|
+
let end = text.length
|
|
36
|
+
while (end > 0 && text[end - 1] === "#") end--
|
|
37
|
+
if (end < text.length && (end === 0 || text[end - 1] === " " || text[end - 1] === "\t"))
|
|
38
|
+
text = text.slice(0, end).trim()
|
|
39
|
+
return text ? { level, text } : null
|
|
40
|
+
}
|
|
41
|
+
const FENCE = /^\s*(```|~~~)/
|
|
42
|
+
|
|
43
|
+
export function chunkPage(title: string, markdown: string): Array<Chunk> {
|
|
44
|
+
const slugger = new GithubSlugger()
|
|
45
|
+
const sections: Array<Section> = [{ trail: [], anchor: null, lines: [] }]
|
|
46
|
+
const stack: Array<{ level: number; text: string }> = []
|
|
47
|
+
let fence: string | null = null
|
|
48
|
+
|
|
49
|
+
for (const line of markdown.replace(/\r\n?/g, "\n").split("\n")) {
|
|
50
|
+
const fenceMatch = FENCE.exec(line)
|
|
51
|
+
if (fenceMatch) fence = fence === null ? fenceMatch[1]! : fence === fenceMatch[1] ? null : fence
|
|
52
|
+
const heading = fence === null && !fenceMatch ? parseHeading(line) : null
|
|
53
|
+
if (!heading) {
|
|
54
|
+
sections.at(-1)!.lines.push(line)
|
|
55
|
+
continue
|
|
56
|
+
}
|
|
57
|
+
const level = heading.level
|
|
58
|
+
const text = stripInline(heading.text)
|
|
59
|
+
if (level === 1) continue // le titre de la page, déjà porté par `title`
|
|
60
|
+
while (stack.length && stack.at(-1)!.level >= level) stack.pop()
|
|
61
|
+
stack.push({ level, text })
|
|
62
|
+
sections.push({ trail: stack.map((s) => s.text), anchor: slugger.slug(text), lines: [] })
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const chunks: Array<Chunk> = []
|
|
66
|
+
for (const section of sections) {
|
|
67
|
+
const body = section.lines.join("\n").trim()
|
|
68
|
+
if (!body) continue
|
|
69
|
+
const heading = [title, ...section.trail].join(" › ")
|
|
70
|
+
for (const part of splitBody(body)) chunks.push({ heading, anchor: section.anchor, text: part })
|
|
71
|
+
}
|
|
72
|
+
return mergeSmall(chunks)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Paragraphes regroupés jusqu'à `MAX_CHARS`, sans couper un bloc de code. */
|
|
76
|
+
function splitBody(body: string): Array<string> {
|
|
77
|
+
if (body.length <= MAX_CHARS) return [body]
|
|
78
|
+
const blocks: Array<string> = []
|
|
79
|
+
let current: Array<string> = []
|
|
80
|
+
let fence = false
|
|
81
|
+
for (const line of body.split("\n")) {
|
|
82
|
+
if (FENCE.test(line)) fence = !fence
|
|
83
|
+
if (!fence && line.trim() === "" && current.length) {
|
|
84
|
+
blocks.push(current.join("\n"))
|
|
85
|
+
current = []
|
|
86
|
+
continue
|
|
87
|
+
}
|
|
88
|
+
current.push(line)
|
|
89
|
+
}
|
|
90
|
+
if (current.length) blocks.push(current.join("\n"))
|
|
91
|
+
|
|
92
|
+
const parts: Array<string> = []
|
|
93
|
+
let buffer = ""
|
|
94
|
+
for (const block of blocks) {
|
|
95
|
+
if (buffer && buffer.length + block.length + 2 > MAX_CHARS) {
|
|
96
|
+
parts.push(buffer)
|
|
97
|
+
buffer = ""
|
|
98
|
+
}
|
|
99
|
+
buffer = buffer ? `${buffer}\n\n${block}` : block
|
|
100
|
+
while (buffer.length > MAX_CHARS * 2) {
|
|
101
|
+
parts.push(buffer.slice(0, MAX_CHARS))
|
|
102
|
+
buffer = buffer.slice(MAX_CHARS)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (buffer) parts.push(buffer)
|
|
106
|
+
return parts
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Un passage très court rejoint le suivant de la même section : seul, il ne répond à rien. */
|
|
110
|
+
function mergeSmall(chunks: Array<Chunk>): Array<Chunk> {
|
|
111
|
+
const merged: Array<Chunk> = []
|
|
112
|
+
for (const chunk of chunks) {
|
|
113
|
+
const previous = merged.at(-1)
|
|
114
|
+
if (previous && previous.text.length < MIN_CHARS && previous.anchor === chunk.anchor) {
|
|
115
|
+
previous.text = `${previous.text}\n\n${chunk.text}`
|
|
116
|
+
continue
|
|
117
|
+
}
|
|
118
|
+
merged.push({ ...chunk })
|
|
119
|
+
}
|
|
120
|
+
return merged
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function stripInline(text: string): string {
|
|
124
|
+
return text
|
|
125
|
+
.replace(/`([^`]*)`/g, "$1")
|
|
126
|
+
.replace(/\[([^\]]*)\]\([^)]*\)/g, "$1")
|
|
127
|
+
.replace(/[*_]/g, "")
|
|
128
|
+
.trim()
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Texte envoyé au modèle d'embeddings : le contexte du titre aide à retrouver un passage court. */
|
|
132
|
+
export function embeddingText(chunk: Chunk): string {
|
|
133
|
+
return `${chunk.heading}\n\n${chunk.text}`
|
|
134
|
+
}
|