@dogsbay/plugin-ask 0.2.0-beta.101

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/dist/index.js ADDED
@@ -0,0 +1,73 @@
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
+ // 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");
32
+ export default function ask(input) {
33
+ const opts = parseOptions(input);
34
+ return {
35
+ name: "@dogsbay/plugin-ask",
36
+ clientModules() {
37
+ return [CLIENT_PATH];
38
+ },
39
+ styles() {
40
+ return [STYLES_PATH];
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
+ },
50
+ defineClientConfig(ctx) {
51
+ // JSON-safe runtime config exposed at
52
+ // virtual:dogsbay-plugin-config/@dogsbay/plugin-ask
53
+ return {
54
+ endpoint: opts.endpoint,
55
+ site: opts.site,
56
+ mode: opts.mode,
57
+ buttonLabel: opts.buttonLabel,
58
+ title: opts.title,
59
+ placeholder: opts.placeholder,
60
+ position: opts.position,
61
+ placement: opts.placement,
62
+ conversational: opts.conversational,
63
+ maxTurns: opts.maxTurns,
64
+ persistConversation: opts.persistConversation,
65
+ feedback: opts.feedback,
66
+ // Base URL used to turn cited doc paths into links.
67
+ linkBase: ctx.config.site.url ?? "",
68
+ turnstileSitekey: opts.turnstileSitekey,
69
+ };
70
+ },
71
+ };
72
+ }
73
+ //# 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;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,cAAc,EAAE,IAAI,CAAC,cAAc;gBACnC,QAAQ,EAAE,IAAI,CAAC,QAAQ;gBACvB,mBAAmB,EAAE,IAAI,CAAC,mBAAmB;gBAC7C,QAAQ,EAAE,IAAI,CAAC,QAAQ;gBACvB,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/md.d.ts ADDED
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Minimal, dependency-free, XSS-safe Markdown → HTML renderer for LLM answers.
3
+ *
4
+ * Safety model: the entire source is HTML-escaped FIRST, so any markup in the
5
+ * answer becomes inert text; only the tags this renderer emits are real. Link
6
+ * targets are restricted to http(s)/relative/anchor (no javascript: etc.).
7
+ *
8
+ * Scope: headings, bold, italic, inline code, fenced code, ordered/unordered
9
+ * lists, links, paragraphs — enough for documentation answers. Unsupported
10
+ * constructs degrade to readable text.
11
+ */
12
+ export declare function escapeHtml(s: string): string;
13
+ export declare function renderMarkdown(src: string): string;
14
+ //# sourceMappingURL=md.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"md.d.ts","sourceRoot":"","sources":["../src/md.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,wBAAgB,UAAU,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAM5C;AAmBD,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAuClD"}
package/dist/md.js ADDED
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Minimal, dependency-free, XSS-safe Markdown → HTML renderer for LLM answers.
3
+ *
4
+ * Safety model: the entire source is HTML-escaped FIRST, so any markup in the
5
+ * answer becomes inert text; only the tags this renderer emits are real. Link
6
+ * targets are restricted to http(s)/relative/anchor (no javascript: etc.).
7
+ *
8
+ * Scope: headings, bold, italic, inline code, fenced code, ordered/unordered
9
+ * lists, links, paragraphs — enough for documentation answers. Unsupported
10
+ * constructs degrade to readable text.
11
+ */
12
+ export function escapeHtml(s) {
13
+ return s
14
+ .replace(/&/g, "&amp;")
15
+ .replace(/</g, "&lt;")
16
+ .replace(/>/g, "&gt;")
17
+ .replace(/"/g, "&quot;");
18
+ }
19
+ function isSafeUrl(u) {
20
+ // u is already HTML-escaped. Allow absolute http(s), root/relative, anchors.
21
+ return /^(https?:\/\/|\/|#|\.\/|[A-Za-z0-9._~-]+\/)/.test(u) && !/^\s*javascript:/i.test(u);
22
+ }
23
+ // Inline formatting on already-escaped text.
24
+ function inline(text) {
25
+ let t = text.replace(/`([^`]+)`/g, (_m, c) => `<code>${c}</code>`);
26
+ t = t.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, (_m, label, url) => isSafeUrl(url) ? `<a href="${url}">${label}</a>` : label);
27
+ t = t.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
28
+ t = t.replace(/(^|[^*])\*([^*\n]+)\*/g, "$1<em>$2</em>");
29
+ t = t.replace(/(^|[^_])_([^_\n]+)_/g, "$1<em>$2</em>");
30
+ return t;
31
+ }
32
+ export function renderMarkdown(src) {
33
+ const lines = escapeHtml(src).split(/\r?\n/);
34
+ let html = "";
35
+ let ul = false;
36
+ let ol = false;
37
+ const closeLists = () => {
38
+ if (ul) {
39
+ html += "</ul>";
40
+ ul = false;
41
+ }
42
+ if (ol) {
43
+ html += "</ol>";
44
+ ol = false;
45
+ }
46
+ };
47
+ let i = 0;
48
+ while (i < lines.length) {
49
+ const line = lines[i];
50
+ if (/^\s*```/.test(line)) {
51
+ closeLists();
52
+ i++;
53
+ let code = "";
54
+ while (i < lines.length && !/^\s*```/.test(lines[i])) {
55
+ code += lines[i] + "\n";
56
+ i++;
57
+ }
58
+ i++; // closing fence
59
+ html += `<pre><code>${code}</code></pre>`;
60
+ continue;
61
+ }
62
+ const h = line.match(/^(#{1,6})\s+(.*)$/);
63
+ if (h) {
64
+ closeLists();
65
+ const lvl = h[1].length;
66
+ html += `<h${lvl}>${inline(h[2])}</h${lvl}>`;
67
+ i++;
68
+ continue;
69
+ }
70
+ const uli = line.match(/^\s*[-*+]\s+(.*)$/);
71
+ if (uli) {
72
+ if (ol) {
73
+ html += "</ol>";
74
+ ol = false;
75
+ }
76
+ if (!ul) {
77
+ html += "<ul>";
78
+ ul = true;
79
+ }
80
+ html += `<li>${inline(uli[1])}</li>`;
81
+ i++;
82
+ continue;
83
+ }
84
+ const oli = line.match(/^\s*\d+\.\s+(.*)$/);
85
+ if (oli) {
86
+ if (ul) {
87
+ html += "</ul>";
88
+ ul = false;
89
+ }
90
+ if (!ol) {
91
+ html += "<ol>";
92
+ ol = true;
93
+ }
94
+ html += `<li>${inline(oli[1])}</li>`;
95
+ i++;
96
+ continue;
97
+ }
98
+ if (line.trim() === "") {
99
+ closeLists();
100
+ i++;
101
+ continue;
102
+ }
103
+ closeLists();
104
+ let para = line;
105
+ i++;
106
+ while (i < lines.length && lines[i].trim() !== "" && !/^(\s*```|#{1,6}\s|\s*[-*+]\s|\s*\d+\.\s)/.test(lines[i])) {
107
+ para += " " + lines[i];
108
+ i++;
109
+ }
110
+ html += `<p>${inline(para)}</p>`;
111
+ }
112
+ closeLists();
113
+ return html;
114
+ }
115
+ //# sourceMappingURL=md.js.map
package/dist/md.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"md.js","sourceRoot":"","sources":["../src/md.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,74 @@
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
+ export interface ChatMessage {
12
+ role: "user" | "assistant";
13
+ content: string;
14
+ }
15
+ export interface ChatRequest {
16
+ site: string;
17
+ mode: "chat";
18
+ messages: ChatMessage[];
19
+ }
20
+ /** Build the JSON body the proxy expects. Trims the query. */
21
+ export declare function buildAskRequest(site: string, query: string, mode: AskMode): AskRequest;
22
+ /**
23
+ * Whether a failed request should be retried once after obtaining a Turnstile
24
+ * token. The proxy 403s when a Turnstile secret is configured but no valid
25
+ * token arrived — which happens if the widget submits before the bot token has
26
+ * resolved (the docked panel lazy-loads Turnstile on focus). Retry only that
27
+ * case: a 403 while Turnstile is enabled on this site.
28
+ */
29
+ export declare function shouldRetryTurnstile(status: number, turnstileEnabled: boolean): boolean;
30
+ /** Build the JSON body for a multi-turn chat request. */
31
+ export declare function buildChatRequest(site: string, messages: ChatMessage[]): ChatRequest;
32
+ export interface FeedbackRequest {
33
+ type: "feedback";
34
+ askId: string;
35
+ rating: 1 | -1;
36
+ }
37
+ /**
38
+ * Build the JSON body for a thumbs up/down vote on a prior answer. `askId` is
39
+ * the `X-Ask-Id` the proxy stamped on that answer; `rating` is +1 (helpful) or
40
+ * -1 (not helpful). The proxy records it only when analytics is configured.
41
+ */
42
+ export declare function buildFeedbackRequest(askId: string, rating: 1 | -1): FeedbackRequest;
43
+ /**
44
+ * Trim conversation history to the last `maxTurns` messages, never leading with
45
+ * an `assistant` turn — the proxy prepends the `system` message, so the first
46
+ * sent message must be a `user`. Returns a new array.
47
+ */
48
+ export declare function trimHistory(messages: ChatMessage[], maxTurns: number): ChatMessage[];
49
+ /**
50
+ * Tolerant extraction of human-readable text from a streamed proxy chunk.
51
+ *
52
+ * The proxy passes AI Search's streamed answer straight through. Depending on
53
+ * the instance/model that arrives either as raw text or as SSE `data:` frames.
54
+ * This strips common SSE framing and ignores the `[DONE]` sentinel so the
55
+ * widget can append whatever is left. Returns "" for control-only lines.
56
+ *
57
+ * NOTE: citation/structured-answer rendering is finalized against a live
58
+ * instance — see README "Streaming format".
59
+ */
60
+ export declare function extractText(chunk: string): string;
61
+ export interface Source {
62
+ key: string;
63
+ label: string;
64
+ href: string | null;
65
+ }
66
+ /**
67
+ * Map an AI Search item key (a "__"-encoded doc path) back to a doc link.
68
+ * Non-lossy keys reverse to `<linkBase>/<path>`; lossy (hash-suffixed) keys
69
+ * can't be reconstructed, so they get a readable label with no href.
70
+ */
71
+ export declare function sourceLink(key: string, linkBase: string): Source;
72
+ /** Find the AI Search `event: chunks` frame in a raw stream and map it to deduped sources. */
73
+ export declare function parseSources(rawStream: string, linkBase: string): Source[];
74
+ //# 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,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,GAAG,WAAW,CAAC;IAC3B,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,WAAW,EAAE,CAAC;CACzB;AAED,8DAA8D;AAC9D,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,UAAU,CAEtF;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,EAAE,gBAAgB,EAAE,OAAO,GAAG,OAAO,CAEvF;AAED,yDAAyD;AACzD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,WAAW,EAAE,GAAG,WAAW,CAEnF;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,UAAU,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;CAChB;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,GAAG,eAAe,CAEnF;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,WAAW,EAAE,EAAE,QAAQ,EAAE,MAAM,GAAG,WAAW,EAAE,CAMpF;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CA8BjD;AAED,MAAM,WAAW,MAAM;IACrB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CACrB;AAUD;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAUhE;AAED,8FAA8F;AAC9F,wBAAgB,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,EAAE,CAsB1E"}
@@ -0,0 +1,136 @@
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
+ * Whether a failed request should be retried once after obtaining a Turnstile
7
+ * token. The proxy 403s when a Turnstile secret is configured but no valid
8
+ * token arrived — which happens if the widget submits before the bot token has
9
+ * resolved (the docked panel lazy-loads Turnstile on focus). Retry only that
10
+ * case: a 403 while Turnstile is enabled on this site.
11
+ */
12
+ export function shouldRetryTurnstile(status, turnstileEnabled) {
13
+ return status === 403 && turnstileEnabled;
14
+ }
15
+ /** Build the JSON body for a multi-turn chat request. */
16
+ export function buildChatRequest(site, messages) {
17
+ return { site, mode: "chat", messages };
18
+ }
19
+ /**
20
+ * Build the JSON body for a thumbs up/down vote on a prior answer. `askId` is
21
+ * the `X-Ask-Id` the proxy stamped on that answer; `rating` is +1 (helpful) or
22
+ * -1 (not helpful). The proxy records it only when analytics is configured.
23
+ */
24
+ export function buildFeedbackRequest(askId, rating) {
25
+ return { type: "feedback", askId, rating };
26
+ }
27
+ /**
28
+ * Trim conversation history to the last `maxTurns` messages, never leading with
29
+ * an `assistant` turn — the proxy prepends the `system` message, so the first
30
+ * sent message must be a `user`. Returns a new array.
31
+ */
32
+ export function trimHistory(messages, maxTurns) {
33
+ let trimmed = messages.slice(-Math.max(1, maxTurns));
34
+ while (trimmed.length > 0 && trimmed[0].role === "assistant") {
35
+ trimmed = trimmed.slice(1);
36
+ }
37
+ return trimmed;
38
+ }
39
+ /**
40
+ * Tolerant extraction of human-readable text from a streamed proxy chunk.
41
+ *
42
+ * The proxy passes AI Search's streamed answer straight through. Depending on
43
+ * the instance/model that arrives either as raw text or as SSE `data:` frames.
44
+ * This strips common SSE framing and ignores the `[DONE]` sentinel so the
45
+ * widget can append whatever is left. Returns "" for control-only lines.
46
+ *
47
+ * NOTE: citation/structured-answer rendering is finalized against a live
48
+ * instance — see README "Streaming format".
49
+ */
50
+ export function extractText(chunk) {
51
+ return chunk
52
+ .split(/\r?\n/)
53
+ .map((line) => {
54
+ const trimmed = line.trimStart();
55
+ if (trimmed === "")
56
+ return "";
57
+ // Drop SSE control framing (event:/id:/retry:/comment) — only `data:`
58
+ // lines carry content. A `data: [...]` chunks array parses to an array
59
+ // with no token field and is harmlessly skipped below.
60
+ if (/^(event|id|retry):/.test(trimmed) || trimmed.startsWith(":"))
61
+ return "";
62
+ if (!trimmed.startsWith("data:"))
63
+ return line;
64
+ const payload = trimmed.slice("data:".length).trim();
65
+ if (payload === "" || payload === "[DONE]")
66
+ return "";
67
+ // Try to pull a token out of a JSON event; else emit raw payload.
68
+ // Handles OpenAI-compatible chat-completions chunks
69
+ // ({choices:[{delta:{content}}]}) plus a few simpler shapes.
70
+ try {
71
+ const obj = JSON.parse(payload);
72
+ const token = obj.choices?.[0]?.delta?.content ??
73
+ obj.choices?.[0]?.message?.content ??
74
+ obj.response ??
75
+ obj.text ??
76
+ obj.delta;
77
+ return typeof token === "string" ? token : "";
78
+ }
79
+ catch {
80
+ return payload;
81
+ }
82
+ })
83
+ .join("");
84
+ }
85
+ // Ingest-script lossy shortener produces `<head>__<leaf>__<8hex>.md` for paths
86
+ // too long to fit the filename limit — those can't be reversed to a real URL.
87
+ const LOSSY = /__[0-9a-f]{8}\.md$/;
88
+ function humanize(seg) {
89
+ return seg.replace(/[-_]+/g, " ").trim();
90
+ }
91
+ /**
92
+ * Map an AI Search item key (a "__"-encoded doc path) back to a doc link.
93
+ * Non-lossy keys reverse to `<linkBase>/<path>`; lossy (hash-suffixed) keys
94
+ * can't be reconstructed, so they get a readable label with no href.
95
+ */
96
+ export function sourceLink(key, linkBase) {
97
+ const base = (linkBase || "").replace(/\/+$/, "");
98
+ if (LOSSY.test(key)) {
99
+ const parts = key.replace(/\.md$/, "").split("__");
100
+ const leaf = parts.length >= 2 ? parts[parts.length - 2] : parts[0];
101
+ return { key, label: humanize(leaf), href: null };
102
+ }
103
+ const path = key.replace(/\.md$/, "").split("__").join("/");
104
+ const leaf = path.split("/").pop() || path;
105
+ return { key, label: humanize(leaf), href: base ? `${base}/${path}` : `/${path}` };
106
+ }
107
+ /** Find the AI Search `event: chunks` frame in a raw stream and map it to deduped sources. */
108
+ export function parseSources(rawStream, linkBase) {
109
+ for (const raw of rawStream.split(/\r?\n/)) {
110
+ const line = raw.trimStart();
111
+ if (!line.startsWith("data:"))
112
+ continue;
113
+ const payload = line.slice("data:".length).trim();
114
+ if (!payload.startsWith("["))
115
+ continue;
116
+ try {
117
+ const arr = JSON.parse(payload);
118
+ const seen = new Set();
119
+ const out = [];
120
+ for (const c of arr) {
121
+ const key = typeof c.item === "string" ? c.item : c.item?.key;
122
+ if (!key || seen.has(key))
123
+ continue;
124
+ seen.add(key);
125
+ out.push(sourceLink(key, linkBase));
126
+ }
127
+ if (out.length)
128
+ return out;
129
+ }
130
+ catch {
131
+ // not the chunks frame; keep scanning
132
+ }
133
+ }
134
+ return [];
135
+ }
136
+ //# sourceMappingURL=protocol.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"protocol.js","sourceRoot":"","sources":["../src/protocol.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,100 @@
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
+ /**
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";
21
+ export interface AskOptions {
22
+ /**
23
+ * URL of the dogsbay-ask proxy (the shared Worker). Required.
24
+ * Example: "https://dogsbay-ask.<subdomain>.workers.dev".
25
+ */
26
+ endpoint?: string;
27
+ /**
28
+ * Site id sent to the proxy to select the AI Search instance. Required.
29
+ * This is the site's GitHub Pages subfolder / Dogsbay base path, e.g.
30
+ * "openshift-docs-dogsbay". Must match a key in the proxy's SITES map.
31
+ */
32
+ site?: string;
33
+ /** Retrieval mode: "answer" (RAG answer, default) or "search" (chunks). */
34
+ mode?: AskMode;
35
+ /**
36
+ * Multi-turn conversation: keep prior turns on screen and send them as
37
+ * context so follow-ups are understood. Default `true`. Set `false` for the
38
+ * single-shot behaviour (one question → one answer, no history).
39
+ */
40
+ conversational?: boolean;
41
+ /**
42
+ * Max prior messages sent as context when `conversational` (bounds prompt
43
+ * size / cost / latency). Positive integer; default `8`.
44
+ */
45
+ maxTurns?: number;
46
+ /**
47
+ * Persist the conversation in `sessionStorage` (tab-scoped, cleared when the
48
+ * tab closes) so it survives a page refresh and same-window navigation (e.g.
49
+ * clicking a source link). Default `true`. Readers can still opt out via the
50
+ * widget's "Remember conversation" toggle. Set `false` to disable persistence
51
+ * site-wide. Only applies in `conversational` answer mode.
52
+ */
53
+ persistConversation?: boolean;
54
+ /**
55
+ * Show a 👍/👎 helpfulness control under each answer. The vote is sent to the
56
+ * proxy referencing the answer's id; the proxy stores it only if analytics is
57
+ * configured (otherwise it is a no-op). Default `true`. Set `false` to hide.
58
+ */
59
+ feedback?: boolean;
60
+ /** Floating button label. Default "Ask AI". */
61
+ buttonLabel?: string;
62
+ /** Modal heading. Default "Ask the docs". */
63
+ title?: string;
64
+ /** Input placeholder. Default "Ask a question…". */
65
+ placeholder?: string;
66
+ /** Corner the floating button sits in. Default "bottom-right". */
67
+ position?: AskPosition;
68
+ /**
69
+ * Where the widget renders: docked rail panel, floating modal, or `auto`.
70
+ * Default "auto". See {@link AskPlacement}.
71
+ */
72
+ placement?: AskPlacement;
73
+ /**
74
+ * Cloudflare Turnstile sitekey. If set, the widget obtains a Turnstile
75
+ * token and sends it with each request; the proxy verifies it. Omit to
76
+ * disable the bot check.
77
+ */
78
+ turnstileSitekey?: string;
79
+ }
80
+ export interface ResolvedAskOptions {
81
+ endpoint: string;
82
+ site: string;
83
+ mode: AskMode;
84
+ buttonLabel: string;
85
+ title: string;
86
+ placeholder: string;
87
+ position: AskPosition;
88
+ placement: AskPlacement;
89
+ conversational: boolean;
90
+ maxTurns: number;
91
+ persistConversation: boolean;
92
+ feedback: boolean;
93
+ turnstileSitekey: string;
94
+ }
95
+ /**
96
+ * Validate + resolve user options. Throws structured errors naming the
97
+ * offending field so users see a clear message rather than a stack trace.
98
+ */
99
+ export declare function parseOptions(input: unknown): ResolvedAskOptions;
100
+ //# 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;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;;;;OAIG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;;;OAMG;IACH,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B;;;;OAIG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,+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,cAAc,EAAE,OAAO,CAAC;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,mBAAmB,EAAE,OAAO,CAAC;IAC7B,QAAQ,EAAE,OAAO,CAAC;IAClB,gBAAgB,EAAE,MAAM,CAAC;CAC1B;AAkBD;;;GAGG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,kBAAkB,CA+D/D"}
package/dist/schema.js ADDED
@@ -0,0 +1,113 @@
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
+ placement: "auto",
17
+ conversational: true,
18
+ maxTurns: 8,
19
+ persistConversation: true,
20
+ feedback: true,
21
+ turnstileSitekey: "",
22
+ };
23
+ const PKG = "@dogsbay/plugin-ask";
24
+ /**
25
+ * Validate + resolve user options. Throws structured errors naming the
26
+ * offending field so users see a clear message rather than a stack trace.
27
+ */
28
+ export function parseOptions(input) {
29
+ if (input === undefined || input === null || typeof input !== "object" || Array.isArray(input)) {
30
+ throw new Error(`${PKG}: options are required and must be an object with at least { endpoint, site }, got ${describe(input)}.`);
31
+ }
32
+ const opts = input;
33
+ const endpoint = requireUrl(opts.endpoint, "endpoint");
34
+ const site = requireNonEmptyString(opts.site, "site");
35
+ const out = { ...DEFAULTS, endpoint, site };
36
+ if (opts.mode !== undefined) {
37
+ if (opts.mode !== "answer" && opts.mode !== "search") {
38
+ throw new Error(`${PKG}: options.mode must be "answer" or "search".`);
39
+ }
40
+ out.mode = opts.mode;
41
+ }
42
+ if (opts.buttonLabel !== undefined)
43
+ out.buttonLabel = requireNonEmptyString(opts.buttonLabel, "buttonLabel");
44
+ if (opts.title !== undefined)
45
+ out.title = requireNonEmptyString(opts.title, "title");
46
+ if (opts.placeholder !== undefined)
47
+ out.placeholder = requireNonEmptyString(opts.placeholder, "placeholder");
48
+ if (opts.position !== undefined) {
49
+ if (opts.position !== "bottom-right" && opts.position !== "bottom-left") {
50
+ throw new Error(`${PKG}: options.position must be "bottom-right" or "bottom-left".`);
51
+ }
52
+ out.position = opts.position;
53
+ }
54
+ if (opts.placement !== undefined) {
55
+ if (opts.placement !== "auto" && opts.placement !== "rail" && opts.placement !== "floating") {
56
+ throw new Error(`${PKG}: options.placement must be "auto", "rail", or "floating".`);
57
+ }
58
+ out.placement = opts.placement;
59
+ }
60
+ if (opts.conversational !== undefined) {
61
+ if (typeof opts.conversational !== "boolean") {
62
+ throw new Error(`${PKG}: options.conversational must be a boolean.`);
63
+ }
64
+ out.conversational = opts.conversational;
65
+ }
66
+ if (opts.maxTurns !== undefined) {
67
+ if (typeof opts.maxTurns !== "number" || !Number.isInteger(opts.maxTurns) || opts.maxTurns < 1) {
68
+ throw new Error(`${PKG}: options.maxTurns must be a positive integer.`);
69
+ }
70
+ out.maxTurns = opts.maxTurns;
71
+ }
72
+ if (opts.persistConversation !== undefined) {
73
+ if (typeof opts.persistConversation !== "boolean") {
74
+ throw new Error(`${PKG}: options.persistConversation must be a boolean.`);
75
+ }
76
+ out.persistConversation = opts.persistConversation;
77
+ }
78
+ if (opts.feedback !== undefined) {
79
+ if (typeof opts.feedback !== "boolean") {
80
+ throw new Error(`${PKG}: options.feedback must be a boolean.`);
81
+ }
82
+ out.feedback = opts.feedback;
83
+ }
84
+ if (opts.turnstileSitekey !== undefined) {
85
+ out.turnstileSitekey = requireNonEmptyString(opts.turnstileSitekey, "turnstileSitekey");
86
+ }
87
+ return out;
88
+ }
89
+ function requireNonEmptyString(value, field) {
90
+ if (typeof value !== "string" || value.trim() === "") {
91
+ throw new Error(`${PKG}: options.${field} must be a non-empty string.`);
92
+ }
93
+ return value;
94
+ }
95
+ function requireUrl(value, field) {
96
+ const s = requireNonEmptyString(value, field);
97
+ try {
98
+ // eslint-disable-next-line no-new
99
+ new URL(s);
100
+ }
101
+ catch {
102
+ throw new Error(`${PKG}: options.${field} must be a valid absolute URL, got "${s}".`);
103
+ }
104
+ return s;
105
+ }
106
+ function describe(value) {
107
+ if (value === null)
108
+ return "null";
109
+ if (Array.isArray(value))
110
+ return "array";
111
+ return typeof value;
112
+ }
113
+ //# sourceMappingURL=schema.js.map