blume 2.0.0 → 2.0.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/AGENTS.md +1 -1
- package/CHANGELOG.md +28 -0
- package/README.md +2 -2
- package/dist/cli/{chunk-f7t03s3g.js → chunk-27g6wdth.js} +2 -2
- package/dist/cli/{chunk-mnqj32sj.js → chunk-2hn4b8z7.js} +13 -13
- package/dist/cli/chunk-2hn4b8z7.js.map +12 -0
- package/dist/cli/{chunk-by2290sx.js → chunk-5shv93fd.js} +2 -2
- package/dist/cli/{chunk-a9kptbw5.js → chunk-6crbhc3x.js} +3 -3
- package/dist/cli/{chunk-a9kptbw5.js.map → chunk-6crbhc3x.js.map} +1 -1
- package/dist/cli/{chunk-mwt1k8n7.js → chunk-6hsn950k.js} +20 -20
- package/dist/cli/chunk-6hsn950k.js.map +10 -0
- package/dist/cli/{chunk-11j0384y.js → chunk-6vm74dry.js} +13 -13
- package/dist/cli/{chunk-11j0384y.js.map → chunk-6vm74dry.js.map} +3 -3
- package/dist/cli/{chunk-j8mw0za6.js → chunk-79jhk4py.js} +8 -8
- package/dist/cli/{chunk-j8mw0za6.js.map → chunk-79jhk4py.js.map} +2 -2
- package/dist/cli/{chunk-nk3ts2xk.js → chunk-82bbrxdn.js} +2 -2
- package/dist/cli/{chunk-2q1dwty4.js → chunk-ah61y8py.js} +8 -8
- package/dist/cli/{chunk-2q1dwty4.js.map → chunk-ah61y8py.js.map} +4 -4
- package/dist/cli/{chunk-zxccj738.js → chunk-ce574jw2.js} +1 -1
- package/dist/cli/{chunk-y3e45rc8.js → chunk-ch6g3ar0.js} +3 -3
- package/dist/cli/{chunk-beat36xx.js → chunk-dh8cwk36.js} +5 -5
- package/dist/cli/{chunk-beat36xx.js.map → chunk-dh8cwk36.js.map} +2 -2
- package/dist/cli/{chunk-1w8dp3qb.js → chunk-epjnccmv.js} +13 -13
- package/dist/cli/{chunk-ernrthtr.js → chunk-f2z5v128.js} +13 -13
- package/dist/cli/{chunk-zg2gtj10.js → chunk-fs23ddbb.js} +2 -2
- package/dist/cli/{chunk-7ez8ny0t.js → chunk-fxypxtvm.js} +2 -2
- package/dist/cli/{chunk-tzne8qfq.js → chunk-fz5wtpmh.js} +13 -13
- package/dist/cli/{chunk-b5aj94ah.js → chunk-hdpx1tax.js} +4 -4
- package/dist/cli/{chunk-d80hr03s.js → chunk-jwyddg7y.js} +9 -9
- package/dist/cli/{chunk-d80hr03s.js.map → chunk-jwyddg7y.js.map} +2 -2
- package/dist/cli/{chunk-fh5hj5jt.js → chunk-kdp5q7ke.js} +15 -15
- package/dist/cli/{chunk-6k8vp3ta.js → chunk-kpf8rrjc.js} +9 -9
- package/dist/cli/{chunk-6k8vp3ta.js.map → chunk-kpf8rrjc.js.map} +3 -3
- package/dist/cli/{chunk-5a2z0198.js → chunk-m3vmjgmq.js} +9 -9
- package/dist/cli/{chunk-5a2z0198.js.map → chunk-m3vmjgmq.js.map} +2 -2
- package/dist/cli/{chunk-bctazmbk.js → chunk-mb2919y2.js} +4 -4
- package/dist/cli/{chunk-79njf86q.js → chunk-q5163e60.js} +13 -13
- package/dist/cli/{chunk-xaz13gwg.js → chunk-qkqwkpte.js} +196 -208
- package/dist/cli/{chunk-xaz13gwg.js.map → chunk-qkqwkpte.js.map} +48 -48
- package/dist/cli/{chunk-sqn5t4q0.js → chunk-qs4q5p4e.js} +3 -3
- package/dist/cli/{chunk-bw22s759.js → chunk-qwsrynx5.js} +1 -1
- package/dist/cli/{chunk-z01ze5c1.js → chunk-s1p84fyh.js} +15 -15
- package/dist/cli/{chunk-pnnvybbk.js → chunk-s6jhgk0q.js} +5 -5
- package/dist/cli/{chunk-pnnvybbk.js.map → chunk-s6jhgk0q.js.map} +2 -2
- package/dist/cli/{chunk-f2972sbt.js → chunk-vtk4a6dg.js} +1 -1
- package/dist/cli/{chunk-z1f5arsg.js → chunk-wgm7m9qk.js} +21 -21
- package/dist/cli/{chunk-z1f5arsg.js.map → chunk-wgm7m9qk.js.map} +4 -4
- package/dist/cli/{chunk-bnbmcwfb.js → chunk-wm7js3j9.js} +5 -5
- package/dist/cli/{chunk-bnbmcwfb.js.map → chunk-wm7js3j9.js.map} +3 -3
- package/dist/cli/{chunk-d1tadaw7.js → chunk-yt5n7ppj.js} +3 -3
- package/dist/cli/{chunk-pat2zzwc.js → chunk-yw7dm696.js} +1 -1
- package/dist/cli/{chunk-pat2zzwc.js.map → chunk-yw7dm696.js.map} +1 -1
- package/dist/cli/{chunk-88cpgt6h.js → chunk-zxcczpyx.js} +1 -1
- package/dist/cli/{chunk-41za066z.js → chunk-zxh4d9vy.js} +4 -4
- package/dist/cli/index.js +17 -17
- package/dist/types/ai/agent-readability.d.ts +1 -1
- package/dist/types/ai/api/paths.d.ts +1 -1
- package/dist/types/ai/ask-context.d.ts +7 -7
- package/dist/types/ai/ask.d.ts +43 -43
- package/dist/types/ai/index.d.ts +3 -3
- package/dist/types/ai/openapi-components.d.ts +1 -1
- package/dist/types/ai/serializers.d.ts +1 -1
- package/dist/types/ai/visibility.d.ts +1 -1
- package/dist/types/core/config-input.d.ts +17 -17
- package/dist/types/core/config.d.ts +3 -3
- package/dist/types/core/data.d.ts +3 -3
- package/dist/types/core/i18n-ui.d.ts +6 -8
- package/dist/types/core/schema.d.ts +5 -5
- package/dist/types/core/unrecognized-keys.d.ts +1 -1
- package/dist/types/search/documents.d.ts +1 -1
- package/dist/types/search/orama-index.d.ts +1 -1
- package/docs/02-deployment.mdx +4 -4
- package/docs/03-upgrading.mdx +22 -9
- package/docs/04-migrating.mdx +4 -4
- package/docs/08-faq.mdx +3 -3
- package/docs/advanced/custom-pages.mdx +2 -2
- package/docs/advanced/skills.mdx +1 -1
- package/docs/cli/audit.mdx +3 -3
- package/docs/cli/doctor.mdx +3 -3
- package/docs/cli/evals.mdx +7 -7
- package/docs/cli/index.mdx +2 -2
- package/docs/cli/translate.mdx +8 -8
- package/docs/configuration/{ask-ai.mdx → assistant.mdx} +19 -19
- package/docs/configuration/customization.mdx +2 -2
- package/docs/configuration/index.mdx +2 -2
- package/docs/configuration/meta.ts +1 -1
- package/docs/configuration/search.mdx +1 -1
- package/docs/content/i18n.mdx +2 -2
- package/docs/content/islands.mdx +1 -1
- package/docs/discoverability/agent-discovery.mdx +1 -1
- package/docs/discoverability/index.mdx +1 -1
- package/docs/index.mdx +2 -2
- package/package.json +1 -1
- package/skills/blume/SKILL.md +5 -5
- package/skills/blume-migrate/SKILL.md +2 -2
- package/src/ai/agent-readability.ts +4 -4
- package/src/ai/api/paths.ts +1 -1
- package/src/ai/ask-context.ts +7 -7
- package/src/ai/ask-data.ts +2 -2
- package/src/ai/ask.ts +84 -71
- package/src/ai/cors.ts +3 -3
- package/src/ai/index.ts +16 -16
- package/src/ai/openapi-components.ts +1 -1
- package/src/ai/serializers.ts +1 -1
- package/src/ai/visibility.ts +1 -1
- package/src/astro/generate.ts +19 -18
- package/src/astro/module-types.ts +1 -1
- package/src/astro/runtime-deps.ts +6 -6
- package/src/astro/templates.ts +26 -26
- package/src/blume-modules.d.ts +2 -2
- package/src/cli/commands/audit.ts +1 -1
- package/src/cli/commands/doctor.ts +7 -5
- package/src/cli/commands/eval.ts +3 -3
- package/src/cli/commands/migrate.ts +2 -2
- package/src/cli/commands/translate.ts +3 -3
- package/src/cli/commands/upgrade.ts +2 -2
- package/src/cli/required-secrets.ts +3 -3
- package/src/components/copy-feedback.ts +1 -1
- package/src/components/islands/{AskAI.astro → Assistant.astro} +10 -10
- package/src/components/islands/{ask-ai.tsx → assistant.tsx} +23 -23
- package/src/components/islands/hooks.ts +14 -12
- package/src/components/layout/Header.astro +10 -10
- package/src/components/layout/PageLayout.astro +6 -6
- package/src/components/layout/Pagination.astro +7 -7
- package/src/components/layout/ReferenceLayout.astro +1 -1
- package/src/components/layout/RootLayout.astro +6 -6
- package/src/components/layout/Search.astro +13 -13
- package/src/components/layout/analytics-client.ts +1 -1
- package/src/components/layout/drawer-inert.ts +1 -1
- package/src/components/openapi/description.ts +2 -2
- package/src/core/code-fences.ts +1 -1
- package/src/core/config-input.ts +19 -19
- package/src/core/config.ts +3 -3
- package/src/core/data.ts +3 -3
- package/src/core/i18n-ui.ts +35 -9
- package/src/core/request-body.ts +1 -1
- package/src/core/schema.ts +25 -20
- package/src/core/server-features.ts +2 -2
- package/src/core/ui-packs/ar.ts +4 -5
- package/src/core/ui-packs/bg.ts +4 -5
- package/src/core/ui-packs/bn.ts +4 -5
- package/src/core/ui-packs/ca.ts +4 -5
- package/src/core/ui-packs/cs.ts +4 -5
- package/src/core/ui-packs/da.ts +4 -5
- package/src/core/ui-packs/de.ts +4 -5
- package/src/core/ui-packs/el.ts +4 -5
- package/src/core/ui-packs/es.ts +4 -5
- package/src/core/ui-packs/fa.ts +4 -5
- package/src/core/ui-packs/fi.ts +4 -5
- package/src/core/ui-packs/fr.ts +4 -5
- package/src/core/ui-packs/he.ts +4 -5
- package/src/core/ui-packs/hi.ts +4 -5
- package/src/core/ui-packs/hr.ts +4 -5
- package/src/core/ui-packs/hu.ts +4 -5
- package/src/core/ui-packs/id.ts +4 -5
- package/src/core/ui-packs/it.ts +4 -5
- package/src/core/ui-packs/ja.ts +4 -5
- package/src/core/ui-packs/ko.ts +4 -5
- package/src/core/ui-packs/nl.ts +4 -5
- package/src/core/ui-packs/no.ts +4 -5
- package/src/core/ui-packs/pl.ts +4 -5
- package/src/core/ui-packs/pt-br.ts +4 -5
- package/src/core/ui-packs/pt.ts +4 -5
- package/src/core/ui-packs/ro.ts +4 -5
- package/src/core/ui-packs/ru.ts +4 -5
- package/src/core/ui-packs/sk.ts +4 -5
- package/src/core/ui-packs/sr.ts +4 -5
- package/src/core/ui-packs/sv.ts +4 -5
- package/src/core/ui-packs/th.ts +4 -5
- package/src/core/ui-packs/tr.ts +4 -5
- package/src/core/ui-packs/uk.ts +4 -5
- package/src/core/ui-packs/vi.ts +4 -5
- package/src/core/ui-packs/zh-tw.ts +4 -5
- package/src/core/ui-packs/zh.ts +4 -5
- package/src/core/unrecognized-keys.ts +1 -1
- package/src/registry/eject.ts +19 -18
- package/src/search/documents.ts +2 -2
- package/src/search/orama-index.ts +1 -1
- package/src/translate/report.ts +1 -1
- package/src/upgrade/upgrade.ts +1 -1
- package/dist/cli/chunk-mnqj32sj.js.map +0 -12
- package/dist/cli/chunk-mwt1k8n7.js.map +0 -10
- /package/dist/cli/{chunk-f7t03s3g.js.map → chunk-27g6wdth.js.map} +0 -0
- /package/dist/cli/{chunk-by2290sx.js.map → chunk-5shv93fd.js.map} +0 -0
- /package/dist/cli/{chunk-nk3ts2xk.js.map → chunk-82bbrxdn.js.map} +0 -0
- /package/dist/cli/{chunk-zxccj738.js.map → chunk-ce574jw2.js.map} +0 -0
- /package/dist/cli/{chunk-y3e45rc8.js.map → chunk-ch6g3ar0.js.map} +0 -0
- /package/dist/cli/{chunk-1w8dp3qb.js.map → chunk-epjnccmv.js.map} +0 -0
- /package/dist/cli/{chunk-ernrthtr.js.map → chunk-f2z5v128.js.map} +0 -0
- /package/dist/cli/{chunk-zg2gtj10.js.map → chunk-fs23ddbb.js.map} +0 -0
- /package/dist/cli/{chunk-7ez8ny0t.js.map → chunk-fxypxtvm.js.map} +0 -0
- /package/dist/cli/{chunk-tzne8qfq.js.map → chunk-fz5wtpmh.js.map} +0 -0
- /package/dist/cli/{chunk-b5aj94ah.js.map → chunk-hdpx1tax.js.map} +0 -0
- /package/dist/cli/{chunk-fh5hj5jt.js.map → chunk-kdp5q7ke.js.map} +0 -0
- /package/dist/cli/{chunk-bctazmbk.js.map → chunk-mb2919y2.js.map} +0 -0
- /package/dist/cli/{chunk-79njf86q.js.map → chunk-q5163e60.js.map} +0 -0
- /package/dist/cli/{chunk-sqn5t4q0.js.map → chunk-qs4q5p4e.js.map} +0 -0
- /package/dist/cli/{chunk-bw22s759.js.map → chunk-qwsrynx5.js.map} +0 -0
- /package/dist/cli/{chunk-z01ze5c1.js.map → chunk-s1p84fyh.js.map} +0 -0
- /package/dist/cli/{chunk-f2972sbt.js.map → chunk-vtk4a6dg.js.map} +0 -0
- /package/dist/cli/{chunk-d1tadaw7.js.map → chunk-yt5n7ppj.js.map} +0 -0
- /package/dist/cli/{chunk-88cpgt6h.js.map → chunk-zxcczpyx.js.map} +0 -0
- /package/dist/cli/{chunk-41za066z.js.map → chunk-zxh4d9vy.js.map} +0 -0
|
@@ -7,7 +7,7 @@ import { createPortal } from "react-dom";
|
|
|
7
7
|
import type { UIStrings } from "../../core/i18n-ui.ts";
|
|
8
8
|
import { copyText } from "../copy-feedback.ts";
|
|
9
9
|
import { joinBase, prefixBase } from "./base-path.ts";
|
|
10
|
-
import {
|
|
10
|
+
import { useAssistant } from "./hooks.ts";
|
|
11
11
|
|
|
12
12
|
/** A resolved empty-state prompt; `icon` is ready-to-inline SVG (or null). */
|
|
13
13
|
interface Suggestion {
|
|
@@ -16,10 +16,10 @@ interface Suggestion {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
|
-
* The panel's chrome glyphs, resolved server-side in `
|
|
19
|
+
* The panel's chrome glyphs, resolved server-side in `Assistant.astro` and passed in
|
|
20
20
|
* as ready-to-inline Lucide bodies so this client island ships no icon data.
|
|
21
21
|
*/
|
|
22
|
-
interface
|
|
22
|
+
interface AssistantIcons {
|
|
23
23
|
arrowUp: string;
|
|
24
24
|
chat: string;
|
|
25
25
|
clear: string;
|
|
@@ -29,7 +29,7 @@ interface AskIcons {
|
|
|
29
29
|
|
|
30
30
|
// Empty bodies so the island still renders (iconless) if instantiated without
|
|
31
31
|
// the Astro wrapper that resolves the real Lucide glyphs.
|
|
32
|
-
const EMPTY_ICONS:
|
|
32
|
+
const EMPTY_ICONS: AssistantIcons = {
|
|
33
33
|
arrowUp: "",
|
|
34
34
|
chat: "",
|
|
35
35
|
clear: "",
|
|
@@ -38,7 +38,7 @@ const EMPTY_ICONS: AskIcons = {
|
|
|
38
38
|
};
|
|
39
39
|
|
|
40
40
|
// English fallback so the island renders even if no dictionary is passed.
|
|
41
|
-
const DEFAULT_ASK: UIStrings["
|
|
41
|
+
const DEFAULT_ASK: UIStrings["assistant"] = {
|
|
42
42
|
ai: "AI",
|
|
43
43
|
clear: "Clear conversation",
|
|
44
44
|
close: "Close",
|
|
@@ -49,7 +49,7 @@ const DEFAULT_ASK: UIStrings["ask"] = {
|
|
|
49
49
|
placeholder: "Ask a question…",
|
|
50
50
|
send: "Send",
|
|
51
51
|
tip: "Tip: You can open and close chat with",
|
|
52
|
-
title: "
|
|
52
|
+
title: "Assistant",
|
|
53
53
|
you: "You",
|
|
54
54
|
};
|
|
55
55
|
|
|
@@ -130,15 +130,15 @@ const ICON_BUTTON_CLASS =
|
|
|
130
130
|
const ANSWER_CLASS =
|
|
131
131
|
"prose prose-sm max-w-none text-foreground [&_a]:inline-flex [&_a]:items-center [&_a]:gap-1 [&_a]:rounded-full [&_a]:bg-muted [&_a]:px-2 [&_a]:py-1 [&_a]:align-middle [&_a]:font-medium [&_a]:text-[0.7rem] [&_a]:leading-none [&_a]:text-muted-foreground! [&_a]:no-underline! [&_a:hover]:text-foreground!";
|
|
132
132
|
|
|
133
|
-
const
|
|
133
|
+
const Assistant = ({
|
|
134
134
|
endpoint = DEFAULT_ASK_ENDPOINT,
|
|
135
135
|
icons = EMPTY_ICONS,
|
|
136
136
|
strings,
|
|
137
137
|
suggestions = EMPTY_SUGGESTIONS,
|
|
138
138
|
}: {
|
|
139
139
|
endpoint?: string;
|
|
140
|
-
icons?:
|
|
141
|
-
strings?: UIStrings["
|
|
140
|
+
icons?: AssistantIcons;
|
|
141
|
+
strings?: UIStrings["assistant"];
|
|
142
142
|
suggestions?: Suggestion[];
|
|
143
143
|
}) => {
|
|
144
144
|
// Merge per key (not `strings ?? …`) so a dictionary from a stale snapshot
|
|
@@ -147,14 +147,14 @@ const AskAI = ({
|
|
|
147
147
|
const [open, setOpen] = useState(false);
|
|
148
148
|
const [input, setInput] = useState("");
|
|
149
149
|
// The streaming client — request shaping, optimistic assistant bubble,
|
|
150
|
-
// stale-stream/abort guards, error-body handling — is the public
|
|
150
|
+
// stale-stream/abort guards, error-body handling — is the public useAssistant
|
|
151
151
|
// hook, so the built-in panel and custom UIs share one implementation.
|
|
152
152
|
const {
|
|
153
153
|
ask,
|
|
154
154
|
loading: busy,
|
|
155
155
|
messages,
|
|
156
156
|
reset,
|
|
157
|
-
} =
|
|
157
|
+
} = useAssistant({ endpoint, errorMessage: t.error });
|
|
158
158
|
const inputRef = useRef<HTMLTextAreaElement>(null);
|
|
159
159
|
const scrollRef = useRef<HTMLDivElement>(null);
|
|
160
160
|
const triggerRef = useRef<HTMLButtonElement>(null);
|
|
@@ -167,7 +167,7 @@ const AskAI = ({
|
|
|
167
167
|
// Null until mount (guards SSR), then refreshed on every client-router swap:
|
|
168
168
|
// the island rides across navigations via transition:persist, but each swap
|
|
169
169
|
// installs a NEW <body>, discarding the portaled panel with the old one and
|
|
170
|
-
// resetting the `data-blume-
|
|
170
|
+
// resetting the `data-blume-assistant` push attribute to the incoming page's
|
|
171
171
|
// server-rendered set. Reading document.body inline in render would NOT
|
|
172
172
|
// recover from that — it isn't a reactive value, so the memoized portal
|
|
173
173
|
// keeps its stale (detached) container. State identity is what re-anchors
|
|
@@ -183,10 +183,10 @@ const AskAI = ({
|
|
|
183
183
|
return () => document.removeEventListener("astro:after-swap", onSwap);
|
|
184
184
|
}, []);
|
|
185
185
|
|
|
186
|
-
// The search modal forwards its query so "
|
|
186
|
+
// The search modal forwards its query so "Assistant: <query>" carries straight in.
|
|
187
187
|
useEffect(() => {
|
|
188
188
|
const handler = (event: Event) => {
|
|
189
|
-
// SAFETY: `blume:open-
|
|
189
|
+
// SAFETY: `blume:open-assistant` is only ever dispatched as a CustomEvent
|
|
190
190
|
// whose optional detail carries the search query.
|
|
191
191
|
const query = (event as CustomEvent<{ query?: string }>).detail?.query;
|
|
192
192
|
if (query) {
|
|
@@ -194,8 +194,8 @@ const AskAI = ({
|
|
|
194
194
|
}
|
|
195
195
|
setOpen(true);
|
|
196
196
|
};
|
|
197
|
-
window.addEventListener("blume:open-
|
|
198
|
-
return () => window.removeEventListener("blume:open-
|
|
197
|
+
window.addEventListener("blume:open-assistant", handler);
|
|
198
|
+
return () => window.removeEventListener("blume:open-assistant", handler);
|
|
199
199
|
}, []);
|
|
200
200
|
|
|
201
201
|
// ⌘I / Ctrl+I toggles the panel; Escape closes it. Shift/Alt chords are
|
|
@@ -226,17 +226,17 @@ const AskAI = ({
|
|
|
226
226
|
return () => window.removeEventListener("keydown", onKey);
|
|
227
227
|
}, [open]);
|
|
228
228
|
|
|
229
|
-
// Drive the desktop content push from a body attribute (see
|
|
229
|
+
// Drive the desktop content push from a body attribute (see Assistant.astro CSS).
|
|
230
230
|
useEffect(() => {
|
|
231
231
|
if (open) {
|
|
232
232
|
returnFocusRef.current =
|
|
233
233
|
document.activeElement instanceof HTMLElement
|
|
234
234
|
? document.activeElement
|
|
235
235
|
: null;
|
|
236
|
-
document.body.dataset.
|
|
236
|
+
document.body.dataset.blumeAssistant = "open";
|
|
237
237
|
inputRef.current?.focus();
|
|
238
238
|
} else {
|
|
239
|
-
delete document.body.dataset.
|
|
239
|
+
delete document.body.dataset.blumeAssistant;
|
|
240
240
|
// Return focus to the element that opened the panel (or the trigger when
|
|
241
241
|
// it's gone), so closing doesn't strand keyboard focus in an inert tree.
|
|
242
242
|
// `returnFocusRef` is only set on open, so initial mount is a no-op.
|
|
@@ -249,7 +249,7 @@ const AskAI = ({
|
|
|
249
249
|
}
|
|
250
250
|
}
|
|
251
251
|
return () => {
|
|
252
|
-
delete document.body.dataset.
|
|
252
|
+
delete document.body.dataset.blumeAssistant;
|
|
253
253
|
};
|
|
254
254
|
}, [open]);
|
|
255
255
|
|
|
@@ -261,7 +261,7 @@ const AskAI = ({
|
|
|
261
261
|
// document.body (not portalTarget) so the compiler doesn't flag a state
|
|
262
262
|
// mutation; by the time this runs for a swap, they are the same element.
|
|
263
263
|
if (open && portalTarget) {
|
|
264
|
-
document.body.dataset.
|
|
264
|
+
document.body.dataset.blumeAssistant = "open";
|
|
265
265
|
}
|
|
266
266
|
// oxlint-disable-next-line react/exhaustive-effect-dependencies -- see above
|
|
267
267
|
}, [open, portalTarget]);
|
|
@@ -387,7 +387,7 @@ const AskAI = ({
|
|
|
387
387
|
// The closed panel is only translated off-screen; `inert` drops its
|
|
388
388
|
// buttons/textarea from the tab order and the accessibility tree.
|
|
389
389
|
inert={!open}
|
|
390
|
-
className={`border-border bg-background fixed inset-y-0 end-0 z-[60] flex w-[var(--blume-
|
|
390
|
+
className={`border-border bg-background fixed inset-y-0 end-0 z-[60] flex w-[var(--blume-assistant-width)] flex-col border-s shadow-2xl transition-transform duration-200 ease-out ${
|
|
391
391
|
open ? "translate-x-0" : "translate-x-full rtl:-translate-x-full"
|
|
392
392
|
}`}
|
|
393
393
|
>
|
|
@@ -539,4 +539,4 @@ const AskAI = ({
|
|
|
539
539
|
);
|
|
540
540
|
};
|
|
541
541
|
|
|
542
|
-
export default
|
|
542
|
+
export default Assistant;
|
|
@@ -12,7 +12,7 @@ import { joinBase, stripBase } from "./base-path.ts";
|
|
|
12
12
|
* project data can't come through context. Instead the layout serializes a small
|
|
13
13
|
* snapshot into a `<script type="application/json" id="blume-client-data">` tag,
|
|
14
14
|
* and {@link useBlume}/{@link usePage} read it after mount. {@link useSearch} and
|
|
15
|
-
* {@link
|
|
15
|
+
* {@link useAssistant} wrap the generated search client and the assistant endpoint.
|
|
16
16
|
*
|
|
17
17
|
* Import them from `blume/hooks`:
|
|
18
18
|
*
|
|
@@ -131,14 +131,14 @@ export const useSearch = (): UseSearch => {
|
|
|
131
131
|
return { loading, results, search };
|
|
132
132
|
};
|
|
133
133
|
|
|
134
|
-
/** A single
|
|
134
|
+
/** A single assistant chat message. */
|
|
135
135
|
export interface AskMessage {
|
|
136
136
|
content: string;
|
|
137
137
|
role: "assistant" | "user";
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
-
/** State + actions returned by {@link
|
|
141
|
-
export interface
|
|
140
|
+
/** State + actions returned by {@link useAssistant}. */
|
|
141
|
+
export interface UseAssistant {
|
|
142
142
|
ask: (question: string) => Promise<void>;
|
|
143
143
|
loading: boolean;
|
|
144
144
|
messages: AskMessage[];
|
|
@@ -147,8 +147,8 @@ export interface UseAskAI {
|
|
|
147
147
|
|
|
148
148
|
const DEFAULT_ASK_ENDPOINT = joinBase(import.meta.env.BASE_URL, "api/ask");
|
|
149
149
|
|
|
150
|
-
export interface
|
|
151
|
-
/** Existing
|
|
150
|
+
export interface UseAssistantOptions {
|
|
151
|
+
/** Existing assistant endpoint; defaults to Blume's generated `/api/ask`. */
|
|
152
152
|
endpoint?: string;
|
|
153
153
|
/**
|
|
154
154
|
* Shown as the assistant's answer when the request fails or throws.
|
|
@@ -161,11 +161,11 @@ export interface UseAskAIOptions {
|
|
|
161
161
|
/** Shown as the assistant's answer when the request fails or throws. */
|
|
162
162
|
const ASK_ERROR = "Something went wrong answering that. Please try again.";
|
|
163
163
|
|
|
164
|
-
/** How the generated
|
|
165
|
-
const NOT_CONFIGURED = /^
|
|
164
|
+
/** How the assistant's generated route opens its missing-credential notice. */
|
|
165
|
+
const NOT_CONFIGURED = /^The assistant is not configured/u;
|
|
166
166
|
|
|
167
167
|
/**
|
|
168
|
-
* The route's own explanation of a 503 — the generated route's "
|
|
168
|
+
* The route's own explanation of a 503 — the generated route's "The assistant is not
|
|
169
169
|
* configured: set AI_GATEWAY_API_KEY." — when the response is that notice, or
|
|
170
170
|
* null. It names only the variable to set, never a value, so it's safe to
|
|
171
171
|
* show, and it tells a site owner trying a fresh deploy what's missing instead
|
|
@@ -188,10 +188,12 @@ const currentPath = (): string =>
|
|
|
188
188
|
stripBase(import.meta.env.BASE_URL, window.location.pathname);
|
|
189
189
|
|
|
190
190
|
/**
|
|
191
|
-
* Stream answers from the
|
|
191
|
+
* Stream answers from the assistant endpoint. Mirrors the built-in assistant island so
|
|
192
192
|
* a custom chat UI shares the same grounded, page-aware backend.
|
|
193
193
|
*/
|
|
194
|
-
export const
|
|
194
|
+
export const useAssistant = (
|
|
195
|
+
options: UseAssistantOptions = {}
|
|
196
|
+
): UseAssistant => {
|
|
195
197
|
const endpoint = options.endpoint ?? DEFAULT_ASK_ENDPOINT;
|
|
196
198
|
const errorMessage = options.errorMessage ?? ASK_ERROR;
|
|
197
199
|
const [messages, setMessages] = useState<AskMessage[]>([]);
|
|
@@ -208,7 +210,7 @@ export const useAskAI = (options: UseAskAIOptions = {}): UseAskAI => {
|
|
|
208
210
|
// preserves a stable `ask` identity for consumers that depend on it. With the
|
|
209
211
|
// compiler on it's redundant but inert.
|
|
210
212
|
// oxlint-disable-next-line react-doctor/react-compiler-no-manual-memoization -- see above
|
|
211
|
-
const ask = useCallback<
|
|
213
|
+
const ask = useCallback<UseAssistant["ask"]>(
|
|
212
214
|
async (question) => {
|
|
213
215
|
const trimmed = question.trim();
|
|
214
216
|
if (!trimmed || loading) {
|
|
@@ -39,13 +39,13 @@ interface Props {
|
|
|
39
39
|
*/
|
|
40
40
|
locale?: string;
|
|
41
41
|
/**
|
|
42
|
-
* Whether the search modal offers
|
|
43
|
-
*
|
|
42
|
+
* Whether the search modal offers a hand-off to the assistant. Defaults to whether
|
|
43
|
+
* the assistant is configured, so no page has to pass it; a layout can still opt a
|
|
44
44
|
* shell out explicitly.
|
|
45
45
|
*/
|
|
46
|
-
|
|
47
|
-
/** Localized
|
|
48
|
-
|
|
46
|
+
assistantEnabled?: boolean;
|
|
47
|
+
/** Localized assistant strings for the active locale. */
|
|
48
|
+
assistantStrings?: UIStrings["assistant"];
|
|
49
49
|
// The mobile menu button toggles the docs sidebar drawer; custom pages
|
|
50
50
|
// without a sidebar (e.g. a landing page) pass `false` to hide it.
|
|
51
51
|
hasSidebar?: boolean;
|
|
@@ -91,11 +91,11 @@ const {
|
|
|
91
91
|
route,
|
|
92
92
|
searchEnabled,
|
|
93
93
|
locale,
|
|
94
|
-
// `config.
|
|
94
|
+
// `config.assistant` is null whenever the assistant is off, so the header is the one place
|
|
95
95
|
// that has to know — the Ask trigger below and the search modal's hand-off to
|
|
96
96
|
// it both switch on this, and every page gets both for free.
|
|
97
|
-
|
|
98
|
-
|
|
97
|
+
assistantEnabled = Boolean(data.config.assistant),
|
|
98
|
+
assistantStrings,
|
|
99
99
|
hasSidebar = true,
|
|
100
100
|
hasDrawer = true,
|
|
101
101
|
searchStrings,
|
|
@@ -250,7 +250,7 @@ const clickScript = `(()=>{const dr=()=>{const h=document.querySelector("[data-b
|
|
|
250
250
|
{
|
|
251
251
|
searchEnabled && (
|
|
252
252
|
<SearchSlot
|
|
253
|
-
|
|
253
|
+
assistantEnabled={assistantEnabled}
|
|
254
254
|
locale={searchLocale}
|
|
255
255
|
navigation={navigation}
|
|
256
256
|
popularPages={data.config.search.popular}
|
|
@@ -314,7 +314,7 @@ const clickScript = `(()=>{const dr=()=>{const h=document.querySelector("[data-b
|
|
|
314
314
|
<span class="inline-flex dark:hidden"><Icon name="sun" size={18} /></span>
|
|
315
315
|
<span class="hidden dark:inline-flex"><Icon name="moon" size={18} /></span>
|
|
316
316
|
</button>
|
|
317
|
-
{
|
|
317
|
+
{assistantEnabled && <Ask strings={assistantStrings} />}
|
|
318
318
|
</div>
|
|
319
319
|
</header>
|
|
320
320
|
<script is:inline set:html={clickScript} />
|
|
@@ -78,11 +78,11 @@ interface Props {
|
|
|
78
78
|
fontCssVars?: (string | FontHead)[];
|
|
79
79
|
searchEnabled: boolean;
|
|
80
80
|
/**
|
|
81
|
-
* Opt this page out of the header's
|
|
82
|
-
*
|
|
81
|
+
* Opt this page out of the header's assistant trigger. Defaults to whether the
|
|
82
|
+
* assistant is configured, so a custom page gets the same trigger the docs pages have
|
|
83
83
|
* without wiring anything up.
|
|
84
84
|
*/
|
|
85
|
-
|
|
85
|
+
assistantEnabled?: boolean;
|
|
86
86
|
/**
|
|
87
87
|
* Start the header see-through, with its chrome in white, so it can sit over
|
|
88
88
|
* a dark hero at the top of the page; it returns to the frosted bar as soon
|
|
@@ -163,7 +163,7 @@ const {
|
|
|
163
163
|
themeMode,
|
|
164
164
|
fontCssVars,
|
|
165
165
|
searchEnabled,
|
|
166
|
-
|
|
166
|
+
assistantEnabled,
|
|
167
167
|
transparentHeader,
|
|
168
168
|
siteUrl,
|
|
169
169
|
ogEnabled,
|
|
@@ -389,8 +389,8 @@ createIconSprite(Astro.locals);
|
|
|
389
389
|
<span hidden id={NEW_TAB_HINT_ID}>{navStrings.opensInNewTab}</span>
|
|
390
390
|
<Banner banner={banner} strings={strings.banner} />
|
|
391
391
|
<Header
|
|
392
|
-
|
|
393
|
-
|
|
392
|
+
assistantEnabled={assistantEnabled}
|
|
393
|
+
assistantStrings={strings.assistant}
|
|
394
394
|
hasSidebar={false}
|
|
395
395
|
locale={locale}
|
|
396
396
|
localeSwitch={localeSwitch}
|
|
@@ -26,19 +26,19 @@ const s = { ...EN_UI.page, ...strings };
|
|
|
26
26
|
(prev || next) && (
|
|
27
27
|
<nav
|
|
28
28
|
aria-label={s.pagination}
|
|
29
|
-
class="mx-auto mt-12 flex max-w-content justify-between gap-4 border-border border-t pt-6
|
|
29
|
+
class="mx-auto mt-12 flex max-w-content justify-between gap-4 border-border border-t pt-6"
|
|
30
30
|
>
|
|
31
31
|
{prev ? (
|
|
32
32
|
<a
|
|
33
|
-
class="flex max-w-[48%] flex-1 items-center gap-2 rounded-blume border border-border px-4 py-3 text-foreground transition-colors hover:border-foreground
|
|
33
|
+
class="flex max-w-[48%] flex-1 items-center gap-2 rounded-blume border border-border px-4 py-3 text-foreground transition-colors hover:border-foreground"
|
|
34
34
|
href={withBase(prev.route)}
|
|
35
35
|
>
|
|
36
36
|
<Icon class="rtl:-scale-x-100" name="arrow-left" size={16} />
|
|
37
37
|
<span class="min-w-0">
|
|
38
|
-
<span class="block text-muted-foreground text-xs
|
|
38
|
+
<span class="block text-muted-foreground text-xs">
|
|
39
39
|
{s.previous}
|
|
40
40
|
</span>
|
|
41
|
-
<span class="block
|
|
41
|
+
<span class="block font-medium max-md:line-clamp-2 max-md:wrap-break-word md:truncate">{prev.label}</span>
|
|
42
42
|
</span>
|
|
43
43
|
</a>
|
|
44
44
|
) : (
|
|
@@ -46,14 +46,14 @@ const s = { ...EN_UI.page, ...strings };
|
|
|
46
46
|
)}
|
|
47
47
|
{next && (
|
|
48
48
|
<a
|
|
49
|
-
class="ms-auto flex max-w-[48%] flex-1 items-center justify-end gap-2 rounded-blume border border-border px-4 py-3 text-end text-foreground transition-colors hover:border-foreground
|
|
49
|
+
class="ms-auto flex max-w-[48%] flex-1 items-center justify-end gap-2 rounded-blume border border-border px-4 py-3 text-end text-foreground transition-colors hover:border-foreground"
|
|
50
50
|
href={withBase(next.route)}
|
|
51
51
|
>
|
|
52
52
|
<span class="min-w-0">
|
|
53
|
-
<span class="block text-muted-foreground text-xs
|
|
53
|
+
<span class="block text-muted-foreground text-xs">
|
|
54
54
|
{s.next}
|
|
55
55
|
</span>
|
|
56
|
-
<span class="block
|
|
56
|
+
<span class="block font-medium max-md:line-clamp-2 max-md:wrap-break-word md:truncate">{next.label}</span>
|
|
57
57
|
</span>
|
|
58
58
|
<Icon class="rtl:-scale-x-100" name="arrow-right" size={16} />
|
|
59
59
|
</a>
|
|
@@ -148,7 +148,7 @@ createIconSprite(Astro.locals);
|
|
|
148
148
|
<span hidden id={NEW_TAB_HINT_ID}>{navStrings.opensInNewTab}</span>
|
|
149
149
|
<Banner banner={banner} strings={strings.banner} />
|
|
150
150
|
<Header
|
|
151
|
-
|
|
151
|
+
assistantStrings={strings.assistant}
|
|
152
152
|
hasDrawer={false}
|
|
153
153
|
hasSidebar={false}
|
|
154
154
|
locale={locale}
|
|
@@ -122,10 +122,10 @@ interface Props {
|
|
|
122
122
|
canonical?: string | null;
|
|
123
123
|
editUrl?: string | null;
|
|
124
124
|
/**
|
|
125
|
-
* Opt this page out of the header's
|
|
126
|
-
*
|
|
125
|
+
* Opt this page out of the header's assistant trigger. Defaults to whether the
|
|
126
|
+
* assistant is configured, so pages never wire the trigger up themselves.
|
|
127
127
|
*/
|
|
128
|
-
|
|
128
|
+
assistantEnabled?: boolean;
|
|
129
129
|
/** Show the "Was this page helpful?" rating below the content. */
|
|
130
130
|
feedback?: boolean;
|
|
131
131
|
exportPdf?: boolean;
|
|
@@ -248,7 +248,7 @@ const {
|
|
|
248
248
|
x,
|
|
249
249
|
canonical,
|
|
250
250
|
editUrl,
|
|
251
|
-
|
|
251
|
+
assistantEnabled,
|
|
252
252
|
feedback = false,
|
|
253
253
|
exportPdf,
|
|
254
254
|
exportEpub,
|
|
@@ -582,8 +582,8 @@ createIconSprite(Astro.locals);
|
|
|
582
582
|
<Banner banner={banner} strings={strings.banner} />
|
|
583
583
|
<VersionBanner notice={versionNotice ?? null} />
|
|
584
584
|
<HeaderSlot
|
|
585
|
-
|
|
586
|
-
|
|
585
|
+
assistantEnabled={assistantEnabled}
|
|
586
|
+
assistantStrings={strings.assistant}
|
|
587
587
|
layout={layout}
|
|
588
588
|
locale={locale}
|
|
589
589
|
localeSwitch={localeSwitch}
|
|
@@ -12,7 +12,7 @@ import { flattenPages } from "./nav-utils.ts";
|
|
|
12
12
|
// `SearchFn` it returns. Only the configured provider's SDK is bundled.
|
|
13
13
|
|
|
14
14
|
interface Props {
|
|
15
|
-
|
|
15
|
+
assistantEnabled?: boolean;
|
|
16
16
|
navigation?: Navigation;
|
|
17
17
|
/** Curated empty-state pages; falls back to the first sidebar pages when empty or omitted. */
|
|
18
18
|
popularPages?: { icon?: string; label: string; route: string }[];
|
|
@@ -27,7 +27,7 @@ interface Props {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
const {
|
|
30
|
-
|
|
30
|
+
assistantEnabled = false,
|
|
31
31
|
navigation,
|
|
32
32
|
popularPages,
|
|
33
33
|
strings,
|
|
@@ -60,10 +60,10 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
|
|
|
60
60
|
|
|
61
61
|
<blume-search
|
|
62
62
|
class="contents"
|
|
63
|
-
data-ask={
|
|
63
|
+
data-ask={assistantEnabled ? "" : undefined}
|
|
64
64
|
data-i18n-all={s.all}
|
|
65
|
-
data-i18n-ask={s.
|
|
66
|
-
data-i18n-ask-hint={s.
|
|
65
|
+
data-i18n-ask={s.assistant}
|
|
66
|
+
data-i18n-ask-hint={s.assistantHint}
|
|
67
67
|
data-i18n-dev={s.devOnly}
|
|
68
68
|
data-i18n-empty={s.noResults}
|
|
69
69
|
data-i18n-error={s.error}
|
|
@@ -309,7 +309,7 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
|
|
|
309
309
|
loadFailed = false;
|
|
310
310
|
/** First-open client/index load still in flight. */
|
|
311
311
|
loading = false;
|
|
312
|
-
|
|
312
|
+
assistantEnabled = false;
|
|
313
313
|
popular: PopularPage[] = [];
|
|
314
314
|
selectables: Selectable[] = [];
|
|
315
315
|
selectedIndex = -1;
|
|
@@ -323,7 +323,7 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
|
|
|
323
323
|
devOnlyMsg = "Search is available in the production build.";
|
|
324
324
|
noResultsMsg = "No results found.";
|
|
325
325
|
errorMsg = "Something went wrong. Please try again.";
|
|
326
|
-
askMsg = "
|
|
326
|
+
askMsg = "Assistant";
|
|
327
327
|
askHintMsg = "Get an instant answer from AI";
|
|
328
328
|
allMsg = "All";
|
|
329
329
|
popularMsg = "Popular";
|
|
@@ -351,7 +351,7 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
|
|
|
351
351
|
!event.shiftKey &&
|
|
352
352
|
!event.altKey
|
|
353
353
|
) {
|
|
354
|
-
// ⌘K toggles, mirroring the
|
|
354
|
+
// ⌘K toggles, mirroring the assistant panel's ⌘I: pressing it with
|
|
355
355
|
// the dialog open must close it, not re-showModal an open dialog
|
|
356
356
|
// (an InvalidStateError on older engines).
|
|
357
357
|
event.preventDefault();
|
|
@@ -394,7 +394,7 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
|
|
|
394
394
|
this.results = this.querySelector("[data-blume-search-results]")!;
|
|
395
395
|
this.message = this.querySelector("[data-blume-search-message]")!;
|
|
396
396
|
this.preview = this.querySelector("[data-blume-search-preview]")!;
|
|
397
|
-
this.
|
|
397
|
+
this.assistantEnabled = this.hasAttribute("data-ask");
|
|
398
398
|
|
|
399
399
|
const popularScript = this.querySelector(
|
|
400
400
|
"[data-blume-search-popular]"
|
|
@@ -636,7 +636,7 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
|
|
|
636
636
|
|
|
637
637
|
this.renderFilters(result.sections);
|
|
638
638
|
|
|
639
|
-
if (this.
|
|
639
|
+
if (this.assistantEnabled) {
|
|
640
640
|
const group = this.addGroup(this.askMsg);
|
|
641
641
|
const ask = this.createAskRow(query);
|
|
642
642
|
group.appendChild(ask.el);
|
|
@@ -650,7 +650,7 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
|
|
|
650
650
|
group.appendChild(item.el);
|
|
651
651
|
this.selectables.push(item);
|
|
652
652
|
}
|
|
653
|
-
} else if (!this.
|
|
653
|
+
} else if (!this.assistantEnabled) {
|
|
654
654
|
this.setMessage(this.noResultsMsg);
|
|
655
655
|
}
|
|
656
656
|
|
|
@@ -658,7 +658,7 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
|
|
|
658
658
|
}
|
|
659
659
|
|
|
660
660
|
renderEmpty() {
|
|
661
|
-
if (this.
|
|
661
|
+
if (this.assistantEnabled) {
|
|
662
662
|
const group = this.addGroup(this.askMsg);
|
|
663
663
|
const ask = this.createAskRow("");
|
|
664
664
|
group.appendChild(ask.el);
|
|
@@ -874,7 +874,7 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
|
|
|
874
874
|
const query = this.input.value.trim();
|
|
875
875
|
this.dialog.close();
|
|
876
876
|
window.dispatchEvent(
|
|
877
|
-
new CustomEvent("blume:open-
|
|
877
|
+
new CustomEvent("blume:open-assistant", { detail: { query } })
|
|
878
878
|
);
|
|
879
879
|
} else if (item.url) {
|
|
880
880
|
// Close first so the dialog isn't left open over the transition;
|
|
@@ -96,7 +96,7 @@ const trackGlobals = (
|
|
|
96
96
|
* @param props Properties every provider receives.
|
|
97
97
|
* @param local Properties only the `blume:track` CustomEvent carries — free
|
|
98
98
|
* text a site may bridge to a provider on its own terms, but that must not
|
|
99
|
-
* reach third parties unasked (a reader's
|
|
99
|
+
* reach third parties unasked (a reader's question to the assistant, for instance).
|
|
100
100
|
*/
|
|
101
101
|
export const track = (
|
|
102
102
|
event: string,
|
|
@@ -119,7 +119,7 @@ export const syncDrawerInert = (): void => {
|
|
|
119
119
|
});
|
|
120
120
|
document.addEventListener("astro:after-swap", sync);
|
|
121
121
|
document.addEventListener("keydown", (event) => {
|
|
122
|
-
// An open dialog (search,
|
|
122
|
+
// An open dialog (search, the assistant) closes itself on Escape first.
|
|
123
123
|
if (
|
|
124
124
|
event.key !== "Escape" ||
|
|
125
125
|
!isOpen() ||
|
|
@@ -14,7 +14,7 @@ import { Marked } from "marked";
|
|
|
14
14
|
* `marked` rather than the site's own Markdown pipeline: the pipeline is async, plugin-laden and
|
|
15
15
|
* built for whole documents, while these are thousands of short strings per build — a large
|
|
16
16
|
* reference renders tens of thousands of them. `marked` is synchronous, already a dependency,
|
|
17
|
-
* and already how the
|
|
17
|
+
* and already how the assistant island renders model Markdown.
|
|
18
18
|
*/
|
|
19
19
|
const TABLE = /<table>[\s\S]*?<\/table>/gu;
|
|
20
20
|
|
|
@@ -30,7 +30,7 @@ const escapeHtml = (text: string): string =>
|
|
|
30
30
|
text.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">");
|
|
31
31
|
|
|
32
32
|
const markdown = new Marked({
|
|
33
|
-
// `breaks` is deliberately NOT set, unlike the
|
|
33
|
+
// `breaks` is deliberately NOT set, unlike the assistant island. Docstring prose is hard-wrapped at
|
|
34
34
|
// 72 or 79 columns, so honouring single newlines would break every sentence mid-flow at exactly
|
|
35
35
|
// the width the source file happened to use.
|
|
36
36
|
breaks: false,
|
package/src/core/code-fences.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Fenced-code state for line scanners. Kept free of imports so request-time
|
|
3
|
-
* code (the
|
|
3
|
+
* code (the assistant's grounding excerpts) can share it with the build-time source
|
|
4
4
|
* scanners without pulling the filesystem and schema modules along.
|
|
5
5
|
*
|
|
6
6
|
* CommonMark allows backtick *and* tilde fences, three or more characters
|