blume 2.0.3 → 2.1.0

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.
Files changed (455) hide show
  1. package/AGENTS.md +1 -0
  2. package/CHANGELOG.md +48 -0
  3. package/README.md +1 -1
  4. package/dist/cli/{chunk-1d7ve1dm.js → chunk-0gz36akr.js} +18 -2
  5. package/dist/cli/{chunk-1d7ve1dm.js.map → chunk-0gz36akr.js.map} +3 -3
  6. package/dist/cli/{chunk-2eytanqx.js → chunk-1n228xqb.js} +2 -2
  7. package/dist/cli/{chunk-yfyb25rh.js → chunk-2f3ndkpw.js} +24 -8
  8. package/dist/cli/chunk-2f3ndkpw.js.map +14 -0
  9. package/dist/cli/{chunk-r20tn01b.js → chunk-2f7n4qzt.js} +8 -2
  10. package/dist/cli/{chunk-r20tn01b.js.map → chunk-2f7n4qzt.js.map} +3 -3
  11. package/dist/cli/{chunk-35d4wj9f.js → chunk-2n4ps1b3.js} +7 -7
  12. package/dist/cli/{chunk-j85vccga.js → chunk-43zcf4gz.js} +97 -57
  13. package/dist/cli/{chunk-j85vccga.js.map → chunk-43zcf4gz.js.map} +6 -6
  14. package/dist/cli/{chunk-e04dxsz1.js → chunk-4ejxbzee.js} +1 -1
  15. package/dist/cli/{chunk-5m5nmvyq.js → chunk-4h8z9vv5.js} +233 -39
  16. package/dist/cli/chunk-4h8z9vv5.js.map +16 -0
  17. package/dist/cli/{chunk-5f86nr5m.js → chunk-5cg2rzgf.js} +18 -16
  18. package/dist/cli/{chunk-5f86nr5m.js.map → chunk-5cg2rzgf.js.map} +2 -2
  19. package/dist/cli/{chunk-tzmab476.js → chunk-5ffywsmb.js} +4 -4
  20. package/dist/cli/{chunk-2z928egk.js → chunk-5wj2qwn2.js} +5 -5
  21. package/dist/cli/chunk-6bm0xpgd.js +18 -0
  22. package/dist/cli/chunk-6bm0xpgd.js.map +10 -0
  23. package/dist/cli/{chunk-vg9r4eb9.js → chunk-713mngdq.js} +12 -5
  24. package/dist/cli/{chunk-vg9r4eb9.js.map → chunk-713mngdq.js.map} +3 -3
  25. package/dist/cli/{chunk-5xvm6tfj.js → chunk-9geymqns.js} +17 -15
  26. package/dist/cli/{chunk-5xvm6tfj.js.map → chunk-9geymqns.js.map} +2 -2
  27. package/dist/cli/{chunk-q58y5e6a.js → chunk-9xr4dgee.js} +26 -17
  28. package/dist/cli/chunk-9xr4dgee.js.map +10 -0
  29. package/dist/cli/{chunk-b07cmahc.js → chunk-amrb6hgh.js} +2 -2
  30. package/dist/cli/chunk-cm89scdv.js +443 -0
  31. package/dist/cli/chunk-cm89scdv.js.map +11 -0
  32. package/dist/cli/{chunk-akbpwfxc.js → chunk-cmtcnh9f.js} +2 -2
  33. package/dist/cli/chunk-cy5bm0j0.js +104 -0
  34. package/dist/cli/chunk-cy5bm0j0.js.map +11 -0
  35. package/dist/cli/{chunk-crgn1q09.js → chunk-e7vvk5ya.js} +4 -4
  36. package/dist/cli/{chunk-pehfxfta.js → chunk-g2z9x01v.js} +6 -98
  37. package/dist/cli/chunk-g2z9x01v.js.map +10 -0
  38. package/dist/cli/{chunk-tkacnehg.js → chunk-gp84za0h.js} +2 -2
  39. package/dist/cli/{chunk-wrr3j9w9.js → chunk-h2a48yhd.js} +186 -63
  40. package/dist/cli/chunk-h2a48yhd.js.map +36 -0
  41. package/dist/cli/{chunk-hm3vjy5s.js → chunk-jnwj5kjj.js} +14 -9
  42. package/dist/cli/{chunk-hm3vjy5s.js.map → chunk-jnwj5kjj.js.map} +4 -4
  43. package/dist/cli/chunk-kwrg63tm.js +31 -0
  44. package/dist/cli/chunk-kwrg63tm.js.map +10 -0
  45. package/dist/cli/{chunk-2hsdwb9n.js → chunk-nd1821sk.js} +18 -16
  46. package/dist/cli/{chunk-2hsdwb9n.js.map → chunk-nd1821sk.js.map} +2 -2
  47. package/dist/cli/{chunk-6dsbexzp.js → chunk-ntv4qrt7.js} +17 -15
  48. package/dist/cli/{chunk-6dsbexzp.js.map → chunk-ntv4qrt7.js.map} +2 -2
  49. package/dist/cli/{chunk-r9rcc4w7.js → chunk-p804f3pe.js} +7 -7
  50. package/dist/cli/{chunk-ey84smr6.js → chunk-py62fewh.js} +6 -6
  51. package/dist/cli/{chunk-ey84smr6.js.map → chunk-py62fewh.js.map} +1 -1
  52. package/dist/cli/chunk-q3wxxq39.js +126 -0
  53. package/dist/cli/chunk-q3wxxq39.js.map +11 -0
  54. package/dist/cli/{chunk-9t7a85s3.js → chunk-rekqz4qg.js} +23 -13
  55. package/dist/cli/chunk-rekqz4qg.js.map +10 -0
  56. package/dist/cli/{chunk-8ktnccpt.js → chunk-ta27feev.js} +1 -1
  57. package/dist/cli/{chunk-pv29h0wf.js → chunk-xkz5xbtv.js} +3476 -822
  58. package/dist/cli/chunk-xkz5xbtv.js.map +213 -0
  59. package/dist/cli/{chunk-a58773jm.js → chunk-y4ykbark.js} +2 -2
  60. package/dist/cli/{chunk-p3v96n38.js → chunk-y5h4r2y0.js} +4 -4
  61. package/dist/cli/{chunk-acanzt5p.js → chunk-yj17vs39.js} +6 -6
  62. package/dist/cli/{chunk-p73c0m7w.js → chunk-yxmk4pec.js} +16 -14
  63. package/dist/cli/{chunk-p73c0m7w.js.map → chunk-yxmk4pec.js.map} +2 -2
  64. package/dist/cli/{chunk-3em5wd2y.js → chunk-zccbjcj0.js} +4 -3
  65. package/dist/cli/{chunk-3em5wd2y.js.map → chunk-zccbjcj0.js.map} +2 -2
  66. package/dist/cli/{chunk-c8chx29p.js → chunk-zf748avv.js} +384 -207
  67. package/dist/cli/chunk-zf748avv.js.map +37 -0
  68. package/dist/cli/{chunk-g4hq16wv.js → chunk-zhvd6adm.js} +16 -14
  69. package/dist/cli/{chunk-g4hq16wv.js.map → chunk-zhvd6adm.js.map} +2 -2
  70. package/dist/cli/index.js +26 -20
  71. package/dist/cli/index.js.map +5 -5
  72. package/dist/types/ai/agent-surface.d.ts +8 -7
  73. package/dist/types/ai/ai-catalog.d.ts +7 -0
  74. package/dist/types/ai/ask-context.d.ts +22 -0
  75. package/dist/types/ai/ask.d.ts +271 -58
  76. package/dist/types/ai/index.d.ts +2 -2
  77. package/dist/types/ai/site-skill.d.ts +26 -0
  78. package/dist/types/ai/skills.d.ts +16 -0
  79. package/dist/types/astro/integration.d.ts +8 -0
  80. package/dist/types/captcha/hcaptcha.d.ts +27 -0
  81. package/dist/types/captcha/index.d.ts +28 -0
  82. package/dist/types/captcha/schema.d.ts +29 -0
  83. package/dist/types/captcha/turnstile.d.ts +28 -0
  84. package/dist/types/components/content/api-field.d.ts +59 -0
  85. package/dist/types/components/content/api-page.d.ts +95 -0
  86. package/dist/types/components/layout/nav-utils.d.ts +26 -0
  87. package/dist/types/components/openapi/helpers.d.ts +137 -0
  88. package/dist/types/components/openapi/security.d.ts +85 -0
  89. package/dist/types/consent/ethyca.d.ts +48 -0
  90. package/dist/types/consent/index.d.ts +37 -0
  91. package/dist/types/consent/native.d.ts +28 -0
  92. package/dist/types/consent/osano.d.ts +42 -0
  93. package/dist/types/consent/schema.d.ts +110 -0
  94. package/dist/types/core/config-input.d.ts +187 -20
  95. package/dist/types/core/data.d.ts +49 -4
  96. package/dist/types/core/footer.d.ts +8 -0
  97. package/dist/types/core/i18n-ui.d.ts +86 -0
  98. package/dist/types/core/includes.d.ts +8 -2
  99. package/dist/types/core/localizable.d.ts +7 -0
  100. package/dist/types/core/page-modes.d.ts +29 -0
  101. package/dist/types/core/redirect-patterns.d.ts +144 -0
  102. package/dist/types/core/related.d.ts +40 -0
  103. package/dist/types/core/schema.d.ts +551 -56
  104. package/dist/types/core/sources/read.d.ts +5 -0
  105. package/dist/types/core/types.d.ts +1 -1
  106. package/dist/types/core/variables.d.ts +42 -0
  107. package/dist/types/deploy/node-headers.d.ts +13 -2
  108. package/dist/types/deploy/platforms/cloudflare.d.ts +5 -0
  109. package/dist/types/deploy/platforms/netlify.d.ts +7 -0
  110. package/dist/types/deploy/platforms/vercel.d.ts +6 -0
  111. package/dist/types/deploy/redirects.d.ts +39 -11
  112. package/dist/types/deploy/vercel-negotiation.d.ts +9 -0
  113. package/dist/types/markdown/code-title.d.ts +17 -0
  114. package/dist/types/markdown/features.d.ts +3 -2
  115. package/dist/types/narration/build.d.ts +70 -0
  116. package/dist/types/narration/provider.d.ts +26 -0
  117. package/dist/types/narration/script.d.ts +154 -0
  118. package/dist/types/openapi/model.d.ts +12 -6
  119. package/dist/types/openapi/overlay.d.ts +54 -0
  120. package/dist/types/openapi/parse.d.ts +13 -0
  121. package/dist/types/openapi/references.d.ts +14 -4
  122. package/dist/types/ratelimit/cloudflare.d.ts +39 -0
  123. package/dist/types/ratelimit/index.d.ts +27 -0
  124. package/dist/types/ratelimit/memory.d.ts +36 -0
  125. package/dist/types/ratelimit/schema.d.ts +111 -0
  126. package/dist/types/ratelimit/upstash.d.ts +21 -0
  127. package/dist/types/ratelimit/wrangler.d.ts +23 -0
  128. package/dist/types/reference/asyncapi.d.ts +10 -10
  129. package/dist/types/reference/graphql.d.ts +13 -10
  130. package/dist/types/reference/openapi.d.ts +33 -12
  131. package/dist/types/reference/options.d.ts +45 -5
  132. package/dist/types/reference/scalar.d.ts +13 -0
  133. package/dist/types/reference/schema.d.ts +60 -36
  134. package/dist/types/search/documents.d.ts +12 -1
  135. package/dist/types/search/orama-index.d.ts +4 -0
  136. package/dist/types/seo/metatags.d.ts +20 -0
  137. package/docs/02-deployment.mdx +41 -5
  138. package/docs/03-upgrading.mdx +4 -4
  139. package/docs/04-migrating.mdx +1 -1
  140. package/docs/advanced/changelog.mdx +2 -2
  141. package/docs/advanced/skills.mdx +14 -0
  142. package/docs/cli/audit.mdx +21 -1
  143. package/docs/cli/index.mdx +1 -0
  144. package/docs/configuration/analytics.mdx +3 -1
  145. package/docs/configuration/assistant.mdx +188 -36
  146. package/docs/configuration/consent.mdx +100 -0
  147. package/docs/configuration/customization.mdx +4 -4
  148. package/docs/configuration/index.mdx +63 -5
  149. package/docs/configuration/meta.ts +3 -0
  150. package/docs/configuration/narration.mdx +119 -0
  151. package/docs/configuration/rate-limiting.mdx +69 -0
  152. package/docs/configuration/search.mdx +29 -1
  153. package/docs/configuration/theming.mdx +3 -1
  154. package/docs/content/components.mdx +106 -0
  155. package/docs/content/frontmatter.mdx +76 -0
  156. package/docs/content/i18n.mdx +38 -3
  157. package/docs/content/includes.mdx +16 -0
  158. package/docs/content/meta.mdx +1 -0
  159. package/docs/content/meta.ts +1 -0
  160. package/docs/content/navigation.mdx +28 -6
  161. package/docs/content/sources/contentful.mdx +32 -0
  162. package/docs/content/sources/custom.mdx +39 -0
  163. package/docs/content/sources/github-releases.mdx +31 -0
  164. package/docs/content/sources/index.mdx +91 -0
  165. package/docs/content/sources/meta.ts +16 -0
  166. package/docs/content/sources/notion.mdx +32 -0
  167. package/docs/content/sources/obsidian.mdx +37 -0
  168. package/docs/content/sources/payload.mdx +30 -0
  169. package/docs/content/sources/remote-mdx.mdx +40 -0
  170. package/docs/content/sources/sanity.mdx +29 -0
  171. package/docs/content/sources/strapi.mdx +31 -0
  172. package/docs/content/syntax.mdx +67 -2
  173. package/docs/content/variables.mdx +41 -0
  174. package/docs/discoverability/agent-discovery.mdx +29 -2
  175. package/docs/discoverability/index.mdx +1 -1
  176. package/docs/discoverability/llms-txt.mdx +1 -1
  177. package/docs/discoverability/metadata.mdx +17 -0
  178. package/docs/references/api-pages.mdx +74 -0
  179. package/docs/references/asyncapi.mdx +1 -1
  180. package/docs/references/graphql.mdx +2 -2
  181. package/docs/references/meta.ts +1 -1
  182. package/docs/references/openapi.mdx +102 -3
  183. package/docs/references/scalar.mdx +3 -1
  184. package/package.json +36 -1
  185. package/skills/blume/SKILL.md +3 -0
  186. package/skills/blume-migrate/SKILL.md +5 -5
  187. package/skills/blume-migrate/references/docusaurus.md +3 -3
  188. package/skills/blume-migrate/references/mintlify.md +37 -19
  189. package/skills/blume-migrate/references/nextra.md +2 -2
  190. package/skills/blume-migrate/references/starlight.md +2 -1
  191. package/skills/blume-migrate/scripts/mintlify-codemod.mjs +60 -14
  192. package/skills/blume-write-skill/SKILL.md +82 -0
  193. package/src/ai/agent-readability.ts +2 -1
  194. package/src/ai/agent-surface.ts +9 -7
  195. package/src/ai/ai-catalog.ts +6 -3
  196. package/src/ai/ask-context.ts +73 -20
  197. package/src/ai/ask-data.ts +6 -0
  198. package/src/ai/ask-tools.ts +142 -0
  199. package/src/ai/ask.ts +354 -76
  200. package/src/ai/component-markdown.ts +75 -0
  201. package/src/ai/cors.ts +2 -0
  202. package/src/ai/index.ts +12 -0
  203. package/src/ai/llms.ts +6 -2
  204. package/src/ai/markdown.ts +2 -1
  205. package/src/ai/mcp/data.ts +6 -0
  206. package/src/ai/openapi-components.ts +9 -3
  207. package/src/ai/site-skill.ts +274 -0
  208. package/src/ai/skills.ts +35 -2
  209. package/src/ai/write-skill.ts +97 -0
  210. package/src/astro/generate.ts +132 -18
  211. package/src/astro/index.ts +1 -0
  212. package/src/astro/integration.ts +35 -0
  213. package/src/astro/module-types.ts +6 -0
  214. package/src/astro/templates.ts +283 -30
  215. package/src/astro/variables.ts +31 -0
  216. package/src/audit/catalog.ts +19 -0
  217. package/src/audit/checks/contrast.ts +183 -0
  218. package/src/audit/checks/i18n.ts +2 -3
  219. package/src/audit/checks/indexability.ts +2 -3
  220. package/src/audit/checks/links.ts +5 -12
  221. package/src/audit/checks/sitemap.ts +2 -3
  222. package/src/audit/redirects.ts +46 -15
  223. package/src/audit/run.ts +2 -0
  224. package/src/audit/types.ts +2 -1
  225. package/src/blume-modules.d.ts +7 -0
  226. package/src/captcha/client.ts +152 -0
  227. package/src/captcha/hcaptcha.ts +36 -0
  228. package/src/captcha/index.ts +28 -0
  229. package/src/captcha/schema.ts +32 -0
  230. package/src/captcha/turnstile.ts +38 -0
  231. package/src/captcha/verify.ts +71 -0
  232. package/src/cli/command-meta.ts +5 -0
  233. package/src/cli/commands/doctor.ts +22 -16
  234. package/src/cli/commands/skill.ts +96 -0
  235. package/src/cli/index.ts +5 -0
  236. package/src/cli/required-secrets.ts +71 -29
  237. package/src/components/BlumePage.astro +2 -0
  238. package/src/components/content/ApiEndpoint.astro +18 -0
  239. package/src/components/content/ApiField.astro +79 -0
  240. package/src/components/content/ApiPlayground.astro +62 -0
  241. package/src/components/content/ApiRail.astro +19 -0
  242. package/src/components/content/Color.astro +1 -0
  243. package/src/components/content/FileTree.astro +1 -0
  244. package/src/components/content/GithubInfo.astro +1 -0
  245. package/src/components/content/Panel.astro +1 -0
  246. package/src/components/content/ParamField.astro +25 -0
  247. package/src/components/content/RequestExample.astro +17 -0
  248. package/src/components/content/ResponseExample.astro +17 -0
  249. package/src/components/content/ResponseField.astro +32 -0
  250. package/src/components/content/Step.astro +1 -0
  251. package/src/components/content/Tile.astro +1 -0
  252. package/src/components/content/Tooltip.astro +1 -0
  253. package/src/components/content/Tree.astro +1 -0
  254. package/src/components/content/TypeTable.astro +4 -1
  255. package/src/components/content/View.astro +21 -0
  256. package/src/components/content/ViewSwitcher.astro +168 -0
  257. package/src/components/content/api-field.ts +100 -0
  258. package/src/components/content/api-page.ts +224 -0
  259. package/src/components/content/view-switcher.ts +71 -0
  260. package/src/components/index.ts +2 -0
  261. package/src/components/islands/Assistant.astro +10 -1
  262. package/src/components/islands/assistant.tsx +104 -14
  263. package/src/components/islands/code-question.ts +62 -0
  264. package/src/components/islands/hooks.ts +110 -21
  265. package/src/components/islands/support-link.ts +68 -0
  266. package/src/components/layout/Analytics.astro +11 -2
  267. package/src/components/layout/Banner.astro +14 -10
  268. package/src/components/layout/Breadcrumbs.astro +6 -2
  269. package/src/components/layout/ConsentBanner.astro +62 -0
  270. package/src/components/layout/ConsentHead.astro +38 -0
  271. package/src/components/layout/Directory.astro +94 -0
  272. package/src/components/layout/DrawerSwitchers.astro +51 -0
  273. package/src/components/layout/Empty.astro +2 -2
  274. package/src/components/layout/Header.astro +122 -80
  275. package/src/components/layout/LanguageSwitcher.astro +48 -15
  276. package/src/components/layout/NarrationPlayer.astro +1085 -0
  277. package/src/components/layout/NavSelector.astro +33 -7
  278. package/src/components/layout/PageFeedback.astro +54 -49
  279. package/src/components/layout/PageLayout.astro +71 -3
  280. package/src/components/layout/ReferenceLayout.astro +17 -6
  281. package/src/components/layout/RelatedPages.astro +42 -0
  282. package/src/components/layout/RootLayout.astro +308 -45
  283. package/src/components/layout/Search.astro +31 -17
  284. package/src/components/layout/SiteFooter.astro +124 -0
  285. package/src/components/layout/SiteMetatags.astro +16 -0
  286. package/src/components/layout/consent/native.ts +79 -0
  287. package/src/components/layout/consent/types.ts +37 -0
  288. package/src/components/layout/footer-links.ts +53 -0
  289. package/src/components/layout/footer-socials.ts +87 -0
  290. package/src/components/layout/locale-redirect.ts +107 -0
  291. package/src/components/layout/nav-utils.ts +83 -0
  292. package/src/components/layout/page-feedback.ts +96 -0
  293. package/src/components/layout/search/analytics.ts +98 -0
  294. package/src/components/layout/search/flexsearch.ts +17 -5
  295. package/src/components/layout/search/orama-cloud.ts +26 -14
  296. package/src/components/layout/search/types.ts +20 -0
  297. package/src/components/layout/search/typesense.ts +5 -1
  298. package/src/components/openapi/Operation.astro +161 -13
  299. package/src/components/openapi/RequestBody.astro +6 -4
  300. package/src/components/openapi/RequestPanel.astro +59 -7
  301. package/src/components/openapi/Responses.astro +6 -4
  302. package/src/components/openapi/async-snippets.ts +4 -4
  303. package/src/components/openapi/code-samples.ts +97 -0
  304. package/src/components/openapi/security.ts +6 -3
  305. package/src/components/openapi/snippets.ts +869 -8
  306. package/src/components/props.ts +6 -0
  307. package/src/consent/client.ts +95 -0
  308. package/src/consent/clients.ts +19 -0
  309. package/src/consent/ethyca.ts +71 -0
  310. package/src/consent/head.ts +24 -0
  311. package/src/consent/index.ts +37 -0
  312. package/src/consent/init.ts +29 -0
  313. package/src/consent/native.ts +36 -0
  314. package/src/consent/osano.ts +59 -0
  315. package/src/consent/schema.ts +30 -0
  316. package/src/core/builtin-tags.ts +5 -0
  317. package/src/core/client-address.ts +19 -0
  318. package/src/core/config-input.ts +194 -20
  319. package/src/core/data.ts +44 -4
  320. package/src/core/diagnostics.ts +3 -2
  321. package/src/core/footer.ts +26 -0
  322. package/src/core/graph.ts +39 -36
  323. package/src/core/i18n-ui.ts +82 -2
  324. package/src/core/includes.ts +20 -3
  325. package/src/core/links.ts +16 -2
  326. package/src/core/localizable.ts +26 -0
  327. package/src/core/navigation.ts +43 -12
  328. package/src/core/page-modes.ts +56 -0
  329. package/src/core/project-graph.ts +111 -5
  330. package/src/core/redirect-patterns.ts +491 -0
  331. package/src/core/related.ts +131 -0
  332. package/src/core/schema.ts +318 -64
  333. package/src/core/sources/normalize.ts +2 -1
  334. package/src/core/sources/read.ts +8 -3
  335. package/src/core/types.ts +1 -1
  336. package/src/core/ui-packs/ar.ts +43 -0
  337. package/src/core/ui-packs/bg.ts +45 -0
  338. package/src/core/ui-packs/bn.ts +43 -0
  339. package/src/core/ui-packs/ca.ts +46 -0
  340. package/src/core/ui-packs/cs.ts +44 -0
  341. package/src/core/ui-packs/da.ts +45 -0
  342. package/src/core/ui-packs/de.ts +46 -0
  343. package/src/core/ui-packs/el.ts +45 -0
  344. package/src/core/ui-packs/es.ts +46 -0
  345. package/src/core/ui-packs/fa.ts +45 -0
  346. package/src/core/ui-packs/fi.ts +45 -0
  347. package/src/core/ui-packs/fr.ts +46 -0
  348. package/src/core/ui-packs/he.ts +43 -0
  349. package/src/core/ui-packs/hi.ts +44 -0
  350. package/src/core/ui-packs/hr.ts +45 -0
  351. package/src/core/ui-packs/hu.ts +44 -0
  352. package/src/core/ui-packs/id.ts +45 -0
  353. package/src/core/ui-packs/it.ts +45 -0
  354. package/src/core/ui-packs/ja.ts +45 -0
  355. package/src/core/ui-packs/ko.ts +44 -0
  356. package/src/core/ui-packs/nl.ts +46 -0
  357. package/src/core/ui-packs/no.ts +44 -0
  358. package/src/core/ui-packs/pl.ts +45 -0
  359. package/src/core/ui-packs/pt-br.ts +46 -0
  360. package/src/core/ui-packs/pt.ts +45 -0
  361. package/src/core/ui-packs/ro.ts +45 -0
  362. package/src/core/ui-packs/ru.ts +45 -0
  363. package/src/core/ui-packs/sk.ts +44 -0
  364. package/src/core/ui-packs/sr.ts +45 -0
  365. package/src/core/ui-packs/sv.ts +44 -0
  366. package/src/core/ui-packs/th.ts +43 -0
  367. package/src/core/ui-packs/tr.ts +45 -0
  368. package/src/core/ui-packs/uk.ts +45 -0
  369. package/src/core/ui-packs/vi.ts +45 -0
  370. package/src/core/ui-packs/zh-tw.ts +43 -0
  371. package/src/core/ui-packs/zh.ts +43 -0
  372. package/src/core/variables.ts +103 -0
  373. package/src/deploy/artifacts.ts +77 -8
  374. package/src/deploy/cloudflare-negotiation.ts +35 -5
  375. package/src/deploy/headers.ts +2 -1
  376. package/src/deploy/node-headers.ts +63 -9
  377. package/src/deploy/platforms/cloudflare.ts +32 -0
  378. package/src/deploy/platforms/netlify.ts +33 -1
  379. package/src/deploy/platforms/vercel.ts +37 -2
  380. package/src/deploy/redirects.ts +91 -24
  381. package/src/deploy/vercel-negotiation.ts +36 -0
  382. package/src/markdown/api-rail.ts +259 -0
  383. package/src/markdown/code-title.ts +35 -9
  384. package/src/markdown/features.ts +3 -2
  385. package/src/markdown/fence-meta.ts +2 -0
  386. package/src/markdown/include.ts +11 -1
  387. package/src/markdown/index.ts +28 -5
  388. package/src/markdown/math.ts +4 -4
  389. package/src/markdown/variables.ts +56 -0
  390. package/src/markdown/views.ts +82 -0
  391. package/src/narration/build.ts +412 -0
  392. package/src/narration/player.ts +249 -0
  393. package/src/narration/provider.ts +29 -0
  394. package/src/narration/script.ts +592 -0
  395. package/src/og/card.ts +7 -6
  396. package/src/openapi/model.ts +57 -24
  397. package/src/openapi/overlay.ts +256 -0
  398. package/src/openapi/parse.ts +55 -2
  399. package/src/openapi/references.ts +21 -5
  400. package/src/openapi/render-mdx.ts +5 -1
  401. package/src/openapi/scalar.ts +40 -6
  402. package/src/openapi/source.ts +1 -1
  403. package/src/ratelimit/cloudflare.ts +54 -0
  404. package/src/ratelimit/index.ts +30 -0
  405. package/src/ratelimit/memory.ts +45 -0
  406. package/src/ratelimit/runtime.ts +229 -0
  407. package/src/ratelimit/schema.ts +28 -0
  408. package/src/ratelimit/upstash.ts +30 -0
  409. package/src/ratelimit/wrangler.ts +70 -0
  410. package/src/reference/asyncapi.ts +2 -2
  411. package/src/reference/graphql.ts +5 -2
  412. package/src/reference/openapi.ts +21 -7
  413. package/src/reference/options.ts +51 -7
  414. package/src/reference/scalar.ts +33 -8
  415. package/src/registry/eject.ts +20 -4
  416. package/src/registry/registry.ts +7 -0
  417. package/src/search/documents.ts +26 -2
  418. package/src/search/orama-index.ts +28 -3
  419. package/src/search/pagefind-ranking.ts +39 -0
  420. package/src/search/sync/algolia.ts +15 -4
  421. package/src/search/sync/orama-cloud.ts +3 -0
  422. package/src/search/sync/typesense.ts +5 -0
  423. package/src/seo/metatags.ts +67 -0
  424. package/src/theme/chrome-icons.ts +1 -0
  425. package/src/theme/color.ts +500 -0
  426. package/src/theme/entry.ts +56 -3
  427. package/src/theme/palette.ts +121 -45
  428. package/src/translate/prompts.ts +1 -0
  429. package/src/translate/validate.ts +8 -2
  430. package/src/upgrade/upgrade.ts +4 -171
  431. package/dist/cli/chunk-364znk6q.js +0 -75
  432. package/dist/cli/chunk-364znk6q.js.map +0 -11
  433. package/dist/cli/chunk-5m5nmvyq.js.map +0 -13
  434. package/dist/cli/chunk-9t7a85s3.js.map +0 -10
  435. package/dist/cli/chunk-c8chx29p.js.map +0 -36
  436. package/dist/cli/chunk-pehfxfta.js.map +0 -10
  437. package/dist/cli/chunk-pv29h0wf.js.map +0 -190
  438. package/dist/cli/chunk-q58y5e6a.js.map +0 -10
  439. package/dist/cli/chunk-wrr3j9w9.js.map +0 -35
  440. package/dist/cli/chunk-yfyb25rh.js.map +0 -14
  441. package/docs/content/sources.mdx +0 -334
  442. /package/dist/cli/{chunk-2eytanqx.js.map → chunk-1n228xqb.js.map} +0 -0
  443. /package/dist/cli/{chunk-35d4wj9f.js.map → chunk-2n4ps1b3.js.map} +0 -0
  444. /package/dist/cli/{chunk-e04dxsz1.js.map → chunk-4ejxbzee.js.map} +0 -0
  445. /package/dist/cli/{chunk-tzmab476.js.map → chunk-5ffywsmb.js.map} +0 -0
  446. /package/dist/cli/{chunk-2z928egk.js.map → chunk-5wj2qwn2.js.map} +0 -0
  447. /package/dist/cli/{chunk-b07cmahc.js.map → chunk-amrb6hgh.js.map} +0 -0
  448. /package/dist/cli/{chunk-akbpwfxc.js.map → chunk-cmtcnh9f.js.map} +0 -0
  449. /package/dist/cli/{chunk-crgn1q09.js.map → chunk-e7vvk5ya.js.map} +0 -0
  450. /package/dist/cli/{chunk-tkacnehg.js.map → chunk-gp84za0h.js.map} +0 -0
  451. /package/dist/cli/{chunk-r9rcc4w7.js.map → chunk-p804f3pe.js.map} +0 -0
  452. /package/dist/cli/{chunk-8ktnccpt.js.map → chunk-ta27feev.js.map} +0 -0
  453. /package/dist/cli/{chunk-a58773jm.js.map → chunk-y4ykbark.js.map} +0 -0
  454. /package/dist/cli/{chunk-p3v96n38.js.map → chunk-y5h4r2y0.js.map} +0 -0
  455. /package/dist/cli/{chunk-acanzt5p.js.map → chunk-yj17vs39.js.map} +0 -0
@@ -0,0 +1,71 @@
1
+ /**
2
+ * The page view picker's logic (see `ViewSwitcher.astro`). A page written in
3
+ * `<View title>` blocks shows one view at a time. The choice sits on
4
+ * `<body data-blume-view>`, which each page brings fresh, and a per-page style
5
+ * hides everything tagged with another view: the `<View>` blocks, the
6
+ * picker's icons, and the table of contents entries for their headings.
7
+ */
8
+
9
+ /**
10
+ * Where a reader's last-picked view is remembered, across pages. The init
11
+ * script below spells it out, as it does {@link VIEW_PARAM}.
12
+ */
13
+ export const VIEW_STORAGE_KEY = "blume-view";
14
+
15
+ /** The query parameter a shared link picks a view with (`?view=Python`). */
16
+ export const VIEW_PARAM = "view";
17
+
18
+ /**
19
+ * The view to show: the first of the candidates the page has, most specific
20
+ * first (a linked heading's view, the link's `?view=`, the reader's last
21
+ * pick), else the page's first view.
22
+ */
23
+ export const chooseView = (
24
+ titles: readonly string[],
25
+ candidates: readonly (string | null | undefined)[]
26
+ ): string | undefined =>
27
+ candidates.find(
28
+ (candidate): candidate is string =>
29
+ candidate !== null &&
30
+ candidate !== undefined &&
31
+ titles.includes(candidate)
32
+ ) ?? titles[0];
33
+
34
+ /**
35
+ * A CSS string for `value`, safe inside a `<style>` element: quotes and
36
+ * backslashes escaped, `<` as a hex escape so the text can't close the
37
+ * element, and line breaks (which a CSS string can't hold) as spaces.
38
+ */
39
+ const cssString = (value: string): string =>
40
+ `"${value
41
+ .replaceAll(/["\\]/gu, String.raw`\$&`)
42
+ .replaceAll("<", String.raw`\3c `)
43
+ .replaceAll(/[\n\r\f]/gu, " ")}"`;
44
+
45
+ /**
46
+ * The page's view CSS: with a view chosen on `<body>`, everything tagged with
47
+ * another is hidden; before a choice, everything but the first view is.
48
+ */
49
+ export const viewStyle = (titles: readonly string[]): string => {
50
+ const [first] = titles;
51
+ if (first === undefined) {
52
+ return "";
53
+ }
54
+ const other = (title: string): string =>
55
+ `[data-blume-view]:not([data-blume-view=${cssString(title)}])`;
56
+ const selectors = [
57
+ `body:not([data-blume-view]) ${other(first)}`,
58
+ ...titles.map(
59
+ (title) => `body[data-blume-view=${cssString(title)}] ${other(title)}`
60
+ ),
61
+ ];
62
+ return `${selectors.join(",")}{display:none!important}`;
63
+ };
64
+
65
+ /**
66
+ * Runs inline as the picker is parsed, ahead of the views it controls, so a
67
+ * link's `?view=` or the reader's last pick applies before anything paints.
68
+ * Inline scripts run once per session under the client router; after that
69
+ * the picker element applies the choice as each page swaps in.
70
+ */
71
+ export const VIEW_INIT_SCRIPT = `(()=>{const s=document.currentScript;const p=s&&s.closest("blume-view-switcher");if(!p)return;let t=[];try{t=JSON.parse(p.getAttribute("data-views")||"[]")}catch{}let r=null;try{r=localStorage.getItem("blume-view")}catch{}const l=new URLSearchParams(location.search).get("view");const v=[l,r].find((c)=>c&&t.includes(c))||t[0];if(!v)return;document.body.setAttribute("data-blume-view",v);const e=p.querySelector("select");if(e)e.value=v;})();`;
@@ -25,7 +25,9 @@ export type {
25
25
  FrameProps,
26
26
  IconProps,
27
27
  PanelProps,
28
+ ParamFieldProps,
28
29
  PromptProps,
30
+ ResponseFieldProps,
29
31
  StepProps,
30
32
  StepsProps,
31
33
  TabProps,
@@ -1,4 +1,5 @@
1
1
  ---
2
+ import type { CaptchaSettings } from "../../captcha/schema.ts";
2
3
  import type { UIStrings } from "../../core/i18n-ui.ts";
3
4
  import { resolveIcon } from "../../theme/icons.ts";
4
5
  import Assistant from "./assistant.tsx";
@@ -9,12 +10,17 @@ interface Suggestion {
9
10
  }
10
11
 
11
12
  interface Props {
13
+ /** The bot check's public settings (`ai.assistant.captcha`). */
14
+ captcha?: CaptchaSettings;
12
15
  endpoint?: string;
13
16
  strings?: UIStrings["assistant"];
14
17
  suggestions?: Suggestion[];
18
+ /** The Contact support link (`ai.assistant.support`). */
19
+ support?: string;
15
20
  }
16
21
 
17
- const { endpoint, strings, suggestions = [] } = Astro.props;
22
+ const { captcha, endpoint, strings, suggestions = [], support } =
23
+ Astro.props;
18
24
 
19
25
  // Icons resolve to inline SVG here (server-only module), so the client island
20
26
  // gets ready-to-render markup rather than a name it can't resolve. Bare Lucide
@@ -41,6 +47,7 @@ const icons = {
41
47
  clear: bodyFor("trash-2"),
42
48
  close: bodyFor("chevrons-right"),
43
49
  copy: bodyFor("copy"),
50
+ remove: bodyFor("x"),
44
51
  };
45
52
  ---
46
53
 
@@ -50,11 +57,13 @@ const icons = {
50
57
  panel and re-stamps the body push attribute itself on astro:after-swap
51
58
  (see assistant.tsx), since the swap replaces <body> wholesale. */}
52
59
  <Assistant
60
+ captcha={captcha}
53
61
  client:load
54
62
  endpoint={endpoint}
55
63
  icons={icons}
56
64
  strings={strings}
57
65
  suggestions={items}
66
+ support={support}
58
67
  transition:persist="blume-assistant"
59
68
  />
60
69
 
@@ -4,10 +4,14 @@ import { useEffect, useRef, useState } from "react";
4
4
  import type { FormEvent, KeyboardEvent as ReactKeyboardEvent } from "react";
5
5
  import { createPortal } from "react-dom";
6
6
 
7
+ import type { CaptchaSettings } from "../../captcha/schema.ts";
7
8
  import type { UIStrings } from "../../core/i18n-ui.ts";
8
9
  import { copyText } from "../copy-feedback.ts";
9
10
  import { joinBase, mountBase } from "./base-path.ts";
11
+ import { splitCode, withCode } from "./code-question.ts";
12
+ import type { CodeAttachment } from "./code-question.ts";
10
13
  import { useAssistant } from "./hooks.ts";
14
+ import { supportHref } from "./support-link.ts";
11
15
 
12
16
  /** A resolved empty-state prompt; `icon` is ready-to-inline SVG (or null). */
13
17
  interface Suggestion {
@@ -25,6 +29,8 @@ interface AssistantIcons {
25
29
  clear: string;
26
30
  close: string;
27
31
  copy: string;
32
+ /** The attached code chip's dismiss glyph. */
33
+ remove?: string;
28
34
  }
29
35
 
30
36
  // Empty bodies so the island still renders (iconless) if instantiated without
@@ -45,11 +51,18 @@ const DEFAULT_ASK: UIStrings["assistant"] = {
45
51
  copy: "Copy conversation",
46
52
  empty: "Ask a question about the docs.",
47
53
  error: "Sorry, something went wrong.",
54
+ explainCode: "Explain this code.",
48
55
  label: "Ask a question",
56
+ open: "Open assistant",
49
57
  placeholder: "Ask a question…",
58
+ rateLimited: "You've asked a lot of questions. Try again in a few minutes.",
59
+ removeCode: "Remove code",
50
60
  send: "Send",
61
+ support: "Contact support",
62
+ supportSubject: "Question from the docs",
51
63
  tip: "Tip: You can open and close chat with",
52
64
  title: "Assistant",
65
+ verifyFailed: "We couldn't check that you're human. Try again.",
53
66
  you: "You",
54
67
  };
55
68
 
@@ -140,22 +153,54 @@ const ICON_BUTTON_CLASS =
140
153
  const ANSWER_CLASS =
141
154
  "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!";
142
155
 
156
+ /** A reader's message bubble. */
157
+ const USER_MESSAGE_CLASS =
158
+ "rounded-blume bg-muted text-foreground max-w-[85%] self-end px-3 py-2 text-sm whitespace-pre-wrap";
159
+
160
+ /** What `blume:open-assistant` may carry. */
161
+ interface OpenAssistantDetail {
162
+ code?: CodeAttachment;
163
+ query?: string;
164
+ }
165
+
166
+ /** A reader's message, with any code they asked about shown as a block. */
167
+ const UserMessage = ({ content }: { content: string }) => {
168
+ const { code, text } = splitCode(content);
169
+ if (!code) {
170
+ return <div className={USER_MESSAGE_CLASS}>{text}</div>;
171
+ }
172
+ return (
173
+ <div className={USER_MESSAGE_CLASS}>
174
+ {text}
175
+ <pre className="bg-background mt-2 max-h-40 overflow-auto rounded-md p-2 font-mono text-xs whitespace-pre">
176
+ {code.source}
177
+ </pre>
178
+ </div>
179
+ );
180
+ };
181
+
143
182
  const Assistant = ({
183
+ captcha,
144
184
  endpoint = DEFAULT_ASK_ENDPOINT,
145
185
  icons = EMPTY_ICONS,
146
186
  strings,
147
187
  suggestions = EMPTY_SUGGESTIONS,
188
+ support,
148
189
  }: {
190
+ captcha?: CaptchaSettings;
149
191
  endpoint?: string;
150
192
  icons?: AssistantIcons;
151
193
  strings?: UIStrings["assistant"];
152
194
  suggestions?: Suggestion[];
195
+ support?: string;
153
196
  }) => {
154
197
  // Merge per key (not `strings ?? …`) so a dictionary from a stale snapshot
155
198
  // that predates newer keys still resolves every label to its English default.
156
199
  const t = { ...DEFAULT_ASK, ...strings };
157
200
  const [open, setOpen] = useState(false);
158
201
  const [input, setInput] = useState("");
202
+ // A code block the reader is asking about, sent with the next question.
203
+ const [code, setCode] = useState<CodeAttachment | null>(null);
159
204
  // The streaming client — request shaping, optimistic assistant bubble,
160
205
  // stale-stream/abort guards, error-body handling — is the public useAssistant
161
206
  // hook, so the built-in panel and custom UIs share one implementation.
@@ -164,7 +209,14 @@ const Assistant = ({
164
209
  loading: busy,
165
210
  messages,
166
211
  reset,
167
- } = useAssistant({ endpoint, errorMessage: t.error });
212
+ thread,
213
+ } = useAssistant({
214
+ captcha,
215
+ endpoint,
216
+ errorMessage: t.error,
217
+ rateLimitMessage: t.rateLimited,
218
+ verifyMessage: t.verifyFailed,
219
+ });
168
220
  const inputRef = useRef<HTMLTextAreaElement>(null);
169
221
  const scrollRef = useRef<HTMLDivElement>(null);
170
222
  const triggerRef = useRef<HTMLButtonElement>(null);
@@ -193,14 +245,18 @@ const Assistant = ({
193
245
  return () => document.removeEventListener("astro:after-swap", onSwap);
194
246
  }, []);
195
247
 
196
- // The search modal forwards its query so "Assistant: <query>" carries straight in.
248
+ // The search modal forwards its query so "Assistant: <query>" carries
249
+ // straight in, and a code block's Ask button attaches its code.
197
250
  useEffect(() => {
198
251
  const handler = (event: Event) => {
199
252
  // SAFETY: `blume:open-assistant` is only ever dispatched as a CustomEvent
200
- // whose optional detail carries the search query.
201
- const query = (event as CustomEvent<{ query?: string }>).detail?.query;
202
- if (query) {
203
- setInput(query);
253
+ // whose optional detail carries the search query or a code block.
254
+ const { detail } = event as CustomEvent<OpenAssistantDetail | null>;
255
+ if (detail?.query) {
256
+ setInput(detail.query);
257
+ }
258
+ if (detail?.code) {
259
+ setCode(detail.code);
204
260
  }
205
261
  setOpen(true);
206
262
  };
@@ -366,12 +422,14 @@ const Assistant = ({
366
422
  }, [messages, portalTarget]);
367
423
 
368
424
  const runQuestion = (raw: string) => {
369
- const question = raw.trim();
425
+ // A question about attached code may leave the words to us.
426
+ const question = raw.trim() || (code ? t.explainCode : "");
370
427
  if (!question || busy) {
371
428
  return;
372
429
  }
373
- void ask(question);
430
+ void ask(code ? withCode(question, code) : question);
374
431
  setInput("");
432
+ setCode(null);
375
433
  };
376
434
 
377
435
  const clearConversation = () => {
@@ -464,13 +522,11 @@ const Assistant = ({
464
522
  last entry while streaming, or clears wholesale on reset. */}
465
523
  {messages.map((message, index) =>
466
524
  message.role === "user" ? (
467
- <div
468
- className="rounded-blume bg-muted text-foreground max-w-[85%] self-end px-3 py-2 text-sm whitespace-pre-wrap"
525
+ <UserMessage
526
+ content={message.content}
469
527
  // oxlint-disable-next-line react/no-array-index-key -- append-only list, see above
470
528
  key={index}
471
- >
472
- {message.content}
473
- </div>
529
+ />
474
530
  ) : (
475
531
  // oxlint-disable-next-line react/no-array-index-key -- append-only list, see above
476
532
  <div className={ANSWER_CLASS} key={index}>
@@ -491,6 +547,23 @@ const Assistant = ({
491
547
  </div>
492
548
  )
493
549
  )}
550
+ {support && !busy && (
551
+ <a
552
+ className="text-muted-foreground hover:text-foreground self-start text-xs underline underline-offset-2 transition-colors"
553
+ href={supportHref(support, {
554
+ ai: t.ai,
555
+ messages,
556
+ subject: t.supportSubject,
557
+ thread,
558
+ you: t.you,
559
+ })}
560
+ {...(support.startsWith("mailto:") || support.startsWith("/")
561
+ ? {}
562
+ : { rel: "noopener noreferrer", target: "_blank" })}
563
+ >
564
+ {t.support}
565
+ </a>
566
+ )}
494
567
  </div>
495
568
  ) : (
496
569
  <div className="mt-auto flex flex-col gap-0.5 p-4">
@@ -531,6 +604,21 @@ const Assistant = ({
531
604
  className="border-border relative shrink-0 border-t"
532
605
  onSubmit={onSubmit}
533
606
  >
607
+ {code && (
608
+ <div className="flex items-center gap-1.5 px-4 pt-3">
609
+ <span className="rounded-blume bg-muted text-muted-foreground max-w-full truncate px-2 py-1 font-mono text-xs">
610
+ {code.title || code.language || t.explainCode}
611
+ </span>
612
+ <button
613
+ aria-label={t.removeCode}
614
+ className={ICON_BUTTON_CLASS}
615
+ onClick={() => setCode(null)}
616
+ type="button"
617
+ >
618
+ <Glyph path={icons.remove ?? ""} size={14} />
619
+ </button>
620
+ </div>
621
+ )}
534
622
  <textarea
535
623
  aria-label={t.label}
536
624
  className="text-foreground placeholder:text-muted-foreground max-h-48 min-h-[5rem] w-full resize-none bg-transparent px-4 py-3.5 pe-14 text-sm outline-none pointer-coarse:text-base"
@@ -544,7 +632,7 @@ const Assistant = ({
544
632
  <button
545
633
  aria-label={t.send}
546
634
  className="rounded-blume bg-foreground text-background absolute end-3 bottom-3 inline-flex h-8 w-8 cursor-pointer items-center justify-center transition-opacity disabled:cursor-not-allowed disabled:opacity-40"
547
- disabled={busy || input.trim().length === 0}
635
+ disabled={busy || (input.trim().length === 0 && !code)}
548
636
  type="submit"
549
637
  >
550
638
  <Glyph path={icons.arrowUp} />
@@ -559,6 +647,8 @@ const Assistant = ({
559
647
  aria-expanded={open}
560
648
  aria-label={t.title}
561
649
  className={TRIGGER_CLASS}
650
+ // Named by the header's tooltip, like the buttons beside it.
651
+ data-blume-tooltip={t.open}
562
652
  onClick={() => setOpen((value) => !value)}
563
653
  ref={triggerRef}
564
654
  type="button"
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Questions about a code block. A code block's Ask button opens the assistant
3
+ * with the block attached (`blume:open-assistant` with `detail.code`); the
4
+ * question goes out with the code folded in as a fenced block after the
5
+ * reader's words, which is how the model sees it, and the conversation shows
6
+ * the two apart again.
7
+ */
8
+
9
+ /** A code block the reader asked about. */
10
+ export interface CodeAttachment {
11
+ /** The fence language (`ts`), or empty when the block has none. */
12
+ language: string;
13
+ source: string;
14
+ /** The block's title (`server.ts`), when it has one. */
15
+ title?: string;
16
+ }
17
+
18
+ /** The most code one question carries, well inside the route's budget. */
19
+ export const MAX_ATTACHED_CODE = 6000;
20
+
21
+ /** A user message, split into the reader's words and any attached code. */
22
+ export interface SplitQuestion {
23
+ code?: { language: string; source: string };
24
+ text: string;
25
+ }
26
+
27
+ /** A fence longer than any backtick run in the code, so the code can't close it. */
28
+ const fenceFor = (source: string): string => {
29
+ const longest = Math.max(
30
+ 2,
31
+ ...[...source.matchAll(/`+/gu)].map(([run]) => run.length)
32
+ );
33
+ return "`".repeat(longest + 1);
34
+ };
35
+
36
+ /** The question as sent: the reader's words, then the code as a fenced block. */
37
+ export const withCode = (question: string, code: CodeAttachment): string => {
38
+ const source =
39
+ code.source.length > MAX_ATTACHED_CODE
40
+ ? `${code.source.slice(0, MAX_ATTACHED_CODE)}\n…`
41
+ : code.source.replace(/\n+$/u, "");
42
+ const fence = fenceFor(source);
43
+ return `${question}\n\n${fence}${code.language}\n${source}\n${fence}`;
44
+ };
45
+
46
+ const TRAILING_FENCE =
47
+ /\n\n(?<fence>`{3,})(?<language>[^\n`]*)\n(?<source>[\s\S]*)\n\k<fence>$/u;
48
+
49
+ /** A user message split back into the words and the attached code. */
50
+ export const splitCode = (content: string): SplitQuestion => {
51
+ const match = TRAILING_FENCE.exec(content);
52
+ if (!match?.groups) {
53
+ return { text: content };
54
+ }
55
+ return {
56
+ code: {
57
+ language: match.groups.language ?? "",
58
+ source: match.groups.source ?? "",
59
+ },
60
+ text: content.slice(0, match.index),
61
+ };
62
+ };
@@ -1,9 +1,12 @@
1
1
  import { useCallback, useEffect, useRef, useState } from "react";
2
2
 
3
+ import { captchaToken } from "../../captcha/client.ts";
4
+ import type { CaptchaSettings } from "../../captcha/schema.ts";
3
5
  import type { BlumeClientData } from "../../core/data.ts";
4
6
  import { track } from "../layout/analytics-client.ts";
5
7
  import type { SearchFn, SearchResult } from "../layout/search/types.ts";
6
8
  import { joinBase, stripBase } from "./base-path.ts";
9
+ import { newThreadId } from "./support-link.ts";
7
10
 
8
11
  /**
9
12
  * React hooks for Blume islands.
@@ -159,11 +162,22 @@ export interface UseAssistant {
159
162
  loading: boolean;
160
163
  messages: AskMessage[];
161
164
  reset: () => void;
165
+ /**
166
+ * The conversation's id, new after every `reset()`. The `ask` analytics
167
+ * events carry it as `thread`, and the support link passes it along.
168
+ */
169
+ thread: string;
162
170
  }
163
171
 
164
172
  const DEFAULT_ASK_ENDPOINT = joinBase(import.meta.env.BASE_URL, "api/ask");
165
173
 
166
174
  export interface UseAssistantOptions {
175
+ /**
176
+ * The site's bot check (`ai.assistant.captcha`), as `useBlume().config
177
+ * .assistant.captcha` holds it: each question gets a fresh token, sent as
178
+ * `captcha` in the request body.
179
+ */
180
+ captcha?: CaptchaSettings | null;
167
181
  /** Existing assistant endpoint; defaults to Blume's generated `/api/ask`. */
168
182
  endpoint?: string;
169
183
  /**
@@ -172,11 +186,38 @@ export interface UseAssistantOptions {
172
186
  * dictionary string.
173
187
  */
174
188
  errorMessage?: string;
189
+ /**
190
+ * Shown as the assistant's answer when the route's rate limit turns the
191
+ * question away (`429`). Defaults to an English notice; the built-in
192
+ * island passes its localized dictionary string.
193
+ */
194
+ rateLimitMessage?: string;
195
+ /**
196
+ * Shown as the assistant's answer when the bot check fails, in the browser
197
+ * or at the route (`403`). Defaults to an English notice; the built-in
198
+ * island passes its localized dictionary string.
199
+ */
200
+ verifyMessage?: string;
175
201
  }
176
202
 
177
203
  /** Shown as the assistant's answer when the request fails or throws. */
178
204
  const ASK_ERROR = "Something went wrong answering that. Please try again.";
179
205
 
206
+ /** Shown as the assistant's answer when the rate limit turns a question away. */
207
+ const ASK_RATE_LIMITED =
208
+ "You've asked a lot of questions. Try again in a few minutes.";
209
+
210
+ /** Shown as the assistant's answer when the bot check fails. */
211
+ const ASK_VERIFY_FAILED = "We couldn't check that you're human. Try again.";
212
+
213
+ /** What the assistant sends the route. */
214
+ interface AskPayload {
215
+ /** The bot check's token, when the site has one. */
216
+ captcha?: string;
217
+ messages: AskMessage[];
218
+ page: { path: string };
219
+ }
220
+
180
221
  /** How the assistant's generated route opens its missing-credential notice. */
181
222
  const NOT_CONFIGURED = /^The assistant is not configured/u;
182
223
 
@@ -199,6 +240,26 @@ const unavailableNotice = async (
199
240
  return NOT_CONFIGURED.test(text) && text.length <= 200 ? text : null;
200
241
  };
201
242
 
243
+ /** The notices the route's refusals map to. */
244
+ interface RefusalNotices {
245
+ rateLimitMessage: string;
246
+ verifyMessage: string;
247
+ }
248
+
249
+ /**
250
+ * The reader-facing notice for the route turning a question away: the rate
251
+ * limit (`429`) or the bot check (`403`), or `null` for any other status.
252
+ */
253
+ const refusalNotice = (
254
+ status: number,
255
+ notices: RefusalNotices
256
+ ): string | null => {
257
+ if (status === 429) {
258
+ return notices.rateLimitMessage;
259
+ }
260
+ return status === 403 ? notices.verifyMessage : null;
261
+ };
262
+
202
263
  /** The current route with the deployment base stripped, for page grounding. */
203
264
  const currentPath = (): string =>
204
265
  stripBase(import.meta.env.BASE_URL, window.location.pathname);
@@ -212,8 +273,12 @@ export const useAssistant = (
212
273
  ): UseAssistant => {
213
274
  const endpoint = options.endpoint ?? DEFAULT_ASK_ENDPOINT;
214
275
  const errorMessage = options.errorMessage ?? ASK_ERROR;
276
+ const rateLimitMessage = options.rateLimitMessage ?? ASK_RATE_LIMITED;
277
+ const verifyMessage = options.verifyMessage ?? ASK_VERIFY_FAILED;
278
+ const { captcha } = options;
215
279
  const [messages, setMessages] = useState<AskMessage[]>([]);
216
280
  const [loading, setLoading] = useState(false);
281
+ const [thread, setThread] = useState(newThreadId);
217
282
  // The stream writes into the conversation via state updates, so `reset()`
218
283
  // mid-answer must revoke the in-flight request's right to write — otherwise
219
284
  // its next chunk re-appends the assistant bubble onto the emptied list, and
@@ -255,7 +320,7 @@ export const useAssistant = (
255
320
  ) =>
256
321
  track(
257
322
  event,
258
- { ...props, path: pathname, questionChars: trimmed.length },
323
+ { ...props, path: pathname, questionChars: trimmed.length, thread },
259
324
  { question: trimmed }
260
325
  );
261
326
  report("ask", {});
@@ -280,12 +345,32 @@ export const useAssistant = (
280
345
  const assistant: AskMessage = { content: "", role: "assistant" };
281
346
  setMessages([...history, assistant]);
282
347
  setLoading(true);
348
+ // Answer with a notice instead: an error, or the route turning the
349
+ // question away. Silent once a reset has revoked the question.
350
+ const fail = (notice: string) => {
351
+ if (live()) {
352
+ outcome("ask_error", { status });
353
+ assistant.content = notice;
354
+ setMessages([...history, { ...assistant }]);
355
+ }
356
+ };
283
357
  try {
358
+ // A fresh bot-check token per question, when the site has a check;
359
+ // `null` when the check didn't pass in the browser.
360
+ const token = captcha
361
+ ? await captchaToken(captcha).catch(() => null)
362
+ : undefined;
363
+ if (token === null) {
364
+ fail(verifyMessage);
365
+ return;
366
+ }
367
+ const payload: AskPayload = {
368
+ captcha: token,
369
+ messages: history,
370
+ page: { path },
371
+ };
284
372
  const response = await fetch(endpoint, {
285
- body: JSON.stringify({
286
- messages: history,
287
- page: { path },
288
- }),
373
+ body: JSON.stringify(payload),
289
374
  headers: { "content-type": "application/json" },
290
375
  method: "POST",
291
376
  signal: controller.signal,
@@ -294,12 +379,12 @@ export const useAssistant = (
294
379
  if (!response.ok) {
295
380
  // An error body (JSON, HTML error page) must not stream in as the
296
381
  // assistant's answer — only the route's own not-configured notice.
297
- const notice = await unavailableNotice(response).catch(() => null);
298
- if (live()) {
299
- outcome("ask_error", { status });
300
- assistant.content = notice ?? errorMessage;
301
- setMessages([...history, { ...assistant }]);
302
- }
382
+ const notice =
383
+ refusalNotice(status, {
384
+ rateLimitMessage,
385
+ verifyMessage,
386
+ }) ?? (await unavailableNotice(response).catch(() => null));
387
+ fail(notice ?? errorMessage);
303
388
  return;
304
389
  }
305
390
  const reader = response.body?.getReader();
@@ -332,20 +417,14 @@ export const useAssistant = (
332
417
  if (assistant.content) {
333
418
  outcome("ask_answer", { chars: assistant.content.length });
334
419
  } else {
335
- outcome("ask_error", { status });
336
- assistant.content = errorMessage;
337
- setMessages([...history, { ...assistant }]);
420
+ fail(errorMessage);
338
421
  }
339
422
  }
340
423
  } catch {
341
424
  // A thrown fetch (offline, DNS failure, CORS) must not strand the
342
425
  // pre-appended empty assistant message as a stuck placeholder. A
343
426
  // reset's abort lands here too — the guard keeps it silent.
344
- if (live()) {
345
- outcome("ask_error", { status });
346
- assistant.content = errorMessage;
347
- setMessages([...history, { ...assistant }]);
348
- }
427
+ fail(errorMessage);
349
428
  // oxlint-disable-next-line react/todo -- React Compiler cannot lower try/finally; the hook stays manually memoized above
350
429
  } finally {
351
430
  if (live()) {
@@ -353,7 +432,16 @@ export const useAssistant = (
353
432
  }
354
433
  }
355
434
  },
356
- [endpoint, errorMessage, loading, messages]
435
+ [
436
+ captcha,
437
+ endpoint,
438
+ errorMessage,
439
+ loading,
440
+ messages,
441
+ rateLimitMessage,
442
+ thread,
443
+ verifyMessage,
444
+ ]
357
445
  );
358
446
 
359
447
  // Retained for the compiler-off opt-out path (`react: { compiler: false }`):
@@ -364,9 +452,10 @@ export const useAssistant = (
364
452
  abortRef.current?.abort();
365
453
  abortRef.current = null;
366
454
  setMessages([]);
455
+ setThread(newThreadId());
367
456
  // The in-flight `ask`'s finally is now stale and won't clear this.
368
457
  setLoading(false);
369
458
  }, []);
370
459
 
371
- return { ask, loading, messages, reset };
460
+ return { ask, loading, messages, reset, thread };
372
461
  };