@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.
- package/.env.example +18 -0
- package/README.md +136 -5
- package/bin/docsector.js +36 -1
- package/docsector.config.js +44 -0
- package/package.json +3 -2
- package/public/robots.txt +4 -0
- package/src/ai-assistant/config.js +91 -0
- package/src/ai-assistant/indexing.js +50 -0
- package/src/ai-assistant/layout.js +56 -0
- package/src/ai-assistant/messages.js +41 -0
- package/src/ai-assistant/panel.js +22 -0
- package/src/ai-assistant/server.js +348 -0
- package/src/ai-assistant/session.js +91 -0
- package/src/ai-assistant/stream.js +125 -0
- package/src/components/DAssistantPanel.vue +701 -0
- package/src/components/DPage.vue +114 -4
- package/src/components/DPageAnchor.vue +11 -7
- package/src/components/DPageRichContent.vue +105 -0
- package/src/components/DPageTokens.vue +27 -16
- package/src/components/api-block-model.js +77 -1
- package/src/components/inline-code-copy.js +58 -0
- package/src/components/page-section-tokens.js +6 -4
- package/src/components/quasar-api-extends.json +235 -0
- package/src/composables/useAssistant.js +201 -0
- package/src/i18n/helpers.js +2 -0
- package/src/i18n/languages/en-US.hjson +22 -0
- package/src/i18n/languages/pt-BR.hjson +22 -0
- package/src/layouts/DefaultLayout.vue +22 -0
- package/src/markdown-agent.js +32 -0
- package/src/pages/manual/basic/ai-assistant.overview.en-US.md +69 -0
- package/src/pages/manual/basic/ai-assistant.overview.pt-BR.md +69 -0
- package/src/pages/manual/basic/d-page-anchor.overview.en-US.md +1 -1
- package/src/pages/manual/basic/d-page-anchor.overview.pt-BR.md +1 -1
- package/src/pages/manual.index.js +29 -0
- package/src/quasar.factory.js +166 -33
- package/src/sitemap.js +103 -0
- 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
|
|
30
|
-
- π€ **AI-Friendly robots.txt** β Scaffold includes a `robots.txt` explicitly allowing
|
|
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
|
|
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.
|
|
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')
|
package/docsector.config.js
CHANGED
|
@@ -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
|
+
"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": "
|
|
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
|
@@ -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, '<')
|
|
5
|
+
.replace(/>/g, '>')
|
|
6
|
+
.replace(/"/g, '"')
|
|
7
|
+
.replace(/'/g, ''')
|
|
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
|
+
}
|