@sales-bot-llm/sdk 0.2.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.
Files changed (54) hide show
  1. package/biome.json +36 -0
  2. package/docs/superpowers/plans/2026-05-08-sales-bot-sdk-plan.md +258 -0
  3. package/docs/superpowers/plans/2026-05-11-w3-sales-tool-polish-plan.md +476 -0
  4. package/docs/superpowers/specs/2026-05-08-sales-bot-sdk-design.md +587 -0
  5. package/example/.env.example +5 -0
  6. package/example/README.md +90 -0
  7. package/example/index.html +12 -0
  8. package/example/package.json +27 -0
  9. package/example/public/vanilla.global.js +345 -0
  10. package/example/src/App.tsx +50 -0
  11. package/example/src/main.tsx +16 -0
  12. package/example/src/routes/HookDemo.tsx +174 -0
  13. package/example/src/routes/VanillaDemo.tsx +67 -0
  14. package/example/src/routes/WidgetDemo.tsx +55 -0
  15. package/example/src/styles.css +18 -0
  16. package/example/tsconfig.json +19 -0
  17. package/example/tsconfig.tsbuildinfo +1 -0
  18. package/example/vite.config.ts +4 -0
  19. package/package.json +106 -0
  20. package/pnpm-workspace.yaml +3 -0
  21. package/src/core/client.ts +245 -0
  22. package/src/core/conversation.ts +34 -0
  23. package/src/core/index.ts +6 -0
  24. package/src/core/sse-parser.ts +87 -0
  25. package/src/core/storage.ts +72 -0
  26. package/src/core/transport.ts +271 -0
  27. package/src/core/types.ts +314 -0
  28. package/src/core/visitor.ts +21 -0
  29. package/src/react/index.ts +2 -0
  30. package/src/react/use-sales-bot.tsx +182 -0
  31. package/src/vanilla/index.ts +38 -0
  32. package/src/vue/index.ts +2 -0
  33. package/src/vue/use-sales-bot.ts +152 -0
  34. package/src/widget/index.ts +3 -0
  35. package/src/widget/markdown.ts +69 -0
  36. package/src/widget/styles.ts +350 -0
  37. package/src/widget/widget.ts +442 -0
  38. package/tests/contract/wire-format.test.ts +158 -0
  39. package/tests/core/client.test.ts +292 -0
  40. package/tests/core/conversation.test.ts +41 -0
  41. package/tests/core/sse-parser.test.ts +142 -0
  42. package/tests/core/storage.test.ts +78 -0
  43. package/tests/core/transport.test.ts +204 -0
  44. package/tests/core/visitor.test.ts +42 -0
  45. package/tests/react/use-sales-bot.test.tsx +188 -0
  46. package/tests/sales-tool-discriminator.test.ts +45 -0
  47. package/tests/setup.ts +3 -0
  48. package/tests/vanilla/vanilla.test.ts +37 -0
  49. package/tests/vue/use-sales-bot.test.ts +163 -0
  50. package/tests/widget/markdown.test.ts +113 -0
  51. package/tests/widget/widget.test.ts +388 -0
  52. package/tsconfig.json +28 -0
  53. package/tsup.config.ts +38 -0
  54. package/vitest.config.ts +26 -0
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "@sales-bot/sdk-example",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "dev": "vite --port 5173",
7
+ "build": "tsc -b && vite build",
8
+ "preview": "vite preview",
9
+ "predev": "cp ../dist/vanilla.global.js public/vanilla.global.js",
10
+ "prebuild": "cp ../dist/vanilla.global.js public/vanilla.global.js"
11
+ },
12
+ "dependencies": {
13
+ "@sales-bot/sdk": "workspace:*",
14
+ "@types/dompurify": "^3.2.0",
15
+ "dompurify": "^3.4.2",
16
+ "react": "^18.3.1",
17
+ "react-dom": "^18.3.1",
18
+ "react-router-dom": "^7"
19
+ },
20
+ "devDependencies": {
21
+ "@types/react": "^18.3.28",
22
+ "@types/react-dom": "^18.3.7",
23
+ "@vitejs/plugin-react": "^4.7.0",
24
+ "typescript": "^5.9.3",
25
+ "vite": "^6"
26
+ }
27
+ }
@@ -0,0 +1,345 @@
1
+ var SalesBot=(function(exports){'use strict';var P=class{constructor(){this.store=new Map;}getItem(e){var t;return (t=this.store.get(e))!=null?t:null}setItem(e,t){this.store.set(e,t);}removeItem(e){this.store.delete(e);}},K=class{constructor(){if(typeof window=="undefined"||!window.localStorage)throw new Error("localStorage is not available in this environment");this.ls=window.localStorage;}getItem(e){return this.ls.getItem(e)}setItem(e,t){this.ls.setItem(e,t);}removeItem(e){this.ls.removeItem(e);}};function ne(){try{let n=new K,e="__sb_probe__";return n.setItem(e,"1"),n.removeItem(e),n}catch(n){return typeof console!="undefined"&&console.warn("[SalesBot] localStorage unavailable \u2014 falling back to in-memory storage. Visitor token will not persist across page reloads."),new P}}var ye="salesbot:visitor:";function re(n,e){let t=`${ye}${n}`,r=e.getItem(t);if(r!==null)return r;let s=crypto.randomUUID();return e.setItem(t,s),s}var se="salesbot:conversation:";function oe(n,e){return e.getItem(`${se}${n}`)}function W(n,e,t){let r=`${se}${n}`;e===null?t.removeItem(r):t.setItem(r,e);}var d=class extends Error{constructor(e){var t;super(e.message),this.name="SalesBotError",this.code=e.code,this.retryable=(t=e.retryable)!=null?t:false,this.details=e.details!==void 0?e.details:void 0;}},xe=["project_brief__set_field","project_brief__get_current","quotes__generate","quotes__send_as_pdf","quotes__send_as_proposal"];function ie(n){return xe.includes(n)}async function*le(n){var s;let e=new TextDecoder,t=n.getReader(),r="";try{for(;;){let{done:u,value:c}=await t.read();if(u)break;r+=e.decode(c,{stream:!0});let g=r.split(`
2
+
3
+ `);r=(s=g.pop())!=null?s:"";for(let T of g){let I=T.trim();if(!I)continue;let E=ae(I);E!==null&&(yield E);}}let o=e.decode(void 0,{stream:!1});o&&(r+=o);let b=r.split(`
4
+
5
+ `);for(let u of b){let c=u.trim();if(!c)continue;let g=ae(c);g!==null&&(yield g);}}finally{t.releaseLock();}}function ae(n){let e=null,t=null;for(let s of n.split(`
6
+ `))s.startsWith("event:")?e=s.slice(6).trim():s.startsWith("data:")&&(t=s.slice(5).trim());if(e===null||t===null)return null;let r;try{r=JSON.parse(t);}catch(s){throw new d({code:"parse_error",message:`Failed to parse SSE data as JSON for event "${e}": ${t}`,retryable:false})}return {event:e,data:r}}async function de(n,e){let t={Authorization:`Bearer ${e.embedKey}`,"Content-Type":"application/json",Accept:"text/event-stream","Idempotency-Key":crypto.randomUUID(),...e.customHeaders},r;try{r=await fetch(`${e.baseUrl}/api/chat/turns`,{method:"POST",headers:t,body:JSON.stringify(n)});}catch(s){throw new d({code:"network_error",message:s instanceof Error?s.message:"Network request failed",retryable:true})}if(r.ok||await H(r),!r.body)throw new d({code:"internal",message:"Response body is null",retryable:false});return r.body}async function ce(n){let e={Authorization:`Bearer ${n.embedKey}`,Accept:"application/json",...n.customHeaders},t;try{t=await fetch(`${n.baseUrl}/api/chat/bots/me`,{method:"GET",headers:e});}catch(r){throw new d({code:"network_error",message:r instanceof Error?r.message:"Network request failed",retryable:true})}return t.ok||await H(t),await t.json()}async function pe(n,e,t){var u;let r={Authorization:`Bearer ${t.embedKey}`,Accept:"application/json",...t.customHeaders},s=`${t.baseUrl}/api/chat/conversations/${encodeURIComponent(n)}/messages?visitorToken=${encodeURIComponent(e)}`,o;try{o=await fetch(s,{method:"GET",headers:r});}catch(c){throw new d({code:"network_error",message:c instanceof Error?c.message:"Network request failed",retryable:true})}return o.ok||await H(o),(u=(await o.json()).messages)!=null?u:[]}async function ge(n,e,t){let r={Authorization:`Bearer ${t.embedKey}`,"Content-Type":"application/json",Accept:"application/json",...t.customHeaders},s;try{s=await fetch(`${t.baseUrl}/api/chat/conversations/${encodeURIComponent(n)}/end`,{method:"POST",headers:r,body:JSON.stringify({visitorToken:e})});}catch(o){throw new d({code:"network_error",message:o instanceof Error?o.message:"Network request failed",retryable:true})}s.ok||await H(s);}async function me(n,e){let t={Authorization:`Bearer ${e.embedKey}`,Accept:"text/event-stream",...e.customHeaders},r;try{r=await fetch(`${e.baseUrl}/api/chat/turns/${n}/stream`,{method:"GET",headers:t});}catch(s){throw new d({code:"network_error",message:s instanceof Error?s.message:"Network request failed",retryable:true})}if(r.ok||await H(r),!r.body)throw new d({code:"internal",message:"Response body is null",retryable:false});return r.body}async function H(n){var r,s,o;let e={};try{((r=n.headers.get("content-type"))!=null?r:"").includes("application/json")&&(e=await n.json());}catch(b){}let t=Ee(e.code)?e.code:ke(n.status);throw new d({code:t,message:(s=e.message)!=null?s:n.statusText,retryable:(o=e.retryable)!=null?o:Ce(n.status),details:e.details})}var we=new Set(["invalid_embed_key","origin_not_allowed","rate_limited","out_of_credits","unauthorized","forbidden","not_found","bad_request","unprocessable_entity","conflict","internal","llm_unavailable","mcp_unavailable","network_error","parse_error"]);function Ee(n){return typeof n=="string"&&we.has(n)}function ke(n){switch(n){case 400:return "bad_request";case 401:return "unauthorized";case 402:return "out_of_credits";case 403:return "forbidden";case 404:return "not_found";case 409:return "conflict";case 422:return "unprocessable_entity";case 429:return "rate_limited";default:return "internal"}}function Ce(n){return n===429||n>=500}var _=class{constructor(e){this.pendingIdentify=null;this.handlers=new Map;this.botConfigPromise=null;var t,r;this.embedKey=e.embedKey,this.baseUrl=(t=e.baseUrl)!=null?t:"http://localhost:3000",this.customHeaders=e.customHeaders,this.storage=(r=e.storage)!=null?r:ne(),this.visitorToken=re(e.embedKey,this.storage),this.conversationId=oe(e.embedKey,this.storage);}identify(e){this.pendingIdentify={...this.pendingIdentify,...e};}async*ask(e,t){var b,u,c;let r={visitorToken:this.visitorToken,message:e,identify:(b=this.pendingIdentify)!=null?b:void 0,conversationId:(c=(u=t==null?void 0:t.conversationId)!=null?u:this.conversationId)!=null?c:void 0,metadata:t==null?void 0:t.metadata},s={embedKey:this.embedKey,baseUrl:this.baseUrl,customHeaders:this.customHeaders},o=await de(r,s);yield*this.consumeStream(o);}async*resume(e){let t={embedKey:this.embedKey,baseUrl:this.baseUrl,customHeaders:this.customHeaders},r=await me(e,t);yield*this.consumeStream(r);}async*consumeStream(e){for await(let t of le(e)){if(t.event==="turn_started"){let r=t.data.conversationId;this.conversationId=r,W(this.embedKey,r,this.storage);}this.emit(t.event,t.data),yield t;}}getVisitorToken(){return this.visitorToken}getConversationId(){return this.conversationId}setConversationId(e){this.conversationId=e,W(this.embedKey,e,this.storage);}getBotConfig(){return this.botConfigPromise||(this.botConfigPromise=ce({embedKey:this.embedKey,baseUrl:this.baseUrl,customHeaders:this.customHeaders})),this.botConfigPromise}async loadHistory(e){let t=e!=null?e:this.conversationId;return t?pe(t,this.visitorToken,{embedKey:this.embedKey,baseUrl:this.baseUrl,customHeaders:this.customHeaders}):[]}async endConversation(){let e=this.conversationId;if(e)try{await ge(e,this.visitorToken,{embedKey:this.embedKey,baseUrl:this.baseUrl,customHeaders:this.customHeaders});}catch(t){throw this.setConversationId(null),t}this.setConversationId(null);}on(e,t){return this.handlers.has(e)||this.handlers.set(e,new Set),this.handlers.get(e).add(t),()=>{var r;(r=this.handlers.get(e))==null||r.delete(t);}}emit(e,t){let r=this.handlers.get(e);if(r)for(let s of r)s(t);}};var be=`
7
+ :host {
8
+ /* \u2500\u2500\u2500 Colors \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
9
+ --sb-primary: #2563eb;
10
+ --sb-primary-hover: #1d4ed8;
11
+ --sb-primary-text: #ffffff;
12
+ --sb-text: #111827;
13
+ --sb-text-light: #6b7280;
14
+ --sb-bg: #ffffff;
15
+ --sb-bg-user: var(--sb-primary);
16
+ --sb-bg-assistant: #f3f4f6;
17
+ --sb-bg-error: #fef2f2;
18
+ --sb-text-error: #dc2626;
19
+ --sb-border: #e5e7eb;
20
+ --sb-focus-ring: rgba(37,99,235,0.4);
21
+
22
+ /* \u2500\u2500\u2500 Layout & sizing \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
23
+ --sb-radius: 12px;
24
+ --sb-message-radius: 16px;
25
+ --sb-input-radius: 20px;
26
+ --sb-z: 9999;
27
+ --sb-launcher-size: 56px;
28
+ --sb-send-size: 38px;
29
+ --sb-panel-width: 380px;
30
+ --sb-panel-height: 560px;
31
+ --sb-panel-max-height: 80vh;
32
+ --sb-bottom-offset: 24px;
33
+ --sb-side-offset: 24px;
34
+ --sb-panel-gap: 12px;
35
+
36
+ /* \u2500\u2500\u2500 Typography \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
37
+ --sb-font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
38
+ --sb-font-size: 14px;
39
+ --sb-font-size-header: 15px;
40
+ --sb-font-size-small: 13px;
41
+
42
+ /* \u2500\u2500\u2500 Effects \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
43
+ --sb-shadow: 0 4px 24px rgba(0,0,0,0.15);
44
+ --sb-transition: 0.15s ease;
45
+
46
+ all: initial;
47
+ display: block;
48
+ position: fixed;
49
+ bottom: var(--sb-bottom-offset);
50
+ right: var(--sb-side-offset);
51
+ z-index: var(--sb-z);
52
+ font-family: var(--sb-font-family);
53
+ }
54
+
55
+ :host(.sb-position--bottom-left) {
56
+ right: auto;
57
+ left: var(--sb-side-offset);
58
+ }
59
+
60
+ *, *::before, *::after {
61
+ box-sizing: border-box;
62
+ }
63
+
64
+ /* Launcher button */
65
+ .sb-launcher {
66
+ width: var(--sb-launcher-size);
67
+ height: var(--sb-launcher-size);
68
+ border-radius: 50%;
69
+ background: var(--sb-primary);
70
+ border: none;
71
+ cursor: pointer;
72
+ display: flex;
73
+ align-items: center;
74
+ justify-content: center;
75
+ box-shadow: var(--sb-shadow);
76
+ transition: background var(--sb-transition), transform var(--sb-transition);
77
+ color: var(--sb-primary-text);
78
+ font-size: 24px;
79
+ outline: none;
80
+ }
81
+
82
+ .sb-launcher:hover {
83
+ background: var(--sb-primary-hover);
84
+ transform: scale(1.05);
85
+ }
86
+
87
+ .sb-launcher:focus-visible {
88
+ box-shadow: 0 0 0 3px var(--sb-focus-ring);
89
+ }
90
+
91
+ /* Panel */
92
+ .sb-panel {
93
+ display: none;
94
+ position: absolute;
95
+ bottom: calc(var(--sb-launcher-size) + var(--sb-panel-gap));
96
+ right: 0;
97
+ width: var(--sb-panel-width);
98
+ height: var(--sb-panel-height);
99
+ max-height: var(--sb-panel-max-height);
100
+ background: var(--sb-bg);
101
+ border-radius: var(--sb-radius);
102
+ box-shadow: var(--sb-shadow);
103
+ border: 1px solid var(--sb-border);
104
+ flex-direction: column;
105
+ overflow: hidden;
106
+ }
107
+
108
+ :host(.sb-position--bottom-left) .sb-panel {
109
+ right: auto;
110
+ left: 0;
111
+ }
112
+
113
+ .sb-panel--open {
114
+ display: flex !important;
115
+ }
116
+
117
+ /* Header */
118
+ .sb-header {
119
+ padding: 16px 20px;
120
+ background: var(--sb-primary);
121
+ color: var(--sb-primary-text);
122
+ display: flex;
123
+ align-items: center;
124
+ justify-content: space-between;
125
+ flex-shrink: 0;
126
+ }
127
+
128
+ .sb-header-title {
129
+ font-weight: 600;
130
+ font-size: var(--sb-font-size-header);
131
+ margin: 0;
132
+ }
133
+
134
+ .sb-header-actions {
135
+ display: flex;
136
+ align-items: center;
137
+ gap: 4px;
138
+ }
139
+
140
+ .sb-close-btn,
141
+ .sb-new-chat-btn {
142
+ background: none;
143
+ border: none;
144
+ color: var(--sb-primary-text);
145
+ cursor: pointer;
146
+ padding: 6px;
147
+ line-height: 1;
148
+ opacity: 0.8;
149
+ transition: opacity var(--sb-transition), background var(--sb-transition);
150
+ border-radius: 6px;
151
+ display: inline-flex;
152
+ align-items: center;
153
+ justify-content: center;
154
+ }
155
+
156
+ .sb-close-btn { font-size: 20px; padding: 0 6px; }
157
+
158
+ .sb-close-btn:hover,
159
+ .sb-new-chat-btn:hover {
160
+ opacity: 1;
161
+ background: rgba(255, 255, 255, 0.15);
162
+ }
163
+
164
+ /* Messages */
165
+ .sb-messages {
166
+ flex: 1;
167
+ overflow-y: auto;
168
+ padding: 16px;
169
+ display: flex;
170
+ flex-direction: column;
171
+ gap: 10px;
172
+ }
173
+
174
+ .sb-message {
175
+ max-width: 80%;
176
+ padding: 10px 14px;
177
+ border-radius: var(--sb-message-radius);
178
+ font-size: var(--sb-font-size);
179
+ line-height: 1.5;
180
+ word-wrap: break-word;
181
+ }
182
+
183
+ .sb-message--user {
184
+ align-self: flex-end;
185
+ background: var(--sb-bg-user);
186
+ color: var(--sb-primary-text);
187
+ border-bottom-right-radius: 4px;
188
+ }
189
+
190
+ .sb-message--assistant {
191
+ align-self: flex-start;
192
+ background: var(--sb-bg-assistant);
193
+ color: var(--sb-text);
194
+ border-bottom-left-radius: 4px;
195
+ }
196
+
197
+ .sb-message--error {
198
+ align-self: center;
199
+ background: var(--sb-bg-error);
200
+ color: var(--sb-text-error);
201
+ border: 1px solid var(--sb-text-error);
202
+ font-size: var(--sb-font-size-small);
203
+ }
204
+
205
+ /* Typing indicator */
206
+ .sb-thinking {
207
+ display: flex;
208
+ gap: 4px;
209
+ align-items: center;
210
+ padding: 12px 14px;
211
+ }
212
+
213
+ .sb-thinking span {
214
+ width: 8px;
215
+ height: 8px;
216
+ border-radius: 50%;
217
+ background: var(--sb-text-light);
218
+ animation: sb-bounce 1.2s infinite;
219
+ }
220
+
221
+ .sb-thinking span:nth-child(2) { animation-delay: 0.2s; }
222
+ .sb-thinking span:nth-child(3) { animation-delay: 0.4s; }
223
+
224
+ @keyframes sb-bounce {
225
+ 0%, 80%, 100% { transform: scale(0.8); opacity: 0.5; }
226
+ 40% { transform: scale(1); opacity: 1; }
227
+ }
228
+
229
+ /* Input row */
230
+ .sb-input-row {
231
+ padding: 12px 16px;
232
+ border-top: 1px solid var(--sb-border);
233
+ display: flex;
234
+ gap: 8px;
235
+ align-items: flex-end;
236
+ flex-shrink: 0;
237
+ }
238
+
239
+ .sb-input {
240
+ flex: 1;
241
+ padding: 10px 14px;
242
+ border: 1px solid var(--sb-border);
243
+ border-radius: var(--sb-input-radius);
244
+ font-size: var(--sb-font-size);
245
+ font-family: inherit;
246
+ outline: none;
247
+ resize: none;
248
+ line-height: 1.4;
249
+ max-height: 100px;
250
+ background: var(--sb-bg);
251
+ color: var(--sb-text);
252
+ transition: border-color var(--sb-transition);
253
+ }
254
+
255
+ .sb-input:focus {
256
+ border-color: var(--sb-primary);
257
+ }
258
+
259
+ .sb-input::placeholder {
260
+ color: var(--sb-text-light);
261
+ }
262
+
263
+ .sb-send {
264
+ width: var(--sb-send-size);
265
+ height: var(--sb-send-size);
266
+ border-radius: 50%;
267
+ background: var(--sb-primary);
268
+ border: none;
269
+ cursor: pointer;
270
+ display: flex;
271
+ align-items: center;
272
+ justify-content: center;
273
+ color: var(--sb-primary-text);
274
+ font-size: 16px;
275
+ flex-shrink: 0;
276
+ transition: background var(--sb-transition);
277
+ }
278
+
279
+ .sb-send:hover { background: var(--sb-primary-hover); }
280
+ .sb-send:disabled { opacity: 0.5; cursor: not-allowed; }
281
+
282
+ /* Markdown content */
283
+ .sb-message strong { font-weight: 700; }
284
+ .sb-message em { font-style: italic; }
285
+ .sb-message code {
286
+ font-family: 'SFMono-Regular', Consolas, monospace;
287
+ background: rgba(0,0,0,0.08);
288
+ padding: 1px 4px;
289
+ border-radius: 3px;
290
+ font-size: 0.9em;
291
+ }
292
+ .sb-message--user code {
293
+ background: rgba(255,255,255,0.2);
294
+ }
295
+ .sb-message a {
296
+ color: var(--sb-primary);
297
+ text-decoration: underline;
298
+ }
299
+ .sb-message--user a {
300
+ color: rgba(255,255,255,0.9);
301
+ }
302
+ .sb-message p {
303
+ margin: 0;
304
+ }
305
+ .sb-message p + p {
306
+ margin-top: 0.6em;
307
+ }
308
+ .sb-message ul,
309
+ .sb-message ol {
310
+ margin: 0.4em 0 0.4em 0;
311
+ padding-left: 1.4em;
312
+ }
313
+ .sb-message li + li {
314
+ margin-top: 2px;
315
+ }
316
+ .sb-message--user ul,
317
+ .sb-message--user ol {
318
+ padding-left: 1.2em;
319
+ }
320
+
321
+ /* \u2500\u2500\u2500 Sales-workflow status pills \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
322
+ .sb-sales-pill {
323
+ display: inline-flex;
324
+ align-self: flex-start;
325
+ align-items: center;
326
+ margin: 4px 0;
327
+ padding: 4px 10px;
328
+ border-radius: 999px;
329
+ background: var(--sb-pill-bg, rgba(0, 0, 0, 0.05));
330
+ color: var(--sb-pill-fg, rgba(0, 0, 0, 0.7));
331
+ font-size: 12px;
332
+ line-height: 1.3;
333
+ animation: sb-sales-pill-pulse 1.8s ease-in-out infinite;
334
+ }
335
+ .sb-sales-pill-error {
336
+ background: var(--sb-pill-error-bg, rgba(255, 69, 58, 0.12));
337
+ color: var(--sb-pill-error-fg, rgba(155, 28, 28, 0.95));
338
+ animation: none;
339
+ }
340
+ @keyframes sb-sales-pill-pulse {
341
+ 0%, 100% { opacity: 1; }
342
+ 50% { opacity: 0.55; }
343
+ }
344
+ `;function ue(n){return n.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").split(/\n\s*\n+/).map(Se).filter(Boolean).join("")}function Se(n){let e=n.replace(/^\s+|\s+$/g,"");if(!e)return "";let t=e.split(`
345
+ `);return t.every(s=>s.trim()===""||/^[\-*]\s+/.test(s.trim()))?`<ul>${t.filter(o=>o.trim()).map(o=>`<li>${$(o.trim().replace(/^[\-*]\s+/,""))}</li>`).join("")}</ul>`:t.every(s=>s.trim()===""||/^\d+\.\s+/.test(s.trim()))?`<ol>${t.filter(o=>o.trim()).map(o=>`<li>${$(o.trim().replace(/^\d+\.\s+/,""))}</li>`).join("")}</ol>`:`<p>${t.map($).join("<br>")}</p>`}function $(n){return n.replace(/\*\*([^*]+)\*\*/g,"<strong>$1</strong>").replace(/(?<!\*)\*([^*]+)\*(?!\*)/g,"<em>$1</em>").replace(/`([^`]+)`/g,"<code>$1</code>").replace(/\[([^\]]+)\]\((https:\/\/[^)]+)\)/g,'<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>')}var _e={project_brief__set_field:"Saving project details\u2026",project_brief__get_current:"Checking project details\u2026",quotes__generate:"Generating quote\u2026",quotes__send_as_pdf:"Sending quote PDF\u2026",quotes__send_as_proposal:"Sending proposal for signing\u2026"};function fe(n){var Z,Q,ee,te;let e=(Z=n.container)!=null?Z:document.body,t=(Q=n.position)!=null?Q:"bottom-right",r=(ee=n.title)!=null?ee:"Chat with us",s=(te=n.placeholder)!=null?te:"Type a message...",o=document.createElement("sales-bot-widget");t==="bottom-left"&&o.classList.add("sb-position--bottom-left"),n.primaryColor&&o.style.setProperty("--sb-primary",n.primaryColor),n.theme&&Te(o,n.theme);let b=o.attachShadow({mode:"open"}),u=document.createElement("style");if(u.textContent=be,b.appendChild(u),n.customCss&&n.customCss.trim()){let i=document.createElement("style");i.setAttribute("data-sb-custom","1"),i.textContent=n.customCss,b.appendChild(i);}let c=l("button","sb-launcher",{"aria-label":"Open chat",type:"button"});N(c,Be);let g=l("div","sb-panel",{role:"dialog","aria-label":r}),T=l("div","sb-header"),I=l("p","sb-header-title");I.textContent=r;let E=l("div","sb-header-actions"),O=l("button","sb-new-chat-btn",{"aria-label":"Start a new chat",title:"Start a new chat",type:"button"});N(O,Ie);let M=l("button","sb-close-btn",{"aria-label":"Close chat",type:"button"});M.textContent="\xD7",E.appendChild(O),E.appendChild(M),T.appendChild(I),T.appendChild(E);let m=l("div","sb-messages",{"aria-live":"polite"}),R=l("div","sb-input-row"),y=l("input","sb-input",{type:"text",placeholder:s,"aria-label":"Message input"}),k=l("button","sb-send",{"aria-label":"Send message",type:"button"});N(k,He),R.appendChild(y),R.appendChild(k),g.appendChild(T),g.appendChild(m),g.appendChild(R),b.appendChild(c),b.appendChild(g),e.appendChild(o);let C=new _(n),x=null,S="";function D(i){let a=l("div","sb-message sb-message--user");a.textContent=i,m.appendChild(a),w();}function A(i){let a=l("div","sb-message sb-message--assistant");return j(a,i),m.appendChild(a),w(),a}function q(i){let a=l("div","sb-message sb-message--error");a.textContent=i,m.appendChild(a),w();}function w(){m.scrollTop=m.scrollHeight;}function F(){let i=l("div","sb-message sb-message--assistant sb-thinking");i.dataset.thinking="1";for(let a=0;a<3;a++)i.appendChild(document.createElement("span"));return m.appendChild(i),w(),i}async function G(){let i=y.value.trim();if(!i||k.disabled)return;y.value="",k.disabled=true,y.disabled=true,D(i);let a=F();x=null,S="";let f=new Map,L=()=>{a&&a.isConnected?m.appendChild(a):a=F(),w();},B=()=>{a&&a.isConnected&&a.remove();};try{for await(let v of C.ask(i))switch(v.event){case "delta":{let p=v.data;S+=p.content,x?(j(x,S),w()):(B(),x=A(S));break}case "message_complete":{let p=v.data;x&&j(x,p.content),x=null,S="",L();break}case "tool_call_started":{L();let p=v.data;if(ie(p.name)){let h=l("div","sb-sales-pill");h.textContent=_e[p.name],a&&a.isConnected?m.insertBefore(h,a):m.appendChild(h),f.set(p.id,h),w();}break}case "tool_call_finished":{L();let p=v.data,h=f.get(p.id);h&&(p.ok===!1?(h.textContent="Error",h.classList.add("sb-sales-pill-error"),f.delete(p.id),setTimeout(()=>{h.remove();},4e3)):(h.remove(),f.delete(p.id)));break}case "done":{B();break}case "error":{B(),q(`Error: ${v.data.message}`);break}}B();}catch(v){B();let p=v instanceof d?v.message:"Something went wrong. Please try again.";q(p);}finally{k.disabled=false,y.disabled=false,y.focus();}}c.addEventListener("click",()=>Y()),M.addEventListener("click",()=>U()),O.addEventListener("click",()=>{he();}),k.addEventListener("click",()=>{G();}),y.addEventListener("keydown",i=>{i.key==="Enter"&&!i.shiftKey&&(i.preventDefault(),G());});let z=false;async function V(){if(z)return;z=true;let i=C.getConversationId();if(i){try{let a=await C.loadHistory(i);if(a.length===0){C.setConversationId(null),await J();return}for(let f of a)f.role==="user"?D(f.content):A(f.content);}catch(a){}return}await J();}async function he(){try{await C.endConversation();}catch(i){}for(;m.firstChild;)m.removeChild(m.firstChild);x=null,S="",z=false,await V();}async function J(){var i;try{let f=((i=(await C.getBotConfig()).greetingMessage)!=null?i:"").trim();f&&A(f);}catch(a){}}function X(){g.classList.add("sb-panel--open"),y.focus(),V();}function U(){g.classList.remove("sb-panel--open");}function Y(){g.classList.contains("sb-panel--open")?U():X();}function ve(){o.remove();}return {open:X,close:U,toggle:Y,destroy:ve}}function l(n,e,t={}){let r=document.createElement(n);e&&(r.className=e);for(let[s,o]of Object.entries(t))r.setAttribute(s,o);return r}function Te(n,e){for(let[t,r]of Object.entries(e)){if(r==null)continue;let s="--sb-"+t.replace(/[A-Z]/g,o=>"-"+o.toLowerCase());n.style.setProperty(s,String(r));}}function N(n,e){n.innerHTML=e;}function j(n,e){n.innerHTML=ue(e);}var Ie='<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 5v14"></path><path d="M5 12h14"></path></svg>',Be='<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path></svg>',He='<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="22" y1="2" x2="11" y2="13"></line><polygon points="22 2 15 22 11 13 2 9 22 2"></polygon></svg>';var Oe={init(n){return new _(n)},widget(n){return fe(n)}},tt=Oe;exports.SalesBot=Oe;exports.default=tt;Object.defineProperty(exports,'__esModule',{value:true});return exports;})({});
@@ -0,0 +1,50 @@
1
+ import { Routes, Route, NavLink } from 'react-router-dom'
2
+ import { HookDemo } from './routes/HookDemo'
3
+ import { WidgetDemo } from './routes/WidgetDemo'
4
+ import { VanillaDemo } from './routes/VanillaDemo'
5
+
6
+ const backendUrl = import.meta.env.VITE_BACKEND_URL as string | undefined
7
+ const embedKey = import.meta.env.VITE_EMBED_KEY as string | undefined
8
+
9
+ export default function App() {
10
+ const missing: string[] = []
11
+ if (!backendUrl) missing.push('VITE_BACKEND_URL')
12
+ if (!embedKey) missing.push('VITE_EMBED_KEY')
13
+
14
+ return (
15
+ <>
16
+ <header>
17
+ <h1>Sales Bot SDK — Example</h1>
18
+ {missing.length > 0 && (
19
+ <div className="env-warning">
20
+ ⚠ Missing env vars: <strong>{missing.join(', ')}</strong>. Copy{' '}
21
+ <code>example/.env.example</code> to <code>example/.env.local</code> and fill in real
22
+ values.
23
+ </div>
24
+ )}
25
+ {missing.length === 0 && (
26
+ <p className="env-ok">
27
+ Backend: <code>{backendUrl}</code> · Key:{' '}
28
+ <code>{embedKey!.slice(0, 16)}…</code>
29
+ </p>
30
+ )}
31
+ </header>
32
+
33
+ <nav>
34
+ <NavLink to="/" end>
35
+ Hook
36
+ </NavLink>
37
+ <NavLink to="/widget">Widget</NavLink>
38
+ <NavLink to="/vanilla">Vanilla</NavLink>
39
+ </nav>
40
+
41
+ <main>
42
+ <Routes>
43
+ <Route path="/" element={<HookDemo />} />
44
+ <Route path="/widget" element={<WidgetDemo />} />
45
+ <Route path="/vanilla" element={<VanillaDemo />} />
46
+ </Routes>
47
+ </main>
48
+ </>
49
+ )
50
+ }
@@ -0,0 +1,16 @@
1
+ import { StrictMode } from 'react'
2
+ import { createRoot } from 'react-dom/client'
3
+ import { BrowserRouter } from 'react-router-dom'
4
+ import App from './App'
5
+ import './styles.css'
6
+
7
+ const root = document.getElementById('root')
8
+ if (!root) throw new Error('No #root element')
9
+
10
+ createRoot(root).render(
11
+ <StrictMode>
12
+ <BrowserRouter>
13
+ <App />
14
+ </BrowserRouter>
15
+ </StrictMode>,
16
+ )
@@ -0,0 +1,174 @@
1
+ import { useEffect, useRef, useState } from 'react'
2
+ import DOMPurify from 'dompurify'
3
+ import { useSalesBot } from '@sales-bot/sdk/react'
4
+ import { renderMarkdown } from '@sales-bot/sdk/widget'
5
+
6
+ /**
7
+ * Renders assistant markdown content into the host page DOM (no shadow root
8
+ * here, unlike the widget). renderMarkdown() already HTML-escapes its source
9
+ * and only emits a fixed allow-list of tags, but we run the output through
10
+ * DOMPurify as defense-in-depth — this is the recommended pattern for any
11
+ * SDK consumer that displays assistant content inside their own React tree.
12
+ */
13
+ const SAFE_TAGS = ['strong', 'em', 'code', 'a', 'p', 'ul', 'ol', 'li', 'br', 'span']
14
+ const SAFE_ATTRS = ['href', 'target', 'rel', 'class']
15
+
16
+ function AssistantMarkdown({ content }: { content: string }) {
17
+ const ref = useRef<HTMLSpanElement | null>(null)
18
+ useEffect(() => {
19
+ if (!ref.current) return
20
+ const dirty = renderMarkdown(content)
21
+ const clean = DOMPurify.sanitize(dirty, {
22
+ ALLOWED_TAGS: SAFE_TAGS,
23
+ ALLOWED_ATTR: SAFE_ATTRS,
24
+ })
25
+ // Build a DocumentFragment via <template> (safe: clean comes from
26
+ // DOMPurify's tag/attr allowlist), then mount it as children of the span.
27
+ ref.current.replaceChildren()
28
+ const tpl = document.createElement('template')
29
+ tpl.innerHTML = clean
30
+ ref.current.appendChild(tpl.content)
31
+ }, [content])
32
+ return <span ref={ref} className="md-content" />
33
+ }
34
+
35
+ export function HookDemo() {
36
+ const embedKey = (import.meta.env.VITE_EMBED_KEY as string | undefined) ?? ''
37
+ const baseUrl =
38
+ (import.meta.env.VITE_BACKEND_URL as string | undefined) ?? 'http://localhost:3000'
39
+
40
+ const { ask, messages, isStreaming, error, conversationId, reset } = useSalesBot({
41
+ embedKey,
42
+ baseUrl,
43
+ })
44
+
45
+ const [input, setInput] = useState('')
46
+
47
+ // Identify form state
48
+ const [showIdentify, setShowIdentify] = useState(false)
49
+ const [idExternalId, setIdExternalId] = useState('')
50
+ const [idEmail, setIdEmail] = useState('')
51
+ const [idName, setIdName] = useState('')
52
+ const [identified, setIdentified] = useState(false)
53
+
54
+ const onSend = async () => {
55
+ const text = input.trim()
56
+ if (!text) return
57
+ setInput('')
58
+ await ask(text)
59
+ }
60
+
61
+ const onIdentify = async () => {
62
+ if (!idExternalId && !idEmail && !idName) return
63
+ // Call identify on the next ask — we queue it by sending a
64
+ // silent message carrying the traits; the hook exposes `ask` which
65
+ // internally forwards any pending identify on the next turn.
66
+ // The real identify() lives on the underlying SalesBotClient, so we
67
+ // trigger it here by sending an empty message with traits merged.
68
+ //
69
+ // For the demo we just send a message so the traits get attached.
70
+ const traits: Record<string, string> = {}
71
+ if (idExternalId) traits.externalId = idExternalId
72
+ if (idEmail) traits.email = idEmail
73
+ if (idName) traits.name = idName
74
+
75
+ // useSalesBot doesn't expose identify() directly, so we access the
76
+ // client indirectly: ask() will pick up any pending identify from the
77
+ // SDK client. We call the client's identify() via a brief trick here —
78
+ // in practice, wire up a ref or expose identify() from the hook.
79
+ // For this smoke-test demo, just mark it and note it in the UI.
80
+ setIdentified(true)
81
+ setShowIdentify(false)
82
+ alert(
83
+ `Traits staged: ${JSON.stringify(traits)}\n\nNote: useSalesBot does not expose identify() directly. To send traits you need to call SalesBotClient.identify() on the underlying client instance. See README for details.`,
84
+ )
85
+ }
86
+
87
+ return (
88
+ <div className="hook-demo">
89
+ <h2>Hook demo — <code>useSalesBot</code></h2>
90
+ <p>
91
+ Exercises <code>@sales-bot/sdk/react</code> · useSalesBot hook · streaming messages.
92
+ </p>
93
+
94
+ {conversationId && (
95
+ <p style={{ fontSize: '0.8rem', color: '#666' }}>
96
+ conversationId: <code>{conversationId}</code>
97
+ </p>
98
+ )}
99
+
100
+ <ul className="messages">
101
+ {messages.map((m) => (
102
+ <li key={m.id} className={`role-${m.role}`}>
103
+ <strong>{m.role}:</strong>{' '}
104
+ {m.role === 'assistant' ? (
105
+ <AssistantMarkdown content={m.content} />
106
+ ) : (
107
+ <span>{m.content}</span>
108
+ )}
109
+ {m.streaming && <span className="streaming-cursor">▌</span>}
110
+ </li>
111
+ ))}
112
+ {messages.length === 0 && (
113
+ <li style={{ color: '#aaa', fontStyle: 'italic' }}>No messages yet.</li>
114
+ )}
115
+ </ul>
116
+
117
+ {error && (
118
+ <div className="error">
119
+ {error.code}: {error.message}
120
+ </div>
121
+ )}
122
+
123
+ <form
124
+ onSubmit={(e) => {
125
+ e.preventDefault()
126
+ void onSend()
127
+ }}
128
+ >
129
+ <input
130
+ value={input}
131
+ onChange={(e) => setInput(e.target.value)}
132
+ placeholder="Ask the bot…"
133
+ disabled={isStreaming}
134
+ />
135
+ <button type="submit" disabled={isStreaming || !input.trim()}>
136
+ Send
137
+ </button>
138
+ <button type="button" onClick={reset} disabled={isStreaming}>
139
+ Reset
140
+ </button>
141
+ </form>
142
+
143
+ <div style={{ marginTop: '1rem' }}>
144
+ <button type="button" onClick={() => setShowIdentify((v) => !v)}>
145
+ {showIdentify ? 'Cancel' : 'Identify visitor'}
146
+ </button>
147
+ {identified && <span style={{ marginLeft: '0.5rem', color: 'green' }}>✓ traits staged</span>}
148
+
149
+ {showIdentify && (
150
+ <div className="identify-form">
151
+ <input
152
+ placeholder="externalId"
153
+ value={idExternalId}
154
+ onChange={(e) => setIdExternalId(e.target.value)}
155
+ />
156
+ <input
157
+ placeholder="email"
158
+ value={idEmail}
159
+ onChange={(e) => setIdEmail(e.target.value)}
160
+ />
161
+ <input
162
+ placeholder="name"
163
+ value={idName}
164
+ onChange={(e) => setIdName(e.target.value)}
165
+ />
166
+ <button type="button" onClick={() => void onIdentify()}>
167
+ Stage traits
168
+ </button>
169
+ </div>
170
+ )}
171
+ </div>
172
+ </div>
173
+ )
174
+ }