@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 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
- * Imported via the auto-generated `src/lib/plugin-runtime.ts` in the user's
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
- * Astro view transitions: the document is swapped on navigation, so we mount
10
- * on `astro:page-load` and tear down on `astro:before-swap` to avoid stale
11
- * nodes / duplicate buttons.
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
- const ROOT_ID = "dba-root";
27
- function mount() {
28
- if (typeof document === "undefined")
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; // already mounted
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.addEventListener("submit", async (e) => {
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") {
@@ -1 +1 @@
1
- {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
@@ -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;AAS5D,YAAY,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAE9C,MAAM,CAAC,OAAO,UAAU,GAAG,CAAC,KAAK,CAAC,EAAE,UAAU,GAAG,aAAa,CA8B7D"}
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;AAIjE,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,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,oDAAoD;gBACpD,QAAQ,EAAG,GAAG,CAAC,MAAM,CAAC,IAAyB,CAAC,GAAG,IAAI,EAAE;aAC1D,CAAC;QACJ,CAAC;KACF,CAAC;AACJ,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
@@ -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;AAEzD,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;CACxB;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;CACvB;AAYD;;;GAGG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,kBAAkB,CA8B/D"}
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) {
@@ -1 +1 @@
1
- {"version":3,"file":"schema.js","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"schema.js","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AA6DH,MAAM,QAAQ,GAAkD;IAC9D,IAAI,EAAE,QAAQ;IACd,WAAW,EAAE,QAAQ;IACrB,KAAK,EAAE,cAAc;IACrB,WAAW,EAAE,iBAAiB;IAC9B,QAAQ,EAAE,cAAc;IACxB,SAAS,EAAE,MAAM;IACjB,gBAAgB,EAAE,EAAE;CACrB,CAAC;AAEF,MAAM,GAAG,GAAG,qBAAqB,CAAC;AAElC;;;GAGG;AACH,MAAM,UAAU,YAAY,CAAC,KAAc;IACzC,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QAC/F,MAAM,IAAI,KAAK,CACb,GAAG,GAAG,sFAAsF,QAAQ,CAAC,KAAK,CAAC,GAAG,CAC/G,CAAC;IACJ,CAAC;IACD,MAAM,IAAI,GAAG,KAAgC,CAAC;IAE9C,MAAM,QAAQ,GAAG,UAAU,CAAC,IAAI,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAC;IACvD,MAAM,IAAI,GAAG,qBAAqB,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IAEtD,MAAM,GAAG,GAAuB,EAAE,GAAG,QAAQ,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IAEhE,IAAI,IAAI,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;QAC5B,IAAI,IAAI,CAAC,IAAI,KAAK,QAAQ,IAAI,IAAI,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;YACrD,MAAM,IAAI,KAAK,CAAC,GAAG,GAAG,8CAA8C,CAAC,CAAC;QACxE,CAAC;QACD,GAAG,CAAC,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC;IACvB,CAAC;IACD,IAAI,IAAI,CAAC,WAAW,KAAK,SAAS;QAAE,GAAG,CAAC,WAAW,GAAG,qBAAqB,CAAC,IAAI,CAAC,WAAW,EAAE,aAAa,CAAC,CAAC;IAC7G,IAAI,IAAI,CAAC,KAAK,KAAK,SAAS;QAAE,GAAG,CAAC,KAAK,GAAG,qBAAqB,CAAC,IAAI,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;IACrF,IAAI,IAAI,CAAC,WAAW,KAAK,SAAS;QAAE,GAAG,CAAC,WAAW,GAAG,qBAAqB,CAAC,IAAI,CAAC,WAAW,EAAE,aAAa,CAAC,CAAC;IAC7G,IAAI,IAAI,CAAC,QAAQ,KAAK,SAAS,EAAE,CAAC;QAChC,IAAI,IAAI,CAAC,QAAQ,KAAK,cAAc,IAAI,IAAI,CAAC,QAAQ,KAAK,aAAa,EAAE,CAAC;YACxE,MAAM,IAAI,KAAK,CAAC,GAAG,GAAG,6DAA6D,CAAC,CAAC;QACvF,CAAC;QACD,GAAG,CAAC,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC;IAC/B,CAAC;IACD,IAAI,IAAI,CAAC,SAAS,KAAK,SAAS,EAAE,CAAC;QACjC,IAAI,IAAI,CAAC,SAAS,KAAK,MAAM,IAAI,IAAI,CAAC,SAAS,KAAK,MAAM,IAAI,IAAI,CAAC,SAAS,KAAK,UAAU,EAAE,CAAC;YAC5F,MAAM,IAAI,KAAK,CAAC,GAAG,GAAG,4DAA4D,CAAC,CAAC;QACtF,CAAC;QACD,GAAG,CAAC,SAAS,GAAG,IAAI,CAAC,SAAS,CAAC;IACjC,CAAC;IACD,IAAI,IAAI,CAAC,gBAAgB,KAAK,SAAS,EAAE,CAAC;QACxC,GAAG,CAAC,gBAAgB,GAAG,qBAAqB,CAAC,IAAI,CAAC,gBAAgB,EAAE,kBAAkB,CAAC,CAAC;IAC1F,CAAC;IAED,OAAO,GAAG,CAAC;AACb,CAAC;AAED,SAAS,qBAAqB,CAAC,KAAc,EAAE,KAAa;IAC1D,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;QACrD,MAAM,IAAI,KAAK,CAAC,GAAG,GAAG,aAAa,KAAK,8BAA8B,CAAC,CAAC;IAC1E,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,SAAS,UAAU,CAAC,KAAc,EAAE,KAAa;IAC/C,MAAM,CAAC,GAAG,qBAAqB,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;IAC9C,IAAI,CAAC;QACH,kCAAkC;QAClC,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC;IACb,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,KAAK,CAAC,GAAG,GAAG,aAAa,KAAK,uCAAuC,CAAC,IAAI,CAAC,CAAC;IACxF,CAAC;IACD,OAAO,CAAC,CAAC;AACX,CAAC;AAED,SAAS,QAAQ,CAAC,KAAc;IAC9B,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,MAAM,CAAC;IAClC,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,OAAO,CAAC;IACzC,OAAO,OAAO,KAAK,CAAC;AACtB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dogsbay/plugin-ask",
3
- "version": "0.2.0-beta.83",
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.83"
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
- * Imported via the auto-generated `src/lib/plugin-runtime.ts` in the user's
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
- * Astro view transitions: the document is swapped on navigation, so we mount
10
- * on `astro:page-load` and tear down on `astro:before-swap` to avoid stale
11
- * nodes / duplicate buttons.
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
- function mount(): void {
46
- if (typeof document === "undefined") return;
47
- if (document.getElementById(ROOT_ID)) return; // already mounted
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.addEventListener("submit", async (e) => {
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