@chatbotkit/react 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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,12 @@
1
+ # @chatbotkit/react
2
+
3
+ ## 0.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Now with a functional React SDK.
8
+
9
+ ### Patch Changes
10
+
11
+ - Updated dependencies
12
+ - @chatbotkit/sdk@0.8.0
package/README.md ADDED
@@ -0,0 +1,136 @@
1
+ [![Follow on Twitter](https://img.shields.io/twitter/follow/chatbotkit.svg?logo=twitter)](https://twitter.com/chatbotkit)
2
+ [![NPM](https://img.shields.io/npm/v/@chatbotkit/react.svg)](https://www.npmjs.com/package/@chatbotkit/react)
3
+ [![ChatBotKit](https://img.shields.io/badge/credits-ChatBotKit-blue.svg)](https://chatbotkit.com)
4
+
5
+ # ChatBotKit React SDK
6
+
7
+ Welcome to the ChatBotKit React SDK. This SDK is a React solution for building conversational AI chatbots with ease. With [ChatBotKit](https://chatbotkit.com), you can quickly develop and deploy chatbots that can interact with users in natural language.
8
+
9
+ ## React SDK Features
10
+
11
+ - **Easy setup** - The ChatBotKit React SDK is easy to install and set up. You can have your first chatbot up and running in minutes.
12
+ - **No styles** - ChatBotKit React SDK does not enforce any styles. You can easily style your applications just the way you want them.
13
+ - **Modern** - A modern SDK with built-in support for CommonJS, ECMAScript Modules, async/await, streams and much more.
14
+ - **Customizable** - You can easily customize the chatbot's behavior and responses to fit your specific use case.
15
+
16
+ ## ChatBotKit Features
17
+
18
+ - ๐Ÿ—จ **Chat History**: Easily review and reference previous conversations with your bots, ensuring that it has all the information it needs.
19
+ - ๐Ÿ’พ **Custom Datasets**: Manage and organize the data that your chat bots use to respond to user input with bespoke datasets.
20
+ - ๐ŸŽฌ **Media File Importing**: Import MP3, MP4, MPEG, WAV and many other media files directly into your your chatbot datasets
21
+ - ๐ŸŒ **Widget Integration**: Embed ChatBotKit chatbots directly on any website using advanced customization options and theming.
22
+ - ๐Ÿ’ฌ **Slack Bot Integration**: Create and deploy wide-range of Slack bot integrations with just a few click.
23
+ - ๐ŸŽฎ **Discord Bot Integration**: Create and deploy wide-range of Discord chat bot with just a few click.
24
+ - ๐Ÿ“ฑ **WhatsApp Bot Integration**: Connect with your audience instantly on the worldโ€™s most popular AI bot platform.
25
+ - ๐Ÿ—บ **Sitemap Integration**: Automatically ingest website content into a searchable knowledge base for your chatbot to reference.
26
+ - ๐Ÿค– **GPT-3.5 Support**: State-of-the-art language models to power your conversations.
27
+ - ๐Ÿš€ **GPT-4 Support**: The latest and best language model now can power all chatbots.
28
+ - ๐ŸŽฅ **Streaming**: You can turn on and off streaming capabilities for your chatbots.
29
+ - ๐ŸŽจ **Widget Themes**: Customize the appearance of your chatbot widget with different themes to match your website branding or personal preferences.
30
+ - ๐Ÿ’ก **ChatGPT Extended**: Create your own own ChatGPT bot on variety of skills and domain-specific knowledge.
31
+ - ๐Ÿ”„ **Multiple AI Models**: Leverage diverse models from various AI providers to enhance performance and accuracy.
32
+ - ๐Ÿ”’ **Data Security**: Ensuring the security of user data, with robust measures in place to protect against unauthorized access.
33
+ - ๐Ÿ•ต **Focus on Privacy**: Get strong privacy controls out of the box. Privide confindence that your customers' data is being handled responsibly.
34
+ - ๐Ÿšซ **Content Moderation**: All messages are automatically scanned for abusive content and automatically flagged by the system.
35
+ - ๐Ÿ” **Semantic Search**: Your chat bot can provide more relevant and accurate responses.
36
+ - โš™๏ธ **AI Playgrounds**: Interactive environments that provide a safe and controlled space to experiment, explore, and learn.
37
+ - โš’๏ธ **No-Code Platform**: Easily build, customize and deploy chatbots without needing to write any code.
38
+ - ๐Ÿ’ต **Simple Pricing**: Our pricing is straightforward and easy to understand, with no hidden fees or surprises.
39
+ - ๐Ÿ“ฑ **App Platform**: Our platform provides capabilities for building and deploying chatbots for a wide range of applications.
40
+ - ๐Ÿ”ง **Extreme Customization**: Customize your chatbots' responses and behavior based on variety of preferences.
41
+ - ๐ŸŒŸ **Expanding Feature Set**: We are constantly adding new features to so you can always stay up-to-date with the latest AI capabilities.
42
+
43
+ ## Getting Started
44
+
45
+ To get started with ChatBotKit, follow these simple steps:
46
+
47
+ 1. Install the SDK using npm: `npm install @chatbotkit/react`.
48
+ 2. Use the SDK to setup or interact with your chatbot.
49
+
50
+ Here is a simple example for the next.js framework. Within the body of our component we invoke the `useConversationManager` React Hook which setups a simple utility to manage the conversation flow.
51
+
52
+ ```js
53
+ import { AutoTextarea, useConversationManager } from '@chatbotkit/react'
54
+
55
+ export default function Home() {
56
+ const {
57
+ conversationId,
58
+ setConversationId,
59
+
60
+ token,
61
+ setToken,
62
+
63
+ text,
64
+ setText,
65
+
66
+ messages,
67
+
68
+ thinking,
69
+
70
+ interact,
71
+ } = useConversationManager({ stream: true })
72
+
73
+ async function createSession() {
74
+ const response = await fetch(`/api/session/create`)
75
+
76
+ if (!response.ok) {
77
+ throw new Error(`Unexpected error`)
78
+ }
79
+
80
+ const { conversationId, token } = await response.json()
81
+
82
+ setConversationId(conversationId)
83
+ setToken(token)
84
+ }
85
+
86
+ function handleOnKeyDown(event) {
87
+ if (event.keyCode === 13) {
88
+ event.preventDefault()
89
+
90
+ interact()
91
+ }
92
+ }
93
+
94
+ return (
95
+ <div>
96
+ {conversationId && token ? (
97
+ <>
98
+ <div>
99
+ {messages.map(({ id, type, text }) => {
100
+ switch (type) {
101
+ case 'user':
102
+ return <div key={id}>user: {text}</div>
103
+
104
+ case 'bot':
105
+ return <div key={id}>bot: {text}</div>
106
+ }
107
+ })}
108
+ {thinking ? <div key="thinking">bot: thinking...</div> : null}
109
+ </div>
110
+ <AutoTextarea
111
+ value={text}
112
+ onChange={(e) => setText(event.target.value)}
113
+ onKeyDown={handleOnKeyDown}
114
+ />
115
+ </>
116
+ ) : (
117
+ <button onClick={() => createSession()}>Start Chat</button>
118
+ )}
119
+ </div>
120
+ )
121
+ }
122
+ ```
123
+
124
+ ## Examples
125
+
126
+ You can find a wide-range of examples at [here](https://github.com/chatbotkit/node-sdk/tree/main/examples).
127
+
128
+ ## Documentation
129
+
130
+ For detailed documentation on available types, please refer to the [type documentation](https://github.com/chatbotkit/node-sdk/blob/main/docs/react/modules.md).
131
+
132
+ Checkout the [ChatBotKit Documentation](https://chatbotkit.com/docs/react-sdk) for more information about the platform.
133
+
134
+ ## Contributing
135
+
136
+ If you find a bug or would like to contribute to the ChatBotKit SDK, please open an issue or submit a pull request on the [official GitHub repository](https://github.com/chatbotkit/node-sdk).
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const react_1 = __importDefault(require("react"));
7
+ /**
8
+ * @typedef {(event: React.ChangeEvent<HTMLTextAreaElement>) => any} onInputFn
9
+ *
10
+ * @param {{
11
+ * onInput?: onInputFn?,
12
+ * [name: string]: any
13
+ * }} [props]
14
+ */
15
+ // @ts-ignore
16
+ function AutoTextarea({ onInput, ...props } = {}) {
17
+ /**
18
+ * @param {React.ChangeEvent<HTMLTextAreaElement>} event
19
+ */
20
+ function handleOnInput(event) {
21
+ const adjustment = `calc(${[event.target.style.paddingTop, event.target.style.paddingBottom]
22
+ .filter((f) => f)
23
+ .join(' + ') || '0px'})`;
24
+ event.target.style.height = 'auto';
25
+ event.target.style.height = `calc(${event.target.scrollHeight}px - ${adjustment})`;
26
+ if (onInput) {
27
+ onInput(event);
28
+ }
29
+ }
30
+ return react_1.default.createElement("textarea", { ...props, rows: 1, onInput: handleOnInput });
31
+ }
32
+ exports.default = AutoTextarea;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * @typedef {(event: React.ChangeEvent<HTMLTextAreaElement>) => any} onInputFn
3
+ *
4
+ * @param {{
5
+ * onInput?: onInputFn?,
6
+ * [name: string]: any
7
+ * }} [props]
8
+ */
9
+ export default function AutoTextarea({ onInput, ...props }?: {
10
+ [name: string]: any;
11
+ onInput?: onInputFn | null | undefined;
12
+ } | undefined): React.JSX.Element;
13
+ export type onInputFn = (event: React.ChangeEvent<HTMLTextAreaElement>) => any;
14
+ import React from 'react';
@@ -0,0 +1,306 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const react_1 = require("react");
4
+ const index_js_1 = require("@chatbotkit/sdk/conversation/index.cjs");
5
+ const string_js_1 = require("../utils/string.cjs");
6
+ /**
7
+ * @typedef {{
8
+ * id: string,
9
+ * type: string,
10
+ * text: string,
11
+ * createdAt: number,
12
+ * meta?: Record<string,any>
13
+ * }} Message
14
+ *
15
+ * @typedef {(error: any) => any} onErrorFn
16
+ * @typedef {(conversationId: string, messages: Message[], data: Record<string,any>) => any} onSendFn
17
+ * @typedef {(conversationId: string, messages: Message[], data: Record<string,any>) => any} onReceiveFn
18
+ *
19
+ * @param {{
20
+ * conversationId?: string,
21
+ * token?: string,
22
+ * messages?: Message[],
23
+ * parse?: boolean,
24
+ * stream?: boolean,
25
+ * verbose?: boolean,
26
+ * onError?: onErrorFn?,
27
+ * onSend?: onSendFn?,
28
+ * onReceive?: onReceiveFn?,
29
+ * }}[options]
30
+ */
31
+ // @ts-ignore
32
+ function useConversationManager({ conversationId: _conversationId = '', token: _token = '', messages: _messages = [], parse = false, stream = false, verbose = false, onError = null, onSend = null, onReceive = null, } = {}) {
33
+ // general states
34
+ const [conversationId, setConversationId] = (0, react_1.useState)(_conversationId);
35
+ const [token, setToken] = (0, react_1.useState)(_token);
36
+ const [messages, setMessages] = (0, react_1.useState)(_messages);
37
+ const [text, setText] = (0, react_1.useState)('');
38
+ const [entities, setEntities] = (0, react_1.useState)({});
39
+ const [outgoingMessage, setOutgoingMessage] = (0, react_1.useState)('');
40
+ // loading & thinking state
41
+ const [loading, setLoading] = (0, react_1.useState)(false);
42
+ const [thinking, setThinking] = (0, react_1.useState)(false);
43
+ (0, react_1.useEffect)(() => {
44
+ // NOTE: if we are loading we must be thinking
45
+ if (loading) {
46
+ setThinking(true);
47
+ }
48
+ }, [loading]);
49
+ // entity methods
50
+ /**
51
+ * @param {string} text
52
+ * @param {Record<string,string>} en
53
+ */
54
+ function redactEnitities(text, en = entities) {
55
+ const steps = (0, string_js_1.replaceWithCoordinates)(text, Object.entries(en));
56
+ const output = steps.pop();
57
+ steps.forEach((step) => {
58
+ // @ts-ignore
59
+ delete step.input;
60
+ // @ts-ignore
61
+ delete step.output;
62
+ });
63
+ return [output, ...steps];
64
+ }
65
+ /**
66
+ * @param {string} text
67
+ * @param {Record<string,string>} en
68
+ */
69
+ function unredactEnitities(text, en = entities) {
70
+ Object.entries(en).forEach(([real, redacted]) => {
71
+ text = text.split(redacted).join(real);
72
+ });
73
+ return text;
74
+ }
75
+ // state utility methods
76
+ /**
77
+ * @returns {Promise<void>}
78
+ */
79
+ async function flushUserMessage() {
80
+ if (!text) {
81
+ return;
82
+ }
83
+ setText('');
84
+ setOutgoingMessage(text);
85
+ let newMessages = messages.slice(0);
86
+ newMessages = newMessages.concat({
87
+ id: (0, string_js_1.getRandomId)('message-'),
88
+ type: 'user',
89
+ text,
90
+ createdAt: Date.now(),
91
+ });
92
+ setMessages(newMessages);
93
+ }
94
+ // utility states
95
+ const [nextStep, setNextStep] = (0, react_1.useState)(null);
96
+ (0, react_1.useEffect)(() => {
97
+ if (!nextStep) {
98
+ return;
99
+ }
100
+ setNextStep(null);
101
+ // @ts-ignore
102
+ switch (nextStep.fn) {
103
+ case 'continueConversation':
104
+ // @ts-ignore
105
+ continueConversation(nextStep.options);
106
+ break;
107
+ default:
108
+ // @ts-ignore
109
+ throw new Error(`Unrecognised fn: ${nextStep.fn}`);
110
+ }
111
+ }, [nextStep]);
112
+ // base methods
113
+ /**
114
+ * @param {{
115
+ * token?: string,
116
+ * }} [options]
117
+ * @returns {Promise<void>}
118
+ */
119
+ async function continueConversation(options) {
120
+ let newMessages = messages.slice(0);
121
+ let thisText;
122
+ if (text) {
123
+ thisText = text;
124
+ const message = {
125
+ id: (0, string_js_1.getRandomId)('message-'),
126
+ text: text,
127
+ type: 'user',
128
+ createdAt: Date.now(),
129
+ };
130
+ newMessages = newMessages.concat([message]);
131
+ setText('');
132
+ }
133
+ else if (outgoingMessage) {
134
+ thisText = outgoingMessage;
135
+ setOutgoingMessage('');
136
+ }
137
+ else {
138
+ return;
139
+ }
140
+ setMessages(newMessages);
141
+ setLoading(true);
142
+ const [redactedText, ...redactedEntities] = redactEnitities(thisText, entities);
143
+ const secret = options?.token || token;
144
+ const client = new index_js_1.ConversationClient({ secret });
145
+ const completion = client.complete(conversationId, {
146
+ // @ts-ignore
147
+ text: redactedText,
148
+ // @ts-ignore
149
+ entities: redactedEntities,
150
+ parse: parse,
151
+ });
152
+ let iter;
153
+ if (stream) {
154
+ iter = completion.stream();
155
+ }
156
+ else {
157
+ iter = (async function* () {
158
+ yield { type: 'receiveResult', data: await completion };
159
+ })();
160
+ }
161
+ try {
162
+ if (onSend) {
163
+ await onSend(conversationId, newMessages, {});
164
+ }
165
+ const tempId = (0, string_js_1.getRandomId)('tmp-');
166
+ let tempText = '';
167
+ for await (let { type, data } of iter) {
168
+ switch (type) {
169
+ case 'intentDetectionEnd': {
170
+ if (!verbose) {
171
+ break;
172
+ }
173
+ const { action } = data;
174
+ if (!action) {
175
+ break;
176
+ }
177
+ const { name, input } = action;
178
+ switch (name) {
179
+ case 'search': {
180
+ const id = (0, string_js_1.getRandomId)('tmp-');
181
+ newMessages = newMessages.concat([
182
+ {
183
+ id: id,
184
+ text: '',
185
+ type: 'context',
186
+ createdAt: Date.now(),
187
+ meta: {
188
+ search: input,
189
+ },
190
+ },
191
+ ]);
192
+ setMessages(newMessages);
193
+ break;
194
+ }
195
+ }
196
+ break;
197
+ }
198
+ case 'token': {
199
+ if (!stream) {
200
+ break;
201
+ }
202
+ const { token } = data;
203
+ tempText += token;
204
+ setMessages([
205
+ ...newMessages,
206
+ {
207
+ id: tempId,
208
+ text: tempText,
209
+ type: 'bot',
210
+ createdAt: Date.now(),
211
+ meta: {},
212
+ },
213
+ ]);
214
+ if (text.length) {
215
+ setThinking(false);
216
+ }
217
+ break;
218
+ }
219
+ case 'sendResult': {
220
+ const { entities: newEntities } = data;
221
+ setEntities({ ...entities, ...newEntities });
222
+ break;
223
+ }
224
+ case 'receiveResult': {
225
+ const { id, text, parse } = data;
226
+ setMessages([
227
+ ...newMessages,
228
+ {
229
+ id: id,
230
+ text: unredactEnitities((parse ? parse.stripped : text).trim(), entities),
231
+ type: 'bot',
232
+ createdAt: Date.now(),
233
+ },
234
+ ]);
235
+ setThinking(false);
236
+ if (onReceive) {
237
+ await onReceive(conversationId, newMessages, data);
238
+ }
239
+ break;
240
+ }
241
+ }
242
+ }
243
+ }
244
+ catch (e) {
245
+ if (onError) {
246
+ onError(e);
247
+ }
248
+ }
249
+ setLoading(false);
250
+ }
251
+ // helper methods
252
+ /**
253
+ * @param {{
254
+ * token?: string,
255
+ * conversationId?: string
256
+ * messages?: Message[]
257
+ * }} [options]
258
+ * @returns {void}
259
+ */
260
+ function interact(options) {
261
+ if (options?.token) {
262
+ setToken(options.token);
263
+ }
264
+ if (options?.conversationId) {
265
+ setConversationId(options.conversationId);
266
+ }
267
+ if (options?.messages) {
268
+ setMessages(options.messages);
269
+ }
270
+ if (options?.conversationId || conversationId) {
271
+ setNextStep({
272
+ // @ts-ignore
273
+ fn: 'continueConversation',
274
+ options,
275
+ });
276
+ }
277
+ else {
278
+ throw new Error(`No conversation id specified`);
279
+ }
280
+ }
281
+ /**
282
+ * @returns {void}
283
+ */
284
+ function reset() {
285
+ setMessages([]);
286
+ setConversationId('');
287
+ }
288
+ // final
289
+ return {
290
+ text,
291
+ setText,
292
+ token,
293
+ setToken,
294
+ conversationId,
295
+ setConversationId,
296
+ messages,
297
+ setMessages,
298
+ flushUserMessage,
299
+ continueConversation,
300
+ interact,
301
+ reset,
302
+ loading,
303
+ thinking,
304
+ };
305
+ }
306
+ exports.default = useConversationManager;
@@ -0,0 +1,67 @@
1
+ /**
2
+ * @typedef {{
3
+ * id: string,
4
+ * type: string,
5
+ * text: string,
6
+ * createdAt: number,
7
+ * meta?: Record<string,any>
8
+ * }} Message
9
+ *
10
+ * @typedef {(error: any) => any} onErrorFn
11
+ * @typedef {(conversationId: string, messages: Message[], data: Record<string,any>) => any} onSendFn
12
+ * @typedef {(conversationId: string, messages: Message[], data: Record<string,any>) => any} onReceiveFn
13
+ *
14
+ * @param {{
15
+ * conversationId?: string,
16
+ * token?: string,
17
+ * messages?: Message[],
18
+ * parse?: boolean,
19
+ * stream?: boolean,
20
+ * verbose?: boolean,
21
+ * onError?: onErrorFn?,
22
+ * onSend?: onSendFn?,
23
+ * onReceive?: onReceiveFn?,
24
+ * }}[options]
25
+ */
26
+ export default function useConversationManager({ conversationId: _conversationId, token: _token, messages: _messages, parse, stream, verbose, onError, onSend, onReceive, }?: {
27
+ conversationId?: string | undefined;
28
+ token?: string | undefined;
29
+ messages?: Message[] | undefined;
30
+ parse?: boolean | undefined;
31
+ stream?: boolean | undefined;
32
+ verbose?: boolean | undefined;
33
+ onError?: onErrorFn | null | undefined;
34
+ onSend?: onSendFn | null | undefined;
35
+ onReceive?: onReceiveFn | null | undefined;
36
+ } | undefined): {
37
+ text: string;
38
+ setText: import("react").Dispatch<import("react").SetStateAction<string>>;
39
+ token: string;
40
+ setToken: import("react").Dispatch<import("react").SetStateAction<string>>;
41
+ conversationId: string;
42
+ setConversationId: import("react").Dispatch<import("react").SetStateAction<string>>;
43
+ messages: Message[];
44
+ setMessages: import("react").Dispatch<import("react").SetStateAction<Message[]>>;
45
+ flushUserMessage: () => Promise<void>;
46
+ continueConversation: (options?: {
47
+ token?: string | undefined;
48
+ } | undefined) => Promise<void>;
49
+ interact: (options?: {
50
+ token?: string | undefined;
51
+ conversationId?: string | undefined;
52
+ messages?: Message[] | undefined;
53
+ } | undefined) => void;
54
+ reset: () => void;
55
+ loading: boolean;
56
+ thinking: boolean;
57
+ };
58
+ export type Message = {
59
+ id: string;
60
+ type: string;
61
+ text: string;
62
+ createdAt: number;
63
+ meta?: Record<string, any>;
64
+ };
65
+ export type onErrorFn = (error: any) => any;
66
+ export type onSendFn = (conversationId: string, messages: Message[], data: Record<string, any>) => any;
67
+ export type onReceiveFn = (conversationId: string, messages: Message[], data: Record<string, any>) => any;
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.useConversationManager = exports.AutoTextarea = void 0;
7
+ var AutoTextarea_js_1 = require("./components/AutoTextarea.cjs");
8
+ Object.defineProperty(exports, "AutoTextarea", { enumerable: true, get: function () { return __importDefault(AutoTextarea_js_1).default; } });
9
+ var useConversationManager_js_1 = require("./hooks/useConversationManager.cjs");
10
+ Object.defineProperty(exports, "useConversationManager", { enumerable: true, get: function () { return __importDefault(useConversationManager_js_1).default; } });
@@ -0,0 +1,2 @@
1
+ export { default as AutoTextarea } from "./components/AutoTextarea.cjs";
2
+ export { default as useConversationManager } from "./hooks/useConversationManager.cjs";
@@ -0,0 +1,56 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.replaceWithCoordinates = exports.replaceBetween = exports.getRandomId = void 0;
4
+ /**
5
+ * @param {string} [prefix]
6
+ * @returns {string}
7
+ */
8
+ function getRandomId(prefix) {
9
+ return `${prefix || ''}${Math.random().toString(32).slice(2)}`;
10
+ }
11
+ exports.getRandomId = getRandomId;
12
+ /**
13
+ * @param {string} input
14
+ * @param {number} begin
15
+ * @param {number} end
16
+ * @param {string} replacement
17
+ * @returns {string}
18
+ */
19
+ function replaceBetween(input, begin, end, replacement) {
20
+ return input.substring(0, begin) + replacement + input.substring(end);
21
+ }
22
+ exports.replaceBetween = replaceBetween;
23
+ /**
24
+ * @param {string} input
25
+ * @param {[string, string][]} replacements
26
+ * @returns {({begin: number, end: number, input: string, output: string}|string)[]}
27
+ */
28
+ function replaceWithCoordinates(input, replacements) {
29
+ const output = [];
30
+ let currentIndex = 0;
31
+ while (currentIndex < input.length) {
32
+ let found = false;
33
+ for (const [search, replacement] of replacements) {
34
+ if (input.startsWith(search, currentIndex)) {
35
+ const origInput = input;
36
+ const resultOutput = replaceBetween(input, currentIndex, currentIndex + search.length, replacement);
37
+ output.push({
38
+ begin: currentIndex,
39
+ end: currentIndex + replacement.length,
40
+ input: origInput,
41
+ output: resultOutput,
42
+ });
43
+ input = resultOutput;
44
+ currentIndex += replacement.length;
45
+ found = true;
46
+ break;
47
+ }
48
+ }
49
+ if (!found) {
50
+ currentIndex += 1;
51
+ }
52
+ }
53
+ output.push(input);
54
+ return output;
55
+ }
56
+ exports.replaceWithCoordinates = replaceWithCoordinates;
@@ -0,0 +1,24 @@
1
+ /**
2
+ * @param {string} [prefix]
3
+ * @returns {string}
4
+ */
5
+ export function getRandomId(prefix?: string | undefined): string;
6
+ /**
7
+ * @param {string} input
8
+ * @param {number} begin
9
+ * @param {number} end
10
+ * @param {string} replacement
11
+ * @returns {string}
12
+ */
13
+ export function replaceBetween(input: string, begin: number, end: number, replacement: string): string;
14
+ /**
15
+ * @param {string} input
16
+ * @param {[string, string][]} replacements
17
+ * @returns {({begin: number, end: number, input: string, output: string}|string)[]}
18
+ */
19
+ export function replaceWithCoordinates(input: string, replacements: [string, string][]): (string | {
20
+ begin: number;
21
+ end: number;
22
+ input: string;
23
+ output: string;
24
+ })[];
@@ -0,0 +1,14 @@
1
+ /**
2
+ * @typedef {(event: React.ChangeEvent<HTMLTextAreaElement>) => any} onInputFn
3
+ *
4
+ * @param {{
5
+ * onInput?: onInputFn?,
6
+ * [name: string]: any
7
+ * }} [props]
8
+ */
9
+ export default function AutoTextarea({ onInput, ...props }?: {
10
+ [name: string]: any;
11
+ onInput?: onInputFn | null | undefined;
12
+ } | undefined): React.JSX.Element;
13
+ export type onInputFn = (event: React.ChangeEvent<HTMLTextAreaElement>) => any;
14
+ import React from 'react';
@@ -0,0 +1,26 @@
1
+ import React from 'react';
2
+ /**
3
+ * @typedef {(event: React.ChangeEvent<HTMLTextAreaElement>) => any} onInputFn
4
+ *
5
+ * @param {{
6
+ * onInput?: onInputFn?,
7
+ * [name: string]: any
8
+ * }} [props]
9
+ */
10
+ // @ts-ignore
11
+ export default function AutoTextarea({ onInput, ...props } = {}) {
12
+ /**
13
+ * @param {React.ChangeEvent<HTMLTextAreaElement>} event
14
+ */
15
+ function handleOnInput(event) {
16
+ const adjustment = `calc(${[event.target.style.paddingTop, event.target.style.paddingBottom]
17
+ .filter((f) => f)
18
+ .join(' + ') || '0px'})`;
19
+ event.target.style.height = 'auto';
20
+ event.target.style.height = `calc(${event.target.scrollHeight}px - ${adjustment})`;
21
+ if (onInput) {
22
+ onInput(event);
23
+ }
24
+ }
25
+ return React.createElement("textarea", { ...props, rows: 1, onInput: handleOnInput });
26
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * @typedef {{
3
+ * id: string,
4
+ * type: string,
5
+ * text: string,
6
+ * createdAt: number,
7
+ * meta?: Record<string,any>
8
+ * }} Message
9
+ *
10
+ * @typedef {(error: any) => any} onErrorFn
11
+ * @typedef {(conversationId: string, messages: Message[], data: Record<string,any>) => any} onSendFn
12
+ * @typedef {(conversationId: string, messages: Message[], data: Record<string,any>) => any} onReceiveFn
13
+ *
14
+ * @param {{
15
+ * conversationId?: string,
16
+ * token?: string,
17
+ * messages?: Message[],
18
+ * parse?: boolean,
19
+ * stream?: boolean,
20
+ * verbose?: boolean,
21
+ * onError?: onErrorFn?,
22
+ * onSend?: onSendFn?,
23
+ * onReceive?: onReceiveFn?,
24
+ * }}[options]
25
+ */
26
+ export default function useConversationManager({ conversationId: _conversationId, token: _token, messages: _messages, parse, stream, verbose, onError, onSend, onReceive, }?: {
27
+ conversationId?: string | undefined;
28
+ token?: string | undefined;
29
+ messages?: Message[] | undefined;
30
+ parse?: boolean | undefined;
31
+ stream?: boolean | undefined;
32
+ verbose?: boolean | undefined;
33
+ onError?: onErrorFn | null | undefined;
34
+ onSend?: onSendFn | null | undefined;
35
+ onReceive?: onReceiveFn | null | undefined;
36
+ } | undefined): {
37
+ text: string;
38
+ setText: import("react").Dispatch<import("react").SetStateAction<string>>;
39
+ token: string;
40
+ setToken: import("react").Dispatch<import("react").SetStateAction<string>>;
41
+ conversationId: string;
42
+ setConversationId: import("react").Dispatch<import("react").SetStateAction<string>>;
43
+ messages: Message[];
44
+ setMessages: import("react").Dispatch<import("react").SetStateAction<Message[]>>;
45
+ flushUserMessage: () => Promise<void>;
46
+ continueConversation: (options?: {
47
+ token?: string | undefined;
48
+ } | undefined) => Promise<void>;
49
+ interact: (options?: {
50
+ token?: string | undefined;
51
+ conversationId?: string | undefined;
52
+ messages?: Message[] | undefined;
53
+ } | undefined) => void;
54
+ reset: () => void;
55
+ loading: boolean;
56
+ thinking: boolean;
57
+ };
58
+ export type Message = {
59
+ id: string;
60
+ type: string;
61
+ text: string;
62
+ createdAt: number;
63
+ meta?: Record<string, any>;
64
+ };
65
+ export type onErrorFn = (error: any) => any;
66
+ export type onSendFn = (conversationId: string, messages: Message[], data: Record<string, any>) => any;
67
+ export type onReceiveFn = (conversationId: string, messages: Message[], data: Record<string, any>) => any;
@@ -0,0 +1,303 @@
1
+ import { useState, useEffect } from 'react';
2
+ import { ConversationClient } from '@chatbotkit/sdk/conversation/index.js';
3
+ import { getRandomId, replaceWithCoordinates } from '../utils/string.js';
4
+ /**
5
+ * @typedef {{
6
+ * id: string,
7
+ * type: string,
8
+ * text: string,
9
+ * createdAt: number,
10
+ * meta?: Record<string,any>
11
+ * }} Message
12
+ *
13
+ * @typedef {(error: any) => any} onErrorFn
14
+ * @typedef {(conversationId: string, messages: Message[], data: Record<string,any>) => any} onSendFn
15
+ * @typedef {(conversationId: string, messages: Message[], data: Record<string,any>) => any} onReceiveFn
16
+ *
17
+ * @param {{
18
+ * conversationId?: string,
19
+ * token?: string,
20
+ * messages?: Message[],
21
+ * parse?: boolean,
22
+ * stream?: boolean,
23
+ * verbose?: boolean,
24
+ * onError?: onErrorFn?,
25
+ * onSend?: onSendFn?,
26
+ * onReceive?: onReceiveFn?,
27
+ * }}[options]
28
+ */
29
+ // @ts-ignore
30
+ export default function useConversationManager({ conversationId: _conversationId = '', token: _token = '', messages: _messages = [], parse = false, stream = false, verbose = false, onError = null, onSend = null, onReceive = null, } = {}) {
31
+ // general states
32
+ const [conversationId, setConversationId] = useState(_conversationId);
33
+ const [token, setToken] = useState(_token);
34
+ const [messages, setMessages] = useState(_messages);
35
+ const [text, setText] = useState('');
36
+ const [entities, setEntities] = useState({});
37
+ const [outgoingMessage, setOutgoingMessage] = useState('');
38
+ // loading & thinking state
39
+ const [loading, setLoading] = useState(false);
40
+ const [thinking, setThinking] = useState(false);
41
+ useEffect(() => {
42
+ // NOTE: if we are loading we must be thinking
43
+ if (loading) {
44
+ setThinking(true);
45
+ }
46
+ }, [loading]);
47
+ // entity methods
48
+ /**
49
+ * @param {string} text
50
+ * @param {Record<string,string>} en
51
+ */
52
+ function redactEnitities(text, en = entities) {
53
+ const steps = replaceWithCoordinates(text, Object.entries(en));
54
+ const output = steps.pop();
55
+ steps.forEach((step) => {
56
+ // @ts-ignore
57
+ delete step.input;
58
+ // @ts-ignore
59
+ delete step.output;
60
+ });
61
+ return [output, ...steps];
62
+ }
63
+ /**
64
+ * @param {string} text
65
+ * @param {Record<string,string>} en
66
+ */
67
+ function unredactEnitities(text, en = entities) {
68
+ Object.entries(en).forEach(([real, redacted]) => {
69
+ text = text.split(redacted).join(real);
70
+ });
71
+ return text;
72
+ }
73
+ // state utility methods
74
+ /**
75
+ * @returns {Promise<void>}
76
+ */
77
+ async function flushUserMessage() {
78
+ if (!text) {
79
+ return;
80
+ }
81
+ setText('');
82
+ setOutgoingMessage(text);
83
+ let newMessages = messages.slice(0);
84
+ newMessages = newMessages.concat({
85
+ id: getRandomId('message-'),
86
+ type: 'user',
87
+ text,
88
+ createdAt: Date.now(),
89
+ });
90
+ setMessages(newMessages);
91
+ }
92
+ // utility states
93
+ const [nextStep, setNextStep] = useState(null);
94
+ useEffect(() => {
95
+ if (!nextStep) {
96
+ return;
97
+ }
98
+ setNextStep(null);
99
+ // @ts-ignore
100
+ switch (nextStep.fn) {
101
+ case 'continueConversation':
102
+ // @ts-ignore
103
+ continueConversation(nextStep.options);
104
+ break;
105
+ default:
106
+ // @ts-ignore
107
+ throw new Error(`Unrecognised fn: ${nextStep.fn}`);
108
+ }
109
+ }, [nextStep]);
110
+ // base methods
111
+ /**
112
+ * @param {{
113
+ * token?: string,
114
+ * }} [options]
115
+ * @returns {Promise<void>}
116
+ */
117
+ async function continueConversation(options) {
118
+ let newMessages = messages.slice(0);
119
+ let thisText;
120
+ if (text) {
121
+ thisText = text;
122
+ const message = {
123
+ id: getRandomId('message-'),
124
+ text: text,
125
+ type: 'user',
126
+ createdAt: Date.now(),
127
+ };
128
+ newMessages = newMessages.concat([message]);
129
+ setText('');
130
+ }
131
+ else if (outgoingMessage) {
132
+ thisText = outgoingMessage;
133
+ setOutgoingMessage('');
134
+ }
135
+ else {
136
+ return;
137
+ }
138
+ setMessages(newMessages);
139
+ setLoading(true);
140
+ const [redactedText, ...redactedEntities] = redactEnitities(thisText, entities);
141
+ const secret = options?.token || token;
142
+ const client = new ConversationClient({ secret });
143
+ const completion = client.complete(conversationId, {
144
+ // @ts-ignore
145
+ text: redactedText,
146
+ // @ts-ignore
147
+ entities: redactedEntities,
148
+ parse: parse,
149
+ });
150
+ let iter;
151
+ if (stream) {
152
+ iter = completion.stream();
153
+ }
154
+ else {
155
+ iter = (async function* () {
156
+ yield { type: 'receiveResult', data: await completion };
157
+ })();
158
+ }
159
+ try {
160
+ if (onSend) {
161
+ await onSend(conversationId, newMessages, {});
162
+ }
163
+ const tempId = getRandomId('tmp-');
164
+ let tempText = '';
165
+ for await (let { type, data } of iter) {
166
+ switch (type) {
167
+ case 'intentDetectionEnd': {
168
+ if (!verbose) {
169
+ break;
170
+ }
171
+ const { action } = data;
172
+ if (!action) {
173
+ break;
174
+ }
175
+ const { name, input } = action;
176
+ switch (name) {
177
+ case 'search': {
178
+ const id = getRandomId('tmp-');
179
+ newMessages = newMessages.concat([
180
+ {
181
+ id: id,
182
+ text: '',
183
+ type: 'context',
184
+ createdAt: Date.now(),
185
+ meta: {
186
+ search: input,
187
+ },
188
+ },
189
+ ]);
190
+ setMessages(newMessages);
191
+ break;
192
+ }
193
+ }
194
+ break;
195
+ }
196
+ case 'token': {
197
+ if (!stream) {
198
+ break;
199
+ }
200
+ const { token } = data;
201
+ tempText += token;
202
+ setMessages([
203
+ ...newMessages,
204
+ {
205
+ id: tempId,
206
+ text: tempText,
207
+ type: 'bot',
208
+ createdAt: Date.now(),
209
+ meta: {},
210
+ },
211
+ ]);
212
+ if (text.length) {
213
+ setThinking(false);
214
+ }
215
+ break;
216
+ }
217
+ case 'sendResult': {
218
+ const { entities: newEntities } = data;
219
+ setEntities({ ...entities, ...newEntities });
220
+ break;
221
+ }
222
+ case 'receiveResult': {
223
+ const { id, text, parse } = data;
224
+ setMessages([
225
+ ...newMessages,
226
+ {
227
+ id: id,
228
+ text: unredactEnitities((parse ? parse.stripped : text).trim(), entities),
229
+ type: 'bot',
230
+ createdAt: Date.now(),
231
+ },
232
+ ]);
233
+ setThinking(false);
234
+ if (onReceive) {
235
+ await onReceive(conversationId, newMessages, data);
236
+ }
237
+ break;
238
+ }
239
+ }
240
+ }
241
+ }
242
+ catch (e) {
243
+ if (onError) {
244
+ onError(e);
245
+ }
246
+ }
247
+ setLoading(false);
248
+ }
249
+ // helper methods
250
+ /**
251
+ * @param {{
252
+ * token?: string,
253
+ * conversationId?: string
254
+ * messages?: Message[]
255
+ * }} [options]
256
+ * @returns {void}
257
+ */
258
+ function interact(options) {
259
+ if (options?.token) {
260
+ setToken(options.token);
261
+ }
262
+ if (options?.conversationId) {
263
+ setConversationId(options.conversationId);
264
+ }
265
+ if (options?.messages) {
266
+ setMessages(options.messages);
267
+ }
268
+ if (options?.conversationId || conversationId) {
269
+ setNextStep({
270
+ // @ts-ignore
271
+ fn: 'continueConversation',
272
+ options,
273
+ });
274
+ }
275
+ else {
276
+ throw new Error(`No conversation id specified`);
277
+ }
278
+ }
279
+ /**
280
+ * @returns {void}
281
+ */
282
+ function reset() {
283
+ setMessages([]);
284
+ setConversationId('');
285
+ }
286
+ // final
287
+ return {
288
+ text,
289
+ setText,
290
+ token,
291
+ setToken,
292
+ conversationId,
293
+ setConversationId,
294
+ messages,
295
+ setMessages,
296
+ flushUserMessage,
297
+ continueConversation,
298
+ interact,
299
+ reset,
300
+ loading,
301
+ thinking,
302
+ };
303
+ }
@@ -0,0 +1,2 @@
1
+ export { default as AutoTextarea } from "./components/AutoTextarea.js";
2
+ export { default as useConversationManager } from "./hooks/useConversationManager.js";
@@ -0,0 +1,2 @@
1
+ export { default as AutoTextarea } from './components/AutoTextarea.js';
2
+ export { default as useConversationManager } from './hooks/useConversationManager.js';
@@ -0,0 +1,24 @@
1
+ /**
2
+ * @param {string} [prefix]
3
+ * @returns {string}
4
+ */
5
+ export function getRandomId(prefix?: string | undefined): string;
6
+ /**
7
+ * @param {string} input
8
+ * @param {number} begin
9
+ * @param {number} end
10
+ * @param {string} replacement
11
+ * @returns {string}
12
+ */
13
+ export function replaceBetween(input: string, begin: number, end: number, replacement: string): string;
14
+ /**
15
+ * @param {string} input
16
+ * @param {[string, string][]} replacements
17
+ * @returns {({begin: number, end: number, input: string, output: string}|string)[]}
18
+ */
19
+ export function replaceWithCoordinates(input: string, replacements: [string, string][]): (string | {
20
+ begin: number;
21
+ end: number;
22
+ input: string;
23
+ output: string;
24
+ })[];
@@ -0,0 +1,50 @@
1
+ /**
2
+ * @param {string} [prefix]
3
+ * @returns {string}
4
+ */
5
+ export function getRandomId(prefix) {
6
+ return `${prefix || ''}${Math.random().toString(32).slice(2)}`;
7
+ }
8
+ /**
9
+ * @param {string} input
10
+ * @param {number} begin
11
+ * @param {number} end
12
+ * @param {string} replacement
13
+ * @returns {string}
14
+ */
15
+ export function replaceBetween(input, begin, end, replacement) {
16
+ return input.substring(0, begin) + replacement + input.substring(end);
17
+ }
18
+ /**
19
+ * @param {string} input
20
+ * @param {[string, string][]} replacements
21
+ * @returns {({begin: number, end: number, input: string, output: string}|string)[]}
22
+ */
23
+ export function replaceWithCoordinates(input, replacements) {
24
+ const output = [];
25
+ let currentIndex = 0;
26
+ while (currentIndex < input.length) {
27
+ let found = false;
28
+ for (const [search, replacement] of replacements) {
29
+ if (input.startsWith(search, currentIndex)) {
30
+ const origInput = input;
31
+ const resultOutput = replaceBetween(input, currentIndex, currentIndex + search.length, replacement);
32
+ output.push({
33
+ begin: currentIndex,
34
+ end: currentIndex + replacement.length,
35
+ input: origInput,
36
+ output: resultOutput,
37
+ });
38
+ input = resultOutput;
39
+ currentIndex += replacement.length;
40
+ found = true;
41
+ break;
42
+ }
43
+ }
44
+ if (!found) {
45
+ currentIndex += 1;
46
+ }
47
+ }
48
+ output.push(input);
49
+ return output;
50
+ }
package/package.json ADDED
@@ -0,0 +1,97 @@
1
+ {
2
+ "name": "@chatbotkit/react",
3
+ "version": "0.2.0",
4
+ "description": "The fastest way to build advanced AI chat bots",
5
+ "license": "ISC",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/chatbotkit/node-sdk.git"
9
+ },
10
+ "bugs": {
11
+ "url": "https://github.com/chatbotkit/node-sdk/issues"
12
+ },
13
+ "homepage": "https://github.com/chatbotkit/node-sdk#readme",
14
+ "author": "",
15
+ "keywords": [
16
+ "gpt3",
17
+ "gpt4",
18
+ "chat",
19
+ "chatbot",
20
+ "chatbotkit",
21
+ "react"
22
+ ],
23
+ "type": "module",
24
+ "main": "./dist/esm/index.js",
25
+ "exports": {
26
+ ".": {
27
+ "require": {
28
+ "types": "./dist/cjs/index.d.ts",
29
+ "default": "./dist/cjs/index.cjs"
30
+ },
31
+ "import": {
32
+ "types": "./dist/esm/index.d.ts",
33
+ "default": "./dist/esm/index.js"
34
+ }
35
+ },
36
+ "./components/AutoTextarea.js": {
37
+ "require": {
38
+ "types": "./dist/cjs/components/AutoTextarea.d.ts",
39
+ "default": "./dist/cjs/components/AutoTextarea.cjs"
40
+ },
41
+ "import": {
42
+ "types": "./dist/esm/components/AutoTextarea.d.ts",
43
+ "default": "./dist/esm/components/AutoTextarea.js"
44
+ }
45
+ },
46
+ "./hooks/useConversationManager.js": {
47
+ "require": {
48
+ "types": "./dist/cjs/hooks/useConversationManager.d.ts",
49
+ "default": "./dist/cjs/hooks/useConversationManager.cjs"
50
+ },
51
+ "import": {
52
+ "types": "./dist/esm/hooks/useConversationManager.d.ts",
53
+ "default": "./dist/esm/hooks/useConversationManager.js"
54
+ }
55
+ },
56
+ "./index.js": {
57
+ "require": {
58
+ "types": "./dist/cjs/index.d.ts",
59
+ "default": "./dist/cjs/index.cjs"
60
+ },
61
+ "import": {
62
+ "types": "./dist/esm/index.d.ts",
63
+ "default": "./dist/esm/index.js"
64
+ }
65
+ },
66
+ "./utils/string.js": {
67
+ "require": {
68
+ "types": "./dist/cjs/utils/string.d.ts",
69
+ "default": "./dist/cjs/utils/string.cjs"
70
+ },
71
+ "import": {
72
+ "types": "./dist/esm/utils/string.d.ts",
73
+ "default": "./dist/esm/utils/string.js"
74
+ }
75
+ }
76
+ },
77
+ "scripts": {
78
+ "build": "run-s build:*",
79
+ "build:cjs": "tsc -p tsconfig.cjs.json",
80
+ "build:cjs_rename": "find ./dist/cjs -name '*.js' -exec rename .js .cjs {} +",
81
+ "build:cjs_rewrite_cjs": "find ./dist/cjs -name '*.cjs' -exec sed -i 's/require(\"\\(.*\\)\\.js\")/require(\"\\1.cjs\")/g' {} +",
82
+ "build:cjs_rewrite_ts": "find ./dist/cjs -name '*.ts' -exec sed -i 's/from \"\\(.*\\)\\.js\"/from \"\\1.cjs\"/g' {} +",
83
+ "build:esm": "tsc -p tsconfig.esm.json",
84
+ "build:exports": "node scripts/build-exports.js",
85
+ "test": "true"
86
+ },
87
+ "types": "./dist/esm/index.d.ts",
88
+ "dependencies": {
89
+ "@chatbotkit/sdk": "*"
90
+ },
91
+ "devDependencies": {
92
+ "@tsconfig/recommended": "^1.0.2",
93
+ "@types/react": "^18.2.6",
94
+ "npm-run-all": "^4.1.5",
95
+ "typescript": "^5.0.4"
96
+ }
97
+ }