From ba6f98ec4cef655963863b767cfdc328642954b3 Mon Sep 17 00:00:00 2001 From: William Lee <43682783+wlee221@users.noreply.github.com> Date: Mon, 31 Aug 2020 14:16:32 -0700 Subject: [PATCH] feat(@aws-amplify/ui-components): Add Chatbot UI to main (#6684) * feat(@aws-amplify/ui-components): Add ChatBot Component Fixes: #5024 * amplify-chatbot initial import * Use interface and comment out test * Expose additional css variables and add icon variant to button * Update snapshot * Clean up code * Remove unused test case * Add snapshot testing * Apply comments from @ashika01 * Rename --icon-color to --icon-fill * Remove unused class css * Update css for compatibility with existing components * Set default height * Integrate Interactions text message * Update snapshots * Simplify code * Add audiorecorder and integrate voice chat * Use interface over type * Reorder functions and add byte descriptions * Add loading animation * Update interaction types * Scroll to bottom * set methods private * Rename css class * Update snapshot * Add error handling and reorder functions * Refactor error handling * Refactor chatbot functions * Cleanup * Update snapshot * Expose width css variable from amplify-button * px to rem * Expose width and height variable; Control height at top level * Add header slot * Add listening animation * Cleanup * Update angular module * Move visualization to helper and downsample data array * Separate animation scss * Remove console.logs * Control width / height at host; expose message colors * Use I18n with strings * Fix typo * Use enum for chat state * Revert width back to 100% * Rename updateProps to validateProps * Separate out interaction enum strings * Move MIME type string to constants file * Use async/await pattern in recorder.ts * Check isBrowser and add silence props * Separate init from recorder for async control * Remove fieldId * Add try catch around Interactions.send * Remove requestId * Update snapshot * Expose Interactions types * Remove duplicate logic * Use enum to describe where the message is from * Clean up css and set enum value * Add slot description * Simplify import * Default noop to visualizer * Comment AudioRecorder and separate constants * Update snapshot * Reorder css * Enable conversationModeOn prop * Update packages/amplify-ui-components/src/common/audio-control/helper.ts Co-authored-by: Ashika <35131273+ashika01@users.noreply.github.com> * Move error strings to translations * Remove trailing comma * Wrap audioContext resume with error logger * Try catch `resume` and make startRecording async * Use callback based decode for safari Co-authored-by: Ashika <35131273+ashika01@users.noreply.github.com> * ci: enable preview release from ui-components/main (#6648) * Enable publish from ui-preview branch * Revert checkout Co-authored-by: Jordan Ranz * fix(@aws-amplify/ui-components): Update scss and reset chat state upon finish (#6652) * Move width/height control to container level * Add min-height to footer * Reset chat state upon session finish * Remove trailing comma * Handle error based on whether it's recoverable or not * Put a different placeholder if only voice is enabled * Make dot color customizable * Fix typo in translations * Let users stop audio and remove speaking chat state * Add --amplify-blue as bg chat color * Remove console.error * ci: add interactions integ test (#6678) Co-authored-by: Ashika <35131273+ashika01@users.noreply.github.com> Co-authored-by: Jordan Ranz --- .circleci/config.yml | 59 ++- package.json | 10 +- .../amplify-ui-angular/src/amplify-module.ts | 2 + packages/amplify-ui-components/Readme.md | 1 + .../src/common/Translations.ts | 12 +- .../src/common/audio-control/helper.ts | 127 ++++++ .../src/common/audio-control/index.ts | 3 + .../src/common/audio-control/recorder.ts | 221 ++++++++++ .../src/common/audio-control/settings.ts | 8 + .../src/common/audio-control/visualizer.ts | 44 ++ .../src/common/constants.ts | 2 + .../src/common/types/interactions-types.ts | 4 + .../src/common/types/ui-types.ts | 2 +- .../amplify-ui-components/src/components.d.ts | 100 ++++- .../amplify-authenticator/readme.md | 1 + .../amplify-button/amplify-button.scss | 20 +- .../amplify-button/amplify-button.tsx | 4 + .../src/components/amplify-button/readme.md | 20 +- .../amplify-chatbot.spec.tsx.snap | 21 + .../amplify-chatbot/amplify-chatbot.scss | 88 ++++ .../amplify-chatbot/amplify-chatbot.spec.tsx | 12 + .../amplify-chatbot/amplify-chatbot.tsx | 382 ++++++++++++++++++ .../components/amplify-chatbot/animation.scss | 48 +++ .../src/components/amplify-chatbot/readme.md | 58 +++ .../amplify-confirm-sign-in/readme.md | 1 + .../amplify-confirm-sign-up/readme.md | 1 + .../amplify-federated-sign-in/readme.md | 1 + .../amplify-forgot-password/readme.md | 1 + .../components/amplify-form-section/readme.md | 1 + .../components/amplify-greetings/readme.md | 1 + .../amplify-icon-button.spec.ts.snap | 42 ++ .../components/amplify-icon-button/readme.md | 10 +- .../__snapshots__/amplify-icon.spec.ts.snap | 192 +++++---- .../components/amplify-icon/amplify-icon.scss | 6 +- .../components/amplify-icon/amplify-icon.tsx | 4 +- .../src/components/amplify-icon/icons.tsx | 24 ++ .../src/components/amplify-icon/readme.md | 8 +- .../amplify-input/amplify-input.scss | 6 +- .../src/components/amplify-input/readme.md | 2 + .../components/amplify-photo-picker/readme.md | 1 + .../src/components/amplify-picker/readme.md | 1 + .../amplify-require-new-password/readme.md | 1 + .../src/components/amplify-s3-album/readme.md | 1 + .../amplify-s3-image-picker/readme.md | 1 + .../amplify-s3-text-picker/readme.md | 1 + .../amplify-select-mfa-type/readme.md | 1 + .../src/components/amplify-sign-in/readme.md | 1 + .../src/components/amplify-sign-out/readme.md | 1 + .../src/components/amplify-sign-up/readme.md | 1 + .../src/components/amplify-toast/readme.md | 2 + .../components/amplify-totp-setup/readme.md | 1 + .../amplify-verify-contact/readme.md | 1 + .../amplify-ui-components/src/global/theme.ts | 2 +- .../amplify-ui-components/stencil.config.ts | 8 +- .../src/Interactions/ChatBot.tsx | 1 - packages/interactions/src/Interactions.ts | 7 +- packages/interactions/src/index.ts | 1 + 57 files changed, 1452 insertions(+), 130 deletions(-) create mode 100644 packages/amplify-ui-components/src/common/audio-control/helper.ts create mode 100644 packages/amplify-ui-components/src/common/audio-control/index.ts create mode 100644 packages/amplify-ui-components/src/common/audio-control/recorder.ts create mode 100644 packages/amplify-ui-components/src/common/audio-control/settings.ts create mode 100644 packages/amplify-ui-components/src/common/audio-control/visualizer.ts create mode 100644 packages/amplify-ui-components/src/common/types/interactions-types.ts create mode 100644 packages/amplify-ui-components/src/components/amplify-chatbot/__snapshots__/amplify-chatbot.spec.tsx.snap create mode 100644 packages/amplify-ui-components/src/components/amplify-chatbot/amplify-chatbot.scss create mode 100644 packages/amplify-ui-components/src/components/amplify-chatbot/amplify-chatbot.spec.tsx create mode 100644 packages/amplify-ui-components/src/components/amplify-chatbot/amplify-chatbot.tsx create mode 100644 packages/amplify-ui-components/src/components/amplify-chatbot/animation.scss create mode 100644 packages/amplify-ui-components/src/components/amplify-chatbot/readme.md diff --git a/.circleci/config.yml b/.circleci/config.yml index e2492f84f44..83a0eae3f18 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -412,6 +412,45 @@ jobs: sample_name: multi-user-translation spec: multiuser-translation + integ_react_interactions: + executor: js-test-executor + <<: *test_env_vars + working_directory: ~/amplify-js-samples-staging/samples/react/interactions/chatbot-component + steps: + - prepare_test_env + - integ_test_js: + test_name: 'React Interactions' + framework: react + category: interactions + sample_name: chatbot-component + spec: chatbot-component + + integ_vue_interactions: + executor: js-test-executor + <<: *test_env_vars + working_directory: ~/amplify-js-samples-staging/samples/vue/interactions/chatbot-component + steps: + - prepare_test_env + - integ_test_js: + test_name: 'Vue Interactions' + framework: vue + category: interactions + sample_name: chatbot-component + spec: chatbot-component + + integ_angular_interactions: + executor: js-test-executor + <<: *test_env_vars + working_directory: ~/amplify-js-samples-staging/samples/angular/interactions/chatbot-component + steps: + - prepare_test_env + - integ_test_js: + test_name: 'Angular Interactions' + framework: angular + category: interactions + sample_name: chatbot-component + spec: chatbot-component + integ_react_datastore: executor: js-test-executor <<: *test_env_vars @@ -618,7 +657,7 @@ releasable_branches: &releasable_branches only: - release - main - - ui-components/master + - ui-components/main - 1.0-stable workflows: @@ -658,6 +697,24 @@ workflows: - build filters: <<: *releasable_branches + - integ_react_interactions: + requires: + - integ_setup + - build + filters: + <<: *releasable_branches + - integ_vue_interactions: + requires: + - integ_setup + - build + filters: + <<: *releasable_branches + - integ_angular_interactions: + requires: + - integ_setup + - build + filters: + <<: *releasable_branches - integ_react_datastore: requires: - integ_setup diff --git a/package.json b/package.json index b716b594169..a651c789bb0 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "publish:beta": "lerna publish --canary --force-publish \"*\" --yes --dist-tag=beta --preid=beta --exact", "publish:release": "lerna publish --conventional-commits --yes --message 'chore(release): Publish [ci skip]'", "publish:1.0-stable": "lerna publish --conventional-commits --yes --dist-tag=stable-1.0 --message 'chore(release): Publish [ci skip]'", + "publish:ui-components/main": "lerna publish --canary --force-publish \"*\" --yes --dist-tag=ui-preview --preid=ui-preview --exact", "publish:verdaccio": "lerna publish --no-push --canary minor --dist-tag=unstable --preid=unstable --exact --force-publish --yes" }, "husky": { @@ -31,8 +32,13 @@ } }, "workspaces": { - "packages": ["packages/*"], - "nohoist": ["**/@types/react-native", "**/@types/react-native/**"] + "packages": [ + "packages/*" + ], + "nohoist": [ + "**/@types/react-native", + "**/@types/react-native/**" + ] }, "repository": { "type": "git", diff --git a/packages/amplify-ui-angular/src/amplify-module.ts b/packages/amplify-ui-angular/src/amplify-module.ts index e251759d55c..370b7826c5a 100644 --- a/packages/amplify-ui-angular/src/amplify-module.ts +++ b/packages/amplify-ui-angular/src/amplify-module.ts @@ -7,6 +7,7 @@ import { AmplifyAuthenticator, AmplifyAuthFields, AmplifyButton, + AmplifyChatbot, AmplifyCheckbox, AmplifyCodeField, AmplifyConfirmSignIn, @@ -65,6 +66,7 @@ const DECLARATIONS = [ AmplifyAuthenticator, AmplifyAuthFields, AmplifyButton, + AmplifyChatbot, AmplifyCheckbox, AmplifyCodeField, AmplifyConfirmSignIn, diff --git a/packages/amplify-ui-components/Readme.md b/packages/amplify-ui-components/Readme.md index 693003f1c4c..1adaa440668 100644 --- a/packages/amplify-ui-components/Readme.md +++ b/packages/amplify-ui-components/Readme.md @@ -351,6 +351,7 @@ Theming for the UI components can be achieved by using [CSS Variables](https://d | `--amplify-light-grey` | #c4c4c4 | | `--amplify-white` | #ffffff | | `--amplify-red` | #dd3f5b | +| `--amplify-blue` | #099ac8 | ## Amplify Authenticator `usernameAlias` diff --git a/packages/amplify-ui-components/src/common/Translations.ts b/packages/amplify-ui-components/src/common/Translations.ts index 5916e658ff9..9969660f373 100644 --- a/packages/amplify-ui-components/src/common/Translations.ts +++ b/packages/amplify-ui-components/src/common/Translations.ts @@ -109,5 +109,13 @@ export enum AuthStrings { SIGN_UP_FAILED = 'Sign Up Failed', } -type Translations = AuthErrorStrings | AuthStrings; -export const Translations = { ...AuthStrings, ...AuthErrorStrings }; +export enum InteractionsStrings { + CHATBOT_TITLE = 'ChatBot Lex', + TEXT_INPUT_PLACEHOLDER = 'Write a message', + VOICE_INPUT_PLACEHOLDER = 'Click mic to speak', + CHAT_DISABLED_ERROR = 'Error: Either voice or text must be enabled for the chatbot', + NO_BOT_NAME_ERROR = 'Error: Bot name must be provided to ChatBot', +} + +type Translations = AuthErrorStrings | AuthStrings | InteractionsStrings; +export const Translations = { ...AuthStrings, ...AuthErrorStrings, ...InteractionsStrings }; diff --git a/packages/amplify-ui-components/src/common/audio-control/helper.ts b/packages/amplify-ui-components/src/common/audio-control/helper.ts new file mode 100644 index 00000000000..94f3c96fca2 --- /dev/null +++ b/packages/amplify-ui-components/src/common/audio-control/helper.ts @@ -0,0 +1,127 @@ +import { RECORDER_EXPORT_MIME_TYPE } from './settings'; + +/** + * Merges multiple buffers into one. + */ +const mergeBuffers = (bufferArray: Float32Array[], recLength: number) => { + const result = new Float32Array(recLength); + let offset = 0; + for (let i = 0; i < bufferArray.length; i++) { + result.set(bufferArray[i], offset); + offset += bufferArray[i].length; + } + return result; +}; + +/** + * Downsamples audio to desired export sample rate. + */ +const downsampleBuffer = (buffer: Float32Array, recordSampleRate: number, exportSampleRate: number) => { + if (exportSampleRate === recordSampleRate) { + return buffer; + } + const sampleRateRatio = recordSampleRate / exportSampleRate; + const newLength = Math.round(buffer.length / sampleRateRatio); + const result = new Float32Array(newLength); + let offsetResult = 0; + let offsetBuffer = 0; + while (offsetResult < result.length) { + const nextOffsetBuffer = Math.round((offsetResult + 1) * sampleRateRatio); + let accum = 0, + count = 0; + for (let i = offsetBuffer; i < nextOffsetBuffer && i < buffer.length; i++) { + accum += buffer[i]; + count++; + } + result[offsetResult] = accum / count; + offsetResult++; + offsetBuffer = nextOffsetBuffer; + } + return result; +}; + +/** + * converts raw audio values to 16 bit pcm. + */ +const floatTo16BitPCM = (output: DataView, offset: number, input: Float32Array) => { + let byteOffset = offset; + for (let i = 0; i < input.length; i++, byteOffset += 2) { + const s = Math.max(-1, Math.min(1, input[i])); + output.setInt16(byteOffset, s < 0 ? s * 0x8000 : s * 0x7fff, true); + } +}; + +/** + * Write given strings in big-endian order. + */ +const writeString = (view: DataView, offset: number, string: string) => { + for (let i = 0; i < string.length; i++) { + view.setUint8(offset + i, string.charCodeAt(i)); + } +}; + +/** + * Encodes raw pcm audio into a wav file. + */ +const encodeWAV = (samples: Float32Array, exportSampleRate?: number) => { + /** + * WAV file consists of three parts: RIFF header, WAVE subchunk, and data subchunk. We precompute the size of them. + */ + + const audioSize = samples.length * 2; // We use 16-bit samples, so we have (2 * sampleLength) bytes. + const fmtSize = 24; // Byte size of the fmt subchunk: 24 bytes that the audio information that we'll set below. + const dataSize = 8 + audioSize; // Byte size of the data subchunk: raw sound data plus 8 bytes for the subchunk descriptions. + + const totalByteSize = 12 + fmtSize + dataSize; // Byte size of the whole file, including the chunk header / descriptor. + + // create DataView object to write byte values into + const buffer = new ArrayBuffer(totalByteSize); // buffer to write the chunk values in. + const view = new DataView(buffer); + + /** + * Start writing the .wav file. We write top to bottom, so byte offset (first numeric argument) increases strictly. + */ + // RIFF header + writeString(view, 0, 'RIFF'); // At offset 0, write the letters "RIFF" + view.setUint32(4, fmtSize + dataSize, true); // At offset 4, write the size of fmt and data chunk size combined. + writeString(view, 8, 'WAVE'); // At offset 8, write the format type "WAVE" + + // fmt subchunk + writeString(view, 12, 'fmt '); //chunkdId 'fmt ' + view.setUint32(16, fmtSize - 8, true); // fmt subchunk size below this value. We set 8 bytes already, so subtract 8 bytes from fmtSize. + view.setUint16(20, 1, true); // Audiio format code, which is 1 for PCM. + view.setUint16(22, 1, true); // Number of audio channels. We use mono, ie 1. + view.setUint32(24, exportSampleRate, true); // Sample rate of the audio file. + view.setUint32(28, exportSampleRate * 2, true); // Data rate, or # of data bytes per second. Since each sample is 2 bytes, this is 2 * sampleRate. + view.setUint16(32, 2, true); // block align, # of bytes per sample including all channels, ie. 2 bytes. + view.setUint16(34, 16, true); // bits per sample, ie. 16 bits + + // data subchunk + writeString(view, 36, 'data'); // write the chunkId 'data' + view.setUint32(40, audioSize, true); // Audio byte size + floatTo16BitPCM(view, 44, samples); // raw pcm values then go here. + return view; +}; + +/** + * Given arrays of raw pcm audio, downsamples the audio to desired sample rate and encodes it to a wav audio file. + * + * @param recBuffer {Float32Array[]} - 2d float array containing the recorded raw audio + * @param recLength {number} - total length of recorded audio + * @param recordSampleRate {number} - sample rate of the recorded audio + * @param exportSampleRate {number} - desired sample rate of the exported file + */ +export const exportBuffer = ( + recBuffer: Float32Array[], + recLength: number, + recordSampleRate: number, + exportSampleRate: number, +) => { + const mergedBuffers = mergeBuffers(recBuffer, recLength); + const downsampledBuffer = downsampleBuffer(mergedBuffers, recordSampleRate, exportSampleRate); + const encodedWav = encodeWAV(downsampledBuffer, exportSampleRate); + const audioBlob = new Blob([encodedWav], { + type: RECORDER_EXPORT_MIME_TYPE, + }); + return audioBlob; +}; diff --git a/packages/amplify-ui-components/src/common/audio-control/index.ts b/packages/amplify-ui-components/src/common/audio-control/index.ts new file mode 100644 index 00000000000..80bcda15afb --- /dev/null +++ b/packages/amplify-ui-components/src/common/audio-control/index.ts @@ -0,0 +1,3 @@ +export * from './recorder'; +export * from './helper'; +export * from './visualizer'; diff --git a/packages/amplify-ui-components/src/common/audio-control/recorder.ts b/packages/amplify-ui-components/src/common/audio-control/recorder.ts new file mode 100644 index 00000000000..474ba7f8bb6 --- /dev/null +++ b/packages/amplify-ui-components/src/common/audio-control/recorder.ts @@ -0,0 +1,221 @@ +import { exportBuffer } from './helper'; +import { browserOrNode, Logger } from '@aws-amplify/core'; +import { + DEFAULT_EXPORT_SAMPLE_RATE, + FFT_MAX_DECIBELS, + FFT_MIN_DECIBELS, + FFT_SIZE, + FFT_SMOOTHING_TIME_CONSTANT, +} from './settings'; + +interface SilenceDetectionConfig { + time: number; + amplitude: number; +} + +type SilenceHandler = () => void; +type Visualizer = (dataArray: Uint8Array, bufferLength: number) => void; +const logger = new Logger('AudioRecorder'); + +export class AudioRecorder { + private options: SilenceDetectionConfig; + private audioContext: AudioContext; + private audioSupported: boolean; + + private analyserNode: AnalyserNode; + private playbackSource: AudioBufferSourceNode; + private onSilence: SilenceHandler; + private visualizer: Visualizer; + + // input mic stream is stored in a buffer + private streamBuffer: Float32Array[] = []; + private streamBufferLength = 0; + + // recording props + private start: number; + private recording = false; + + constructor(options: SilenceDetectionConfig) { + this.options = options; + } + + /** + * This must be called first to enable audio context and request microphone access. + * Once access granted, it connects all the necessary audio nodes to the context so that it can begin recording or playing. + */ + async init() { + if (browserOrNode().isBrowser) { + window.AudioContext = window.AudioContext || (window as any).webkitAudioContext; + this.audioContext = new AudioContext(); + await navigator.mediaDevices + .getUserMedia({ audio: true }) + .then(stream => { + this.audioSupported = true; + this.setupAudioNodes(stream); + }) + .catch(() => { + this.audioSupported = false; + return Promise.reject('Audio is not supported'); + }); + } else { + this.audioSupported = false; + return Promise.reject('Audio is not supported'); + } + } + + /** + * Setup audio nodes after successful `init`. + */ + private async setupAudioNodes(stream: MediaStream) { + try { + await this.audioContext.resume(); + } catch (err) { + logger.error(err); + } + const sourceNode = this.audioContext.createMediaStreamSource(stream); + const processorNode = this.audioContext.createScriptProcessor(4096, 1, 1); + + processorNode.onaudioprocess = audioProcessingEvent => { + if (!this.recording) return; + const stream = audioProcessingEvent.inputBuffer.getChannelData(0); + this.streamBuffer.push(new Float32Array(stream)); // set to a copy of the stream + this.streamBufferLength += stream.length; + this.analyse(); + }; + + const analyserNode = this.audioContext.createAnalyser(); + analyserNode.minDecibels = FFT_MIN_DECIBELS; + analyserNode.maxDecibels = FFT_MAX_DECIBELS; + analyserNode.smoothingTimeConstant = FFT_SMOOTHING_TIME_CONSTANT; + + sourceNode.connect(analyserNode); + analyserNode.connect(processorNode); + processorNode.connect(sourceNode.context.destination); + + this.analyserNode = analyserNode; + } + + /** + * Start recording audio and listen for silence. + * + * @param onSilence {SilenceHandler} - called whenever silence is detected + * @param visualizer {Visualizer} - called with audio data on each audio process to be used for visualization. + */ + public async startRecording(onSilence?: SilenceHandler, visualizer?: Visualizer) { + if (this.recording || !this.audioSupported) return; + this.onSilence = onSilence || function() {}; + this.visualizer = visualizer || function() {}; + + const context = this.audioContext; + try { + await context.resume(); + } catch (err) { + logger.error(err); + } + this.start = Date.now(); + this.recording = true; + } + + /** + * Pause recording + */ + public stopRecording() { + if (!this.audioSupported) return; + this.recording = false; + } + + /** + * Pause recording and clear audio buffer + */ + public clear() { + this.stopRecording(); + this.streamBufferLength = 0; + this.streamBuffer = []; + } + + /** + * Plays given audioStream with audioContext + * + * @param buffer {Uint8Array} - audioStream to be played + */ + public play(buffer: Uint8Array) { + if (!buffer || !this.audioSupported) return; + const myBlob = new Blob([buffer]); + + return new Promise((res, rej) => { + const fileReader = new FileReader(); + fileReader.onload = () => { + if (this.playbackSource) this.playbackSource.disconnect(); // disconnect previous playback source + this.playbackSource = this.audioContext.createBufferSource(); + + const successCallback = (buf: AudioBuffer) => { + this.playbackSource.buffer = buf; + this.playbackSource.connect(this.audioContext.destination); + this.playbackSource.onended = () => { + return res(); + }; + this.playbackSource.start(0); + }; + const errorCallback = err => { + return rej(err); + }; + + this.audioContext.decodeAudioData(fileReader.result as ArrayBuffer, successCallback, errorCallback); + }; + fileReader.onerror = () => rej(); + fileReader.readAsArrayBuffer(myBlob); + }); + } + + /** + * Stops playing audio if there's a playback source connected. + */ + public stop() { + if (this.playbackSource) { + this.playbackSource.stop(); + } + } + + /** + * Called after each audioProcess. Check for silence and give fft time domain data to visualizer. + */ + private analyse() { + if (!this.audioSupported) return; + const analyser = this.analyserNode; + analyser.fftSize = FFT_SIZE; + + const bufferLength = analyser.fftSize; + const dataArray = new Uint8Array(bufferLength); + const amplitude = this.options.amplitude; + const time = this.options.time; + + analyser.getByteTimeDomainData(dataArray); + this.visualizer(dataArray, bufferLength); + + for (let i = 0; i < bufferLength; i++) { + // Normalize between -1 and 1. + const curr_value_time = dataArray[i] / 128 - 1.0; + if (curr_value_time > amplitude || curr_value_time < -1 * amplitude) { + this.start = Date.now(); + } + } + const newtime = Date.now(); + const elapsedTime = newtime - this.start; + if (elapsedTime > time) { + this.onSilence(); + } + } + + /** + * Encodes recorded buffer to a wav file and exports it to a blob. + * + * @param exportSampleRate {number} - desired sample rate of the exported buffer + */ + public async exportWAV(exportSampleRate: number = DEFAULT_EXPORT_SAMPLE_RATE) { + if (!this.audioSupported) return; + const recordSampleRate = this.audioContext.sampleRate; + const blob = exportBuffer(this.streamBuffer, this.streamBufferLength, recordSampleRate, exportSampleRate); + this.clear(); + return blob; + } +} diff --git a/packages/amplify-ui-components/src/common/audio-control/settings.ts b/packages/amplify-ui-components/src/common/audio-control/settings.ts new file mode 100644 index 00000000000..ab6075b9104 --- /dev/null +++ b/packages/amplify-ui-components/src/common/audio-control/settings.ts @@ -0,0 +1,8 @@ +// AudioRecorder settings +export const RECORDER_EXPORT_MIME_TYPE = 'application/octet-stream'; +export const DEFAULT_EXPORT_SAMPLE_RATE = 16000; + +export const FFT_SIZE = 2048; // window size in samples for Fast Fourier Transform (FFT) +export const FFT_MAX_DECIBELS = -10; // maximum power value in the scaling range for the FFT analysis data +export const FFT_MIN_DECIBELS = -90; // minimum power value in the scaling range for the FFT analysis data +export const FFT_SMOOTHING_TIME_CONSTANT = 0.85; // averaging constant with the last analysis frame diff --git a/packages/amplify-ui-components/src/common/audio-control/visualizer.ts b/packages/amplify-ui-components/src/common/audio-control/visualizer.ts new file mode 100644 index 00000000000..d7a8c13f734 --- /dev/null +++ b/packages/amplify-ui-components/src/common/audio-control/visualizer.ts @@ -0,0 +1,44 @@ +import { browserOrNode } from '@aws-amplify/core'; + +export const visualize = (dataArray: Uint8Array, bufferLength: number, canvas: HTMLCanvasElement) => { + if (!canvas) return; + if (!browserOrNode().isBrowser) throw new Error('Visualization is not supported on non-browsers.'); + const { width, height } = canvas.getBoundingClientRect(); + + // need to update the default canvas width and height + canvas.width = width; + canvas.height = height; + + const canvasCtx = canvas.getContext('2d'); + + canvasCtx.fillStyle = 'white'; + canvasCtx.clearRect(0, 0, width, height); + + const draw = () => { + canvasCtx.fillRect(0, 0, width, height); + canvasCtx.lineWidth = 1; + const color = getComputedStyle(document.documentElement).getPropertyValue('--amplify-primary-color'); + canvasCtx.strokeStyle = !color || color === '' ? '#ff9900' : color; // TODO: try separate css variable + canvasCtx.beginPath(); + + const sliceWidth = (width * 1.0) / bufferLength; + let x = 0; + + for (let i = 0; i < bufferLength || i % 3 === 0; i++) { + const value = dataArray[i] / 128.0; + const y = (value * height) / 2; + if (i === 0) { + canvasCtx.moveTo(x, y); + } else { + canvasCtx.lineTo(x, y); + } + x += sliceWidth; + } + + canvasCtx.lineTo(canvas.width, canvas.height / 2); + canvasCtx.stroke(); + }; + + // Register our draw function with requestAnimationFrame. + requestAnimationFrame(draw); +}; diff --git a/packages/amplify-ui-components/src/common/constants.ts b/packages/amplify-ui-components/src/common/constants.ts index ee2718ad19e..23416affef4 100644 --- a/packages/amplify-ui-components/src/common/constants.ts +++ b/packages/amplify-ui-components/src/common/constants.ts @@ -20,6 +20,8 @@ export const AUTHENTICATOR_AUTHSTATE = 'amplify-authenticator-authState'; export const PHONE_EMPTY_ERROR_MESSAGE = 'Phone number can not be empty'; export const NO_AUTH_MODULE_FOUND = 'No Auth module found, please ensure @aws-amplify/auth is imported'; export const NO_STORAGE_MODULE_FOUND = 'No Storage module found, please ensure @aws-amplify/storage is imported'; +export const NO_INTERACTIONS_MODULE_FOUND = + 'No Interactions module found, please ensure @aws-amplify/interactions is imported'; // TOTP Messages export const SETUP_TOTP = 'SETUP_TOTP'; diff --git a/packages/amplify-ui-components/src/common/types/interactions-types.ts b/packages/amplify-ui-components/src/common/types/interactions-types.ts new file mode 100644 index 00000000000..ce8eefc0b7c --- /dev/null +++ b/packages/amplify-ui-components/src/common/types/interactions-types.ts @@ -0,0 +1,4 @@ +export interface ChatResult { + data?: object; + err?: string; +} diff --git a/packages/amplify-ui-components/src/common/types/ui-types.ts b/packages/amplify-ui-components/src/common/types/ui-types.ts index 46702a06a09..d1bd541a86d 100644 --- a/packages/amplify-ui-components/src/common/types/ui-types.ts +++ b/packages/amplify-ui-components/src/common/types/ui-types.ts @@ -1,4 +1,4 @@ export interface InputEvent extends Event {} export type TextFieldTypes = 'date' | 'email' | 'number' | 'password' | 'search' | 'tel' | 'text' | 'url' | 'time'; export type ButtonTypes = 'button' | 'submit' | 'reset'; -export type ButtonVariant = 'button' | 'anchor'; +export type ButtonVariant = 'button' | 'anchor' | 'icon'; diff --git a/packages/amplify-ui-components/src/components.d.ts b/packages/amplify-ui-components/src/components.d.ts index efffc6dbb71..2ff6d9e62c0 100644 --- a/packages/amplify-ui-components/src/components.d.ts +++ b/packages/amplify-ui-components/src/components.d.ts @@ -8,9 +8,10 @@ import { HTMLStencilElement, JSXBase } from "@stencil/core/internal"; import { AuthState, AuthStateHandler, CognitoUserInterface, FederatedConfig, MFATypesInterface, UsernameAliasStrings } from "./common/types/auth-types"; import { FormFieldTypes } from "./components/amplify-auth-fields/amplify-auth-fields-interface"; import { ButtonTypes, ButtonVariant, InputEvent, TextFieldTypes } from "./common/types/ui-types"; +import { IconNameType } from "./components/amplify-icon/icons"; +import { ChatResult } from "./common/types/interactions-types"; import { FunctionalComponent } from "@stencil/core"; import { CountryCodeDialOptions } from "./components/amplify-country-dial-code/amplify-country-dial-code-interface"; -import { IconNameType } from "./components/amplify-icon/icons"; import { AccessLevel, StorageObject } from "./common/types/storage-types"; import { SelectOptionsNumber, SelectOptionsString } from "./components/amplify-select/amplify-select-interface"; export namespace Components { @@ -67,6 +68,10 @@ export namespace Components { * (Optional) Callback called when a user clicks on the button */ "handleButtonClick": (evt: Event) => void; + /** + * Name of icon to be placed inside the button + */ + "icon"?: IconNameType; /** * Type of the button: 'button', 'submit' or 'reset' */ @@ -76,6 +81,44 @@ export namespace Components { */ "variant": ButtonVariant; } + interface AmplifyChatbot { + /** + * Name of the bot + */ + "botName": string; + /** + * Text placed in the top header + */ + "botTitle": string; + /** + * Clear messages when conversation finishes + */ + "clearOnComplete": boolean; + /** + * Continue listening to users after they send the message + */ + "conversationModeOn": boolean; + /** + * Noise threshold between -1 and 1. Anything below is considered a silence. + */ + "silenceThreshold": number; + /** + * Amount of silence (in ms) to wait for + */ + "silenceTime": number; + /** + * Whether text chat is enabled + */ + "textEnabled": boolean; + /** + * Whether voice chat is enabled + */ + "voiceEnabled": boolean; + /** + * Greeting message displayed to users + */ + "welcomeMessage": string; + } interface AmplifyCheckbox { /** * If `true`, the checkbox is selected. @@ -1120,6 +1163,12 @@ declare global { prototype: HTMLAmplifyButtonElement; new (): HTMLAmplifyButtonElement; }; + interface HTMLAmplifyChatbotElement extends Components.AmplifyChatbot, HTMLStencilElement { + } + var HTMLAmplifyChatbotElement: { + prototype: HTMLAmplifyChatbotElement; + new (): HTMLAmplifyChatbotElement; + }; interface HTMLAmplifyCheckboxElement extends Components.AmplifyCheckbox, HTMLStencilElement { } var HTMLAmplifyCheckboxElement: { @@ -1414,6 +1463,7 @@ declare global { "amplify-auth0-button": HTMLAmplifyAuth0ButtonElement; "amplify-authenticator": HTMLAmplifyAuthenticatorElement; "amplify-button": HTMLAmplifyButtonElement; + "amplify-chatbot": HTMLAmplifyChatbotElement; "amplify-checkbox": HTMLAmplifyCheckboxElement; "amplify-code-field": HTMLAmplifyCodeFieldElement; "amplify-confirm-sign-in": HTMLAmplifyConfirmSignInElement; @@ -1518,6 +1568,10 @@ declare namespace LocalJSX { * (Optional) Callback called when a user clicks on the button */ "handleButtonClick"?: (evt: Event) => void; + /** + * Name of icon to be placed inside the button + */ + "icon"?: IconNameType; /** * Type of the button: 'button', 'submit' or 'reset' */ @@ -1527,6 +1581,48 @@ declare namespace LocalJSX { */ "variant"?: ButtonVariant; } + interface AmplifyChatbot { + /** + * Name of the bot + */ + "botName"?: string; + /** + * Text placed in the top header + */ + "botTitle"?: string; + /** + * Clear messages when conversation finishes + */ + "clearOnComplete"?: boolean; + /** + * Continue listening to users after they send the message + */ + "conversationModeOn"?: boolean; + /** + * Event emitted when conversation is completed + */ + "onChatCompleted"?: (event: CustomEvent) => void; + /** + * Noise threshold between -1 and 1. Anything below is considered a silence. + */ + "silenceThreshold"?: number; + /** + * Amount of silence (in ms) to wait for + */ + "silenceTime"?: number; + /** + * Whether text chat is enabled + */ + "textEnabled"?: boolean; + /** + * Whether voice chat is enabled + */ + "voiceEnabled"?: boolean; + /** + * Greeting message displayed to users + */ + "welcomeMessage"?: string; + } interface AmplifyCheckbox { /** * If `true`, the checkbox is selected. @@ -2549,6 +2645,7 @@ declare namespace LocalJSX { "amplify-auth0-button": AmplifyAuth0Button; "amplify-authenticator": AmplifyAuthenticator; "amplify-button": AmplifyButton; + "amplify-chatbot": AmplifyChatbot; "amplify-checkbox": AmplifyCheckbox; "amplify-code-field": AmplifyCodeField; "amplify-confirm-sign-in": AmplifyConfirmSignIn; @@ -2608,6 +2705,7 @@ declare module "@stencil/core" { "amplify-auth0-button": LocalJSX.AmplifyAuth0Button & JSXBase.HTMLAttributes; "amplify-authenticator": LocalJSX.AmplifyAuthenticator & JSXBase.HTMLAttributes; "amplify-button": LocalJSX.AmplifyButton & JSXBase.HTMLAttributes; + "amplify-chatbot": LocalJSX.AmplifyChatbot & JSXBase.HTMLAttributes; "amplify-checkbox": LocalJSX.AmplifyCheckbox & JSXBase.HTMLAttributes; "amplify-code-field": LocalJSX.AmplifyCodeField & JSXBase.HTMLAttributes; "amplify-confirm-sign-in": LocalJSX.AmplifyConfirmSignIn & JSXBase.HTMLAttributes; diff --git a/packages/amplify-ui-components/src/components/amplify-authenticator/readme.md b/packages/amplify-ui-components/src/components/amplify-authenticator/readme.md index 2d3d39849cc..34b4f27a621 100644 --- a/packages/amplify-ui-components/src/components/amplify-authenticator/readme.md +++ b/packages/amplify-ui-components/src/components/amplify-authenticator/readme.md @@ -45,6 +45,7 @@ graph TD; amplify-sign-in --> amplify-strike amplify-sign-in --> amplify-auth-fields amplify-sign-in --> amplify-loading-spinner + amplify-button --> amplify-icon amplify-form-section --> amplify-section amplify-form-section --> amplify-button amplify-form-section --> amplify-loading-spinner diff --git a/packages/amplify-ui-components/src/components/amplify-button/amplify-button.scss b/packages/amplify-ui-components/src/components/amplify-button/amplify-button.scss index 29a344182e7..5df4cd71ddc 100644 --- a/packages/amplify-ui-components/src/components/amplify-button/amplify-button.scss +++ b/packages/amplify-ui-components/src/components/amplify-button/amplify-button.scss @@ -10,8 +10,12 @@ --link-hover: var(--amplify-primary-tint); --link-active: var(--amplify-primary-shade); --text-transform: uppercase; + --icon-fill: var(--amplify-white); + --icon-height: 1.25rem; + --padding: 1rem; + --width: 100%; - width: 100%; + width: var(--width); text-align: center; @include md { @@ -35,7 +39,7 @@ user-select: none; background-image: none; color: var(--color); - padding: 1rem; + padding: var(--padding); letter-spacing: 0.75px; text-transform: var(--text-transform); background-color: var(--background-color); @@ -59,6 +63,18 @@ } } +.icon { + background-color: inherit; + border: none; + font: inherit; + cursor: pointer; + padding: var(--padding); + amplify-icon { + --icon-fill-color: var(--icon-fill); + --height: var(--icon-height); + } +} + .anchor { color: var(--link-color); background-color: inherit; diff --git a/packages/amplify-ui-components/src/components/amplify-button/amplify-button.tsx b/packages/amplify-ui-components/src/components/amplify-button/amplify-button.tsx index 2cf30064cb4..bd77d663e07 100644 --- a/packages/amplify-ui-components/src/components/amplify-button/amplify-button.tsx +++ b/packages/amplify-ui-components/src/components/amplify-button/amplify-button.tsx @@ -1,6 +1,7 @@ import { Element, Component, Prop, h } from '@stencil/core'; import { ButtonTypes, ButtonVariant } from '../../common/types/ui-types'; import { hasShadowDom } from '../../common/helpers'; +import { IconNameType } from '../amplify-icon/icons'; @Component({ tag: 'amplify-button', @@ -17,6 +18,8 @@ export class AmplifyButton { @Prop() handleButtonClick: (evt: Event) => void; /** Disabled state of the button */ @Prop() disabled?: boolean = false; + /** Name of icon to be placed inside the button */ + @Prop() icon?: IconNameType; private handleClick = (ev: Event) => { if (this.handleButtonClick) { @@ -55,6 +58,7 @@ export class AmplifyButton { disabled={this.disabled} onClick={this.handleClick} > + {this.variant === 'icon' && } ); diff --git a/packages/amplify-ui-components/src/components/amplify-button/readme.md b/packages/amplify-ui-components/src/components/amplify-button/readme.md index b22405cb80c..966c26956bd 100644 --- a/packages/amplify-ui-components/src/components/amplify-button/readme.md +++ b/packages/amplify-ui-components/src/components/amplify-button/readme.md @@ -5,18 +5,20 @@ ## Properties -| Property | Attribute | Description | Type | Default | -| ------------------- | ---------- | ----------------------------------------------------------- | --------------------------------- | ----------- | -| `disabled` | `disabled` | Disabled state of the button | `boolean` | `false` | -| `handleButtonClick` | -- | (Optional) Callback called when a user clicks on the button | `(evt: Event) => void` | `undefined` | -| `type` | `type` | Type of the button: 'button', 'submit' or 'reset' | `"button" \| "reset" \| "submit"` | `'button'` | -| `variant` | `variant` | Variant of a button: 'button' \| 'anchor' | `"anchor" \| "button"` | `'button'` | +| Property | Attribute | Description | Type | Default | +| ------------------- | ---------- | ----------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- | +| `disabled` | `disabled` | Disabled state of the button | `boolean` | `false` | +| `handleButtonClick` | -- | (Optional) Callback called when a user clicks on the button | `(evt: Event) => void` | `undefined` | +| `icon` | `icon` | Name of icon to be placed inside the button | `"amazon" \| "auth0" \| "ban" \| "enter-vr" \| "exit-vr" \| "facebook" \| "google" \| "loading" \| "maximize" \| "microphone" \| "minimize" \| "photoPlaceholder" \| "send" \| "sound" \| "sound-mute" \| "warning"` | `undefined` | +| `type` | `type` | Type of the button: 'button', 'submit' or 'reset' | `"button" \| "reset" \| "submit"` | `'button'` | +| `variant` | `variant` | Variant of a button: 'button' \| 'anchor' | `"anchor" \| "button" \| "icon"` | `'button'` | ## Dependencies ### Used by + - [amplify-chatbot](../amplify-chatbot) - [amplify-confirm-sign-in](../amplify-confirm-sign-in) - [amplify-confirm-sign-up](../amplify-confirm-sign-up) - [amplify-forgot-password](../amplify-forgot-password) @@ -29,9 +31,15 @@ - [amplify-sign-up](../amplify-sign-up) - [amplify-verify-contact](../amplify-verify-contact) +### Depends on + +- [amplify-icon](../amplify-icon) + ### Graph ```mermaid graph TD; + amplify-button --> amplify-icon + amplify-chatbot --> amplify-button amplify-confirm-sign-in --> amplify-button amplify-confirm-sign-up --> amplify-button amplify-forgot-password --> amplify-button diff --git a/packages/amplify-ui-components/src/components/amplify-chatbot/__snapshots__/amplify-chatbot.spec.tsx.snap b/packages/amplify-ui-components/src/components/amplify-chatbot/__snapshots__/amplify-chatbot.spec.tsx.snap new file mode 100644 index 00000000000..f718ff2cf10 --- /dev/null +++ b/packages/amplify-ui-components/src/components/amplify-chatbot/__snapshots__/amplify-chatbot.spec.tsx.snap @@ -0,0 +1,21 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`amplify-chatbot renders chatbot 1`] = ` + + +
+ +
+ ChatBot Lex +
+
+
+ + +
+
+
+`; diff --git a/packages/amplify-ui-components/src/components/amplify-chatbot/amplify-chatbot.scss b/packages/amplify-ui-components/src/components/amplify-chatbot/amplify-chatbot.scss new file mode 100644 index 00000000000..b7d49f58f19 --- /dev/null +++ b/packages/amplify-ui-components/src/components/amplify-chatbot/amplify-chatbot.scss @@ -0,0 +1,88 @@ +@import 'animation.scss'; +:host { + --width: 28.75rem; + --height: 37.5rem; + --header-color: var(--amplify-secondary-color); + --header-size: var(--amplify-text-lg); + --bot-background-color: rgb(230, 230, 230); + --bot-text-color: black; + --bot-dot-color: var(--bot-text-color); + --user-background-color: var(--amplify-blue); + --user-text-color: var(--amplify-white); + --user-dot-color: var(--user-text-color); +} +.amplify-chatbot { + display: inline-flex; + flex-direction: column; + background-color: var(--background-color); + border-radius: 0.375rem; + box-shadow: 0.0625rem 0rem 0.25rem 0 rgba(0, 0, 0, 0.15); + box-sizing: border-box; + font-family: var(--amplify-font-family); + margin-bottom: 1rem; + width: 100%; + height: var(--height); + @include md { + width: var(--width); + } + max-width: var(--width); +} +.header { + padding: 1.25rem 0.375rem 1.25rem 0.375rem; + color: var(--header-color); + font-size: var(--header-size); + font-weight: bold; + text-align: center; + word-wrap: break-word; +} +.body { + border-top: 0.0625rem solid rgba(0, 0, 0, 0.05); + padding: 1.5rem 1rem 0 1rem; + display: flex; + flex-grow: 1; + flex-direction: column; + overflow: auto; +} +.bubble { + max-width: 100%; + padding: 0.8em 1.4em; + text-align: left; + word-wrap: break-word; + margin-bottom: 0.625rem; +} +.bot { + margin-right: auto; + background-color: var(--bot-background-color); + color: var(--bot-text-color); + border-radius: 1.5rem 1.5rem 1.5rem 0; +} +.user { + margin-left: auto; + background-color: var(--user-background-color); + color: var(--user-text-color); + border-radius: 1.5rem 1.5rem 0 1.5rem; +} +.footer { + display: flex; + align-items: center; + border-top: 0.062rem solid rgba(0, 0, 0, 0.05); + padding-right: 0.625rem; + min-height: 3.125rem; + amplify-input { + --border: none; + --margin: 0; + flex-grow: 1; + } +} +canvas { + margin-left: 0.625rem; + margin-right: 0.625rem; + flex-grow: 1; + height: 3.125rem; +} +.icon-button { + --icon-height: 1.25rem; + --icon-fill: var(--amplify-primary-color); + --padding: 0.625rem; + --width: auto; +} diff --git a/packages/amplify-ui-components/src/components/amplify-chatbot/amplify-chatbot.spec.tsx b/packages/amplify-ui-components/src/components/amplify-chatbot/amplify-chatbot.spec.tsx new file mode 100644 index 00000000000..9f9840ae35d --- /dev/null +++ b/packages/amplify-ui-components/src/components/amplify-chatbot/amplify-chatbot.spec.tsx @@ -0,0 +1,12 @@ +import { newSpecPage } from '@stencil/core/testing'; +import { AmplifyChatbot } from './amplify-chatbot'; + +describe('amplify-chatbot', () => { + it('renders chatbot', async () => { + const page = await newSpecPage({ + components: [AmplifyChatbot], + html: ``, + }); + expect(page.root).toMatchSnapshot(); + }); +}); diff --git a/packages/amplify-ui-components/src/components/amplify-chatbot/amplify-chatbot.tsx b/packages/amplify-ui-components/src/components/amplify-chatbot/amplify-chatbot.tsx new file mode 100644 index 00000000000..a29b94ce923 --- /dev/null +++ b/packages/amplify-ui-components/src/components/amplify-chatbot/amplify-chatbot.tsx @@ -0,0 +1,382 @@ +import { Component, Host, h, Prop, State, Listen, Event, EventEmitter, Element } from '@stencil/core'; +import { I18n } from '@aws-amplify/core'; +import { Interactions } from '@aws-amplify/interactions'; +import { JSXBase } from '@stencil/core/internal'; +import { AudioRecorder, visualize } from '../../common/audio-control'; +import { ChatResult } from '../../common/types/interactions-types'; +import { NO_INTERACTIONS_MODULE_FOUND } from '../../common/constants'; +import { Translations } from '../../common/Translations'; +import { InteractionsResponse } from '@aws-amplify/interactions'; + +// enum for possible bot states +enum ChatState { + Initial, + Listening, + SendingText, + SendingVoice, + Error, +} + +// Message types +enum MessageFrom { + Bot = 'bot', + User = 'user', +} +interface Message { + content: string; + from: MessageFrom; +} + +// Error types +enum ChatErrorType { + Recoverable, + Unrecoverable, +} +interface ChatError { + message: string; + errorType: ChatErrorType; +} + +/** + * @slot header - title content placed at the top + */ +@Component({ + tag: 'amplify-chatbot', + styleUrl: 'amplify-chatbot.scss', + shadow: true, +}) +export class AmplifyChatbot { + /** Name of the bot */ + @Prop() botName: string; + /** Clear messages when conversation finishes */ + @Prop() clearOnComplete: boolean = false; + /** Continue listening to users after they send the message */ + @Prop() conversationModeOn: boolean = false; + /** Greeting message displayed to users */ + @Prop() welcomeMessage: string; + /** Text placed in the top header */ + @Prop() botTitle: string = Translations.CHATBOT_TITLE; + /** Whether voice chat is enabled */ + @Prop() voiceEnabled: boolean = false; + /** Whether text chat is enabled */ + @Prop() textEnabled: boolean = true; + /** Amount of silence (in ms) to wait for */ + @Prop() silenceTime: number = 1500; + /** Noise threshold between -1 and 1. Anything below is considered a silence. */ + @Prop() silenceThreshold: number = 0.2; + + /** Messages in current session */ + @State() messages: Message[] = []; + /** Text input box value */ + @State() text: string = ''; + /** Current app state */ + @State() chatState: ChatState = ChatState.Initial; + /** Toast error */ + @State() error: ChatError; + + @Element() element: HTMLAmplifyChatbotElement; + + private audioRecorder: AudioRecorder; + + // Occurs when user presses enter in input box + @Listen('formSubmit') + submitHandler(_event: CustomEvent) { + this.sendTextMessage(); + } + + /** Event emitted when conversation is completed */ + @Event() chatCompleted: EventEmitter; + + /** + * Lifecycle functions + */ + componentWillLoad() { + if (!Interactions || typeof Interactions.onComplete !== 'function') { + throw new Error(NO_INTERACTIONS_MODULE_FOUND); + } + this.validateProps(); + } + + componentDidRender() { + // scroll to the bottom if necessary + const body = this.element.shadowRoot.querySelector('.body'); + body.scrollTop = body.scrollHeight; + } + + private validateProps() { + if (!this.voiceEnabled && !this.textEnabled) { + this.setError(Translations.CHAT_DISABLED_ERROR, ChatErrorType.Unrecoverable); + return; + } else if (!this.botName) { + this.setError(Translations.NO_BOT_NAME_ERROR, ChatErrorType.Unrecoverable); + return; + } + + if (this.welcomeMessage) this.appendToChat(this.welcomeMessage, MessageFrom.Bot); + // Initialize AudioRecorder if voice is enabled + if (this.voiceEnabled) { + this.audioRecorder = new AudioRecorder({ + time: this.silenceTime, + amplitude: this.silenceThreshold, + }); + this.audioRecorder.init().catch(err => { + this.setError(err, ChatErrorType.Recoverable); + }); + } + + // Callback function to be called after chat is completed + const onComplete = (err: string, data: object) => { + this.chatCompleted.emit({ + data, + err, + }); + if (this.clearOnComplete) { + this.reset(); + } else { + this.chatState = ChatState.Initial; + } + }; + + try { + Interactions.onComplete(this.botName, onComplete); + } catch (err) { + this.setError(err, ChatErrorType.Unrecoverable); + } + } + + /** + * Handlers + */ + private handleMicButton() { + if (this.chatState !== ChatState.Initial) return; + this.audioRecorder.stop(); + this.chatState = ChatState.Listening; + this.audioRecorder.startRecording( + () => this.handleSilence(), + (data, length) => this.visualizer(data, length), + ); + } + + private handleSilence() { + this.chatState = ChatState.SendingVoice; + this.audioRecorder.stopRecording(); + this.audioRecorder.exportWAV().then(blob => { + this.sendVoiceMessage(blob); + }); + } + + private handleTextChange(event: Event) { + const target = event.target as HTMLInputElement; + this.text = target.value; + } + + private handleCancelButton() { + this.audioRecorder.clear(); + this.chatState = ChatState.Initial; + } + + private handleToastClose(errorType: ChatErrorType) { + this.error = undefined; // clear error + // if error is recoverable, reset the app state to initial + if (errorType === ChatErrorType.Recoverable) { + this.chatState = ChatState.Initial; + } + } + + /** + * Visualization + */ + private visualizer(dataArray: Uint8Array, bufferLength: number) { + const canvas = this.element.shadowRoot.querySelector('canvas'); + visualize(dataArray, bufferLength, canvas); + } + + /** + * Interactions helpers + */ + private async sendTextMessage() { + if (this.text.length === 0 || this.chatState !== ChatState.Initial) return; + const text = this.text; + this.text = ''; + this.appendToChat(text, MessageFrom.User); + this.chatState = ChatState.SendingText; + + let response: InteractionsResponse; + try { + response = await Interactions.send(this.botName, text); + } catch (err) { + this.setError(err, ChatErrorType.Recoverable); + return; + } + if (response.message) { + this.appendToChat(response.message, MessageFrom.Bot); + } + this.chatState = ChatState.Initial; + } + + private async sendVoiceMessage(audioInput: Blob) { + const interactionsMessage = { + content: audioInput, + options: { + messageType: 'voice', + }, + }; + + let response: InteractionsResponse; + try { + response = await Interactions.send(this.botName, interactionsMessage); + } catch (err) { + this.setError(err, ChatErrorType.Recoverable); + return; + } + + this.chatState = ChatState.Initial; + const dialogState = response.dialogState; + if (response.inputTranscript) this.appendToChat(response.inputTranscript, MessageFrom.User); + this.appendToChat(response.message, MessageFrom.Bot); + + await this.audioRecorder + .play(response.audioStream) + .then(() => { + // if conversationMode is on, chat is incomplete, and mic button isn't pressed yet, resume listening. + if ( + this.conversationModeOn && + dialogState !== 'Fulfilled' && + dialogState !== 'Failed' && + this.chatState === ChatState.Initial + ) { + this.handleMicButton(); + } + }) + .catch(err => this.setError(err, ChatErrorType.Recoverable)); + } + + private appendToChat(content: string, from: MessageFrom) { + this.messages = [ + ...this.messages, + { + content, + from, + }, + ]; + } + + /** + * State control methods + */ + private setError(error: string | Error, errorType: ChatErrorType) { + const message = typeof error === 'string' ? error : error.message; + this.chatState = ChatState.Error; + this.error = { message, errorType }; + } + + private reset() { + this.chatState = ChatState.Initial; + this.text = ''; + this.error = undefined; + this.messages = []; + if (this.welcomeMessage) this.appendToChat(this.welcomeMessage, MessageFrom.Bot); + this.audioRecorder && this.audioRecorder.clear(); + } + + /** + * Rendering methods + */ + private messageJSX = (messages: Message[]) => { + const messageList = messages.map(message =>
{message.content}
); + if (this.chatState === ChatState.SendingText || this.chatState === ChatState.SendingVoice) { + // if waiting for voice message, show animation on user side because app is waiting for transcript. Else put it on bot side. + const client = this.chatState === ChatState.SendingText ? MessageFrom.Bot : MessageFrom.User; + + messageList.push( +
+
+ + + +
+
, + ); + } + return messageList; + }; + + private listeningFooterJSX(): JSXBase.IntrinsicElements[] { + const visualization = ; + const cancelButton = ( + this.handleCancelButton()} + class="icon-button" + variant="icon" + icon="ban" + /> + ); + return [visualization, cancelButton]; + } + + private footerJSX(): JSXBase.IntrinsicElements[] { + if (this.chatState === ChatState.Listening) return this.listeningFooterJSX(); + + const inputPlaceholder = this.textEnabled + ? Translations.TEXT_INPUT_PLACEHOLDER + : Translations.VOICE_INPUT_PLACEHOLDER; + const textInput = ( + this.handleTextChange(evt)} + value={this.text} + disabled={this.chatState === ChatState.Error || !this.textEnabled} + /> + ); + const micButton = this.voiceEnabled && ( + this.handleMicButton()} + class="icon-button" + variant="icon" + icon="microphone" + disabled={this.chatState === ChatState.Error || this.chatState !== ChatState.Initial} + /> + ); + const sendButton = this.textEnabled && ( + this.sendTextMessage()} + disabled={this.chatState === ChatState.Error || this.chatState !== ChatState.Initial} + /> + ); + return [textInput, micButton, sendButton]; + } + + private errorToast() { + if (!this.error) return; + const { message, errorType } = this.error; + return this.handleToastClose(errorType)} />; + } + + render() { + return ( + +
+ +
+ {I18n.get(this.botTitle)} +
+
+
+ {this.messageJSX(this.messages)} +
+ + {this.errorToast()} +
+
+ ); + } +} diff --git a/packages/amplify-ui-components/src/components/amplify-chatbot/animation.scss b/packages/amplify-ui-components/src/components/amplify-chatbot/animation.scss new file mode 100644 index 00000000000..14c80888538 --- /dev/null +++ b/packages/amplify-ui-components/src/components/amplify-chatbot/animation.scss @@ -0,0 +1,48 @@ +/** + * Loading animation adapted from: https://github.com/nzbin/three-dots. + */ + +// set dot color for bot +.bot .dot { + background-color: var(--bot-dot-color); +} + +// set dot color for user +.user .dot { + background-color: var(--user-dot-color); +} + +.dot-flashing { + width: 2.625rem; + .dot { + display: inline-block; + width: 0.625rem; + height: 0.625rem; + border-radius: 10rem; + opacity: 0.65; + } + .left { + animation: dot-flashing 1s infinite alternate; + animation-delay: 0s; + } + .middle { + margin-left: 0.375rem; + margin-right: 0.375rem; + animation: dot-flashing 1s infinite linear alternate; + animation-delay: 0.5s; + } + .right { + animation: dot-flashing 1s infinite alternate; + animation-delay: 1s; + } +} + +@keyframes dot-flashing { + 0% { + opacity: 0.65; + } + 50%, + 100% { + opacity: 0.1; + } +} diff --git a/packages/amplify-ui-components/src/components/amplify-chatbot/readme.md b/packages/amplify-ui-components/src/components/amplify-chatbot/readme.md new file mode 100644 index 00000000000..f05102b015b --- /dev/null +++ b/packages/amplify-ui-components/src/components/amplify-chatbot/readme.md @@ -0,0 +1,58 @@ +# amplify-chatbot + + + + + + +## Properties + +| Property | Attribute | Description | Type | Default | +| -------------------- | ---------------------- | ------------------------------------------------------------------------- | --------- | ---------------------------- | +| `botName` | `bot-name` | Name of the bot | `string` | `undefined` | +| `botTitle` | `bot-title` | Text placed in the top header | `string` | `Translations.CHATBOT_TITLE` | +| `clearOnComplete` | `clear-on-complete` | Clear messages when conversation finishes | `boolean` | `false` | +| `conversationModeOn` | `conversation-mode-on` | Continue listening to users after they send the message | `boolean` | `false` | +| `silenceThreshold` | `silence-threshold` | Noise threshold between -1 and 1. Anything below is considered a silence. | `number` | `0.2` | +| `silenceTime` | `silence-time` | Amount of silence (in ms) to wait for | `number` | `1500` | +| `textEnabled` | `text-enabled` | Whether text chat is enabled | `boolean` | `true` | +| `voiceEnabled` | `voice-enabled` | Whether voice chat is enabled | `boolean` | `false` | +| `welcomeMessage` | `welcome-message` | Greeting message displayed to users | `string` | `undefined` | + + +## Events + +| Event | Description | Type | +| --------------- | -------------------------------------------- | ------------------------- | +| `chatCompleted` | Event emitted when conversation is completed | `CustomEvent` | + + +## Slots + +| Slot | Description | +| ---------- | ------------------------------- | +| `"header"` | title content placed at the top | + + +## Dependencies + +### Depends on + +- [amplify-button](../amplify-button) +- [amplify-input](../amplify-input) +- [amplify-toast](../amplify-toast) + +### Graph +```mermaid +graph TD; + amplify-chatbot --> amplify-button + amplify-chatbot --> amplify-input + amplify-chatbot --> amplify-toast + amplify-button --> amplify-icon + amplify-toast --> amplify-icon + style amplify-chatbot fill:#f9f,stroke:#333,stroke-width:4px +``` + +---------------------------------------------- + +*Built with [StencilJS](https://stenciljs.com/)* diff --git a/packages/amplify-ui-components/src/components/amplify-confirm-sign-in/readme.md b/packages/amplify-ui-components/src/components/amplify-confirm-sign-in/readme.md index 2532deafb6b..704aca3d27e 100644 --- a/packages/amplify-ui-components/src/components/amplify-confirm-sign-in/readme.md +++ b/packages/amplify-ui-components/src/components/amplify-confirm-sign-in/readme.md @@ -36,6 +36,7 @@ graph TD; amplify-form-section --> amplify-section amplify-form-section --> amplify-button amplify-form-section --> amplify-loading-spinner + amplify-button --> amplify-icon amplify-loading-spinner --> amplify-icon amplify-auth-fields --> amplify-username-field amplify-auth-fields --> amplify-password-field diff --git a/packages/amplify-ui-components/src/components/amplify-confirm-sign-up/readme.md b/packages/amplify-ui-components/src/components/amplify-confirm-sign-up/readme.md index f0aeea45dad..c4bb9494f30 100644 --- a/packages/amplify-ui-components/src/components/amplify-confirm-sign-up/readme.md +++ b/packages/amplify-ui-components/src/components/amplify-confirm-sign-up/readme.md @@ -34,6 +34,7 @@ graph TD; amplify-confirm-sign-up --> amplify-button amplify-confirm-sign-up --> amplify-form-section amplify-confirm-sign-up --> amplify-auth-fields + amplify-button --> amplify-icon amplify-form-section --> amplify-section amplify-form-section --> amplify-button amplify-form-section --> amplify-loading-spinner diff --git a/packages/amplify-ui-components/src/components/amplify-federated-sign-in/readme.md b/packages/amplify-ui-components/src/components/amplify-federated-sign-in/readme.md index 5a533fdfbeb..327164f9c40 100644 --- a/packages/amplify-ui-components/src/components/amplify-federated-sign-in/readme.md +++ b/packages/amplify-ui-components/src/components/amplify-federated-sign-in/readme.md @@ -28,6 +28,7 @@ graph TD; amplify-form-section --> amplify-section amplify-form-section --> amplify-button amplify-form-section --> amplify-loading-spinner + amplify-button --> amplify-icon amplify-loading-spinner --> amplify-icon amplify-federated-buttons --> amplify-google-button amplify-federated-buttons --> amplify-facebook-button diff --git a/packages/amplify-ui-components/src/components/amplify-forgot-password/readme.md b/packages/amplify-ui-components/src/components/amplify-forgot-password/readme.md index 2c91f148281..ec2cab7812a 100644 --- a/packages/amplify-ui-components/src/components/amplify-forgot-password/readme.md +++ b/packages/amplify-ui-components/src/components/amplify-forgot-password/readme.md @@ -38,6 +38,7 @@ graph TD; amplify-form-section --> amplify-section amplify-form-section --> amplify-button amplify-form-section --> amplify-loading-spinner + amplify-button --> amplify-icon amplify-loading-spinner --> amplify-icon amplify-auth-fields --> amplify-username-field amplify-auth-fields --> amplify-password-field diff --git a/packages/amplify-ui-components/src/components/amplify-form-section/readme.md b/packages/amplify-ui-components/src/components/amplify-form-section/readme.md index c374954fcca..678b18142f6 100644 --- a/packages/amplify-ui-components/src/components/amplify-form-section/readme.md +++ b/packages/amplify-ui-components/src/components/amplify-form-section/readme.md @@ -42,6 +42,7 @@ graph TD; amplify-form-section --> amplify-section amplify-form-section --> amplify-button amplify-form-section --> amplify-loading-spinner + amplify-button --> amplify-icon amplify-loading-spinner --> amplify-icon amplify-confirm-sign-in --> amplify-form-section amplify-confirm-sign-up --> amplify-form-section diff --git a/packages/amplify-ui-components/src/components/amplify-greetings/readme.md b/packages/amplify-ui-components/src/components/amplify-greetings/readme.md index 47107a7e886..85ed25c0442 100644 --- a/packages/amplify-ui-components/src/components/amplify-greetings/readme.md +++ b/packages/amplify-ui-components/src/components/amplify-greetings/readme.md @@ -36,6 +36,7 @@ graph TD; amplify-greetings --> amplify-nav amplify-greetings --> amplify-sign-out amplify-sign-out --> amplify-button + amplify-button --> amplify-icon style amplify-greetings fill:#f9f,stroke:#333,stroke-width:4px ``` diff --git a/packages/amplify-ui-components/src/components/amplify-icon-button/__snapshots__/amplify-icon-button.spec.ts.snap b/packages/amplify-ui-components/src/components/amplify-icon-button/__snapshots__/amplify-icon-button.spec.ts.snap index f3918328753..377e91ef6c2 100644 --- a/packages/amplify-ui-components/src/components/amplify-icon-button/__snapshots__/amplify-icon-button.spec.ts.snap +++ b/packages/amplify-ui-components/src/components/amplify-icon-button/__snapshots__/amplify-icon-button.spec.ts.snap @@ -28,6 +28,20 @@ exports[`amplify-icon-button spec: Render logic -> renders auth0 icon button cor `; +exports[`amplify-icon-button spec: Render logic -> renders ban icon button correctly 1`] = ` + + + + + + + + + +`; + exports[`amplify-icon-button spec: Render logic -> renders enter-vr icon button correctly 1`] = ` @@ -112,6 +126,20 @@ exports[`amplify-icon-button spec: Render logic -> renders maximize icon button `; +exports[`amplify-icon-button spec: Render logic -> renders microphone icon button correctly 1`] = ` + + + + + + + + + +`; + exports[`amplify-icon-button spec: Render logic -> renders minimize icon button correctly 1`] = ` @@ -140,6 +168,20 @@ exports[`amplify-icon-button spec: Render logic -> renders photoPlaceholder icon `; +exports[`amplify-icon-button spec: Render logic -> renders send icon button correctly 1`] = ` + + + + + + + + + +`; + exports[`amplify-icon-button spec: Render logic -> renders sound icon button correctly 1`] = ` diff --git a/packages/amplify-ui-components/src/components/amplify-icon-button/readme.md b/packages/amplify-ui-components/src/components/amplify-icon-button/readme.md index bfdb7363d23..f7b0cdba2a9 100644 --- a/packages/amplify-ui-components/src/components/amplify-icon-button/readme.md +++ b/packages/amplify-ui-components/src/components/amplify-icon-button/readme.md @@ -5,11 +5,11 @@ ## Properties -| Property | Attribute | Description | Type | Default | -| ----------------- | ------------------- | ------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- | -| `autoShowTooltip` | `auto-show-tooltip` | (Optional) Whether or not to show the tooltip automatically | `boolean` | `false` | -| `name` | `name` | The name of the icon used inside of the button | `"amazon" \| "auth0" \| "enter-vr" \| "exit-vr" \| "facebook" \| "google" \| "loading" \| "maximize" \| "minimize" \| "photoPlaceholder" \| "sound" \| "sound-mute" \| "warning"` | `undefined` | -| `tooltip` | `tooltip` | (Optional) The tooltip that will show on hover of the button | `string` | `null` | +| Property | Attribute | Description | Type | Default | +| ----------------- | ------------------- | ------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- | +| `autoShowTooltip` | `auto-show-tooltip` | (Optional) Whether or not to show the tooltip automatically | `boolean` | `false` | +| `name` | `name` | The name of the icon used inside of the button | `"amazon" \| "auth0" \| "ban" \| "enter-vr" \| "exit-vr" \| "facebook" \| "google" \| "loading" \| "maximize" \| "microphone" \| "minimize" \| "photoPlaceholder" \| "send" \| "sound" \| "sound-mute" \| "warning"` | `undefined` | +| `tooltip` | `tooltip` | (Optional) The tooltip that will show on hover of the button | `string` | `null` | ## Dependencies diff --git a/packages/amplify-ui-components/src/components/amplify-icon/__snapshots__/amplify-icon.spec.ts.snap b/packages/amplify-ui-components/src/components/amplify-icon/__snapshots__/amplify-icon.spec.ts.snap index f8f2f730998..d64b1e08809 100644 --- a/packages/amplify-ui-components/src/components/amplify-icon/__snapshots__/amplify-icon.spec.ts.snap +++ b/packages/amplify-ui-components/src/components/amplify-icon/__snapshots__/amplify-icon.spec.ts.snap @@ -2,168 +2,166 @@ exports[`amplify-icon spec: Render logic -> renders amazon correctly 1`] = ` - - - - - - - + + + + + `; exports[`amplify-icon spec: Render logic -> renders auth0 correctly 1`] = ` - - - - - + + + + +`; + +exports[`amplify-icon spec: Render logic -> renders ban correctly 1`] = ` + + + + `; exports[`amplify-icon spec: Render logic -> renders enter-vr correctly 1`] = ` - - - - - - + + + + - - + + `; exports[`amplify-icon spec: Render logic -> renders exit-vr correctly 1`] = ` - - - - - - + + + + - - + + `; exports[`amplify-icon spec: Render logic -> renders facebook correctly 1`] = ` - - - - - - + + + + - - + + `; exports[`amplify-icon spec: Render logic -> renders google correctly 1`] = ` - - - - - - - - + + + + + + `; exports[`amplify-icon spec: Render logic -> renders loading correctly 1`] = ` - - - - - - - - + + + + + + - - + + `; exports[`amplify-icon spec: Render logic -> renders maximize correctly 1`] = ` - - - - - - - + + + + + + +`; + +exports[`amplify-icon spec: Render logic -> renders microphone correctly 1`] = ` + + + + `; exports[`amplify-icon spec: Render logic -> renders minimize correctly 1`] = ` - - - - - - - + + + + + `; exports[`amplify-icon spec: Render logic -> renders photoPlaceholder correctly 1`] = ` - - - - - - - + + + + + + +`; + +exports[`amplify-icon spec: Render logic -> renders send correctly 1`] = ` + + + + `; exports[`amplify-icon spec: Render logic -> renders sound correctly 1`] = ` - - - - - - - + + + + + `; exports[`amplify-icon spec: Render logic -> renders sound-mute correctly 1`] = ` - - - - - - - + + + + + `; exports[`amplify-icon spec: Render logic -> renders warning correctly 1`] = ` - - - - - - + + + + - - + + `; diff --git a/packages/amplify-ui-components/src/components/amplify-icon/amplify-icon.scss b/packages/amplify-ui-components/src/components/amplify-icon/amplify-icon.scss index 1e3390f5434..b61ee492ed2 100644 --- a/packages/amplify-ui-components/src/components/amplify-icon/amplify-icon.scss +++ b/packages/amplify-ui-components/src/components/amplify-icon/amplify-icon.scss @@ -1,7 +1,11 @@ :host { + --width: auto; + --height: auto; --icon-fill-color: var(--amplify-white); } -.icon { +svg { fill: var(--icon-fill-color); + width: var(--width); + height: var(--height); } diff --git a/packages/amplify-ui-components/src/components/amplify-icon/amplify-icon.tsx b/packages/amplify-ui-components/src/components/amplify-icon/amplify-icon.tsx index 31859a8a2c7..1b3e11ae5d8 100644 --- a/packages/amplify-ui-components/src/components/amplify-icon/amplify-icon.tsx +++ b/packages/amplify-ui-components/src/components/amplify-icon/amplify-icon.tsx @@ -1,4 +1,4 @@ -import { Component, Prop, Watch, h } from '@stencil/core'; +import { Component, Prop, Watch } from '@stencil/core'; import { icons, IconNameType } from './icons'; @Component({ @@ -20,6 +20,6 @@ export class AmplifyIcon { // https://stenciljs.com/docs/templating-jsx#avoid-shared-jsx-nodes render() { - return {icons[this.name]()}; + return icons[this.name](); } } diff --git a/packages/amplify-ui-components/src/components/amplify-icon/icons.tsx b/packages/amplify-ui-components/src/components/amplify-icon/icons.tsx index 5d8752521b4..c6f8efce1b3 100644 --- a/packages/amplify-ui-components/src/components/amplify-icon/icons.tsx +++ b/packages/amplify-ui-components/src/components/amplify-icon/icons.tsx @@ -189,6 +189,30 @@ export const icons = { ); }, + + microphone() { + return ( + + + + ); + }, + + send() { + return ( + + + + ); + }, + + ban() { + return ( + + + + ); + }, }; export type IconNameType = keyof typeof icons; diff --git a/packages/amplify-ui-components/src/components/amplify-icon/readme.md b/packages/amplify-ui-components/src/components/amplify-icon/readme.md index fde691c4ede..06dade8549a 100644 --- a/packages/amplify-ui-components/src/components/amplify-icon/readme.md +++ b/packages/amplify-ui-components/src/components/amplify-icon/readme.md @@ -5,15 +5,16 @@ ## Properties -| Property | Attribute | Description | Type | Default | -| -------- | --------- | ----------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- | -| `name` | `name` | (Required) Name of icon used to determine the icon rendered | `"amazon" \| "auth0" \| "enter-vr" \| "exit-vr" \| "facebook" \| "google" \| "loading" \| "maximize" \| "minimize" \| "photoPlaceholder" \| "sound" \| "sound-mute" \| "warning"` | `undefined` | +| Property | Attribute | Description | Type | Default | +| -------- | --------- | ----------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- | +| `name` | `name` | (Required) Name of icon used to determine the icon rendered | `"amazon" \| "auth0" \| "ban" \| "enter-vr" \| "exit-vr" \| "facebook" \| "google" \| "loading" \| "maximize" \| "microphone" \| "minimize" \| "photoPlaceholder" \| "send" \| "sound" \| "sound-mute" \| "warning"` | `undefined` | ## Dependencies ### Used by + - [amplify-button](../amplify-button) - [amplify-icon-button](../amplify-icon-button) - [amplify-loading-spinner](../amplify-loading-spinner) - [amplify-photo-picker](../amplify-photo-picker) @@ -23,6 +24,7 @@ ### Graph ```mermaid graph TD; + amplify-button --> amplify-icon amplify-icon-button --> amplify-icon amplify-loading-spinner --> amplify-icon amplify-photo-picker --> amplify-icon diff --git a/packages/amplify-ui-components/src/components/amplify-input/amplify-input.scss b/packages/amplify-ui-components/src/components/amplify-input/amplify-input.scss index af47aec08ba..5f460f7559f 100644 --- a/packages/amplify-ui-components/src/components/amplify-input/amplify-input.scss +++ b/packages/amplify-ui-components/src/components/amplify-input/amplify-input.scss @@ -3,6 +3,8 @@ --background-color: var(--amplify-secondary-contrast); --border-color: var(--amplify-light-grey); --border-color-focus: var(--amplify-primary-color); + --border: 1px solid var(--border-color); + --margin: 0 0 0.625rem 0; } .input-host { @@ -17,10 +19,10 @@ color: var(--color); background-color: var(--background-color); background-image: none; - border: 1px solid var(--border-color); + border: var(--border); border-radius: 3px; box-sizing: border-box; - margin: 0 0 0.625rem 0; + margin: var(--margin); height: 3.125rem; line-height: 1.1; diff --git a/packages/amplify-ui-components/src/components/amplify-input/readme.md b/packages/amplify-ui-components/src/components/amplify-input/readme.md index ec9c74c03ba..167b7efa99e 100644 --- a/packages/amplify-ui-components/src/components/amplify-input/readme.md +++ b/packages/amplify-ui-components/src/components/amplify-input/readme.md @@ -29,6 +29,7 @@ ### Used by + - [amplify-chatbot](../amplify-chatbot) - [amplify-form-field](../amplify-form-field) - [amplify-phone-field](../amplify-phone-field) - [amplify-verify-contact](../amplify-verify-contact) @@ -36,6 +37,7 @@ ### Graph ```mermaid graph TD; + amplify-chatbot --> amplify-input amplify-form-field --> amplify-input amplify-phone-field --> amplify-input amplify-verify-contact --> amplify-input diff --git a/packages/amplify-ui-components/src/components/amplify-photo-picker/readme.md b/packages/amplify-ui-components/src/components/amplify-photo-picker/readme.md index 1bbfd5be7a9..973a8e794d7 100644 --- a/packages/amplify-ui-components/src/components/amplify-photo-picker/readme.md +++ b/packages/amplify-ui-components/src/components/amplify-photo-picker/readme.md @@ -36,6 +36,7 @@ graph TD; amplify-photo-picker --> amplify-icon amplify-photo-picker --> amplify-button amplify-picker --> amplify-button + amplify-button --> amplify-icon amplify-s3-image-picker --> amplify-photo-picker style amplify-photo-picker fill:#f9f,stroke:#333,stroke-width:4px ``` diff --git a/packages/amplify-ui-components/src/components/amplify-picker/readme.md b/packages/amplify-ui-components/src/components/amplify-picker/readme.md index 1c30bc85bfb..9b3c57e8181 100644 --- a/packages/amplify-ui-components/src/components/amplify-picker/readme.md +++ b/packages/amplify-ui-components/src/components/amplify-picker/readme.md @@ -28,6 +28,7 @@ ```mermaid graph TD; amplify-picker --> amplify-button + amplify-button --> amplify-icon amplify-photo-picker --> amplify-picker amplify-s3-album --> amplify-picker amplify-s3-text-picker --> amplify-picker diff --git a/packages/amplify-ui-components/src/components/amplify-require-new-password/readme.md b/packages/amplify-ui-components/src/components/amplify-require-new-password/readme.md index 2a3cba69f7f..51a2c203549 100644 --- a/packages/amplify-ui-components/src/components/amplify-require-new-password/readme.md +++ b/packages/amplify-ui-components/src/components/amplify-require-new-password/readme.md @@ -36,6 +36,7 @@ graph TD; amplify-form-section --> amplify-section amplify-form-section --> amplify-button amplify-form-section --> amplify-loading-spinner + amplify-button --> amplify-icon amplify-loading-spinner --> amplify-icon amplify-auth-fields --> amplify-username-field amplify-auth-fields --> amplify-password-field diff --git a/packages/amplify-ui-components/src/components/amplify-s3-album/readme.md b/packages/amplify-ui-components/src/components/amplify-s3-album/readme.md index f19d6c07d4f..675314ffc95 100644 --- a/packages/amplify-ui-components/src/components/amplify-s3-album/readme.md +++ b/packages/amplify-ui-components/src/components/amplify-s3-album/readme.md @@ -36,6 +36,7 @@ graph TD; amplify-s3-album --> amplify-s3-image amplify-s3-album --> amplify-picker amplify-picker --> amplify-button + amplify-button --> amplify-icon style amplify-s3-album fill:#f9f,stroke:#333,stroke-width:4px ``` diff --git a/packages/amplify-ui-components/src/components/amplify-s3-image-picker/readme.md b/packages/amplify-ui-components/src/components/amplify-s3-image-picker/readme.md index 81943228183..c5d8ef319fa 100644 --- a/packages/amplify-ui-components/src/components/amplify-s3-image-picker/readme.md +++ b/packages/amplify-ui-components/src/components/amplify-s3-image-picker/readme.md @@ -36,6 +36,7 @@ graph TD; amplify-photo-picker --> amplify-icon amplify-photo-picker --> amplify-button amplify-picker --> amplify-button + amplify-button --> amplify-icon style amplify-s3-image-picker fill:#f9f,stroke:#333,stroke-width:4px ``` diff --git a/packages/amplify-ui-components/src/components/amplify-s3-text-picker/readme.md b/packages/amplify-ui-components/src/components/amplify-s3-text-picker/readme.md index 2c7bc987edb..d62c09969ad 100644 --- a/packages/amplify-ui-components/src/components/amplify-s3-text-picker/readme.md +++ b/packages/amplify-ui-components/src/components/amplify-s3-text-picker/readme.md @@ -31,6 +31,7 @@ graph TD; amplify-s3-text-picker --> amplify-s3-text amplify-s3-text-picker --> amplify-picker amplify-picker --> amplify-button + amplify-button --> amplify-icon style amplify-s3-text-picker fill:#f9f,stroke:#333,stroke-width:4px ``` diff --git a/packages/amplify-ui-components/src/components/amplify-select-mfa-type/readme.md b/packages/amplify-ui-components/src/components/amplify-select-mfa-type/readme.md index 2f399a8600c..1126d910ec4 100644 --- a/packages/amplify-ui-components/src/components/amplify-select-mfa-type/readme.md +++ b/packages/amplify-ui-components/src/components/amplify-select-mfa-type/readme.md @@ -29,6 +29,7 @@ graph TD; amplify-form-section --> amplify-section amplify-form-section --> amplify-button amplify-form-section --> amplify-loading-spinner + amplify-button --> amplify-icon amplify-loading-spinner --> amplify-icon amplify-radio-button --> amplify-label amplify-totp-setup --> amplify-form-section diff --git a/packages/amplify-ui-components/src/components/amplify-sign-in/readme.md b/packages/amplify-ui-components/src/components/amplify-sign-in/readme.md index 71516df407f..e267528a813 100644 --- a/packages/amplify-ui-components/src/components/amplify-sign-in/readme.md +++ b/packages/amplify-ui-components/src/components/amplify-sign-in/readme.md @@ -51,6 +51,7 @@ graph TD; amplify-sign-in --> amplify-strike amplify-sign-in --> amplify-auth-fields amplify-sign-in --> amplify-loading-spinner + amplify-button --> amplify-icon amplify-form-section --> amplify-section amplify-form-section --> amplify-button amplify-form-section --> amplify-loading-spinner diff --git a/packages/amplify-ui-components/src/components/amplify-sign-out/readme.md b/packages/amplify-ui-components/src/components/amplify-sign-out/readme.md index 699179dced8..bf48b2f601b 100644 --- a/packages/amplify-ui-components/src/components/amplify-sign-out/readme.md +++ b/packages/amplify-ui-components/src/components/amplify-sign-out/readme.md @@ -25,6 +25,7 @@ ```mermaid graph TD; amplify-sign-out --> amplify-button + amplify-button --> amplify-icon amplify-greetings --> amplify-sign-out style amplify-sign-out fill:#f9f,stroke:#333,stroke-width:4px ``` diff --git a/packages/amplify-ui-components/src/components/amplify-sign-up/readme.md b/packages/amplify-ui-components/src/components/amplify-sign-up/readme.md index c50d5986663..10d274d1734 100644 --- a/packages/amplify-ui-components/src/components/amplify-sign-up/readme.md +++ b/packages/amplify-ui-components/src/components/amplify-sign-up/readme.md @@ -51,6 +51,7 @@ graph TD; amplify-form-section --> amplify-section amplify-form-section --> amplify-button amplify-form-section --> amplify-loading-spinner + amplify-button --> amplify-icon amplify-loading-spinner --> amplify-icon amplify-auth-fields --> amplify-username-field amplify-auth-fields --> amplify-password-field diff --git a/packages/amplify-ui-components/src/components/amplify-toast/readme.md b/packages/amplify-ui-components/src/components/amplify-toast/readme.md index 06f011d9227..eb7b50adffb 100644 --- a/packages/amplify-ui-components/src/components/amplify-toast/readme.md +++ b/packages/amplify-ui-components/src/components/amplify-toast/readme.md @@ -16,6 +16,7 @@ ### Used by - [amplify-authenticator](../amplify-authenticator) + - [amplify-chatbot](../amplify-chatbot) ### Depends on @@ -26,6 +27,7 @@ graph TD; amplify-toast --> amplify-icon amplify-authenticator --> amplify-toast + amplify-chatbot --> amplify-toast style amplify-toast fill:#f9f,stroke:#333,stroke-width:4px ``` diff --git a/packages/amplify-ui-components/src/components/amplify-totp-setup/readme.md b/packages/amplify-ui-components/src/components/amplify-totp-setup/readme.md index 8bf0c07d3fe..384fd2f200e 100644 --- a/packages/amplify-ui-components/src/components/amplify-totp-setup/readme.md +++ b/packages/amplify-ui-components/src/components/amplify-totp-setup/readme.md @@ -33,6 +33,7 @@ graph TD; amplify-form-section --> amplify-section amplify-form-section --> amplify-button amplify-form-section --> amplify-loading-spinner + amplify-button --> amplify-icon amplify-loading-spinner --> amplify-icon amplify-form-field --> amplify-label amplify-form-field --> amplify-input diff --git a/packages/amplify-ui-components/src/components/amplify-verify-contact/readme.md b/packages/amplify-ui-components/src/components/amplify-verify-contact/readme.md index 64e4800ace0..a75b06031f4 100644 --- a/packages/amplify-ui-components/src/components/amplify-verify-contact/readme.md +++ b/packages/amplify-ui-components/src/components/amplify-verify-contact/readme.md @@ -35,6 +35,7 @@ graph TD; amplify-form-section --> amplify-section amplify-form-section --> amplify-button amplify-form-section --> amplify-loading-spinner + amplify-button --> amplify-icon amplify-loading-spinner --> amplify-icon amplify-authenticator --> amplify-verify-contact style amplify-verify-contact fill:#f9f,stroke:#333,stroke-width:4px diff --git a/packages/amplify-ui-components/src/global/theme.ts b/packages/amplify-ui-components/src/global/theme.ts index c7f9b47745e..c22dbd5a8a5 100644 --- a/packages/amplify-ui-components/src/global/theme.ts +++ b/packages/amplify-ui-components/src/global/theme.ts @@ -54,8 +54,8 @@ if (browserOrNode().isBrowser) { --amplify-light-grey: #c4c4c4; --amplify-white: #ffffff; --amplify-smoke-white: #f5f5f5; - --amplify-red: #dd3f5b; + --amplify-blue: #099ac8; } `), ); diff --git a/packages/amplify-ui-components/stencil.config.ts b/packages/amplify-ui-components/stencil.config.ts index 6e449aaf5c8..6e17326c604 100644 --- a/packages/amplify-ui-components/stencil.config.ts +++ b/packages/amplify-ui-components/stencil.config.ts @@ -14,7 +14,13 @@ export const config: Config = { plugins: [ externals({ // deps to include in externals (default: []) - include: ['@aws-amplify/auth', '@aws-amplify/core', '@aws-amplify/storage', '@aws-amplify/xr'], + include: [ + '@aws-amplify/auth', + '@aws-amplify/core', + '@aws-amplify/storage', + '@aws-amplify/xr', + '@aws-amplify/interactions', + ], }), nodePolyfills(), sass({ diff --git a/packages/aws-amplify-react/src/Interactions/ChatBot.tsx b/packages/aws-amplify-react/src/Interactions/ChatBot.tsx index aa156b4c8d7..b2a6f812816 100644 --- a/packages/aws-amplify-react/src/Interactions/ChatBot.tsx +++ b/packages/aws-amplify-react/src/Interactions/ChatBot.tsx @@ -97,7 +97,6 @@ export class ChatBot extends React.Component { constructor(props) { super(props); - if (this.props.voiceEnabled) { require('./aws-lex-audio'); // @ts-ignore diff --git a/packages/interactions/src/Interactions.ts b/packages/interactions/src/Interactions.ts index 01a402118d6..b5c6e269d56 100644 --- a/packages/interactions/src/Interactions.ts +++ b/packages/interactions/src/Interactions.ts @@ -104,11 +104,14 @@ export class InteractionsClass { botname: string, message: InteractionsMessage ): Promise; - public async send(botname: string, message: object): Promise; + public async send( + botname: string, + message: object + ): Promise; public async send( botname: string, message: string | object - ): Promise { + ): Promise { if (!this._options.bots || !this._options.bots[botname]) { throw new Error('Bot ' + botname + ' does not exist'); } diff --git a/packages/interactions/src/index.ts b/packages/interactions/src/index.ts index c5cea4961cf..06d7057e430 100644 --- a/packages/interactions/src/index.ts +++ b/packages/interactions/src/index.ts @@ -17,6 +17,7 @@ import { Interactions } from './Interactions'; */ export default Interactions; +export * from './types'; export * from './Providers/AWSLexProvider'; export { Interactions };