@sendystack/widget 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,43 @@
1
+ # @sendystack/widget
2
+
3
+ Universal AI chat widget for any website. Add it once and it:
4
+
5
+ - renders a chat bubble, themed from appearance you manage at [app.sendystack.org](https://app.sendystack.org)
6
+ - answers visitor questions using your site's knowledge base (Gemini/Claude/ChatGPT-backed)
7
+ - indexes the current page automatically, and crawls the rest of the site in the background
8
+ - feeds everything it learns into Claude, ChatGPT, and Gemini via MCP
9
+
10
+ Get an embed token from the **Sources** page in the dashboard (Add site → Any website).
11
+
12
+ ## Plain `<script>` tag (no build step)
13
+
14
+ ```html
15
+ <script async src="https://app.sendystack.org/widget.js" data-token="emb_xxx"></script>
16
+ ```
17
+
18
+ That's it — the widget reads its own `data-token` attribute and initializes itself.
19
+
20
+ ## npm / bundler
21
+
22
+ ```bash
23
+ npm install @sendystack/widget
24
+ ```
25
+
26
+ ```ts
27
+ import { init } from "@sendystack/widget"
28
+
29
+ init({ token: "emb_xxx" })
30
+ ```
31
+
32
+ ## How it works
33
+
34
+ - **Appearance** (colors, position, assistant name, welcome message) is fetched from your
35
+ dashboard on load — there's nothing to configure in code.
36
+ - **Chat** goes through your workspace's connected AI provider key; replies are grounded in
37
+ whatever's been indexed for this site.
38
+ - **Indexing**: every page the widget loads on gets reported automatically. A full site crawl
39
+ also kicks off in the background (throttled server-side to once per 24h) so the rest of the
40
+ site gets indexed even on pages that never get a visit.
41
+
42
+ Each embed token is scoped to exactly one site — pasting it on a different domain doesn't grant
43
+ access to any other site's data in your workspace.
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Sendystack universal embed widget. Works as:
3
+ * - a plain <script data-token="emb_..."> tag (auto-inits from its own tag), or
4
+ * - an npm import: `import { init } from "@sendystack/widget"`.
5
+ *
6
+ * On load it fetches appearance (never secrets) from the dashboard, renders
7
+ * the bubble+panel, wires chat through the embed-token `answer` endpoint,
8
+ * reports the current page's text to `ingest` so the page is searchable
9
+ * immediately, and fires a throttled `crawlSite` request so the rest of the
10
+ * site gets indexed in the background without the visitor waiting on it.
11
+ */
12
+ export type SendystackOptions = {
13
+ token: string;
14
+ apiBase?: string;
15
+ };
16
+ export declare function init(options: SendystackOptions): Promise<void>;
@@ -0,0 +1,170 @@
1
+ //#region src/index.ts
2
+ var e = "sendystack-widget-root", t = (e) => `sendystack_session_${e}`, n = (e) => `sendystack_crawled_${e}`;
3
+ async function r(e, t) {
4
+ let n = await (await fetch(`${e}/embedConfig?embedToken=${encodeURIComponent(t)}`)).json();
5
+ if (!n.ok) throw Error(n.error || "failed to load widget config");
6
+ return n.appearance;
7
+ }
8
+ function i(t) {
9
+ let n = t.position === "left" ? "left" : "right", r = document.createElement("style");
10
+ r.textContent = `
11
+ #${e} *{box-sizing:border-box;}
12
+ .ssk-bubble{position:fixed;${n}:20px;bottom:20px;width:58px;height:58px;border-radius:50%;
13
+ background:linear-gradient(135deg,${t.accent2},${t.accent});color:#fff;display:grid;
14
+ place-items:center;cursor:pointer;box-shadow:0 12px 28px rgba(0,0,0,.22);z-index:2147483000;border:none;font-size:24px;}
15
+ .ssk-panel{position:fixed;${n}:20px;bottom:90px;width:340px;max-height:70vh;background:#fff;border-radius:16px;
16
+ box-shadow:0 24px 60px rgba(0,0,0,.25);display:none;flex-direction:column;overflow:hidden;z-index:2147483000;
17
+ font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;}
18
+ .ssk-panel.open{display:flex;}
19
+ .ssk-head{background:linear-gradient(135deg,${t.accent2},${t.accent});color:#fff;padding:14px 16px;
20
+ display:flex;align-items:center;justify-content:space-between;font-weight:700;font-size:14px;flex-shrink:0;}
21
+ .ssk-head button{background:none;border:none;color:#fff;font-size:16px;cursor:pointer;opacity:.85;padding:0;}
22
+ .ssk-body{flex:1;overflow-y:auto;padding:14px;background:#faf9f7;display:flex;flex-direction:column;gap:8px;min-height:200px;}
23
+ .ssk-msg{max-width:85%;padding:9px 12px;border-radius:12px;font-size:13.5px;line-height:1.45;white-space:pre-wrap;}
24
+ .ssk-msg.bot{background:#fff;border:1px solid rgba(0,0,0,.08);align-self:flex-start;color:#1b2733;}
25
+ .ssk-msg.user{background:${t.accent};color:#fff;align-self:flex-end;}
26
+ .ssk-inputrow{display:flex;border-top:1px solid rgba(0,0,0,.08);padding:8px;gap:6px;flex-shrink:0;}
27
+ .ssk-inputrow input{flex:1;border:1px solid rgba(0,0,0,.12);border-radius:10px;padding:9px 11px;font-size:13.5px;outline:none;}
28
+ .ssk-inputrow button{background:${t.accent};color:#fff;border:none;border-radius:10px;padding:0 14px;cursor:pointer;font-weight:700;}
29
+ `, document.head.appendChild(r);
30
+ }
31
+ function a(e, t) {
32
+ let n = document.createElement("div");
33
+ n.className = `ssk-msg ${t.role}`, n.textContent = t.text, e.appendChild(n), e.scrollTop = e.scrollHeight;
34
+ }
35
+ function o(e, n) {
36
+ let r = Array.from(n.children).map((e) => ({
37
+ role: e.classList.contains("user") ? "user" : "bot",
38
+ text: e.textContent || ""
39
+ }));
40
+ try {
41
+ sessionStorage.setItem(t(e), JSON.stringify(r.slice(-40)));
42
+ } catch {}
43
+ }
44
+ function s(e) {
45
+ try {
46
+ return JSON.parse(sessionStorage.getItem(t(e)) || "[]");
47
+ } catch {
48
+ return [];
49
+ }
50
+ }
51
+ function c() {
52
+ let t = document.body.cloneNode(!0);
53
+ t.querySelectorAll(`script,style,nav,header,footer,svg,noscript,#${e}`).forEach((e) => e.remove());
54
+ let n = (t.textContent || "").replace(/\s+/g, " ").trim();
55
+ return {
56
+ title: document.title,
57
+ text: n
58
+ };
59
+ }
60
+ async function l(e, t) {
61
+ let { title: n, text: r } = c();
62
+ r.length < 40 || await fetch(`${e}/ingest`, {
63
+ method: "POST",
64
+ headers: { "Content-Type": "application/json" },
65
+ body: JSON.stringify({
66
+ embedToken: t,
67
+ items: [{
68
+ url: location.href,
69
+ title: n,
70
+ text: r
71
+ }]
72
+ })
73
+ }).catch(() => {});
74
+ }
75
+ async function u(e, t) {
76
+ let r = !1;
77
+ try {
78
+ r = !!sessionStorage.getItem(n(t)), sessionStorage.setItem(n(t), "1");
79
+ } catch {}
80
+ r || await fetch(`${e}/crawlSite`, {
81
+ method: "POST",
82
+ headers: { "Content-Type": "application/json" },
83
+ body: JSON.stringify({ embedToken: t })
84
+ }).catch(() => {});
85
+ }
86
+ async function d(t) {
87
+ if (typeof document > "u" || document.getElementById(e)) return;
88
+ let n = (t.token || "").trim();
89
+ if (!n) {
90
+ console.error("[Sendystack] init() called without a token");
91
+ return;
92
+ }
93
+ let c = t.apiBase || "https://us-central1-sendystack-fab32.cloudfunctions.net", d;
94
+ try {
95
+ d = await r(c, n);
96
+ } catch (e) {
97
+ console.error("[Sendystack] failed to load widget config:", e);
98
+ return;
99
+ }
100
+ i(d);
101
+ let f = document.createElement("div");
102
+ f.id = e;
103
+ let p = document.createElement("div");
104
+ p.className = "ssk-body";
105
+ let m = s(n);
106
+ if (m.length) for (let e of m) a(p, e);
107
+ else a(p, {
108
+ role: "bot",
109
+ text: d.welcomeMessage
110
+ });
111
+ let h = document.createElement("div");
112
+ h.className = "ssk-head";
113
+ let g = document.createElement("span");
114
+ g.textContent = d.assistantName;
115
+ let _ = document.createElement("button");
116
+ _.textContent = "✕", h.append(g, _);
117
+ let v = document.createElement("input");
118
+ v.type = "text", v.placeholder = "Ask anything…";
119
+ let y = document.createElement("button");
120
+ y.textContent = "Send";
121
+ let b = document.createElement("div");
122
+ b.className = "ssk-inputrow", b.append(v, y);
123
+ let x = document.createElement("div");
124
+ x.className = "ssk-panel", x.append(h, p, b);
125
+ let S = document.createElement("button");
126
+ S.className = "ssk-bubble", S.textContent = "💬", S.setAttribute("aria-label", "Open chat"), f.append(x, S), document.body.appendChild(f), S.onclick = () => x.classList.toggle("open"), _.onclick = () => x.classList.remove("open");
127
+ async function C() {
128
+ let e = v.value.trim();
129
+ if (e) {
130
+ v.value = "", a(p, {
131
+ role: "user",
132
+ text: e
133
+ }), o(n, p), y.disabled = !0;
134
+ try {
135
+ let t = await (await fetch(`${c}/answer`, {
136
+ method: "POST",
137
+ headers: { "Content-Type": "application/json" },
138
+ body: JSON.stringify({
139
+ embedToken: n,
140
+ query: e
141
+ })
142
+ })).json();
143
+ a(p, {
144
+ role: "bot",
145
+ text: t.ok ? t.reply : "Sorry, I couldn't process that right now."
146
+ });
147
+ } catch {
148
+ a(p, {
149
+ role: "bot",
150
+ text: "Sorry, something went wrong. Please try again."
151
+ });
152
+ } finally {
153
+ y.disabled = !1, o(n, p);
154
+ }
155
+ }
156
+ }
157
+ y.onclick = C, v.addEventListener("keydown", (e) => {
158
+ e.key === "Enter" && C();
159
+ }), l(c, n), u(c, n);
160
+ }
161
+ function f() {
162
+ let e = document.currentScript || Array.from(document.getElementsByTagName("script")).find((e) => /widget(\.global)?\.js/.test(e.src)), t = e?.dataset.token;
163
+ t && d({
164
+ token: t,
165
+ apiBase: e?.dataset.apiBase
166
+ });
167
+ }
168
+ typeof document < "u" && f();
169
+ //#endregion
170
+ export { d as init };
@@ -0,0 +1,20 @@
1
+ var Sendystack=(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:`Module`});var t=`https://us-central1-sendystack-fab32.cloudfunctions.net`,n=`sendystack-widget-root`,r=e=>`sendystack_session_${e}`,i=e=>`sendystack_crawled_${e}`;async function a(e,t){let n=await(await fetch(`${e}/embedConfig?embedToken=${encodeURIComponent(t)}`)).json();if(!n.ok)throw Error(n.error||`failed to load widget config`);return n.appearance}function o(e){let t=e.position===`left`?`left`:`right`,r=document.createElement(`style`);r.textContent=`
2
+ #${n} *{box-sizing:border-box;}
3
+ .ssk-bubble{position:fixed;${t}:20px;bottom:20px;width:58px;height:58px;border-radius:50%;
4
+ background:linear-gradient(135deg,${e.accent2},${e.accent});color:#fff;display:grid;
5
+ place-items:center;cursor:pointer;box-shadow:0 12px 28px rgba(0,0,0,.22);z-index:2147483000;border:none;font-size:24px;}
6
+ .ssk-panel{position:fixed;${t}:20px;bottom:90px;width:340px;max-height:70vh;background:#fff;border-radius:16px;
7
+ box-shadow:0 24px 60px rgba(0,0,0,.25);display:none;flex-direction:column;overflow:hidden;z-index:2147483000;
8
+ font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;}
9
+ .ssk-panel.open{display:flex;}
10
+ .ssk-head{background:linear-gradient(135deg,${e.accent2},${e.accent});color:#fff;padding:14px 16px;
11
+ display:flex;align-items:center;justify-content:space-between;font-weight:700;font-size:14px;flex-shrink:0;}
12
+ .ssk-head button{background:none;border:none;color:#fff;font-size:16px;cursor:pointer;opacity:.85;padding:0;}
13
+ .ssk-body{flex:1;overflow-y:auto;padding:14px;background:#faf9f7;display:flex;flex-direction:column;gap:8px;min-height:200px;}
14
+ .ssk-msg{max-width:85%;padding:9px 12px;border-radius:12px;font-size:13.5px;line-height:1.45;white-space:pre-wrap;}
15
+ .ssk-msg.bot{background:#fff;border:1px solid rgba(0,0,0,.08);align-self:flex-start;color:#1b2733;}
16
+ .ssk-msg.user{background:${e.accent};color:#fff;align-self:flex-end;}
17
+ .ssk-inputrow{display:flex;border-top:1px solid rgba(0,0,0,.08);padding:8px;gap:6px;flex-shrink:0;}
18
+ .ssk-inputrow input{flex:1;border:1px solid rgba(0,0,0,.12);border-radius:10px;padding:9px 11px;font-size:13.5px;outline:none;}
19
+ .ssk-inputrow button{background:${e.accent};color:#fff;border:none;border-radius:10px;padding:0 14px;cursor:pointer;font-weight:700;}
20
+ `,document.head.appendChild(r)}function s(e,t){let n=document.createElement(`div`);n.className=`ssk-msg ${t.role}`,n.textContent=t.text,e.appendChild(n),e.scrollTop=e.scrollHeight}function c(e,t){let n=Array.from(t.children).map(e=>({role:e.classList.contains(`user`)?`user`:`bot`,text:e.textContent||``}));try{sessionStorage.setItem(r(e),JSON.stringify(n.slice(-40)))}catch{}}function l(e){try{return JSON.parse(sessionStorage.getItem(r(e))||`[]`)}catch{return[]}}function u(){let e=document.body.cloneNode(!0);e.querySelectorAll(`script,style,nav,header,footer,svg,noscript,#${n}`).forEach(e=>e.remove());let t=(e.textContent||``).replace(/\s+/g,` `).trim();return{title:document.title,text:t}}async function d(e,t){let{title:n,text:r}=u();r.length<40||await fetch(`${e}/ingest`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({embedToken:t,items:[{url:location.href,title:n,text:r}]})}).catch(()=>{})}async function f(e,t){let n=!1;try{n=!!sessionStorage.getItem(i(t)),sessionStorage.setItem(i(t),`1`)}catch{}n||await fetch(`${e}/crawlSite`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({embedToken:t})}).catch(()=>{})}async function p(e){if(typeof document>`u`||document.getElementById(n))return;let r=(e.token||``).trim();if(!r){console.error(`[Sendystack] init() called without a token`);return}let i=e.apiBase||t,u;try{u=await a(i,r)}catch(e){console.error(`[Sendystack] failed to load widget config:`,e);return}o(u);let p=document.createElement(`div`);p.id=n;let m=document.createElement(`div`);m.className=`ssk-body`;let h=l(r);if(h.length)for(let e of h)s(m,e);else s(m,{role:`bot`,text:u.welcomeMessage});let g=document.createElement(`div`);g.className=`ssk-head`;let _=document.createElement(`span`);_.textContent=u.assistantName;let v=document.createElement(`button`);v.textContent=`✕`,g.append(_,v);let y=document.createElement(`input`);y.type=`text`,y.placeholder=`Ask anything…`;let b=document.createElement(`button`);b.textContent=`Send`;let x=document.createElement(`div`);x.className=`ssk-inputrow`,x.append(y,b);let S=document.createElement(`div`);S.className=`ssk-panel`,S.append(g,m,x);let C=document.createElement(`button`);C.className=`ssk-bubble`,C.textContent=`💬`,C.setAttribute(`aria-label`,`Open chat`),p.append(S,C),document.body.appendChild(p),C.onclick=()=>S.classList.toggle(`open`),v.onclick=()=>S.classList.remove(`open`);async function w(){let e=y.value.trim();if(e){y.value=``,s(m,{role:`user`,text:e}),c(r,m),b.disabled=!0;try{let t=await(await fetch(`${i}/answer`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({embedToken:r,query:e})})).json();s(m,{role:`bot`,text:t.ok?t.reply:`Sorry, I couldn't process that right now.`})}catch{s(m,{role:`bot`,text:`Sorry, something went wrong. Please try again.`})}finally{b.disabled=!1,c(r,m)}}}b.onclick=w,y.addEventListener(`keydown`,e=>{e.key===`Enter`&&w()}),d(i,r),f(i,r)}function m(){let e=document.currentScript||Array.from(document.getElementsByTagName(`script`)).find(e=>/widget(\.global)?\.js/.test(e.src)),t=e?.dataset.token;t&&p({token:t,apiBase:e?.dataset.apiBase})}return typeof document<`u`&&m(),e.init=p,e})({});
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@sendystack/widget",
3
+ "version": "0.1.0",
4
+ "description": "Universal AI chat widget for any website -- auto-injects the chatbot, crawls your site into the knowledge base, and feeds Claude/ChatGPT/Gemini via MCP. Appearance is managed at app.sendystack.org.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "main": "./dist/index.es.js",
8
+ "module": "./dist/index.es.js",
9
+ "types": "./dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "import": "./dist/index.es.js"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist",
18
+ "README.md"
19
+ ],
20
+ "sideEffects": true,
21
+ "scripts": {
22
+ "build": "vite build && tsc -p tsconfig.json --emitDeclarationOnly --outDir dist && npm run copy:cdn",
23
+ "dev": "vite build --watch",
24
+ "copy:cdn": "node -e \"require('fs').copyFileSync('dist/widget.global.js', '../public/widget.js')\""
25
+ },
26
+ "keywords": [
27
+ "sendystack",
28
+ "chatbot",
29
+ "ai",
30
+ "mcp",
31
+ "widget",
32
+ "wordpress",
33
+ "woocommerce"
34
+ ],
35
+ "homepage": "https://sendystack.org",
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "https://github.com/sendystack/chat"
39
+ },
40
+ "publishConfig": {
41
+ "access": "public"
42
+ },
43
+ "devDependencies": {
44
+ "typescript": "~6.0.2",
45
+ "vite": "^8.1.0"
46
+ }
47
+ }