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.
- package/AGENTS.md +1 -0
- package/CHANGELOG.md +48 -0
- package/README.md +1 -1
- package/dist/cli/{chunk-1d7ve1dm.js → chunk-0gz36akr.js} +18 -2
- package/dist/cli/{chunk-1d7ve1dm.js.map → chunk-0gz36akr.js.map} +3 -3
- package/dist/cli/{chunk-2eytanqx.js → chunk-1n228xqb.js} +2 -2
- package/dist/cli/{chunk-yfyb25rh.js → chunk-2f3ndkpw.js} +24 -8
- package/dist/cli/chunk-2f3ndkpw.js.map +14 -0
- package/dist/cli/{chunk-r20tn01b.js → chunk-2f7n4qzt.js} +8 -2
- package/dist/cli/{chunk-r20tn01b.js.map → chunk-2f7n4qzt.js.map} +3 -3
- package/dist/cli/{chunk-35d4wj9f.js → chunk-2n4ps1b3.js} +7 -7
- package/dist/cli/{chunk-j85vccga.js → chunk-43zcf4gz.js} +97 -57
- package/dist/cli/{chunk-j85vccga.js.map → chunk-43zcf4gz.js.map} +6 -6
- package/dist/cli/{chunk-e04dxsz1.js → chunk-4ejxbzee.js} +1 -1
- package/dist/cli/{chunk-5m5nmvyq.js → chunk-4h8z9vv5.js} +233 -39
- package/dist/cli/chunk-4h8z9vv5.js.map +16 -0
- package/dist/cli/{chunk-5f86nr5m.js → chunk-5cg2rzgf.js} +18 -16
- package/dist/cli/{chunk-5f86nr5m.js.map → chunk-5cg2rzgf.js.map} +2 -2
- package/dist/cli/{chunk-tzmab476.js → chunk-5ffywsmb.js} +4 -4
- package/dist/cli/{chunk-2z928egk.js → chunk-5wj2qwn2.js} +5 -5
- package/dist/cli/chunk-6bm0xpgd.js +18 -0
- package/dist/cli/chunk-6bm0xpgd.js.map +10 -0
- package/dist/cli/{chunk-vg9r4eb9.js → chunk-713mngdq.js} +12 -5
- package/dist/cli/{chunk-vg9r4eb9.js.map → chunk-713mngdq.js.map} +3 -3
- package/dist/cli/{chunk-5xvm6tfj.js → chunk-9geymqns.js} +17 -15
- package/dist/cli/{chunk-5xvm6tfj.js.map → chunk-9geymqns.js.map} +2 -2
- package/dist/cli/{chunk-q58y5e6a.js → chunk-9xr4dgee.js} +26 -17
- package/dist/cli/chunk-9xr4dgee.js.map +10 -0
- package/dist/cli/{chunk-b07cmahc.js → chunk-amrb6hgh.js} +2 -2
- package/dist/cli/chunk-cm89scdv.js +443 -0
- package/dist/cli/chunk-cm89scdv.js.map +11 -0
- package/dist/cli/{chunk-akbpwfxc.js → chunk-cmtcnh9f.js} +2 -2
- package/dist/cli/chunk-cy5bm0j0.js +104 -0
- package/dist/cli/chunk-cy5bm0j0.js.map +11 -0
- package/dist/cli/{chunk-crgn1q09.js → chunk-e7vvk5ya.js} +4 -4
- package/dist/cli/{chunk-pehfxfta.js → chunk-g2z9x01v.js} +6 -98
- package/dist/cli/chunk-g2z9x01v.js.map +10 -0
- package/dist/cli/{chunk-tkacnehg.js → chunk-gp84za0h.js} +2 -2
- package/dist/cli/{chunk-wrr3j9w9.js → chunk-h2a48yhd.js} +186 -63
- package/dist/cli/chunk-h2a48yhd.js.map +36 -0
- package/dist/cli/{chunk-hm3vjy5s.js → chunk-jnwj5kjj.js} +14 -9
- package/dist/cli/{chunk-hm3vjy5s.js.map → chunk-jnwj5kjj.js.map} +4 -4
- package/dist/cli/chunk-kwrg63tm.js +31 -0
- package/dist/cli/chunk-kwrg63tm.js.map +10 -0
- package/dist/cli/{chunk-2hsdwb9n.js → chunk-nd1821sk.js} +18 -16
- package/dist/cli/{chunk-2hsdwb9n.js.map → chunk-nd1821sk.js.map} +2 -2
- package/dist/cli/{chunk-6dsbexzp.js → chunk-ntv4qrt7.js} +17 -15
- package/dist/cli/{chunk-6dsbexzp.js.map → chunk-ntv4qrt7.js.map} +2 -2
- package/dist/cli/{chunk-r9rcc4w7.js → chunk-p804f3pe.js} +7 -7
- package/dist/cli/{chunk-ey84smr6.js → chunk-py62fewh.js} +6 -6
- package/dist/cli/{chunk-ey84smr6.js.map → chunk-py62fewh.js.map} +1 -1
- package/dist/cli/chunk-q3wxxq39.js +126 -0
- package/dist/cli/chunk-q3wxxq39.js.map +11 -0
- package/dist/cli/{chunk-9t7a85s3.js → chunk-rekqz4qg.js} +23 -13
- package/dist/cli/chunk-rekqz4qg.js.map +10 -0
- package/dist/cli/{chunk-8ktnccpt.js → chunk-ta27feev.js} +1 -1
- package/dist/cli/{chunk-pv29h0wf.js → chunk-xkz5xbtv.js} +3476 -822
- package/dist/cli/chunk-xkz5xbtv.js.map +213 -0
- package/dist/cli/{chunk-a58773jm.js → chunk-y4ykbark.js} +2 -2
- package/dist/cli/{chunk-p3v96n38.js → chunk-y5h4r2y0.js} +4 -4
- package/dist/cli/{chunk-acanzt5p.js → chunk-yj17vs39.js} +6 -6
- package/dist/cli/{chunk-p73c0m7w.js → chunk-yxmk4pec.js} +16 -14
- package/dist/cli/{chunk-p73c0m7w.js.map → chunk-yxmk4pec.js.map} +2 -2
- package/dist/cli/{chunk-3em5wd2y.js → chunk-zccbjcj0.js} +4 -3
- package/dist/cli/{chunk-3em5wd2y.js.map → chunk-zccbjcj0.js.map} +2 -2
- package/dist/cli/{chunk-c8chx29p.js → chunk-zf748avv.js} +384 -207
- package/dist/cli/chunk-zf748avv.js.map +37 -0
- package/dist/cli/{chunk-g4hq16wv.js → chunk-zhvd6adm.js} +16 -14
- package/dist/cli/{chunk-g4hq16wv.js.map → chunk-zhvd6adm.js.map} +2 -2
- package/dist/cli/index.js +26 -20
- package/dist/cli/index.js.map +5 -5
- package/dist/types/ai/agent-surface.d.ts +8 -7
- package/dist/types/ai/ai-catalog.d.ts +7 -0
- package/dist/types/ai/ask-context.d.ts +22 -0
- package/dist/types/ai/ask.d.ts +271 -58
- package/dist/types/ai/index.d.ts +2 -2
- package/dist/types/ai/site-skill.d.ts +26 -0
- package/dist/types/ai/skills.d.ts +16 -0
- package/dist/types/astro/integration.d.ts +8 -0
- package/dist/types/captcha/hcaptcha.d.ts +27 -0
- package/dist/types/captcha/index.d.ts +28 -0
- package/dist/types/captcha/schema.d.ts +29 -0
- package/dist/types/captcha/turnstile.d.ts +28 -0
- package/dist/types/components/content/api-field.d.ts +59 -0
- package/dist/types/components/content/api-page.d.ts +95 -0
- package/dist/types/components/layout/nav-utils.d.ts +26 -0
- package/dist/types/components/openapi/helpers.d.ts +137 -0
- package/dist/types/components/openapi/security.d.ts +85 -0
- package/dist/types/consent/ethyca.d.ts +48 -0
- package/dist/types/consent/index.d.ts +37 -0
- package/dist/types/consent/native.d.ts +28 -0
- package/dist/types/consent/osano.d.ts +42 -0
- package/dist/types/consent/schema.d.ts +110 -0
- package/dist/types/core/config-input.d.ts +187 -20
- package/dist/types/core/data.d.ts +49 -4
- package/dist/types/core/footer.d.ts +8 -0
- package/dist/types/core/i18n-ui.d.ts +86 -0
- package/dist/types/core/includes.d.ts +8 -2
- package/dist/types/core/localizable.d.ts +7 -0
- package/dist/types/core/page-modes.d.ts +29 -0
- package/dist/types/core/redirect-patterns.d.ts +144 -0
- package/dist/types/core/related.d.ts +40 -0
- package/dist/types/core/schema.d.ts +551 -56
- package/dist/types/core/sources/read.d.ts +5 -0
- package/dist/types/core/types.d.ts +1 -1
- package/dist/types/core/variables.d.ts +42 -0
- package/dist/types/deploy/node-headers.d.ts +13 -2
- package/dist/types/deploy/platforms/cloudflare.d.ts +5 -0
- package/dist/types/deploy/platforms/netlify.d.ts +7 -0
- package/dist/types/deploy/platforms/vercel.d.ts +6 -0
- package/dist/types/deploy/redirects.d.ts +39 -11
- package/dist/types/deploy/vercel-negotiation.d.ts +9 -0
- package/dist/types/markdown/code-title.d.ts +17 -0
- package/dist/types/markdown/features.d.ts +3 -2
- package/dist/types/narration/build.d.ts +70 -0
- package/dist/types/narration/provider.d.ts +26 -0
- package/dist/types/narration/script.d.ts +154 -0
- package/dist/types/openapi/model.d.ts +12 -6
- package/dist/types/openapi/overlay.d.ts +54 -0
- package/dist/types/openapi/parse.d.ts +13 -0
- package/dist/types/openapi/references.d.ts +14 -4
- package/dist/types/ratelimit/cloudflare.d.ts +39 -0
- package/dist/types/ratelimit/index.d.ts +27 -0
- package/dist/types/ratelimit/memory.d.ts +36 -0
- package/dist/types/ratelimit/schema.d.ts +111 -0
- package/dist/types/ratelimit/upstash.d.ts +21 -0
- package/dist/types/ratelimit/wrangler.d.ts +23 -0
- package/dist/types/reference/asyncapi.d.ts +10 -10
- package/dist/types/reference/graphql.d.ts +13 -10
- package/dist/types/reference/openapi.d.ts +33 -12
- package/dist/types/reference/options.d.ts +45 -5
- package/dist/types/reference/scalar.d.ts +13 -0
- package/dist/types/reference/schema.d.ts +60 -36
- package/dist/types/search/documents.d.ts +12 -1
- package/dist/types/search/orama-index.d.ts +4 -0
- package/dist/types/seo/metatags.d.ts +20 -0
- package/docs/02-deployment.mdx +41 -5
- package/docs/03-upgrading.mdx +4 -4
- package/docs/04-migrating.mdx +1 -1
- package/docs/advanced/changelog.mdx +2 -2
- package/docs/advanced/skills.mdx +14 -0
- package/docs/cli/audit.mdx +21 -1
- package/docs/cli/index.mdx +1 -0
- package/docs/configuration/analytics.mdx +3 -1
- package/docs/configuration/assistant.mdx +188 -36
- package/docs/configuration/consent.mdx +100 -0
- package/docs/configuration/customization.mdx +4 -4
- package/docs/configuration/index.mdx +63 -5
- package/docs/configuration/meta.ts +3 -0
- package/docs/configuration/narration.mdx +119 -0
- package/docs/configuration/rate-limiting.mdx +69 -0
- package/docs/configuration/search.mdx +29 -1
- package/docs/configuration/theming.mdx +3 -1
- package/docs/content/components.mdx +106 -0
- package/docs/content/frontmatter.mdx +76 -0
- package/docs/content/i18n.mdx +38 -3
- package/docs/content/includes.mdx +16 -0
- package/docs/content/meta.mdx +1 -0
- package/docs/content/meta.ts +1 -0
- package/docs/content/navigation.mdx +28 -6
- package/docs/content/sources/contentful.mdx +32 -0
- package/docs/content/sources/custom.mdx +39 -0
- package/docs/content/sources/github-releases.mdx +31 -0
- package/docs/content/sources/index.mdx +91 -0
- package/docs/content/sources/meta.ts +16 -0
- package/docs/content/sources/notion.mdx +32 -0
- package/docs/content/sources/obsidian.mdx +37 -0
- package/docs/content/sources/payload.mdx +30 -0
- package/docs/content/sources/remote-mdx.mdx +40 -0
- package/docs/content/sources/sanity.mdx +29 -0
- package/docs/content/sources/strapi.mdx +31 -0
- package/docs/content/syntax.mdx +67 -2
- package/docs/content/variables.mdx +41 -0
- package/docs/discoverability/agent-discovery.mdx +29 -2
- package/docs/discoverability/index.mdx +1 -1
- package/docs/discoverability/llms-txt.mdx +1 -1
- package/docs/discoverability/metadata.mdx +17 -0
- package/docs/references/api-pages.mdx +74 -0
- package/docs/references/asyncapi.mdx +1 -1
- package/docs/references/graphql.mdx +2 -2
- package/docs/references/meta.ts +1 -1
- package/docs/references/openapi.mdx +102 -3
- package/docs/references/scalar.mdx +3 -1
- package/package.json +36 -1
- package/skills/blume/SKILL.md +3 -0
- package/skills/blume-migrate/SKILL.md +5 -5
- package/skills/blume-migrate/references/docusaurus.md +3 -3
- package/skills/blume-migrate/references/mintlify.md +37 -19
- package/skills/blume-migrate/references/nextra.md +2 -2
- package/skills/blume-migrate/references/starlight.md +2 -1
- package/skills/blume-migrate/scripts/mintlify-codemod.mjs +60 -14
- package/skills/blume-write-skill/SKILL.md +82 -0
- package/src/ai/agent-readability.ts +2 -1
- package/src/ai/agent-surface.ts +9 -7
- package/src/ai/ai-catalog.ts +6 -3
- package/src/ai/ask-context.ts +73 -20
- package/src/ai/ask-data.ts +6 -0
- package/src/ai/ask-tools.ts +142 -0
- package/src/ai/ask.ts +354 -76
- package/src/ai/component-markdown.ts +75 -0
- package/src/ai/cors.ts +2 -0
- package/src/ai/index.ts +12 -0
- package/src/ai/llms.ts +6 -2
- package/src/ai/markdown.ts +2 -1
- package/src/ai/mcp/data.ts +6 -0
- package/src/ai/openapi-components.ts +9 -3
- package/src/ai/site-skill.ts +274 -0
- package/src/ai/skills.ts +35 -2
- package/src/ai/write-skill.ts +97 -0
- package/src/astro/generate.ts +132 -18
- package/src/astro/index.ts +1 -0
- package/src/astro/integration.ts +35 -0
- package/src/astro/module-types.ts +6 -0
- package/src/astro/templates.ts +283 -30
- package/src/astro/variables.ts +31 -0
- package/src/audit/catalog.ts +19 -0
- package/src/audit/checks/contrast.ts +183 -0
- package/src/audit/checks/i18n.ts +2 -3
- package/src/audit/checks/indexability.ts +2 -3
- package/src/audit/checks/links.ts +5 -12
- package/src/audit/checks/sitemap.ts +2 -3
- package/src/audit/redirects.ts +46 -15
- package/src/audit/run.ts +2 -0
- package/src/audit/types.ts +2 -1
- package/src/blume-modules.d.ts +7 -0
- package/src/captcha/client.ts +152 -0
- package/src/captcha/hcaptcha.ts +36 -0
- package/src/captcha/index.ts +28 -0
- package/src/captcha/schema.ts +32 -0
- package/src/captcha/turnstile.ts +38 -0
- package/src/captcha/verify.ts +71 -0
- package/src/cli/command-meta.ts +5 -0
- package/src/cli/commands/doctor.ts +22 -16
- package/src/cli/commands/skill.ts +96 -0
- package/src/cli/index.ts +5 -0
- package/src/cli/required-secrets.ts +71 -29
- package/src/components/BlumePage.astro +2 -0
- package/src/components/content/ApiEndpoint.astro +18 -0
- package/src/components/content/ApiField.astro +79 -0
- package/src/components/content/ApiPlayground.astro +62 -0
- package/src/components/content/ApiRail.astro +19 -0
- package/src/components/content/Color.astro +1 -0
- package/src/components/content/FileTree.astro +1 -0
- package/src/components/content/GithubInfo.astro +1 -0
- package/src/components/content/Panel.astro +1 -0
- package/src/components/content/ParamField.astro +25 -0
- package/src/components/content/RequestExample.astro +17 -0
- package/src/components/content/ResponseExample.astro +17 -0
- package/src/components/content/ResponseField.astro +32 -0
- package/src/components/content/Step.astro +1 -0
- package/src/components/content/Tile.astro +1 -0
- package/src/components/content/Tooltip.astro +1 -0
- package/src/components/content/Tree.astro +1 -0
- package/src/components/content/TypeTable.astro +4 -1
- package/src/components/content/View.astro +21 -0
- package/src/components/content/ViewSwitcher.astro +168 -0
- package/src/components/content/api-field.ts +100 -0
- package/src/components/content/api-page.ts +224 -0
- package/src/components/content/view-switcher.ts +71 -0
- package/src/components/index.ts +2 -0
- package/src/components/islands/Assistant.astro +10 -1
- package/src/components/islands/assistant.tsx +104 -14
- package/src/components/islands/code-question.ts +62 -0
- package/src/components/islands/hooks.ts +110 -21
- package/src/components/islands/support-link.ts +68 -0
- package/src/components/layout/Analytics.astro +11 -2
- package/src/components/layout/Banner.astro +14 -10
- package/src/components/layout/Breadcrumbs.astro +6 -2
- package/src/components/layout/ConsentBanner.astro +62 -0
- package/src/components/layout/ConsentHead.astro +38 -0
- package/src/components/layout/Directory.astro +94 -0
- package/src/components/layout/DrawerSwitchers.astro +51 -0
- package/src/components/layout/Empty.astro +2 -2
- package/src/components/layout/Header.astro +122 -80
- package/src/components/layout/LanguageSwitcher.astro +48 -15
- package/src/components/layout/NarrationPlayer.astro +1085 -0
- package/src/components/layout/NavSelector.astro +33 -7
- package/src/components/layout/PageFeedback.astro +54 -49
- package/src/components/layout/PageLayout.astro +71 -3
- package/src/components/layout/ReferenceLayout.astro +17 -6
- package/src/components/layout/RelatedPages.astro +42 -0
- package/src/components/layout/RootLayout.astro +308 -45
- package/src/components/layout/Search.astro +31 -17
- package/src/components/layout/SiteFooter.astro +124 -0
- package/src/components/layout/SiteMetatags.astro +16 -0
- package/src/components/layout/consent/native.ts +79 -0
- package/src/components/layout/consent/types.ts +37 -0
- package/src/components/layout/footer-links.ts +53 -0
- package/src/components/layout/footer-socials.ts +87 -0
- package/src/components/layout/locale-redirect.ts +107 -0
- package/src/components/layout/nav-utils.ts +83 -0
- package/src/components/layout/page-feedback.ts +96 -0
- package/src/components/layout/search/analytics.ts +98 -0
- package/src/components/layout/search/flexsearch.ts +17 -5
- package/src/components/layout/search/orama-cloud.ts +26 -14
- package/src/components/layout/search/types.ts +20 -0
- package/src/components/layout/search/typesense.ts +5 -1
- package/src/components/openapi/Operation.astro +161 -13
- package/src/components/openapi/RequestBody.astro +6 -4
- package/src/components/openapi/RequestPanel.astro +59 -7
- package/src/components/openapi/Responses.astro +6 -4
- package/src/components/openapi/async-snippets.ts +4 -4
- package/src/components/openapi/code-samples.ts +97 -0
- package/src/components/openapi/security.ts +6 -3
- package/src/components/openapi/snippets.ts +869 -8
- package/src/components/props.ts +6 -0
- package/src/consent/client.ts +95 -0
- package/src/consent/clients.ts +19 -0
- package/src/consent/ethyca.ts +71 -0
- package/src/consent/head.ts +24 -0
- package/src/consent/index.ts +37 -0
- package/src/consent/init.ts +29 -0
- package/src/consent/native.ts +36 -0
- package/src/consent/osano.ts +59 -0
- package/src/consent/schema.ts +30 -0
- package/src/core/builtin-tags.ts +5 -0
- package/src/core/client-address.ts +19 -0
- package/src/core/config-input.ts +194 -20
- package/src/core/data.ts +44 -4
- package/src/core/diagnostics.ts +3 -2
- package/src/core/footer.ts +26 -0
- package/src/core/graph.ts +39 -36
- package/src/core/i18n-ui.ts +82 -2
- package/src/core/includes.ts +20 -3
- package/src/core/links.ts +16 -2
- package/src/core/localizable.ts +26 -0
- package/src/core/navigation.ts +43 -12
- package/src/core/page-modes.ts +56 -0
- package/src/core/project-graph.ts +111 -5
- package/src/core/redirect-patterns.ts +491 -0
- package/src/core/related.ts +131 -0
- package/src/core/schema.ts +318 -64
- package/src/core/sources/normalize.ts +2 -1
- package/src/core/sources/read.ts +8 -3
- package/src/core/types.ts +1 -1
- package/src/core/ui-packs/ar.ts +43 -0
- package/src/core/ui-packs/bg.ts +45 -0
- package/src/core/ui-packs/bn.ts +43 -0
- package/src/core/ui-packs/ca.ts +46 -0
- package/src/core/ui-packs/cs.ts +44 -0
- package/src/core/ui-packs/da.ts +45 -0
- package/src/core/ui-packs/de.ts +46 -0
- package/src/core/ui-packs/el.ts +45 -0
- package/src/core/ui-packs/es.ts +46 -0
- package/src/core/ui-packs/fa.ts +45 -0
- package/src/core/ui-packs/fi.ts +45 -0
- package/src/core/ui-packs/fr.ts +46 -0
- package/src/core/ui-packs/he.ts +43 -0
- package/src/core/ui-packs/hi.ts +44 -0
- package/src/core/ui-packs/hr.ts +45 -0
- package/src/core/ui-packs/hu.ts +44 -0
- package/src/core/ui-packs/id.ts +45 -0
- package/src/core/ui-packs/it.ts +45 -0
- package/src/core/ui-packs/ja.ts +45 -0
- package/src/core/ui-packs/ko.ts +44 -0
- package/src/core/ui-packs/nl.ts +46 -0
- package/src/core/ui-packs/no.ts +44 -0
- package/src/core/ui-packs/pl.ts +45 -0
- package/src/core/ui-packs/pt-br.ts +46 -0
- package/src/core/ui-packs/pt.ts +45 -0
- package/src/core/ui-packs/ro.ts +45 -0
- package/src/core/ui-packs/ru.ts +45 -0
- package/src/core/ui-packs/sk.ts +44 -0
- package/src/core/ui-packs/sr.ts +45 -0
- package/src/core/ui-packs/sv.ts +44 -0
- package/src/core/ui-packs/th.ts +43 -0
- package/src/core/ui-packs/tr.ts +45 -0
- package/src/core/ui-packs/uk.ts +45 -0
- package/src/core/ui-packs/vi.ts +45 -0
- package/src/core/ui-packs/zh-tw.ts +43 -0
- package/src/core/ui-packs/zh.ts +43 -0
- package/src/core/variables.ts +103 -0
- package/src/deploy/artifacts.ts +77 -8
- package/src/deploy/cloudflare-negotiation.ts +35 -5
- package/src/deploy/headers.ts +2 -1
- package/src/deploy/node-headers.ts +63 -9
- package/src/deploy/platforms/cloudflare.ts +32 -0
- package/src/deploy/platforms/netlify.ts +33 -1
- package/src/deploy/platforms/vercel.ts +37 -2
- package/src/deploy/redirects.ts +91 -24
- package/src/deploy/vercel-negotiation.ts +36 -0
- package/src/markdown/api-rail.ts +259 -0
- package/src/markdown/code-title.ts +35 -9
- package/src/markdown/features.ts +3 -2
- package/src/markdown/fence-meta.ts +2 -0
- package/src/markdown/include.ts +11 -1
- package/src/markdown/index.ts +28 -5
- package/src/markdown/math.ts +4 -4
- package/src/markdown/variables.ts +56 -0
- package/src/markdown/views.ts +82 -0
- package/src/narration/build.ts +412 -0
- package/src/narration/player.ts +249 -0
- package/src/narration/provider.ts +29 -0
- package/src/narration/script.ts +592 -0
- package/src/og/card.ts +7 -6
- package/src/openapi/model.ts +57 -24
- package/src/openapi/overlay.ts +256 -0
- package/src/openapi/parse.ts +55 -2
- package/src/openapi/references.ts +21 -5
- package/src/openapi/render-mdx.ts +5 -1
- package/src/openapi/scalar.ts +40 -6
- package/src/openapi/source.ts +1 -1
- package/src/ratelimit/cloudflare.ts +54 -0
- package/src/ratelimit/index.ts +30 -0
- package/src/ratelimit/memory.ts +45 -0
- package/src/ratelimit/runtime.ts +229 -0
- package/src/ratelimit/schema.ts +28 -0
- package/src/ratelimit/upstash.ts +30 -0
- package/src/ratelimit/wrangler.ts +70 -0
- package/src/reference/asyncapi.ts +2 -2
- package/src/reference/graphql.ts +5 -2
- package/src/reference/openapi.ts +21 -7
- package/src/reference/options.ts +51 -7
- package/src/reference/scalar.ts +33 -8
- package/src/registry/eject.ts +20 -4
- package/src/registry/registry.ts +7 -0
- package/src/search/documents.ts +26 -2
- package/src/search/orama-index.ts +28 -3
- package/src/search/pagefind-ranking.ts +39 -0
- package/src/search/sync/algolia.ts +15 -4
- package/src/search/sync/orama-cloud.ts +3 -0
- package/src/search/sync/typesense.ts +5 -0
- package/src/seo/metatags.ts +67 -0
- package/src/theme/chrome-icons.ts +1 -0
- package/src/theme/color.ts +500 -0
- package/src/theme/entry.ts +56 -3
- package/src/theme/palette.ts +121 -45
- package/src/translate/prompts.ts +1 -0
- package/src/translate/validate.ts +8 -2
- package/src/upgrade/upgrade.ts +4 -171
- package/dist/cli/chunk-364znk6q.js +0 -75
- package/dist/cli/chunk-364znk6q.js.map +0 -11
- package/dist/cli/chunk-5m5nmvyq.js.map +0 -13
- package/dist/cli/chunk-9t7a85s3.js.map +0 -10
- package/dist/cli/chunk-c8chx29p.js.map +0 -36
- package/dist/cli/chunk-pehfxfta.js.map +0 -10
- package/dist/cli/chunk-pv29h0wf.js.map +0 -190
- package/dist/cli/chunk-q58y5e6a.js.map +0 -10
- package/dist/cli/chunk-wrr3j9w9.js.map +0 -35
- package/dist/cli/chunk-yfyb25rh.js.map +0 -14
- package/docs/content/sources.mdx +0 -334
- /package/dist/cli/{chunk-2eytanqx.js.map → chunk-1n228xqb.js.map} +0 -0
- /package/dist/cli/{chunk-35d4wj9f.js.map → chunk-2n4ps1b3.js.map} +0 -0
- /package/dist/cli/{chunk-e04dxsz1.js.map → chunk-4ejxbzee.js.map} +0 -0
- /package/dist/cli/{chunk-tzmab476.js.map → chunk-5ffywsmb.js.map} +0 -0
- /package/dist/cli/{chunk-2z928egk.js.map → chunk-5wj2qwn2.js.map} +0 -0
- /package/dist/cli/{chunk-b07cmahc.js.map → chunk-amrb6hgh.js.map} +0 -0
- /package/dist/cli/{chunk-akbpwfxc.js.map → chunk-cmtcnh9f.js.map} +0 -0
- /package/dist/cli/{chunk-crgn1q09.js.map → chunk-e7vvk5ya.js.map} +0 -0
- /package/dist/cli/{chunk-tkacnehg.js.map → chunk-gp84za0h.js.map} +0 -0
- /package/dist/cli/{chunk-r9rcc4w7.js.map → chunk-p804f3pe.js.map} +0 -0
- /package/dist/cli/{chunk-8ktnccpt.js.map → chunk-ta27feev.js.map} +0 -0
- /package/dist/cli/{chunk-a58773jm.js.map → chunk-y4ykbark.js.map} +0 -0
- /package/dist/cli/{chunk-p3v96n38.js.map → chunk-y5h4r2y0.js.map} +0 -0
- /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;})();`;
|
package/src/components/index.ts
CHANGED
|
@@ -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 = [] } =
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
<
|
|
468
|
-
|
|
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 =
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
[
|
|
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
|
};
|