@customerhero/js 0.0.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 CustomerHero
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,61 @@
1
+ # @customerhero/js
2
+
3
+ Framework-agnostic JavaScript client for the [CustomerHero](https://customerhero.app) chat widget. Use this package directly in vanilla JS / TS apps, or via the React bindings in [`@customerhero/react`](https://www.npmjs.com/package/@customerhero/react).
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install @customerhero/js
9
+ ```
10
+
11
+ ## Quick start
12
+
13
+ ```ts
14
+ import { CustomerHeroChat } from "@customerhero/js";
15
+
16
+ const chat = new CustomerHeroChat({
17
+ chatbotId: "bot_xxxxxxxxxxxxxxxxxxxx",
18
+ });
19
+
20
+ chat.subscribe((state) => {
21
+ // React to state changes — messages, open/closed, loading, etc.
22
+ });
23
+
24
+ chat.open();
25
+ await chat.sendMessage("Hello!");
26
+ ```
27
+
28
+ ## Identify a signed-in user
29
+
30
+ Link conversations to a user in your system by calling `identify` as soon as you know who the user is.
31
+
32
+ ```ts
33
+ chat.identify({
34
+ userId: "usr_123",
35
+ email: "jane@example.com",
36
+ name: "Jane Doe",
37
+ // Optional HMAC for identity verification (recommended in production)
38
+ userHash: "<hmac-sha256(userId, secret)>",
39
+ });
40
+ ```
41
+
42
+ ## Configuration
43
+
44
+ | Option | Type | Description |
45
+ | ------------------- | --------------------------------- | --------------------------------------------------------------------------- |
46
+ | `chatbotId` | `string` (required) | The chatbot to connect to. |
47
+ | `apiBase` | `string` | API base URL. Defaults to `https://customerhero.app`. |
48
+ | `primaryColor` | `string` | Accent color override. |
49
+ | `backgroundColor` | `string` | Chat window background override. |
50
+ | `textColor` | `string` | Text color override. |
51
+ | `position` | `"bottom-right" \| "bottom-left"` | Widget position. |
52
+ | `placeholderText` | `string` | Input placeholder override. |
53
+ | `welcomeMessage` | `string` | Welcome message override. |
54
+ | `title` | `string` | Header title override. |
55
+ | `avatarUrl` | `string` | Bot avatar URL override. |
56
+ | `locale` | `string` | Widget locale (e.g. `"en"`, `"es"`). Auto-detected from browser if omitted. |
57
+ | `suggestedMessages` | `string[]` | Quick-reply options shown before the first message. |
58
+
59
+ ## License
60
+
61
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,324 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ CustomerHeroChat: () => CustomerHeroChat,
24
+ DEFAULTS: () => DEFAULTS
25
+ });
26
+ module.exports = __toCommonJS(index_exports);
27
+
28
+ // src/defaults.ts
29
+ var DEFAULTS = {
30
+ apiBase: "https://customerhero.app",
31
+ primaryColor: "#6C3CE1",
32
+ backgroundColor: "#FFFFFF",
33
+ textColor: "#1A1A2E",
34
+ position: "bottom-right",
35
+ placeholderText: "Type your message...",
36
+ welcomeMessage: "Hi! How can I help you today?",
37
+ title: "CustomerHero"
38
+ };
39
+
40
+ // src/i18n.ts
41
+ var en = {
42
+ online: "Online",
43
+ typing: "Typing...",
44
+ unable_to_load: "Unable to load chat",
45
+ powered_by: "Powered by",
46
+ new_conversation: "New conversation",
47
+ open_chat: "Open chat",
48
+ close_chat: "Close chat",
49
+ send_message: "Send message",
50
+ helpful: "Helpful",
51
+ not_helpful: "Not helpful",
52
+ menu: "Menu"
53
+ };
54
+ var es = {
55
+ online: "En l\xEDnea",
56
+ typing: "Escribiendo...",
57
+ unable_to_load: "No se pudo cargar el chat",
58
+ powered_by: "Desarrollado por",
59
+ new_conversation: "Nueva conversaci\xF3n",
60
+ open_chat: "Abrir chat",
61
+ close_chat: "Cerrar chat",
62
+ send_message: "Enviar mensaje",
63
+ helpful: "\xDAtil",
64
+ not_helpful: "No \xFAtil",
65
+ menu: "Men\xFA"
66
+ };
67
+ var locales = { en, es };
68
+ function createTranslate(locale) {
69
+ const resolved = resolveLocale(locale);
70
+ const translations = locales[resolved] ?? en;
71
+ return (key) => translations[key] ?? en[key] ?? key;
72
+ }
73
+ function resolveLocale(locale) {
74
+ if (locale) {
75
+ const base = locale.split("-")[0].toLowerCase();
76
+ if (locales[base]) return base;
77
+ }
78
+ if (typeof navigator !== "undefined" && navigator.language) {
79
+ const base = navigator.language.split("-")[0].toLowerCase();
80
+ if (locales[base]) return base;
81
+ }
82
+ return "en";
83
+ }
84
+
85
+ // src/client.ts
86
+ function resolveConfig(userConfig, fetched) {
87
+ return {
88
+ chatbotId: userConfig.chatbotId,
89
+ apiBase: userConfig.apiBase ?? DEFAULTS.apiBase,
90
+ primaryColor: userConfig.primaryColor ?? fetched?.primaryColor ?? DEFAULTS.primaryColor,
91
+ backgroundColor: userConfig.backgroundColor ?? fetched?.backgroundColor ?? DEFAULTS.backgroundColor,
92
+ textColor: userConfig.textColor ?? fetched?.textColor ?? DEFAULTS.textColor,
93
+ position: userConfig.position ?? fetched?.position ?? DEFAULTS.position,
94
+ placeholderText: userConfig.placeholderText ?? fetched?.placeholderText ?? DEFAULTS.placeholderText,
95
+ welcomeMessage: userConfig.welcomeMessage ?? fetched?.welcomeMessage ?? DEFAULTS.welcomeMessage,
96
+ title: userConfig.title ?? fetched?.title ?? DEFAULTS.title,
97
+ avatarUrl: userConfig.avatarUrl ?? fetched?.avatarUrl,
98
+ suggestedMessages: userConfig.suggestedMessages ?? fetched?.suggestedMessages ?? []
99
+ };
100
+ }
101
+ function getStorage() {
102
+ try {
103
+ return typeof window !== "undefined" ? window.localStorage : null;
104
+ } catch {
105
+ return null;
106
+ }
107
+ }
108
+ var CustomerHeroChat = class {
109
+ state;
110
+ listeners = /* @__PURE__ */ new Set();
111
+ storage;
112
+ userConfig;
113
+ identityData = null;
114
+ t;
115
+ constructor(config) {
116
+ this.userConfig = config;
117
+ this.storage = getStorage();
118
+ this.t = createTranslate(config.locale);
119
+ const resolved = resolveConfig(config);
120
+ const storedConvId = this.storage?.getItem(`ch_conv_${config.chatbotId}`);
121
+ this.state = {
122
+ messages: [],
123
+ isOpen: false,
124
+ isLoading: false,
125
+ conversationId: storedConvId ?? null,
126
+ config: resolved,
127
+ configLoaded: false,
128
+ configError: null,
129
+ error: null,
130
+ identity: null
131
+ };
132
+ }
133
+ subscribe(listener) {
134
+ this.listeners.add(listener);
135
+ return () => {
136
+ this.listeners.delete(listener);
137
+ };
138
+ }
139
+ getState() {
140
+ return this.state;
141
+ }
142
+ setState(partial) {
143
+ this.state = { ...this.state, ...partial };
144
+ this.notifyListeners();
145
+ }
146
+ notifyListeners() {
147
+ for (const listener of this.listeners) {
148
+ listener(this.state);
149
+ }
150
+ }
151
+ async fetchConfig() {
152
+ const { chatbotId } = this.userConfig;
153
+ const apiBase = this.userConfig.apiBase ?? DEFAULTS.apiBase;
154
+ try {
155
+ const response = await fetch(`${apiBase}/api/widget/${chatbotId}/config`);
156
+ if (!response.ok) {
157
+ throw new Error(`Failed to fetch config: ${response.status}`);
158
+ }
159
+ const fetched = await response.json();
160
+ const resolved = resolveConfig(this.userConfig, fetched);
161
+ this.setState({ config: resolved, configLoaded: true });
162
+ } catch (error) {
163
+ const errorMsg = error instanceof Error ? error.message : "Failed to load widget config";
164
+ console.error("CustomerHero: Failed to fetch widget config", error);
165
+ this.setState({
166
+ configLoaded: true,
167
+ configError: errorMsg
168
+ });
169
+ return;
170
+ }
171
+ if (this.state.conversationId) {
172
+ await this.loadHistory();
173
+ }
174
+ }
175
+ async loadHistory() {
176
+ const { chatbotId, apiBase } = this.state.config;
177
+ const { conversationId } = this.state;
178
+ if (!conversationId) return;
179
+ try {
180
+ const response = await fetch(
181
+ `${apiBase}/api/chat/${chatbotId}/messages/${conversationId}`
182
+ );
183
+ if (!response.ok) {
184
+ this.storage?.removeItem(`ch_conv_${chatbotId}`);
185
+ this.setState({ conversationId: null });
186
+ return;
187
+ }
188
+ const data = await response.json();
189
+ const messages = (data.messages ?? []).map(
190
+ (m) => ({
191
+ id: m.id,
192
+ role: m.role,
193
+ content: m.content
194
+ })
195
+ );
196
+ if (messages.length > 0) {
197
+ this.setState({ messages });
198
+ }
199
+ } catch {
200
+ }
201
+ }
202
+ async sendMessage(message) {
203
+ const trimmed = message.trim();
204
+ if (!trimmed || this.state.isLoading) return;
205
+ const userMsg = { role: "user", content: trimmed };
206
+ this.setState({
207
+ messages: [...this.state.messages, userMsg],
208
+ isLoading: true,
209
+ error: null
210
+ });
211
+ const { chatbotId } = this.state.config;
212
+ const apiBase = this.state.config.apiBase;
213
+ try {
214
+ const response = await fetch(`${apiBase}/api/chat/${chatbotId}`, {
215
+ method: "POST",
216
+ headers: { "Content-Type": "application/json" },
217
+ body: JSON.stringify({
218
+ message: trimmed,
219
+ ...this.state.conversationId ? { conversationId: this.state.conversationId } : {},
220
+ ...this.identityData ? { identity: this.identityData } : {}
221
+ })
222
+ });
223
+ if (!response.ok) {
224
+ const data2 = await response.json().catch(() => null);
225
+ const errorMsg = data2?.error ?? `Request failed: ${response.status}`;
226
+ throw new Error(errorMsg);
227
+ }
228
+ const data = await response.json();
229
+ const botMsg = {
230
+ id: data.messageId,
231
+ role: "bot",
232
+ content: data.message
233
+ };
234
+ const conversationId = data.conversationId ?? this.state.conversationId;
235
+ if (conversationId) {
236
+ this.storage?.setItem(`ch_conv_${chatbotId}`, conversationId);
237
+ }
238
+ this.setState({
239
+ messages: [...this.state.messages, botMsg],
240
+ isLoading: false,
241
+ conversationId
242
+ });
243
+ } catch (error) {
244
+ const errorMsg = error instanceof Error ? error.message : "Something went wrong";
245
+ this.setState({
246
+ isLoading: false,
247
+ error: errorMsg
248
+ });
249
+ }
250
+ }
251
+ async rateMessage(messageId, rating) {
252
+ const { chatbotId, apiBase } = this.state.config;
253
+ const { conversationId } = this.state;
254
+ if (!conversationId) return;
255
+ try {
256
+ await fetch(`${apiBase}/api/chat/${chatbotId}/rate`, {
257
+ method: "POST",
258
+ headers: { "Content-Type": "application/json" },
259
+ body: JSON.stringify({ conversationId, messageId, rating })
260
+ });
261
+ } catch (error) {
262
+ console.error("CustomerHero: Failed to rate message", error);
263
+ }
264
+ }
265
+ toggle() {
266
+ const willOpen = !this.state.isOpen;
267
+ if (willOpen && this.state.messages.length === 0 && !this.state.conversationId && this.state.config.welcomeMessage) {
268
+ this.setState({
269
+ isOpen: true,
270
+ messages: [{ role: "bot", content: this.state.config.welcomeMessage }]
271
+ });
272
+ } else {
273
+ this.setState({ isOpen: willOpen });
274
+ }
275
+ }
276
+ open() {
277
+ if (!this.state.isOpen) this.toggle();
278
+ }
279
+ close() {
280
+ if (this.state.isOpen) this.setState({ isOpen: false });
281
+ }
282
+ reset() {
283
+ const { chatbotId, welcomeMessage } = this.state.config;
284
+ this.storage?.removeItem(`ch_conv_${chatbotId}`);
285
+ this.setState({
286
+ messages: welcomeMessage ? [{ role: "bot", content: welcomeMessage }] : [],
287
+ conversationId: null,
288
+ isLoading: false,
289
+ error: null
290
+ });
291
+ }
292
+ identify(payload) {
293
+ const { userId, email, name, phone, company, userHash, ...rest } = payload;
294
+ const customProperties = {};
295
+ for (const [k, v] of Object.entries(rest)) {
296
+ if (typeof v === "string" || typeof v === "number" || typeof v === "boolean") {
297
+ customProperties[k] = v;
298
+ }
299
+ }
300
+ this.identityData = {
301
+ userId,
302
+ email,
303
+ name,
304
+ phone,
305
+ company,
306
+ userHash,
307
+ customProperties: Object.keys(customProperties).length > 0 ? customProperties : void 0
308
+ };
309
+ const { chatbotId, welcomeMessage } = this.state.config;
310
+ this.storage?.removeItem(`ch_conv_${chatbotId}`);
311
+ this.setState({
312
+ messages: welcomeMessage ? [{ role: "bot", content: welcomeMessage }] : [],
313
+ conversationId: null,
314
+ isLoading: false,
315
+ error: null,
316
+ identity: this.identityData
317
+ });
318
+ }
319
+ };
320
+ // Annotate the CommonJS export names for ESM import in node:
321
+ 0 && (module.exports = {
322
+ CustomerHeroChat,
323
+ DEFAULTS
324
+ });
@@ -0,0 +1,122 @@
1
+ type TranslationKey = "online" | "typing" | "unable_to_load" | "powered_by" | "new_conversation" | "open_chat" | "close_chat" | "send_message" | "helpful" | "not_helpful" | "menu";
2
+ type TranslateFn = (key: TranslationKey) => string;
3
+
4
+ interface CustomerHeroChatConfig {
5
+ /** The chatbot ID to connect to */
6
+ chatbotId: string;
7
+ /** API base URL (defaults to "https://customerhero.app") */
8
+ apiBase?: string;
9
+ /** Override primary/accent color */
10
+ primaryColor?: string;
11
+ /** Override chat window background color */
12
+ backgroundColor?: string;
13
+ /** Override text color */
14
+ textColor?: string;
15
+ /** Widget position */
16
+ position?: "bottom-right" | "bottom-left";
17
+ /** Override input placeholder text */
18
+ placeholderText?: string;
19
+ /** Override welcome message */
20
+ welcomeMessage?: string;
21
+ /** Override header title */
22
+ title?: string;
23
+ /** Override avatar URL */
24
+ avatarUrl?: string;
25
+ /** Widget locale (e.g. "en", "es"). Auto-detected from browser if omitted. */
26
+ locale?: string;
27
+ /** Predefined quick-reply options shown before the user sends a message */
28
+ suggestedMessages?: string[];
29
+ }
30
+ interface ResolvedConfig {
31
+ chatbotId: string;
32
+ apiBase: string;
33
+ primaryColor: string;
34
+ backgroundColor: string;
35
+ textColor: string;
36
+ position: "bottom-right" | "bottom-left";
37
+ placeholderText: string;
38
+ welcomeMessage: string;
39
+ title: string;
40
+ avatarUrl?: string;
41
+ suggestedMessages: string[];
42
+ }
43
+ interface ChatMessage {
44
+ /** Message ID from the API (only present for bot messages) */
45
+ id?: string;
46
+ role: "user" | "bot";
47
+ content: string;
48
+ }
49
+ type MessageRating = "positive" | "negative";
50
+ interface IdentifyPayload {
51
+ /** Stable unique user ID from your system (required) */
52
+ userId: string;
53
+ /** User's email address */
54
+ email?: string;
55
+ /** User's display name */
56
+ name?: string;
57
+ /** User's phone number */
58
+ phone?: string;
59
+ /** User's company name */
60
+ company?: string;
61
+ /** HMAC-SHA256 hash for identity verification */
62
+ userHash?: string;
63
+ /** Additional custom properties (string, number, or boolean values) */
64
+ [key: string]: unknown;
65
+ }
66
+ interface IdentityData {
67
+ userId: string;
68
+ email?: string;
69
+ name?: string;
70
+ phone?: string;
71
+ company?: string;
72
+ userHash?: string;
73
+ customProperties?: Record<string, string | number | boolean>;
74
+ }
75
+ interface ChatState {
76
+ messages: ChatMessage[];
77
+ isOpen: boolean;
78
+ isLoading: boolean;
79
+ conversationId: string | null;
80
+ config: ResolvedConfig;
81
+ configLoaded: boolean;
82
+ configError: string | null;
83
+ error: string | null;
84
+ identity: IdentityData | null;
85
+ }
86
+
87
+ type Listener = (state: ChatState) => void;
88
+ declare class CustomerHeroChat {
89
+ private state;
90
+ private listeners;
91
+ private storage;
92
+ private userConfig;
93
+ private identityData;
94
+ readonly t: TranslateFn;
95
+ constructor(config: CustomerHeroChatConfig);
96
+ subscribe(listener: Listener): () => void;
97
+ getState(): ChatState;
98
+ private setState;
99
+ private notifyListeners;
100
+ fetchConfig(): Promise<void>;
101
+ private loadHistory;
102
+ sendMessage(message: string): Promise<void>;
103
+ rateMessage(messageId: string, rating: MessageRating): Promise<void>;
104
+ toggle(): void;
105
+ open(): void;
106
+ close(): void;
107
+ reset(): void;
108
+ identify(payload: IdentifyPayload): void;
109
+ }
110
+
111
+ declare const DEFAULTS: {
112
+ apiBase: string;
113
+ primaryColor: string;
114
+ backgroundColor: string;
115
+ textColor: string;
116
+ position: "bottom-right";
117
+ placeholderText: string;
118
+ welcomeMessage: string;
119
+ title: string;
120
+ };
121
+
122
+ export { type ChatMessage, type ChatState, CustomerHeroChat, type CustomerHeroChatConfig, DEFAULTS, type IdentifyPayload, type IdentityData, type MessageRating, type ResolvedConfig, type TranslateFn, type TranslationKey };
@@ -0,0 +1,122 @@
1
+ type TranslationKey = "online" | "typing" | "unable_to_load" | "powered_by" | "new_conversation" | "open_chat" | "close_chat" | "send_message" | "helpful" | "not_helpful" | "menu";
2
+ type TranslateFn = (key: TranslationKey) => string;
3
+
4
+ interface CustomerHeroChatConfig {
5
+ /** The chatbot ID to connect to */
6
+ chatbotId: string;
7
+ /** API base URL (defaults to "https://customerhero.app") */
8
+ apiBase?: string;
9
+ /** Override primary/accent color */
10
+ primaryColor?: string;
11
+ /** Override chat window background color */
12
+ backgroundColor?: string;
13
+ /** Override text color */
14
+ textColor?: string;
15
+ /** Widget position */
16
+ position?: "bottom-right" | "bottom-left";
17
+ /** Override input placeholder text */
18
+ placeholderText?: string;
19
+ /** Override welcome message */
20
+ welcomeMessage?: string;
21
+ /** Override header title */
22
+ title?: string;
23
+ /** Override avatar URL */
24
+ avatarUrl?: string;
25
+ /** Widget locale (e.g. "en", "es"). Auto-detected from browser if omitted. */
26
+ locale?: string;
27
+ /** Predefined quick-reply options shown before the user sends a message */
28
+ suggestedMessages?: string[];
29
+ }
30
+ interface ResolvedConfig {
31
+ chatbotId: string;
32
+ apiBase: string;
33
+ primaryColor: string;
34
+ backgroundColor: string;
35
+ textColor: string;
36
+ position: "bottom-right" | "bottom-left";
37
+ placeholderText: string;
38
+ welcomeMessage: string;
39
+ title: string;
40
+ avatarUrl?: string;
41
+ suggestedMessages: string[];
42
+ }
43
+ interface ChatMessage {
44
+ /** Message ID from the API (only present for bot messages) */
45
+ id?: string;
46
+ role: "user" | "bot";
47
+ content: string;
48
+ }
49
+ type MessageRating = "positive" | "negative";
50
+ interface IdentifyPayload {
51
+ /** Stable unique user ID from your system (required) */
52
+ userId: string;
53
+ /** User's email address */
54
+ email?: string;
55
+ /** User's display name */
56
+ name?: string;
57
+ /** User's phone number */
58
+ phone?: string;
59
+ /** User's company name */
60
+ company?: string;
61
+ /** HMAC-SHA256 hash for identity verification */
62
+ userHash?: string;
63
+ /** Additional custom properties (string, number, or boolean values) */
64
+ [key: string]: unknown;
65
+ }
66
+ interface IdentityData {
67
+ userId: string;
68
+ email?: string;
69
+ name?: string;
70
+ phone?: string;
71
+ company?: string;
72
+ userHash?: string;
73
+ customProperties?: Record<string, string | number | boolean>;
74
+ }
75
+ interface ChatState {
76
+ messages: ChatMessage[];
77
+ isOpen: boolean;
78
+ isLoading: boolean;
79
+ conversationId: string | null;
80
+ config: ResolvedConfig;
81
+ configLoaded: boolean;
82
+ configError: string | null;
83
+ error: string | null;
84
+ identity: IdentityData | null;
85
+ }
86
+
87
+ type Listener = (state: ChatState) => void;
88
+ declare class CustomerHeroChat {
89
+ private state;
90
+ private listeners;
91
+ private storage;
92
+ private userConfig;
93
+ private identityData;
94
+ readonly t: TranslateFn;
95
+ constructor(config: CustomerHeroChatConfig);
96
+ subscribe(listener: Listener): () => void;
97
+ getState(): ChatState;
98
+ private setState;
99
+ private notifyListeners;
100
+ fetchConfig(): Promise<void>;
101
+ private loadHistory;
102
+ sendMessage(message: string): Promise<void>;
103
+ rateMessage(messageId: string, rating: MessageRating): Promise<void>;
104
+ toggle(): void;
105
+ open(): void;
106
+ close(): void;
107
+ reset(): void;
108
+ identify(payload: IdentifyPayload): void;
109
+ }
110
+
111
+ declare const DEFAULTS: {
112
+ apiBase: string;
113
+ primaryColor: string;
114
+ backgroundColor: string;
115
+ textColor: string;
116
+ position: "bottom-right";
117
+ placeholderText: string;
118
+ welcomeMessage: string;
119
+ title: string;
120
+ };
121
+
122
+ export { type ChatMessage, type ChatState, CustomerHeroChat, type CustomerHeroChatConfig, DEFAULTS, type IdentifyPayload, type IdentityData, type MessageRating, type ResolvedConfig, type TranslateFn, type TranslationKey };
package/dist/index.js ADDED
@@ -0,0 +1,296 @@
1
+ // src/defaults.ts
2
+ var DEFAULTS = {
3
+ apiBase: "https://customerhero.app",
4
+ primaryColor: "#6C3CE1",
5
+ backgroundColor: "#FFFFFF",
6
+ textColor: "#1A1A2E",
7
+ position: "bottom-right",
8
+ placeholderText: "Type your message...",
9
+ welcomeMessage: "Hi! How can I help you today?",
10
+ title: "CustomerHero"
11
+ };
12
+
13
+ // src/i18n.ts
14
+ var en = {
15
+ online: "Online",
16
+ typing: "Typing...",
17
+ unable_to_load: "Unable to load chat",
18
+ powered_by: "Powered by",
19
+ new_conversation: "New conversation",
20
+ open_chat: "Open chat",
21
+ close_chat: "Close chat",
22
+ send_message: "Send message",
23
+ helpful: "Helpful",
24
+ not_helpful: "Not helpful",
25
+ menu: "Menu"
26
+ };
27
+ var es = {
28
+ online: "En l\xEDnea",
29
+ typing: "Escribiendo...",
30
+ unable_to_load: "No se pudo cargar el chat",
31
+ powered_by: "Desarrollado por",
32
+ new_conversation: "Nueva conversaci\xF3n",
33
+ open_chat: "Abrir chat",
34
+ close_chat: "Cerrar chat",
35
+ send_message: "Enviar mensaje",
36
+ helpful: "\xDAtil",
37
+ not_helpful: "No \xFAtil",
38
+ menu: "Men\xFA"
39
+ };
40
+ var locales = { en, es };
41
+ function createTranslate(locale) {
42
+ const resolved = resolveLocale(locale);
43
+ const translations = locales[resolved] ?? en;
44
+ return (key) => translations[key] ?? en[key] ?? key;
45
+ }
46
+ function resolveLocale(locale) {
47
+ if (locale) {
48
+ const base = locale.split("-")[0].toLowerCase();
49
+ if (locales[base]) return base;
50
+ }
51
+ if (typeof navigator !== "undefined" && navigator.language) {
52
+ const base = navigator.language.split("-")[0].toLowerCase();
53
+ if (locales[base]) return base;
54
+ }
55
+ return "en";
56
+ }
57
+
58
+ // src/client.ts
59
+ function resolveConfig(userConfig, fetched) {
60
+ return {
61
+ chatbotId: userConfig.chatbotId,
62
+ apiBase: userConfig.apiBase ?? DEFAULTS.apiBase,
63
+ primaryColor: userConfig.primaryColor ?? fetched?.primaryColor ?? DEFAULTS.primaryColor,
64
+ backgroundColor: userConfig.backgroundColor ?? fetched?.backgroundColor ?? DEFAULTS.backgroundColor,
65
+ textColor: userConfig.textColor ?? fetched?.textColor ?? DEFAULTS.textColor,
66
+ position: userConfig.position ?? fetched?.position ?? DEFAULTS.position,
67
+ placeholderText: userConfig.placeholderText ?? fetched?.placeholderText ?? DEFAULTS.placeholderText,
68
+ welcomeMessage: userConfig.welcomeMessage ?? fetched?.welcomeMessage ?? DEFAULTS.welcomeMessage,
69
+ title: userConfig.title ?? fetched?.title ?? DEFAULTS.title,
70
+ avatarUrl: userConfig.avatarUrl ?? fetched?.avatarUrl,
71
+ suggestedMessages: userConfig.suggestedMessages ?? fetched?.suggestedMessages ?? []
72
+ };
73
+ }
74
+ function getStorage() {
75
+ try {
76
+ return typeof window !== "undefined" ? window.localStorage : null;
77
+ } catch {
78
+ return null;
79
+ }
80
+ }
81
+ var CustomerHeroChat = class {
82
+ state;
83
+ listeners = /* @__PURE__ */ new Set();
84
+ storage;
85
+ userConfig;
86
+ identityData = null;
87
+ t;
88
+ constructor(config) {
89
+ this.userConfig = config;
90
+ this.storage = getStorage();
91
+ this.t = createTranslate(config.locale);
92
+ const resolved = resolveConfig(config);
93
+ const storedConvId = this.storage?.getItem(`ch_conv_${config.chatbotId}`);
94
+ this.state = {
95
+ messages: [],
96
+ isOpen: false,
97
+ isLoading: false,
98
+ conversationId: storedConvId ?? null,
99
+ config: resolved,
100
+ configLoaded: false,
101
+ configError: null,
102
+ error: null,
103
+ identity: null
104
+ };
105
+ }
106
+ subscribe(listener) {
107
+ this.listeners.add(listener);
108
+ return () => {
109
+ this.listeners.delete(listener);
110
+ };
111
+ }
112
+ getState() {
113
+ return this.state;
114
+ }
115
+ setState(partial) {
116
+ this.state = { ...this.state, ...partial };
117
+ this.notifyListeners();
118
+ }
119
+ notifyListeners() {
120
+ for (const listener of this.listeners) {
121
+ listener(this.state);
122
+ }
123
+ }
124
+ async fetchConfig() {
125
+ const { chatbotId } = this.userConfig;
126
+ const apiBase = this.userConfig.apiBase ?? DEFAULTS.apiBase;
127
+ try {
128
+ const response = await fetch(`${apiBase}/api/widget/${chatbotId}/config`);
129
+ if (!response.ok) {
130
+ throw new Error(`Failed to fetch config: ${response.status}`);
131
+ }
132
+ const fetched = await response.json();
133
+ const resolved = resolveConfig(this.userConfig, fetched);
134
+ this.setState({ config: resolved, configLoaded: true });
135
+ } catch (error) {
136
+ const errorMsg = error instanceof Error ? error.message : "Failed to load widget config";
137
+ console.error("CustomerHero: Failed to fetch widget config", error);
138
+ this.setState({
139
+ configLoaded: true,
140
+ configError: errorMsg
141
+ });
142
+ return;
143
+ }
144
+ if (this.state.conversationId) {
145
+ await this.loadHistory();
146
+ }
147
+ }
148
+ async loadHistory() {
149
+ const { chatbotId, apiBase } = this.state.config;
150
+ const { conversationId } = this.state;
151
+ if (!conversationId) return;
152
+ try {
153
+ const response = await fetch(
154
+ `${apiBase}/api/chat/${chatbotId}/messages/${conversationId}`
155
+ );
156
+ if (!response.ok) {
157
+ this.storage?.removeItem(`ch_conv_${chatbotId}`);
158
+ this.setState({ conversationId: null });
159
+ return;
160
+ }
161
+ const data = await response.json();
162
+ const messages = (data.messages ?? []).map(
163
+ (m) => ({
164
+ id: m.id,
165
+ role: m.role,
166
+ content: m.content
167
+ })
168
+ );
169
+ if (messages.length > 0) {
170
+ this.setState({ messages });
171
+ }
172
+ } catch {
173
+ }
174
+ }
175
+ async sendMessage(message) {
176
+ const trimmed = message.trim();
177
+ if (!trimmed || this.state.isLoading) return;
178
+ const userMsg = { role: "user", content: trimmed };
179
+ this.setState({
180
+ messages: [...this.state.messages, userMsg],
181
+ isLoading: true,
182
+ error: null
183
+ });
184
+ const { chatbotId } = this.state.config;
185
+ const apiBase = this.state.config.apiBase;
186
+ try {
187
+ const response = await fetch(`${apiBase}/api/chat/${chatbotId}`, {
188
+ method: "POST",
189
+ headers: { "Content-Type": "application/json" },
190
+ body: JSON.stringify({
191
+ message: trimmed,
192
+ ...this.state.conversationId ? { conversationId: this.state.conversationId } : {},
193
+ ...this.identityData ? { identity: this.identityData } : {}
194
+ })
195
+ });
196
+ if (!response.ok) {
197
+ const data2 = await response.json().catch(() => null);
198
+ const errorMsg = data2?.error ?? `Request failed: ${response.status}`;
199
+ throw new Error(errorMsg);
200
+ }
201
+ const data = await response.json();
202
+ const botMsg = {
203
+ id: data.messageId,
204
+ role: "bot",
205
+ content: data.message
206
+ };
207
+ const conversationId = data.conversationId ?? this.state.conversationId;
208
+ if (conversationId) {
209
+ this.storage?.setItem(`ch_conv_${chatbotId}`, conversationId);
210
+ }
211
+ this.setState({
212
+ messages: [...this.state.messages, botMsg],
213
+ isLoading: false,
214
+ conversationId
215
+ });
216
+ } catch (error) {
217
+ const errorMsg = error instanceof Error ? error.message : "Something went wrong";
218
+ this.setState({
219
+ isLoading: false,
220
+ error: errorMsg
221
+ });
222
+ }
223
+ }
224
+ async rateMessage(messageId, rating) {
225
+ const { chatbotId, apiBase } = this.state.config;
226
+ const { conversationId } = this.state;
227
+ if (!conversationId) return;
228
+ try {
229
+ await fetch(`${apiBase}/api/chat/${chatbotId}/rate`, {
230
+ method: "POST",
231
+ headers: { "Content-Type": "application/json" },
232
+ body: JSON.stringify({ conversationId, messageId, rating })
233
+ });
234
+ } catch (error) {
235
+ console.error("CustomerHero: Failed to rate message", error);
236
+ }
237
+ }
238
+ toggle() {
239
+ const willOpen = !this.state.isOpen;
240
+ if (willOpen && this.state.messages.length === 0 && !this.state.conversationId && this.state.config.welcomeMessage) {
241
+ this.setState({
242
+ isOpen: true,
243
+ messages: [{ role: "bot", content: this.state.config.welcomeMessage }]
244
+ });
245
+ } else {
246
+ this.setState({ isOpen: willOpen });
247
+ }
248
+ }
249
+ open() {
250
+ if (!this.state.isOpen) this.toggle();
251
+ }
252
+ close() {
253
+ if (this.state.isOpen) this.setState({ isOpen: false });
254
+ }
255
+ reset() {
256
+ const { chatbotId, welcomeMessage } = this.state.config;
257
+ this.storage?.removeItem(`ch_conv_${chatbotId}`);
258
+ this.setState({
259
+ messages: welcomeMessage ? [{ role: "bot", content: welcomeMessage }] : [],
260
+ conversationId: null,
261
+ isLoading: false,
262
+ error: null
263
+ });
264
+ }
265
+ identify(payload) {
266
+ const { userId, email, name, phone, company, userHash, ...rest } = payload;
267
+ const customProperties = {};
268
+ for (const [k, v] of Object.entries(rest)) {
269
+ if (typeof v === "string" || typeof v === "number" || typeof v === "boolean") {
270
+ customProperties[k] = v;
271
+ }
272
+ }
273
+ this.identityData = {
274
+ userId,
275
+ email,
276
+ name,
277
+ phone,
278
+ company,
279
+ userHash,
280
+ customProperties: Object.keys(customProperties).length > 0 ? customProperties : void 0
281
+ };
282
+ const { chatbotId, welcomeMessage } = this.state.config;
283
+ this.storage?.removeItem(`ch_conv_${chatbotId}`);
284
+ this.setState({
285
+ messages: welcomeMessage ? [{ role: "bot", content: welcomeMessage }] : [],
286
+ conversationId: null,
287
+ isLoading: false,
288
+ error: null,
289
+ identity: this.identityData
290
+ });
291
+ }
292
+ };
293
+ export {
294
+ CustomerHeroChat,
295
+ DEFAULTS
296
+ };
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "@customerhero/js",
3
+ "version": "0.0.1",
4
+ "private": false,
5
+ "description": "Framework-agnostic JavaScript client for the CustomerHero chat widget.",
6
+ "keywords": [
7
+ "customerhero",
8
+ "chat",
9
+ "chatbot",
10
+ "customer-support",
11
+ "widget",
12
+ "sdk"
13
+ ],
14
+ "homepage": "https://customerhero.app",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/customerhero/customerhero-sdk.git",
18
+ "directory": "packages/js"
19
+ },
20
+ "bugs": {
21
+ "url": "https://github.com/customerhero/customerhero-sdk/issues"
22
+ },
23
+ "license": "MIT",
24
+ "author": "CustomerHero",
25
+ "type": "module",
26
+ "sideEffects": false,
27
+ "exports": {
28
+ ".": {
29
+ "import": {
30
+ "types": "./dist/index.d.ts",
31
+ "default": "./dist/index.js"
32
+ },
33
+ "require": {
34
+ "types": "./dist/index.d.cts",
35
+ "default": "./dist/index.cjs"
36
+ }
37
+ }
38
+ },
39
+ "main": "./dist/index.cjs",
40
+ "module": "./dist/index.js",
41
+ "types": "./dist/index.d.ts",
42
+ "files": [
43
+ "dist",
44
+ "README.md",
45
+ "LICENSE"
46
+ ],
47
+ "engines": {
48
+ "node": ">=18"
49
+ },
50
+ "scripts": {
51
+ "build": "tsup",
52
+ "dev": "tsup --watch",
53
+ "typecheck": "tsc --noEmit",
54
+ "test": "vitest run"
55
+ },
56
+ "devDependencies": {
57
+ "tsup": "^8.4.0",
58
+ "typescript": "^5.7.0",
59
+ "vitest": "^2.1.9"
60
+ },
61
+ "publishConfig": {
62
+ "access": "public",
63
+ "registry": "https://registry.npmjs.org/"
64
+ }
65
+ }