@dogsbay/plugin-ask 0.2.0-beta.81
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 +45 -0
- package/dist/client.d.ts +2 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +124 -0
- package/dist/client.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +53 -0
- package/dist/index.js.map +1 -0
- package/dist/protocol.d.ts +25 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +51 -0
- package/dist/protocol.js.map +1 -0
- package/dist/schema.d.ts +49 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +74 -0
- package/dist/schema.js.map +1 -0
- package/package.json +50 -0
- package/skills/ask-config.md +46 -0
- package/src/client.ts +139 -0
- package/src/styles.css +82 -0
package/README.md
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# @dogsbay/plugin-ask
|
|
2
|
+
|
|
3
|
+
A floating "Ask AI" button for Dogsbay docs sites. It opens a modal, sends the
|
|
4
|
+
question to a configured backend, and shows the answer — so a static docs site
|
|
5
|
+
can offer AI search without shipping any secrets.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pnpm add @dogsbay/plugin-ask
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Use
|
|
14
|
+
|
|
15
|
+
```yaml
|
|
16
|
+
# dogsbay.config.yml
|
|
17
|
+
plugins:
|
|
18
|
+
- name: "@dogsbay/plugin-ask"
|
|
19
|
+
options:
|
|
20
|
+
endpoint: "https://<your-ask-endpoint>"
|
|
21
|
+
site: "openshift-docs-dogsbay"
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
`endpoint` and `site` are required; everything else is presentation.
|
|
25
|
+
|
|
26
|
+
## Options
|
|
27
|
+
|
|
28
|
+
| Option | Required | Default | Description |
|
|
29
|
+
|---|---|---|---|
|
|
30
|
+
| `endpoint` | yes | — | URL of the backend the widget calls. |
|
|
31
|
+
| `site` | yes | — | Site id sent with each request to select the corpus. |
|
|
32
|
+
| `mode` | no | `answer` | `answer` (generated answer) or `search` (results). |
|
|
33
|
+
| `buttonLabel` | no | `Ask AI` | Floating button text. |
|
|
34
|
+
| `title` | no | `Ask the docs` | Modal heading. |
|
|
35
|
+
| `placeholder` | no | `Ask a question…` | Input placeholder. |
|
|
36
|
+
| `position` | no | `bottom-right` | `bottom-right` or `bottom-left`. |
|
|
37
|
+
|
|
38
|
+
## Styling
|
|
39
|
+
|
|
40
|
+
Styles use Dogsbay theme tokens with fallbacks; override the `.dba-*` classes to
|
|
41
|
+
customize.
|
|
42
|
+
|
|
43
|
+
## License
|
|
44
|
+
|
|
45
|
+
MIT.
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":""}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client runtime for `@dogsbay/plugin-ask`.
|
|
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.
|
|
8
|
+
*
|
|
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.
|
|
12
|
+
*/
|
|
13
|
+
// @ts-expect-error: virtual module resolved by the Dogsbay Vite alias at build.
|
|
14
|
+
import config from "virtual:dogsbay-plugin-config/@dogsbay/plugin-ask";
|
|
15
|
+
import { buildAskRequest, extractText } from "./protocol.js";
|
|
16
|
+
const cfg = config;
|
|
17
|
+
const ROOT_ID = "dba-root";
|
|
18
|
+
function mount() {
|
|
19
|
+
if (typeof document === "undefined")
|
|
20
|
+
return;
|
|
21
|
+
if (document.getElementById(ROOT_ID))
|
|
22
|
+
return; // already mounted
|
|
23
|
+
const root = document.createElement("div");
|
|
24
|
+
root.id = ROOT_ID;
|
|
25
|
+
root.dataset.position = cfg.position;
|
|
26
|
+
const button = document.createElement("button");
|
|
27
|
+
button.type = "button";
|
|
28
|
+
button.className = "dba-trigger";
|
|
29
|
+
button.textContent = cfg.buttonLabel;
|
|
30
|
+
button.setAttribute("aria-haspopup", "dialog");
|
|
31
|
+
const dialog = document.createElement("dialog");
|
|
32
|
+
dialog.className = "dba-dialog";
|
|
33
|
+
dialog.setAttribute("aria-label", cfg.title);
|
|
34
|
+
dialog.innerHTML = `
|
|
35
|
+
<form class="dba-form" method="dialog">
|
|
36
|
+
<header class="dba-header">
|
|
37
|
+
<h2 class="dba-title"></h2>
|
|
38
|
+
<button type="button" class="dba-close" aria-label="Close">×</button>
|
|
39
|
+
</header>
|
|
40
|
+
<div class="dba-inputrow">
|
|
41
|
+
<input class="dba-input" type="text" autocomplete="off" enterkeyhint="search" />
|
|
42
|
+
<button type="submit" class="dba-submit">Ask</button>
|
|
43
|
+
</div>
|
|
44
|
+
<div class="dba-answer" aria-live="polite" tabindex="-1"></div>
|
|
45
|
+
</form>
|
|
46
|
+
`;
|
|
47
|
+
// textContent (not innerHTML) for user-configurable strings — no injection.
|
|
48
|
+
dialog.querySelector(".dba-title").textContent = cfg.title;
|
|
49
|
+
const input = dialog.querySelector(".dba-input");
|
|
50
|
+
input.placeholder = cfg.placeholder;
|
|
51
|
+
const answer = dialog.querySelector(".dba-answer");
|
|
52
|
+
const form = dialog.querySelector(".dba-form");
|
|
53
|
+
const submit = dialog.querySelector(".dba-submit");
|
|
54
|
+
button.addEventListener("click", () => {
|
|
55
|
+
dialog.showModal();
|
|
56
|
+
input.focus();
|
|
57
|
+
});
|
|
58
|
+
dialog.querySelector(".dba-close").addEventListener("click", () => dialog.close());
|
|
59
|
+
form.addEventListener("submit", async (e) => {
|
|
60
|
+
e.preventDefault();
|
|
61
|
+
const query = input.value.trim();
|
|
62
|
+
if (!query)
|
|
63
|
+
return;
|
|
64
|
+
submit.disabled = true;
|
|
65
|
+
answer.dataset.state = "loading";
|
|
66
|
+
answer.textContent = "…";
|
|
67
|
+
try {
|
|
68
|
+
const res = await fetch(cfg.endpoint, {
|
|
69
|
+
method: "POST",
|
|
70
|
+
headers: { "Content-Type": "application/json" },
|
|
71
|
+
body: JSON.stringify(buildAskRequest(cfg.site, query, cfg.mode)),
|
|
72
|
+
});
|
|
73
|
+
if (!res.ok || !res.body) {
|
|
74
|
+
throw new Error(`Request failed (${res.status})`);
|
|
75
|
+
}
|
|
76
|
+
answer.textContent = "";
|
|
77
|
+
answer.dataset.state = "streaming";
|
|
78
|
+
const reader = res.body.getReader();
|
|
79
|
+
const decoder = new TextDecoder();
|
|
80
|
+
let buf = "";
|
|
81
|
+
for (;;) {
|
|
82
|
+
const { value, done } = await reader.read();
|
|
83
|
+
if (done)
|
|
84
|
+
break;
|
|
85
|
+
buf += decoder.decode(value, { stream: true });
|
|
86
|
+
// Only process complete lines; keep any partial line buffered so a
|
|
87
|
+
// `data:` frame split across reads isn't parsed half-formed.
|
|
88
|
+
const nl = buf.lastIndexOf("\n");
|
|
89
|
+
if (nl >= 0) {
|
|
90
|
+
answer.textContent += extractText(buf.slice(0, nl + 1));
|
|
91
|
+
buf = buf.slice(nl + 1);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (buf)
|
|
95
|
+
answer.textContent += extractText(buf);
|
|
96
|
+
answer.dataset.state = "done";
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
answer.dataset.state = "error";
|
|
100
|
+
answer.textContent = `Sorry — something went wrong. ${err.message}`;
|
|
101
|
+
}
|
|
102
|
+
finally {
|
|
103
|
+
submit.disabled = false;
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
root.append(button, dialog);
|
|
107
|
+
document.body.appendChild(root);
|
|
108
|
+
}
|
|
109
|
+
function unmount() {
|
|
110
|
+
if (typeof document === "undefined")
|
|
111
|
+
return;
|
|
112
|
+
document.getElementById(ROOT_ID)?.remove();
|
|
113
|
+
}
|
|
114
|
+
if (typeof document !== "undefined") {
|
|
115
|
+
if (document.readyState === "loading") {
|
|
116
|
+
document.addEventListener("DOMContentLoaded", mount);
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
mount();
|
|
120
|
+
}
|
|
121
|
+
document.addEventListener("astro:page-load", mount);
|
|
122
|
+
document.addEventListener("astro:before-swap", unmount);
|
|
123
|
+
}
|
|
124
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +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,CA4B7D"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@dogsbay/plugin-ask` — adds a floating "Ask AI" widget to every page of a
|
|
3
|
+
* Dogsbay docs site. The widget POSTs the user's question to a shared
|
|
4
|
+
* dogsbay-ask proxy (a Cloudflare Worker over AI Search) and streams back a
|
|
5
|
+
* grounded answer. No build-time content transform — purely a client module +
|
|
6
|
+
* styles + a build-time→client config bridge.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
*
|
|
10
|
+
* # dogsbay.config.yml
|
|
11
|
+
* plugins:
|
|
12
|
+
* - name: "@dogsbay/plugin-ask"
|
|
13
|
+
* options:
|
|
14
|
+
* endpoint: "https://dogsbay-ask.<subdomain>.workers.dev"
|
|
15
|
+
* site: "openshift-docs-dogsbay"
|
|
16
|
+
*
|
|
17
|
+
* The `endpoint` + `site` are required; everything else is presentation.
|
|
18
|
+
*/
|
|
19
|
+
import path from "node:path";
|
|
20
|
+
import { fileURLToPath } from "node:url";
|
|
21
|
+
import { parseOptions } from "./schema.js";
|
|
22
|
+
// __dirname is `dist/` for the published package. Client JS is built next to
|
|
23
|
+
// this file; styles ship un-built at the package root under `src/`.
|
|
24
|
+
const PLUGIN_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
25
|
+
const PACKAGE_ROOT = path.resolve(PLUGIN_DIR, "..");
|
|
26
|
+
const CLIENT_PATH = path.join(PLUGIN_DIR, "client.js");
|
|
27
|
+
const STYLES_PATH = path.join(PACKAGE_ROOT, "src", "styles.css");
|
|
28
|
+
export default function ask(input) {
|
|
29
|
+
const opts = parseOptions(input);
|
|
30
|
+
return {
|
|
31
|
+
name: "@dogsbay/plugin-ask",
|
|
32
|
+
clientModules() {
|
|
33
|
+
return [CLIENT_PATH];
|
|
34
|
+
},
|
|
35
|
+
styles() {
|
|
36
|
+
return [STYLES_PATH];
|
|
37
|
+
},
|
|
38
|
+
defineClientConfig() {
|
|
39
|
+
// JSON-safe runtime config exposed at
|
|
40
|
+
// virtual:dogsbay-plugin-config/@dogsbay/plugin-ask
|
|
41
|
+
return {
|
|
42
|
+
endpoint: opts.endpoint,
|
|
43
|
+
site: opts.site,
|
|
44
|
+
mode: opts.mode,
|
|
45
|
+
buttonLabel: opts.buttonLabel,
|
|
46
|
+
title: opts.title,
|
|
47
|
+
placeholder: opts.placeholder,
|
|
48
|
+
position: opts.position,
|
|
49
|
+
};
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +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;YAChB,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;aACxB,CAAC;QACJ,CAAC;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wire protocol shared between the client widget and the dogsbay-ask proxy.
|
|
3
|
+
* Kept dependency-free and DOM-free so it is unit-testable in Node.
|
|
4
|
+
*/
|
|
5
|
+
import type { AskMode } from "./schema.js";
|
|
6
|
+
export interface AskRequest {
|
|
7
|
+
site: string;
|
|
8
|
+
query: string;
|
|
9
|
+
mode: AskMode;
|
|
10
|
+
}
|
|
11
|
+
/** Build the JSON body the proxy expects. Trims the query. */
|
|
12
|
+
export declare function buildAskRequest(site: string, query: string, mode: AskMode): AskRequest;
|
|
13
|
+
/**
|
|
14
|
+
* Tolerant extraction of human-readable text from a streamed proxy chunk.
|
|
15
|
+
*
|
|
16
|
+
* The proxy passes AI Search's streamed answer straight through. Depending on
|
|
17
|
+
* the instance/model that arrives either as raw text or as SSE `data:` frames.
|
|
18
|
+
* This strips common SSE framing and ignores the `[DONE]` sentinel so the
|
|
19
|
+
* widget can append whatever is left. Returns "" for control-only lines.
|
|
20
|
+
*
|
|
21
|
+
* NOTE: citation/structured-answer rendering is finalized against a live
|
|
22
|
+
* instance — see README "Streaming format".
|
|
23
|
+
*/
|
|
24
|
+
export declare function extractText(chunk: string): string;
|
|
25
|
+
//# sourceMappingURL=protocol.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"protocol.d.ts","sourceRoot":"","sources":["../src/protocol.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAE3C,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,OAAO,CAAC;CACf;AAED,8DAA8D;AAC9D,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,UAAU,CAEtF;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CA8BjD"}
|
package/dist/protocol.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/** Build the JSON body the proxy expects. Trims the query. */
|
|
2
|
+
export function buildAskRequest(site, query, mode) {
|
|
3
|
+
return { site, query: query.trim(), mode };
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Tolerant extraction of human-readable text from a streamed proxy chunk.
|
|
7
|
+
*
|
|
8
|
+
* The proxy passes AI Search's streamed answer straight through. Depending on
|
|
9
|
+
* the instance/model that arrives either as raw text or as SSE `data:` frames.
|
|
10
|
+
* This strips common SSE framing and ignores the `[DONE]` sentinel so the
|
|
11
|
+
* widget can append whatever is left. Returns "" for control-only lines.
|
|
12
|
+
*
|
|
13
|
+
* NOTE: citation/structured-answer rendering is finalized against a live
|
|
14
|
+
* instance — see README "Streaming format".
|
|
15
|
+
*/
|
|
16
|
+
export function extractText(chunk) {
|
|
17
|
+
return chunk
|
|
18
|
+
.split(/\r?\n/)
|
|
19
|
+
.map((line) => {
|
|
20
|
+
const trimmed = line.trimStart();
|
|
21
|
+
if (trimmed === "")
|
|
22
|
+
return "";
|
|
23
|
+
// Drop SSE control framing (event:/id:/retry:/comment) — only `data:`
|
|
24
|
+
// lines carry content. A `data: [...]` chunks array parses to an array
|
|
25
|
+
// with no token field and is harmlessly skipped below.
|
|
26
|
+
if (/^(event|id|retry):/.test(trimmed) || trimmed.startsWith(":"))
|
|
27
|
+
return "";
|
|
28
|
+
if (!trimmed.startsWith("data:"))
|
|
29
|
+
return line;
|
|
30
|
+
const payload = trimmed.slice("data:".length).trim();
|
|
31
|
+
if (payload === "" || payload === "[DONE]")
|
|
32
|
+
return "";
|
|
33
|
+
// Try to pull a token out of a JSON event; else emit raw payload.
|
|
34
|
+
// Handles OpenAI-compatible chat-completions chunks
|
|
35
|
+
// ({choices:[{delta:{content}}]}) plus a few simpler shapes.
|
|
36
|
+
try {
|
|
37
|
+
const obj = JSON.parse(payload);
|
|
38
|
+
const token = obj.choices?.[0]?.delta?.content ??
|
|
39
|
+
obj.choices?.[0]?.message?.content ??
|
|
40
|
+
obj.response ??
|
|
41
|
+
obj.text ??
|
|
42
|
+
obj.delta;
|
|
43
|
+
return typeof token === "string" ? token : "";
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return payload;
|
|
47
|
+
}
|
|
48
|
+
})
|
|
49
|
+
.join("");
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=protocol.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"protocol.js","sourceRoot":"","sources":["../src/protocol.ts"],"names":[],"mappings":"AAYA,8DAA8D;AAC9D,MAAM,UAAU,eAAe,CAAC,IAAY,EAAE,KAAa,EAAE,IAAa;IACxE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,CAAC,IAAI,EAAE,EAAE,IAAI,EAAE,CAAC;AAC7C,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,WAAW,CAAC,KAAa;IACvC,OAAO,KAAK;SACT,KAAK,CAAC,OAAO,CAAC;SACd,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE;QACZ,MAAM,OAAO,GAAG,IAAI,CAAC,SAAS,EAAE,CAAC;QACjC,IAAI,OAAO,KAAK,EAAE;YAAE,OAAO,EAAE,CAAC;QAC9B,sEAAsE;QACtE,uEAAuE;QACvE,uDAAuD;QACvD,IAAI,oBAAoB,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC;YAAE,OAAO,EAAE,CAAC;QAC7E,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,OAAO,CAAC;YAAE,OAAO,IAAI,CAAC;QAC9C,MAAM,OAAO,GAAG,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC;QACrD,IAAI,OAAO,KAAK,EAAE,IAAI,OAAO,KAAK,QAAQ;YAAE,OAAO,EAAE,CAAC;QACtD,kEAAkE;QAClE,oDAAoD;QACpD,6DAA6D;QAC7D,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAwB,CAAC;YACvD,MAAM,KAAK,GACT,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,OAAO;gBAChC,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,OAAO;gBAClC,GAAG,CAAC,QAAQ;gBACZ,GAAG,CAAC,IAAI;gBACR,GAAG,CAAC,KAAK,CAAC;YACZ,OAAO,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;QAChD,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,OAAO,CAAC;QACjB,CAAC;IACH,CAAC,CAAC;SACD,IAAI,CAAC,EAAE,CAAC,CAAC;AACd,CAAC"}
|
package/dist/schema.d.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Options schema for `@dogsbay/plugin-ask`.
|
|
3
|
+
*
|
|
4
|
+
* Hand-rolled validation (no Zod dep), matching the convention in
|
|
5
|
+
* `@dogsbay/plugin-image-zoom`. `parseOptions` returns a fully-resolved
|
|
6
|
+
* options object with every field set to either the user value or the
|
|
7
|
+
* documented default. `endpoint` and `site` are REQUIRED — the widget is
|
|
8
|
+
* useless without a backend to call and a site id to route by.
|
|
9
|
+
*/
|
|
10
|
+
export type AskMode = "answer" | "search";
|
|
11
|
+
export type AskPosition = "bottom-right" | "bottom-left";
|
|
12
|
+
export interface AskOptions {
|
|
13
|
+
/**
|
|
14
|
+
* URL of the dogsbay-ask proxy (the shared Worker). Required.
|
|
15
|
+
* Example: "https://dogsbay-ask.<subdomain>.workers.dev".
|
|
16
|
+
*/
|
|
17
|
+
endpoint?: string;
|
|
18
|
+
/**
|
|
19
|
+
* Site id sent to the proxy to select the AI Search instance. Required.
|
|
20
|
+
* This is the site's GitHub Pages subfolder / Dogsbay base path, e.g.
|
|
21
|
+
* "openshift-docs-dogsbay". Must match a key in the proxy's SITES map.
|
|
22
|
+
*/
|
|
23
|
+
site?: string;
|
|
24
|
+
/** Retrieval mode: "answer" (RAG answer, default) or "search" (chunks). */
|
|
25
|
+
mode?: AskMode;
|
|
26
|
+
/** Floating button label. Default "Ask AI". */
|
|
27
|
+
buttonLabel?: string;
|
|
28
|
+
/** Modal heading. Default "Ask the docs". */
|
|
29
|
+
title?: string;
|
|
30
|
+
/** Input placeholder. Default "Ask a question…". */
|
|
31
|
+
placeholder?: string;
|
|
32
|
+
/** Corner the floating button sits in. Default "bottom-right". */
|
|
33
|
+
position?: AskPosition;
|
|
34
|
+
}
|
|
35
|
+
export interface ResolvedAskOptions {
|
|
36
|
+
endpoint: string;
|
|
37
|
+
site: string;
|
|
38
|
+
mode: AskMode;
|
|
39
|
+
buttonLabel: string;
|
|
40
|
+
title: string;
|
|
41
|
+
placeholder: string;
|
|
42
|
+
position: AskPosition;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Validate + resolve user options. Throws structured errors naming the
|
|
46
|
+
* offending field so users see a clear message rather than a stack trace.
|
|
47
|
+
*/
|
|
48
|
+
export declare function parseOptions(input: unknown): ResolvedAskOptions;
|
|
49
|
+
//# sourceMappingURL=schema.d.ts.map
|
|
@@ -0,0 +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"}
|
package/dist/schema.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Options schema for `@dogsbay/plugin-ask`.
|
|
3
|
+
*
|
|
4
|
+
* Hand-rolled validation (no Zod dep), matching the convention in
|
|
5
|
+
* `@dogsbay/plugin-image-zoom`. `parseOptions` returns a fully-resolved
|
|
6
|
+
* options object with every field set to either the user value or the
|
|
7
|
+
* documented default. `endpoint` and `site` are REQUIRED — the widget is
|
|
8
|
+
* useless without a backend to call and a site id to route by.
|
|
9
|
+
*/
|
|
10
|
+
const DEFAULTS = {
|
|
11
|
+
mode: "answer",
|
|
12
|
+
buttonLabel: "Ask AI",
|
|
13
|
+
title: "Ask the docs",
|
|
14
|
+
placeholder: "Ask a question…",
|
|
15
|
+
position: "bottom-right",
|
|
16
|
+
};
|
|
17
|
+
const PKG = "@dogsbay/plugin-ask";
|
|
18
|
+
/**
|
|
19
|
+
* Validate + resolve user options. Throws structured errors naming the
|
|
20
|
+
* offending field so users see a clear message rather than a stack trace.
|
|
21
|
+
*/
|
|
22
|
+
export function parseOptions(input) {
|
|
23
|
+
if (input === undefined || input === null || typeof input !== "object" || Array.isArray(input)) {
|
|
24
|
+
throw new Error(`${PKG}: options are required and must be an object with at least { endpoint, site }, got ${describe(input)}.`);
|
|
25
|
+
}
|
|
26
|
+
const opts = input;
|
|
27
|
+
const endpoint = requireUrl(opts.endpoint, "endpoint");
|
|
28
|
+
const site = requireNonEmptyString(opts.site, "site");
|
|
29
|
+
const out = { ...DEFAULTS, endpoint, site };
|
|
30
|
+
if (opts.mode !== undefined) {
|
|
31
|
+
if (opts.mode !== "answer" && opts.mode !== "search") {
|
|
32
|
+
throw new Error(`${PKG}: options.mode must be "answer" or "search".`);
|
|
33
|
+
}
|
|
34
|
+
out.mode = opts.mode;
|
|
35
|
+
}
|
|
36
|
+
if (opts.buttonLabel !== undefined)
|
|
37
|
+
out.buttonLabel = requireNonEmptyString(opts.buttonLabel, "buttonLabel");
|
|
38
|
+
if (opts.title !== undefined)
|
|
39
|
+
out.title = requireNonEmptyString(opts.title, "title");
|
|
40
|
+
if (opts.placeholder !== undefined)
|
|
41
|
+
out.placeholder = requireNonEmptyString(opts.placeholder, "placeholder");
|
|
42
|
+
if (opts.position !== undefined) {
|
|
43
|
+
if (opts.position !== "bottom-right" && opts.position !== "bottom-left") {
|
|
44
|
+
throw new Error(`${PKG}: options.position must be "bottom-right" or "bottom-left".`);
|
|
45
|
+
}
|
|
46
|
+
out.position = opts.position;
|
|
47
|
+
}
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
function requireNonEmptyString(value, field) {
|
|
51
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
52
|
+
throw new Error(`${PKG}: options.${field} must be a non-empty string.`);
|
|
53
|
+
}
|
|
54
|
+
return value;
|
|
55
|
+
}
|
|
56
|
+
function requireUrl(value, field) {
|
|
57
|
+
const s = requireNonEmptyString(value, field);
|
|
58
|
+
try {
|
|
59
|
+
// eslint-disable-next-line no-new
|
|
60
|
+
new URL(s);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
throw new Error(`${PKG}: options.${field} must be a valid absolute URL, got "${s}".`);
|
|
64
|
+
}
|
|
65
|
+
return s;
|
|
66
|
+
}
|
|
67
|
+
function describe(value) {
|
|
68
|
+
if (value === null)
|
|
69
|
+
return "null";
|
|
70
|
+
if (Array.isArray(value))
|
|
71
|
+
return "array";
|
|
72
|
+
return typeof value;
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=schema.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema.js","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dogsbay/plugin-ask",
|
|
3
|
+
"version": "0.2.0-beta.81",
|
|
4
|
+
"description": "Floating \"Ask AI\" widget for Dogsbay docs sites — calls a shared AI Search proxy (dogsbay-ask).",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./client": "./dist/client.js",
|
|
14
|
+
"./styles.css": "./src/styles.css"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"src/styles.css",
|
|
19
|
+
"src/client.ts",
|
|
20
|
+
"skills",
|
|
21
|
+
"README.md"
|
|
22
|
+
],
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@dogsbay/types": "0.2.0-beta.81"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"typescript": "^5.7.0",
|
|
28
|
+
"vitest": "^3.0.0",
|
|
29
|
+
"@types/node": "^22.0.0"
|
|
30
|
+
},
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"keywords": [
|
|
33
|
+
"dogsbay",
|
|
34
|
+
"dogsbay-plugin",
|
|
35
|
+
"documentation",
|
|
36
|
+
"ai-search",
|
|
37
|
+
"rag",
|
|
38
|
+
"ask-ai"
|
|
39
|
+
],
|
|
40
|
+
"repository": {
|
|
41
|
+
"type": "git",
|
|
42
|
+
"url": "https://github.com/dogsbay/dogsbay.git",
|
|
43
|
+
"directory": "packages/plugin-ask"
|
|
44
|
+
},
|
|
45
|
+
"scripts": {
|
|
46
|
+
"build": "tsc",
|
|
47
|
+
"test": "vitest run",
|
|
48
|
+
"test:watch": "vitest"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: plugin-ask-config
|
|
3
|
+
description: Configure the @dogsbay/plugin-ask "Ask AI" widget — endpoint, site id, and presentation options — in dogsbay.config.yml. Use when adding or debugging the Ask AI button on a Dogsbay docs site.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Configuring @dogsbay/plugin-ask
|
|
7
|
+
|
|
8
|
+
`@dogsbay/plugin-ask` adds a floating "Ask AI" button to every page. It sends
|
|
9
|
+
the question to a configured backend and shows the answer, so the docs site
|
|
10
|
+
stays static and holds no secrets.
|
|
11
|
+
|
|
12
|
+
## Enable it
|
|
13
|
+
|
|
14
|
+
```yaml
|
|
15
|
+
# dogsbay.config.yml
|
|
16
|
+
plugins:
|
|
17
|
+
- name: "@dogsbay/plugin-ask"
|
|
18
|
+
options:
|
|
19
|
+
endpoint: "https://<your-ask-endpoint>"
|
|
20
|
+
site: "openshift-docs-dogsbay"
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
- **`endpoint`** (required) — URL of the backend the widget calls.
|
|
24
|
+
- **`site`** (required) — the site id sent with each request so the backend
|
|
25
|
+
knows which corpus to answer from. Conventionally the site's base path /
|
|
26
|
+
GitHub Pages subfolder, e.g. `tigera-dogsbay`. The team operating the backend
|
|
27
|
+
owns the `site` → corpus mapping; use the value they give you.
|
|
28
|
+
|
|
29
|
+
Optional presentation: `mode` (`answer` | `search`), `buttonLabel`, `title`,
|
|
30
|
+
`placeholder`, `position` (`bottom-right` | `bottom-left`).
|
|
31
|
+
|
|
32
|
+
## When it isn't working
|
|
33
|
+
|
|
34
|
+
1. **Button not appearing** — confirm the plugin is listed under `plugins:` and
|
|
35
|
+
that `dogsbay site build` ran; the widget mounts via a client module on every
|
|
36
|
+
page.
|
|
37
|
+
2. **Errors when asking** — confirm `endpoint` is reachable from the browser and
|
|
38
|
+
that `site` matches the value the backend expects.
|
|
39
|
+
3. **Empty or "I don't know" answers** — the backend's corpus may not be ready;
|
|
40
|
+
check with whoever operates the endpoint.
|
|
41
|
+
|
|
42
|
+
## Styling
|
|
43
|
+
|
|
44
|
+
Styles use Dogsbay theme tokens (`--color-accent`, `--color-surface`,
|
|
45
|
+
`--radius`, …) with fallbacks, so the widget picks up the site theme. Override
|
|
46
|
+
the `.dba-*` classes in your own CSS to customize further.
|
package/src/client.ts
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client runtime for `@dogsbay/plugin-ask`.
|
|
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.
|
|
8
|
+
*
|
|
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.
|
|
12
|
+
*/
|
|
13
|
+
// @ts-expect-error: virtual module resolved by the Dogsbay Vite alias at build.
|
|
14
|
+
import config from "virtual:dogsbay-plugin-config/@dogsbay/plugin-ask";
|
|
15
|
+
import { buildAskRequest, extractText } from "./protocol.js";
|
|
16
|
+
import type { AskMode, AskPosition } from "./schema.js";
|
|
17
|
+
|
|
18
|
+
interface RuntimeConfig {
|
|
19
|
+
endpoint: string;
|
|
20
|
+
site: string;
|
|
21
|
+
mode: AskMode;
|
|
22
|
+
buttonLabel: string;
|
|
23
|
+
title: string;
|
|
24
|
+
placeholder: string;
|
|
25
|
+
position: AskPosition;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const cfg = config as RuntimeConfig;
|
|
29
|
+
|
|
30
|
+
const ROOT_ID = "dba-root";
|
|
31
|
+
|
|
32
|
+
function mount(): void {
|
|
33
|
+
if (typeof document === "undefined") return;
|
|
34
|
+
if (document.getElementById(ROOT_ID)) return; // already mounted
|
|
35
|
+
|
|
36
|
+
const root = document.createElement("div");
|
|
37
|
+
root.id = ROOT_ID;
|
|
38
|
+
root.dataset.position = cfg.position;
|
|
39
|
+
|
|
40
|
+
const button = document.createElement("button");
|
|
41
|
+
button.type = "button";
|
|
42
|
+
button.className = "dba-trigger";
|
|
43
|
+
button.textContent = cfg.buttonLabel;
|
|
44
|
+
button.setAttribute("aria-haspopup", "dialog");
|
|
45
|
+
|
|
46
|
+
const dialog = document.createElement("dialog");
|
|
47
|
+
dialog.className = "dba-dialog";
|
|
48
|
+
dialog.setAttribute("aria-label", cfg.title);
|
|
49
|
+
dialog.innerHTML = `
|
|
50
|
+
<form class="dba-form" method="dialog">
|
|
51
|
+
<header class="dba-header">
|
|
52
|
+
<h2 class="dba-title"></h2>
|
|
53
|
+
<button type="button" class="dba-close" aria-label="Close">×</button>
|
|
54
|
+
</header>
|
|
55
|
+
<div class="dba-inputrow">
|
|
56
|
+
<input class="dba-input" type="text" autocomplete="off" enterkeyhint="search" />
|
|
57
|
+
<button type="submit" class="dba-submit">Ask</button>
|
|
58
|
+
</div>
|
|
59
|
+
<div class="dba-answer" aria-live="polite" tabindex="-1"></div>
|
|
60
|
+
</form>
|
|
61
|
+
`;
|
|
62
|
+
|
|
63
|
+
// textContent (not innerHTML) for user-configurable strings — no injection.
|
|
64
|
+
dialog.querySelector<HTMLHeadingElement>(".dba-title")!.textContent = cfg.title;
|
|
65
|
+
const input = dialog.querySelector<HTMLInputElement>(".dba-input")!;
|
|
66
|
+
input.placeholder = cfg.placeholder;
|
|
67
|
+
const answer = dialog.querySelector<HTMLDivElement>(".dba-answer")!;
|
|
68
|
+
const form = dialog.querySelector<HTMLFormElement>(".dba-form")!;
|
|
69
|
+
const submit = dialog.querySelector<HTMLButtonElement>(".dba-submit")!;
|
|
70
|
+
|
|
71
|
+
button.addEventListener("click", () => {
|
|
72
|
+
dialog.showModal();
|
|
73
|
+
input.focus();
|
|
74
|
+
});
|
|
75
|
+
dialog.querySelector<HTMLButtonElement>(".dba-close")!.addEventListener("click", () => dialog.close());
|
|
76
|
+
|
|
77
|
+
form.addEventListener("submit", async (e) => {
|
|
78
|
+
e.preventDefault();
|
|
79
|
+
const query = input.value.trim();
|
|
80
|
+
if (!query) return;
|
|
81
|
+
|
|
82
|
+
submit.disabled = true;
|
|
83
|
+
answer.dataset.state = "loading";
|
|
84
|
+
answer.textContent = "…";
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
const res = await fetch(cfg.endpoint, {
|
|
88
|
+
method: "POST",
|
|
89
|
+
headers: { "Content-Type": "application/json" },
|
|
90
|
+
body: JSON.stringify(buildAskRequest(cfg.site, query, cfg.mode)),
|
|
91
|
+
});
|
|
92
|
+
if (!res.ok || !res.body) {
|
|
93
|
+
throw new Error(`Request failed (${res.status})`);
|
|
94
|
+
}
|
|
95
|
+
answer.textContent = "";
|
|
96
|
+
answer.dataset.state = "streaming";
|
|
97
|
+
const reader = res.body.getReader();
|
|
98
|
+
const decoder = new TextDecoder();
|
|
99
|
+
let buf = "";
|
|
100
|
+
for (;;) {
|
|
101
|
+
const { value, done } = await reader.read();
|
|
102
|
+
if (done) break;
|
|
103
|
+
buf += decoder.decode(value, { stream: true });
|
|
104
|
+
// Only process complete lines; keep any partial line buffered so a
|
|
105
|
+
// `data:` frame split across reads isn't parsed half-formed.
|
|
106
|
+
const nl = buf.lastIndexOf("\n");
|
|
107
|
+
if (nl >= 0) {
|
|
108
|
+
answer.textContent += extractText(buf.slice(0, nl + 1));
|
|
109
|
+
buf = buf.slice(nl + 1);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (buf) answer.textContent += extractText(buf);
|
|
113
|
+
answer.dataset.state = "done";
|
|
114
|
+
} catch (err) {
|
|
115
|
+
answer.dataset.state = "error";
|
|
116
|
+
answer.textContent = `Sorry — something went wrong. ${(err as Error).message}`;
|
|
117
|
+
} finally {
|
|
118
|
+
submit.disabled = false;
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
root.append(button, dialog);
|
|
123
|
+
document.body.appendChild(root);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function unmount(): void {
|
|
127
|
+
if (typeof document === "undefined") return;
|
|
128
|
+
document.getElementById(ROOT_ID)?.remove();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (typeof document !== "undefined") {
|
|
132
|
+
if (document.readyState === "loading") {
|
|
133
|
+
document.addEventListener("DOMContentLoaded", mount);
|
|
134
|
+
} else {
|
|
135
|
+
mount();
|
|
136
|
+
}
|
|
137
|
+
document.addEventListener("astro:page-load", mount);
|
|
138
|
+
document.addEventListener("astro:before-swap", unmount);
|
|
139
|
+
}
|
package/src/styles.css
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Styles for @dogsbay/plugin-ask. Uses Dogsbay theme tokens where available
|
|
3
|
+
* with safe fallbacks, so it themes automatically but never breaks if a token
|
|
4
|
+
* is missing. All selectors are namespaced under .dba- to avoid collisions.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
#dba-root {
|
|
8
|
+
--dba-accent: var(--color-accent, #2563eb);
|
|
9
|
+
--dba-accent-contrast: var(--color-accent-contrast, #ffffff);
|
|
10
|
+
--dba-surface: var(--color-surface, #ffffff);
|
|
11
|
+
--dba-text: var(--color-text, #1f2937);
|
|
12
|
+
--dba-border: var(--color-border, #e5e7eb);
|
|
13
|
+
--dba-radius: var(--radius, 0.5rem);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.dba-trigger {
|
|
17
|
+
position: fixed;
|
|
18
|
+
bottom: 1.25rem;
|
|
19
|
+
z-index: 60;
|
|
20
|
+
padding: 0.625rem 1rem;
|
|
21
|
+
font: inherit;
|
|
22
|
+
font-weight: 600;
|
|
23
|
+
color: var(--dba-accent-contrast);
|
|
24
|
+
background: var(--dba-accent);
|
|
25
|
+
border: none;
|
|
26
|
+
border-radius: 999px;
|
|
27
|
+
box-shadow: 0 2px 10px rgb(0 0 0 / 0.18);
|
|
28
|
+
cursor: pointer;
|
|
29
|
+
}
|
|
30
|
+
#dba-root[data-position="bottom-right"] .dba-trigger { right: 1.25rem; }
|
|
31
|
+
#dba-root[data-position="bottom-left"] .dba-trigger { left: 1.25rem; }
|
|
32
|
+
.dba-trigger:hover { filter: brightness(1.05); }
|
|
33
|
+
.dba-trigger:focus-visible { outline: 2px solid var(--dba-accent); outline-offset: 2px; }
|
|
34
|
+
|
|
35
|
+
.dba-dialog {
|
|
36
|
+
width: min(40rem, 92vw);
|
|
37
|
+
max-height: 80vh;
|
|
38
|
+
padding: 0;
|
|
39
|
+
color: var(--dba-text);
|
|
40
|
+
background: var(--dba-surface);
|
|
41
|
+
border: 1px solid var(--dba-border);
|
|
42
|
+
border-radius: var(--dba-radius);
|
|
43
|
+
box-shadow: 0 10px 40px rgb(0 0 0 / 0.25);
|
|
44
|
+
}
|
|
45
|
+
.dba-dialog::backdrop { background: rgb(0 0 0 / 0.4); }
|
|
46
|
+
|
|
47
|
+
.dba-form { display: flex; flex-direction: column; gap: 0.75rem; padding: 1rem 1.25rem 1.25rem; }
|
|
48
|
+
.dba-header { display: flex; align-items: center; justify-content: space-between; }
|
|
49
|
+
.dba-title { margin: 0; font-size: 1.05rem; }
|
|
50
|
+
.dba-close {
|
|
51
|
+
font-size: 1.5rem; line-height: 1; padding: 0 0.25rem;
|
|
52
|
+
color: inherit; background: none; border: none; cursor: pointer; opacity: 0.6;
|
|
53
|
+
}
|
|
54
|
+
.dba-close:hover { opacity: 1; }
|
|
55
|
+
|
|
56
|
+
.dba-inputrow { display: flex; gap: 0.5rem; }
|
|
57
|
+
.dba-input {
|
|
58
|
+
flex: 1; padding: 0.55rem 0.75rem; font: inherit;
|
|
59
|
+
color: var(--dba-text); background: var(--dba-surface);
|
|
60
|
+
border: 1px solid var(--dba-border); border-radius: var(--dba-radius);
|
|
61
|
+
}
|
|
62
|
+
.dba-input:focus-visible { outline: 2px solid var(--dba-accent); outline-offset: 1px; }
|
|
63
|
+
.dba-submit {
|
|
64
|
+
padding: 0.55rem 1rem; font: inherit; font-weight: 600;
|
|
65
|
+
color: var(--dba-accent-contrast); background: var(--dba-accent);
|
|
66
|
+
border: none; border-radius: var(--dba-radius); cursor: pointer;
|
|
67
|
+
}
|
|
68
|
+
.dba-submit:disabled { opacity: 0.6; cursor: progress; }
|
|
69
|
+
|
|
70
|
+
.dba-answer {
|
|
71
|
+
min-height: 2rem; max-height: 50vh; overflow-y: auto;
|
|
72
|
+
padding: 0.75rem; white-space: pre-wrap; line-height: 1.55;
|
|
73
|
+
background: color-mix(in srgb, var(--dba-text) 4%, transparent);
|
|
74
|
+
border-radius: var(--dba-radius);
|
|
75
|
+
}
|
|
76
|
+
.dba-answer[data-state="loading"] { opacity: 0.6; }
|
|
77
|
+
.dba-answer[data-state="error"] { color: #b91c1c; }
|
|
78
|
+
|
|
79
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
80
|
+
.dba-dialog[open] { animation: dba-in 0.12s ease-out; }
|
|
81
|
+
}
|
|
82
|
+
@keyframes dba-in { from { transform: translateY(6px); opacity: 0; } to { transform: none; opacity: 1; } }
|