├── .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 | [![npm version](https://badge.fury.io/js/chat-ui-react.svg)](https://badge.fury.io/js/chat-ui-react) [![ci](https://github.com/twihike/chat-ui-react/workflows/ci/badge.svg)](https://github.com/twihike/chat-ui-react/actions) [![release](https://github.com/twihike/chat-ui-react/workflows/release/badge.svg)](https://github.com/twihike/chat-ui-react/actions) [![license](https://img.shields.io/github/license/twihike/chat-ui-react)](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 | ![demo](https://raw.githubusercontent.com/twihike/chat-ui-react/assets/chat-ui-react-demo.gif) 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 | [![npm version](https://badge.fury.io/js/chat-ui-react.svg)](https://badge.fury.io/js/chat-ui-react) [![ci](https://github.com/twihike/chat-ui-react/workflows/ci/badge.svg)](https://github.com/twihike/chat-ui-react/actions) [![release](https://github.com/twihike/chat-ui-react/workflows/release/badge.svg)](https://github.com/twihike/chat-ui-react/actions) [![license](https://img.shields.io/github/license/twihike/chat-ui-react)](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 | ![demo](https://raw.githubusercontent.com/twihike/chat-ui-react/assets/chat-ui-react-demo.gif) 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 | File 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 | --------------------------------------------------------------------------------