Skip to content

Commit

Permalink
feat(@aws-amplify/ui-components): Add Chatbot UI to main (aws-amplify…
Browse files Browse the repository at this point in the history
…#6684)

* feat(@aws-amplify/ui-components): Add ChatBot Component

Fixes: aws-amplify#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 (aws-amplify#6648)

* Enable publish from ui-preview branch

* Revert checkout

Co-authored-by: Jordan Ranz <jordanmranz@gmail.com>

* fix(@aws-amplify/ui-components): Update scss and reset chat state upon finish  (aws-amplify#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 (aws-amplify#6678)

Co-authored-by: Ashika <35131273+ashika01@users.noreply.github.com>
Co-authored-by: Jordan Ranz <jordanmranz@gmail.com>
  • Loading branch information
3 people authored and nubpro committed Oct 2, 2020
1 parent 90e8b81 commit ba6f98e
Show file tree
Hide file tree
Showing 57 changed files with 1,452 additions and 130 deletions.
59 changes: 58 additions & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -618,7 +657,7 @@ releasable_branches: &releasable_branches
only:
- release
- main
- ui-components/master
- ui-components/main
- 1.0-stable

workflows:
Expand Down Expand Up @@ -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
Expand Down
10 changes: 8 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand All @@ -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",
Expand Down
2 changes: 2 additions & 0 deletions packages/amplify-ui-angular/src/amplify-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
AmplifyAuthenticator,
AmplifyAuthFields,
AmplifyButton,
AmplifyChatbot,
AmplifyCheckbox,
AmplifyCodeField,
AmplifyConfirmSignIn,
Expand Down Expand Up @@ -65,6 +66,7 @@ const DECLARATIONS = [
AmplifyAuthenticator,
AmplifyAuthFields,
AmplifyButton,
AmplifyChatbot,
AmplifyCheckbox,
AmplifyCodeField,
AmplifyConfirmSignIn,
Expand Down
1 change: 1 addition & 0 deletions packages/amplify-ui-components/Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`

Expand Down
12 changes: 10 additions & 2 deletions packages/amplify-ui-components/src/common/Translations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
127 changes: 127 additions & 0 deletions packages/amplify-ui-components/src/common/audio-control/helper.ts
Original file line number Diff line number Diff line change
@@ -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;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './recorder';
export * from './helper';
export * from './visualizer';
Loading

0 comments on commit ba6f98e

Please sign in to comment.