@stackql/docusaurus-plugin-aeo 0.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.
@@ -0,0 +1,55 @@
1
+ // Hand-rolled minimal SVGs for the four supported AI providers. Glyphs only;
2
+ // outer wrapper supplies size and color. Using `currentColor` so the icon
3
+ // inherits theme colors via CSS.
4
+
5
+ const React = require('react');
6
+
7
+ function svg(children, viewBox = '0 0 24 24') {
8
+ return React.createElement(
9
+ 'svg',
10
+ {
11
+ xmlns: 'http://www.w3.org/2000/svg',
12
+ width: '1em',
13
+ height: '1em',
14
+ viewBox,
15
+ fill: 'currentColor',
16
+ 'aria-hidden': 'true',
17
+ },
18
+ children,
19
+ );
20
+ }
21
+
22
+ const ClaudeIcon = () =>
23
+ svg(
24
+ React.createElement('path', {
25
+ d: 'M5.5 17.5 9.6 6.6h2.6l4.1 10.9h-2.4l-.9-2.5h-3.9l-.9 2.5H5.5Zm4.6-4.5h2.8l-1.4-4-1.4 4Zm6.4 4.5V6.6h2.2v10.9h-2.2Z',
26
+ }),
27
+ );
28
+
29
+ const ChatGptIcon = () =>
30
+ svg(
31
+ React.createElement('path', {
32
+ d: 'M12 2a10 10 0 1 0 10 10A10 10 0 0 0 12 2Zm4.6 12.4-3.9 2.3a1.5 1.5 0 0 1-1.4 0L7.4 14.4a1.5 1.5 0 0 1-.7-1.3V8.6a1.5 1.5 0 0 1 .7-1.3l3.9-2.3a1.5 1.5 0 0 1 1.4 0l3.9 2.3a1.5 1.5 0 0 1 .7 1.3v4.5a1.5 1.5 0 0 1-.7 1.3ZM12 8.5l-3.2 1.8v3.4L12 15.5l3.2-1.8v-3.4Z',
33
+ }),
34
+ );
35
+
36
+ const PerplexityIcon = () =>
37
+ svg(
38
+ React.createElement('path', {
39
+ d: 'M12 2 3 7v10l9 5 9-5V7l-9-5Zm0 2.3 6.7 3.7L12 11.7 5.3 8 12 4.3ZM5 9.7l6 3.3v6.6l-6-3.3V9.7Zm14 0v6.6l-6 3.3V13l6-3.3Z',
40
+ }),
41
+ );
42
+
43
+ const GeminiIcon = () =>
44
+ svg(
45
+ React.createElement('path', {
46
+ d: 'M12 2 9.5 9.5 2 12l7.5 2.5L12 22l2.5-7.5L22 12l-7.5-2.5L12 2Z',
47
+ }),
48
+ );
49
+
50
+ module.exports = {
51
+ claude: ClaudeIcon,
52
+ chatgpt: ChatGptIcon,
53
+ perplexity: PerplexityIcon,
54
+ gemini: GeminiIcon,
55
+ };
@@ -0,0 +1,116 @@
1
+ import React, { useCallback, useEffect, useRef, useState } from 'react';
2
+ import { useLocation } from '@docusaurus/router';
3
+ import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
4
+ import { usePluginData } from '@docusaurus/useGlobalData';
5
+ import icons from './icons.js';
6
+ import styles from './styles.module.css';
7
+
8
+ const PROVIDER_LABELS = {
9
+ claude: 'Ask Claude',
10
+ chatgpt: 'Ask ChatGPT',
11
+ perplexity: 'Ask Perplexity',
12
+ gemini: 'Ask Gemini',
13
+ };
14
+
15
+ const PROVIDER_URLS = {
16
+ claude: 'https://claude.ai/new?q=',
17
+ chatgpt: 'https://chatgpt.com/?q=',
18
+ perplexity: 'https://www.perplexity.ai/search?q=',
19
+ gemini: 'https://gemini.google.com/app?q=',
20
+ };
21
+
22
+ function buildPrompt(template, pageUrl) {
23
+ return template.replace('{pageUrl}', pageUrl);
24
+ }
25
+
26
+ export default function AskAiButton(props) {
27
+ const { siteConfig } = useDocusaurusContext();
28
+ const data = usePluginData('@stackql/docusaurus-plugin-aeo') || {};
29
+ const cfg = data.askAi || {};
30
+ const location = useLocation();
31
+
32
+ const [open, setOpen] = useState(false);
33
+ const wrapperRef = useRef(null);
34
+
35
+ useEffect(() => {
36
+ function onDocClick(e) {
37
+ if (wrapperRef.current && !wrapperRef.current.contains(e.target)) {
38
+ setOpen(false);
39
+ }
40
+ }
41
+ function onEsc(e) {
42
+ if (e.key === 'Escape') setOpen(false);
43
+ }
44
+ if (open) {
45
+ document.addEventListener('mousedown', onDocClick);
46
+ document.addEventListener('keydown', onEsc);
47
+ }
48
+ return () => {
49
+ document.removeEventListener('mousedown', onDocClick);
50
+ document.removeEventListener('keydown', onEsc);
51
+ };
52
+ }, [open]);
53
+
54
+ const toggle = useCallback(() => setOpen((v) => !v), []);
55
+
56
+ if (cfg.enabled === false) return null;
57
+
58
+ const baseUrl = (siteConfig.url || '').replace(/\/$/, '');
59
+ const pageUrl = `${baseUrl}${location.pathname.replace(/\/$/, '') || ''}`;
60
+ const promptTemplate =
61
+ cfg.promptTemplate ||
62
+ 'Read {pageUrl}.md and help me with the following question about it: ';
63
+ const targetUrl = cfg.companionsEnabled === false
64
+ ? pageUrl
65
+ : pageUrl; // template controls .md suffix; pageUrl is the HTML route
66
+ const prompt = buildPrompt(promptTemplate, targetUrl);
67
+ const encoded = encodeURIComponent(prompt);
68
+
69
+ const order =
70
+ Array.isArray(cfg.providerOrder) && cfg.providerOrder.length > 0
71
+ ? cfg.providerOrder
72
+ : ['claude', 'chatgpt', 'perplexity', 'gemini'];
73
+
74
+ return (
75
+ <div className={styles.wrapper} ref={wrapperRef}>
76
+ <button
77
+ type="button"
78
+ className={styles.trigger}
79
+ aria-haspopup="menu"
80
+ aria-expanded={open}
81
+ onClick={toggle}
82
+ >
83
+ <span>Ask AI about this page</span>
84
+ <span className={styles.caret}>{open ? '▲' : '▼'}</span>
85
+ </button>
86
+ {open && (
87
+ <ul className={styles.menu} role="menu">
88
+ {order.map((key) => {
89
+ const Icon = icons[key];
90
+ const label = PROVIDER_LABELS[key] || key;
91
+ const base = PROVIDER_URLS[key];
92
+ if (!base || !Icon) return null;
93
+ const href = `${base}${encoded}`;
94
+ return (
95
+ <li key={key} className={styles.item} role="none">
96
+ <a
97
+ role="menuitem"
98
+ className={styles.itemLink}
99
+ href={href}
100
+ target="_blank"
101
+ rel="noopener noreferrer"
102
+ onClick={() => setOpen(false)}
103
+ >
104
+ <span className={styles.icon}>
105
+ <Icon />
106
+ </span>
107
+ <span>{label}</span>
108
+ </a>
109
+ </li>
110
+ );
111
+ })}
112
+ </ul>
113
+ )}
114
+ </div>
115
+ );
116
+ }
@@ -0,0 +1,74 @@
1
+ .wrapper {
2
+ position: relative;
3
+ display: inline-block;
4
+ margin: 1.5rem 0;
5
+ }
6
+
7
+ .trigger {
8
+ display: inline-flex;
9
+ align-items: center;
10
+ gap: 0.5rem;
11
+ padding: 0.5rem 1rem;
12
+ background: var(--ifm-color-primary);
13
+ color: var(--ifm-color-primary-contrast-foreground, #fff);
14
+ border: 1px solid var(--ifm-color-primary-dark);
15
+ border-radius: var(--ifm-button-border-radius, 0.4rem);
16
+ font: inherit;
17
+ font-weight: 600;
18
+ cursor: pointer;
19
+ transition: background-color 0.15s ease, transform 0.05s ease;
20
+ }
21
+
22
+ .trigger:hover {
23
+ background: var(--ifm-color-primary-dark);
24
+ }
25
+
26
+ .trigger:active {
27
+ transform: translateY(1px);
28
+ }
29
+
30
+ .caret {
31
+ font-size: 0.7em;
32
+ opacity: 0.85;
33
+ }
34
+
35
+ .menu {
36
+ position: absolute;
37
+ top: calc(100% + 0.25rem);
38
+ left: 0;
39
+ z-index: 50;
40
+ min-width: 12rem;
41
+ margin: 0;
42
+ padding: 0.25rem 0;
43
+ list-style: none;
44
+ background: var(--ifm-background-surface-color, var(--ifm-background-color));
45
+ border: 1px solid var(--ifm-color-emphasis-300);
46
+ border-radius: var(--ifm-button-border-radius, 0.4rem);
47
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
48
+ }
49
+
50
+ .item {
51
+ margin: 0;
52
+ }
53
+
54
+ .itemLink {
55
+ display: flex;
56
+ align-items: center;
57
+ gap: 0.6rem;
58
+ padding: 0.5rem 0.85rem;
59
+ color: var(--ifm-font-color-base);
60
+ text-decoration: none;
61
+ font-size: 0.95em;
62
+ }
63
+
64
+ .itemLink:hover {
65
+ background: var(--ifm-color-emphasis-100);
66
+ text-decoration: none;
67
+ }
68
+
69
+ .icon {
70
+ display: inline-flex;
71
+ align-items: center;
72
+ font-size: 1.1em;
73
+ color: var(--ifm-color-primary);
74
+ }
@@ -0,0 +1,12 @@
1
+ import React from 'react';
2
+ import Footer from '@theme-original/BlogPostItem/Footer';
3
+ import AskAiButton from '@theme/AskAiButton';
4
+
5
+ export default function FooterWrapper(props) {
6
+ return (
7
+ <>
8
+ <AskAiButton />
9
+ <Footer {...props} />
10
+ </>
11
+ );
12
+ }
@@ -0,0 +1,12 @@
1
+ import React from 'react';
2
+ import Footer from '@theme-original/DocItem/Footer';
3
+ import AskAiButton from '@theme/AskAiButton';
4
+
5
+ export default function FooterWrapper(props) {
6
+ return (
7
+ <>
8
+ <AskAiButton />
9
+ <Footer {...props} />
10
+ </>
11
+ );
12
+ }