@stackql/docusaurus-plugin-aeo 0.4.1 → 0.5.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/CHANGELOG.md +48 -0
- package/README.md +119 -48
- package/package.json +18 -6
- package/src/askAiPrompt.js +21 -0
- package/src/companionPath.js +55 -0
- package/src/features/aiRoutes.js +3 -0
- package/src/features/alternateLinks.js +100 -0
- package/src/features/companions.js +84 -68
- package/src/features/llmsTxt.js +121 -53
- package/src/features/plainMarkdown.js +623 -0
- package/src/index.js +69 -12
- package/src/theme/AskAiButton/brand-icons/ChatGpt.jsx +26 -0
- package/src/theme/AskAiButton/brand-icons/Claude.jsx +25 -0
- package/src/theme/AskAiButton/brand-icons/Perplexity.jsx +23 -0
- package/src/theme/AskAiButton/icons.js +19 -38
- package/src/theme/AskAiButton/index.jsx +31 -7
package/src/index.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
const path = require('path');
|
|
2
2
|
const emitCompanions = require('./features/companions');
|
|
3
|
+
const { collectCompanionItems } = require('./features/companions');
|
|
3
4
|
const emitLlmsTxt = require('./features/llmsTxt');
|
|
5
|
+
const injectAlternateLinks = require('./features/alternateLinks');
|
|
4
6
|
const { validateAiRoutes } = require('./features/aiRoutes');
|
|
5
7
|
|
|
6
8
|
const DEFAULT_LLMS_EXCLUDE = [
|
|
@@ -12,10 +14,10 @@ const DEFAULT_LLMS_EXCLUDE = [
|
|
|
12
14
|
'/blog/authors/**',
|
|
13
15
|
];
|
|
14
16
|
|
|
15
|
-
const DEFAULT_PROVIDER_ORDER = ['claude', 'chatgpt', 'perplexity'
|
|
17
|
+
const DEFAULT_PROVIDER_ORDER = ['claude', 'chatgpt', 'perplexity'];
|
|
16
18
|
const VALID_PROVIDERS = new Set(DEFAULT_PROVIDER_ORDER);
|
|
17
19
|
const DEFAULT_PROMPT_TEMPLATE =
|
|
18
|
-
'Read {
|
|
20
|
+
'Read {companionUrl} and help me understand it. Summarize the key points, then ask me one clarifying question to dig deeper.';
|
|
19
21
|
|
|
20
22
|
function normalizeOptions(raw) {
|
|
21
23
|
const opts = raw || {};
|
|
@@ -28,8 +30,13 @@ function normalizeOptions(raw) {
|
|
|
28
30
|
return {
|
|
29
31
|
companions: {
|
|
30
32
|
enabled: companions.enabled !== false,
|
|
31
|
-
format: companions.format || '
|
|
33
|
+
format: companions.format || 'plain',
|
|
32
34
|
exclude: companions.exclude || [],
|
|
35
|
+
// <link rel="alternate" type="text/markdown"> in every page that has
|
|
36
|
+
// a companion, and optionally the same as a Link header via a
|
|
37
|
+
// Netlify-style _headers file.
|
|
38
|
+
alternateLink: companions.alternateLink !== false,
|
|
39
|
+
linkHeader: companions.linkHeader === true,
|
|
33
40
|
},
|
|
34
41
|
llmsTxt: {
|
|
35
42
|
enabled: llmsTxt.enabled !== false,
|
|
@@ -45,7 +52,10 @@ function normalizeOptions(raw) {
|
|
|
45
52
|
// When set, supersedes `sections` and switches feature 2 from
|
|
46
53
|
// per-type grouping to per-instance grouping.
|
|
47
54
|
instanceSections: llmsTxt.instanceSections || null,
|
|
48
|
-
|
|
55
|
+
// true (everything), false, or { include: ['<plugin>@<id>'], maxBytes }
|
|
56
|
+
fullTxt: llmsTxt.fullTxt === undefined ? true : llmsTxt.fullTxt,
|
|
57
|
+
// "## Optional" section in llms.txt pointing at llms-full.txt
|
|
58
|
+
linkFullTxt: llmsTxt.linkFullTxt !== false,
|
|
49
59
|
},
|
|
50
60
|
askAi: {
|
|
51
61
|
enabled: askAi.enabled !== false,
|
|
@@ -63,11 +73,33 @@ function normalizeOptions(raw) {
|
|
|
63
73
|
function validateOptions(opts) {
|
|
64
74
|
const errs = [];
|
|
65
75
|
|
|
66
|
-
if (!['raw', 'plain'].includes(opts.companions.format)) {
|
|
76
|
+
if (!['raw', 'plain', 'clean'].includes(opts.companions.format)) {
|
|
67
77
|
errs.push(
|
|
68
|
-
`companions.format must be "
|
|
78
|
+
`companions.format must be "plain" (or its synonym "clean") or "raw", got "${opts.companions.format}"`,
|
|
69
79
|
);
|
|
70
80
|
}
|
|
81
|
+
if (typeof opts.companions.alternateLink !== 'boolean') {
|
|
82
|
+
errs.push('companions.alternateLink must be a boolean');
|
|
83
|
+
}
|
|
84
|
+
if (typeof opts.companions.linkHeader !== 'boolean') {
|
|
85
|
+
errs.push('companions.linkHeader must be a boolean');
|
|
86
|
+
}
|
|
87
|
+
const ft = opts.llmsTxt.fullTxt;
|
|
88
|
+
if (typeof ft !== 'boolean') {
|
|
89
|
+
if (!ft || typeof ft !== 'object' || Array.isArray(ft)) {
|
|
90
|
+
errs.push('llmsTxt.fullTxt must be a boolean or an object { include?: string[], maxBytes?: number }');
|
|
91
|
+
} else {
|
|
92
|
+
if (ft.include !== undefined && (!Array.isArray(ft.include) || ft.include.some((k) => typeof k !== 'string'))) {
|
|
93
|
+
errs.push('llmsTxt.fullTxt.include must be an array of "<plugin>@<id>" strings');
|
|
94
|
+
}
|
|
95
|
+
if (ft.maxBytes !== undefined && (!Number.isFinite(ft.maxBytes) || ft.maxBytes <= 0)) {
|
|
96
|
+
errs.push('llmsTxt.fullTxt.maxBytes must be a positive number');
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (typeof opts.llmsTxt.linkFullTxt !== 'boolean') {
|
|
101
|
+
errs.push('llmsTxt.linkFullTxt must be a boolean');
|
|
102
|
+
}
|
|
71
103
|
if (!Array.isArray(opts.companions.exclude)) {
|
|
72
104
|
errs.push('companions.exclude must be an array of glob patterns');
|
|
73
105
|
}
|
|
@@ -117,8 +149,12 @@ function validateOptions(opts) {
|
|
|
117
149
|
} else {
|
|
118
150
|
for (const p of opts.askAi.providerOrder) {
|
|
119
151
|
if (!VALID_PROVIDERS.has(p)) {
|
|
152
|
+
const hint =
|
|
153
|
+
p === 'gemini'
|
|
154
|
+
? '. Gemini was removed in 0.4.2 because gemini.google.com does not accept URL-encoded prompts'
|
|
155
|
+
: '';
|
|
120
156
|
errs.push(
|
|
121
|
-
`askAi.providerOrder contains unknown provider "${p}". Valid: ${[...VALID_PROVIDERS].join(', ')}`,
|
|
157
|
+
`askAi.providerOrder contains unknown provider "${p}"${hint}. Valid providers: ${[...VALID_PROVIDERS].join(', ')}`,
|
|
122
158
|
);
|
|
123
159
|
}
|
|
124
160
|
}
|
|
@@ -156,10 +192,10 @@ module.exports = function pluginAeo(context, rawOptions) {
|
|
|
156
192
|
const plugin = {
|
|
157
193
|
name: '@stackql/docusaurus-plugin-aeo',
|
|
158
194
|
|
|
159
|
-
// Surface the askAi config to theme components
|
|
160
|
-
//
|
|
161
|
-
//
|
|
162
|
-
//
|
|
195
|
+
// Surface the askAi config to theme components, which read it via
|
|
196
|
+
// usePluginData('@stackql/docusaurus-plugin-aeo') at render time. The
|
|
197
|
+
// companion route list is added from allContentLoaded below; Docusaurus
|
|
198
|
+
// merges global data set from the two hooks (shallow, per plugin).
|
|
163
199
|
async contentLoaded({ actions }) {
|
|
164
200
|
await actions.setGlobalData({
|
|
165
201
|
askAi: {
|
|
@@ -176,7 +212,7 @@ module.exports = function pluginAeo(context, rawOptions) {
|
|
|
176
212
|
// allContentLoaded in Docusaurus 3.x. contentLoaded receives only the
|
|
177
213
|
// current plugin's own content, so feature 1 needs this hook to see
|
|
178
214
|
// the docs/blog source files it has to mirror.
|
|
179
|
-
async allContentLoaded({ allContent }) {
|
|
215
|
+
async allContentLoaded({ allContent, actions }) {
|
|
180
216
|
if (allContent) {
|
|
181
217
|
for (const [pluginName, byId] of Object.entries(allContent)) {
|
|
182
218
|
if (!byId) continue;
|
|
@@ -196,6 +232,18 @@ module.exports = function pluginAeo(context, rawOptions) {
|
|
|
196
232
|
verbose: options.verbose,
|
|
197
233
|
});
|
|
198
234
|
}
|
|
235
|
+
|
|
236
|
+
// Which routes will have a companion, so the Ask AI button can point
|
|
237
|
+
// at the file and stay hidden on pages without one (generated
|
|
238
|
+
// category indexes, React pages). Same list postBuild emits from.
|
|
239
|
+
if (actions && typeof actions.setGlobalData === 'function') {
|
|
240
|
+
const companionRoutes = options.companions.enabled
|
|
241
|
+
? collectCompanionItems(loadedContentByPlugin, options.companions).map(
|
|
242
|
+
(item) => item.permalink,
|
|
243
|
+
)
|
|
244
|
+
: [];
|
|
245
|
+
await actions.setGlobalData({ companionRoutes });
|
|
246
|
+
}
|
|
199
247
|
},
|
|
200
248
|
|
|
201
249
|
async postBuild(props) {
|
|
@@ -212,6 +260,15 @@ module.exports = function pluginAeo(context, rawOptions) {
|
|
|
212
260
|
console.log('[plugin-aeo] companions disabled, skipping feature 1');
|
|
213
261
|
}
|
|
214
262
|
|
|
263
|
+
if (options.companions.enabled && options.companions.alternateLink) {
|
|
264
|
+
await injectAlternateLinks({
|
|
265
|
+
props,
|
|
266
|
+
emittedCompanions,
|
|
267
|
+
linkHeader: options.companions.linkHeader,
|
|
268
|
+
verbose: options.verbose,
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
|
|
215
272
|
if (options.llmsTxt.enabled) {
|
|
216
273
|
await emitLlmsTxt({
|
|
217
274
|
props,
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// OpenAI / ChatGPT petaled mark. SVG path lifted verbatim from
|
|
2
|
+
// @lobehub/icons (MIT) before the React 19 hard-pin made the
|
|
3
|
+
// runtime install impossible on React 18 hosts. Brand color #000000
|
|
4
|
+
// is hardcoded to match OpenAI's brand guidelines (the black
|
|
5
|
+
// monochrome mark is the canonical one).
|
|
6
|
+
|
|
7
|
+
import React from 'react';
|
|
8
|
+
|
|
9
|
+
const PATH =
|
|
10
|
+
'M9.205 8.658v-2.26c0-.19.072-.333.238-.428l4.543-2.616c.619-.357 1.356-.523 2.117-.523 2.854 0 4.662 2.212 4.662 4.566 0 .167 0 .357-.024.547l-4.71-2.759a.797.797 0 00-.856 0l-5.97 3.473zm10.609 8.8V12.06c0-.333-.143-.57-.429-.737l-5.97-3.473 1.95-1.118a.433.433 0 01.476 0l4.543 2.617c1.309.76 2.189 2.378 2.189 3.948 0 1.808-1.07 3.473-2.76 4.163zM7.802 12.703l-1.95-1.142c-.167-.095-.239-.238-.239-.428V5.899c0-2.545 1.95-4.472 4.591-4.472 1 0 1.927.333 2.712.928L8.23 5.067c-.285.166-.428.404-.428.737v6.898zM12 15.128l-2.795-1.57v-3.33L12 8.658l2.795 1.57v3.33L12 15.128zm1.796 7.23c-1 0-1.927-.332-2.712-.927l4.686-2.712c.285-.166.428-.404.428-.737v-6.898l1.974 1.142c.167.095.238.238.238.428v5.233c0 2.545-1.974 4.472-4.614 4.472zm-5.637-5.303l-4.544-2.617c-1.308-.761-2.188-2.378-2.188-3.948A4.482 4.482 0 014.21 6.327v5.423c0 .333.143.571.428.738l5.947 3.449-1.95 1.118a.432.432 0 01-.476 0zm-.262 3.9c-2.688 0-4.662-2.021-4.662-4.519 0-.19.024-.38.047-.57l4.686 2.71c.286.167.571.167.856 0l5.97-3.448v2.26c0 .19-.07.333-.237.428l-4.543 2.616c-.619.357-1.356.523-2.117.523zm5.899 2.83a5.947 5.947 0 005.827-4.756C22.287 18.339 24 15.84 24 13.296c0-1.665-.713-3.282-1.998-4.448.119-.5.19-.999.19-1.498 0-3.401-2.759-5.947-5.946-5.947-.642 0-1.26.095-1.88.31A5.962 5.962 0 0010.205 0a5.947 5.947 0 00-5.827 4.757C1.713 5.447 0 7.945 0 10.49c0 1.666.713 3.283 1.998 4.448-.119.5-.19 1-.19 1.499 0 3.401 2.759 5.946 5.946 5.946.642 0 1.26-.095 1.88-.309a5.96 5.96 0 004.162 1.713z';
|
|
11
|
+
|
|
12
|
+
export default function ChatGptIcon({ size = 18 }) {
|
|
13
|
+
return (
|
|
14
|
+
<svg
|
|
15
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
16
|
+
width={size}
|
|
17
|
+
height={size}
|
|
18
|
+
viewBox="0 0 24 24"
|
|
19
|
+
role="img"
|
|
20
|
+
aria-hidden="true"
|
|
21
|
+
style={{ flex: 'none', lineHeight: 1 }}
|
|
22
|
+
>
|
|
23
|
+
<path d={PATH} fill="#000000" />
|
|
24
|
+
</svg>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Claude wordmark / petal. SVG path lifted verbatim from simple-icons
|
|
2
|
+
// (CC0-1.0) so the mark is faithful to the brand kit. Brand color
|
|
3
|
+
// #D97757 is hardcoded; ListItemIcon's `color: 'inherit'` keeps MUI
|
|
4
|
+
// from tinting it.
|
|
5
|
+
|
|
6
|
+
import React from 'react';
|
|
7
|
+
|
|
8
|
+
const PATH =
|
|
9
|
+
'm4.7144 15.9555 4.7174-2.6471.079-.2307-.079-.1275h-.2307l-.7893-.0486-2.6956-.0729-2.3375-.0971-2.2646-.1214-.5707-.1215-.5343-.7042.0546-.3522.4797-.3218.686.0608 1.5179.1032 2.2767.1578 1.6514.0972 2.4468.255h.3886l.0546-.1579-.1336-.0971-.1032-.0972L6.973 9.8356l-2.55-1.6879-1.3356-.9714-.7225-.4918-.3643-.4614-.1578-1.0078.6557-.7225.8803.0607.2246.0607.8925.686 1.9064 1.4754 2.4893 1.8336.3643.3035.1457-.1032.0182-.0728-.164-.2733-1.3539-2.4467-1.445-2.4893-.6435-1.032-.17-.6194c-.0607-.255-.1032-.4674-.1032-.7285L6.287.1335 6.6997 0l.9957.1336.419.3642.6192 1.4147 1.0018 2.2282 1.5543 3.0296.4553.8985.2429.8318.091.255h.1579v-.1457l.1275-1.706.2368-2.0947.2307-2.6957.0789-.7589.3764-.9107.7468-.4918.5828.2793.4797.686-.0668.4433-.2853 1.8517-.5586 2.9021-.3643 1.9429h.2125l.2429-.2429.9835-1.3053 1.6514-2.0643.7286-.8196.85-.9046.5464-.4311h1.0321l.759 1.1293-.34 1.1657-1.0625 1.3478-.8804 1.1414-1.2628 1.7-.7893 1.36.0729.1093.1882-.0183 2.8535-.607 1.5421-.2794 1.8396-.3157.8318.3886.091.3946-.3278.8075-1.967.4857-2.3072.4614-3.4364.8136-.0425.0304.0486.0607 1.5482.1457.6618.0364h1.621l3.0175.2247.7892.522.4736.6376-.079.4857-1.2142.6193-1.6393-.3886-3.825-.9107-1.3113-.3279h-.1822v.1093l1.0929 1.0686 2.0035 1.8092 2.5075 2.3314.1275.5768-.3218.4554-.34-.0486-2.2039-1.6575-.85-.7468-1.9246-1.621h-.1275v.17l.4432.6496 2.3436 3.5214.1214 1.0807-.17.3521-.6071.2125-.6679-.1214-1.3721-1.9246L14.38 17.959l-1.1414-1.9428-.1397.079-.674 7.2552-.3156.3703-.7286.2793-.6071-.4614-.3218-.7468.3218-1.4753.3886-1.9246.3157-1.53.2853-1.9004.17-.6314-.0121-.0425-.1397.0182-1.4328 1.9672-2.1796 2.9446-1.7243 1.8456-.4128.164-.7164-.3704.0667-.6618.4008-.5889 2.386-3.0357 1.4389-1.882.929-1.0868-.0062-.1579h-.0546l-6.3385 4.1164-1.1293.1457-.4857-.4554.0608-.7467.2307-.2429 1.9064-1.3114Z';
|
|
10
|
+
|
|
11
|
+
export default function ClaudeIcon({ size = 18 }) {
|
|
12
|
+
return (
|
|
13
|
+
<svg
|
|
14
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
15
|
+
width={size}
|
|
16
|
+
height={size}
|
|
17
|
+
viewBox="0 0 24 24"
|
|
18
|
+
role="img"
|
|
19
|
+
aria-hidden="true"
|
|
20
|
+
style={{ flex: 'none', lineHeight: 1 }}
|
|
21
|
+
>
|
|
22
|
+
<path d={PATH} fill="#D97757" />
|
|
23
|
+
</svg>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Perplexity wordmark. SVG path lifted verbatim from simple-icons
|
|
2
|
+
// (CC0-1.0). Brand color #21808D is hardcoded.
|
|
3
|
+
|
|
4
|
+
import React from 'react';
|
|
5
|
+
|
|
6
|
+
const PATH =
|
|
7
|
+
'M22.3977 7.0896h-2.3106V.0676l-7.5094 6.3542V.1577h-1.1554v6.1966L4.4904 0v7.0896H1.6023v10.3976h2.8882V24l6.932-6.3591v6.2005h1.1554v-6.0469l6.9318 6.1807v-6.4879h2.8882V7.0896zm-3.4657-4.531v4.531h-5.355l5.355-4.531zm-13.2862.0676 4.8691 4.4634H5.6458V2.6262zM2.7576 16.332V8.245h7.8476l-6.1149 6.1147v1.9723H2.7576zm2.8882 5.0404v-3.8852h.0001v-2.6488l5.7763-5.7764v7.0111l-5.7764 5.2993zm12.7086.0248-5.7766-5.1509V9.0618l5.7766 5.7766v6.5588zm2.8882-5.0652h-1.733v-1.9723L13.3948 8.245h7.8478v8.087z';
|
|
8
|
+
|
|
9
|
+
export default function PerplexityIcon({ size = 18 }) {
|
|
10
|
+
return (
|
|
11
|
+
<svg
|
|
12
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
13
|
+
width={size}
|
|
14
|
+
height={size}
|
|
15
|
+
viewBox="0 0 24 24"
|
|
16
|
+
role="img"
|
|
17
|
+
aria-hidden="true"
|
|
18
|
+
style={{ flex: 'none', lineHeight: 1 }}
|
|
19
|
+
>
|
|
20
|
+
<path d={PATH} fill="#21808D" />
|
|
21
|
+
</svg>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
@@ -1,45 +1,26 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// tint it with the ChatGPT brand green via inline style.
|
|
1
|
+
// Provider brand icons. Hand-rolled inline SVGs lifted from
|
|
2
|
+
// publicly-available marks (simple-icons paths under CC0-1.0 for
|
|
3
|
+
// Claude and Perplexity; the @lobehub/icons OpenAI Mono path for
|
|
4
|
+
// ChatGPT). Brand color is hardcoded on each path so MUI's
|
|
5
|
+
// ListItemIcon color override doesn't tint them.
|
|
7
6
|
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
import PerplexityIcons from '@lobehub/icons/es/Perplexity';
|
|
17
|
-
import GeminiIcons from '@lobehub/icons/es/Gemini';
|
|
18
|
-
|
|
19
|
-
const ICON_SIZE = 18;
|
|
20
|
-
|
|
21
|
-
// OpenAI ships only the monochrome mark (Color variant absent from
|
|
22
|
-
// @lobehub/icons as of 5.10.0). Use the known ChatGPT brand green so
|
|
23
|
-
// the row visually matches the other three colored marks.
|
|
24
|
-
const CHATGPT_GREEN = '#10A37F';
|
|
7
|
+
// We hand-roll rather than depend on @lobehub/icons because lobehub
|
|
8
|
+
// pinned react@^19 from 5.x onward, breaking React 18 consumers.
|
|
9
|
+
// simple-icons works on any Node + React but no longer ships an
|
|
10
|
+
// OpenAI mark following a 2024 trademark request, so a unified
|
|
11
|
+
// "single library" path is no longer available - hand-rolled is the
|
|
12
|
+
// only zero-conflict option.
|
|
13
|
+
//
|
|
14
|
+
// Bundle cost: ~3KB across the three SVG components combined.
|
|
25
15
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
return function Icon() {
|
|
30
|
-
const props = { size: ICON_SIZE };
|
|
31
|
-
if (usingFallback && fallbackColor) {
|
|
32
|
-
props.style = { color: fallbackColor };
|
|
33
|
-
}
|
|
34
|
-
return React.createElement(Component, props);
|
|
35
|
-
};
|
|
36
|
-
}
|
|
16
|
+
import Claude from './brand-icons/Claude.jsx';
|
|
17
|
+
import ChatGpt from './brand-icons/ChatGpt.jsx';
|
|
18
|
+
import Perplexity from './brand-icons/Perplexity.jsx';
|
|
37
19
|
|
|
38
20
|
const icons = {
|
|
39
|
-
claude:
|
|
40
|
-
chatgpt:
|
|
41
|
-
perplexity:
|
|
42
|
-
gemini: makeColorIcon(GeminiIcons),
|
|
21
|
+
claude: Claude,
|
|
22
|
+
chatgpt: ChatGpt,
|
|
23
|
+
perplexity: Perplexity,
|
|
43
24
|
};
|
|
44
25
|
|
|
45
26
|
export default icons;
|
|
@@ -10,23 +10,29 @@ import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
|
|
10
10
|
import { usePluginData } from '@docusaurus/useGlobalData';
|
|
11
11
|
import icons from './icons.js';
|
|
12
12
|
import styles from './styles.module.css';
|
|
13
|
+
// CommonJS, shared with the Node-side features so the button and the
|
|
14
|
+
// emitted file can never disagree about where the companion lives.
|
|
15
|
+
import companionPathModule from '../../companionPath.js';
|
|
16
|
+
import askAiPromptModule from '../../askAiPrompt.js';
|
|
17
|
+
|
|
18
|
+
const { companionUrl, normalizeRoute } = companionPathModule;
|
|
19
|
+
const { fillPrompt } = askAiPromptModule;
|
|
13
20
|
|
|
14
21
|
const PROVIDER_LABELS = {
|
|
15
22
|
claude: 'Ask Claude',
|
|
16
23
|
chatgpt: 'Ask ChatGPT',
|
|
17
24
|
perplexity: 'Ask Perplexity',
|
|
18
|
-
gemini: 'Ask Gemini',
|
|
19
25
|
};
|
|
20
26
|
|
|
21
27
|
const PROVIDER_URLS = {
|
|
22
28
|
claude: 'https://claude.ai/new?q=',
|
|
23
29
|
chatgpt: 'https://chatgpt.com/?q=',
|
|
24
30
|
perplexity: 'https://www.perplexity.ai/search?q=',
|
|
25
|
-
gemini: 'https://gemini.google.com/app?q=',
|
|
26
31
|
};
|
|
27
32
|
|
|
28
33
|
const DEFAULT_PROMPT_TEMPLATE =
|
|
29
|
-
'Read {
|
|
34
|
+
'Read {companionUrl} and help me understand it. Summarize the key points, then ask me one clarifying question to dig deeper.';
|
|
35
|
+
|
|
30
36
|
|
|
31
37
|
export default function AskAiButton() {
|
|
32
38
|
const { siteConfig } = useDocusaurusContext();
|
|
@@ -39,16 +45,34 @@ export default function AskAiButton() {
|
|
|
39
45
|
|
|
40
46
|
if (cfg.enabled === false) return null;
|
|
41
47
|
|
|
42
|
-
const
|
|
43
|
-
const
|
|
48
|
+
const siteUrl = (siteConfig.url || '').replace(/\/+$/, '');
|
|
49
|
+
const route = normalizeRoute(location.pathname);
|
|
50
|
+
const pageUrl = route === '/' ? siteUrl : `${siteUrl}${route}`;
|
|
51
|
+
|
|
52
|
+
// Only pages with a companion get the button: a generated category index
|
|
53
|
+
// or a React page has no .md file, so a prompt pointing at one would
|
|
54
|
+
// send the reader to a 404. `companionRoutes` arrives from
|
|
55
|
+
// allContentLoaded; if an older core did not merge it, fall back to
|
|
56
|
+
// showing the button everywhere as before. With companions disabled the
|
|
57
|
+
// prompt uses the page itself.
|
|
58
|
+
const companionsEnabled = cfg.companionsEnabled !== false;
|
|
59
|
+
const routes = data.companionRoutes;
|
|
60
|
+
if (companionsEnabled && Array.isArray(routes)) {
|
|
61
|
+
const known = routes.some((r) => normalizeRoute(r) === route);
|
|
62
|
+
if (!known) return null;
|
|
63
|
+
}
|
|
64
|
+
const mdUrl = companionsEnabled
|
|
65
|
+
? companionUrl(siteUrl, route, siteConfig.trailingSlash)
|
|
66
|
+
: pageUrl;
|
|
67
|
+
|
|
44
68
|
const promptTemplate = cfg.promptTemplate || DEFAULT_PROMPT_TEMPLATE;
|
|
45
|
-
const prompt = promptTemplate
|
|
69
|
+
const prompt = fillPrompt(promptTemplate, { pageUrl, companionUrl: mdUrl });
|
|
46
70
|
const encoded = encodeURIComponent(prompt);
|
|
47
71
|
|
|
48
72
|
const order =
|
|
49
73
|
Array.isArray(cfg.providerOrder) && cfg.providerOrder.length > 0
|
|
50
74
|
? cfg.providerOrder
|
|
51
|
-
: ['claude', 'chatgpt', 'perplexity'
|
|
75
|
+
: ['claude', 'chatgpt', 'perplexity'];
|
|
52
76
|
|
|
53
77
|
const handleOpen = (e) => setAnchorEl(e.currentTarget);
|
|
54
78
|
const handleClose = () => setAnchorEl(null);
|