@aichatbot-saas/react 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.
@@ -0,0 +1,104 @@
1
+ import * as react from 'react';
2
+ import { CSSProperties } from 'react';
3
+ import { ChatTheme, ChatState, ChatController, ChatMessage, FeedbackValue } from '@aichatbot-saas/core';
4
+ export { AIChatbotClient, ChatConfig, ChatController, ChatControllerOptions, ChatMessage, ChatResponse, ChatRole, ChatState, ChatStatus, ChatTheme, DEFAULT_THEME, FeedbackValue, Suggestion, SuggestionInput, createChatController, resolveTheme } from '@aichatbot-saas/core';
5
+
6
+ interface AIChatbotProps {
7
+ /** Publishable API key (required). */
8
+ apiKey: string;
9
+ /** Backend base URL. Defaults to same-origin. */
10
+ apiUrl?: string;
11
+ /** Force a language; otherwise config.languages[0] -> "en". */
12
+ language?: string;
13
+ /** Local theme overrides (highest precedence: client -> backend -> default). */
14
+ theme?: Partial<ChatTheme>;
15
+ /** Stable device/session id forwarded to the backend for threading. */
16
+ sessionId?: string;
17
+ /** Extra class on the root panel. */
18
+ className?: string;
19
+ /** Extra inline styles merged onto the root panel (e.g. width/height). */
20
+ style?: CSSProperties;
21
+ }
22
+ /**
23
+ * `<AIChatbot>` — a native, inline, embeddable chat panel for the web.
24
+ *
25
+ * It fills its container, so the host controls size and placement (drop it into
26
+ * a sized `<div>`). All behavior (config load, greeting, suggestions, send flow,
27
+ * conversation persistence, feedback, error handling) lives in the shared
28
+ * headless controller; this component only renders that state and wires events.
29
+ *
30
+ * Styling is 100% inline and derived from the resolved theme — no global CSS, no
31
+ * external UI libs — so it never clobbers host styles and is SSR-safe.
32
+ *
33
+ * In Next.js / RSC, render it from a Client Component (add `"use client"`).
34
+ */
35
+ declare function AIChatbot({ apiKey, apiUrl, language, theme, sessionId, className, style, }: AIChatbotProps): react.JSX.Element;
36
+
37
+ /** Options accepted by {@link useChatController}. Mirrors ChatControllerOptions. */
38
+ interface UseChatControllerOptions {
39
+ apiKey: string;
40
+ apiUrl?: string;
41
+ language?: string;
42
+ theme?: Partial<ChatTheme>;
43
+ sessionId?: string;
44
+ }
45
+ interface UseChatControllerResult {
46
+ state: ChatState;
47
+ controller: ChatController;
48
+ }
49
+ /**
50
+ * Creates and drives a headless {@link ChatController}, exposing its immutable
51
+ * state to React via `useSyncExternalStore` (React 18+, concurrent-safe).
52
+ *
53
+ * The controller is memoized on the identity-defining inputs (apiKey, apiUrl,
54
+ * language, serialized theme, sessionId); when any of those change a fresh
55
+ * controller is created, started, and the previous one is destroyed.
56
+ *
57
+ * SSR-safe: the controller is constructed lazily and never touches
58
+ * window/document during render. `start()` (which performs network/storage I/O)
59
+ * runs only inside an effect, so it never executes on the server.
60
+ */
61
+ declare function useChatController(options: UseChatControllerOptions): UseChatControllerResult;
62
+
63
+ interface MessageBubbleProps {
64
+ message: ChatMessage;
65
+ theme: ChatTheme;
66
+ /**
67
+ * Called when the user rates this (bot) message. Only rendered for bot
68
+ * messages that carry a `serverId` (i.e. not the greeting and not error
69
+ * bubbles).
70
+ */
71
+ onFeedback?: (message: ChatMessage, value: FeedbackValue) => void;
72
+ }
73
+ /**
74
+ * A single chat bubble. User messages align right with the primary color; bot
75
+ * messages align left with a bordered white surface. Bot messages eligible for
76
+ * feedback render thumbs up/down beneath the bubble (replaced by "Thanks!" once
77
+ * rated). All styling is inline and derived from the theme.
78
+ */
79
+ declare function MessageBubble({ message, theme, onFeedback }: MessageBubbleProps): react.JSX.Element;
80
+
81
+ interface SuggestionChipsProps {
82
+ suggestions: string[];
83
+ theme: ChatTheme;
84
+ disabled?: boolean;
85
+ onSelect: (text: string) => void;
86
+ }
87
+ /**
88
+ * A wrapping row of tappable suggestion chips, themed from the primary color.
89
+ * Renders nothing when there are no suggestions.
90
+ */
91
+ declare function SuggestionChips({ suggestions, theme, disabled, onSelect, }: SuggestionChipsProps): react.JSX.Element | null;
92
+
93
+ interface TypingIndicatorProps {
94
+ theme: ChatTheme;
95
+ /** Bot name to personalize the label, e.g. "Assistant is typing…". */
96
+ botName?: string;
97
+ }
98
+ /**
99
+ * The "<bot> is typing…" indicator shown while the controller is awaiting a
100
+ * reply. Themed via mutedTextColor.
101
+ */
102
+ declare function TypingIndicator({ theme, botName }: TypingIndicatorProps): react.JSX.Element;
103
+
104
+ export { AIChatbot, type AIChatbotProps, MessageBubble, type MessageBubbleProps, SuggestionChips, type SuggestionChipsProps, TypingIndicator, type TypingIndicatorProps, type UseChatControllerOptions, type UseChatControllerResult, AIChatbot as default, useChatController };
package/dist/index.js ADDED
@@ -0,0 +1,386 @@
1
+ // src/AIChatbot.tsx
2
+ import {
3
+ useCallback,
4
+ useEffect as useEffect2,
5
+ useId,
6
+ useRef as useRef2,
7
+ useState
8
+ } from "react";
9
+
10
+ // src/useChatController.ts
11
+ import { useEffect, useMemo, useRef, useSyncExternalStore } from "react";
12
+ import {
13
+ createChatController
14
+ } from "@aichatbot-saas/core";
15
+ function useChatController(options) {
16
+ var _a;
17
+ const { apiKey, apiUrl, language, theme, sessionId } = options;
18
+ const themeKey = useMemo(() => JSON.stringify(theme != null ? theme : {}), [theme]);
19
+ const controller = useMemo(
20
+ () => createChatController({
21
+ apiKey,
22
+ apiUrl,
23
+ language,
24
+ theme,
25
+ sessionId
26
+ }),
27
+ // theme is captured via its serialized key; eslint can't see that.
28
+ // eslint-disable-next-line react-hooks/exhaustive-deps
29
+ [apiKey, apiUrl, language, themeKey, sessionId]
30
+ );
31
+ const serverSnapshotRef = useRef(
32
+ null
33
+ );
34
+ if (((_a = serverSnapshotRef.current) == null ? void 0 : _a.controller) !== controller) {
35
+ serverSnapshotRef.current = { controller, state: controller.getState() };
36
+ }
37
+ const getServerSnapshot = () => serverSnapshotRef.current.state;
38
+ const state = useSyncExternalStore(
39
+ controller.subscribe,
40
+ controller.getState,
41
+ getServerSnapshot
42
+ );
43
+ useEffect(() => {
44
+ void controller.start();
45
+ return () => {
46
+ controller.destroy();
47
+ };
48
+ }, [controller]);
49
+ return { state, controller };
50
+ }
51
+
52
+ // src/components/MessageBubble.tsx
53
+ import { jsx, jsxs } from "react/jsx-runtime";
54
+ function MessageBubble({ message, theme, onFeedback }) {
55
+ const isUser = message.role === "user";
56
+ const canRate = !isUser && !!message.serverId && !message.error;
57
+ const rowStyle = {
58
+ display: "flex",
59
+ flexDirection: "column",
60
+ alignItems: isUser ? "flex-end" : "flex-start",
61
+ margin: "8px 0"
62
+ };
63
+ const bubbleStyle = {
64
+ maxWidth: "80%",
65
+ padding: "9px 13px",
66
+ borderRadius: theme.bubbleRadius,
67
+ fontSize: 14,
68
+ lineHeight: 1.45,
69
+ whiteSpace: "pre-wrap",
70
+ wordWrap: "break-word",
71
+ overflowWrap: "anywhere",
72
+ ...isUser ? {
73
+ background: theme.userBubbleColor,
74
+ color: theme.userBubbleTextColor,
75
+ borderBottomRightRadius: 4
76
+ } : {
77
+ background: theme.botBubbleColor,
78
+ color: theme.botBubbleTextColor,
79
+ border: `1px solid ${theme.botBubbleBorderColor}`,
80
+ borderBottomLeftRadius: 4
81
+ }
82
+ };
83
+ return /* @__PURE__ */ jsxs("div", { style: rowStyle, children: [
84
+ /* @__PURE__ */ jsx("div", { style: bubbleStyle, children: message.text }),
85
+ canRate && (message.feedback ? /* @__PURE__ */ jsx("div", { style: { marginTop: 4, fontSize: 13, color: theme.mutedTextColor }, children: "Thanks!" }) : /* @__PURE__ */ jsx(FeedbackButtons, { message, theme, onFeedback }))
86
+ ] });
87
+ }
88
+ function FeedbackButtons({ message, theme, onFeedback }) {
89
+ const btnStyle = {
90
+ background: "none",
91
+ border: "none",
92
+ cursor: "pointer",
93
+ fontSize: 14,
94
+ padding: "0 2px",
95
+ color: theme.mutedTextColor,
96
+ opacity: 0.7,
97
+ lineHeight: 1
98
+ };
99
+ return /* @__PURE__ */ jsxs("div", { style: { marginTop: 4, display: "flex", gap: 2 }, children: [
100
+ /* @__PURE__ */ jsx(
101
+ "button",
102
+ {
103
+ type: "button",
104
+ title: "Helpful",
105
+ "aria-label": "Helpful",
106
+ style: btnStyle,
107
+ onClick: () => onFeedback == null ? void 0 : onFeedback(message, "positive"),
108
+ children: "\u{1F44D}"
109
+ }
110
+ ),
111
+ /* @__PURE__ */ jsx(
112
+ "button",
113
+ {
114
+ type: "button",
115
+ title: "Not helpful",
116
+ "aria-label": "Not helpful",
117
+ style: btnStyle,
118
+ onClick: () => onFeedback == null ? void 0 : onFeedback(message, "negative"),
119
+ children: "\u{1F44E}"
120
+ }
121
+ )
122
+ ] });
123
+ }
124
+
125
+ // src/components/SuggestionChips.tsx
126
+ import { jsx as jsx2 } from "react/jsx-runtime";
127
+ function SuggestionChips({
128
+ suggestions,
129
+ theme,
130
+ disabled = false,
131
+ onSelect
132
+ }) {
133
+ if (!suggestions.length) return null;
134
+ const rowStyle = {
135
+ display: "flex",
136
+ flexWrap: "wrap",
137
+ gap: 6,
138
+ margin: "6px 0 2px"
139
+ };
140
+ const chipStyle = {
141
+ background: theme.backgroundColor,
142
+ border: `1px solid ${theme.primaryColor}`,
143
+ color: theme.primaryColor,
144
+ borderRadius: 16,
145
+ padding: "6px 12px",
146
+ fontSize: 13,
147
+ cursor: disabled ? "default" : "pointer",
148
+ opacity: disabled ? 0.6 : 1,
149
+ fontFamily: "inherit",
150
+ lineHeight: 1.2
151
+ };
152
+ return /* @__PURE__ */ jsx2("div", { style: rowStyle, children: suggestions.map((label, i) => /* @__PURE__ */ jsx2(
153
+ "button",
154
+ {
155
+ type: "button",
156
+ style: chipStyle,
157
+ disabled,
158
+ onClick: () => onSelect(label),
159
+ children: label
160
+ },
161
+ `${i}-${label}`
162
+ )) });
163
+ }
164
+
165
+ // src/components/TypingIndicator.tsx
166
+ import { jsx as jsx3 } from "react/jsx-runtime";
167
+ function TypingIndicator({ theme, botName }) {
168
+ const style = {
169
+ fontSize: 13,
170
+ color: theme.mutedTextColor,
171
+ fontStyle: "italic",
172
+ margin: "6px 2px"
173
+ };
174
+ return /* @__PURE__ */ jsx3("div", { style, "aria-live": "polite", children: (botName || "Assistant") + " is typing\u2026" });
175
+ }
176
+
177
+ // src/AIChatbot.tsx
178
+ import { jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
179
+ function AIChatbot({
180
+ apiKey,
181
+ apiUrl,
182
+ language,
183
+ theme,
184
+ sessionId,
185
+ className,
186
+ style
187
+ }) {
188
+ var _a, _b;
189
+ const { state, controller } = useChatController({
190
+ apiKey,
191
+ apiUrl,
192
+ language,
193
+ theme,
194
+ sessionId
195
+ });
196
+ const [draft, setDraft] = useState("");
197
+ const scrollRef = useRef2(null);
198
+ const headingId = useId();
199
+ const t = state.theme;
200
+ const botName = ((_a = state.config) == null ? void 0 : _a.bot_name) || "Assistant";
201
+ const iconUrl = (_b = state.config) == null ? void 0 : _b.bot_icon_url;
202
+ useEffect2(() => {
203
+ const node = scrollRef.current;
204
+ if (node) node.scrollTop = node.scrollHeight;
205
+ }, [state.messages, state.isTyping]);
206
+ const handleSubmit = useCallback(
207
+ (e) => {
208
+ e.preventDefault();
209
+ const value = draft;
210
+ setDraft("");
211
+ void controller.send(value);
212
+ },
213
+ [controller, draft]
214
+ );
215
+ const handleSuggestion = useCallback(
216
+ (text) => {
217
+ void controller.sendSuggestion(text);
218
+ },
219
+ [controller]
220
+ );
221
+ const panelStyle = {
222
+ display: "flex",
223
+ flexDirection: "column",
224
+ width: "100%",
225
+ height: "100%",
226
+ minHeight: 0,
227
+ boxSizing: "border-box",
228
+ background: t.backgroundColor,
229
+ color: t.botBubbleTextColor,
230
+ fontFamily: t.fontFamily,
231
+ fontSize: 14,
232
+ overflow: "hidden",
233
+ ...style
234
+ };
235
+ const headerStyle = {
236
+ background: t.primaryColor,
237
+ color: t.onPrimaryColor,
238
+ padding: "14px 16px",
239
+ fontWeight: 600,
240
+ display: "flex",
241
+ alignItems: "center",
242
+ gap: 10,
243
+ flex: "0 0 auto"
244
+ };
245
+ const avatarStyle = {
246
+ width: 28,
247
+ height: 28,
248
+ borderRadius: "50%",
249
+ objectFit: "cover",
250
+ flex: "0 0 auto",
251
+ background: "rgba(255,255,255,0.2)"
252
+ };
253
+ const bodyStyle = {
254
+ flex: "1 1 auto",
255
+ minHeight: 0,
256
+ overflowY: "auto",
257
+ padding: 14,
258
+ background: t.surfaceColor
259
+ };
260
+ const footerStyle = {
261
+ display: "flex",
262
+ gap: 6,
263
+ borderTop: `1px solid ${t.botBubbleBorderColor}`,
264
+ padding: 8,
265
+ background: t.backgroundColor,
266
+ flex: "0 0 auto"
267
+ };
268
+ const inputStyle = {
269
+ flex: 1,
270
+ minWidth: 0,
271
+ border: `1px solid ${t.botBubbleBorderColor}`,
272
+ borderRadius: t.inputRadius,
273
+ padding: "9px 14px",
274
+ fontSize: 14,
275
+ fontFamily: "inherit",
276
+ outline: "none",
277
+ color: t.botBubbleTextColor,
278
+ background: t.backgroundColor
279
+ };
280
+ const sendDisabled = state.sending || state.status !== "ready" || draft.trim().length === 0;
281
+ const sendStyle = {
282
+ background: t.primaryColor,
283
+ color: t.onPrimaryColor,
284
+ border: "none",
285
+ borderRadius: t.inputRadius,
286
+ padding: "0 16px",
287
+ cursor: sendDisabled ? "default" : "pointer",
288
+ opacity: sendDisabled ? 0.6 : 1,
289
+ fontSize: 14,
290
+ fontFamily: "inherit",
291
+ flex: "0 0 auto"
292
+ };
293
+ return /* @__PURE__ */ jsxs2(
294
+ "div",
295
+ {
296
+ className,
297
+ style: panelStyle,
298
+ role: "region",
299
+ "aria-label": `${botName} chat`,
300
+ "aria-labelledby": headingId,
301
+ children: [
302
+ /* @__PURE__ */ jsxs2("header", { style: headerStyle, children: [
303
+ iconUrl ? /* @__PURE__ */ jsx4("img", { src: iconUrl, alt: "", style: avatarStyle }) : null,
304
+ /* @__PURE__ */ jsx4("span", { id: headingId, children: botName })
305
+ ] }),
306
+ /* @__PURE__ */ jsxs2("div", { style: bodyStyle, ref: scrollRef, children: [
307
+ state.status === "loading" && state.messages.length === 0 ? /* @__PURE__ */ jsx4("div", { style: { color: t.mutedTextColor, fontSize: 13, padding: 4 }, children: "Loading\u2026" }) : null,
308
+ state.messages.map((message) => /* @__PURE__ */ jsx4(
309
+ MessageBubble,
310
+ {
311
+ message,
312
+ theme: t,
313
+ onFeedback: controller.submitFeedback.bind(controller)
314
+ },
315
+ message.id
316
+ )),
317
+ state.isTyping ? /* @__PURE__ */ jsx4(TypingIndicator, { theme: t, botName }) : null,
318
+ state.status === "error" ? /* @__PURE__ */ jsx4(
319
+ "div",
320
+ {
321
+ role: "alert",
322
+ style: {
323
+ background: t.botBubbleColor,
324
+ color: t.botBubbleTextColor,
325
+ border: `1px solid ${t.botBubbleBorderColor}`,
326
+ borderRadius: t.bubbleRadius,
327
+ padding: "9px 13px",
328
+ fontSize: 14,
329
+ margin: "8px 0"
330
+ },
331
+ children: state.error || "Sorry, something went wrong loading the chat."
332
+ }
333
+ ) : null,
334
+ /* @__PURE__ */ jsx4(
335
+ SuggestionChips,
336
+ {
337
+ suggestions: state.suggestions,
338
+ theme: t,
339
+ disabled: state.sending,
340
+ onSelect: handleSuggestion
341
+ }
342
+ )
343
+ ] }),
344
+ /* @__PURE__ */ jsxs2("form", { style: footerStyle, onSubmit: handleSubmit, children: [
345
+ /* @__PURE__ */ jsx4(
346
+ "input",
347
+ {
348
+ style: inputStyle,
349
+ type: "text",
350
+ value: draft,
351
+ maxLength: 4e3,
352
+ placeholder: "Type your message\u2026",
353
+ "aria-label": "Message",
354
+ onChange: (e) => setDraft(e.target.value),
355
+ disabled: state.status === "error"
356
+ }
357
+ ),
358
+ /* @__PURE__ */ jsx4("button", { type: "submit", style: sendStyle, disabled: sendDisabled, children: "Send" })
359
+ ] })
360
+ ]
361
+ }
362
+ );
363
+ }
364
+ var AIChatbot_default = AIChatbot;
365
+
366
+ // src/index.ts
367
+ import {
368
+ AIChatbotClient,
369
+ ChatController,
370
+ createChatController as createChatController2,
371
+ resolveTheme,
372
+ DEFAULT_THEME
373
+ } from "@aichatbot-saas/core";
374
+ export {
375
+ AIChatbot,
376
+ AIChatbotClient,
377
+ ChatController,
378
+ DEFAULT_THEME,
379
+ MessageBubble,
380
+ SuggestionChips,
381
+ TypingIndicator,
382
+ createChatController2 as createChatController,
383
+ AIChatbot_default as default,
384
+ resolveTheme,
385
+ useChatController
386
+ };
package/package.json ADDED
@@ -0,0 +1,66 @@
1
+ {
2
+ "name": "@aichatbot-saas/react",
3
+ "version": "0.1.0",
4
+ "description": "Native in-page React (web) chat UI component for the AIChatbot SaaS — a thin, themeable binding over @aichatbot-saas/core.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "./dist/index.cjs",
8
+ "module": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "import": "./dist/index.js",
14
+ "require": "./dist/index.cjs"
15
+ }
16
+ },
17
+ "sideEffects": false,
18
+ "files": [
19
+ "dist",
20
+ "src",
21
+ "README.md"
22
+ ],
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "git+https://github.com/piyushshri01/chat-bot-saas.git",
26
+ "directory": "sdks/react"
27
+ },
28
+ "homepage": "https://github.com/piyushshri01/chat-bot-saas/tree/main/sdks/react#readme",
29
+ "bugs": {
30
+ "url": "https://github.com/piyushshri01/chat-bot-saas/issues"
31
+ },
32
+ "keywords": [
33
+ "react",
34
+ "react-dom",
35
+ "chatbot",
36
+ "ai",
37
+ "chat",
38
+ "sdk",
39
+ "aichatbot",
40
+ "support",
41
+ "widget",
42
+ "embeddable"
43
+ ],
44
+ "scripts": {
45
+ "build": "tsup src/index.ts --format esm,cjs --dts --clean --external react,react-dom,@aichatbot-saas/core",
46
+ "typecheck": "tsc --noEmit"
47
+ },
48
+ "dependencies": {
49
+ "@aichatbot-saas/core": "^0.1.0"
50
+ },
51
+ "peerDependencies": {
52
+ "react": ">=17",
53
+ "react-dom": ">=17"
54
+ },
55
+ "devDependencies": {
56
+ "@types/react": "^18.2.0",
57
+ "@types/react-dom": "^18.2.0",
58
+ "react": "^18.2.0",
59
+ "react-dom": "^18.2.0",
60
+ "tsup": "^8.0.0",
61
+ "typescript": "^5.4.0"
62
+ },
63
+ "publishConfig": {
64
+ "access": "public"
65
+ }
66
+ }