blume 2.0.3 → 2.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.
Files changed (607) hide show
  1. package/AGENTS.md +1 -0
  2. package/CHANGELOG.md +142 -0
  3. package/README.md +1 -1
  4. package/dist/cli/{chunk-5f86nr5m.js → chunk-073rdvgb.js} +21 -48
  5. package/dist/cli/{chunk-5f86nr5m.js.map → chunk-073rdvgb.js.map} +4 -5
  6. package/dist/cli/{chunk-ga0pf4aj.js → chunk-0n9xkp3d.js} +12 -3
  7. package/dist/cli/{chunk-ga0pf4aj.js.map → chunk-0n9xkp3d.js.map} +3 -3
  8. package/dist/cli/chunk-150jk7km.js +52 -0
  9. package/dist/cli/chunk-150jk7km.js.map +10 -0
  10. package/dist/cli/{chunk-9t7a85s3.js → chunk-313vnn40.js} +30 -19
  11. package/dist/cli/chunk-313vnn40.js.map +10 -0
  12. package/dist/cli/{chunk-tkacnehg.js → chunk-3v3ffbrj.js} +2 -2
  13. package/dist/cli/{chunk-q58y5e6a.js → chunk-3x6x3pph.js} +46 -24
  14. package/dist/cli/chunk-3x6x3pph.js.map +10 -0
  15. package/dist/cli/{chunk-1d7ve1dm.js → chunk-42t4jxdx.js} +37 -5
  16. package/dist/cli/chunk-42t4jxdx.js.map +12 -0
  17. package/dist/cli/{chunk-8ktnccpt.js → chunk-54ztw1nj.js} +21 -2
  18. package/dist/cli/chunk-54ztw1nj.js.map +10 -0
  19. package/dist/cli/{chunk-hm3vjy5s.js → chunk-5vjtgs6e.js} +28 -10
  20. package/dist/cli/{chunk-hm3vjy5s.js.map → chunk-5vjtgs6e.js.map} +4 -4
  21. package/dist/cli/chunk-66w9j7p6.js +322 -0
  22. package/dist/cli/chunk-66w9j7p6.js.map +14 -0
  23. package/dist/cli/{chunk-2hsdwb9n.js → chunk-7ppf1rva.js} +34 -18
  24. package/dist/cli/chunk-7ppf1rva.js.map +10 -0
  25. package/dist/cli/{chunk-r9rcc4w7.js → chunk-8y2kmjv8.js} +7 -7
  26. package/dist/cli/{chunk-r20tn01b.js → chunk-c1y538kd.js} +8 -2
  27. package/dist/cli/{chunk-r20tn01b.js.map → chunk-c1y538kd.js.map} +3 -3
  28. package/dist/cli/{chunk-2eytanqx.js → chunk-c887fj0s.js} +2 -2
  29. package/dist/cli/{chunk-p3v96n38.js → chunk-cfyg47zg.js} +16 -6
  30. package/dist/cli/chunk-cfyg47zg.js.map +10 -0
  31. package/dist/cli/chunk-cm89scdv.js +443 -0
  32. package/dist/cli/chunk-cm89scdv.js.map +11 -0
  33. package/dist/cli/{chunk-akbpwfxc.js → chunk-dzwehpy7.js} +2 -2
  34. package/dist/cli/{chunk-wrr3j9w9.js → chunk-e6atmn4h.js} +299 -72
  35. package/dist/cli/chunk-e6atmn4h.js.map +36 -0
  36. package/dist/cli/{chunk-tzmab476.js → chunk-e8xw9qkh.js} +8 -6
  37. package/dist/cli/{chunk-tzmab476.js.map → chunk-e8xw9qkh.js.map} +3 -3
  38. package/dist/cli/{chunk-6dsbexzp.js → chunk-e9q7a2ak.js} +17 -15
  39. package/dist/cli/{chunk-6dsbexzp.js.map → chunk-e9q7a2ak.js.map} +2 -2
  40. package/dist/cli/{chunk-pv29h0wf.js → chunk-ehweg4fr.js} +5155 -1249
  41. package/dist/cli/chunk-ehweg4fr.js.map +218 -0
  42. package/dist/cli/{chunk-c8chx29p.js → chunk-ep6nd0rn.js} +390 -318
  43. package/dist/cli/chunk-ep6nd0rn.js.map +36 -0
  44. package/dist/cli/{chunk-p73c0m7w.js → chunk-fdrkhpjy.js} +18 -15
  45. package/dist/cli/{chunk-p73c0m7w.js.map → chunk-fdrkhpjy.js.map} +3 -3
  46. package/dist/cli/{chunk-2z928egk.js → chunk-ft9vfys6.js} +5 -5
  47. package/dist/cli/{chunk-5m5nmvyq.js → chunk-gb420vvz.js} +316 -79
  48. package/dist/cli/chunk-gb420vvz.js.map +17 -0
  49. package/dist/cli/{chunk-vg9r4eb9.js → chunk-hebn0tpx.js} +86 -13
  50. package/dist/cli/chunk-hebn0tpx.js.map +14 -0
  51. package/dist/cli/{chunk-35d4wj9f.js → chunk-htrwgabr.js} +41 -23
  52. package/dist/cli/chunk-htrwgabr.js.map +15 -0
  53. package/dist/cli/{chunk-g4hq16wv.js → chunk-hx3xqnk9.js} +16 -14
  54. package/dist/cli/{chunk-g4hq16wv.js.map → chunk-hx3xqnk9.js.map} +2 -2
  55. package/dist/cli/chunk-j134vrp2.js +65 -0
  56. package/dist/cli/chunk-j134vrp2.js.map +10 -0
  57. package/dist/cli/{chunk-a58773jm.js → chunk-kma9f6sf.js} +2 -2
  58. package/dist/cli/chunk-kwrg63tm.js +31 -0
  59. package/dist/cli/chunk-kwrg63tm.js.map +10 -0
  60. package/dist/cli/{chunk-ey84smr6.js → chunk-mrcccgnh.js} +6 -6
  61. package/dist/cli/{chunk-ey84smr6.js.map → chunk-mrcccgnh.js.map} +1 -1
  62. package/dist/cli/{chunk-yfyb25rh.js → chunk-n0v7m8ry.js} +24 -8
  63. package/dist/cli/chunk-n0v7m8ry.js.map +14 -0
  64. package/dist/cli/{chunk-j85vccga.js → chunk-ncj7w596.js} +576 -367
  65. package/dist/cli/chunk-ncj7w596.js.map +35 -0
  66. package/dist/cli/{chunk-5xvm6tfj.js → chunk-nq1d20w2.js} +17 -15
  67. package/dist/cli/{chunk-5xvm6tfj.js.map → chunk-nq1d20w2.js.map} +2 -2
  68. package/dist/cli/{chunk-pehfxfta.js → chunk-nrns10mj.js} +6 -98
  69. package/dist/cli/chunk-nrns10mj.js.map +10 -0
  70. package/dist/cli/{chunk-crgn1q09.js → chunk-sds923fa.js} +4 -4
  71. package/dist/cli/chunk-t2m5j4pr.js +104 -0
  72. package/dist/cli/chunk-t2m5j4pr.js.map +11 -0
  73. package/dist/cli/{chunk-acanzt5p.js → chunk-tdqhyxfz.js} +6 -6
  74. package/dist/cli/chunk-tphac74f.js +126 -0
  75. package/dist/cli/chunk-tphac74f.js.map +11 -0
  76. package/dist/cli/chunk-w1r4vase.js +18 -0
  77. package/dist/cli/chunk-w1r4vase.js.map +10 -0
  78. package/dist/cli/{chunk-3em5wd2y.js → chunk-zccbjcj0.js} +4 -3
  79. package/dist/cli/{chunk-3em5wd2y.js.map → chunk-zccbjcj0.js.map} +2 -2
  80. package/dist/cli/index.js +31 -20
  81. package/dist/cli/index.js.map +5 -5
  82. package/dist/types/ai/agent-surface.d.ts +8 -7
  83. package/dist/types/ai/ai-catalog.d.ts +7 -0
  84. package/dist/types/ai/ask-context.d.ts +26 -0
  85. package/dist/types/ai/ask.d.ts +271 -58
  86. package/dist/types/ai/component-markdown.d.ts +2 -0
  87. package/dist/types/ai/index.d.ts +2 -2
  88. package/dist/types/ai/openapi-components.d.ts +4 -1
  89. package/dist/types/ai/site-skill.d.ts +26 -0
  90. package/dist/types/ai/skills.d.ts +16 -0
  91. package/dist/types/astro/integration.d.ts +9 -0
  92. package/dist/types/astro/markdown-negotiation.d.ts +3 -0
  93. package/dist/types/astro/pages.d.ts +10 -0
  94. package/dist/types/captcha/hcaptcha.d.ts +27 -0
  95. package/dist/types/captcha/index.d.ts +28 -0
  96. package/dist/types/captcha/schema.d.ts +29 -0
  97. package/dist/types/captcha/turnstile.d.ts +28 -0
  98. package/dist/types/cli/init/scaffold.d.ts +1 -1
  99. package/dist/types/components/content/api-field.d.ts +62 -0
  100. package/dist/types/components/content/api-page.d.ts +100 -0
  101. package/dist/types/components/layout/nav-utils.d.ts +26 -0
  102. package/dist/types/components/openapi/code-samples.d.ts +29 -0
  103. package/dist/types/components/openapi/helpers.d.ts +158 -0
  104. package/dist/types/components/openapi/security.d.ts +95 -0
  105. package/dist/types/components/openapi/snippets.d.ts +60 -0
  106. package/dist/types/consent/ethyca.d.ts +48 -0
  107. package/dist/types/consent/index.d.ts +37 -0
  108. package/dist/types/consent/native.d.ts +28 -0
  109. package/dist/types/consent/osano.d.ts +42 -0
  110. package/dist/types/consent/schema.d.ts +110 -0
  111. package/dist/types/core/changelog-index.d.ts +8 -0
  112. package/dist/types/core/config-input.d.ts +217 -23
  113. package/dist/types/core/data.d.ts +53 -4
  114. package/dist/types/core/footer.d.ts +8 -0
  115. package/dist/types/core/i18n-ui.d.ts +88 -0
  116. package/dist/types/core/includes.d.ts +8 -2
  117. package/dist/types/core/links.d.ts +8 -0
  118. package/dist/types/core/localizable.d.ts +7 -0
  119. package/dist/types/core/page-modes.d.ts +29 -0
  120. package/dist/types/core/probe.d.ts +2 -0
  121. package/dist/types/core/project-graph.d.ts +6 -0
  122. package/dist/types/core/redirect-patterns.d.ts +152 -0
  123. package/dist/types/core/related.d.ts +40 -0
  124. package/dist/types/core/schema.d.ts +670 -56
  125. package/dist/types/core/sources/cache.d.ts +3 -0
  126. package/dist/types/core/sources/contentful-rich-text.d.ts +11 -2
  127. package/dist/types/core/sources/contentful.d.ts +17 -1
  128. package/dist/types/core/sources/lexical.d.ts +5 -0
  129. package/dist/types/core/sources/normalize.d.ts +20 -1
  130. package/dist/types/core/sources/read.d.ts +5 -0
  131. package/dist/types/core/sources/remote.d.ts +37 -10
  132. package/dist/types/core/sources/sanity.d.ts +1 -1
  133. package/dist/types/core/types.d.ts +18 -1
  134. package/dist/types/core/variables.d.ts +42 -0
  135. package/dist/types/deploy/adapter-output.d.ts +2 -1
  136. package/dist/types/deploy/adapters/cloudflare.d.ts +7 -5
  137. package/dist/types/deploy/adapters/node.d.ts +58 -23
  138. package/dist/types/deploy/adapters/registry.d.ts +10 -0
  139. package/dist/types/deploy/adapters/types.d.ts +3 -2
  140. package/dist/types/deploy/node-headers.d.ts +13 -2
  141. package/dist/types/deploy/platforms/cloudflare.d.ts +15 -7
  142. package/dist/types/deploy/platforms/netlify.d.ts +7 -0
  143. package/dist/types/deploy/platforms/node.d.ts +3 -1
  144. package/dist/types/deploy/platforms/types.d.ts +18 -7
  145. package/dist/types/deploy/platforms/vercel.d.ts +26 -1
  146. package/dist/types/deploy/redirects.d.ts +55 -11
  147. package/dist/types/deploy/sitemap.d.ts +2 -1
  148. package/dist/types/deploy/vercel-negotiation.d.ts +51 -21
  149. package/dist/types/markdown/code-title.d.ts +17 -0
  150. package/dist/types/markdown/features.d.ts +3 -2
  151. package/dist/types/narration/build.d.ts +70 -0
  152. package/dist/types/narration/provider.d.ts +26 -0
  153. package/dist/types/narration/script.d.ts +154 -0
  154. package/dist/types/openapi/checks.d.ts +27 -0
  155. package/dist/types/openapi/graphql.d.ts +6 -0
  156. package/dist/types/openapi/model.d.ts +27 -10
  157. package/dist/types/openapi/overlay.d.ts +54 -0
  158. package/dist/types/openapi/parse.d.ts +16 -0
  159. package/dist/types/openapi/references.d.ts +30 -4
  160. package/dist/types/ratelimit/cloudflare.d.ts +39 -0
  161. package/dist/types/ratelimit/index.d.ts +27 -0
  162. package/dist/types/ratelimit/memory.d.ts +36 -0
  163. package/dist/types/ratelimit/schema.d.ts +119 -0
  164. package/dist/types/ratelimit/upstash.d.ts +46 -0
  165. package/dist/types/ratelimit/wrangler.d.ts +23 -0
  166. package/dist/types/reference/asyncapi.d.ts +10 -10
  167. package/dist/types/reference/graphql.d.ts +89 -11
  168. package/dist/types/reference/index.d.ts +1 -1
  169. package/dist/types/reference/openapi.d.ts +33 -12
  170. package/dist/types/reference/options.d.ts +82 -6
  171. package/dist/types/reference/scalar.d.ts +13 -0
  172. package/dist/types/reference/schema.d.ts +144 -36
  173. package/dist/types/search/adapters/registry.d.ts +3 -0
  174. package/dist/types/search/adapters/typesense.d.ts +14 -4
  175. package/dist/types/search/build.d.ts +3 -1
  176. package/dist/types/search/documents.d.ts +12 -1
  177. package/dist/types/search/orama-index.d.ts +13 -9
  178. package/dist/types/search/sync/algolia.d.ts +5 -2
  179. package/dist/types/search/sync/index.d.ts +3 -1
  180. package/dist/types/search/sync/typesense.d.ts +11 -7
  181. package/dist/types/seo/metatags.d.ts +20 -0
  182. package/dist/types/sources/contentful.d.ts +16 -0
  183. package/dist/types/sources/github-releases.d.ts +8 -1
  184. package/dist/types/sources/payload.d.ts +2 -0
  185. package/dist/types/sources/registry.d.ts +54 -0
  186. package/dist/types/sources/shared.d.ts +3 -0
  187. package/dist/types/sources/strapi.d.ts +2 -0
  188. package/docs/01-quickstart.mdx +4 -2
  189. package/docs/02-deployment.mdx +71 -17
  190. package/docs/03-upgrading.mdx +4 -4
  191. package/docs/04-migrating.mdx +1 -1
  192. package/docs/advanced/changelog.mdx +13 -2
  193. package/docs/advanced/skills.mdx +15 -1
  194. package/docs/cli/audit.mdx +51 -8
  195. package/docs/cli/doctor.mdx +5 -1
  196. package/docs/cli/evals.mdx +4 -4
  197. package/docs/cli/index.mdx +1 -0
  198. package/docs/cli/translate.mdx +1 -0
  199. package/docs/cli/validate.mdx +4 -3
  200. package/docs/configuration/analytics.mdx +3 -1
  201. package/docs/configuration/assistant.mdx +194 -38
  202. package/docs/configuration/consent.mdx +100 -0
  203. package/docs/configuration/customization.mdx +8 -6
  204. package/docs/configuration/export.mdx +5 -3
  205. package/docs/configuration/index.mdx +64 -5
  206. package/docs/configuration/meta.ts +3 -0
  207. package/docs/configuration/narration.mdx +119 -0
  208. package/docs/configuration/rate-limiting.mdx +90 -0
  209. package/docs/configuration/search.mdx +63 -12
  210. package/docs/configuration/theming.mdx +3 -1
  211. package/docs/content/components.mdx +108 -2
  212. package/docs/content/frontmatter.mdx +80 -2
  213. package/docs/content/i18n.mdx +39 -4
  214. package/docs/content/includes.mdx +16 -0
  215. package/docs/content/meta.mdx +1 -0
  216. package/docs/content/meta.ts +1 -0
  217. package/docs/content/navigation.mdx +32 -10
  218. package/docs/content/sources/contentful.mdx +35 -0
  219. package/docs/content/sources/custom.mdx +43 -0
  220. package/docs/content/sources/github-releases.mdx +39 -0
  221. package/docs/content/sources/index.mdx +91 -0
  222. package/docs/content/sources/meta.ts +16 -0
  223. package/docs/content/sources/notion.mdx +32 -0
  224. package/docs/content/sources/obsidian.mdx +37 -0
  225. package/docs/content/sources/payload.mdx +31 -0
  226. package/docs/content/sources/remote-mdx.mdx +44 -0
  227. package/docs/content/sources/sanity.mdx +29 -0
  228. package/docs/content/sources/strapi.mdx +32 -0
  229. package/docs/content/syntax.mdx +69 -4
  230. package/docs/content/variables.mdx +41 -0
  231. package/docs/discoverability/agent-discovery.mdx +29 -2
  232. package/docs/discoverability/index.mdx +2 -2
  233. package/docs/discoverability/json-api.mdx +2 -0
  234. package/docs/discoverability/llms-txt.mdx +2 -2
  235. package/docs/discoverability/markdown.mdx +3 -1
  236. package/docs/discoverability/mcp.mdx +4 -0
  237. package/docs/discoverability/metadata.mdx +17 -0
  238. package/docs/discoverability/sitemap-and-robots.mdx +1 -1
  239. package/docs/references/api-pages.mdx +74 -0
  240. package/docs/references/asyncapi.mdx +4 -2
  241. package/docs/references/graphql.mdx +14 -2
  242. package/docs/references/meta.ts +1 -1
  243. package/docs/references/openapi.mdx +148 -7
  244. package/docs/references/scalar.mdx +3 -1
  245. package/package.json +43 -8
  246. package/skills/blume/SKILL.md +3 -0
  247. package/skills/blume-migrate/SKILL.md +6 -6
  248. package/skills/blume-migrate/references/docusaurus.md +7 -7
  249. package/skills/blume-migrate/references/fumadocs.md +17 -13
  250. package/skills/blume-migrate/references/mintlify.md +37 -19
  251. package/skills/blume-migrate/references/nextra.md +10 -7
  252. package/skills/blume-migrate/references/starlight.md +11 -8
  253. package/skills/blume-migrate/scripts/mintlify-codemod.mjs +60 -14
  254. package/skills/blume-write-skill/SKILL.md +82 -0
  255. package/src/ai/agent-readability.ts +2 -1
  256. package/src/ai/agent-surface.ts +9 -7
  257. package/src/ai/ai-catalog.ts +6 -3
  258. package/src/ai/ask-context.ts +103 -20
  259. package/src/ai/ask-data.ts +6 -0
  260. package/src/ai/ask-limits.ts +13 -0
  261. package/src/ai/ask-tools.ts +142 -0
  262. package/src/ai/ask.ts +354 -76
  263. package/src/ai/changelog-markdown.ts +11 -6
  264. package/src/ai/component-markdown.ts +76 -1
  265. package/src/ai/cors.ts +14 -3
  266. package/src/ai/index.ts +12 -0
  267. package/src/ai/llms.ts +6 -2
  268. package/src/ai/markdown.ts +2 -1
  269. package/src/ai/mcp/data.ts +6 -0
  270. package/src/ai/mcp/query.ts +14 -6
  271. package/src/ai/openapi-components.ts +36 -6
  272. package/src/ai/relative-links.ts +4 -38
  273. package/src/ai/site-skill.ts +274 -0
  274. package/src/ai/skills.ts +35 -2
  275. package/src/ai/write-skill.ts +97 -0
  276. package/src/astro/generate.ts +210 -66
  277. package/src/astro/index.ts +1 -0
  278. package/src/astro/integration.ts +42 -4
  279. package/src/astro/markdown-negotiation.ts +3 -0
  280. package/src/astro/module-types.ts +9 -1
  281. package/src/astro/pages.ts +23 -0
  282. package/src/astro/templates.ts +406 -108
  283. package/src/astro/variables.ts +31 -0
  284. package/src/audit/catalog.ts +38 -3
  285. package/src/audit/checks/contrast.ts +183 -0
  286. package/src/audit/checks/i18n.ts +2 -3
  287. package/src/audit/checks/indexability.ts +2 -3
  288. package/src/audit/checks/links.ts +19 -13
  289. package/src/audit/checks/network.ts +110 -6
  290. package/src/audit/checks/redirects.ts +6 -4
  291. package/src/audit/checks/robots.ts +58 -2
  292. package/src/audit/checks/sitemap.ts +2 -3
  293. package/src/audit/redirects.ts +46 -15
  294. package/src/audit/run.ts +5 -0
  295. package/src/audit/types.ts +4 -1
  296. package/src/audit/url.ts +14 -5
  297. package/src/blume-modules.d.ts +12 -1
  298. package/src/captcha/client.ts +152 -0
  299. package/src/captcha/hcaptcha.ts +36 -0
  300. package/src/captcha/index.ts +28 -0
  301. package/src/captcha/schema.ts +32 -0
  302. package/src/captcha/turnstile.ts +38 -0
  303. package/src/captcha/verify.ts +71 -0
  304. package/src/cli/args.ts +51 -0
  305. package/src/cli/build-failure.ts +23 -0
  306. package/src/cli/command-meta.ts +5 -0
  307. package/src/cli/commands/audit.ts +10 -1
  308. package/src/cli/commands/dev.ts +18 -2
  309. package/src/cli/commands/doctor.ts +67 -28
  310. package/src/cli/commands/eval.ts +36 -4
  311. package/src/cli/commands/init.ts +6 -4
  312. package/src/cli/commands/skill.ts +96 -0
  313. package/src/cli/commands/validate.ts +10 -1
  314. package/src/cli/index.ts +5 -0
  315. package/src/cli/init/scaffold.ts +3 -3
  316. package/src/cli/prepare.ts +6 -3
  317. package/src/cli/required-secrets.ts +72 -30
  318. package/src/components/BlumePage.astro +2 -0
  319. package/src/components/content/ApiEndpoint.astro +18 -0
  320. package/src/components/content/ApiField.astro +79 -0
  321. package/src/components/content/ApiPlayground.astro +66 -0
  322. package/src/components/content/ApiRail.astro +19 -0
  323. package/src/components/content/Color.astro +1 -0
  324. package/src/components/content/FileTree.astro +1 -0
  325. package/src/components/content/GithubInfo.astro +1 -0
  326. package/src/components/content/Panel.astro +1 -0
  327. package/src/components/content/ParamField.astro +25 -0
  328. package/src/components/content/RequestExample.astro +17 -0
  329. package/src/components/content/ResponseExample.astro +17 -0
  330. package/src/components/content/ResponseField.astro +32 -0
  331. package/src/components/content/Step.astro +1 -0
  332. package/src/components/content/Tile.astro +1 -0
  333. package/src/components/content/Tooltip.astro +1 -0
  334. package/src/components/content/Tree.astro +1 -0
  335. package/src/components/content/TypeTable.astro +4 -1
  336. package/src/components/content/View.astro +21 -0
  337. package/src/components/content/ViewSwitcher.astro +168 -0
  338. package/src/components/content/api-field.ts +103 -0
  339. package/src/components/content/api-page.ts +248 -0
  340. package/src/components/content/mermaid-element.ts +15 -1
  341. package/src/components/content/view-switcher.ts +71 -0
  342. package/src/components/index.ts +2 -0
  343. package/src/components/islands/Assistant.astro +10 -1
  344. package/src/components/islands/assistant.tsx +104 -14
  345. package/src/components/islands/code-question.ts +62 -0
  346. package/src/components/islands/hooks.ts +136 -21
  347. package/src/components/islands/support-link.ts +68 -0
  348. package/src/components/layout/Analytics.astro +11 -2
  349. package/src/components/layout/Banner.astro +14 -10
  350. package/src/components/layout/Breadcrumbs.astro +6 -2
  351. package/src/components/layout/ConsentBanner.astro +62 -0
  352. package/src/components/layout/ConsentHead.astro +38 -0
  353. package/src/components/layout/Directory.astro +94 -0
  354. package/src/components/layout/DrawerSwitchers.astro +51 -0
  355. package/src/components/layout/Empty.astro +2 -2
  356. package/src/components/layout/Header.astro +122 -80
  357. package/src/components/layout/LanguageSwitcher.astro +48 -15
  358. package/src/components/layout/NarrationPlayer.astro +1085 -0
  359. package/src/components/layout/NavSelector.astro +33 -7
  360. package/src/components/layout/PageActions.astro +139 -24
  361. package/src/components/layout/PageFeedback.astro +54 -49
  362. package/src/components/layout/PageLayout.astro +71 -3
  363. package/src/components/layout/ReferenceLayout.astro +17 -6
  364. package/src/components/layout/RelatedPages.astro +42 -0
  365. package/src/components/layout/RootLayout.astro +318 -47
  366. package/src/components/layout/Search.astro +47 -19
  367. package/src/components/layout/SiteFooter.astro +124 -0
  368. package/src/components/layout/SiteMetatags.astro +16 -0
  369. package/src/components/layout/TableOfContents.astro +24 -17
  370. package/src/components/layout/consent/native.ts +79 -0
  371. package/src/components/layout/consent/types.ts +37 -0
  372. package/src/components/layout/footer-links.ts +53 -0
  373. package/src/components/layout/footer-socials.ts +87 -0
  374. package/src/components/layout/head-scripts.ts +6 -1
  375. package/src/components/layout/locale-redirect.ts +107 -0
  376. package/src/components/layout/nav-utils.ts +83 -0
  377. package/src/components/layout/page-feedback.ts +96 -0
  378. package/src/components/layout/search/algolia.ts +10 -1
  379. package/src/components/layout/search/analytics.ts +126 -0
  380. package/src/components/layout/search/endpoint.ts +48 -5
  381. package/src/components/layout/search/flexsearch.ts +17 -5
  382. package/src/components/layout/search/orama-cloud.ts +26 -14
  383. package/src/components/layout/search/pagefind.ts +114 -5
  384. package/src/components/layout/search/types.ts +32 -0
  385. package/src/components/layout/search/typesense.ts +19 -3
  386. package/src/components/openapi/AsyncApiOperation.astro +14 -7
  387. package/src/components/openapi/Authorization.astro +10 -0
  388. package/src/components/openapi/GraphqlOperation.astro +5 -0
  389. package/src/components/openapi/Operation.astro +184 -13
  390. package/src/components/openapi/ParametersTable.astro +8 -4
  391. package/src/components/openapi/Playground.astro +7 -1
  392. package/src/components/openapi/RequestBody.astro +24 -5
  393. package/src/components/openapi/RequestPanel.astro +59 -7
  394. package/src/components/openapi/Responses.astro +7 -4
  395. package/src/components/openapi/SchemaTable.astro +15 -3
  396. package/src/components/openapi/async-snippets.ts +65 -9
  397. package/src/components/openapi/async.ts +123 -29
  398. package/src/components/openapi/code-samples.ts +97 -0
  399. package/src/components/openapi/graphql-helpers.ts +35 -12
  400. package/src/components/openapi/helpers.ts +36 -2
  401. package/src/components/openapi/message-model.ts +54 -11
  402. package/src/components/openapi/message.ts +14 -4
  403. package/src/components/openapi/operation-model.ts +16 -7
  404. package/src/components/openapi/playground-client.ts +24 -5
  405. package/src/components/openapi/playground-schema.ts +1 -1
  406. package/src/components/openapi/schema-tree.ts +44 -14
  407. package/src/components/openapi/security.ts +16 -3
  408. package/src/components/openapi/snippets.ts +873 -10
  409. package/src/components/openapi/validate-json.ts +4 -3
  410. package/src/components/props.ts +6 -0
  411. package/src/consent/client.ts +95 -0
  412. package/src/consent/clients.ts +19 -0
  413. package/src/consent/ethyca.ts +71 -0
  414. package/src/consent/head.ts +24 -0
  415. package/src/consent/index.ts +37 -0
  416. package/src/consent/init.ts +29 -0
  417. package/src/consent/native.ts +36 -0
  418. package/src/consent/osano.ts +59 -0
  419. package/src/consent/schema.ts +30 -0
  420. package/src/core/builtin-tags.ts +5 -0
  421. package/src/core/changelog-index.ts +31 -1
  422. package/src/core/client-address.ts +19 -0
  423. package/src/core/component-diagnostics.ts +53 -0
  424. package/src/core/component-overrides.ts +32 -0
  425. package/src/core/config-input.ts +225 -23
  426. package/src/core/data.ts +46 -4
  427. package/src/core/diagnostics.ts +8 -2
  428. package/src/core/footer.ts +26 -0
  429. package/src/core/graph.ts +39 -36
  430. package/src/core/i18n-ui.ts +84 -2
  431. package/src/core/includes.ts +21 -4
  432. package/src/core/links.ts +63 -19
  433. package/src/core/localizable.ts +26 -0
  434. package/src/core/navigation.ts +43 -12
  435. package/src/core/page-modes.ts +56 -0
  436. package/src/core/probe.ts +26 -6
  437. package/src/core/project-graph.ts +163 -16
  438. package/src/core/redirect-patterns.ts +520 -0
  439. package/src/core/related.ts +131 -0
  440. package/src/core/schema.ts +339 -66
  441. package/src/core/sources/cache.ts +6 -0
  442. package/src/core/sources/contentful-rich-text.ts +29 -6
  443. package/src/core/sources/contentful.ts +135 -25
  444. package/src/core/sources/filesystem.ts +26 -4
  445. package/src/core/sources/github-releases.ts +12 -1
  446. package/src/core/sources/lexical.ts +15 -10
  447. package/src/core/sources/mdx-remote.ts +62 -10
  448. package/src/core/sources/normalize.ts +228 -41
  449. package/src/core/sources/notion.ts +7 -1
  450. package/src/core/sources/obsidian.ts +16 -3
  451. package/src/core/sources/payload.ts +38 -15
  452. package/src/core/sources/read.ts +8 -3
  453. package/src/core/sources/remote.ts +72 -15
  454. package/src/core/sources/resolve.ts +11 -1
  455. package/src/core/sources/sanity.ts +43 -25
  456. package/src/core/sources/strapi.ts +2 -2
  457. package/src/core/types.ts +19 -1
  458. package/src/core/ui-packs/ar.ts +44 -0
  459. package/src/core/ui-packs/bg.ts +46 -0
  460. package/src/core/ui-packs/bn.ts +44 -0
  461. package/src/core/ui-packs/ca.ts +47 -0
  462. package/src/core/ui-packs/cs.ts +45 -0
  463. package/src/core/ui-packs/da.ts +46 -0
  464. package/src/core/ui-packs/de.ts +47 -0
  465. package/src/core/ui-packs/el.ts +46 -0
  466. package/src/core/ui-packs/es.ts +47 -0
  467. package/src/core/ui-packs/fa.ts +46 -0
  468. package/src/core/ui-packs/fi.ts +46 -0
  469. package/src/core/ui-packs/fr.ts +47 -0
  470. package/src/core/ui-packs/he.ts +44 -0
  471. package/src/core/ui-packs/hi.ts +45 -0
  472. package/src/core/ui-packs/hr.ts +46 -0
  473. package/src/core/ui-packs/hu.ts +45 -0
  474. package/src/core/ui-packs/id.ts +46 -0
  475. package/src/core/ui-packs/it.ts +46 -0
  476. package/src/core/ui-packs/ja.ts +46 -0
  477. package/src/core/ui-packs/ko.ts +45 -0
  478. package/src/core/ui-packs/nl.ts +47 -0
  479. package/src/core/ui-packs/no.ts +45 -0
  480. package/src/core/ui-packs/pl.ts +46 -0
  481. package/src/core/ui-packs/pt-br.ts +47 -0
  482. package/src/core/ui-packs/pt.ts +46 -0
  483. package/src/core/ui-packs/ro.ts +46 -0
  484. package/src/core/ui-packs/ru.ts +46 -0
  485. package/src/core/ui-packs/sk.ts +45 -0
  486. package/src/core/ui-packs/sr.ts +46 -0
  487. package/src/core/ui-packs/sv.ts +45 -0
  488. package/src/core/ui-packs/th.ts +44 -0
  489. package/src/core/ui-packs/tr.ts +46 -0
  490. package/src/core/ui-packs/uk.ts +46 -0
  491. package/src/core/ui-packs/vi.ts +46 -0
  492. package/src/core/ui-packs/zh-tw.ts +44 -0
  493. package/src/core/ui-packs/zh.ts +44 -0
  494. package/src/core/variables.ts +103 -0
  495. package/src/deploy/adapter-output.ts +7 -2
  496. package/src/deploy/adapters/cloudflare.ts +7 -5
  497. package/src/deploy/adapters/node.ts +47 -9
  498. package/src/deploy/adapters/types.ts +7 -3
  499. package/src/deploy/artifacts.ts +79 -9
  500. package/src/deploy/cloudflare-negotiation.ts +35 -5
  501. package/src/deploy/headers.ts +2 -1
  502. package/src/deploy/node-headers.ts +63 -9
  503. package/src/deploy/platforms/cloudflare.ts +45 -9
  504. package/src/deploy/platforms/netlify.ts +35 -2
  505. package/src/deploy/platforms/node.ts +12 -2
  506. package/src/deploy/platforms/types.ts +21 -7
  507. package/src/deploy/platforms/vercel.ts +149 -17
  508. package/src/deploy/redirects.ts +151 -24
  509. package/src/deploy/rss.ts +3 -1
  510. package/src/deploy/sitemap.ts +39 -5
  511. package/src/deploy/vercel-negotiation.ts +326 -102
  512. package/src/eval/agents.ts +18 -1
  513. package/src/eval/prompts.ts +6 -3
  514. package/src/eval/report.ts +34 -13
  515. package/src/eval/run.ts +5 -0
  516. package/src/markdown/api-rail.ts +259 -0
  517. package/src/markdown/code-title.ts +35 -9
  518. package/src/markdown/features.ts +3 -2
  519. package/src/markdown/fence-meta.ts +2 -0
  520. package/src/markdown/include.ts +11 -1
  521. package/src/markdown/index.ts +28 -5
  522. package/src/markdown/math.ts +4 -4
  523. package/src/markdown/relative-links.ts +3 -2
  524. package/src/markdown/variables.ts +56 -0
  525. package/src/markdown/views.ts +82 -0
  526. package/src/narration/build.ts +412 -0
  527. package/src/narration/player.ts +249 -0
  528. package/src/narration/provider.ts +29 -0
  529. package/src/narration/script.ts +592 -0
  530. package/src/og/card.ts +7 -6
  531. package/src/openapi/checks.ts +273 -0
  532. package/src/openapi/graphql-build.ts +21 -0
  533. package/src/openapi/graphql.ts +11 -0
  534. package/src/openapi/model.ts +72 -27
  535. package/src/openapi/overlay.ts +256 -0
  536. package/src/openapi/parse.ts +182 -4
  537. package/src/openapi/references.ts +64 -8
  538. package/src/openapi/render-mdx.ts +5 -1
  539. package/src/openapi/scalar.ts +40 -6
  540. package/src/openapi/source.ts +40 -15
  541. package/src/ratelimit/cloudflare.ts +54 -0
  542. package/src/ratelimit/index.ts +30 -0
  543. package/src/ratelimit/memory.ts +45 -0
  544. package/src/ratelimit/runtime.ts +247 -0
  545. package/src/ratelimit/schema.ts +28 -0
  546. package/src/ratelimit/upstash.ts +60 -0
  547. package/src/ratelimit/wrangler.ts +70 -0
  548. package/src/reference/asyncapi.ts +2 -2
  549. package/src/reference/graphql.ts +20 -3
  550. package/src/reference/index.ts +1 -0
  551. package/src/reference/openapi.ts +21 -7
  552. package/src/reference/options.ts +80 -8
  553. package/src/reference/scalar.ts +33 -8
  554. package/src/registry/eject.ts +23 -5
  555. package/src/registry/registry.ts +7 -0
  556. package/src/search/adapters/locale-scope.ts +30 -0
  557. package/src/search/adapters/typesense.ts +14 -4
  558. package/src/search/build.ts +21 -3
  559. package/src/search/documents.ts +26 -2
  560. package/src/search/orama-index.ts +191 -34
  561. package/src/search/pagefind-ranking.ts +39 -0
  562. package/src/search/source-pages.ts +29 -0
  563. package/src/search/sync/algolia.ts +111 -10
  564. package/src/search/sync/index.ts +52 -13
  565. package/src/search/sync/orama-cloud.ts +3 -0
  566. package/src/search/sync/typesense.ts +76 -26
  567. package/src/seo/metatags.ts +67 -0
  568. package/src/sources/contentful.ts +12 -0
  569. package/src/sources/github-releases.ts +7 -1
  570. package/src/sources/shared.ts +3 -0
  571. package/src/theme/chrome-icons.ts +1 -0
  572. package/src/theme/color.ts +500 -0
  573. package/src/theme/entry.ts +85 -6
  574. package/src/theme/palette.ts +121 -45
  575. package/src/translate/prompts.ts +1 -0
  576. package/src/translate/validate.ts +31 -3
  577. package/src/upgrade/upgrade.ts +4 -171
  578. package/dist/cli/chunk-1d7ve1dm.js.map +0 -12
  579. package/dist/cli/chunk-2hsdwb9n.js.map +0 -10
  580. package/dist/cli/chunk-35d4wj9f.js.map +0 -15
  581. package/dist/cli/chunk-364znk6q.js +0 -75
  582. package/dist/cli/chunk-364znk6q.js.map +0 -11
  583. package/dist/cli/chunk-5m5nmvyq.js.map +0 -13
  584. package/dist/cli/chunk-8ktnccpt.js.map +0 -10
  585. package/dist/cli/chunk-9t7a85s3.js.map +0 -10
  586. package/dist/cli/chunk-b07cmahc.js +0 -145
  587. package/dist/cli/chunk-b07cmahc.js.map +0 -11
  588. package/dist/cli/chunk-c8chx29p.js.map +0 -36
  589. package/dist/cli/chunk-e04dxsz1.js +0 -39
  590. package/dist/cli/chunk-e04dxsz1.js.map +0 -10
  591. package/dist/cli/chunk-j85vccga.js.map +0 -36
  592. package/dist/cli/chunk-p3v96n38.js.map +0 -10
  593. package/dist/cli/chunk-pehfxfta.js.map +0 -10
  594. package/dist/cli/chunk-pv29h0wf.js.map +0 -190
  595. package/dist/cli/chunk-q58y5e6a.js.map +0 -10
  596. package/dist/cli/chunk-vg9r4eb9.js.map +0 -14
  597. package/dist/cli/chunk-wrr3j9w9.js.map +0 -35
  598. package/dist/cli/chunk-yfyb25rh.js.map +0 -14
  599. package/docs/content/sources.mdx +0 -334
  600. /package/dist/cli/{chunk-tkacnehg.js.map → chunk-3v3ffbrj.js.map} +0 -0
  601. /package/dist/cli/{chunk-r9rcc4w7.js.map → chunk-8y2kmjv8.js.map} +0 -0
  602. /package/dist/cli/{chunk-2eytanqx.js.map → chunk-c887fj0s.js.map} +0 -0
  603. /package/dist/cli/{chunk-akbpwfxc.js.map → chunk-dzwehpy7.js.map} +0 -0
  604. /package/dist/cli/{chunk-2z928egk.js.map → chunk-ft9vfys6.js.map} +0 -0
  605. /package/dist/cli/{chunk-a58773jm.js.map → chunk-kma9f6sf.js.map} +0 -0
  606. /package/dist/cli/{chunk-crgn1q09.js.map → chunk-sds923fa.js.map} +0 -0
  607. /package/dist/cli/{chunk-acanzt5p.js.map → chunk-tdqhyxfz.js.map} +0 -0
@@ -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,16 @@
1
1
  import { useCallback, useEffect, useRef, useState } from "react";
2
2
 
3
+ import {
4
+ ASK_MAX_MESSAGES,
5
+ ASK_MAX_MESSAGES_CHARS,
6
+ } from "../../ai/ask-limits.ts";
7
+ import { captchaToken } from "../../captcha/client.ts";
8
+ import type { CaptchaSettings } from "../../captcha/schema.ts";
3
9
  import type { BlumeClientData } from "../../core/data.ts";
4
10
  import { track } from "../layout/analytics-client.ts";
5
11
  import type { SearchFn, SearchResult } from "../layout/search/types.ts";
6
12
  import { joinBase, stripBase } from "./base-path.ts";
13
+ import { newThreadId } from "./support-link.ts";
7
14
 
8
15
  /**
9
16
  * React hooks for Blume islands.
@@ -159,11 +166,22 @@ export interface UseAssistant {
159
166
  loading: boolean;
160
167
  messages: AskMessage[];
161
168
  reset: () => void;
169
+ /**
170
+ * The conversation's id, new after every `reset()`. The `ask` analytics
171
+ * events carry it as `thread`, and the support link passes it along.
172
+ */
173
+ thread: string;
162
174
  }
163
175
 
164
176
  const DEFAULT_ASK_ENDPOINT = joinBase(import.meta.env.BASE_URL, "api/ask");
165
177
 
166
178
  export interface UseAssistantOptions {
179
+ /**
180
+ * The site's bot check (`ai.assistant.captcha`), as `useBlume().config
181
+ * .assistant.captcha` holds it: each question gets a fresh token, sent as
182
+ * `captcha` in the request body.
183
+ */
184
+ captcha?: CaptchaSettings | null;
167
185
  /** Existing assistant endpoint; defaults to Blume's generated `/api/ask`. */
168
186
  endpoint?: string;
169
187
  /**
@@ -172,11 +190,38 @@ export interface UseAssistantOptions {
172
190
  * dictionary string.
173
191
  */
174
192
  errorMessage?: string;
193
+ /**
194
+ * Shown as the assistant's answer when the route's rate limit turns the
195
+ * question away (`429`). Defaults to an English notice; the built-in
196
+ * island passes its localized dictionary string.
197
+ */
198
+ rateLimitMessage?: string;
199
+ /**
200
+ * Shown as the assistant's answer when the bot check fails, in the browser
201
+ * or at the route (`403`). Defaults to an English notice; the built-in
202
+ * island passes its localized dictionary string.
203
+ */
204
+ verifyMessage?: string;
175
205
  }
176
206
 
177
207
  /** Shown as the assistant's answer when the request fails or throws. */
178
208
  const ASK_ERROR = "Something went wrong answering that. Please try again.";
179
209
 
210
+ /** Shown as the assistant's answer when the rate limit turns a question away. */
211
+ const ASK_RATE_LIMITED =
212
+ "You've asked a lot of questions. Try again in a few minutes.";
213
+
214
+ /** Shown as the assistant's answer when the bot check fails. */
215
+ const ASK_VERIFY_FAILED = "We couldn't check that you're human. Try again.";
216
+
217
+ /** What the assistant sends the route. */
218
+ interface AskPayload {
219
+ /** The bot check's token, when the site has one. */
220
+ captcha?: string;
221
+ messages: AskMessage[];
222
+ page: { path: string };
223
+ }
224
+
180
225
  /** How the assistant's generated route opens its missing-credential notice. */
181
226
  const NOT_CONFIGURED = /^The assistant is not configured/u;
182
227
 
@@ -199,10 +244,52 @@ const unavailableNotice = async (
199
244
  return NOT_CONFIGURED.test(text) && text.length <= 200 ? text : null;
200
245
  };
201
246
 
247
+ /** The notices the route's refusals map to. */
248
+ interface RefusalNotices {
249
+ rateLimitMessage: string;
250
+ verifyMessage: string;
251
+ }
252
+
253
+ /**
254
+ * The reader-facing notice for the route turning a question away: the rate
255
+ * limit (`429`) or the bot check (`403`), or `null` for any other status.
256
+ */
257
+ const refusalNotice = (
258
+ status: number,
259
+ notices: RefusalNotices
260
+ ): string | null => {
261
+ if (status === 429) {
262
+ return notices.rateLimitMessage;
263
+ }
264
+ return status === 403 ? notices.verifyMessage : null;
265
+ };
266
+
202
267
  /** The current route with the deployment base stripped, for page grounding. */
203
268
  const currentPath = (): string =>
204
269
  stripBase(import.meta.env.BASE_URL, window.location.pathname);
205
270
 
271
+ /**
272
+ * The conversation as the route is sent it: all of it while it fits the
273
+ * route's limits (`ai/ask-limits.ts`), else the latest turns that do, the
274
+ * oldest dropped first, so a long chat keeps working instead of answering
275
+ * `400`. The kept turns open with a question, not an answer cut from its
276
+ * own, and the question being asked always goes, even alone over the limit.
277
+ */
278
+ const withinAskLimits = (history: AskMessage[]): AskMessage[] => {
279
+ const last = history.length - 1;
280
+ let start = Math.max(0, history.length - ASK_MAX_MESSAGES);
281
+ while (
282
+ start < last &&
283
+ JSON.stringify(history.slice(start)).length > ASK_MAX_MESSAGES_CHARS
284
+ ) {
285
+ start += 1;
286
+ }
287
+ while (start < last && history[start]?.role !== "user") {
288
+ start += 1;
289
+ }
290
+ return history.slice(start);
291
+ };
292
+
206
293
  /**
207
294
  * Stream answers from the assistant endpoint. Mirrors the built-in assistant island so
208
295
  * a custom chat UI shares the same grounded, page-aware backend.
@@ -212,8 +299,12 @@ export const useAssistant = (
212
299
  ): UseAssistant => {
213
300
  const endpoint = options.endpoint ?? DEFAULT_ASK_ENDPOINT;
214
301
  const errorMessage = options.errorMessage ?? ASK_ERROR;
302
+ const rateLimitMessage = options.rateLimitMessage ?? ASK_RATE_LIMITED;
303
+ const verifyMessage = options.verifyMessage ?? ASK_VERIFY_FAILED;
304
+ const { captcha } = options;
215
305
  const [messages, setMessages] = useState<AskMessage[]>([]);
216
306
  const [loading, setLoading] = useState(false);
307
+ const [thread, setThread] = useState(newThreadId);
217
308
  // The stream writes into the conversation via state updates, so `reset()`
218
309
  // mid-answer must revoke the in-flight request's right to write — otherwise
219
310
  // its next chunk re-appends the assistant bubble onto the emptied list, and
@@ -255,7 +346,7 @@ export const useAssistant = (
255
346
  ) =>
256
347
  track(
257
348
  event,
258
- { ...props, path: pathname, questionChars: trimmed.length },
349
+ { ...props, path: pathname, questionChars: trimmed.length, thread },
259
350
  { question: trimmed }
260
351
  );
261
352
  report("ask", {});
@@ -280,12 +371,32 @@ export const useAssistant = (
280
371
  const assistant: AskMessage = { content: "", role: "assistant" };
281
372
  setMessages([...history, assistant]);
282
373
  setLoading(true);
374
+ // Answer with a notice instead: an error, or the route turning the
375
+ // question away. Silent once a reset has revoked the question.
376
+ const fail = (notice: string) => {
377
+ if (live()) {
378
+ outcome("ask_error", { status });
379
+ assistant.content = notice;
380
+ setMessages([...history, { ...assistant }]);
381
+ }
382
+ };
283
383
  try {
384
+ // A fresh bot-check token per question, when the site has a check;
385
+ // `null` when the check didn't pass in the browser.
386
+ const token = captcha
387
+ ? await captchaToken(captcha).catch(() => null)
388
+ : undefined;
389
+ if (token === null) {
390
+ fail(verifyMessage);
391
+ return;
392
+ }
393
+ const payload: AskPayload = {
394
+ captcha: token,
395
+ messages: withinAskLimits(history),
396
+ page: { path },
397
+ };
284
398
  const response = await fetch(endpoint, {
285
- body: JSON.stringify({
286
- messages: history,
287
- page: { path },
288
- }),
399
+ body: JSON.stringify(payload),
289
400
  headers: { "content-type": "application/json" },
290
401
  method: "POST",
291
402
  signal: controller.signal,
@@ -294,12 +405,12 @@ export const useAssistant = (
294
405
  if (!response.ok) {
295
406
  // An error body (JSON, HTML error page) must not stream in as the
296
407
  // 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
- }
408
+ const notice =
409
+ refusalNotice(status, {
410
+ rateLimitMessage,
411
+ verifyMessage,
412
+ }) ?? (await unavailableNotice(response).catch(() => null));
413
+ fail(notice ?? errorMessage);
303
414
  return;
304
415
  }
305
416
  const reader = response.body?.getReader();
@@ -332,20 +443,14 @@ export const useAssistant = (
332
443
  if (assistant.content) {
333
444
  outcome("ask_answer", { chars: assistant.content.length });
334
445
  } else {
335
- outcome("ask_error", { status });
336
- assistant.content = errorMessage;
337
- setMessages([...history, { ...assistant }]);
446
+ fail(errorMessage);
338
447
  }
339
448
  }
340
449
  } catch {
341
450
  // A thrown fetch (offline, DNS failure, CORS) must not strand the
342
451
  // pre-appended empty assistant message as a stuck placeholder. A
343
452
  // 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
- }
453
+ fail(errorMessage);
349
454
  // oxlint-disable-next-line react/todo -- React Compiler cannot lower try/finally; the hook stays manually memoized above
350
455
  } finally {
351
456
  if (live()) {
@@ -353,7 +458,16 @@ export const useAssistant = (
353
458
  }
354
459
  }
355
460
  },
356
- [endpoint, errorMessage, loading, messages]
461
+ [
462
+ captcha,
463
+ endpoint,
464
+ errorMessage,
465
+ loading,
466
+ messages,
467
+ rateLimitMessage,
468
+ thread,
469
+ verifyMessage,
470
+ ]
357
471
  );
358
472
 
359
473
  // Retained for the compiler-off opt-out path (`react: { compiler: false }`):
@@ -364,9 +478,10 @@ export const useAssistant = (
364
478
  abortRef.current?.abort();
365
479
  abortRef.current = null;
366
480
  setMessages([]);
481
+ setThread(newThreadId());
367
482
  // The in-flight `ask`'s finally is now stale and won't clear this.
368
483
  setLoading(false);
369
484
  }, []);
370
485
 
371
- return { ask, loading, messages, reset };
486
+ return { ask, loading, messages, reset, thread };
372
487
  };
@@ -0,0 +1,68 @@
1
+ /**
2
+ * The assistant's handoff to your support channel (`ai.assistant.support`).
3
+ * A `mailto:` link starts an email with the conversation as its body, so the
4
+ * reader doesn't have to repeat themselves; any other link gets the
5
+ * conversation's `thread` id in its query, the same id the assistant's
6
+ * analytics events carry, so a support team can find the conversation.
7
+ */
8
+
9
+ /** One turn of the conversation. */
10
+ export interface SupportMessage {
11
+ content: string;
12
+ role: "assistant" | "user";
13
+ }
14
+
15
+ /** What the link is built from. */
16
+ export interface SupportContext {
17
+ /** The label the transcript gives the assistant's turns. */
18
+ ai: string;
19
+ messages: readonly SupportMessage[];
20
+ /** The email's subject. */
21
+ subject: string;
22
+ thread: string;
23
+ /** The label the transcript gives the reader's turns. */
24
+ you: string;
25
+ }
26
+
27
+ /**
28
+ * How much conversation an email carries: mail clients cut long `mailto:`
29
+ * links, so a long conversation is trimmed from the start, keeping the
30
+ * latest turns.
31
+ */
32
+ export const MAX_TRANSCRIPT_CHARS = 1500;
33
+
34
+ const transcriptOf = (context: SupportContext): string => {
35
+ const full = context.messages
36
+ .map(
37
+ (message) =>
38
+ `${message.role === "user" ? context.you : context.ai}: ${message.content}`
39
+ )
40
+ .join("\n\n");
41
+ return full.length > MAX_TRANSCRIPT_CHARS
42
+ ? `…${full.slice(-MAX_TRANSCRIPT_CHARS)}`
43
+ : full;
44
+ };
45
+
46
+ /** The support link for this conversation. */
47
+ export const supportHref = (
48
+ support: string,
49
+ context: SupportContext
50
+ ): string => {
51
+ if (support.startsWith("mailto:")) {
52
+ // `mailto:` wants %20, not the `+` URLSearchParams writes for a space.
53
+ const body = `${transcriptOf(context)}\n\n(${context.thread})`;
54
+ const separator = support.includes("?") ? "&" : "?";
55
+ return `${support}${separator}subject=${encodeURIComponent(context.subject)}&body=${encodeURIComponent(body)}`;
56
+ }
57
+ const url = new URL(support, "https://blume.invalid");
58
+ url.searchParams.set("thread", context.thread);
59
+ return url.origin === "https://blume.invalid"
60
+ ? `${url.pathname}${url.search}${url.hash}`
61
+ : url.href;
62
+ };
63
+
64
+ /** A fresh conversation id: 16 hex characters. */
65
+ export const newThreadId = (): string =>
66
+ Array.from(crypto.getRandomValues(new Uint8Array(8)), (byte) =>
67
+ byte.toString(16).padStart(2, "0")
68
+ ).join("");
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  import VercelAnalytics from "@vercel/analytics/astro";
3
+ import data from "blume:data";
3
4
 
4
5
  import { analyticsHead } from "../../analytics/head.ts";
5
6
  import type { BlumeDataConfig } from "../../core/data.ts";
@@ -10,12 +11,20 @@ import type { BlumeDataConfig } from "../../core/data.ts";
10
11
  // component for `vercel()`, one <script> for every other adapter. Loads ONLY
11
12
  // in production builds so `blume dev` stays clean and local traffic never
12
13
  // reaches your analytics.
14
+ //
15
+ // With `consent` configured, every tag is held as inert `text/plain` until
16
+ // the reader allows analytics, when the consent runtime runs it
17
+ // (`consent/client.ts`); Vercel's component sends nothing until then instead
18
+ // (`consent/init.ts`).
13
19
  interface Props {
14
20
  analytics?: BlumeDataConfig["analytics"] | null;
15
21
  }
16
22
 
17
23
  const { analytics } = Astro.props;
18
24
  const nodes = analyticsHead(import.meta.env.PROD ? (analytics ?? []) : []);
25
+ const held = data.config.consent
26
+ ? { "data-blume-consent": "analytics", type: "text/plain" }
27
+ : {};
19
28
  ---
20
29
 
21
30
  {
@@ -24,9 +33,9 @@ const nodes = analyticsHead(import.meta.env.PROD ? (analytics ?? []) : []);
24
33
  return <VercelAnalytics {...node.props} />;
25
34
  }
26
35
  return node.content === null ? (
27
- <script is:inline {...node.attributes} />
36
+ <script is:inline {...node.attributes} {...held} />
28
37
  ) : (
29
- <script is:inline set:html={node.content} {...node.attributes} />
38
+ <script is:inline set:html={node.content} {...node.attributes} {...held} />
30
39
  );
31
40
  })
32
41
  }