├── .eslintrc.js
├── .github
└── workflows
│ ├── ci.yml
│ └── release.yml
├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── README-ja.md
├── README.md
├── babel.config.js
├── examples
├── cdn
│ ├── .gitignore
│ ├── README.md
│ ├── package.json
│ ├── src
│ │ └── index.html
│ └── yarn.lock
└── echo-bot
│ ├── .gitignore
│ ├── README.md
│ ├── package.json
│ ├── public
│ └── index.html
│ ├── src
│ ├── App.tsx
│ ├── index.css
│ ├── index.tsx
│ └── react-app-env.d.ts
│ ├── tsconfig.json
│ └── yarn.lock
├── package.json
├── prettier.config.js
├── rollup.config.js
├── src
├── audio-media-recorder.ts
├── chat-controller.ts
├── chat-types.ts
├── index.ts
└── mui
│ ├── MuiAudioInput.tsx
│ ├── MuiChat.tsx
│ ├── MuiFileInput.tsx
│ ├── MuiMessage.tsx
│ ├── MuiMultiSelectInput.tsx
│ ├── MuiSelectInput.tsx
│ └── MuiTextInput.tsx
├── tsconfig.build.json
├── tsconfig.json
└── yarn.lock
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | extends: ['@twihike'],
4 | };
5 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: ci
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 |
8 | jobs:
9 | demo-site:
10 | strategy:
11 | matrix:
12 | os:
13 | - ubuntu-20.04
14 | node-version:
15 | - 14.x
16 | runs-on: ${{ matrix.os }}
17 | steps:
18 | - uses: actions/checkout@v2
19 | - uses: actions/setup-node@v1
20 | with:
21 | node-version: ${{ matrix.node-version }}
22 | - run: yarn install --frozen-lockfile
23 | working-directory: examples/echo-bot
24 | - run: yarn build
25 | working-directory: examples/echo-bot
26 | - run: yarn add -D netlify-cli
27 | working-directory: examples/echo-bot
28 | - run: yarn netlify --telemetry-disable
29 | working-directory: examples/echo-bot
30 | - run: yarn netlify deploy --dir=build
31 | working-directory: examples/echo-bot
32 | env:
33 | NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
34 | NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
35 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: release
2 |
3 | on:
4 | push:
5 | tags:
6 | - v*
7 |
8 | jobs:
9 | release:
10 | strategy:
11 | matrix:
12 | os:
13 | - ubuntu-20.04
14 | node-version:
15 | - 14.x
16 | runs-on: ${{ matrix.os }}
17 | steps:
18 | - uses: actions/checkout@v2
19 | - uses: actions/setup-node@v1
20 | with:
21 | node-version: ${{ matrix.node-version }}
22 | registry-url: 'https://registry.npmjs.org'
23 | - run: yarn install --frozen-lockfile
24 | - run: yarn build
25 | - run: yarn publish
26 | env:
27 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
28 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 | /dist
14 |
15 | # misc
16 | .DS_Store
17 | .env.local
18 | .env.development.local
19 | .env.test.local
20 | .env.production.local
21 |
22 | npm-debug.log*
23 | yarn-debug.log*
24 | yarn-error.log*
25 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4 |
5 | ## [0.3.0](https://github.com/twihike/chat-ui-react/compare/v0.2.15...v0.3.0) (2021-10-25)
6 |
7 | ### [0.2.15](https://github.com/twihike/chat-ui-react/compare/v0.2.14...v0.2.15) (2021-09-18)
8 |
9 | ### [0.2.14](https://github.com/twihike/chat-ui-react/compare/v0.2.13...v0.2.14) (2021-07-04)
10 |
11 | ### [0.2.13](https://github.com/twihike/chat-ui-react/compare/v0.2.12...v0.2.13) (2021-03-19)
12 |
13 | ### [0.2.12](https://github.com/twihike/chat-ui-react/compare/v0.2.11...v0.2.12) (2021-03-01)
14 |
15 | ### [0.2.11](https://github.com/twihike/chat-ui-react/compare/v0.2.10...v0.2.11) (2020-12-20)
16 |
17 | ### [0.2.10](https://github.com/twihike/chat-ui-react/compare/v0.2.9...v0.2.10) (2020-10-12)
18 |
19 | ### [0.2.9](https://github.com/twihike/chat-ui-react/compare/v0.2.8...v0.2.9) (2020-09-25)
20 |
21 | ### [0.2.8](https://github.com/twihike/chat-ui-react/compare/v0.2.7...v0.2.8) (2020-08-25)
22 |
23 | ### [0.2.7](https://github.com/twihike/chat-ui-react/compare/v0.2.6...v0.2.7) (2020-07-17)
24 |
25 | ### [0.2.6](https://github.com/twihike/chat-ui-react/compare/v0.2.5...v0.2.6) (2020-07-16)
26 |
27 | ### [0.2.5](https://github.com/twihike/chat-ui-react/compare/v0.2.4...v0.2.5) (2020-06-22)
28 |
29 | ### [0.2.4](https://github.com/twihike/chat-ui-react/compare/v0.2.3...v0.2.4) (2020-06-09)
30 |
31 | ### [0.2.3](https://github.com/twihike/chat-ui-react/compare/v0.2.2...v0.2.3) (2020-06-09)
32 |
33 | ### [0.2.2](https://github.com/twihike/chat-ui-react/compare/v0.2.1...v0.2.2) (2020-06-09)
34 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 twihike
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.
22 |
--------------------------------------------------------------------------------
/README-ja.md:
--------------------------------------------------------------------------------
1 | # chat-ui-react
2 |
3 | [](https://badge.fury.io/js/chat-ui-react) [](https://github.com/twihike/chat-ui-react/actions) [](https://github.com/twihike/chat-ui-react/actions) [](LICENSE)
4 |
5 | chat-ui-reactは会話型のWebUIを構築するためのnpmパッケージです。
6 | このパッケージが提供するものは次の通りです。
7 |
8 | - Reactコンポーネント
9 | - チャットのメッセージ
10 | - メッセージの入力フォーム
11 | - コンポーネントの表示制御を行うクラス
12 |
13 | あなたのオンラインチャットやチャットボットにこれを組み込むことができます。
14 |
15 | 現在、コンポーネントはReactのUIフレームワークであるMaterial-UIを利用しています。
16 | Material-UI以外のコンポーネントを望むなら、オリジナルのコンポーネントに差し替えて利用することもできます。
17 |
18 | 
19 |
20 | ## デモ
21 |
22 | [デモサイト](https://chat-ui-react-demo.netlify.app)をご覧ください。
23 |
24 | ## サンプル
25 |
26 | `examples`ディレクトリをご覧ください。
27 |
28 | - echo-bot: ユーザの入力をおうむ返しするチャットボットです。
29 | - cdn: すぐに始める簡単な方法です。
30 |
31 | ## インストール
32 |
33 | ### Node.js
34 |
35 | With npm:
36 |
37 | ```shell
38 | npm install chat-ui-react react react-dom @material-ui/core
39 | ```
40 |
41 | With yarn:
42 |
43 | ```shell
44 | yarn add chat-ui-react react react-dom @material-ui/core
45 | ```
46 |
47 | ### CDN
48 |
49 | ```html
50 |
51 |
52 |
53 |
54 |
55 | ```
56 |
57 | ## 使い方
58 |
59 | ### はじめに
60 |
61 | このパッケージは、チャットを表示する`MuiChat`コンポーネントとチャットの表示を制御する`ChatController`クラスで構成されます。以下はそれぞれの関係を表した図です。
62 |
63 | ```text
64 | +------------+ +------------------+ +-----------+
65 | | | Call | | Call | |
66 | | | | | | |
67 | | Your App | +-----> | ChatController | +-----> | MuiChat |
68 | | | | | | |
69 | | | | | | |
70 | +------------+ +------------------+ +-----------+
71 | ```
72 |
73 | この構造により、私たちはチャットの表示内容を`ChatController`に渡すことだけに専念できます。コンポーネントの表示制御を気にする必要はありません。
74 |
75 | 見た目で気に入らない部分があれば、`MuiChat`を別のコンポーネントに差し替えることができます。差し替えによるアプリの変更は生じません。
76 |
77 | 具体的な使い方を理解するために、簡単な例を示します。
78 |
79 | ```tsx
80 | function App(): React.ReactElement {
81 | const [chatCtl] = React.useState(new ChatController());
82 |
83 | React.useMemo(async () => {
84 | // チャットの内容はChatControllerを使って表示します
85 | await chatCtl.addMessage({
86 | type: 'text',
87 | content: `Hello, What's your name.`,
88 | self: false,
89 | });
90 | const name = await chatCtl.setActionRequest({ type: 'text' });
91 | }, [chatCtl]);
92 |
93 | // 表示に使用するコンポーネントは一つだけです
94 | return ;
95 | }
96 | ```
97 |
98 | 以降では`ChatController`の使い方を説明します。
99 |
100 | ### メッセージ
101 |
102 | チャットのメッセージを表示するには`addMessage`メソッドを利用します。
103 | `self`オプションに自分のメッセージか他人のメッセージかを指定します。
104 |
105 | ```typescript
106 | await chatCtl.addMessage({
107 | type: 'text',
108 | content: `Hello, What's your name.`,
109 | self: false,
110 | });
111 | ```
112 |
113 | ### アクション
114 |
115 | ユーザにメッセージの入力を促すには`setActionRequest`メソッドを利用します。
116 |
117 | #### アクションの回数
118 |
119 | アクションには1回限りのアクションを要求する方法と常にアクションを要求する方法があります。
120 |
121 | ##### 1回限りのアクション
122 |
123 | ユーザから1回限りのアクションを要求するには`always`オプションに`false`を指定します。
124 | メソッドの返却値は、ユーザの入力を返却する`Promise`です。
125 |
126 | ```typescript
127 | const response = await chatCtl.setActionRequest({
128 | type: 'text',
129 | always: false,
130 | });
131 | console.log(response.value);
132 | ```
133 |
134 | ##### 常時アクション
135 |
136 | ユーザから常にアクションを要求するには`always`オプションに`true`を指定します。
137 | ユーザから複数回入力されるため、入力を受け取るコールバック関数を指定します。
138 | ユーザからの入力要求を中止するには`cancelActionRequest`メソッドを呼び出します。
139 |
140 | ```typescript
141 | chatCtl.setActionRequest(
142 | { type: 'text', always: true },
143 | (response) => {
144 | console.log(response.value);
145 | }
146 | );
147 | chatCtl.cancelActionRequest();
148 | ```
149 |
150 | #### アクションタイプ
151 |
152 | アクションにはテキストや選択などいくつかの種類があります。
153 |
154 | ##### テキスト
155 |
156 | このアクションは文字列を入力します。
157 |
158 | `type`に`text`を指定します。
159 | メソッドの返却値はユーザが入力したメッセージです。
160 |
161 | ```typescript
162 | const response = await chatCtl.setActionRequest({ type: 'text' });
163 | console.log(response.value);
164 | ```
165 |
166 | ##### 単一選択
167 |
168 | このアクションは選択肢から1つ選びます。
169 |
170 | `type`に`select`を指定します。`options`に選択肢を指定します。`value`はhtmlの属性、`text`は画面表示に使います。
171 | メソッドの返却値はユーザが選択した`options`の要素です。
172 |
173 | ```typescript
174 | const response = await chatCtl.setActionRequest({
175 | type: 'select',
176 | options: [
177 | {
178 | value: 'a',
179 | text: 'A',
180 | },
181 | {
182 | value: 'b',
183 | text: 'B',
184 | },
185 | ],
186 | });
187 | console.log(response.option);
188 | // Aが選択された場合
189 | // { value: 'a', text: 'A' }
190 | ```
191 |
192 | ##### 複数選択
193 |
194 | このアクションは選択肢から複数選びます。
195 |
196 | `type`に`multi-select`を指定します。`options`に選択肢を指定します。`value`はhtmlの属性、`text`は表示に使います。メソッドの返却値は選択された`options`です。
197 |
198 | ```typescript
199 | const response = await chatCtl.setActionRequest({
200 | type: 'multi-select',
201 | options: [
202 | {
203 | value: 'a',
204 | text: 'A',
205 | },
206 | {
207 | value: 'b',
208 | text: 'B',
209 | },
210 | ],
211 | });
212 | console.log(response.options);
213 | // AとBが選択された場合
214 | // [{ value: 'a', text: 'A' }, { value: 'b', text: 'B' }]
215 | ```
216 |
217 | ##### ファイル
218 |
219 | このアクションはファイルを入力します。
220 |
221 | `type`に`file`を指定します。`input`タグの属性として`accept`と `multiple`を指定できます。メソッドの返却値はユーザが入力したファイルの配列です。
222 |
223 | ```typescript
224 | const response = await chatCtl.setActionRequest({
225 | type: 'file',
226 | accept: 'image/*',
227 | multiple: true,
228 | });
229 | console.log(response.files);
230 | ```
231 |
232 | ##### 音声
233 |
234 | このアクションは音声を入力します。
235 |
236 | `type`に`audio`を指定します。メソッドの返却値はユーザが入力した音声の`Blob`です。音声入力に失敗した場合は`reject`された`Promise`を返します。
237 |
238 | ```typescript
239 | try {
240 | const response = await chatCtl.setActionRequest({
241 | type: 'audio',
242 | });
243 | console.log(response.audio);
244 | } catch (e) {
245 | console.log(e);
246 | }
247 | ```
248 |
249 | ##### カスタム
250 |
251 | このアクションはあなたのカスタムコンポーネントを利用して入力します。
252 | `type`に`custom`を指定します。`Component`にあなたのコンポーネントを指定します。
253 |
254 | カスタムコンポーネントは、Reactの作法に倣っていつも通り入力フォームを作成します。
255 | プロパティとして`chatController`と`actionRequest`を受け取ります。これはchat-ui-reactにより自動でセットされます。
256 | そして、ユーザから受け取った入力を`ChatController`クラスの`setActionResponse`メソッドを使って伝搬します。
257 | これはアプリケーションが`setActionRequest`の返却値として受け取ることができます。
258 |
259 | ```tsx
260 | function GoodInput({
261 | chatController,
262 | actionRequest,
263 | }: {
264 | chatController: ChatController;
265 | actionRequest: ActionRequest;
266 | }) {
267 | const chatCtl = chatController;
268 |
269 | const setResponse = React.useCallback((): void => {
270 | const res = { type: 'custom', value: 'Good!' };
271 | chatCtl.setActionResponse(actionRequest, res);
272 | }, [actionRequest, chatCtl]);
273 |
274 | return (
275 |
283 | );
284 | }
285 |
286 | const custom = await chatCtl.setActionRequest({
287 | type: 'custom',
288 | Component: GoodInput,
289 | });
290 | console.log(custom.value);
291 | ```
292 |
293 | ## License
294 |
295 | Copyright (c) 2020 twihike. All rights reserved.
296 |
297 | This project is licensed under the terms of the MIT license.
298 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # chat-ui-react
2 |
3 | [](https://badge.fury.io/js/chat-ui-react) [](https://github.com/twihike/chat-ui-react/actions) [](https://github.com/twihike/chat-ui-react/actions) [](LICENSE)
4 |
5 | chat-ui-react is an npm package for building conversational web UIs.
6 | This package offers the following:
7 |
8 | - React component
9 | - Chat message
10 | - Message input form
11 | - Class that controls the display of components
12 |
13 | You can incorporate this into your online chats and chatbots.
14 |
15 | Currently, the component uses React's UI framework Material-UI.
16 | If you want a component other than Material-UI, you can replace it with the original component and use it.
17 |
18 | 
19 |
20 | ## Demo
21 |
22 | See the [demo site](https://chat-ui-react-demo.netlify.app).
23 |
24 | ## Example
25 |
26 | See the `example` directory.
27 |
28 | - echo-bot: A chatbot that echoes user input.
29 | - cdn: An easy way to get started quickly.
30 |
31 | ## Installation
32 |
33 | ### Node.js
34 |
35 | With npm:
36 |
37 | ```shell
38 | npm install chat-ui-react react react-dom @material-ui/core
39 | ```
40 |
41 | With yarn:
42 |
43 | ```shell
44 | yarn add chat-ui-react react react-dom @material-ui/core
45 | ```
46 |
47 | ### CDN
48 |
49 | ```html
50 |
51 |
52 |
53 |
54 |
55 | ```
56 |
57 | ## Usage
58 |
59 | This package consists of a `MuiChat` component that displays the chat and a `ChatController` class that controls the display of the chat. The figure below shows each relationship.
60 |
61 | ```text
62 | +------------+ +------------------+ +-----------+
63 | | | Call | | Call | |
64 | | | | | | |
65 | | Your App | +-----> | ChatController | +-----> | MuiChat |
66 | | | | | | |
67 | | | | | | |
68 | +------------+ +------------------+ +-----------+
69 | ```
70 |
71 | This structure allows us to focus solely on passing the chat display to the `ChatController`. You don't have to worry about display control of components.
72 |
73 | If you don't like what you see, you can replace `MuiChat` with another component. There is no change in the app due to replacement.
74 |
75 | Here's a simple example to understand how to use it.
76 |
77 | ```tsx
78 | function App(): React.ReactElement {
79 | const [chatCtl] = React.useState(new ChatController());
80 |
81 | React.useMemo(async () => {
82 | // Chat content is displayed using ChatController
83 | await chatCtl.addMessage({
84 | type: 'text',
85 | content: `Hello, What's your name.`,
86 | self: false,
87 | });
88 | const name = await chatCtl.setActionRequest({ type: 'text' });
89 | }, [chatCtl]);
90 |
91 | // Only one component used for display
92 | return ;
93 | }
94 | ```
95 |
96 | In the following, we will explain how to use `ChatController`.
97 |
98 | ### Message
99 |
100 | To display the chat message, use the `addMessage` method.
101 | In the `self` option, specify whether it is your own message or someone else's message.
102 |
103 | ```typescript
104 | await chatCtl.addMessage({
105 | type: 'text',
106 | content: `Hello, What's your name.`,
107 | self: false,
108 | });
109 | ```
110 |
111 | ### Action
112 |
113 | Use the `setActionRequest` method to prompt the user for a message.
114 |
115 | #### Number of actions
116 |
117 | There are two ways to request an action: one-time action and always request action.
118 |
119 | ##### One-time action
120 |
121 | Specify `false` for the `always` option to request a one-time action from the user.
122 | The return value of the method is a `Promise` that returns the user input.
123 |
124 | ```typescript
125 | const response = await chatCtl.setActionRequest({
126 | type: 'text',
127 | always: false,
128 | });
129 | console.log(response.value);
130 | ```
131 |
132 | ##### Always action
133 |
134 | To always request an action from the user, specify `true` in the `always` option.
135 | Specify the callback function that receives the input because it is input multiple times by the user.
136 | To cancel the input request from the user, call the `cancelActionRequest` method.
137 |
138 | ```typescript
139 | chatCtl.setActionRequest(
140 | { type: 'text', always: true },
141 | (response) => {
142 | console.log(response.value);
143 | }
144 | );
145 | chatCtl.cancelActionRequest();
146 | ```
147 |
148 | #### Action type
149 |
150 | There are several types of actions such as text and selection.
151 |
152 | ##### Text
153 |
154 | This action inputs a string.
155 |
156 | Specify `text` for `type`.
157 | The return value of the method is the message entered by the user.
158 |
159 | ```typescript
160 | const response = await chatCtl.setActionRequest({ type: 'text' });
161 | console.log(response.value);
162 | ```
163 |
164 | ##### Single selection
165 |
166 | This action selects one from the options.
167 |
168 | Specify `select` for `type`. Specify the options in `options`. `value` is used for html attributes and `text` is used for screen display.
169 | The return value of the method is the element of the `options` selected by the user.
170 |
171 | ```typescript
172 | const response = await chatCtl.setActionRequest({
173 | type: 'select',
174 | options: [
175 | {
176 | value: 'a',
177 | text: 'A',
178 | },
179 | {
180 | value: 'b',
181 | text: 'B',
182 | },
183 | ],
184 | });
185 | console.log(response.option);
186 | // If A is selected
187 | // { value: 'a', text: 'A' }
188 | ```
189 |
190 | ##### Multiple selection
191 |
192 | This action selects multiple options.
193 |
194 | Specify `multi-select` for `type`. Specify the options in `options`. `value` is used for html attributes and `text` is used for display. The return value of the method is the selected `options`.
195 |
196 | ```typescript
197 | const response = await chatCtl.setActionRequest({
198 | type: 'multi-select',
199 | options: [
200 | {
201 | value: 'a',
202 | text: 'A',
203 | },
204 | {
205 | value: 'b',
206 | text: 'B',
207 | },
208 | ],
209 | });
210 | console.log(response.options);
211 | // If A and B are selected
212 | // [{ value: 'a', text: 'A' }, { value: 'b', text: 'B' }]
213 | ```
214 |
215 | ##### File
216 |
217 | This action inputs a file.
218 |
219 | Specify `file` for `type`. You can specify `accept` and `multiple` as attributes of the `input` tag. The return value of the method is an array of files entered by the user.
220 |
221 | ```typescript
222 | const response = await chatCtl.setActionRequest({
223 | type: 'file',
224 | accept: 'image/*',
225 | multiple: true,
226 | });
227 | console.log(response.files);
228 | ```
229 |
230 | ##### Audio
231 |
232 | This action inputs audio.
233 |
234 | Specify `audio` for `type`. The return value of the method is the `Blob` of the audio input by the user. If the audio input fails, the `Reject` rejected `Promise` is returned.
235 |
236 | ```typescript
237 | try {
238 | const response = await chatCtl.setActionRequest({
239 | type: 'audio',
240 | });
241 | console.log(response.audio);
242 | } catch (e) {
243 | console.log(e);
244 | }
245 | ```
246 |
247 | ##### Custom
248 |
249 | This action uses your custom component as input.
250 | Specify `custom` for `type`. Specify your component in `Component`.
251 |
252 | Custom components follow the React conventions to create input forms as usual.
253 | It receives `chatController` and `actionRequest` as properties. This is automatically set by chat-ui-react.
254 | Then, set the input received from the user to the `setActionResponse` method of the `ChatController` class.
255 | This can be received by the application as the return value of `setActionRequest`.
256 |
257 | ```tsx
258 | function GoodInput({
259 | chatController,
260 | actionRequest,
261 | }: {
262 | chatController: ChatController;
263 | actionRequest: ActionRequest;
264 | }) {
265 | const chatCtl = chatController;
266 |
267 | const setResponse = React.useCallback((): void => {
268 | const res = { type: 'custom', value: 'Good!' };
269 | chatCtl.setActionResponse(actionRequest, res);
270 | }, [actionRequest, chatCtl]);
271 |
272 | return (
273 |
281 | );
282 | }
283 |
284 | const custom = await chatCtl.setActionRequest({
285 | type: 'custom',
286 | Component: GoodInput,
287 | });
288 | console.log(custom.value);
289 | ```
290 |
291 | ## License
292 |
293 | Copyright (c) 2020 twihike. All rights reserved.
294 |
295 | This project is licensed under the terms of the MIT license.
296 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = function (api) {
2 | api.cache(true);
3 |
4 | const envNames = [
5 | 'esm',
6 | 'cjs',
7 | 'rollupUmd',
8 | 'rollupUmdPolyfill',
9 | 'rollupEsm',
10 | 'rollupEsmPolyfill',
11 | ];
12 |
13 | const getPresetEnvOption = (envName) => {
14 | const options = {
15 | esm: {
16 | modules: false,
17 | targets: {
18 | node: '12',
19 | },
20 | },
21 | cjs: {
22 | modules: 'commonjs',
23 | targets: {
24 | node: '10',
25 | },
26 | },
27 | rollupUmd: {
28 | bugfixes: true,
29 | modules: false,
30 | },
31 | rollupUmdPolyfill: {
32 | bugfixes: true,
33 | modules: false,
34 | },
35 | rollupEsm: {
36 | bugfixes: true,
37 | modules: false,
38 | targets: {
39 | esmodules: true,
40 | },
41 | },
42 | rollupEsmPolyfill: {
43 | bugfixes: true,
44 | modules: false,
45 | targets: {
46 | esmodules: true,
47 | },
48 | },
49 | };
50 |
51 | return options[envName];
52 | };
53 |
54 | const getTransformRuntimeOption = (envName) => {
55 | const version = '^7.9.0';
56 | const corejs = {
57 | version: 3,
58 | proposals: true,
59 | };
60 | const options = {
61 | esm: {
62 | useESModules: true,
63 | version,
64 | },
65 | cjs: {
66 | useESModules: false,
67 | version,
68 | },
69 | rollupUmd: {
70 | useESModules: true,
71 | version,
72 | },
73 | rollupUmdPolyfill: {
74 | useESModules: true,
75 | version,
76 | corejs,
77 | },
78 | rollupEsm: {
79 | useESModules: true,
80 | version,
81 | },
82 | rollupEsmPolyfill: {
83 | useESModules: true,
84 | version,
85 | corejs,
86 | },
87 | };
88 |
89 | return options[envName];
90 | };
91 |
92 | const getPresets = (envName) => [
93 | ['@babel/preset-env', getPresetEnvOption(envName)],
94 | '@babel/preset-react',
95 | [
96 | '@babel/preset-typescript',
97 | {
98 | allowDeclareFields: true,
99 | },
100 | ],
101 | ];
102 |
103 | const getPlugins = (envName) => [
104 | ['@babel/plugin-transform-runtime', getTransformRuntimeOption(envName)],
105 | // For typescript
106 | '@babel/proposal-class-properties',
107 | '@babel/proposal-object-rest-spread',
108 | ];
109 |
110 | return {
111 | env: envNames.reduce(
112 | (envOptions, envName) => ({
113 | ...envOptions,
114 | [envName]: {
115 | presets: getPresets(envName),
116 | plugins: getPlugins(envName),
117 | },
118 | }),
119 | {},
120 | ),
121 | sourceMaps: 'inline',
122 | };
123 | };
124 |
--------------------------------------------------------------------------------
/examples/cdn/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/examples/cdn/README.md:
--------------------------------------------------------------------------------
1 | # CDN
2 |
3 | An easy way to get started quickly.
4 |
5 | ## Usage
6 |
7 | ```shell
8 | yarn start
9 | ```
10 |
--------------------------------------------------------------------------------
/examples/cdn/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "cdn",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "start": "serve src"
7 | },
8 | "devDependencies": {
9 | "serve": "12.0.1"
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/examples/cdn/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Chat UI React
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
77 |
78 |
79 |
--------------------------------------------------------------------------------
/examples/cdn/yarn.lock:
--------------------------------------------------------------------------------
1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
2 | # yarn lockfile v1
3 |
4 |
5 | "@zeit/schemas@2.6.0":
6 | version "2.6.0"
7 | resolved "https://registry.yarnpkg.com/@zeit/schemas/-/schemas-2.6.0.tgz#004e8e553b4cd53d538bd38eac7bcbf58a867fe3"
8 | integrity sha512-uUrgZ8AxS+Lio0fZKAipJjAh415JyrOZowliZAzmnJSsf7piVL5w+G0+gFJ0KSu3QRhvui/7zuvpLz03YjXAhg==
9 |
10 | accepts@~1.3.5:
11 | version "1.3.7"
12 | resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd"
13 | integrity sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==
14 | dependencies:
15 | mime-types "~2.1.24"
16 | negotiator "0.6.2"
17 |
18 | ajv@6.12.6:
19 | version "6.12.6"
20 | resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4"
21 | integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==
22 | dependencies:
23 | fast-deep-equal "^3.1.1"
24 | fast-json-stable-stringify "^2.0.0"
25 | json-schema-traverse "^0.4.1"
26 | uri-js "^4.2.2"
27 |
28 | ansi-align@^2.0.0:
29 | version "2.0.0"
30 | resolved "https://registry.yarnpkg.com/ansi-align/-/ansi-align-2.0.0.tgz#c36aeccba563b89ceb556f3690f0b1d9e3547f7f"
31 | integrity sha1-w2rsy6VjuJzrVW82kPCx2eNUf38=
32 | dependencies:
33 | string-width "^2.0.0"
34 |
35 | ansi-regex@^3.0.0:
36 | version "3.0.0"
37 | resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998"
38 | integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=
39 |
40 | ansi-styles@^3.2.1:
41 | version "3.2.1"
42 | resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"
43 | integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==
44 | dependencies:
45 | color-convert "^1.9.0"
46 |
47 | arch@^2.1.1:
48 | version "2.2.0"
49 | resolved "https://registry.yarnpkg.com/arch/-/arch-2.2.0.tgz#1bc47818f305764f23ab3306b0bfc086c5a29d11"
50 | integrity sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==
51 |
52 | arg@2.0.0:
53 | version "2.0.0"
54 | resolved "https://registry.yarnpkg.com/arg/-/arg-2.0.0.tgz#c06e7ff69ab05b3a4a03ebe0407fac4cba657545"
55 | integrity sha512-XxNTUzKnz1ctK3ZIcI2XUPlD96wbHP2nGqkPKpvk/HNRlPveYrXIVSTk9m3LcqOgDPg3B1nMvdV/K8wZd7PG4w==
56 |
57 | balanced-match@^1.0.0:
58 | version "1.0.2"
59 | resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
60 | integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
61 |
62 | boxen@1.3.0:
63 | version "1.3.0"
64 | resolved "https://registry.yarnpkg.com/boxen/-/boxen-1.3.0.tgz#55c6c39a8ba58d9c61ad22cd877532deb665a20b"
65 | integrity sha512-TNPjfTr432qx7yOjQyaXm3dSR0MH9vXp7eT1BFSl/C51g+EFnOR9hTg1IreahGBmDNCehscshe45f+C1TBZbLw==
66 | dependencies:
67 | ansi-align "^2.0.0"
68 | camelcase "^4.0.0"
69 | chalk "^2.0.1"
70 | cli-boxes "^1.0.0"
71 | string-width "^2.0.0"
72 | term-size "^1.2.0"
73 | widest-line "^2.0.0"
74 |
75 | brace-expansion@^1.1.7:
76 | version "1.1.11"
77 | resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
78 | integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==
79 | dependencies:
80 | balanced-match "^1.0.0"
81 | concat-map "0.0.1"
82 |
83 | bytes@3.0.0:
84 | version "3.0.0"
85 | resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048"
86 | integrity sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=
87 |
88 | camelcase@^4.0.0:
89 | version "4.1.0"
90 | resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd"
91 | integrity sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=
92 |
93 | chalk@2.4.1:
94 | version "2.4.1"
95 | resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.1.tgz#18c49ab16a037b6eb0152cc83e3471338215b66e"
96 | integrity sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==
97 | dependencies:
98 | ansi-styles "^3.2.1"
99 | escape-string-regexp "^1.0.5"
100 | supports-color "^5.3.0"
101 |
102 | chalk@^2.0.1:
103 | version "2.4.2"
104 | resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
105 | integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==
106 | dependencies:
107 | ansi-styles "^3.2.1"
108 | escape-string-regexp "^1.0.5"
109 | supports-color "^5.3.0"
110 |
111 | cli-boxes@^1.0.0:
112 | version "1.0.0"
113 | resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-1.0.0.tgz#4fa917c3e59c94a004cd61f8ee509da651687143"
114 | integrity sha1-T6kXw+WclKAEzWH47lCdplFocUM=
115 |
116 | clipboardy@2.3.0:
117 | version "2.3.0"
118 | resolved "https://registry.yarnpkg.com/clipboardy/-/clipboardy-2.3.0.tgz#3c2903650c68e46a91b388985bc2774287dba290"
119 | integrity sha512-mKhiIL2DrQIsuXMgBgnfEHOZOryC7kY7YO//TN6c63wlEm3NG5tz+YgY5rVi29KCmq/QQjKYvM7a19+MDOTHOQ==
120 | dependencies:
121 | arch "^2.1.1"
122 | execa "^1.0.0"
123 | is-wsl "^2.1.1"
124 |
125 | color-convert@^1.9.0:
126 | version "1.9.3"
127 | resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
128 | integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==
129 | dependencies:
130 | color-name "1.1.3"
131 |
132 | color-name@1.1.3:
133 | version "1.1.3"
134 | resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
135 | integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=
136 |
137 | compressible@~2.0.14:
138 | version "2.0.18"
139 | resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba"
140 | integrity sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==
141 | dependencies:
142 | mime-db ">= 1.43.0 < 2"
143 |
144 | compression@1.7.3:
145 | version "1.7.3"
146 | resolved "https://registry.yarnpkg.com/compression/-/compression-1.7.3.tgz#27e0e176aaf260f7f2c2813c3e440adb9f1993db"
147 | integrity sha512-HSjyBG5N1Nnz7tF2+O7A9XUhyjru71/fwgNb7oIsEVHR0WShfs2tIS/EySLgiTe98aOK18YDlMXpzjCXY/n9mg==
148 | dependencies:
149 | accepts "~1.3.5"
150 | bytes "3.0.0"
151 | compressible "~2.0.14"
152 | debug "2.6.9"
153 | on-headers "~1.0.1"
154 | safe-buffer "5.1.2"
155 | vary "~1.1.2"
156 |
157 | concat-map@0.0.1:
158 | version "0.0.1"
159 | resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
160 | integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
161 |
162 | content-disposition@0.5.2:
163 | version "0.5.2"
164 | resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4"
165 | integrity sha1-DPaLud318r55YcOoUXjLhdunjLQ=
166 |
167 | cross-spawn@^5.0.1:
168 | version "5.1.0"
169 | resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449"
170 | integrity sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=
171 | dependencies:
172 | lru-cache "^4.0.1"
173 | shebang-command "^1.2.0"
174 | which "^1.2.9"
175 |
176 | cross-spawn@^6.0.0:
177 | version "6.0.5"
178 | resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"
179 | integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==
180 | dependencies:
181 | nice-try "^1.0.4"
182 | path-key "^2.0.1"
183 | semver "^5.5.0"
184 | shebang-command "^1.2.0"
185 | which "^1.2.9"
186 |
187 | debug@2.6.9:
188 | version "2.6.9"
189 | resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
190 | integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
191 | dependencies:
192 | ms "2.0.0"
193 |
194 | deep-extend@^0.6.0:
195 | version "0.6.0"
196 | resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac"
197 | integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==
198 |
199 | end-of-stream@^1.1.0:
200 | version "1.4.4"
201 | resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0"
202 | integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==
203 | dependencies:
204 | once "^1.4.0"
205 |
206 | escape-string-regexp@^1.0.5:
207 | version "1.0.5"
208 | resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
209 | integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=
210 |
211 | execa@^0.7.0:
212 | version "0.7.0"
213 | resolved "https://registry.yarnpkg.com/execa/-/execa-0.7.0.tgz#944becd34cc41ee32a63a9faf27ad5a65fc59777"
214 | integrity sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=
215 | dependencies:
216 | cross-spawn "^5.0.1"
217 | get-stream "^3.0.0"
218 | is-stream "^1.1.0"
219 | npm-run-path "^2.0.0"
220 | p-finally "^1.0.0"
221 | signal-exit "^3.0.0"
222 | strip-eof "^1.0.0"
223 |
224 | execa@^1.0.0:
225 | version "1.0.0"
226 | resolved "https://registry.yarnpkg.com/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8"
227 | integrity sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==
228 | dependencies:
229 | cross-spawn "^6.0.0"
230 | get-stream "^4.0.0"
231 | is-stream "^1.1.0"
232 | npm-run-path "^2.0.0"
233 | p-finally "^1.0.0"
234 | signal-exit "^3.0.0"
235 | strip-eof "^1.0.0"
236 |
237 | fast-deep-equal@^3.1.1:
238 | version "3.1.3"
239 | resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
240 | integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
241 |
242 | fast-json-stable-stringify@^2.0.0:
243 | version "2.1.0"
244 | resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633"
245 | integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==
246 |
247 | fast-url-parser@1.1.3:
248 | version "1.1.3"
249 | resolved "https://registry.yarnpkg.com/fast-url-parser/-/fast-url-parser-1.1.3.tgz#f4af3ea9f34d8a271cf58ad2b3759f431f0b318d"
250 | integrity sha1-9K8+qfNNiicc9YrSs3WfQx8LMY0=
251 | dependencies:
252 | punycode "^1.3.2"
253 |
254 | get-stream@^3.0.0:
255 | version "3.0.0"
256 | resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14"
257 | integrity sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=
258 |
259 | get-stream@^4.0.0:
260 | version "4.1.0"
261 | resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5"
262 | integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==
263 | dependencies:
264 | pump "^3.0.0"
265 |
266 | has-flag@^3.0.0:
267 | version "3.0.0"
268 | resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
269 | integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0=
270 |
271 | ini@~1.3.0:
272 | version "1.3.8"
273 | resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c"
274 | integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==
275 |
276 | is-docker@^2.0.0:
277 | version "2.2.1"
278 | resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa"
279 | integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==
280 |
281 | is-fullwidth-code-point@^2.0.0:
282 | version "2.0.0"
283 | resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f"
284 | integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=
285 |
286 | is-stream@^1.1.0:
287 | version "1.1.0"
288 | resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"
289 | integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ=
290 |
291 | is-wsl@^2.1.1:
292 | version "2.2.0"
293 | resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271"
294 | integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==
295 | dependencies:
296 | is-docker "^2.0.0"
297 |
298 | isexe@^2.0.0:
299 | version "2.0.0"
300 | resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
301 | integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=
302 |
303 | json-schema-traverse@^0.4.1:
304 | version "0.4.1"
305 | resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660"
306 | integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==
307 |
308 | lru-cache@^4.0.1:
309 | version "4.1.5"
310 | resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd"
311 | integrity sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==
312 | dependencies:
313 | pseudomap "^1.0.2"
314 | yallist "^2.1.2"
315 |
316 | mime-db@1.48.0, "mime-db@>= 1.43.0 < 2":
317 | version "1.48.0"
318 | resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.48.0.tgz#e35b31045dd7eada3aaad537ed88a33afbef2d1d"
319 | integrity sha512-FM3QwxV+TnZYQ2aRqhlKBMHxk10lTbMt3bBkMAp54ddrNeVSfcQYOOKuGuy3Ddrm38I04If834fOUSq1yzslJQ==
320 |
321 | mime-db@~1.33.0:
322 | version "1.33.0"
323 | resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.33.0.tgz#a3492050a5cb9b63450541e39d9788d2272783db"
324 | integrity sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==
325 |
326 | mime-types@2.1.18:
327 | version "2.1.18"
328 | resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.18.tgz#6f323f60a83d11146f831ff11fd66e2fe5503bb8"
329 | integrity sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==
330 | dependencies:
331 | mime-db "~1.33.0"
332 |
333 | mime-types@~2.1.24:
334 | version "2.1.31"
335 | resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.31.tgz#a00d76b74317c61f9c2db2218b8e9f8e9c5c9e6b"
336 | integrity sha512-XGZnNzm3QvgKxa8dpzyhFTHmpP3l5YNusmne07VUOXxou9CqUqYa/HBy124RqtVh/O2pECas/MOcsDgpilPOPg==
337 | dependencies:
338 | mime-db "1.48.0"
339 |
340 | minimatch@3.0.4:
341 | version "3.0.4"
342 | resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
343 | integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==
344 | dependencies:
345 | brace-expansion "^1.1.7"
346 |
347 | minimist@^1.2.0:
348 | version "1.2.5"
349 | resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
350 | integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
351 |
352 | ms@2.0.0:
353 | version "2.0.0"
354 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
355 | integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=
356 |
357 | negotiator@0.6.2:
358 | version "0.6.2"
359 | resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb"
360 | integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==
361 |
362 | nice-try@^1.0.4:
363 | version "1.0.5"
364 | resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
365 | integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==
366 |
367 | npm-run-path@^2.0.0:
368 | version "2.0.2"
369 | resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f"
370 | integrity sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=
371 | dependencies:
372 | path-key "^2.0.0"
373 |
374 | on-headers@~1.0.1:
375 | version "1.0.2"
376 | resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f"
377 | integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==
378 |
379 | once@^1.3.1, once@^1.4.0:
380 | version "1.4.0"
381 | resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
382 | integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E=
383 | dependencies:
384 | wrappy "1"
385 |
386 | p-finally@^1.0.0:
387 | version "1.0.0"
388 | resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae"
389 | integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=
390 |
391 | path-is-inside@1.0.2:
392 | version "1.0.2"
393 | resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53"
394 | integrity sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=
395 |
396 | path-key@^2.0.0, path-key@^2.0.1:
397 | version "2.0.1"
398 | resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40"
399 | integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=
400 |
401 | path-to-regexp@2.2.1:
402 | version "2.2.1"
403 | resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-2.2.1.tgz#90b617025a16381a879bc82a38d4e8bdeb2bcf45"
404 | integrity sha512-gu9bD6Ta5bwGrrU8muHzVOBFFREpp2iRkVfhBJahwJ6p6Xw20SjT0MxLnwkjOibQmGSYhiUnf2FLe7k+jcFmGQ==
405 |
406 | pseudomap@^1.0.2:
407 | version "1.0.2"
408 | resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3"
409 | integrity sha1-8FKijacOYYkX7wqKw0wa5aaChrM=
410 |
411 | pump@^3.0.0:
412 | version "3.0.0"
413 | resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64"
414 | integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==
415 | dependencies:
416 | end-of-stream "^1.1.0"
417 | once "^1.3.1"
418 |
419 | punycode@^1.3.2:
420 | version "1.4.1"
421 | resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"
422 | integrity sha1-wNWmOycYgArY4esPpSachN1BhF4=
423 |
424 | punycode@^2.1.0:
425 | version "2.1.1"
426 | resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
427 | integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
428 |
429 | range-parser@1.2.0:
430 | version "1.2.0"
431 | resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e"
432 | integrity sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=
433 |
434 | rc@^1.0.1, rc@^1.1.6:
435 | version "1.2.8"
436 | resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed"
437 | integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==
438 | dependencies:
439 | deep-extend "^0.6.0"
440 | ini "~1.3.0"
441 | minimist "^1.2.0"
442 | strip-json-comments "~2.0.1"
443 |
444 | registry-auth-token@3.3.2:
445 | version "3.3.2"
446 | resolved "https://registry.yarnpkg.com/registry-auth-token/-/registry-auth-token-3.3.2.tgz#851fd49038eecb586911115af845260eec983f20"
447 | integrity sha512-JL39c60XlzCVgNrO+qq68FoNb56w/m7JYvGR2jT5iR1xBrUA3Mfx5Twk5rqTThPmQKMWydGmq8oFtDlxfrmxnQ==
448 | dependencies:
449 | rc "^1.1.6"
450 | safe-buffer "^5.0.1"
451 |
452 | registry-url@3.1.0:
453 | version "3.1.0"
454 | resolved "https://registry.yarnpkg.com/registry-url/-/registry-url-3.1.0.tgz#3d4ef870f73dde1d77f0cf9a381432444e174942"
455 | integrity sha1-PU74cPc93h138M+aOBQyRE4XSUI=
456 | dependencies:
457 | rc "^1.0.1"
458 |
459 | safe-buffer@5.1.2:
460 | version "5.1.2"
461 | resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
462 | integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
463 |
464 | safe-buffer@^5.0.1:
465 | version "5.2.1"
466 | resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
467 | integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
468 |
469 | semver@^5.5.0:
470 | version "5.7.1"
471 | resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
472 | integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
473 |
474 | serve-handler@6.1.3:
475 | version "6.1.3"
476 | resolved "https://registry.yarnpkg.com/serve-handler/-/serve-handler-6.1.3.tgz#1bf8c5ae138712af55c758477533b9117f6435e8"
477 | integrity sha512-FosMqFBNrLyeiIDvP1zgO6YoTzFYHxLDEIavhlmQ+knB2Z7l1t+kGLHkZIDN7UVWqQAmKI3D20A6F6jo3nDd4w==
478 | dependencies:
479 | bytes "3.0.0"
480 | content-disposition "0.5.2"
481 | fast-url-parser "1.1.3"
482 | mime-types "2.1.18"
483 | minimatch "3.0.4"
484 | path-is-inside "1.0.2"
485 | path-to-regexp "2.2.1"
486 | range-parser "1.2.0"
487 |
488 | serve@12.0.1:
489 | version "12.0.1"
490 | resolved "https://registry.yarnpkg.com/serve/-/serve-12.0.1.tgz#5b0e05849f5ed9b8aab0f30a298c3664bba052bb"
491 | integrity sha512-CQ4ikLpxg/wmNM7yivulpS6fhjRiFG6OjmP8ty3/c1SBnSk23fpKmLAV4HboTA2KrZhkUPlDfjDhnRmAjQ5Phw==
492 | dependencies:
493 | "@zeit/schemas" "2.6.0"
494 | ajv "6.12.6"
495 | arg "2.0.0"
496 | boxen "1.3.0"
497 | chalk "2.4.1"
498 | clipboardy "2.3.0"
499 | compression "1.7.3"
500 | serve-handler "6.1.3"
501 | update-check "1.5.2"
502 |
503 | shebang-command@^1.2.0:
504 | version "1.2.0"
505 | resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea"
506 | integrity sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=
507 | dependencies:
508 | shebang-regex "^1.0.0"
509 |
510 | shebang-regex@^1.0.0:
511 | version "1.0.0"
512 | resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3"
513 | integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=
514 |
515 | signal-exit@^3.0.0:
516 | version "3.0.3"
517 | resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c"
518 | integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==
519 |
520 | string-width@^2.0.0, string-width@^2.1.1:
521 | version "2.1.1"
522 | resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e"
523 | integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==
524 | dependencies:
525 | is-fullwidth-code-point "^2.0.0"
526 | strip-ansi "^4.0.0"
527 |
528 | strip-ansi@^4.0.0:
529 | version "4.0.0"
530 | resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f"
531 | integrity sha1-qEeQIusaw2iocTibY1JixQXuNo8=
532 | dependencies:
533 | ansi-regex "^3.0.0"
534 |
535 | strip-eof@^1.0.0:
536 | version "1.0.0"
537 | resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf"
538 | integrity sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=
539 |
540 | strip-json-comments@~2.0.1:
541 | version "2.0.1"
542 | resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
543 | integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo=
544 |
545 | supports-color@^5.3.0:
546 | version "5.5.0"
547 | resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
548 | integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==
549 | dependencies:
550 | has-flag "^3.0.0"
551 |
552 | term-size@^1.2.0:
553 | version "1.2.0"
554 | resolved "https://registry.yarnpkg.com/term-size/-/term-size-1.2.0.tgz#458b83887f288fc56d6fffbfad262e26638efa69"
555 | integrity sha1-RYuDiH8oj8Vtb/+/rSYuJmOO+mk=
556 | dependencies:
557 | execa "^0.7.0"
558 |
559 | update-check@1.5.2:
560 | version "1.5.2"
561 | resolved "https://registry.yarnpkg.com/update-check/-/update-check-1.5.2.tgz#2fe09f725c543440b3d7dabe8971f2d5caaedc28"
562 | integrity sha512-1TrmYLuLj/5ZovwUS7fFd1jMH3NnFDN1y1A8dboedIDt7zs/zJMo6TwwlhYKkSeEwzleeiSBV5/3c9ufAQWDaQ==
563 | dependencies:
564 | registry-auth-token "3.3.2"
565 | registry-url "3.1.0"
566 |
567 | uri-js@^4.2.2:
568 | version "4.4.1"
569 | resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e"
570 | integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==
571 | dependencies:
572 | punycode "^2.1.0"
573 |
574 | vary@~1.1.2:
575 | version "1.1.2"
576 | resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
577 | integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=
578 |
579 | which@^1.2.9:
580 | version "1.3.1"
581 | resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
582 | integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==
583 | dependencies:
584 | isexe "^2.0.0"
585 |
586 | widest-line@^2.0.0:
587 | version "2.0.1"
588 | resolved "https://registry.yarnpkg.com/widest-line/-/widest-line-2.0.1.tgz#7438764730ec7ef4381ce4df82fb98a53142a3fc"
589 | integrity sha512-Ba5m9/Fa4Xt9eb2ELXt77JxVDV8w7qQrH0zS/TWSJdLyAwQjWoOzpzj5lwVftDz6n/EOu3tNACS84v509qwnJA==
590 | dependencies:
591 | string-width "^2.1.1"
592 |
593 | wrappy@1:
594 | version "1.0.2"
595 | resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
596 | integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
597 |
598 | yallist@^2.1.2:
599 | version "2.1.2"
600 | resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52"
601 | integrity sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=
602 |
--------------------------------------------------------------------------------
/examples/echo-bot/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/examples/echo-bot/README.md:
--------------------------------------------------------------------------------
1 | # Echo bot
2 |
3 | A chatbot that echoes user input.
4 |
5 | ## Usage
6 |
7 | ```shell
8 | yarn start
9 | ```
10 |
--------------------------------------------------------------------------------
/examples/echo-bot/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "echo-bot",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@emotion/react": "11.5.0",
7 | "@emotion/styled": "11.3.0",
8 | "@mui/material": "5.0.4",
9 | "@testing-library/jest-dom": "5.14.1",
10 | "@testing-library/react": "12.1.0",
11 | "@testing-library/user-event": "13.2.1",
12 | "@twihike/eslint-config": "0.1.17",
13 | "@twihike/prettier-config": "0.1.17",
14 | "@twihike/stylelint-config": "0.1.17",
15 | "@types/jest": "27.0.1",
16 | "@types/node": "16.9.2",
17 | "@types/react": "17.0.21",
18 | "@types/react-dom": "17.0.9",
19 | "@typescript-eslint/eslint-plugin": "4.31.1",
20 | "chat-ui-react": "0.3.0",
21 | "cross-env": "7.0.3",
22 | "eslint": "7.32.0",
23 | "eslint-plugin-eslint-comments": "3.2.0",
24 | "eslint-plugin-import": "2.24.2",
25 | "eslint-plugin-jest": "24.4.2",
26 | "eslint-plugin-jsx-a11y": "6.4.1",
27 | "eslint-plugin-node": "11.1.0",
28 | "eslint-plugin-promise": "5.1.0",
29 | "eslint-plugin-react": "7.25.2",
30 | "eslint-plugin-react-hooks": "4.2.0",
31 | "eslint-plugin-unicorn": "36.0.0",
32 | "husky": "4.3.8",
33 | "lint-staged": "11.1.2",
34 | "npm-run-all": "4.1.5",
35 | "prettier": "2.4.1",
36 | "react": "17.0.2",
37 | "react-dom": "17.0.2",
38 | "react-scripts": "4.0.3",
39 | "stylelint": "13.13.1",
40 | "typescript": "4.4.3",
41 | "web-vitals": "2.1.0"
42 | },
43 | "scripts": {
44 | "start": "cross-env SKIP_PREFLIGHT_CHECK=true react-scripts start",
45 | "build": "cross-env SKIP_PREFLIGHT_CHECK=true react-scripts build",
46 | "lint": "run-s -c lint:*",
47 | "lint:eslint": "eslint src",
48 | "lint:prettier": "prettier --check src",
49 | "format": "run-s format:*",
50 | "format:eslint": "eslint --fix src",
51 | "format:prettier": "prettier --write src",
52 | "test": "cross-env SKIP_PREFLIGHT_CHECK=true react-scripts test",
53 | "eject": "react-scripts eject"
54 | },
55 | "husky": {
56 | "hooks": {
57 | "pre-commit": "lint-staged"
58 | }
59 | },
60 | "lint-staged": {
61 | "*.{js,mjs,jsx,ts,tsx}": "eslint",
62 | "*.{css}": "stylelint"
63 | },
64 | "eslintConfig": {
65 | "root": true,
66 | "extends": "@twihike"
67 | },
68 | "stylelint": {
69 | "extends": "@twihike/stylelint-config"
70 | },
71 | "prettier": "@twihike/prettier-config",
72 | "browserslist": {
73 | "production": [
74 | ">0.2%",
75 | "not dead",
76 | "not op_mini all"
77 | ],
78 | "development": [
79 | "last 1 chrome version",
80 | "last 1 firefox version",
81 | "last 1 safari version"
82 | ]
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/examples/echo-bot/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | chat-ui-react demo
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/examples/echo-bot/src/App.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Box,
3 | Button,
4 | CssBaseline,
5 | Divider,
6 | Link,
7 | ThemeProvider,
8 | Typography,
9 | createTheme,
10 | } from '@mui/material';
11 | import {
12 | ActionRequest,
13 | AudioActionResponse,
14 | ChatController,
15 | FileActionResponse,
16 | MuiChat,
17 | } from 'chat-ui-react';
18 | import React from 'react';
19 |
20 | const muiTheme = createTheme({
21 | palette: {
22 | primary: {
23 | main: '#007aff',
24 | },
25 | },
26 | });
27 |
28 | export function App(): React.ReactElement {
29 | const [chatCtl] = React.useState(
30 | new ChatController({
31 | showDateTime: true,
32 | }),
33 | );
34 |
35 | React.useMemo(() => {
36 | echo(chatCtl);
37 | }, [chatCtl]);
38 |
39 | return (
40 |
41 |
42 |
43 |
54 |
55 | Welcome to{' '}
56 |
57 | chat-ui-react
58 | {' '}
59 | demo site.
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 | );
69 | }
70 |
71 | async function echo(chatCtl: ChatController): Promise {
72 | await chatCtl.addMessage({
73 | type: 'text',
74 | content: `Please enter something.`,
75 | self: false,
76 | avatar: '-',
77 | });
78 | const text = await chatCtl.setActionRequest({
79 | type: 'text',
80 | placeholder: 'Please enter something',
81 | });
82 | await chatCtl.addMessage({
83 | type: 'text',
84 | content: `You have entered:\n${text.value}`,
85 | self: false,
86 | avatar: '-',
87 | });
88 |
89 | await chatCtl.addMessage({
90 | type: 'text',
91 | content: `What is your gender?`,
92 | self: false,
93 | avatar: '-',
94 | });
95 | const sel = await chatCtl.setActionRequest({
96 | type: 'select',
97 | options: [
98 | {
99 | value: 'man',
100 | text: 'Man',
101 | },
102 | {
103 | value: 'woman',
104 | text: 'Woman',
105 | },
106 | {
107 | value: 'other',
108 | text: 'Other',
109 | },
110 | ],
111 | });
112 | await chatCtl.addMessage({
113 | type: 'text',
114 | content: `You have selected ${sel.value}.`,
115 | self: false,
116 | avatar: '-',
117 | });
118 |
119 | await chatCtl.addMessage({
120 | type: 'text',
121 | content: `What is your favorite fruit?`,
122 | self: false,
123 | avatar: '-',
124 | });
125 | const mulSel = await chatCtl.setActionRequest({
126 | type: 'multi-select',
127 | options: [
128 | {
129 | value: 'apple',
130 | text: 'Apple',
131 | },
132 | {
133 | value: 'orange',
134 | text: 'Orange',
135 | },
136 | {
137 | value: 'none',
138 | text: 'None',
139 | },
140 | ],
141 | });
142 | await chatCtl.addMessage({
143 | type: 'text',
144 | content: `You have selected '${mulSel.value}'.`,
145 | self: false,
146 | avatar: '-',
147 | });
148 |
149 | await chatCtl.addMessage({
150 | type: 'text',
151 | content: `What is your favorite picture?`,
152 | self: false,
153 | avatar: '-',
154 | });
155 | const file = (await chatCtl.setActionRequest({
156 | type: 'file',
157 | accept: 'image/*',
158 | multiple: true,
159 | })) as FileActionResponse;
160 | await chatCtl.addMessage({
161 | type: 'jsx',
162 | content: (
163 |
164 | {file.files.map((f) => (
165 |
})
171 | ))}
172 |
173 | ),
174 | self: false,
175 | avatar: '-',
176 | });
177 |
178 | await chatCtl.addMessage({
179 | type: 'text',
180 | content: `Please enter your voice.`,
181 | self: false,
182 | avatar: '-',
183 | });
184 | const audio = (await chatCtl
185 | .setActionRequest({
186 | type: 'audio',
187 | })
188 | .catch(() => ({
189 | type: 'audio',
190 | value: 'Voice input failed.',
191 | avatar: '-',
192 | }))) as AudioActionResponse;
193 | await (audio.audio
194 | ? chatCtl.addMessage({
195 | type: 'jsx',
196 | content: (
197 | Audio downlaod
198 | ),
199 | self: false,
200 | avatar: '-',
201 | })
202 | : chatCtl.addMessage({
203 | type: 'text',
204 | content: audio.value,
205 | self: false,
206 | avatar: '-',
207 | }));
208 |
209 | await chatCtl.addMessage({
210 | type: 'text',
211 | content: `Please press the button.`,
212 | self: false,
213 | avatar: '-',
214 | });
215 | const good = await chatCtl.setActionRequest({
216 | type: 'custom',
217 | Component: GoodInput,
218 | });
219 | await chatCtl.addMessage({
220 | type: 'text',
221 | content: `You have pressed the ${good.value} button.`,
222 | self: false,
223 | avatar: '-',
224 | });
225 |
226 | echo(chatCtl);
227 | }
228 |
229 | function GoodInput({
230 | chatController,
231 | actionRequest,
232 | }: {
233 | chatController: ChatController;
234 | actionRequest: ActionRequest;
235 | }) {
236 | const chatCtl = chatController;
237 |
238 | const setResponse = React.useCallback((): void => {
239 | const res = { type: 'custom', value: 'Good!' };
240 | chatCtl.setActionResponse(actionRequest, res);
241 | }, [actionRequest, chatCtl]);
242 |
243 | return (
244 |
245 |
253 |
254 | );
255 | }
256 |
--------------------------------------------------------------------------------
/examples/echo-bot/src/index.css:
--------------------------------------------------------------------------------
1 | html,
2 | body,
3 | #root {
4 | height: 100%;
5 | }
6 |
--------------------------------------------------------------------------------
/examples/echo-bot/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 |
4 | import { App } from './App';
5 |
6 | import './index.css';
7 |
8 | ReactDOM.render(
9 |
10 |
11 | ,
12 | document.querySelector('#root'),
13 | );
14 |
--------------------------------------------------------------------------------
/examples/echo-bot/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/examples/echo-bot/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "esModuleInterop": true,
12 | "allowSyntheticDefaultImports": true,
13 | "strict": true,
14 | "forceConsistentCasingInFileNames": true,
15 | "noFallthroughCasesInSwitch": true,
16 | "module": "esnext",
17 | "moduleResolution": "node",
18 | "resolveJsonModule": true,
19 | "isolatedModules": true,
20 | "noEmit": true,
21 | "jsx": "react-jsx"
22 | },
23 | "include": [
24 | "src"
25 | ]
26 | }
27 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "chat-ui-react",
3 | "version": "0.3.0",
4 | "description": "React component for conversational UI.",
5 | "keywords": [
6 | "chat",
7 | "bot",
8 | "react",
9 | "material-ui"
10 | ],
11 | "license": "MIT",
12 | "author": "twihike",
13 | "files": [
14 | "dist"
15 | ],
16 | "main": "dist/cjs/index.js",
17 | "browser": "dist/browser/chat-ui-react.umd.min.js",
18 | "module": "dist/esm/index.js",
19 | "types": "dist/types/index.d.ts",
20 | "repository": {
21 | "type": "git",
22 | "url": "https://github.com/twihike/chat-ui-react.git"
23 | },
24 | "scripts": {
25 | "build": "run-s build:*",
26 | "build:clean": "rimraf dist",
27 | "build:types": "tsc -p tsconfig.build.json",
28 | "build:esm": "cross-env NODE_ENV=production BABEL_ENV=esm babel src --out-dir dist/esm --extensions \".ts,.tsx,js,jsx\"",
29 | "build:cjs": "cross-env NODE_ENV=production BABEL_ENV=cjs babel src --out-dir dist/cjs --extensions \".ts,.tsx,js,jsx\"",
30 | "build:browser": "cross-env NODE_ENV=production rollup -c",
31 | "lint": "run-s -c lint:*",
32 | "lint:eslint": "eslint src",
33 | "lint:prettier": "prettier --check src",
34 | "format": "run-s format:*",
35 | "format:eslint": "eslint --fix src",
36 | "format:prettier": "prettier --write src",
37 | "release:version": "standard-version"
38 | },
39 | "dependencies": {
40 | "@babel/runtime-corejs3": "^7.15.4",
41 | "audio-recorder-polyfill": "^0.4.1",
42 | "dayjs": "^1.10.7"
43 | },
44 | "devDependencies": {
45 | "@babel/cli": "^7.15.7",
46 | "@babel/core": "^7.15.5",
47 | "@babel/plugin-proposal-class-properties": "^7.14.5",
48 | "@babel/plugin-proposal-object-rest-spread": "^7.15.6",
49 | "@babel/plugin-transform-runtime": "^7.15.0",
50 | "@babel/preset-env": "^7.15.6",
51 | "@babel/preset-react": "^7.14.5",
52 | "@babel/preset-typescript": "^7.15.0",
53 | "@emotion/react": "^11.5.0",
54 | "@emotion/styled": "^11.3.0",
55 | "@mui/material": "^5.0.4",
56 | "@rollup/plugin-babel": "^5.3.0",
57 | "@rollup/plugin-commonjs": "^20.0.0",
58 | "@rollup/plugin-node-resolve": "^13.0.4",
59 | "@rollup/plugin-replace": "^3.0.0",
60 | "@rollup/plugin-typescript": "^8.2.5",
61 | "@twihike/eslint-config": "^0.1.17",
62 | "@twihike/prettier-config": "^0.1.17",
63 | "@types/dom-mediacapture-record": "^1.0.10",
64 | "@types/node": "^16.9.2",
65 | "@types/react": "^17.0.21",
66 | "@types/react-dom": "^17.0.9",
67 | "@typescript-eslint/eslint-plugin": "^4.31.1",
68 | "cross-env": "^7.0.3",
69 | "eslint": "^7.32.0",
70 | "eslint-plugin-eslint-comments": "^3.2.0",
71 | "eslint-plugin-import": "^2.24.2",
72 | "eslint-plugin-jest": "^24.4.2",
73 | "eslint-plugin-jsx-a11y": "^6.4.1",
74 | "eslint-plugin-node": "^11.1.0",
75 | "eslint-plugin-promise": "^5.1.0",
76 | "eslint-plugin-react": "^7.25.2",
77 | "eslint-plugin-react-hooks": "^4.2.0",
78 | "eslint-plugin-unicorn": "^36.0.0",
79 | "jest": "^27.2.0",
80 | "npm-run-all": "^4.1.5",
81 | "prettier": "^2.4.1",
82 | "rimraf": "^3.0.2",
83 | "rollup": "^2.56.3",
84 | "rollup-plugin-filesize": "^9.1.1",
85 | "rollup-plugin-license": "^2.5.0",
86 | "rollup-plugin-node-license": "^0.2.1",
87 | "rollup-plugin-peer-deps-external": "^2.2.4",
88 | "rollup-plugin-sizes": "^1.0.4",
89 | "rollup-plugin-terser": "^7.0.2",
90 | "rollup-plugin-visualizer": "^5.5.2",
91 | "standard-version": "^9.3.1",
92 | "typescript": "~4.4.3"
93 | },
94 | "peerDependencies": {
95 | "@mui/material": "^5.0.4",
96 | "react": "^17.0.1",
97 | "react-dom": "^17.0.1"
98 | },
99 | "browserslist": {
100 | "production": [
101 | ">0.2%",
102 | "not dead",
103 | "not op_mini all"
104 | ],
105 | "development": [
106 | "last 1 chrome version",
107 | "last 1 firefox version",
108 | "last 1 safari version"
109 | ]
110 | },
111 | "optionalDependencies": {}
112 | }
113 |
--------------------------------------------------------------------------------
/prettier.config.js:
--------------------------------------------------------------------------------
1 | module.exports = '@twihike/prettier-config';
2 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import { DEFAULT_EXTENSIONS } from '@babel/core';
2 | import babel from '@rollup/plugin-babel';
3 | import commonjs from '@rollup/plugin-commonjs';
4 | import resolve from '@rollup/plugin-node-resolve';
5 | import replace from '@rollup/plugin-replace';
6 | import filesize from 'rollup-plugin-filesize';
7 | import license from 'rollup-plugin-node-license';
8 | import peerDepsExternal from 'rollup-plugin-peer-deps-external';
9 | import sizes from 'rollup-plugin-sizes';
10 | import { terser } from 'rollup-plugin-terser';
11 | import { visualizer } from 'rollup-plugin-visualizer';
12 |
13 | import pkg from './package.json';
14 |
15 | const GLOBAL_NAME = 'ChatUiReact';
16 |
17 | const baseGlobals = {
18 | react: 'React',
19 | 'react-dom': 'ReactDOM',
20 | '@mui/material': 'MaterialUI',
21 | };
22 |
23 | const banner = `/*!
24 | * ${pkg.name} v${pkg.version}
25 | *
26 | * Copyright (c) 2020 ${pkg.author}. All rights reserved.
27 | * This source code is licensed under the ${pkg.license} license.
28 | */`;
29 |
30 | const extensions = [...DEFAULT_EXTENSIONS, '.ts', '.tsx'];
31 |
32 | const getBaseConfig = ({ nodeEnv, babelEnv }) => ({
33 | input: 'src/index.ts',
34 | external: [
35 | ...Object.keys(baseGlobals),
36 | ...Object.keys(pkg.devDependencies || {}),
37 | ],
38 | plugins: [
39 | // Resolve node_modules
40 | resolve({
41 | extensions,
42 | }),
43 | peerDepsExternal(),
44 | babel({
45 | envName: babelEnv,
46 | // sourcemaps: 'inline',
47 | exclude: /node_modules/,
48 | extensions,
49 | babelHelpers: 'runtime',
50 | }),
51 | // Convert cjs to esm
52 | commonjs({ include: /node_modules/ }),
53 | replace({
54 | preventAssignment: true,
55 | 'process.env.NODE_ENV': JSON.stringify(nodeEnv),
56 | }),
57 | license({ format: 'jsdoc' }),
58 | filesize(),
59 | sizes(),
60 | process.env.VISUALIZER ? visualizer() : false,
61 | ],
62 | });
63 |
64 | const getConfig = ({ format, nodeEnv, babelEnv, file }) => {
65 | const nodeEnvConfig = {
66 | development: { sourcemap: 'inline', plugins: [] },
67 | production: {
68 | sourcemap: true,
69 | plugins: [
70 | terser({
71 | output: {
72 | comments: (node, comment) => {
73 | const { type, value } = comment;
74 | return type === 'comment2' && /^!/i.test(value);
75 | },
76 | },
77 | }),
78 | ],
79 | },
80 | };
81 | const formatConfig = {
82 | umd: {
83 | name: GLOBAL_NAME,
84 | },
85 | esm: {},
86 | };
87 |
88 | return {
89 | ...getBaseConfig({ nodeEnv, babelEnv }),
90 | output: [
91 | {
92 | file,
93 | format,
94 | banner,
95 | globals: baseGlobals,
96 | ...formatConfig[format],
97 | ...nodeEnvConfig[nodeEnv],
98 | },
99 | ],
100 | };
101 | };
102 |
103 | const getAllConfig = () => {
104 | const args = [
105 | {
106 | format: 'umd',
107 | nodeEnv: 'development',
108 | babelEnv: 'rollupUmd',
109 | file: `dist/browser/${pkg.name}.umd.js`,
110 | },
111 | {
112 | format: 'umd',
113 | nodeEnv: 'production',
114 | babelEnv: 'rollupUmd',
115 | file: `dist/browser/${pkg.name}.umd.min.js`,
116 | },
117 | {
118 | format: 'umd',
119 | nodeEnv: 'development',
120 | babelEnv: 'rollupUmdPolyfill',
121 | file: `dist/browser/${pkg.name}.umd.polyfill.js`,
122 | },
123 | {
124 | format: 'umd',
125 | nodeEnv: 'production',
126 | babelEnv: 'rollupUmdPolyfill',
127 | file: `dist/browser/${pkg.name}.umd.polyfill.min.js`,
128 | },
129 | // {
130 | // format: 'esm',
131 | // nodeEnv: 'development',
132 | // babelEnv: 'rollupEsm',
133 | // file: `dist/browser/${pkg.name}.esm.js`,
134 | // },
135 | // {
136 | // format: 'esm',
137 | // nodeEnv: 'production',
138 | // babelEnv: 'rollupEsm',
139 | // file: `dist/browser/${pkg.name}.esm.min.js`,
140 | // },
141 | // {
142 | // format: 'esm',
143 | // nodeEnv: 'development',
144 | // babelEnv: 'rollupEsmPolyfill',
145 | // file: `dist/browser/${pkg.name}.esm.polyfill.js`,
146 | // },
147 | // {
148 | // format: 'esm',
149 | // nodeEnv: 'production',
150 | // babelEnv: 'rollupEsmPolyfill',
151 | // file: `dist/browser/${pkg.name}.esm.polyfill.min.js`,
152 | // },
153 | ];
154 | return args.map((a) => getConfig(a));
155 | };
156 |
157 | export default getAllConfig();
158 |
--------------------------------------------------------------------------------
/src/audio-media-recorder.ts:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment
2 | // @ts-ignore
3 | import AudioRecorder from 'audio-recorder-polyfill';
4 |
5 | export class AudioMediaRecorder {
6 | private static instance: AudioMediaRecorder;
7 |
8 | static getInstance(): AudioMediaRecorder {
9 | if (!this.instance) {
10 | this.instance = new AudioMediaRecorder();
11 | }
12 |
13 | return this.instance;
14 | }
15 |
16 | private md?: MediaRecorder;
17 |
18 | private recordChunks: Blob[];
19 |
20 | constructor() {
21 | if (!window.MediaRecorder) {
22 | window.MediaRecorder = AudioRecorder;
23 | }
24 | this.recordChunks = [];
25 | }
26 |
27 | async initialize(): Promise {
28 | if (this.md) {
29 | return this;
30 | }
31 |
32 | const stream = await navigator.mediaDevices.getUserMedia({
33 | audio: true,
34 | video: false,
35 | });
36 | this.md = new MediaRecorder(stream);
37 | this.recordChunks = [];
38 |
39 | return this;
40 | }
41 |
42 | async startRecord(): Promise {
43 | return new Promise((resolve) => {
44 | if (!this.md) {
45 | throw new Error('Must be initialized.');
46 | }
47 |
48 | this.recordChunks = [];
49 |
50 | this.md.addEventListener('start', () => {
51 | resolve();
52 | });
53 |
54 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment
55 | // @ts-ignore
56 | this.md.addEventListener('dataavailable', (e: BlobEvent) => {
57 | if (e.data.size > 0) {
58 | this.recordChunks.push(e.data);
59 | }
60 | });
61 |
62 | this.md.start();
63 | });
64 | }
65 |
66 | async stopRecord(): Promise {
67 | return new Promise((resolve) => {
68 | if (!this.md) {
69 | throw new Error('Must be initialized.');
70 | }
71 |
72 | this.md.addEventListener('stop', () => {
73 | resolve(new Blob(this.recordChunks));
74 | });
75 |
76 | this.md.stop();
77 | });
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/src/chat-controller.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ActionRequest,
3 | ActionResponse,
4 | ChatOption,
5 | Message,
6 | MessageContent,
7 | OnActionChanged,
8 | OnActionResponsed,
9 | OnMessagesChanged,
10 | } from './chat-types';
11 |
12 | interface ChatState {
13 | option: ChatOption;
14 | messages: Message[];
15 | action: Action;
16 | actionHistory: Action[];
17 | onMessagesChanged: OnMessagesChanged[];
18 | onActionChanged: OnActionChanged[];
19 | }
20 |
21 | interface Action {
22 | request: ActionRequest;
23 | responses: ActionResponse[];
24 | onResnponsed: OnActionResponsed[];
25 | }
26 |
27 | export class ChatController {
28 | private state: ChatState;
29 |
30 | private defaultOption: ChatOption = {
31 | delay: 300,
32 | };
33 |
34 | private emptyAction: Action = {
35 | request: { type: 'empty' },
36 | responses: [],
37 | onResnponsed: [],
38 | };
39 |
40 | private defaultActionRequest = {
41 | always: false,
42 | addMessage: true,
43 | };
44 |
45 | constructor(option?: ChatOption) {
46 | this.state = {
47 | option: { ...this.defaultOption, ...option },
48 | messages: [],
49 | action: this.emptyAction,
50 | actionHistory: [],
51 | onMessagesChanged: [],
52 | onActionChanged: [],
53 | };
54 | }
55 |
56 | addMessage(message: Message): Promise {
57 | return new Promise((resolve) => {
58 | setTimeout(() => {
59 | const len = this.state.messages.push(message);
60 | const idx = len - 1;
61 | this.state.messages[idx].createdAt = new Date();
62 | this.callOnMessagesChanged();
63 |
64 | resolve(idx);
65 | }, this.state.option.delay);
66 | });
67 | }
68 |
69 | updateMessage(index: number, message: Message): void {
70 | if (message !== this.state.messages[index]) {
71 | const { createdAt } = this.state.messages[index];
72 | this.state.messages[index] = message;
73 | this.state.messages[index].createdAt = createdAt;
74 | }
75 |
76 | this.state.messages[index].updatedAt = new Date();
77 | this.callOnMessagesChanged();
78 | }
79 |
80 | removeMessage(index: number): void {
81 | this.state.messages[index].deletedAt = new Date();
82 | this.callOnMessagesChanged();
83 | }
84 |
85 | getMessages(): Message[] {
86 | return this.state.messages;
87 | }
88 |
89 | setMessages(messages: Message[]): void {
90 | this.clearMessages();
91 | this.state.messages = [...messages];
92 | this.callOnMessagesChanged();
93 | }
94 |
95 | clearMessages(): void {
96 | this.state.messages = [];
97 | this.callOnMessagesChanged();
98 | }
99 |
100 | private callOnMessagesChanged(): void {
101 | this.state.onMessagesChanged.map((h) => h(this.state.messages));
102 | }
103 |
104 | addOnMessagesChanged(callback: OnMessagesChanged): void {
105 | this.state.onMessagesChanged.push(callback);
106 | }
107 |
108 | removeOnMessagesChanged(callback: OnMessagesChanged): void {
109 | const idx = this.state.onMessagesChanged.indexOf(callback);
110 | // eslint-disable-next-line @typescript-eslint/no-empty-function
111 | this.state.onActionChanged[idx] = (): void => {};
112 | }
113 |
114 | setActionRequest(
115 | request: T,
116 | onResponse?: OnActionResponsed,
117 | ): Promise {
118 | const action: Action = {
119 | request: { ...this.defaultActionRequest, ...request },
120 | responses: [],
121 | onResnponsed: [],
122 | };
123 |
124 | // See setActionResponse method
125 | return new Promise((resolve, reject) => {
126 | if (!request.always) {
127 | const returnResponse = (response: ActionResponse): void => {
128 | if (!response.error) {
129 | resolve(response);
130 | } else {
131 | reject(response.error);
132 | }
133 | };
134 | action.onResnponsed.push(returnResponse);
135 | }
136 |
137 | if (onResponse) {
138 | action.onResnponsed.push(onResponse);
139 | }
140 |
141 | this.state.action = action;
142 | this.state.actionHistory.push(action);
143 | this.callOnActionChanged(action.request);
144 |
145 | if (request.always) {
146 | resolve({ type: 'text', value: 'dummy' });
147 | }
148 | });
149 | }
150 |
151 | cancelActionRequest(): void {
152 | this.state.action = this.emptyAction;
153 | this.callOnActionChanged(this.emptyAction.request);
154 | }
155 |
156 | getActionRequest(): ActionRequest | undefined {
157 | const { request, responses } = this.state.action;
158 | if (!request.always && responses.length > 0) {
159 | return undefined;
160 | }
161 |
162 | return request;
163 | }
164 |
165 | async setActionResponse(
166 | request: ActionRequest,
167 | response: ActionResponse,
168 | ): Promise {
169 | const { request: origReq, responses, onResnponsed } = this.state.action;
170 | if (request !== origReq) {
171 | throw new Error('Invalid action.');
172 | }
173 | if (!request.always && onResnponsed.length === 0) {
174 | throw new Error('onResponsed is not set.');
175 | }
176 |
177 | responses.push(response);
178 | this.callOnActionChanged(request, response);
179 |
180 | if (request.addMessage) {
181 | await this.addMessage({
182 | type: 'text',
183 | content: response.value,
184 | self: true,
185 | });
186 | }
187 |
188 | onResnponsed.map((h) => h(response));
189 | }
190 |
191 | getActionResponses(): ActionResponse[] {
192 | return this.state.action.responses;
193 | }
194 |
195 | private callOnActionChanged(
196 | request: ActionRequest,
197 | response?: ActionResponse,
198 | ): void {
199 | this.state.onActionChanged.map((h) => h(request, response));
200 | }
201 |
202 | addOnActionChanged(callback: OnActionChanged): void {
203 | this.state.onActionChanged.push(callback);
204 | }
205 |
206 | removeOnActionChanged(callback: OnActionChanged): void {
207 | const idx = this.state.onActionChanged.indexOf(callback);
208 | // eslint-disable-next-line @typescript-eslint/no-empty-function
209 | this.state.onActionChanged[idx] = (): void => {};
210 | }
211 |
212 | getOption(): ChatOption {
213 | return this.state.option;
214 | }
215 | }
216 |
--------------------------------------------------------------------------------
/src/chat-types.ts:
--------------------------------------------------------------------------------
1 | export interface ChatOption {
2 | delay?: number;
3 | showDateTime?: boolean;
4 | }
5 |
6 | export interface Message {
7 | type: string;
8 | content: C;
9 | self: boolean;
10 | username?: string;
11 | avatar?: string;
12 | createdAt?: Date;
13 | updatedAt?: Date;
14 | deletedAt?: Date;
15 | }
16 |
17 | export type MessageContent = string | JSX.Element;
18 |
19 | export interface TextMessage extends Message {
20 | type: 'text';
21 | content: string;
22 | }
23 |
24 | export interface JSXMessage extends Message {
25 | type: 'jsx';
26 | content: JSX.Element;
27 | }
28 |
29 | export interface ActionRequest {
30 | type: string;
31 | always?: boolean;
32 | addMessage?: boolean;
33 | response?: ActionResponse;
34 | }
35 |
36 | export interface TextActionRequest extends ActionRequest {
37 | type: 'text';
38 | defaultValue?: string;
39 | placeholder?: string;
40 | sendButtonText?: string;
41 | response?: TextActionResponse;
42 | }
43 |
44 | export interface SelectActionRequest extends ActionRequest {
45 | type: 'select';
46 | options: {
47 | value: string;
48 | text: string;
49 | }[];
50 | response?: SelectActionResponse;
51 | }
52 |
53 | export interface MultiSelectActionRequest extends ActionRequest {
54 | type: 'multi-select';
55 | options: {
56 | value: string;
57 | text: string;
58 | }[];
59 | sendButtonText?: string;
60 | response?: MultiSelectActionResponse;
61 | }
62 |
63 | export interface FileActionRequest extends ActionRequest {
64 | type: 'file';
65 | accept?: string;
66 | multiple?: boolean;
67 | response?: FileActionResponse;
68 | sendButtonText?: string;
69 | }
70 |
71 | export interface AudioActionRequest extends ActionRequest {
72 | type: 'audio';
73 | sendButtonText?: string;
74 | response?: AudioActionResponse;
75 | }
76 |
77 | export interface CustomActionRequest extends ActionRequest {
78 | type: 'custom';
79 | Component: JSX.Element;
80 | response?: CustomActionResponse;
81 | }
82 |
83 | export interface ActionResponse {
84 | type: string;
85 | value: string;
86 | error?: Error;
87 | }
88 |
89 | export interface TextActionResponse extends ActionResponse {
90 | type: 'text';
91 | }
92 |
93 | export interface SelectActionResponse extends ActionResponse {
94 | type: 'select';
95 | option: {
96 | value: string;
97 | text: string;
98 | };
99 | }
100 |
101 | export interface MultiSelectActionResponse extends ActionResponse {
102 | type: 'multi-select';
103 | options: {
104 | value: string;
105 | text: string;
106 | }[];
107 | }
108 |
109 | export interface FileActionResponse extends ActionResponse {
110 | type: 'file';
111 | files: File[];
112 | }
113 |
114 | export interface AudioActionResponse extends ActionResponse {
115 | type: 'audio';
116 | audio?: Blob;
117 | }
118 |
119 | export interface CustomActionResponse extends ActionResponse {
120 | type: 'custom';
121 | }
122 |
123 | export interface OnMessagesChanged {
124 | (messages: Message[]): void;
125 | }
126 |
127 | export interface OnActionChanged {
128 | (request: ActionRequest, response?: ActionResponse): void;
129 | }
130 |
131 | export interface OnActionResponsed {
132 | (response: ActionResponse): void;
133 | }
134 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './chat-types';
2 | export { ChatController } from './chat-controller';
3 | export { AudioMediaRecorder } from './audio-media-recorder';
4 | export { MuiChat } from './mui/MuiChat';
5 | export { MuiMessage } from './mui/MuiMessage';
6 | export { MuiTextInput } from './mui/MuiTextInput';
7 | export { MuiSelectInput } from './mui/MuiSelectInput';
8 | export { MuiMultiSelectInput } from './mui/MuiMultiSelectInput';
9 | export { MuiFileInput } from './mui/MuiFileInput';
10 | export { MuiAudioInput } from './mui/MuiAudioInput';
11 |
--------------------------------------------------------------------------------
/src/mui/MuiAudioInput.tsx:
--------------------------------------------------------------------------------
1 | import { Box, Button, Icon } from '@mui/material';
2 | import React from 'react';
3 |
4 | import { AudioMediaRecorder } from '../audio-media-recorder';
5 | import { ChatController } from '../chat-controller';
6 | import { AudioActionRequest, AudioActionResponse } from '../chat-types';
7 |
8 | export function MuiAudioInput({
9 | chatController,
10 | actionRequest,
11 | }: {
12 | chatController: ChatController;
13 | actionRequest: AudioActionRequest;
14 | }): React.ReactElement {
15 | const chatCtl = chatController;
16 | const [audioRec] = React.useState(AudioMediaRecorder.getInstance());
17 | const [stopped, setStopped] = React.useState(true);
18 | const [audio, setAudio] = React.useState();
19 |
20 | const handleError = React.useCallback(
21 | (error: Error): void => {
22 | const value: AudioActionResponse = {
23 | type: 'audio',
24 | value: error.message,
25 | error,
26 | };
27 | chatCtl.setActionResponse(actionRequest, value);
28 | },
29 | [actionRequest, chatCtl],
30 | );
31 |
32 | const handleStart = React.useCallback(async (): Promise => {
33 | try {
34 | await audioRec.initialize();
35 | await audioRec.startRecord();
36 | setStopped(false);
37 | } catch (error) {
38 | handleError(error as Error);
39 | }
40 | }, [audioRec, handleError]);
41 |
42 | const handleStop = React.useCallback(async (): Promise => {
43 | try {
44 | const a = await audioRec.stopRecord();
45 | setAudio(a);
46 | setStopped(true);
47 | } catch (error) {
48 | handleError(error as Error);
49 | }
50 | }, [audioRec, handleError]);
51 |
52 | const sendResponse = React.useCallback((): void => {
53 | if (audio) {
54 | const value: AudioActionResponse = {
55 | type: 'audio',
56 | value: 'Audio',
57 | audio,
58 | };
59 | chatCtl.setActionResponse(actionRequest, value);
60 | setAudio(undefined);
61 | }
62 | }, [actionRequest, audio, chatCtl]);
63 |
64 | const sendButtonText = actionRequest.sendButtonText
65 | ? actionRequest.sendButtonText
66 | : 'Send';
67 |
68 | return (
69 | *': {
74 | flex: '1 1 auto',
75 | minWidth: 0,
76 | },
77 | '& > * + *': {
78 | ml: 1,
79 | },
80 | }}
81 | >
82 | {stopped && (
83 |
93 | )}
94 | {!stopped && (
95 |
105 | )}
106 |
116 |
117 | );
118 | }
119 |
--------------------------------------------------------------------------------
/src/mui/MuiChat.tsx:
--------------------------------------------------------------------------------
1 | import { Box } from '@mui/material';
2 | import dayjs from 'dayjs';
3 | import React from 'react';
4 |
5 | import { ChatController } from '../chat-controller';
6 | import {
7 | ActionRequest,
8 | AudioActionRequest,
9 | CustomActionRequest,
10 | FileActionRequest,
11 | MultiSelectActionRequest,
12 | SelectActionRequest,
13 | TextActionRequest,
14 | } from '../chat-types';
15 |
16 | import { MuiAudioInput } from './MuiAudioInput';
17 | import { MuiFileInput } from './MuiFileInput';
18 | import { MuiMessage } from './MuiMessage';
19 | import { MuiMultiSelectInput } from './MuiMultiSelectInput';
20 | import { MuiSelectInput } from './MuiSelectInput';
21 | import { MuiTextInput } from './MuiTextInput';
22 |
23 | export function MuiChat({
24 | chatController,
25 | }: React.PropsWithChildren<{
26 | chatController: ChatController;
27 | }>): React.ReactElement {
28 | const chatCtl = chatController;
29 | const [messages, setMessages] = React.useState(chatCtl.getMessages());
30 | const [actReq, setActReq] = React.useState(chatCtl.getActionRequest());
31 |
32 | const msgRef = React.useRef(null);
33 | const scroll = React.useCallback((): void => {
34 | if (msgRef.current) {
35 | msgRef.current.scrollTop = msgRef.current.scrollHeight;
36 | // msgRef.current.scrollIntoView(true);
37 | }
38 | }, [msgRef]);
39 | React.useEffect(() => {
40 | function handleMassagesChanged(): void {
41 | setMessages([...chatCtl.getMessages()]);
42 | scroll();
43 | }
44 | function handleActionChanged(): void {
45 | setActReq(chatCtl.getActionRequest());
46 | scroll();
47 | }
48 | chatCtl.addOnMessagesChanged(handleMassagesChanged);
49 | chatCtl.addOnActionChanged(handleActionChanged);
50 | }, [chatCtl, scroll]);
51 |
52 | type CustomComponentType = React.FC<{
53 | chatController: ChatController;
54 | actionRequest: ActionRequest;
55 | }>;
56 | const CustomComponent = React.useMemo((): CustomComponentType => {
57 | if (!actReq || actReq.type !== 'custom') {
58 | return null as unknown as CustomComponentType;
59 | }
60 | return (actReq as CustomActionRequest)
61 | .Component as unknown as CustomComponentType;
62 | }, [actReq]);
63 |
64 | const unknownMsg = {
65 | type: 'text',
66 | content: 'Unknown message.',
67 | self: false,
68 | };
69 |
70 | let prevDate = dayjs(0);
71 | let prevTime = dayjs(0);
72 |
73 | return (
74 | *': {
83 | maxWidth: '100%',
84 | },
85 | '& > * + *': {
86 | mt: 1,
87 | },
88 | }}
89 | >
90 | *': {
98 | maxWidth: '100%',
99 | },
100 | }}
101 | ref={msgRef}
102 | >
103 | {messages.map((msg): React.ReactElement => {
104 | let showDate = false;
105 | let showTime = !!chatCtl.getOption().showDateTime;
106 | if (!!chatCtl.getOption().showDateTime && !msg.deletedAt) {
107 | const current = dayjs(
108 | msg.updatedAt ? msg.updatedAt : msg.createdAt,
109 | );
110 |
111 | if (current.format('YYYYMMDD') !== prevDate.format('YYYYMMDD')) {
112 | showDate = true;
113 | }
114 | prevDate = current;
115 |
116 | if (current.diff(prevTime) < 60_000) {
117 | showTime = false;
118 | } else {
119 | prevTime = current;
120 | }
121 | }
122 | if (msg.type === 'text' || msg.type === 'jsx') {
123 | return (
124 |
131 | );
132 | }
133 | return (
134 |
141 | );
142 | })}
143 |
144 | *': {
150 | minWidth: 0,
151 | },
152 | }}
153 | >
154 | {actReq && actReq.type === 'text' && (
155 |
159 | )}
160 | {actReq && actReq.type === 'select' && (
161 |
165 | )}
166 | {actReq && actReq.type === 'multi-select' && (
167 |
171 | )}
172 | {actReq && actReq.type === 'file' && (
173 |
177 | )}
178 | {actReq && actReq.type === 'audio' && (
179 |
183 | )}
184 | {actReq && actReq.type === 'custom' && (
185 |
189 | )}
190 |
191 |
192 | );
193 | }
194 |
--------------------------------------------------------------------------------
/src/mui/MuiFileInput.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Box,
3 | Button,
4 | Divider,
5 | Icon,
6 | List,
7 | ListItem,
8 | ListItemIcon,
9 | Typography,
10 | } from '@mui/material';
11 | import React from 'react';
12 |
13 | import { ChatController } from '../chat-controller';
14 | import { FileActionRequest, FileActionResponse } from '../chat-types';
15 |
16 | export function MuiFileInput({
17 | chatController,
18 | actionRequest,
19 | }: {
20 | chatController: ChatController;
21 | actionRequest: FileActionRequest;
22 | }): React.ReactElement {
23 | const chatCtl = chatController;
24 | const [files, setFiles] = React.useState([]);
25 |
26 | const handleFileChange = React.useCallback(
27 | (fileList: FileList | null): void => {
28 | // Convert FileList to File[]
29 | const fileArray: File[] = [];
30 | if (fileList) {
31 | for (let i = 0; i < fileList.length; i += 1) {
32 | const file = fileList.item(i);
33 | if (file) {
34 | fileArray.push(file);
35 | }
36 | }
37 | }
38 | setFiles(fileArray);
39 | },
40 | [],
41 | );
42 |
43 | const setResponse = React.useCallback((): void => {
44 | if (files.length > 0) {
45 | const value = files.map((f) => f.name).toString();
46 | const res: FileActionResponse = { type: 'file', value, files };
47 | chatCtl.setActionResponse(actionRequest, res);
48 | }
49 | }, [actionRequest, chatCtl, files]);
50 |
51 | const sendButtonText = actionRequest.sendButtonText
52 | ? actionRequest.sendButtonText
53 | : 'Send';
54 |
55 | return (
56 | *': {
63 | flex: '0 0 auto',
64 | maxWidth: '100%',
65 | },
66 | '& > * + *': {
67 | mt: 1,
68 | },
69 | }}
70 | >
71 |
72 | {files.map((f) => (
73 |
74 |
75 |
76 |
77 | attach_file
78 |
79 |
80 | {f.name}
81 |
82 | {/* */}
83 |
84 |
85 | ))}
86 |
87 | *': {
91 | flex: '1 1 auto',
92 | minWidth: 0,
93 | },
94 | '& > * + *': {
95 | ml: 1,
96 | },
97 | }}
98 | >
99 |
115 |
125 |
126 |
127 | );
128 | }
129 |
--------------------------------------------------------------------------------
/src/mui/MuiMessage.tsx:
--------------------------------------------------------------------------------
1 | import { Avatar, Box, Grow, Typography } from '@mui/material';
2 | import React from 'react';
3 |
4 | import { Message, MessageContent } from '../chat-types';
5 |
6 | export function MuiMessage({
7 | id,
8 | message,
9 | showDate,
10 | showTime,
11 | }: {
12 | id: string;
13 | message: Message;
14 | showDate: boolean;
15 | showTime: boolean;
16 | }): React.ReactElement {
17 | if (message.deletedAt) {
18 | return ;
19 | }
20 |
21 | const dispDate = message.updatedAt ? message.updatedAt : message.createdAt;
22 |
23 | const ChatAvator = (
24 |
30 |
31 |
32 | );
33 |
34 | const ChatUsername = (
35 |
36 |
37 | {message.username}
38 |
39 |
40 | );
41 |
42 | const ChatDate = (
43 |
44 |
49 | {dispDate?.toLocaleTimeString([], {
50 | hour: '2-digit',
51 | minute: '2-digit',
52 | })}
53 |
54 |
55 | );
56 |
57 | return (
58 |
59 |
60 | {showDate && (
61 |
62 | {dispDate?.toLocaleDateString()}
63 |
64 | )}
65 |
75 | {message.avatar && !message.self && ChatAvator}
76 |
77 | {message.username && ChatUsername}
78 |
87 | {message.type === 'text' && (
88 |
89 | {message.content}
90 |
91 | )}
92 | {message.type === 'jsx' && {message.content}
}
93 |
94 | {showTime && ChatDate}
95 |
96 | {message.avatar && message.self && ChatAvator}
97 |
98 |
99 |
100 | );
101 | }
102 |
--------------------------------------------------------------------------------
/src/mui/MuiMultiSelectInput.tsx:
--------------------------------------------------------------------------------
1 | import { Box, Button, Icon } from '@mui/material';
2 | import React from 'react';
3 |
4 | import { ChatController } from '../chat-controller';
5 | import {
6 | MultiSelectActionRequest,
7 | MultiSelectActionResponse,
8 | } from '../chat-types';
9 |
10 | export function MuiMultiSelectInput({
11 | chatController,
12 | actionRequest,
13 | }: {
14 | chatController: ChatController;
15 | actionRequest: MultiSelectActionRequest;
16 | }): React.ReactElement {
17 | const chatCtl = chatController;
18 | const [values, setValues] = React.useState([]);
19 |
20 | const handleSelect = React.useCallback(
21 | (value: string): void => {
22 | if (!values.includes(value)) {
23 | setValues([...values, value]);
24 | } else {
25 | setValues(values.filter((v) => v !== value));
26 | }
27 | },
28 | [values],
29 | );
30 |
31 | const setResponse = React.useCallback((): void => {
32 | const options = actionRequest.options.filter((o) =>
33 | values.includes(o.value),
34 | );
35 |
36 | const res: MultiSelectActionResponse = {
37 | type: 'multi-select',
38 | value: options.map((o) => o.text).toString(),
39 | options,
40 | };
41 | chatCtl.setActionResponse(actionRequest, res);
42 | setValues([]);
43 | }, [actionRequest, chatCtl, values]);
44 |
45 | const sendButtonText = actionRequest.sendButtonText
46 | ? actionRequest.sendButtonText
47 | : 'Send';
48 |
49 | return (
50 | *': {
56 | flex: '0 0 auto',
57 | maxWidth: '100%',
58 | },
59 | '& > * + *': {
60 | mt: 1,
61 | },
62 | }}
63 | >
64 | {actionRequest.options.map((o) => (
65 |
75 | ))}
76 |
86 |
87 | );
88 | }
89 |
--------------------------------------------------------------------------------
/src/mui/MuiSelectInput.tsx:
--------------------------------------------------------------------------------
1 | import { Box, Button } from '@mui/material';
2 | import React from 'react';
3 |
4 | import { ChatController } from '../chat-controller';
5 | import { SelectActionRequest, SelectActionResponse } from '../chat-types';
6 |
7 | export function MuiSelectInput({
8 | chatController,
9 | actionRequest,
10 | }: {
11 | chatController: ChatController;
12 | actionRequest: SelectActionRequest;
13 | }): React.ReactElement {
14 | const chatCtl = chatController;
15 |
16 | const setResponse = React.useCallback(
17 | (value: string): void => {
18 | const option = actionRequest.options.find((o) => o.value === value);
19 | if (!option) {
20 | throw new Error(`Unknown value: ${value}`);
21 | }
22 | const res: SelectActionResponse = {
23 | type: 'select',
24 | value: option.text,
25 | option,
26 | };
27 | chatCtl.setActionResponse(actionRequest, res);
28 | },
29 | [actionRequest, chatCtl],
30 | );
31 |
32 | return (
33 | *': {
39 | flex: '0 0 auto',
40 | maxWidth: '100%',
41 | },
42 | '& > * + *': {
43 | mt: 1,
44 | },
45 | }}
46 | >
47 | {actionRequest.options.map((o) => (
48 |
58 | ))}
59 |
60 | );
61 | }
62 |
--------------------------------------------------------------------------------
/src/mui/MuiTextInput.tsx:
--------------------------------------------------------------------------------
1 | import { Box, Button, Icon, TextField } from '@mui/material';
2 | import React from 'react';
3 |
4 | import { ChatController } from '../chat-controller';
5 | import { TextActionRequest, TextActionResponse } from '../chat-types';
6 |
7 | export function MuiTextInput({
8 | chatController,
9 | actionRequest,
10 | }: {
11 | chatController: ChatController;
12 | actionRequest: TextActionRequest;
13 | }): React.ReactElement {
14 | const chatCtl = chatController;
15 | const [value, setValue] = React.useState(actionRequest.defaultValue);
16 |
17 | const setResponse = React.useCallback((): void => {
18 | if (value) {
19 | const res: TextActionResponse = { type: 'text', value };
20 | chatCtl.setActionResponse(actionRequest, res);
21 | setValue('');
22 | }
23 | }, [actionRequest, chatCtl, value]);
24 |
25 | const handleKeyDown = React.useCallback(
26 | (e: React.KeyboardEvent): void => {
27 | if (e.nativeEvent.isComposing) {
28 | return;
29 | }
30 |
31 | if (e.key === 'Enter' && !e.shiftKey) {
32 | e.preventDefault();
33 | setResponse();
34 | }
35 | },
36 | [setResponse],
37 | );
38 |
39 | const sendButtonText = actionRequest.sendButtonText
40 | ? actionRequest.sendButtonText
41 | : 'Send';
42 |
43 | return (
44 | *': {
49 | flex: '1 1 auto',
50 | minWidth: 0,
51 | },
52 | '& > * + *': {
53 | ml: 1,
54 | },
55 | '& :last-child': {
56 | flex: '0 1 auto',
57 | },
58 | }}
59 | >
60 | setValue(e.target.value)}
64 | autoFocus
65 | multiline
66 | inputProps={{ onKeyDown: handleKeyDown }}
67 | variant="outlined"
68 | maxRows={10}
69 | />
70 |
80 |
81 | );
82 | }
83 |
--------------------------------------------------------------------------------
/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "declaration": true,
5 | "noEmit": false,
6 | "emitDeclarationOnly": true,
7 | "outDir": "./dist/types",
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "esModuleInterop": true,
12 | "allowSyntheticDefaultImports": true,
13 | "strict": true,
14 | "forceConsistentCasingInFileNames": true,
15 | "module": "esnext",
16 | "moduleResolution": "node",
17 | "resolveJsonModule": true,
18 | "isolatedModules": true,
19 | "noEmit": true,
20 | "jsx": "react"
21 | },
22 | "include": [
23 | "src"
24 | ]
25 | }
26 |
--------------------------------------------------------------------------------