@docsector/docsector-reader 4.3.2 β†’ 4.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/.env.example +18 -0
  2. package/README.md +136 -5
  3. package/bin/docsector.js +36 -1
  4. package/docsector.config.js +44 -0
  5. package/package.json +3 -2
  6. package/public/robots.txt +4 -0
  7. package/src/ai-assistant/config.js +91 -0
  8. package/src/ai-assistant/indexing.js +50 -0
  9. package/src/ai-assistant/layout.js +56 -0
  10. package/src/ai-assistant/messages.js +41 -0
  11. package/src/ai-assistant/panel.js +22 -0
  12. package/src/ai-assistant/server.js +348 -0
  13. package/src/ai-assistant/session.js +91 -0
  14. package/src/ai-assistant/stream.js +125 -0
  15. package/src/components/DAssistantPanel.vue +701 -0
  16. package/src/components/DPage.vue +114 -4
  17. package/src/components/DPageAnchor.vue +11 -7
  18. package/src/components/DPageRichContent.vue +105 -0
  19. package/src/components/DPageTokens.vue +27 -16
  20. package/src/components/api-block-model.js +77 -1
  21. package/src/components/inline-code-copy.js +58 -0
  22. package/src/components/page-section-tokens.js +6 -4
  23. package/src/components/quasar-api-extends.json +235 -0
  24. package/src/composables/useAssistant.js +201 -0
  25. package/src/i18n/helpers.js +2 -0
  26. package/src/i18n/languages/en-US.hjson +22 -0
  27. package/src/i18n/languages/pt-BR.hjson +22 -0
  28. package/src/layouts/DefaultLayout.vue +22 -0
  29. package/src/markdown-agent.js +32 -0
  30. package/src/pages/manual/basic/ai-assistant.overview.en-US.md +69 -0
  31. package/src/pages/manual/basic/ai-assistant.overview.pt-BR.md +69 -0
  32. package/src/pages/manual/basic/d-page-anchor.overview.en-US.md +1 -1
  33. package/src/pages/manual/basic/d-page-anchor.overview.pt-BR.md +1 -1
  34. package/src/pages/manual.index.js +29 -0
  35. package/src/quasar.factory.js +166 -33
  36. package/src/sitemap.js +103 -0
  37. package/src/store/Layout.js +9 -1
package/.env.example ADDED
@@ -0,0 +1,18 @@
1
+ # -----------------------------------------------------------------------------
2
+ # Docsector Reader / Cloudflare environment example
3
+ # -----------------------------------------------------------------------------
4
+ # This project can run with Cloudflare Pages Functions and AI Search.
5
+ #
6
+ # For production, set values as Cloudflare Pages environment variables/secrets.
7
+ # For local wrangler pages dev, prefer .dev.vars.
8
+ # For local Node tooling, .env can be used when your runner loads it.
9
+
10
+ # AI Assistant (Cloudflare AI Search REST fallback)
11
+ AI_SEARCH_INSTANCE_NAME=
12
+ CLOUDFLARE_ACCOUNT_ID=
13
+ CLOUDFLARE_API_TOKEN=
14
+
15
+ # Optional: Web Bot Auth runtime variables
16
+ # WEB_BOT_AUTH_JWKS=
17
+ # WEB_BOT_AUTH_PRIVATE_JWK=
18
+ # WEB_BOT_AUTH_KEY_ID=
package/README.md CHANGED
@@ -25,14 +25,15 @@ Transform Markdown content into beautiful, navigable documentation sites β€” wit
25
25
  - 🧠 **Markdown Negotiation** β€” Requests with `Accept: text/markdown` receive markdown responses, while browsers keep HTML by default
26
26
  - πŸ” **Web Bot Auth Directory** β€” Optional signed JWKS directory at `/.well-known/http-message-signatures-directory` for bot identity verification
27
27
  - πŸ€– **Open in ChatGPT / Claude** β€” One-click links to open the current page directly in ChatGPT or Claude for Q&A
28
- - πŸ€– **LLM Bot Detection** β€” Automatically serves raw Markdown to known AI crawlers (GPTBot, ClaudeBot, PerplexityBot, GrokBot, and others)
29
- - πŸ—ΊοΈ **Sitemap Generation** β€” Automatic `sitemap.xml` generation at build time with all page URLs (requires `siteUrl` in config)
30
- - πŸ€– **AI-Friendly robots.txt** β€” Scaffold includes a `robots.txt` explicitly allowing 23 AI crawlers (GPTBot, ClaudeBot, PerplexityBot, GrokBot, etc.)
28
+ - πŸ€– **LLM Bot Detection** β€” Automatically serves raw Markdown to known AI crawlers (GPTBot, ClaudeBot, PerplexityBot, Cloudflare-AI-Search, GrokBot, and others)
29
+ - πŸ—ΊοΈ **Sitemap Generation** β€” Automatic `sitemap.xml` generation at build time with root-relative URLs by default and absolute URLs when `siteUrl` is configured
30
+ - πŸ€– **AI-Friendly robots.txt** β€” Scaffold includes a `robots.txt` explicitly allowing 24 AI crawlers (GPTBot, ClaudeBot, PerplexityBot, Cloudflare-AI-Search, GrokBot, etc.) and advertises `Sitemap: /sitemap.xml`
31
31
  - 🧭 **Content Signals** β€” Optional `Content-Signal` directive for declaring AI usage policy (`ai-train`, `search`, `ai-input`) in `robots.txt`
32
32
  - 🧩 **Agent Skills Discovery Index** β€” Optional `/.well-known/agent-skills/index.json` with RFC v0.2.0 schema and SHA-256 digests
33
33
  - ✍️ **Docsector Authoring Skill** β€” Publishable `SKILL.md` that teaches agents Docsector blocks, page patterns, MCP lookup, and WebMCP tools
34
34
  - πŸͺͺ **MCP Server Card** β€” Optional `/.well-known/mcp/server-card.json` for MCP server discovery before connection
35
35
  - 🌐 **WebMCP Browser Tools** β€” Optional registration of in-page tools via `navigator.modelContext` for browser agents
36
+ - πŸ€– **AI Assistant Panel** β€” Optional documentation assistant drawer backed by Cloudflare AI Search through an internal same-origin endpoint
36
37
  - πŸ”— **Homepage Link Headers** β€” Auto-generated `Link` response headers for agent discovery (`api-catalog`, `service-doc`, `service-desc`, `describedby`) per RFC 8288 / RFC 9727
37
38
  - πŸ”Œ **MCP Server** β€” Auto-generated [MCP](https://modelcontextprotocol.io) server at `/mcp` for AI assistant integration (Claude Desktop, VS Code, etc.)
38
39
  - πŸ“„ **llms.txt / llms-full.txt** β€” Auto-generated [llms.txt](https://llmstxt.org) index and full-content file for LLM discovery (requires `siteUrl` in config)
@@ -42,6 +43,7 @@ Transform Markdown content into beautiful, navigable documentation sites β€” wit
42
43
  ## ✨ Features
43
44
 
44
45
  - πŸ“ **Markdown Rendering** β€” Write docs in Markdown, rendered with syntax highlighting (Prism.js)
46
+ - πŸ“‹ **Clickable Inline Code** β€” Backtick-rendered inline code snippets are clickable across pages, subpages, and AI assistant answers
45
47
  - πŸ”½ **Nested Markdown Lists** β€” Ordered and unordered lists preserve sublist hierarchy across multiple indentation levels
46
48
  - β˜‘οΈ **Markdown Task Lists** β€” GitBook-style `- [ ]` and `- [x]` items render as read-only checkboxes with nested subtasks
47
49
  - πŸ–ΌοΈ **Block Image Captions & Zoom** β€” Standalone Markdown images render as zoomable figures, and raw `figure` / `picture` markup supports separate alt text and captions
@@ -52,7 +54,7 @@ Transform Markdown content into beautiful, navigable documentation sites β€” wit
52
54
  - 🌍 **Internationalization (i18n)** β€” Multi-language support with HJSON locale files and per-page translations
53
55
  - πŸŒ— **Dark/Light Mode** β€” Automatic theme switching with Quasar Dark Plugin
54
56
  - 🧰 **Docsector CLI Skill Installer** β€” Install the built-in authoring skill into older scaffolds with `docsector install-skill`
55
- - πŸ”— **Anchor Navigation** β€” Right-side source-ordered Table of Contents tree with stable scroll tracking, auto-scroll to the active section, and active-heading resolution based on the last heading that crossed the content threshold
57
+ - πŸ”— **Anchor Navigation** β€” Right-side source-ordered Table of Contents tree with stable scroll tracking, resize-safe drawer state, auto-scroll to the active section, and active-heading resolution based on the last heading that crossed the content threshold
56
58
  - πŸ–±οΈ **Active Menu Item UX** β€” Active menu entries keep pointer cursor, clear URL hash without redundant navigation, and prevent accidental label text selection
57
59
  - πŸ”Ž **Search** β€” Menu search across all documentation content and tags
58
60
  - πŸ“± **Responsive** β€” Mobile-friendly with collapsible sidebar and drawers
@@ -291,9 +293,116 @@ Check `checks.discovery.webMcp.status` equals `"pass"`.
291
293
 
292
294
  ---
293
295
 
296
+ ## πŸ€– AI Assistant Panel
297
+
298
+ Docsector Reader can add an opt-in assistant panel for documentation Q&A. Users open it from the global header while reading pages and subpages; it is not a dedicated documentation route. The drawer posts to a same-origin Cloudflare Pages Function, and that function calls Cloudflare AI Search so secrets, rate-limit strategy, provider errors, and future auth stay server-side.
299
+
300
+ The panel is disabled by default. When enabled, desktop pages get a dedicated right-side assistant rail that can sit beside the table of contents on wide screens. Mobile uses a fullscreen dialog.
301
+
302
+ ### Configure
303
+
304
+ ```javascript
305
+ export default {
306
+ // ...other config
307
+
308
+ siteUrl: 'https://my-docs.example.com',
309
+
310
+ aiAssistant: {
311
+ enabled: true,
312
+ provider: 'aiSearch',
313
+ endpoint: '/assistant',
314
+ ui: {
315
+ title: 'Docs Assistant',
316
+ drawerWidth: 380,
317
+ wideBreakpoint: 1280,
318
+ showCitations: true,
319
+ suggestedPrompts: [
320
+ 'How do I get started?',
321
+ 'Summarize this page.',
322
+ 'Where is the related API reference?'
323
+ ]
324
+ },
325
+ aiSearch: {
326
+ binding: 'AI_SEARCH',
327
+ instanceNameEnv: 'AI_SEARCH_INSTANCE_NAME',
328
+ accountIdEnv: 'CLOUDFLARE_ACCOUNT_ID',
329
+ apiTokenEnv: 'CLOUDFLARE_API_TOKEN',
330
+ model: '@cf/meta/llama-3.3-70b-instruct-fp8-fast',
331
+ retrievalType: 'hybrid',
332
+ maxResults: 6,
333
+ matchThreshold: 0.4,
334
+ contextExpansion: 1,
335
+ queryRewrite: { enabled: true },
336
+ reranking: { enabled: false },
337
+ stream: true
338
+ }
339
+ }
340
+ }
341
+ ```
342
+
343
+ ### Cloudflare setup
344
+
345
+ Use Cloudflare AI Search as the first provider path:
346
+
347
+ - Create an AI Search instance in Cloudflare.
348
+ - Build and deploy the Docsector site first; build output always publishes `/sitemap.xml` and adds `Sitemap: /sitemap.xml` to `robots.txt` for crawler discovery.
349
+ - Use a Website data source. For the cleanest retrieval, point its specific sitemap to `/ai-search-sitemap.xml`; otherwise the crawler can discover `/sitemap.xml` from `robots.txt`.
350
+ - Add metadata fields such as title, path, locale, book, version, and subpage if you want filtering later.
351
+ - Set `AI_SEARCH_INSTANCE_NAME` as a Cloudflare Pages environment variable or local `.dev.vars` entry.
352
+ - Bind the instance to Pages as `AI_SEARCH` when available, or set encrypted Pages secrets for `CLOUDFLARE_ACCOUNT_ID` and `CLOUDFLARE_API_TOKEN` with AI Search run access.
353
+ - Keep AI Search public endpoints optional; the built-in UI uses the configured internal endpoint by default.
354
+
355
+ ### Build output
356
+
357
+ When enabled, `docsector build` can generate:
358
+
359
+ | File | Purpose |
360
+ |---|---|
361
+ | `functions/assistant.js` | Cloudflare Pages Function for browser assistant requests |
362
+ | `dist/spa/sitemap.xml` | Default crawler sitemap advertised from `robots.txt` |
363
+ | `dist/spa/robots.txt` | Crawler policy with `Sitemap: /sitemap.xml` |
364
+ | `dist/spa/ai-search-sitemap.xml` | Markdown-focused sitemap for AI Search crawling |
365
+ | `dist/spa/.well-known/ai-search/manifest.json` | Source metadata for indexed documentation pages |
366
+ | `dist/spa/_routes.json` | Routes the internal assistant endpoint to the Pages Function |
367
+
368
+ ### Validate
369
+
370
+ ```bash
371
+ npx docsector build
372
+ cat dist/spa/sitemap.xml
373
+ cat dist/spa/robots.txt
374
+ cat dist/spa/ai-search-sitemap.xml
375
+ cat dist/spa/.well-known/ai-search/manifest.json
376
+ npx wrangler pages dev dist/spa
377
+ ```
378
+
379
+ Workers AI, AI Search, and remote bindings can incur Cloudflare usage during local development.
380
+
381
+ ### Environment variables quick guide
382
+
383
+ Docsector now ships `.env.example` so teams can standardize Cloudflare variables.
384
+
385
+ Use the right place for each environment:
386
+
387
+ - Cloudflare Pages production/preview: set vars in Pages settings (recommended).
388
+ - Local `wrangler pages dev`: use `.dev.vars` for Function runtime variables.
389
+ - Local Node-based tools: `.env` works when your runner actually loads it.
390
+
391
+ Minimum variables when not using direct AI Search binding:
392
+
393
+ ```bash
394
+ AI_SEARCH_INSTANCE_NAME=...
395
+ CLOUDFLARE_ACCOUNT_ID=...
396
+ CLOUDFLARE_API_TOKEN=...
397
+ ```
398
+
399
+ If you bind AI Search as `AI_SEARCH`, the Assistant tries binding first and uses REST fallback when binding is not available.
400
+
401
+ ---
402
+
294
403
  ## οΏ½ llms.txt (LLM Discovery)
295
404
 
296
- Docsector Reader automatically generates [llms.txt](https://llmstxt.org) files at build time when `siteUrl` is configured (same requirement as sitemap.xml).
405
+ Docsector Reader automatically generates [llms.txt](https://llmstxt.org) files at build time when `siteUrl` is configured. `sitemap.xml` is generated even without `siteUrl`; `llms.txt` keeps the `siteUrl` requirement because it contains absolute Markdown links.
297
406
 
298
407
  | File | Purpose |
299
408
  |---|---|
@@ -499,6 +608,7 @@ Notes:
499
608
  - `aiTrain`, `search`, and `aiInput` accept `yes` / `no` (or booleans).
500
609
  - Default scope is only `User-agent: *`.
501
610
  - Build patch is idempotent: repeated builds do not duplicate `Content-Signal` lines.
611
+ - Build also keeps `Sitemap: /sitemap.xml` discoverable in `robots.txt` so crawlers can find the generated sitemap automatically.
502
612
 
503
613
  ### Validate
504
614
 
@@ -751,6 +861,27 @@ export default {
751
861
  agentFallback: true
752
862
  },
753
863
 
864
+ aiAssistant: {
865
+ enabled: false,
866
+ provider: 'aiSearch',
867
+ endpoint: '/assistant',
868
+ ui: {
869
+ title: 'Docsector Assistant',
870
+ drawerWidth: 380,
871
+ wideBreakpoint: 1280,
872
+ showCitations: true
873
+ },
874
+ aiSearch: {
875
+ binding: 'AI_SEARCH',
876
+ instanceNameEnv: 'AI_SEARCH_INSTANCE_NAME',
877
+ accountIdEnv: 'CLOUDFLARE_ACCOUNT_ID',
878
+ apiTokenEnv: 'CLOUDFLARE_API_TOKEN',
879
+ retrievalType: 'hybrid',
880
+ maxResults: 6,
881
+ stream: true
882
+ }
883
+ },
884
+
754
885
  mcpServerCard: {
755
886
  enabled: true,
756
887
  path: '/.well-known/mcp/server-card.json',
package/bin/docsector.js CHANGED
@@ -24,7 +24,7 @@ const packageRoot = resolve(__dirname, '..')
24
24
  const args = process.argv.slice(2)
25
25
  const command = args[0]
26
26
 
27
- const VERSION = '4.3.2'
27
+ const VERSION = '4.4.0'
28
28
  const AUTHORING_SKILL_NAME = 'docsector-documentation-authoring'
29
29
  const AUTHORING_SKILL_DESCRIPTION = 'Author Docsector documentation with Markdown, custom blocks, MCP, and WebMCP.'
30
30
  const AUTHORING_SKILL_PUBLIC_PATH = `/.well-known/agent-skills/${AUTHORING_SKILL_NAME}/SKILL.md`
@@ -152,6 +152,11 @@ export default {
152
152
  editBaseUrl: 'https://github.com/your-org/your-repo/edit/main/src/pages'
153
153
  },
154
154
 
155
+ // @ Site URL (optional)
156
+ // Set this for absolute URLs in sitemap.xml, llms.txt, and AI metadata.
157
+ // sitemap.xml is still generated with root-relative URLs when omitted.
158
+ // siteUrl: 'https://docs.example.com',
159
+
155
160
  // @ MCP (Model Context Protocol)
156
161
  // Uncomment to enable an MCP server at /mcp for AI assistant integration.
157
162
  // Requires Cloudflare Pages Functions (or compatible serverless platform).
@@ -587,11 +592,33 @@ node_modules
587
592
  .quasar
588
593
  dist
589
594
  functions
595
+ .env
596
+ .dev.vars
590
597
  npm-debug.log*
591
598
  .DS_Store
592
599
  .thumbs.db
593
600
  `
594
601
 
602
+ const TEMPLATE_ENV_EXAMPLE = `\
603
+ # -----------------------------------------------------------------------------
604
+ # Docsector Reader / Cloudflare environment example
605
+ # -----------------------------------------------------------------------------
606
+ # Copy to .env (or .dev.vars for wrangler pages dev) and fill with real values.
607
+ #
608
+ # AI Assistant (Cloudflare AI Search REST fallback)
609
+ AI_SEARCH_INSTANCE_NAME=
610
+ CLOUDFLARE_ACCOUNT_ID=
611
+ CLOUDFLARE_API_TOKEN=
612
+
613
+ # Optional: AI Search instance binding name (defaults to AI_SEARCH in config)
614
+ # AI_SEARCH=
615
+
616
+ # Optional: Web Bot Auth runtime variables
617
+ # WEB_BOT_AUTH_JWKS=
618
+ # WEB_BOT_AUTH_PRIVATE_JWK=
619
+ # WEB_BOT_AUTH_KEY_ID=
620
+ `
621
+
595
622
  const TEMPLATE_MARKDOWNLINT = `\
596
623
  {
597
624
  "MD013": false,
@@ -608,6 +635,7 @@ const TEMPLATE_ROBOTS_TXT = `\
608
635
  User-agent: *
609
636
  Allow: /
610
637
  Content-Signal: ai-train=yes, search=yes, ai-input=yes
638
+ Sitemap: /sitemap.xml
611
639
 
612
640
  # Explicitly allow AI crawlers
613
641
  # OpenAI
@@ -678,6 +706,10 @@ Allow: /
678
706
  User-agent: DuckAssistBot
679
707
  Allow: /
680
708
 
709
+ # Cloudflare
710
+ User-agent: Cloudflare-AI-Search
711
+ Allow: /
712
+
681
713
  # xAI
682
714
  User-agent: GrokBot
683
715
  Allow: /
@@ -758,6 +790,7 @@ npm run build
758
790
  \`\`\`
759
791
 
760
792
  The optimized SPA output will be in \`dist/spa/\`.
793
+ Docsector also generates \`dist/spa/sitemap.xml\` and keeps \`robots.txt\` discoverable with \`Sitemap: /sitemap.xml\`. Set \`siteUrl\` in \`docsector.config.js\` when you want absolute sitemap URLs.
761
794
  `
762
795
 
763
796
  // =============================================================================
@@ -918,6 +951,7 @@ function initProject (name) {
918
951
  ['package.json', getTemplatePackageJson(name)],
919
952
  ['quasar.config.js', TEMPLATE_QUASAR_CONFIG],
920
953
  ['docsector.config.js', TEMPLATE_DOCSECTOR_CONFIG],
954
+ ['.env.example', TEMPLATE_ENV_EXAMPLE],
921
955
  ['.markdownlint.json', TEMPLATE_MARKDOWNLINT],
922
956
  ['index.html', TEMPLATE_INDEX_HTML],
923
957
  ['postcss.config.cjs', TEMPLATE_POSTCSS],
@@ -948,6 +982,7 @@ function initProject (name) {
948
982
  console.log(` ${name}/`)
949
983
  console.log(' β”œβ”€β”€ docsector.config.js')
950
984
  console.log(' β”œβ”€β”€ quasar.config.js')
985
+ console.log(' β”œβ”€β”€ .env.example')
951
986
  console.log(' β”œβ”€β”€ .markdownlint.json')
952
987
  console.log(' β”œβ”€β”€ package.json')
953
988
  console.log(' β”œβ”€β”€ index.html')
@@ -49,12 +49,56 @@ export default {
49
49
  editBaseUrl: 'https://github.com/docsector/docsector-reader/edit/main/src/pages'
50
50
  },
51
51
 
52
+ // @ Site URL
53
+ // Used for absolute sitemap, llms.txt, MCP, and AI Search metadata URLs.
54
+ siteUrl: 'https://docsector.com',
55
+
52
56
  // @ MCP
53
57
  mcp: {
54
58
  serverName: 'docsector-docs',
55
59
  toolSuffix: 'docsector'
56
60
  },
57
61
 
62
+ // @ AI Assistant
63
+ aiAssistant: {
64
+ enabled: true,
65
+ provider: 'aiSearch',
66
+ endpoint: '/assistant',
67
+ ui: {
68
+ title: 'Docsector AI Assistant',
69
+ subtitle: 'Ask, search, or explain the docs.',
70
+ drawerWidth: 380,
71
+ wideBreakpoint: 1280,
72
+ showCitations: true,
73
+ suggestedPrompts: [
74
+ 'How do I get started?',
75
+ 'Summarize this page.',
76
+ 'Where is the related API reference?'
77
+ ]
78
+ },
79
+ aiSearch: {
80
+ binding: 'AI_SEARCH',
81
+ instanceNameEnv: 'AI_SEARCH_INSTANCE_NAME',
82
+ namespace: '',
83
+ accountIdEnv: 'CLOUDFLARE_ACCOUNT_ID',
84
+ apiTokenEnv: 'CLOUDFLARE_API_TOKEN',
85
+ model: '@cf/meta/llama-3.3-70b-instruct-fp8-fast',
86
+ retrievalType: 'vector',
87
+ maxResults: 6,
88
+ matchThreshold: 0.4,
89
+ contextExpansion: 1,
90
+ queryRewrite: {
91
+ enabled: true
92
+ },
93
+ reranking: {
94
+ enabled: false,
95
+ model: '@cf/baai/bge-reranker-base',
96
+ matchThreshold: 0.4
97
+ },
98
+ stream: true
99
+ }
100
+ },
101
+
58
102
  // @ Agent Skills
59
103
  agentSkills: {
60
104
  enabled: true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@docsector/docsector-reader",
3
- "version": "4.3.2",
3
+ "version": "4.4.0",
4
4
  "description": "A documentation rendering engine built with Vue 3, Quasar v2 and Vite. Transform Markdown into beautiful, navigable documentation sites.",
5
5
  "productName": "Docsector Reader",
6
6
  "author": "Rodrigo de Araujo Vieira",
@@ -33,6 +33,7 @@
33
33
  "index.html",
34
34
  "postcss.config.cjs",
35
35
  ".eslintrc.cjs",
36
+ ".env.example",
36
37
  "jsconfig.json",
37
38
  "README.md",
38
39
  "LICENSE.md"
@@ -56,7 +57,7 @@
56
57
  "url": "https://github.com/docsector/docsector-reader/issues"
57
58
  },
58
59
  "scripts": {
59
- "dev": "quasar dev",
60
+ "dev": "npx wrangler pages dev dist/spa",
60
61
  "build": "quasar build",
61
62
  "lint": "eslint --ext .js,.vue ./",
62
63
  "format": "prettier --write \"**/*.{js,vue,scss,html,md,json}\" --ignore-path .gitignore",
package/public/robots.txt CHANGED
@@ -1,2 +1,6 @@
1
1
  User-agent: *
2
2
  Allow: /
3
+ Sitemap: /sitemap.xml
4
+
5
+ User-agent: Cloudflare-AI-Search
6
+ Allow: /
@@ -0,0 +1,91 @@
1
+ export const DEFAULT_ASSISTANT_ENDPOINT = '/assistant'
2
+ export const DEFAULT_ASSISTANT_PROVIDER = 'aiSearch'
3
+ export const DEFAULT_ASSISTANT_DRAWER_WIDTH = 380
4
+ export const DEFAULT_ASSISTANT_WIDE_BREAKPOINT = 1280
5
+
6
+ const DEFAULT_SUGGESTED_PROMPTS = [
7
+ 'How do I get started?',
8
+ 'Summarize this page.',
9
+ 'Where is the related API reference?'
10
+ ]
11
+
12
+ function toBoolean (value, fallback = false) {
13
+ if (typeof value === 'boolean') return value
14
+ return fallback
15
+ }
16
+
17
+ function toPositiveInteger (value, fallback, { min = 1, max = Number.MAX_SAFE_INTEGER } = {}) {
18
+ const number = Number(value)
19
+ if (!Number.isFinite(number)) return fallback
20
+ return Math.min(max, Math.max(min, Math.round(number)))
21
+ }
22
+
23
+ function toBoundedNumber (value, fallback, { min = 0, max = 1 } = {}) {
24
+ const number = Number(value)
25
+ if (!Number.isFinite(number)) return fallback
26
+ return Math.min(max, Math.max(min, number))
27
+ }
28
+
29
+ function toCleanString (value, fallback = '') {
30
+ if (typeof value !== 'string') return fallback
31
+ const trimmed = value.trim()
32
+ return trimmed || fallback
33
+ }
34
+
35
+ function normalizeSuggestedPrompts (value) {
36
+ const prompts = Array.isArray(value) ? value : DEFAULT_SUGGESTED_PROMPTS
37
+ const normalized = prompts
38
+ .map(prompt => toCleanString(prompt))
39
+ .filter(Boolean)
40
+ .slice(0, 6)
41
+
42
+ return normalized.length > 0 ? normalized : [...DEFAULT_SUGGESTED_PROMPTS]
43
+ }
44
+
45
+ export function isAssistantEnabled (config = {}) {
46
+ return config?.aiAssistant?.enabled === true
47
+ }
48
+
49
+ export function normalizeAiAssistantConfig (config = {}) {
50
+ const assistant = config.aiAssistant || {}
51
+ const provider = toCleanString(assistant.provider, DEFAULT_ASSISTANT_PROVIDER)
52
+ const aiSearch = assistant.aiSearch || {}
53
+ const ui = assistant.ui || {}
54
+
55
+ return {
56
+ enabled: assistant.enabled === true,
57
+ provider,
58
+ endpoint: toCleanString(assistant.endpoint, DEFAULT_ASSISTANT_ENDPOINT),
59
+ ui: {
60
+ title: toCleanString(ui.title, 'Docsector Assistant'),
61
+ subtitle: toCleanString(ui.subtitle, 'Ask, search, or explain the docs.'),
62
+ drawerWidth: toPositiveInteger(ui.drawerWidth, DEFAULT_ASSISTANT_DRAWER_WIDTH, { min: 320, max: 520 }),
63
+ wideBreakpoint: toPositiveInteger(ui.wideBreakpoint, DEFAULT_ASSISTANT_WIDE_BREAKPOINT, { min: 960, max: 2400 }),
64
+ showCitations: toBoolean(ui.showCitations, true),
65
+ suggestedPrompts: normalizeSuggestedPrompts(ui.suggestedPrompts)
66
+ },
67
+ aiSearch: {
68
+ binding: toCleanString(aiSearch.binding, 'AI_SEARCH'),
69
+ instanceName: toCleanString(aiSearch.instanceName || aiSearch.instanceId, ''),
70
+ instanceNameEnv: toCleanString(aiSearch.instanceNameEnv, 'AI_SEARCH_INSTANCE_NAME'),
71
+ namespace: toCleanString(aiSearch.namespace, ''),
72
+ accountIdEnv: toCleanString(aiSearch.accountIdEnv, 'CLOUDFLARE_ACCOUNT_ID'),
73
+ apiTokenEnv: toCleanString(aiSearch.apiTokenEnv, 'CLOUDFLARE_API_TOKEN'),
74
+ model: toCleanString(aiSearch.model, '@cf/meta/llama-3.3-70b-instruct-fp8-fast'),
75
+ retrievalType: toCleanString(aiSearch.retrievalType, 'hybrid'),
76
+ maxResults: toPositiveInteger(aiSearch.maxResults, 6, { min: 1, max: 50 }),
77
+ matchThreshold: toBoundedNumber(aiSearch.matchThreshold, 0.4),
78
+ contextExpansion: toPositiveInteger(aiSearch.contextExpansion, 1, { min: 0, max: 3 }),
79
+ queryRewrite: {
80
+ enabled: toBoolean(aiSearch.queryRewrite?.enabled, true),
81
+ model: toCleanString(aiSearch.queryRewrite?.model, '')
82
+ },
83
+ reranking: {
84
+ enabled: toBoolean(aiSearch.reranking?.enabled, false),
85
+ model: toCleanString(aiSearch.reranking?.model, '@cf/baai/bge-reranker-base'),
86
+ matchThreshold: toBoundedNumber(aiSearch.reranking?.matchThreshold, 0.4)
87
+ },
88
+ stream: aiSearch.stream !== false
89
+ }
90
+ }
91
+ }
@@ -0,0 +1,50 @@
1
+ function escapeXml (value) {
2
+ return String(value || '')
3
+ .replace(/&/g, '&')
4
+ .replace(/</g, '&lt;')
5
+ .replace(/>/g, '&gt;')
6
+ .replace(/"/g, '&quot;')
7
+ .replace(/'/g, '&apos;')
8
+ }
9
+
10
+ export function createAiSearchIndexArtifacts ({ siteUrl = '', entries = [], generatedAt = new Date().toISOString() } = {}) {
11
+ const baseUrl = String(siteUrl || '').replace(/\/+$/g, '')
12
+
13
+ const pages = (Array.isArray(entries) ? entries : [])
14
+ .filter(entry => entry && entry.path && entry.markdownPath)
15
+ .map(entry => {
16
+ const markdownPath = String(entry.markdownPath).replace(/^\/+/, '')
17
+ const routePath = String(entry.path).replace(/^\/+/, '')
18
+ const markdownUrl = baseUrl ? `${baseUrl}/${markdownPath}` : `/${markdownPath}`
19
+ const url = baseUrl ? `${baseUrl}/${routePath}` : `/${routePath}`
20
+ return {
21
+ title: entry.title || routePath,
22
+ path: routePath,
23
+ markdownUrl,
24
+ url,
25
+ locale: entry.locale || '',
26
+ book: entry.book || '',
27
+ version: entry.version || '',
28
+ subpage: entry.subpage || ''
29
+ }
30
+ })
31
+
32
+ const sitemapUrls = pages.map(page => [
33
+ ' <url>',
34
+ ` <loc>${escapeXml(page.markdownUrl)}</loc>`,
35
+ ` <lastmod>${escapeXml(generatedAt.slice(0, 10))}</lastmod>`,
36
+ ' </url>'
37
+ ].join('\n')).join('\n')
38
+
39
+ const sitemap = pages.length > 0
40
+ ? `<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${sitemapUrls}\n</urlset>\n`
41
+ : ''
42
+
43
+ return {
44
+ manifest: {
45
+ generatedAt,
46
+ pages
47
+ },
48
+ sitemap
49
+ }
50
+ }
@@ -0,0 +1,56 @@
1
+ export const DEFAULT_TOC_WIDTH = 308
2
+ export const DEFAULT_RIGHT_GAP = 24
3
+ export const DEFAULT_MIN_CONTENT_WIDTH = 680
4
+ export const DEFAULT_MOBILE_BREAKPOINT = 768
5
+
6
+ function toWidth (value, fallback) {
7
+ const number = Number(value)
8
+ if (!Number.isFinite(number)) return fallback
9
+ return Math.max(0, Math.round(number))
10
+ }
11
+
12
+ export function getAssistantRightRailState ({
13
+ tocOpen = false,
14
+ assistantOpen = false,
15
+ screenWidth = 1440,
16
+ tocWidth = DEFAULT_TOC_WIDTH,
17
+ assistantWidth = 380,
18
+ gap = DEFAULT_RIGHT_GAP,
19
+ minContentWidth = DEFAULT_MIN_CONTENT_WIDTH,
20
+ mobileBreakpoint = DEFAULT_MOBILE_BREAKPOINT
21
+ } = {}) {
22
+ const width = toWidth(screenWidth, 1440)
23
+ const isMobile = width < mobileBreakpoint
24
+ const normalizedTocWidth = toWidth(tocWidth, DEFAULT_TOC_WIDTH)
25
+ const normalizedAssistantWidth = toWidth(assistantWidth, 380)
26
+ const normalizedGap = toWidth(gap, DEFAULT_RIGHT_GAP)
27
+
28
+ if (isMobile) {
29
+ return {
30
+ isMobile,
31
+ showToc: tocOpen,
32
+ showAssistant: assistantOpen,
33
+ tocWidth: 0,
34
+ assistantWidth: 0,
35
+ totalWidth: 0,
36
+ backToTopRightOffset: `${normalizedGap}px`
37
+ }
38
+ }
39
+
40
+ const requestedWidth = (tocOpen ? normalizedTocWidth : 0) + (assistantOpen ? normalizedAssistantWidth : 0)
41
+ const canShowBoth = !tocOpen || !assistantOpen || (width - requestedWidth >= minContentWidth)
42
+ const showToc = tocOpen && (canShowBoth || !assistantOpen)
43
+ const showAssistant = assistantOpen
44
+ const totalWidth = (showToc ? normalizedTocWidth : 0) + (showAssistant ? normalizedAssistantWidth : 0)
45
+ const backOffset = totalWidth > 0 ? totalWidth + normalizedGap : normalizedGap
46
+
47
+ return {
48
+ isMobile,
49
+ showToc,
50
+ showAssistant,
51
+ tocWidth: showToc ? normalizedTocWidth : 0,
52
+ assistantWidth: showAssistant ? normalizedAssistantWidth : 0,
53
+ totalWidth,
54
+ backToTopRightOffset: `${backOffset}px`
55
+ }
56
+ }
@@ -0,0 +1,41 @@
1
+ const VALID_ROLES = new Set(['system', 'developer', 'user', 'assistant', 'tool'])
2
+
3
+ function cleanContent (value, maxLength) {
4
+ const content = String(value || '').replace(/\s+$/g, '')
5
+ if (!Number.isFinite(maxLength) || maxLength <= 0 || content.length <= maxLength) {
6
+ return content
7
+ }
8
+
9
+ return content.slice(0, maxLength).trimEnd()
10
+ }
11
+
12
+ export function normalizeAssistantMessages (messages = [], { maxMessages = 12, maxContentLength = 4000 } = {}) {
13
+ return (Array.isArray(messages) ? messages : [])
14
+ .map(message => {
15
+ const role = VALID_ROLES.has(message?.role) ? message.role : 'user'
16
+ const content = cleanContent(message?.content, maxContentLength)
17
+ return content ? { role, content } : null
18
+ })
19
+ .filter(Boolean)
20
+ .slice(-Math.max(1, maxMessages))
21
+ }
22
+
23
+ export function createAssistantRequestPayload ({ messages = [], route, locale, context } = {}, options = {}) {
24
+ const normalizedMessages = normalizeAssistantMessages(messages, options)
25
+ const path = typeof route?.path === 'string' ? route.path : ''
26
+ const hash = typeof route?.hash === 'string' ? route.hash : ''
27
+
28
+ return {
29
+ messages: normalizedMessages,
30
+ locale: typeof locale === 'string' && locale.trim() ? locale.trim() : 'en-US',
31
+ route: {
32
+ path,
33
+ hash
34
+ },
35
+ context: {
36
+ title: typeof context?.title === 'string' ? context.title.trim() : '',
37
+ markdownUrl: typeof context?.markdownUrl === 'string' ? context.markdownUrl.trim() : '',
38
+ selectedText: cleanContent(context?.selectedText, options.maxSelectedTextLength || 1200)
39
+ }
40
+ }
41
+ }
@@ -0,0 +1,22 @@
1
+ export function hasAssistantMessageContent(message) {
2
+ return String(message?.content || '').trim().length > 0
3
+ }
4
+
5
+ export function listVisibleAssistantMessages(messages = []) {
6
+ return messages.filter((message) => {
7
+ if (message?.role !== 'assistant') {
8
+ return true
9
+ }
10
+
11
+ return hasAssistantMessageContent(message)
12
+ })
13
+ }
14
+
15
+ export function isAssistantThinkingState({ loading = false, messages = [] } = {}) {
16
+ if (!loading) {
17
+ return false
18
+ }
19
+
20
+ const lastMessage = messages[messages.length - 1]
21
+ return Boolean(lastMessage && lastMessage.role === 'assistant' && !hasAssistantMessageContent(lastMessage))
22
+ }