├── src
├── polyfill.js
├── index.css
├── reducers
│ ├── index.js
│ ├── events.js
│ ├── users.js
│ └── app.js
├── constants
│ └── chatActionTypes.js
├── components
│ ├── Minimized.js
│ ├── Maximized.js
│ └── App.js
├── index.js
├── store.js
├── actions
│ └── chatActions.js
├── containers
│ └── AppContainer.js
├── logo.svg
├── registerServiceWorker.js
└── sagas
│ └── index.js
├── public
├── favicon.ico
├── manifest.json
└── index.html
├── .env
├── .gitignore
├── README.md
└── package.json
/src/polyfill.js:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line no-native-reassign
2 | window.global = window
3 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | padding: 0;
4 | font-family: sans-serif;
5 | }
6 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/livechat/chat-widget-sample/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/.env:
--------------------------------------------------------------------------------
1 | REACT_APP_BOTENGINE_CLIENT_TOKEN=4cc01931fa302f2b50bcdeb70e961fc47d156face3bdb8b616e3e9f3e18e0cc8
2 | REACT_APP_BOTENGINE_STORY_ID=5a439b8bb9677d000790134d
3 | REACT_APP_LIVECHAT_LICENSE=1520
4 |
--------------------------------------------------------------------------------
/src/reducers/index.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux'
2 | import events from './events'
3 | import users from './users'
4 | import app from './app'
5 |
6 | const rootReducer = combineReducers({
7 | app,
8 | events,
9 | users,
10 | })
11 |
12 | export default rootReducer
13 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 |
6 | # testing
7 | /coverage
8 |
9 | # production
10 | /build
11 |
12 | # misc
13 | .DS_Store
14 | .env.local
15 | .env.development.local
16 | .env.test.local
17 | .env.production.local
18 |
19 | npm-debug.log*
20 | yarn-debug.log*
21 | yarn-error.log*
22 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": "./index.html",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/src/constants/chatActionTypes.js:
--------------------------------------------------------------------------------
1 | export const NEW_MESSAGE = 'NEW_MESSAGE'
2 | export const CHAT_STARTED = 'CHAT_STARTED'
3 | export const CHAT_ENDED = 'CHAT_ENDED'
4 | export const SEND_MESSAGE = 'SEND_MESSAGE'
5 | export const NEW_USER = 'NEW_AGENT'
6 | export const OWN_DATA_RECEIVED = 'OWN_DATA_RECEIVED'
7 | export const CHANGE_CHAT_SERVICE = 'CHANGE_CHAT_SERVICE'
8 | export const RATE_GOOD = 'RATE_GOOD'
9 | export const RATE_BAD = 'RATE_BAD'
10 | export const CHAT_RATED = 'CHAT_RATED'
11 |
--------------------------------------------------------------------------------
/src/components/Minimized.js:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { IconButton, ChatIcon } from '@livechat/ui-kit'
3 |
4 | const Minimized = ({ maximize }) => (
5 |
19 |
20 |
21 |
22 |
23 | )
24 |
25 | export default Minimized
26 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { render } from 'react-dom'
3 | import { PersistGate } from 'redux-persist/lib/integration/react'
4 | import { Provider } from 'react-redux'
5 | import configureStore from './store'
6 | import rootSaga from './sagas'
7 | import AppContainer from './containers/AppContainer'
8 |
9 | const { store, persistor } = configureStore()
10 | store.runSaga(rootSaga, store)
11 |
12 | const root = document.getElementById('root')
13 |
14 | render(
15 |
16 |
17 |
18 |
19 | ,
20 | root,
21 | )
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Chat widget sample
2 | Sample chat widget built with [LiveChat React chat UI kit](https://docs.livechatinc.com/react-chat-ui-kit/). In this widget, [BotEngine](https://www.botengine.ai/) handles the incoming chats. When the bot returns `LiveChat.transfer` action, the chat is transferred to a human agent together with the transcript of the initial conversation with
3 | the bot.
4 |
5 | The sample app uses [Visitor SDK](https://docs.livechatinc.com/visitor-sdk/) to communicate with LiveChat and [the API](https://docs.botengine.ai/api/introduction) to connect with BotEngine.
6 |
7 | ## Installation
8 |
9 | Copy `.env` file as `.env.local` and fill required variables:
10 |
11 | * REACT_APP_BOTENGINE_CLIENT_TOKEN - client token for [BotEngine](https://www.botengine.ai/) account
12 | * REACT_APP_BOTENGINE_STORY_ID - id of choosen botengine story
13 | * REACT_APP_LIVECHAT_LICENSE - [LiveChat](https://livechatinc.com) license number
14 |
15 |
--------------------------------------------------------------------------------
/src/reducers/events.js:
--------------------------------------------------------------------------------
1 | import { createReducer } from 'redux-create-reducer'
2 | import * as actionTypes from '../constants/chatActionTypes'
3 |
4 | const initialState = [
5 | {
6 | id: 'bot-message',
7 | authorId: 'bot',
8 | text: 'Hello, how can I help you?',
9 | timestamp: Date.now(),
10 | },
11 | ]
12 |
13 | export default createReducer(initialState, {
14 | [actionTypes.NEW_MESSAGE](state, action) {
15 | if (action.payload.customId === 'VISITOR_CHAT_HISTORY') {
16 | return state
17 | }
18 | const foundEvent = state.filter(event => {
19 | return (
20 | (event.customId && event.customId === action.payload.customId) || (event.id && event.id === action.payload.id)
21 | )
22 | })
23 | if (!foundEvent.length) {
24 | return [...state, action.payload]
25 | }
26 | return state
27 | },
28 | [actionTypes.SEND_MESSAGE](state, action) {
29 | if (action.payload.customId === 'VISITOR_CHAT_HISTORY') {
30 | return state
31 | }
32 | return [
33 | ...state,
34 | {
35 | ...action.payload,
36 | status: 'SENDING',
37 | own: true,
38 | },
39 | ]
40 | },
41 | })
42 | export const getEvents = state => state.events
43 |
--------------------------------------------------------------------------------
/src/store.js:
--------------------------------------------------------------------------------
1 | import { applyMiddleware, createStore, compose } from 'redux'
2 | import { persistStore, persistCombineReducers } from 'redux-persist'
3 | import storage from 'redux-persist/es/storage'
4 | import createSagaMiddleware from 'redux-saga'
5 | import events from './reducers/events'
6 | import users from './reducers/users'
7 | import app from './reducers/app'
8 |
9 | const config = {
10 | key: 'root',
11 | storage,
12 | }
13 |
14 | const reducer = persistCombineReducers(config, {
15 | app,
16 | events,
17 | users,
18 | })
19 |
20 | const sagaMiddleware = createSagaMiddleware()
21 |
22 | const composeCreateStore = () =>
23 | compose(applyMiddleware(sagaMiddleware), window.devToolsExtension ? window.devToolsExtension() : fn => fn)(
24 | createStore,
25 | )
26 |
27 | const configureStore = port => {
28 | const finalCreateStore = composeCreateStore(port)
29 | const store = {
30 | ...finalCreateStore(reducer),
31 | runSaga: sagaMiddleware.run,
32 | }
33 |
34 | // if (module.hot) {
35 | // module.hot.accept('./reducers', () => store.replaceReducer(reducer))
36 | // }
37 | const persistor = persistStore(store)
38 | return { persistor, store }
39 | }
40 |
41 | export default configureStore
42 |
--------------------------------------------------------------------------------
/src/reducers/users.js:
--------------------------------------------------------------------------------
1 | import { createReducer } from 'redux-create-reducer'
2 | import * as actionTypes from '../constants/chatActionTypes'
3 |
4 | const initialState = {
5 | byIds: {
6 | bot: {
7 | id: 'bot',
8 | name: 'Bot',
9 | avatarUrl:
10 | 'https://static.staging.livechatinc.com/1520/P10064EDGF/7970c9d036275c2ee9282d15535ef57b/botengine-avatar.png',
11 | },
12 | },
13 | ownId: null,
14 | currentAgent: 'bot',
15 | }
16 |
17 | export default createReducer(initialState, {
18 | [actionTypes.NEW_USER](state, action) {
19 | return {
20 | ...state,
21 | currentAgent: action.payload.id,
22 | byIds: {
23 | ...state.byIds,
24 | [action.payload.id]: action.payload,
25 | },
26 | }
27 | },
28 | [actionTypes.OWN_DATA_RECEIVED](state, action) {
29 | return {
30 | ...state,
31 | ownId: action.payload.id,
32 | byIds: {
33 | ...state.byIds,
34 | [action.payload.id]: {
35 | ...action.payload,
36 | name: 'Client',
37 | },
38 | },
39 | }
40 | },
41 | })
42 |
43 | export const getUsers = state => state.users.byIds
44 | export const getOwnId = state => state.users.ownId
45 | export const getCurrentAgent = state => state.users.byIds[state.users.currentAgent]
46 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "chat-widget-sample",
3 | "version": "0.0.1",
4 | "description": "Sample chat widget built with UI kit",
5 | "main": "index.js",
6 | "repository": {
7 | "type": "git",
8 | "url": "git+https://github.com/livechat/chat-widget-sample.git"
9 | },
10 | "keywords": [
11 | "livechat",
12 | "chat",
13 | "widget"
14 | ],
15 | "contributors": [
16 | "Konrad Kruk ",
17 | "Mateusz Burzyński "
18 | ],
19 | "license": "MIT",
20 | "bugs": {
21 | "url": "https://github.com/livechat/chat-widget-sample/issues"
22 | },
23 | "homepage": "https://github.com/livechat/chat-widget-sample#readme",
24 | "dependencies": {
25 | "@livechat/livechat-visitor-sdk": "0.35.2",
26 | "@livechat/ui-kit": "0.2.13",
27 | "react": "^15.5.4",
28 | "react-dom": "^15.5.4",
29 | "react-redux": "^5.0.2",
30 | "redux": "^3.6.0",
31 | "redux-create-reducer": "^1.1.0",
32 | "redux-persist": "^5.4.0",
33 | "redux-saga": "^0.15.6",
34 | "@livechat/saga-utils": "0.1.1"
35 | },
36 | "devDependencies": {
37 | "react-scripts": "1.0.17"
38 | },
39 | "scripts": {
40 | "start": "react-scripts start",
41 | "build": "react-scripts build",
42 | "test": "react-scripts test --env=jsdom",
43 | "eject": "react-scripts eject"
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/reducers/app.js:
--------------------------------------------------------------------------------
1 | import { createReducer } from 'redux-create-reducer'
2 | import * as chatActionTypes from '../constants/chatActionTypes'
3 |
4 | const initialState = {
5 | chatState: 'NOT_CHATTING',
6 | widgetState: 'MAXIMIZED',
7 | chatService: 'botEngine',
8 | rate: 'none',
9 | }
10 |
11 | export default createReducer(initialState, {
12 | [chatActionTypes.CHAT_STARTED](state) {
13 | return {
14 | ...state,
15 | chatState: 'CHATTING',
16 | chatService: 'LiveChat',
17 | }
18 | },
19 | [chatActionTypes.CHANGE_CHAT_SERVICE](state, { payload }) {
20 | return {
21 | ...state,
22 | chatService: payload.chatService,
23 | }
24 | },
25 | [chatActionTypes.CHAT_ENDED](state) {
26 | return {
27 | ...state,
28 | chatState: 'ENDED',
29 | }
30 | },
31 | [chatActionTypes.CHAT_RATED](state, { payload }) {
32 | return {
33 | ...state,
34 | rate: payload.rate || state.rate,
35 | }
36 | },
37 | [chatActionTypes.RATE_GOOD](state, { payload }) {
38 | return {
39 | ...state,
40 | rate: 'good',
41 | }
42 | },
43 | [chatActionTypes.RATE_BAD](state, { payload }) {
44 | return {
45 | ...state,
46 | rate: 'bad',
47 | }
48 | },
49 | })
50 |
51 | export const getChatState = state => state.app.chatState
52 | export const getWidgetState = state => state.app.widgetState
53 | export const getChatService = state => state.app.chatService
54 | export const getRate = state => state.app.rate
55 |
--------------------------------------------------------------------------------
/src/actions/chatActions.js:
--------------------------------------------------------------------------------
1 | import * as actionTypes from '../constants/chatActionTypes'
2 | export const newMessage = ({ id, authorId, customId, text, buttons, title, imageUrl, timestamp }) => ({
3 | type: actionTypes.NEW_MESSAGE,
4 | payload: {
5 | id,
6 | authorId,
7 | customId,
8 | text,
9 | buttons,
10 | title,
11 | imageUrl,
12 | timestamp,
13 | },
14 | })
15 |
16 | export const sendMessage = ({ text, customId }) => ({
17 | type: actionTypes.SEND_MESSAGE,
18 | payload: {
19 | text,
20 | customId: customId || String(Math.random()),
21 | timestamp: new Date(),
22 | },
23 | })
24 |
25 | export const newUser = ({ id, name, email, avatarUrl }) => ({
26 | type: actionTypes.NEW_USER,
27 | payload: {
28 | id,
29 | name,
30 | email,
31 | avatarUrl,
32 | },
33 | })
34 |
35 | export const ownDataReceived = ({ id }) => ({
36 | type: actionTypes.OWN_DATA_RECEIVED,
37 | payload: {
38 | id,
39 | },
40 | })
41 |
42 | export const chatEnded = () => ({
43 | type: actionTypes.CHAT_ENDED,
44 | })
45 |
46 | export const chatStarted = ({ chatId }) => ({
47 | type: actionTypes.CHAT_STARTED,
48 | payload: {
49 | chatId,
50 | },
51 | })
52 |
53 | export const changeChatService = ({ chatService }) => ({
54 | type: actionTypes.CHANGE_CHAT_SERVICE,
55 | payload: {
56 | chatService,
57 | },
58 | })
59 |
60 | export const rateGood = () => ({
61 | type: actionTypes.RATE_GOOD,
62 | })
63 |
64 | export const rateBad = () => ({
65 | type: actionTypes.RATE_BAD,
66 | })
67 |
68 | export const chatRated = ({ rate }) => ({
69 | type: actionTypes.CHAT_RATED,
70 | payload: {
71 | rate,
72 | },
73 | })
74 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
14 |
23 | React App
24 |
30 |
31 |
32 |
33 | You need to enable JavaScript to run this app.
34 |
35 |
36 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/src/containers/AppContainer.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux'
2 | import App from '../components/App'
3 | import { getEvents } from '../reducers/events'
4 | import { getUsers, getOwnId, getCurrentAgent } from '../reducers/users'
5 | import { getRate, getChatState } from '../reducers/app'
6 | import { sendMessage, rateGood, rateBad } from '../actions/chatActions'
7 |
8 | const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
9 |
10 | const parseTimestamp = timestamp => {
11 | const date = new Date(timestamp)
12 | return `${ date.getDate() } ${ months[date.getMonth()] } ${ date.getHours() }:${ date.getMinutes() }`
13 | }
14 |
15 | const parseMessages = messages =>
16 | messages
17 | .map(message => ({
18 | ...message,
19 | parsedDate: parseTimestamp(message.timestamp),
20 | }))
21 | .reduce(
22 | (result, current) => {
23 | const previous = result[result.length - 1]
24 | if (!previous.length || previous[previous.length - 1].authorId === current.authorId) {
25 | result[result.length - 1].push(current)
26 | return result
27 | }
28 | result.push([current])
29 | return result
30 | },
31 | [[]],
32 | )
33 |
34 | const mapStateToProps = state => {
35 | return {
36 | events: parseMessages(getEvents(state)),
37 | users: getUsers(state),
38 | ownId: getOwnId(state),
39 | currentAgent: getCurrentAgent(state),
40 | rate: getRate(state),
41 | chatState: getChatState(state),
42 | }
43 | }
44 |
45 | const mapDispatchToProps = dispatch => ({
46 | onMessageSend: data => {
47 | dispatch(
48 | sendMessage({
49 | text: data,
50 | }),
51 | )
52 | },
53 | sendMessage: text =>
54 | dispatch(
55 | sendMessage({
56 | text,
57 | }),
58 | ),
59 | rateGood: () => {
60 | dispatch(rateGood())
61 | },
62 | rateBad: () => {
63 | dispatch(rateBad())
64 | }
65 | })
66 |
67 | const AppContainer = connect(mapStateToProps, mapDispatchToProps)(App)
68 | export default AppContainer
69 |
--------------------------------------------------------------------------------
/src/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/src/registerServiceWorker.js:
--------------------------------------------------------------------------------
1 | // In production, we register a service worker to serve assets from local cache.
2 |
3 | // This lets the app load faster on subsequent visits in production, and gives
4 | // it offline capabilities. However, it also means that developers (and users)
5 | // will only see deployed updates on the "N+1" visit to a page, since previously
6 | // cached resources are updated in the background.
7 |
8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy.
9 | // This link also includes instructions on opting out of this behavior.
10 |
11 | const isLocalhost = Boolean(
12 | window.location.hostname === 'localhost' ||
13 | // [::1] is the IPv6 localhost address.
14 | window.location.hostname === '[::1]' ||
15 | // 127.0.0.1/8 is considered localhost for IPv4.
16 | window.location.hostname.match(
17 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
18 | )
19 | );
20 |
21 | export default function register() {
22 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
23 | // The URL constructor is available in all browsers that support SW.
24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location);
25 | if (publicUrl.origin !== window.location.origin) {
26 | // Our service worker won't work if PUBLIC_URL is on a different origin
27 | // from what our page is served on. This might happen if a CDN is used to
28 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374
29 | return;
30 | }
31 |
32 | window.addEventListener('load', () => {
33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
34 |
35 | if (isLocalhost) {
36 | // This is running on localhost. Lets check if a service worker still exists or not.
37 | checkValidServiceWorker(swUrl);
38 | } else {
39 | // Is not local host. Just register service worker
40 | registerValidSW(swUrl);
41 | }
42 | });
43 | }
44 | }
45 |
46 | function registerValidSW(swUrl) {
47 | navigator.serviceWorker
48 | .register(swUrl)
49 | .then(registration => {
50 | registration.onupdatefound = () => {
51 | const installingWorker = registration.installing;
52 | installingWorker.onstatechange = () => {
53 | if (installingWorker.state === 'installed') {
54 | if (navigator.serviceWorker.controller) {
55 | // At this point, the old content will have been purged and
56 | // the fresh content will have been added to the cache.
57 | // It's the perfect time to display a "New content is
58 | // available; please refresh." message in your web app.
59 | console.log('New content is available; please refresh.');
60 | } else {
61 | // At this point, everything has been precached.
62 | // It's the perfect time to display a
63 | // "Content is cached for offline use." message.
64 | console.log('Content is cached for offline use.');
65 | }
66 | }
67 | };
68 | };
69 | })
70 | .catch(error => {
71 | console.error('Error during service worker registration:', error);
72 | });
73 | }
74 |
75 | function checkValidServiceWorker(swUrl) {
76 | // Check if the service worker can be found. If it can't reload the page.
77 | fetch(swUrl)
78 | .then(response => {
79 | // Ensure service worker exists, and that we really are getting a JS file.
80 | if (
81 | response.status === 404 ||
82 | response.headers.get('content-type').indexOf('javascript') === -1
83 | ) {
84 | // No service worker found. Probably a different app. Reload the page.
85 | navigator.serviceWorker.ready.then(registration => {
86 | registration.unregister().then(() => {
87 | window.location.reload();
88 | });
89 | });
90 | } else {
91 | // Service worker found. Proceed as normal.
92 | registerValidSW(swUrl);
93 | }
94 | })
95 | .catch(() => {
96 | console.log(
97 | 'No internet connection found. App is running in offline mode.'
98 | );
99 | });
100 | }
101 |
102 | export function unregister() {
103 | if ('serviceWorker' in navigator) {
104 | navigator.serviceWorker.ready.then(registration => {
105 | registration.unregister();
106 | });
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/src/components/Maximized.js:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import {
3 | Avatar,
4 | TitleBar,
5 | TextInput,
6 | MessageList,
7 | Message,
8 | MessageText,
9 | AgentBar,
10 | Title,
11 | Subtitle,
12 | MessageGroup,
13 | MessageButtons,
14 | MessageButton,
15 | MessageTitle,
16 | MessageMedia,
17 | TextComposer,
18 | Row,
19 | Fill,
20 | Fit,
21 | IconButton,
22 | SendButton,
23 | EmojiIcon,
24 | CloseIcon,
25 | Column,
26 | RateGoodIcon,
27 | RateBadIcon,
28 | Bubble,
29 | } from '@livechat/ui-kit'
30 |
31 | const getAvatarForUser = (userId, users) => {
32 | const foundUser = users[userId]
33 | if (foundUser && foundUser.avatarUrl) {
34 | return foundUser.avatarUrl
35 | }
36 | return null
37 | }
38 |
39 | const parseUrl = (url) => url && 'https://' + url.replace(/^(http(s)?\:\/\/)/, '').replace(/^\/\//, '')
40 |
41 | const Maximized = ({
42 | chatState,
43 | events,
44 | onMessageSend,
45 | users,
46 | ownId,
47 | currentAgent,
48 | minimize,
49 | maximizeChatWidget,
50 | sendMessage,
51 | rateGood,
52 | rateBad,
53 | rate,
54 | }) => {
55 | return (
56 |
63 |
66 |
67 | ,
68 | ]}
69 | title="Welcome to LiveChat"
70 | />
71 | {currentAgent && (
72 |
73 |
74 |
75 |
76 |
77 |
78 | {currentAgent.name}
79 | Support hero
80 |
81 |
82 | {chatState === 'CHATTING' &&
83 |
84 |
85 |
88 |
89 |
90 |
93 |
94 |
95 | }
96 |
97 |
98 |
99 | )}
100 |
107 |
108 | {events.map((messageGroup, index) => (
109 |
110 | {messageGroup.map(message => (
111 |
117 |
118 | {message.title && }
119 | {message.text && {message.text} }
120 | {message.imageUrl && (
121 |
122 |
123 |
124 | )}
125 | {message.buttons &&
126 | message.buttons.length !== 0 && (
127 |
128 | {message.buttons.map((button, buttonIndex) => (
129 | {
133 | sendMessage(button.postback)
134 | }}
135 | />
136 | ))}
137 |
138 | )}
139 |
140 |
141 | ))}
142 |
143 | ))}
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
165 | {'Powered by LiveChat'}
166 |
167 |
168 | )
169 | }
170 |
171 | export default Maximized
172 |
--------------------------------------------------------------------------------
/src/sagas/index.js:
--------------------------------------------------------------------------------
1 | import fetch from 'unfetch'
2 | import { init } from '@livechat/livechat-visitor-sdk'
3 | import { call, takeEvery, fork, select, put, take, all } from 'redux-saga/effects'
4 | import { REHYDRATE, PURGE } from 'redux-persist'
5 | import { getChatService } from '../reducers/app'
6 | import {
7 | newMessage,
8 | newUser,
9 | ownDataReceived,
10 | chatEnded,
11 | chatStarted,
12 | changeChatService,
13 | sendMessage,
14 | chatRated,
15 | } from '../actions/chatActions'
16 | import * as actionTypes from '../constants/chatActionTypes'
17 | import { getEvents } from '../reducers/events'
18 | import { getUsers, getOwnId } from '../reducers/users'
19 |
20 | const botEngineClientToken = process.env.REACT_APP_BOTENGINE_CLIENT_TOKEN
21 | const sessionId = String(Math.random())
22 |
23 | const sendQueryToBotEngine = query =>
24 | fetch('https://api.botengine.ai/query', {
25 | headers: {
26 | authorization: `Bearer ${ botEngineClientToken }`,
27 | 'Content-Type': 'application/json',
28 | },
29 | method: 'POST',
30 | body: JSON.stringify({
31 | sessionId: sessionId,
32 | query: query,
33 | storyId: process.env.REACT_APP_BOTENGINE_STORY_ID,
34 | }),
35 | }).then(response => response.json())
36 |
37 | function* transferToLiveChat() {
38 | const events = yield select(getEvents)
39 | const users = yield select(getUsers)
40 | const ownId = yield select(getOwnId)
41 | const parsedEvents = events
42 | .map(event => {
43 | const userName = (users[event.authorId] && users[event.authorId].name) || users[ownId].name
44 | const text = event.text || event.title
45 | return `${ userName }: ${ text }`
46 | })
47 | .join(' \n')
48 | yield put(
49 | changeChatService({
50 | chatService: 'LiveChat',
51 | }),
52 | )
53 | yield put(
54 | sendMessage({
55 | customId: 'VISITOR_CHAT_HISTORY',
56 | text: parsedEvents,
57 | }),
58 | )
59 | }
60 |
61 | function* handleSendMessage(sdk, { payload }) {
62 | const chatService = yield select(getChatService)
63 | if (chatService === 'LiveChat') {
64 | try {
65 | yield call(sdk.sendMessage, {
66 | customId: payload.customId,
67 | text: payload.text,
68 | })
69 | } catch (error) {
70 | console.log('> ERROR', error)
71 | }
72 | return
73 | }
74 | if (chatService === 'botEngine') {
75 | try {
76 | const botEngineResponse = yield call(sendQueryToBotEngine, payload.text)
77 | if (
78 | !botEngineResponse.result ||
79 | !botEngineResponse.result.fulfillment ||
80 | !botEngineResponse.result.fulfillment.length
81 | ) {
82 | yield call(transferToLiveChat)
83 | return
84 | }
85 | const messagesToAdd = botEngineResponse.result.fulfillment.map(fulfillmentItem => {
86 | const message = {
87 | id: Math.random(),
88 | authorId: 'bot',
89 | }
90 | if (fulfillmentItem.message) {
91 | message.text = fulfillmentItem.message
92 | }
93 | if (fulfillmentItem.buttons) {
94 | message.buttons = fulfillmentItem.buttons
95 | }
96 | if (fulfillmentItem.title) {
97 | message.title = fulfillmentItem.title
98 | }
99 | if (fulfillmentItem.imageUrl) {
100 | message.imageUrl = fulfillmentItem.imageUrl
101 | }
102 | if (fulfillmentItem.replies) {
103 | message.buttons = fulfillmentItem.replies.map(reply => ({
104 | title: reply,
105 | }))
106 | }
107 | if (botEngineResponse.timestamp) {
108 | message.timestamp = botEngineResponse.timestamp
109 | }
110 | return newMessage(message)
111 | })
112 | yield all(messagesToAdd.map(action => put(action)))
113 | if (botEngineResponse.result.interaction.action === 'livechat.transfer') {
114 | yield call(transferToLiveChat)
115 | }
116 | } catch (error) {
117 | console.log('>> BOTENGINEERROR', error)
118 | yield call(transferToLiveChat)
119 | }
120 | }
121 | }
122 |
123 | function* handleRateGood(sdk) {
124 | yield call(sdk.rateChat, {
125 | rate: 'good',
126 | })
127 | }
128 |
129 | function* handleRateBad(sdk) {
130 | yield call(sdk.rateChat, {
131 | rate: 'bad',
132 | })
133 | }
134 |
135 | function* handleCallbacks(store) {
136 | const sdk = init({
137 | license: process.env.REACT_APP_LIVECHAT_LICENSE,
138 | })
139 | sdk.on('new_message', data => {
140 | store.dispatch(newMessage(data))
141 | })
142 | sdk.on('agent_changed', data => {
143 | store.dispatch(newUser(data))
144 | })
145 | sdk.on('visitor_data', data => {
146 | store.dispatch(ownDataReceived(data))
147 | })
148 | sdk.on('chat_ended', () => {
149 | console.log('>chat_ended')
150 | store.dispatch(chatEnded())
151 | })
152 | sdk.on('chat_started', data => {
153 | store.dispatch(chatStarted(data))
154 | })
155 | sdk.on('status_changed', data => {
156 | console.log('> status_changed', data)
157 | })
158 | sdk.on('visitor_queued', data => {
159 | console.log('> visitor_queued', data)
160 | })
161 | sdk.on('typing_indicator', data => {
162 | console.log('> typing_indicator', data)
163 | })
164 | sdk.on('connection_status_changed', data => {
165 | console.log('> connection_status_changed', data)
166 | })
167 | sdk.on('chat_rated', data => {
168 | store.dispatch(chatRated({
169 | rate: data.rate,
170 | }))
171 | })
172 |
173 | yield takeEvery(actionTypes.SEND_MESSAGE, handleSendMessage, sdk)
174 | yield takeEvery(actionTypes.RATE_GOOD, handleRateGood, sdk)
175 | yield takeEvery(actionTypes.RATE_BAD, handleRateBad, sdk)
176 | }
177 |
178 | const getPersistSelector = state => state._persist && state._persist.rehydrated
179 |
180 | export default function*(store) {
181 | if (!getPersistSelector) {
182 | yield take(REHYDRATE)
183 | }
184 | yield fork(handleCallbacks, store)
185 | }
186 |
--------------------------------------------------------------------------------
/src/components/App.js:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import Maximized from './Maximized'
3 | import Minimized from './Minimized'
4 | import { ThemeProvider, FixedWrapper, darkTheme, elegantTheme, purpleTheme, defaultTheme } from '@livechat/ui-kit'
5 |
6 | const themes = {
7 | defaultTheme: {
8 | FixedWrapperMaximized: {
9 | css: {
10 | boxShadow: '0 0 1em rgba(0, 0, 0, 0.1)',
11 | },
12 | },
13 | },
14 | purpleTheme: {
15 | ...purpleTheme,
16 | TextComposer: {
17 | ...purpleTheme.TextComposer,
18 | css: {
19 | ...purpleTheme.TextComposer.css,
20 | marginTop: '1em',
21 | },
22 | },
23 | OwnMessage: {
24 | ...purpleTheme.OwnMessage,
25 | secondaryTextColor: '#fff',
26 | },
27 | },
28 | elegantTheme: {
29 | ...elegantTheme,
30 | Message: {
31 | ...darkTheme.Message,
32 | secondaryTextColor: '#fff',
33 | },
34 | OwnMessage: {
35 | ...darkTheme.OwnMessage,
36 | secondaryTextColor: '#fff',
37 | },
38 | },
39 | darkTheme: {
40 | ...darkTheme,
41 | Message: {
42 | ...darkTheme.Message,
43 | css: {
44 | ...darkTheme.Message.css,
45 | color: '#fff',
46 | },
47 | },
48 | OwnMessage: {
49 | ...darkTheme.OwnMessage,
50 | secondaryTextColor: '#fff',
51 | },
52 | TitleBar: {
53 | ...darkTheme.TitleBar,
54 | css: {
55 | ...darkTheme.TitleBar.css,
56 | padding: '1em',
57 | },
58 | },
59 | },
60 | }
61 |
62 | const commonThemeButton = {
63 | fontSize: '16px',
64 | padding: '1em',
65 | borderRadius: '.6em',
66 | margin: '1em',
67 | cursor: 'pointer',
68 | outline: 'none',
69 | border: 0,
70 | }
71 |
72 | const themePurpleButton = {
73 | ...commonThemeButton,
74 | background: 'linear-gradient(to right, #6D5BBA, #8D58BF)',
75 | color: '#fff',
76 | }
77 |
78 | const themeDarkButton = {
79 | ...commonThemeButton,
80 | background: 'rgba(0, 0, 0, 0.8)',
81 | color: '#fff',
82 | }
83 |
84 | const themeDefaultButton = {
85 | ...commonThemeButton,
86 | background: '#427fe1',
87 | color: '#fff',
88 | }
89 |
90 | const themeElegantButton = {
91 | ...commonThemeButton,
92 | background: '#000',
93 | color: '#D9A646',
94 | }
95 |
96 |
97 | class App extends React.Component {
98 | state = {
99 | theme: 'defaultTheme'
100 | }
101 |
102 | handleThemeChange = ({ target }) => {
103 | console.log('target.name', target.name)
104 | this.setState({
105 | theme: target.name + 'Theme' ,
106 | })
107 | }
108 |
109 | render() {
110 | return (
111 |
112 |
114 |
136 |
140 |
Sample chat widget built with LiveChat React chat UI kit . In this widget, BotEngine handles the incoming chats. When the bot returns `LiveChat.transfer` action, the chat is transferred to a human agent together with the transcript of the initial conversation with the bot.
141 |
The sample app uses Visitor SDK to communicate with LiveChat and the API to connect with BotEngine.
142 |
Source code is avaible at Github .
143 |
Change components theme:
144 |
145 | default
146 |
147 |
148 | purple
149 |
150 |
151 | dark
152 |
153 |
154 | elegant
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 | )
168 | }
169 | }
170 |
171 | export default App
172 |
--------------------------------------------------------------------------------