@dogsbay/plugin-ask 0.2.0-beta.83 → 0.2.0-beta.86
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/README.md +6 -1
- package/dist/client.js +148 -64
- package/dist/client.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -1
- package/dist/schema.d.ts +22 -0
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +11 -0
- package/dist/schema.js.map +1 -1
- package/package.json +3 -2
- package/src/RightRailAsk.astro +55 -0
- package/src/client.ts +161 -61
- package/src/styles.css +27 -0
package/README.md
CHANGED
|
@@ -4,6 +4,10 @@ A floating "Ask AI" button for Dogsbay docs sites. It opens a modal, sends the
|
|
|
4
4
|
question to a configured backend, and shows the answer — so a static docs site
|
|
5
5
|
can offer AI search without shipping any secrets.
|
|
6
6
|
|
|
7
|
+
Full setup (proxy, AI Search, CI, security): see
|
|
8
|
+
[docs/ask-ai.md](../../docs/ask-ai.md) (site authors) and
|
|
9
|
+
[docs-dev/ask-ai-architecture.md](../../docs-dev/ask-ai-architecture.md) (architecture/ops).
|
|
10
|
+
|
|
7
11
|
## Install
|
|
8
12
|
|
|
9
13
|
```bash
|
|
@@ -33,7 +37,8 @@ plugins:
|
|
|
33
37
|
| `buttonLabel` | no | `Ask AI` | Floating button text. |
|
|
34
38
|
| `title` | no | `Ask the docs` | Modal heading. |
|
|
35
39
|
| `placeholder` | no | `Ask a question…` | Input placeholder. |
|
|
36
|
-
| `position` | no | `bottom-right` | `bottom-right` or `bottom-left`. |
|
|
40
|
+
| `position` | no | `bottom-right` | Floating-button corner: `bottom-right` or `bottom-left`. |
|
|
41
|
+
| `placement` | no | `auto` | `auto` (dock in the docs right rail when present, else float), `rail`, or `floating`. |
|
|
37
42
|
|
|
38
43
|
## Styling
|
|
39
44
|
|
package/dist/client.js
CHANGED
|
@@ -1,20 +1,43 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Client runtime for `@dogsbay/plugin-ask`.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* Astro project, so it runs for side effects on every page. It injects a
|
|
6
|
-
* floating button + <dialog> at <body> level (outside the article), and on
|
|
7
|
-
* submit streams an answer from the dogsbay-ask proxy.
|
|
4
|
+
* Runs for side effects on every page. Two render targets share one code path:
|
|
8
5
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
6
|
+
* - **Docked panel** — a server-rendered shell (`RightRailAsk.astro`,
|
|
7
|
+
* `[data-dba-panel="rail"]`) that DocsLayout places in its `right-rail`
|
|
8
|
+
* slot. Visible on desktop; hidden < lg because the rail is `hidden lg:block`.
|
|
9
|
+
* - **Floating modal** — a body-level button + `<dialog>` built here. Used when
|
|
10
|
+
* there's no rail (floating placement, wide/API pages) and as the mobile
|
|
11
|
+
* entry when a docked panel exists.
|
|
12
|
+
*
|
|
13
|
+
* Astro view transitions swap the document, so we mount on `astro:page-load`
|
|
14
|
+
* and tear the floating widget down on `astro:before-swap` (the docked shell is
|
|
15
|
+
* part of the swapped document, so it's re-wired each navigation).
|
|
12
16
|
*/
|
|
13
17
|
// @ts-expect-error: virtual module resolved by the Dogsbay Vite alias at build.
|
|
14
18
|
import config from "virtual:dogsbay-plugin-config/@dogsbay/plugin-ask";
|
|
15
19
|
import { buildAskRequest, extractText, parseSources } from "./protocol.js";
|
|
16
20
|
import { renderMarkdown, escapeHtml } from "./md.js";
|
|
17
21
|
const cfg = config;
|
|
22
|
+
// Turnstile token (refreshed per ask). Populated by the widget callbacks.
|
|
23
|
+
let turnstileToken = null;
|
|
24
|
+
let turnstileScriptLoaded = false;
|
|
25
|
+
function turnstileApi() {
|
|
26
|
+
return window.turnstile;
|
|
27
|
+
}
|
|
28
|
+
function loadTurnstile() {
|
|
29
|
+
if (turnstileScriptLoaded || !cfg.turnstileSitekey)
|
|
30
|
+
return;
|
|
31
|
+
turnstileScriptLoaded = true;
|
|
32
|
+
const w = window;
|
|
33
|
+
w.__dbaTsCb = (t) => { turnstileToken = t; };
|
|
34
|
+
w.__dbaTsExpired = () => { turnstileToken = null; };
|
|
35
|
+
const s = document.createElement("script");
|
|
36
|
+
s.src = "https://challenges.cloudflare.com/turnstile/v0/api.js";
|
|
37
|
+
s.async = true;
|
|
38
|
+
s.defer = true;
|
|
39
|
+
document.head.appendChild(s);
|
|
40
|
+
}
|
|
18
41
|
function sourcesHtml(sources) {
|
|
19
42
|
const items = sources
|
|
20
43
|
.map((s) => s.href
|
|
@@ -23,15 +46,110 @@ function sourcesHtml(sources) {
|
|
|
23
46
|
.join("");
|
|
24
47
|
return `<div class="dba-sources"><div class="dba-sources-title">Sources</div><ul>${items}</ul></div>`;
|
|
25
48
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
49
|
+
/**
|
|
50
|
+
* The one ask flow: POST the question, stream the answer, render Markdown +
|
|
51
|
+
* a Sources list. Shared by the docked panel and the floating dialog.
|
|
52
|
+
*/
|
|
53
|
+
async function runAsk({ input, answer, submit }) {
|
|
54
|
+
const query = input.value.trim();
|
|
55
|
+
if (!query)
|
|
29
56
|
return;
|
|
57
|
+
submit.disabled = true;
|
|
58
|
+
answer.dataset.state = "loading";
|
|
59
|
+
answer.textContent = "…";
|
|
60
|
+
try {
|
|
61
|
+
const res = await fetch(cfg.endpoint, {
|
|
62
|
+
method: "POST",
|
|
63
|
+
headers: { "Content-Type": "application/json" },
|
|
64
|
+
body: JSON.stringify({
|
|
65
|
+
...buildAskRequest(cfg.site, query, cfg.mode),
|
|
66
|
+
...(turnstileToken ? { turnstileToken } : {}),
|
|
67
|
+
}),
|
|
68
|
+
});
|
|
69
|
+
if (!res.ok || !res.body) {
|
|
70
|
+
throw new Error(`Request failed (${res.status})`);
|
|
71
|
+
}
|
|
72
|
+
answer.textContent = "";
|
|
73
|
+
answer.dataset.state = "streaming";
|
|
74
|
+
const reader = res.body.getReader();
|
|
75
|
+
const decoder = new TextDecoder();
|
|
76
|
+
let buf = ""; // line buffer (don't parse a frame split across reads)
|
|
77
|
+
let rawAll = ""; // whole stream, for the trailing `event: chunks` sources
|
|
78
|
+
let answerMd = ""; // accumulated answer markdown
|
|
79
|
+
for (;;) {
|
|
80
|
+
const { value, done } = await reader.read();
|
|
81
|
+
if (done)
|
|
82
|
+
break;
|
|
83
|
+
const text = decoder.decode(value, { stream: true });
|
|
84
|
+
rawAll += text;
|
|
85
|
+
buf += text;
|
|
86
|
+
const nl = buf.lastIndexOf("\n");
|
|
87
|
+
if (nl >= 0) {
|
|
88
|
+
answerMd += extractText(buf.slice(0, nl + 1));
|
|
89
|
+
buf = buf.slice(nl + 1);
|
|
90
|
+
answer.innerHTML = renderMarkdown(answerMd);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (buf)
|
|
94
|
+
answerMd += extractText(buf);
|
|
95
|
+
const sources = parseSources(rawAll, cfg.linkBase);
|
|
96
|
+
answer.innerHTML = renderMarkdown(answerMd) + (sources.length ? sourcesHtml(sources) : "");
|
|
97
|
+
answer.dataset.state = "done";
|
|
98
|
+
}
|
|
99
|
+
catch (err) {
|
|
100
|
+
answer.dataset.state = "error";
|
|
101
|
+
answer.textContent = `Sorry — something went wrong. ${err.message}`;
|
|
102
|
+
}
|
|
103
|
+
finally {
|
|
104
|
+
submit.disabled = false;
|
|
105
|
+
// Turnstile tokens are single-use — refresh for the next question.
|
|
106
|
+
turnstileApi()?.reset?.();
|
|
107
|
+
turnstileToken = null;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function wireForm(els) {
|
|
111
|
+
els.form.addEventListener("submit", (e) => {
|
|
112
|
+
e.preventDefault();
|
|
113
|
+
void runAsk(els);
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
const ROOT_ID = "dba-root";
|
|
117
|
+
/**
|
|
118
|
+
* Wire the server-rendered docked panel if present. Returns whether a docked
|
|
119
|
+
* shell exists on the page (so the caller knows to mark the floating button as
|
|
120
|
+
* a mobile-only entry). Idempotent via a `data-dba-wired` marker.
|
|
121
|
+
*/
|
|
122
|
+
function wireDockedPanel() {
|
|
123
|
+
const panel = document.querySelector('[data-dba-panel="rail"]');
|
|
124
|
+
if (!panel)
|
|
125
|
+
return false;
|
|
126
|
+
if (panel.dataset.dbaWired)
|
|
127
|
+
return true;
|
|
128
|
+
panel.dataset.dbaWired = "1";
|
|
129
|
+
const form = panel.querySelector("[data-dba-form]");
|
|
130
|
+
const input = panel.querySelector(".dba-input");
|
|
131
|
+
const answer = panel.querySelector(".dba-answer");
|
|
132
|
+
const submit = panel.querySelector(".dba-submit");
|
|
133
|
+
if (!form || !input || !answer || !submit)
|
|
134
|
+
return true;
|
|
135
|
+
// Lazy-load Turnstile on first interaction so a page that merely shows the
|
|
136
|
+
// docked panel still sets no third-party cookies until the reader engages.
|
|
137
|
+
input.addEventListener("focus", loadTurnstile, { once: true });
|
|
138
|
+
wireForm({ form, input, answer, submit });
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Build + wire the floating button + dialog. `hasDocked` marks the root so CSS
|
|
143
|
+
* can hide the trigger at desktop widths (where the docked panel is visible).
|
|
144
|
+
*/
|
|
145
|
+
function mountFloating(hasDocked) {
|
|
30
146
|
if (document.getElementById(ROOT_ID))
|
|
31
|
-
return;
|
|
147
|
+
return;
|
|
32
148
|
const root = document.createElement("div");
|
|
33
149
|
root.id = ROOT_ID;
|
|
34
150
|
root.dataset.position = cfg.position;
|
|
151
|
+
if (hasDocked)
|
|
152
|
+
root.dataset.hasDocked = "1";
|
|
35
153
|
const button = document.createElement("button");
|
|
36
154
|
button.type = "button";
|
|
37
155
|
button.className = "dba-trigger";
|
|
@@ -40,8 +158,11 @@ function mount() {
|
|
|
40
158
|
const dialog = document.createElement("dialog");
|
|
41
159
|
dialog.className = "dba-dialog";
|
|
42
160
|
dialog.setAttribute("aria-label", cfg.title);
|
|
161
|
+
const tsMarkup = cfg.turnstileSitekey
|
|
162
|
+
? `<div class="cf-turnstile dba-turnstile" data-sitekey="${cfg.turnstileSitekey}" data-appearance="interaction-only" data-callback="__dbaTsCb" data-expired-callback="__dbaTsExpired" data-error-callback="__dbaTsExpired"></div>`
|
|
163
|
+
: "";
|
|
43
164
|
dialog.innerHTML = `
|
|
44
|
-
<form class="dba-form" method="dialog">
|
|
165
|
+
<form class="dba-form" method="dialog" data-dba-form>
|
|
45
166
|
<header class="dba-header">
|
|
46
167
|
<h2 class="dba-title"></h2>
|
|
47
168
|
<button type="button" class="dba-close" aria-label="Close">×</button>
|
|
@@ -50,6 +171,7 @@ function mount() {
|
|
|
50
171
|
<input class="dba-input" type="text" autocomplete="off" enterkeyhint="search" />
|
|
51
172
|
<button type="submit" class="dba-submit">Ask</button>
|
|
52
173
|
</div>
|
|
174
|
+
${tsMarkup}
|
|
53
175
|
<div class="dba-answer" aria-live="polite" tabindex="-1"></div>
|
|
54
176
|
</form>
|
|
55
177
|
`;
|
|
@@ -61,68 +183,30 @@ function mount() {
|
|
|
61
183
|
const form = dialog.querySelector(".dba-form");
|
|
62
184
|
const submit = dialog.querySelector(".dba-submit");
|
|
63
185
|
button.addEventListener("click", () => {
|
|
186
|
+
// Lazy-load Turnstile only when the user engages, so normal page loads set
|
|
187
|
+
// no third-party cookies (Lighthouse-clean) and don't fetch the widget.
|
|
188
|
+
loadTurnstile();
|
|
64
189
|
dialog.showModal();
|
|
65
190
|
input.focus();
|
|
66
191
|
});
|
|
67
192
|
dialog.querySelector(".dba-close").addEventListener("click", () => dialog.close());
|
|
68
|
-
form
|
|
69
|
-
e.preventDefault();
|
|
70
|
-
const query = input.value.trim();
|
|
71
|
-
if (!query)
|
|
72
|
-
return;
|
|
73
|
-
submit.disabled = true;
|
|
74
|
-
answer.dataset.state = "loading";
|
|
75
|
-
answer.textContent = "…";
|
|
76
|
-
try {
|
|
77
|
-
const res = await fetch(cfg.endpoint, {
|
|
78
|
-
method: "POST",
|
|
79
|
-
headers: { "Content-Type": "application/json" },
|
|
80
|
-
body: JSON.stringify(buildAskRequest(cfg.site, query, cfg.mode)),
|
|
81
|
-
});
|
|
82
|
-
if (!res.ok || !res.body) {
|
|
83
|
-
throw new Error(`Request failed (${res.status})`);
|
|
84
|
-
}
|
|
85
|
-
answer.textContent = "";
|
|
86
|
-
answer.dataset.state = "streaming";
|
|
87
|
-
const reader = res.body.getReader();
|
|
88
|
-
const decoder = new TextDecoder();
|
|
89
|
-
let buf = ""; // line buffer (don't parse a frame split across reads)
|
|
90
|
-
let rawAll = ""; // whole stream, for the trailing `event: chunks` sources
|
|
91
|
-
let answerMd = ""; // accumulated answer markdown
|
|
92
|
-
for (;;) {
|
|
93
|
-
const { value, done } = await reader.read();
|
|
94
|
-
if (done)
|
|
95
|
-
break;
|
|
96
|
-
const text = decoder.decode(value, { stream: true });
|
|
97
|
-
rawAll += text;
|
|
98
|
-
buf += text;
|
|
99
|
-
const nl = buf.lastIndexOf("\n");
|
|
100
|
-
if (nl >= 0) {
|
|
101
|
-
answerMd += extractText(buf.slice(0, nl + 1));
|
|
102
|
-
buf = buf.slice(nl + 1);
|
|
103
|
-
answer.innerHTML = renderMarkdown(answerMd);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
if (buf)
|
|
107
|
-
answerMd += extractText(buf);
|
|
108
|
-
const sources = parseSources(rawAll, cfg.linkBase);
|
|
109
|
-
answer.innerHTML = renderMarkdown(answerMd) + (sources.length ? sourcesHtml(sources) : "");
|
|
110
|
-
answer.dataset.state = "done";
|
|
111
|
-
}
|
|
112
|
-
catch (err) {
|
|
113
|
-
answer.dataset.state = "error";
|
|
114
|
-
answer.textContent = `Sorry — something went wrong. ${err.message}`;
|
|
115
|
-
}
|
|
116
|
-
finally {
|
|
117
|
-
submit.disabled = false;
|
|
118
|
-
}
|
|
119
|
-
});
|
|
193
|
+
wireForm({ form, input, answer, submit });
|
|
120
194
|
root.append(button, dialog);
|
|
121
195
|
document.body.appendChild(root);
|
|
122
196
|
}
|
|
197
|
+
function mount() {
|
|
198
|
+
if (typeof document === "undefined")
|
|
199
|
+
return;
|
|
200
|
+
// Wire the docked panel first; its presence decides whether the floating
|
|
201
|
+
// button is a full widget (no rail) or just the mobile entry (rail present).
|
|
202
|
+
const hasDocked = wireDockedPanel();
|
|
203
|
+
mountFloating(hasDocked);
|
|
204
|
+
}
|
|
123
205
|
function unmount() {
|
|
124
206
|
if (typeof document === "undefined")
|
|
125
207
|
return;
|
|
208
|
+
// Only the floating widget is ours to remove; the docked shell is part of the
|
|
209
|
+
// document Astro swaps, and is re-wired by mount() on the next page-load.
|
|
126
210
|
document.getElementById(ROOT_ID)?.remove();
|
|
127
211
|
}
|
|
128
212
|
if (typeof document !== "undefined") {
|
package/dist/client.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAoBA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AACpD,OAAO,EAAgB,KAAK,UAAU,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAoBA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AACpD,OAAO,EAAgB,KAAK,UAAU,EAAE,MAAM,aAAa,CAAC;AAa5D,YAAY,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAE9C,MAAM,CAAC,OAAO,UAAU,GAAG,CAAC,KAAK,CAAC,EAAE,UAAU,GAAG,aAAa,CAwC7D"}
|
package/dist/index.js
CHANGED
|
@@ -25,6 +25,10 @@ const PLUGIN_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
|
25
25
|
const PACKAGE_ROOT = path.resolve(PLUGIN_DIR, "..");
|
|
26
26
|
const CLIENT_PATH = path.join(PLUGIN_DIR, "client.js");
|
|
27
27
|
const STYLES_PATH = path.join(PACKAGE_ROOT, "src", "styles.css");
|
|
28
|
+
// Server-rendered docked-panel shell. Ships un-built under `src/` (like the
|
|
29
|
+
// stylesheet) and is wired into DocsLayout's `right-rail` slot via the
|
|
30
|
+
// `componentWrappers()` hook when placement allows a rail.
|
|
31
|
+
const RAIL_WRAPPER_PATH = path.join(PACKAGE_ROOT, "src", "RightRailAsk.astro");
|
|
28
32
|
export default function ask(input) {
|
|
29
33
|
const opts = parseOptions(input);
|
|
30
34
|
return {
|
|
@@ -35,6 +39,14 @@ export default function ask(input) {
|
|
|
35
39
|
styles() {
|
|
36
40
|
return [STYLES_PATH];
|
|
37
41
|
},
|
|
42
|
+
componentWrappers() {
|
|
43
|
+
// Contribute the docked panel to DocsLayout's `right-rail` slot unless
|
|
44
|
+
// the site forced the floating-only widget. In "floating" mode the rail
|
|
45
|
+
// stays an inert passthrough and the client renders the modal instead.
|
|
46
|
+
if (opts.placement === "floating")
|
|
47
|
+
return {};
|
|
48
|
+
return { RightRail: RAIL_WRAPPER_PATH };
|
|
49
|
+
},
|
|
38
50
|
defineClientConfig(ctx) {
|
|
39
51
|
// JSON-safe runtime config exposed at
|
|
40
52
|
// virtual:dogsbay-plugin-config/@dogsbay/plugin-ask
|
|
@@ -46,8 +58,10 @@ export default function ask(input) {
|
|
|
46
58
|
title: opts.title,
|
|
47
59
|
placeholder: opts.placeholder,
|
|
48
60
|
position: opts.position,
|
|
61
|
+
placement: opts.placement,
|
|
49
62
|
// Base URL used to turn cited doc paths into links.
|
|
50
63
|
linkBase: ctx.config.site.url ?? "",
|
|
64
|
+
turnstileSitekey: opts.turnstileSitekey,
|
|
51
65
|
};
|
|
52
66
|
},
|
|
53
67
|
};
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,OAAO,EAAE,YAAY,EAAmB,MAAM,aAAa,CAAC;AAE5D,6EAA6E;AAC7E,oEAAoE;AACpE,MAAM,UAAU,GAAG,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAChE,MAAM,YAAY,GAAG,IAAI,CAAC,OAAO,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC;AACpD,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,WAAW,CAAC,CAAC;AACvD,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,KAAK,EAAE,YAAY,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,OAAO,EAAE,YAAY,EAAmB,MAAM,aAAa,CAAC;AAE5D,6EAA6E;AAC7E,oEAAoE;AACpE,MAAM,UAAU,GAAG,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAChE,MAAM,YAAY,GAAG,IAAI,CAAC,OAAO,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC;AACpD,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,WAAW,CAAC,CAAC;AACvD,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,KAAK,EAAE,YAAY,CAAC,CAAC;AACjE,4EAA4E;AAC5E,uEAAuE;AACvE,2DAA2D;AAC3D,MAAM,iBAAiB,GAAG,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,KAAK,EAAE,oBAAoB,CAAC,CAAC;AAI/E,MAAM,CAAC,OAAO,UAAU,GAAG,CAAC,KAAkB;IAC5C,MAAM,IAAI,GAAG,YAAY,CAAC,KAAK,CAAC,CAAC;IAEjC,OAAO;QACL,IAAI,EAAE,qBAAqB;QAE3B,aAAa;YACX,OAAO,CAAC,WAAW,CAAC,CAAC;QACvB,CAAC;QAED,MAAM;YACJ,OAAO,CAAC,WAAW,CAAC,CAAC;QACvB,CAAC;QAED,iBAAiB;YACf,uEAAuE;YACvE,wEAAwE;YACxE,uEAAuE;YACvE,IAAI,IAAI,CAAC,SAAS,KAAK,UAAU;gBAAE,OAAO,EAAE,CAAC;YAC7C,OAAO,EAAE,SAAS,EAAE,iBAAiB,EAAE,CAAC;QAC1C,CAAC;QAED,kBAAkB,CAAC,GAAG;YACpB,sCAAsC;YACtC,oDAAoD;YACpD,OAAO;gBACL,QAAQ,EAAE,IAAI,CAAC,QAAQ;gBACvB,IAAI,EAAE,IAAI,CAAC,IAAI;gBACf,IAAI,EAAE,IAAI,CAAC,IAAI;gBACf,WAAW,EAAE,IAAI,CAAC,WAAW;gBAC7B,KAAK,EAAE,IAAI,CAAC,KAAK;gBACjB,WAAW,EAAE,IAAI,CAAC,WAAW;gBAC7B,QAAQ,EAAE,IAAI,CAAC,QAAQ;gBACvB,SAAS,EAAE,IAAI,CAAC,SAAS;gBACzB,oDAAoD;gBACpD,QAAQ,EAAG,GAAG,CAAC,MAAM,CAAC,IAAyB,CAAC,GAAG,IAAI,EAAE;gBACzD,gBAAgB,EAAE,IAAI,CAAC,gBAAgB;aACxC,CAAC;QACJ,CAAC;KACF,CAAC;AACJ,CAAC"}
|
package/dist/schema.d.ts
CHANGED
|
@@ -9,6 +9,15 @@
|
|
|
9
9
|
*/
|
|
10
10
|
export type AskMode = "answer" | "search";
|
|
11
11
|
export type AskPosition = "bottom-right" | "bottom-left";
|
|
12
|
+
/**
|
|
13
|
+
* Where the widget renders.
|
|
14
|
+
* - `auto` (default) — dock as a panel in the docs right rail when one exists
|
|
15
|
+
* (Dogsbay's `DocsLayout` `right-rail` slot), else fall back to a floating
|
|
16
|
+
* button + dialog.
|
|
17
|
+
* - `rail` — always contribute the docked rail panel (no desktop floating button).
|
|
18
|
+
* - `floating` — the floating button + modal only; never use the rail.
|
|
19
|
+
*/
|
|
20
|
+
export type AskPlacement = "auto" | "rail" | "floating";
|
|
12
21
|
export interface AskOptions {
|
|
13
22
|
/**
|
|
14
23
|
* URL of the dogsbay-ask proxy (the shared Worker). Required.
|
|
@@ -31,6 +40,17 @@ export interface AskOptions {
|
|
|
31
40
|
placeholder?: string;
|
|
32
41
|
/** Corner the floating button sits in. Default "bottom-right". */
|
|
33
42
|
position?: AskPosition;
|
|
43
|
+
/**
|
|
44
|
+
* Where the widget renders: docked rail panel, floating modal, or `auto`.
|
|
45
|
+
* Default "auto". See {@link AskPlacement}.
|
|
46
|
+
*/
|
|
47
|
+
placement?: AskPlacement;
|
|
48
|
+
/**
|
|
49
|
+
* Cloudflare Turnstile sitekey. If set, the widget obtains a Turnstile
|
|
50
|
+
* token and sends it with each request; the proxy verifies it. Omit to
|
|
51
|
+
* disable the bot check.
|
|
52
|
+
*/
|
|
53
|
+
turnstileSitekey?: string;
|
|
34
54
|
}
|
|
35
55
|
export interface ResolvedAskOptions {
|
|
36
56
|
endpoint: string;
|
|
@@ -40,6 +60,8 @@ export interface ResolvedAskOptions {
|
|
|
40
60
|
title: string;
|
|
41
61
|
placeholder: string;
|
|
42
62
|
position: AskPosition;
|
|
63
|
+
placement: AskPlacement;
|
|
64
|
+
turnstileSitekey: string;
|
|
43
65
|
}
|
|
44
66
|
/**
|
|
45
67
|
* Validate + resolve user options. Throws structured errors naming the
|
package/dist/schema.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,MAAM,MAAM,OAAO,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAC1C,MAAM,MAAM,WAAW,GAAG,cAAc,GAAG,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,MAAM,MAAM,OAAO,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAC1C,MAAM,MAAM,WAAW,GAAG,cAAc,GAAG,aAAa,CAAC;AACzD;;;;;;;GAOG;AACH,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,MAAM,GAAG,UAAU,CAAC;AAExD,MAAM,WAAW,UAAU;IACzB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,2EAA2E;IAC3E,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,+CAA+C;IAC/C,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,6CAA6C;IAC7C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,oDAAoD;IACpD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,kEAAkE;IAClE,QAAQ,CAAC,EAAE,WAAW,CAAC;IACvB;;;OAGG;IACH,SAAS,CAAC,EAAE,YAAY,CAAC;IACzB;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,OAAO,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,WAAW,CAAC;IACtB,SAAS,EAAE,YAAY,CAAC;IACxB,gBAAgB,EAAE,MAAM,CAAC;CAC1B;AAcD;;;GAGG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,kBAAkB,CAuC/D"}
|
package/dist/schema.js
CHANGED
|
@@ -13,6 +13,8 @@ const DEFAULTS = {
|
|
|
13
13
|
title: "Ask the docs",
|
|
14
14
|
placeholder: "Ask a question…",
|
|
15
15
|
position: "bottom-right",
|
|
16
|
+
placement: "auto",
|
|
17
|
+
turnstileSitekey: "",
|
|
16
18
|
};
|
|
17
19
|
const PKG = "@dogsbay/plugin-ask";
|
|
18
20
|
/**
|
|
@@ -45,6 +47,15 @@ export function parseOptions(input) {
|
|
|
45
47
|
}
|
|
46
48
|
out.position = opts.position;
|
|
47
49
|
}
|
|
50
|
+
if (opts.placement !== undefined) {
|
|
51
|
+
if (opts.placement !== "auto" && opts.placement !== "rail" && opts.placement !== "floating") {
|
|
52
|
+
throw new Error(`${PKG}: options.placement must be "auto", "rail", or "floating".`);
|
|
53
|
+
}
|
|
54
|
+
out.placement = opts.placement;
|
|
55
|
+
}
|
|
56
|
+
if (opts.turnstileSitekey !== undefined) {
|
|
57
|
+
out.turnstileSitekey = requireNonEmptyString(opts.turnstileSitekey, "turnstileSitekey");
|
|
58
|
+
}
|
|
48
59
|
return out;
|
|
49
60
|
}
|
|
50
61
|
function requireNonEmptyString(value, field) {
|
package/dist/schema.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema.js","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;
|
|
1
|
+
{"version":3,"file":"schema.js","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dogsbay/plugin-ask",
|
|
3
|
-
"version": "0.2.0-beta.
|
|
3
|
+
"version": "0.2.0-beta.86",
|
|
4
4
|
"description": "Floating \"Ask AI\" widget for Dogsbay docs sites — calls a shared AI Search proxy (dogsbay-ask).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -18,11 +18,12 @@
|
|
|
18
18
|
"dist",
|
|
19
19
|
"src/styles.css",
|
|
20
20
|
"src/client.ts",
|
|
21
|
+
"src/RightRailAsk.astro",
|
|
21
22
|
"skills",
|
|
22
23
|
"README.md"
|
|
23
24
|
],
|
|
24
25
|
"dependencies": {
|
|
25
|
-
"@dogsbay/types": "0.2.0-beta.
|
|
26
|
+
"@dogsbay/types": "0.2.0-beta.86"
|
|
26
27
|
},
|
|
27
28
|
"devDependencies": {
|
|
28
29
|
"typescript": "^5.7.0",
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Docked "Ask AI" panel shell — rendered into DocsLayout's `right-rail` named
|
|
4
|
+
* slot via the plugin's `componentWrappers()` hook (slot "RightRail").
|
|
5
|
+
*
|
|
6
|
+
* This is the SERVER-RENDERED structure only; behaviour (open, submit, stream,
|
|
7
|
+
* Turnstile) is wired by the client module (`client.ts`), which finds this shell
|
|
8
|
+
* by `[data-dba-panel="rail"]`. Presentational config comes from the same
|
|
9
|
+
* generated module the client uses, resolvable in SSR via the CLI's Vite alias.
|
|
10
|
+
*
|
|
11
|
+
* Must render `<slot />` to satisfy the wrapper contract (the inner slot is
|
|
12
|
+
* empty — this is an additive panel, not a content wrapper). Being a real
|
|
13
|
+
* element child also keeps DocsLayout's empty-rail rule
|
|
14
|
+
* (`aside[data-right-rail]:not(:has(*))`) from hiding the rail.
|
|
15
|
+
*/
|
|
16
|
+
// @ts-expect-error: virtual module resolved by the Dogsbay Vite alias at build.
|
|
17
|
+
import config from "virtual:dogsbay-plugin-config/@dogsbay/plugin-ask";
|
|
18
|
+
|
|
19
|
+
interface RailConfig {
|
|
20
|
+
title: string;
|
|
21
|
+
placeholder: string;
|
|
22
|
+
turnstileSitekey?: string;
|
|
23
|
+
}
|
|
24
|
+
const cfg = config as RailConfig;
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
<section class="dba-panel" data-dba-panel="rail" aria-label={cfg.title}>
|
|
28
|
+
<form class="dba-form" data-dba-form>
|
|
29
|
+
<header class="dba-header">
|
|
30
|
+
<h2 class="dba-title">{cfg.title}</h2>
|
|
31
|
+
</header>
|
|
32
|
+
<div class="dba-inputrow">
|
|
33
|
+
<input
|
|
34
|
+
class="dba-input"
|
|
35
|
+
type="text"
|
|
36
|
+
autocomplete="off"
|
|
37
|
+
enterkeyhint="search"
|
|
38
|
+
placeholder={cfg.placeholder}
|
|
39
|
+
/>
|
|
40
|
+
<button type="submit" class="dba-submit">Ask</button>
|
|
41
|
+
</div>
|
|
42
|
+
{cfg.turnstileSitekey && (
|
|
43
|
+
<div
|
|
44
|
+
class="cf-turnstile dba-turnstile"
|
|
45
|
+
data-sitekey={cfg.turnstileSitekey}
|
|
46
|
+
data-appearance="interaction-only"
|
|
47
|
+
data-callback="__dbaTsCb"
|
|
48
|
+
data-expired-callback="__dbaTsExpired"
|
|
49
|
+
data-error-callback="__dbaTsExpired"
|
|
50
|
+
/>
|
|
51
|
+
)}
|
|
52
|
+
<div class="dba-answer" aria-live="polite" tabindex="-1"></div>
|
|
53
|
+
</form>
|
|
54
|
+
</section>
|
|
55
|
+
<slot />
|
package/src/client.ts
CHANGED
|
@@ -1,20 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Client runtime for `@dogsbay/plugin-ask`.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* Astro project, so it runs for side effects on every page. It injects a
|
|
6
|
-
* floating button + <dialog> at <body> level (outside the article), and on
|
|
7
|
-
* submit streams an answer from the dogsbay-ask proxy.
|
|
4
|
+
* Runs for side effects on every page. Two render targets share one code path:
|
|
8
5
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
6
|
+
* - **Docked panel** — a server-rendered shell (`RightRailAsk.astro`,
|
|
7
|
+
* `[data-dba-panel="rail"]`) that DocsLayout places in its `right-rail`
|
|
8
|
+
* slot. Visible on desktop; hidden < lg because the rail is `hidden lg:block`.
|
|
9
|
+
* - **Floating modal** — a body-level button + `<dialog>` built here. Used when
|
|
10
|
+
* there's no rail (floating placement, wide/API pages) and as the mobile
|
|
11
|
+
* entry when a docked panel exists.
|
|
12
|
+
*
|
|
13
|
+
* Astro view transitions swap the document, so we mount on `astro:page-load`
|
|
14
|
+
* and tear the floating widget down on `astro:before-swap` (the docked shell is
|
|
15
|
+
* part of the swapped document, so it's re-wired each navigation).
|
|
12
16
|
*/
|
|
13
17
|
// @ts-expect-error: virtual module resolved by the Dogsbay Vite alias at build.
|
|
14
18
|
import config from "virtual:dogsbay-plugin-config/@dogsbay/plugin-ask";
|
|
15
19
|
import { buildAskRequest, extractText, parseSources, type Source } from "./protocol.js";
|
|
16
20
|
import { renderMarkdown, escapeHtml } from "./md.js";
|
|
17
|
-
import type { AskMode, AskPosition } from "./schema.js";
|
|
21
|
+
import type { AskMode, AskPlacement, AskPosition } from "./schema.js";
|
|
18
22
|
|
|
19
23
|
interface RuntimeConfig {
|
|
20
24
|
endpoint: string;
|
|
@@ -24,11 +28,37 @@ interface RuntimeConfig {
|
|
|
24
28
|
title: string;
|
|
25
29
|
placeholder: string;
|
|
26
30
|
position: AskPosition;
|
|
31
|
+
placement: AskPlacement;
|
|
27
32
|
linkBase: string;
|
|
33
|
+
turnstileSitekey: string;
|
|
28
34
|
}
|
|
29
35
|
|
|
30
36
|
const cfg = config as RuntimeConfig;
|
|
31
37
|
|
|
38
|
+
// Turnstile token (refreshed per ask). Populated by the widget callbacks.
|
|
39
|
+
let turnstileToken: string | null = null;
|
|
40
|
+
let turnstileScriptLoaded = false;
|
|
41
|
+
|
|
42
|
+
interface TurnstileApi {
|
|
43
|
+
reset?: (widget?: unknown) => void;
|
|
44
|
+
}
|
|
45
|
+
function turnstileApi(): TurnstileApi | undefined {
|
|
46
|
+
return (window as unknown as { turnstile?: TurnstileApi }).turnstile;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function loadTurnstile(): void {
|
|
50
|
+
if (turnstileScriptLoaded || !cfg.turnstileSitekey) return;
|
|
51
|
+
turnstileScriptLoaded = true;
|
|
52
|
+
const w = window as unknown as Record<string, unknown>;
|
|
53
|
+
w.__dbaTsCb = (t: string) => { turnstileToken = t; };
|
|
54
|
+
w.__dbaTsExpired = () => { turnstileToken = null; };
|
|
55
|
+
const s = document.createElement("script");
|
|
56
|
+
s.src = "https://challenges.cloudflare.com/turnstile/v0/api.js";
|
|
57
|
+
s.async = true;
|
|
58
|
+
s.defer = true;
|
|
59
|
+
document.head.appendChild(s);
|
|
60
|
+
}
|
|
61
|
+
|
|
32
62
|
function sourcesHtml(sources: Source[]): string {
|
|
33
63
|
const items = sources
|
|
34
64
|
.map((s) =>
|
|
@@ -40,15 +70,116 @@ function sourcesHtml(sources: Source[]): string {
|
|
|
40
70
|
return `<div class="dba-sources"><div class="dba-sources-title">Sources</div><ul>${items}</ul></div>`;
|
|
41
71
|
}
|
|
42
72
|
|
|
73
|
+
interface AskEls {
|
|
74
|
+
form: HTMLFormElement;
|
|
75
|
+
input: HTMLInputElement;
|
|
76
|
+
answer: HTMLDivElement;
|
|
77
|
+
submit: HTMLButtonElement;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The one ask flow: POST the question, stream the answer, render Markdown +
|
|
82
|
+
* a Sources list. Shared by the docked panel and the floating dialog.
|
|
83
|
+
*/
|
|
84
|
+
async function runAsk({ input, answer, submit }: AskEls): Promise<void> {
|
|
85
|
+
const query = input.value.trim();
|
|
86
|
+
if (!query) return;
|
|
87
|
+
|
|
88
|
+
submit.disabled = true;
|
|
89
|
+
answer.dataset.state = "loading";
|
|
90
|
+
answer.textContent = "…";
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
const res = await fetch(cfg.endpoint, {
|
|
94
|
+
method: "POST",
|
|
95
|
+
headers: { "Content-Type": "application/json" },
|
|
96
|
+
body: JSON.stringify({
|
|
97
|
+
...buildAskRequest(cfg.site, query, cfg.mode),
|
|
98
|
+
...(turnstileToken ? { turnstileToken } : {}),
|
|
99
|
+
}),
|
|
100
|
+
});
|
|
101
|
+
if (!res.ok || !res.body) {
|
|
102
|
+
throw new Error(`Request failed (${res.status})`);
|
|
103
|
+
}
|
|
104
|
+
answer.textContent = "";
|
|
105
|
+
answer.dataset.state = "streaming";
|
|
106
|
+
const reader = res.body.getReader();
|
|
107
|
+
const decoder = new TextDecoder();
|
|
108
|
+
let buf = ""; // line buffer (don't parse a frame split across reads)
|
|
109
|
+
let rawAll = ""; // whole stream, for the trailing `event: chunks` sources
|
|
110
|
+
let answerMd = ""; // accumulated answer markdown
|
|
111
|
+
for (;;) {
|
|
112
|
+
const { value, done } = await reader.read();
|
|
113
|
+
if (done) break;
|
|
114
|
+
const text = decoder.decode(value, { stream: true });
|
|
115
|
+
rawAll += text;
|
|
116
|
+
buf += text;
|
|
117
|
+
const nl = buf.lastIndexOf("\n");
|
|
118
|
+
if (nl >= 0) {
|
|
119
|
+
answerMd += extractText(buf.slice(0, nl + 1));
|
|
120
|
+
buf = buf.slice(nl + 1);
|
|
121
|
+
answer.innerHTML = renderMarkdown(answerMd);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (buf) answerMd += extractText(buf);
|
|
125
|
+
const sources = parseSources(rawAll, cfg.linkBase);
|
|
126
|
+
answer.innerHTML = renderMarkdown(answerMd) + (sources.length ? sourcesHtml(sources) : "");
|
|
127
|
+
answer.dataset.state = "done";
|
|
128
|
+
} catch (err) {
|
|
129
|
+
answer.dataset.state = "error";
|
|
130
|
+
answer.textContent = `Sorry — something went wrong. ${(err as Error).message}`;
|
|
131
|
+
} finally {
|
|
132
|
+
submit.disabled = false;
|
|
133
|
+
// Turnstile tokens are single-use — refresh for the next question.
|
|
134
|
+
turnstileApi()?.reset?.();
|
|
135
|
+
turnstileToken = null;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function wireForm(els: AskEls): void {
|
|
140
|
+
els.form.addEventListener("submit", (e) => {
|
|
141
|
+
e.preventDefault();
|
|
142
|
+
void runAsk(els);
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
43
146
|
const ROOT_ID = "dba-root";
|
|
44
147
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
148
|
+
/**
|
|
149
|
+
* Wire the server-rendered docked panel if present. Returns whether a docked
|
|
150
|
+
* shell exists on the page (so the caller knows to mark the floating button as
|
|
151
|
+
* a mobile-only entry). Idempotent via a `data-dba-wired` marker.
|
|
152
|
+
*/
|
|
153
|
+
function wireDockedPanel(): boolean {
|
|
154
|
+
const panel = document.querySelector<HTMLElement>('[data-dba-panel="rail"]');
|
|
155
|
+
if (!panel) return false;
|
|
156
|
+
if (panel.dataset.dbaWired) return true;
|
|
157
|
+
panel.dataset.dbaWired = "1";
|
|
158
|
+
|
|
159
|
+
const form = panel.querySelector<HTMLFormElement>("[data-dba-form]");
|
|
160
|
+
const input = panel.querySelector<HTMLInputElement>(".dba-input");
|
|
161
|
+
const answer = panel.querySelector<HTMLDivElement>(".dba-answer");
|
|
162
|
+
const submit = panel.querySelector<HTMLButtonElement>(".dba-submit");
|
|
163
|
+
if (!form || !input || !answer || !submit) return true;
|
|
164
|
+
|
|
165
|
+
// Lazy-load Turnstile on first interaction so a page that merely shows the
|
|
166
|
+
// docked panel still sets no third-party cookies until the reader engages.
|
|
167
|
+
input.addEventListener("focus", loadTurnstile, { once: true });
|
|
168
|
+
wireForm({ form, input, answer, submit });
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Build + wire the floating button + dialog. `hasDocked` marks the root so CSS
|
|
174
|
+
* can hide the trigger at desktop widths (where the docked panel is visible).
|
|
175
|
+
*/
|
|
176
|
+
function mountFloating(hasDocked: boolean): void {
|
|
177
|
+
if (document.getElementById(ROOT_ID)) return;
|
|
48
178
|
|
|
49
179
|
const root = document.createElement("div");
|
|
50
180
|
root.id = ROOT_ID;
|
|
51
181
|
root.dataset.position = cfg.position;
|
|
182
|
+
if (hasDocked) root.dataset.hasDocked = "1";
|
|
52
183
|
|
|
53
184
|
const button = document.createElement("button");
|
|
54
185
|
button.type = "button";
|
|
@@ -59,8 +190,11 @@ function mount(): void {
|
|
|
59
190
|
const dialog = document.createElement("dialog");
|
|
60
191
|
dialog.className = "dba-dialog";
|
|
61
192
|
dialog.setAttribute("aria-label", cfg.title);
|
|
193
|
+
const tsMarkup = cfg.turnstileSitekey
|
|
194
|
+
? `<div class="cf-turnstile dba-turnstile" data-sitekey="${cfg.turnstileSitekey}" data-appearance="interaction-only" data-callback="__dbaTsCb" data-expired-callback="__dbaTsExpired" data-error-callback="__dbaTsExpired"></div>`
|
|
195
|
+
: "";
|
|
62
196
|
dialog.innerHTML = `
|
|
63
|
-
<form class="dba-form" method="dialog">
|
|
197
|
+
<form class="dba-form" method="dialog" data-dba-form>
|
|
64
198
|
<header class="dba-header">
|
|
65
199
|
<h2 class="dba-title"></h2>
|
|
66
200
|
<button type="button" class="dba-close" aria-label="Close">×</button>
|
|
@@ -69,6 +203,7 @@ function mount(): void {
|
|
|
69
203
|
<input class="dba-input" type="text" autocomplete="off" enterkeyhint="search" />
|
|
70
204
|
<button type="submit" class="dba-submit">Ask</button>
|
|
71
205
|
</div>
|
|
206
|
+
${tsMarkup}
|
|
72
207
|
<div class="dba-answer" aria-live="polite" tabindex="-1"></div>
|
|
73
208
|
</form>
|
|
74
209
|
`;
|
|
@@ -82,67 +217,32 @@ function mount(): void {
|
|
|
82
217
|
const submit = dialog.querySelector<HTMLButtonElement>(".dba-submit")!;
|
|
83
218
|
|
|
84
219
|
button.addEventListener("click", () => {
|
|
220
|
+
// Lazy-load Turnstile only when the user engages, so normal page loads set
|
|
221
|
+
// no third-party cookies (Lighthouse-clean) and don't fetch the widget.
|
|
222
|
+
loadTurnstile();
|
|
85
223
|
dialog.showModal();
|
|
86
224
|
input.focus();
|
|
87
225
|
});
|
|
88
226
|
dialog.querySelector<HTMLButtonElement>(".dba-close")!.addEventListener("click", () => dialog.close());
|
|
89
227
|
|
|
90
|
-
form
|
|
91
|
-
e.preventDefault();
|
|
92
|
-
const query = input.value.trim();
|
|
93
|
-
if (!query) return;
|
|
94
|
-
|
|
95
|
-
submit.disabled = true;
|
|
96
|
-
answer.dataset.state = "loading";
|
|
97
|
-
answer.textContent = "…";
|
|
98
|
-
|
|
99
|
-
try {
|
|
100
|
-
const res = await fetch(cfg.endpoint, {
|
|
101
|
-
method: "POST",
|
|
102
|
-
headers: { "Content-Type": "application/json" },
|
|
103
|
-
body: JSON.stringify(buildAskRequest(cfg.site, query, cfg.mode)),
|
|
104
|
-
});
|
|
105
|
-
if (!res.ok || !res.body) {
|
|
106
|
-
throw new Error(`Request failed (${res.status})`);
|
|
107
|
-
}
|
|
108
|
-
answer.textContent = "";
|
|
109
|
-
answer.dataset.state = "streaming";
|
|
110
|
-
const reader = res.body.getReader();
|
|
111
|
-
const decoder = new TextDecoder();
|
|
112
|
-
let buf = ""; // line buffer (don't parse a frame split across reads)
|
|
113
|
-
let rawAll = ""; // whole stream, for the trailing `event: chunks` sources
|
|
114
|
-
let answerMd = ""; // accumulated answer markdown
|
|
115
|
-
for (;;) {
|
|
116
|
-
const { value, done } = await reader.read();
|
|
117
|
-
if (done) break;
|
|
118
|
-
const text = decoder.decode(value, { stream: true });
|
|
119
|
-
rawAll += text;
|
|
120
|
-
buf += text;
|
|
121
|
-
const nl = buf.lastIndexOf("\n");
|
|
122
|
-
if (nl >= 0) {
|
|
123
|
-
answerMd += extractText(buf.slice(0, nl + 1));
|
|
124
|
-
buf = buf.slice(nl + 1);
|
|
125
|
-
answer.innerHTML = renderMarkdown(answerMd);
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
if (buf) answerMd += extractText(buf);
|
|
129
|
-
const sources = parseSources(rawAll, cfg.linkBase);
|
|
130
|
-
answer.innerHTML = renderMarkdown(answerMd) + (sources.length ? sourcesHtml(sources) : "");
|
|
131
|
-
answer.dataset.state = "done";
|
|
132
|
-
} catch (err) {
|
|
133
|
-
answer.dataset.state = "error";
|
|
134
|
-
answer.textContent = `Sorry — something went wrong. ${(err as Error).message}`;
|
|
135
|
-
} finally {
|
|
136
|
-
submit.disabled = false;
|
|
137
|
-
}
|
|
138
|
-
});
|
|
228
|
+
wireForm({ form, input, answer, submit });
|
|
139
229
|
|
|
140
230
|
root.append(button, dialog);
|
|
141
231
|
document.body.appendChild(root);
|
|
142
232
|
}
|
|
143
233
|
|
|
234
|
+
function mount(): void {
|
|
235
|
+
if (typeof document === "undefined") return;
|
|
236
|
+
// Wire the docked panel first; its presence decides whether the floating
|
|
237
|
+
// button is a full widget (no rail) or just the mobile entry (rail present).
|
|
238
|
+
const hasDocked = wireDockedPanel();
|
|
239
|
+
mountFloating(hasDocked);
|
|
240
|
+
}
|
|
241
|
+
|
|
144
242
|
function unmount(): void {
|
|
145
243
|
if (typeof document === "undefined") return;
|
|
244
|
+
// Only the floating widget is ours to remove; the docked shell is part of the
|
|
245
|
+
// document Astro swaps, and is re-wired by mount() on the next page-load.
|
|
146
246
|
document.getElementById(ROOT_ID)?.remove();
|
|
147
247
|
}
|
|
148
248
|
|
package/src/styles.css
CHANGED
|
@@ -13,6 +13,25 @@
|
|
|
13
13
|
--dba-radius: var(--radius, 0.5rem);
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
/* Docked rail panel (RightRailAsk.astro) lives OUTSIDE #dba-root, so it needs
|
|
17
|
+
its own token block to pick up the site theme (same tokens as #dba-root). */
|
|
18
|
+
.dba-panel {
|
|
19
|
+
--dba-accent: var(--color-accent, #2563eb);
|
|
20
|
+
--dba-accent-contrast: var(--color-accent-contrast, #ffffff);
|
|
21
|
+
--dba-surface: var(--color-surface, #ffffff);
|
|
22
|
+
--dba-text: var(--color-text, #1f2937);
|
|
23
|
+
--dba-border: var(--color-border, #e5e7eb);
|
|
24
|
+
--dba-radius: var(--radius, 0.5rem);
|
|
25
|
+
color: var(--dba-text);
|
|
26
|
+
font-size: 0.9rem;
|
|
27
|
+
}
|
|
28
|
+
/* In the narrow rail the form fills the column and stacks the input + button. */
|
|
29
|
+
.dba-panel .dba-form { padding: 0; gap: 0.5rem; }
|
|
30
|
+
.dba-panel .dba-inputrow { flex-direction: column; }
|
|
31
|
+
.dba-panel .dba-title { font-size: 0.95rem; }
|
|
32
|
+
/* The rail aside scrolls, so let the answer grow rather than scroll internally. */
|
|
33
|
+
.dba-panel .dba-answer { max-height: none; }
|
|
34
|
+
|
|
16
35
|
.dba-trigger {
|
|
17
36
|
position: fixed;
|
|
18
37
|
bottom: 1.25rem;
|
|
@@ -32,6 +51,13 @@
|
|
|
32
51
|
.dba-trigger:hover { filter: brightness(1.05); }
|
|
33
52
|
.dba-trigger:focus-visible { outline: 2px solid var(--dba-accent); outline-offset: 2px; }
|
|
34
53
|
|
|
54
|
+
/* When a docked panel is present the floating button is only the mobile entry —
|
|
55
|
+
hide it at the breakpoint where the rail (and its docked panel) is visible.
|
|
56
|
+
1024px matches DocsLayout's `lg:block` rail. */
|
|
57
|
+
@media (min-width: 1024px) {
|
|
58
|
+
#dba-root[data-has-docked] .dba-trigger { display: none; }
|
|
59
|
+
}
|
|
60
|
+
|
|
35
61
|
.dba-dialog {
|
|
36
62
|
width: min(40rem, 92vw);
|
|
37
63
|
max-height: 80vh;
|
|
@@ -73,6 +99,7 @@
|
|
|
73
99
|
background: color-mix(in srgb, var(--dba-text) 4%, transparent);
|
|
74
100
|
border-radius: var(--dba-radius);
|
|
75
101
|
}
|
|
102
|
+
.dba-turnstile { margin: 0.25rem 0; min-height: 0; }
|
|
76
103
|
.dba-answer[data-state="loading"] { opacity: 0.6; }
|
|
77
104
|
.dba-answer[data-state="error"] { color: #b91c1c; }
|
|
78
105
|
|