@carbon/ai-chat 0.3.2-2 → 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/docs/carbon-chat-docs.js +1 -1
- package/dist/docs/carbon-chat.html +1 -1
- package/dist/es/AppContainer.js +7 -7
- package/dist/es/Avatar.js +90 -0
- package/dist/es/BodyAndFooterPanelComponent.js +118 -0
- package/dist/es/BrandingOverlay.js +108 -0
- package/dist/es/Carousel.js +2 -2
- package/dist/es/CatastrophicError.js +153 -0
- package/dist/es/Chat.js +2288 -0
- package/dist/es/Disclaimer.js +213 -0
- package/dist/es/GenesysMessengerServiceDesk.js +2 -2
- package/dist/es/HomeScreenContainer.js +180 -0
- package/dist/es/HumanAgentServiceImpl.js +2 -2
- package/dist/es/IFrameComponent.js +91 -0
- package/dist/es/IFramePanel.js +75 -0
- package/dist/es/InlineError.js +41 -0
- package/dist/es/Input.js +537 -0
- package/dist/es/MessageTypeComponent.js +6492 -0
- package/dist/es/NiceDFOServiceDesk.js +2 -2
- package/dist/es/PDFViewerContainer.js +2 -2
- package/dist/es/ReactPlayer.js +2 -2
- package/dist/es/RichText.js +24801 -0
- package/dist/es/SFServiceDesk.js +2 -2
- package/dist/es/SearchResultBody.js +142 -0
- package/dist/es/ServiceDeskImpl.js +2 -2
- package/dist/es/TourContainer.js +429 -0
- package/dist/es/VideoComponent.js +241 -0
- package/dist/es/ViewSourcePanel.js +85 -0
- package/dist/es/ZendeskServiceDesk.js +2 -2
- package/dist/es/_node-resolve_empty.js +2 -2
- package/dist/es/aiChatEntry.js +2 -2
- package/dist/es/ar-dz.js +2 -2
- package/dist/es/ar-kw.js +2 -2
- package/dist/es/ar-ly.js +2 -2
- package/dist/es/ar-ma.js +2 -2
- package/dist/es/ar-sa.js +2 -2
- package/dist/es/ar-tn.js +2 -2
- package/dist/es/ar.js +2 -2
- package/dist/es/ar2.js +2 -2
- package/dist/es/cds-aichat-code.js +103 -0
- package/dist/es/cds-aichat-container.js +3 -3
- package/dist/es/cds-aichat-custom-element.js +2 -2
- package/dist/es/common.js +15342 -0
- package/dist/es/cs.js +2 -2
- package/dist/es/cs2.js +2 -2
- package/dist/es/customElement.js +2 -2
- package/dist/es/de-at.js +2 -2
- package/dist/es/de-ch.js +2 -2
- package/dist/es/de.js +2 -2
- package/dist/es/de2.js +2 -2
- package/dist/es/decode.js +561 -0
- package/dist/es/en-au.js +2 -2
- package/dist/es/en-ca.js +2 -2
- package/dist/es/en-gb.js +2 -2
- package/dist/es/en-ie.js +2 -2
- package/dist/es/en-il.js +2 -2
- package/dist/es/en-nz.js +2 -2
- package/dist/es/es-do.js +2 -2
- package/dist/es/es-us.js +2 -2
- package/dist/es/es.js +2 -2
- package/dist/es/es2.js +2 -2
- package/dist/es/export.carbon.js +2 -2
- package/dist/es/export.js +3 -3
- package/dist/es/export.legacy.carbon.js +2 -2
- package/dist/es/export.legacy.js +3 -3
- package/dist/es/fr-ca.js +2 -2
- package/dist/es/fr-ch.js +2 -2
- package/dist/es/fr.js +2 -2
- package/dist/es/fr2.js +2 -2
- package/dist/es/it-ch.js +2 -2
- package/dist/es/it.js +2 -2
- package/dist/es/it2.js +2 -2
- package/dist/es/ja.js +2 -2
- package/dist/es/ja2.js +2 -2
- package/dist/es/ko.js +2 -2
- package/dist/es/ko2.js +2 -2
- package/dist/es/markdown.js +9323 -0
- package/dist/es/markdown.worker.js +66 -0
- package/dist/es/mockServiceDesk.js +2 -2
- package/dist/es/moduleFederationPluginUtils.js +159 -0
- package/dist/es/nl.js +2 -2
- package/dist/es/nl2.js +2 -2
- package/dist/es/pt-br.js +2 -2
- package/dist/es/pt-br2.js +2 -2
- package/dist/es/pt.js +2 -2
- package/dist/es/scriptRender.js +2 -2
- package/dist/es/tokenTree.js +143 -0
- package/dist/es/useCounter.js +37 -0
- package/dist/es/useWindowSize.js +30 -0
- package/dist/es/zh-cn.js +2 -2
- package/dist/es/zh-tw.js +2 -2
- package/dist/es/zh-tw2.js +2 -2
- package/dist/es/zh.js +2 -2
- package/package.json +1 -1
package/dist/es/Chat.js
ADDED
|
@@ -0,0 +1,2288 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
*
|
|
4
|
+
* (C) Copyright IBM Corp. 2017, 2025. All Rights Reserved.
|
|
5
|
+
*
|
|
6
|
+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
|
7
|
+
* in compliance with the License. You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software distributed under the License
|
|
12
|
+
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
|
|
13
|
+
* or implied. See the License for the specific language governing permissions and limitations under
|
|
14
|
+
* the License.
|
|
15
|
+
*
|
|
16
|
+
* @carbon/ai-chat 0.3.2
|
|
17
|
+
*
|
|
18
|
+
* Built: Jul 9 2025 11:10 am -04:00
|
|
19
|
+
*
|
|
20
|
+
*
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import React__default, { useContext, useState, useEffect, forwardRef, useRef, useImperativeHandle, PureComponent, Fragment, useLayoutEffect, useCallback, Suspense, Component } from 'react';
|
|
24
|
+
import { injectIntl, FormattedMessage, useIntl } from 'react-intl';
|
|
25
|
+
import { useSelector, connect } from 'react-redux';
|
|
26
|
+
import { a5 as ServiceManagerContext, a6 as isObject, a7 as WEB_COMPONENT_PREFIX, _ as useLanguagePack, a0 as useServiceManager, a8 as selectAgentDisplayState, a9 as AnnounceOnMountComponentExport, aa as doFocusRef, Z as cx, ab as selectInputState, w as actions, ac as AlternateSuggestionsPanelState, ad as WriteableElement, ae as withAriaAnnouncer, af as createDidCatchErrorData, ag as isConnectToAgent, ah as ConnectToAgentBehaviorType, ai as isRequest, aj as nodeToText, ak as VisuallyHidden, al as timestampToTimeString, am as isResponse, M as MessageErrorState, an as renderAsUserDefinedMessage, ao as isChatStatusResponseType, ap as isOptionItem, aq as InternalMessageResponseType, ar as isSingleItemCarousel, as as getScrollBottom, at as doScrollElement, au as arrayLastValue, j as consoleError, av as AriaLiveMessageExport, aw as AUTO_SCROLL_EXTRA, ax as IS_MOBILE, ay as AUTO_SCROLL_THROTTLE_TIMEOUT, az as memoizeOne, aA as lazyPDFViewer, aB as AriaAnnouncerContext, aC as isEmptyString, aD as doFocus, aE as ButtonKindEnum, aF as isSuggestionListItem, aG as AlternateSuggestionsSection, aH as ButtonSizeEnum, aI as THREAD_ID_MAIN, S as assertType, aJ as createTextItem, aK as HideComponentContext, aL as FocusTrap, aM as animateWithClass, aN as ConfirmModal, aO as focusOnFirstFocusableItemInArrayOfElements, aP as BotHeaderExport } from './AppContainer.js';
|
|
27
|
+
import { d as debounce, A as AvailabilityMessage, M as MessageTypeComponent, L as LoadingPDFViewer, g as getSearchLinkLabel, s as shouldShowAsPDFButton, a as getSearchResultMetaData, E as EndAgentChatModal } from './MessageTypeComponent.js';
|
|
28
|
+
import { _ as __decorate, c as carbonElement, o, W as WriteableElementName, u as useOnMount, j as MessageResponseTypes, A as AgentMessageType, i as FileStatusValue, S as ScreenShareState, e as CatastrophicErrorType } from './customElement.js';
|
|
29
|
+
import { LitElement, css, unsafeCSS, html } from 'lit';
|
|
30
|
+
import { property } from 'lit/decorators.js';
|
|
31
|
+
import ScreenOff from '@carbon/icons-react/es/ScreenOff.js';
|
|
32
|
+
import { Button, ActionableNotification, Loading, Tile, unstable__ChatButton } from '@carbon/react';
|
|
33
|
+
import UserAvatar from '@carbon/icons-react/es/UserAvatar.js';
|
|
34
|
+
import ChatBot from '@carbon/icons-react/es/ChatBot.js';
|
|
35
|
+
import CheckmarkFilled from '@carbon/icons-react/es/CheckmarkFilled.js';
|
|
36
|
+
import Headset from '@carbon/icons-react/es/Headset.js';
|
|
37
|
+
import { A as AvatarExport } from './Avatar.js';
|
|
38
|
+
import { I as InlineError } from './InlineError.js';
|
|
39
|
+
import { CatastrophicError as CatastrophicErrorExport } from './CatastrophicError.js';
|
|
40
|
+
import Close from '@carbon/icons-react/es/Close.js';
|
|
41
|
+
import DownToBottom from '@carbon/icons-react/es/DownToBottom.js';
|
|
42
|
+
import HelpDesk from '@carbon/icons-react/es/HelpDesk.js';
|
|
43
|
+
import SatelliteRadar from '@carbon/icons-react/es/SatelliteRadar.js';
|
|
44
|
+
import UpToTop from '@carbon/icons-react/es/UpToTop.js';
|
|
45
|
+
import ChevronDown from '@carbon/icons-react/es/ChevronDown.js';
|
|
46
|
+
import { h as hasSearchResultBody, S as SearchResultBodyExport, s as shouldTruncateBody } from './SearchResultBody.js';
|
|
47
|
+
import Bookmark from '@carbon/icons-react/es/Bookmark.js';
|
|
48
|
+
import ChevronUp from '@carbon/icons-react/es/ChevronUp.js';
|
|
49
|
+
import { u as useWindowSize } from './useWindowSize.js';
|
|
50
|
+
import DocumentView from '@carbon/icons-react/es/DocumentView.js';
|
|
51
|
+
import { S as SkeletonText } from './VideoComponent.js';
|
|
52
|
+
import { I as InputExport } from './Input.js';
|
|
53
|
+
import '@carbon/icons-react/es/AiLaunch.js';
|
|
54
|
+
import '@carbon/icons-react/es/ArrowUpLeft.js';
|
|
55
|
+
import '@carbon/icons-react/es/ChatLaunch.js';
|
|
56
|
+
import '@carbon/icons-react/es/Information.js';
|
|
57
|
+
import '@carbon/icons-react/es/Home.js';
|
|
58
|
+
import '@carbon/web-components/es-custom/components/slug/index.js';
|
|
59
|
+
import '@carbon/icons-react/es/CloseLarge.js';
|
|
60
|
+
import '@carbon/icons-react/es/Menu.js';
|
|
61
|
+
import '@carbon/icons-react/es/Restart.js';
|
|
62
|
+
import '@carbon/icons-react/es/SidePanelClose.js';
|
|
63
|
+
import '@carbon/icons-react/es/SubtractLarge.js';
|
|
64
|
+
import '@carbon/web-components/es-custom/components/ai-label/defs.js';
|
|
65
|
+
import '@carbon/web-components/es-custom/components/popover/defs.js';
|
|
66
|
+
import '@carbon/web-components/es-custom/components/skeleton-icon/index.js';
|
|
67
|
+
import '@carbon/web-components/es-custom/components/button/index.js';
|
|
68
|
+
import '@carbon/web-components/es-custom/components/overflow-menu/index.js';
|
|
69
|
+
import 'react-dom';
|
|
70
|
+
import '@carbon/web-components/es-custom/components/ai-label/ai-label-action-button.js';
|
|
71
|
+
import '@carbon/web-components/es-custom/components/ai-label/ai-label.js';
|
|
72
|
+
import '@carbon/icons-react/es/Attachment.js';
|
|
73
|
+
import './RichText.js';
|
|
74
|
+
import 'lit/directives/unsafe-html.js';
|
|
75
|
+
import '@carbon/web-components/es-custom/components/inline-loading/index.js';
|
|
76
|
+
import '@carbon/icon-helpers';
|
|
77
|
+
import '@carbon/icons/es/checkmark--filled/16.js';
|
|
78
|
+
import '@carbon/icons/es/chevron--right/16.js';
|
|
79
|
+
import '@carbon/icons/es/error--filled/16.js';
|
|
80
|
+
import 'lit/directives/unsafe-svg.js';
|
|
81
|
+
import '@carbon/web-components/es-custom/components/textarea/index.js';
|
|
82
|
+
import '@carbon/web-components/es-custom/components/icon-button/index.js';
|
|
83
|
+
import '@carbon/icons/es/thumbs-down/16.js';
|
|
84
|
+
import '@carbon/icons/es/thumbs-down--filled/16.js';
|
|
85
|
+
import '@carbon/icons/es/thumbs-up/16.js';
|
|
86
|
+
import '@carbon/icons/es/thumbs-up--filled/16.js';
|
|
87
|
+
import '@carbon/web-components/es-custom/components/tag/index.js';
|
|
88
|
+
import '@carbon/web-components/es-custom/components/chat-button/index.js';
|
|
89
|
+
import '@carbon/web-components/es-custom/components/button/button.js';
|
|
90
|
+
import '@carbon/web-components/es-custom/components/layer/index.js';
|
|
91
|
+
import '@carbon/icons-react/es/Checkmark.js';
|
|
92
|
+
import '@carbon/icons-react/es/Logout.js';
|
|
93
|
+
import '@carbon/icons-react/es/TouchInteraction.js';
|
|
94
|
+
import '@carbon/icons-react/es/Send.js';
|
|
95
|
+
import '@carbon/icons-react/es/ArrowRight.js';
|
|
96
|
+
import '@carbon/icons-react/es/Launch.js';
|
|
97
|
+
import '@carbon/icons-react/es/DocumentPdf.js';
|
|
98
|
+
import '@carbon/icons-react/es/Link.js';
|
|
99
|
+
import '@carbon/icons-react/es/Maximize.js';
|
|
100
|
+
import './useCounter.js';
|
|
101
|
+
import './IFrameComponent.js';
|
|
102
|
+
import '@carbon/web-components/es-custom/components/data-table/index.js';
|
|
103
|
+
import '@carbon/web-components/es-custom/components/checkbox/index.js';
|
|
104
|
+
import '@carbon/icons/es/download/16.js';
|
|
105
|
+
import 'lit-html/directives/repeat.js';
|
|
106
|
+
import '@carbon/web-components/es-custom/components/pagination/index.js';
|
|
107
|
+
import '@carbon/web-components/es-custom/components/select/index.js';
|
|
108
|
+
import '@carbon/web-components/es-custom/components/data-table/table-skeleton.js';
|
|
109
|
+
import './moduleFederationPluginUtils.js';
|
|
110
|
+
import '@carbon/icons-react/es/ErrorFilled.js';
|
|
111
|
+
import '@carbon/icons-react/es/Music.js';
|
|
112
|
+
import '@carbon/icons-react/es/SendFilled.js';
|
|
113
|
+
import '@carbon/icons/es/stop--filled/16.js';
|
|
114
|
+
|
|
115
|
+
function withServiceManager(Component) {
|
|
116
|
+
return React__default.forwardRef((props, ref) => {
|
|
117
|
+
const serviceManager = useContext(ServiceManagerContext);
|
|
118
|
+
return React__default.createElement(Component, { ...props, ref: ref, serviceManager: serviceManager });
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Error message constants. */
|
|
123
|
+
var FUNC_ERROR_TEXT = 'Expected a function';
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Creates a throttled function that only invokes `func` at most once per
|
|
127
|
+
* every `wait` milliseconds. The throttled function comes with a `cancel`
|
|
128
|
+
* method to cancel delayed `func` invocations and a `flush` method to
|
|
129
|
+
* immediately invoke them. Provide `options` to indicate whether `func`
|
|
130
|
+
* should be invoked on the leading and/or trailing edge of the `wait`
|
|
131
|
+
* timeout. The `func` is invoked with the last arguments provided to the
|
|
132
|
+
* throttled function. Subsequent calls to the throttled function return the
|
|
133
|
+
* result of the last `func` invocation.
|
|
134
|
+
*
|
|
135
|
+
* **Note:** If `leading` and `trailing` options are `true`, `func` is
|
|
136
|
+
* invoked on the trailing edge of the timeout only if the throttled function
|
|
137
|
+
* is invoked more than once during the `wait` timeout.
|
|
138
|
+
*
|
|
139
|
+
* If `wait` is `0` and `leading` is `false`, `func` invocation is deferred
|
|
140
|
+
* until to the next tick, similar to `setTimeout` with a timeout of `0`.
|
|
141
|
+
*
|
|
142
|
+
* See [David Corbacho's article](https://css-tricks.com/debouncing-throttling-explained-examples/)
|
|
143
|
+
* for details over the differences between `_.throttle` and `_.debounce`.
|
|
144
|
+
*
|
|
145
|
+
* @static
|
|
146
|
+
* @memberOf _
|
|
147
|
+
* @since 0.1.0
|
|
148
|
+
* @category Function
|
|
149
|
+
* @param {Function} func The function to throttle.
|
|
150
|
+
* @param {number} [wait=0] The number of milliseconds to throttle invocations to.
|
|
151
|
+
* @param {Object} [options={}] The options object.
|
|
152
|
+
* @param {boolean} [options.leading=true]
|
|
153
|
+
* Specify invoking on the leading edge of the timeout.
|
|
154
|
+
* @param {boolean} [options.trailing=true]
|
|
155
|
+
* Specify invoking on the trailing edge of the timeout.
|
|
156
|
+
* @returns {Function} Returns the new throttled function.
|
|
157
|
+
* @example
|
|
158
|
+
*
|
|
159
|
+
* // Avoid excessively updating the position while scrolling.
|
|
160
|
+
* jQuery(window).on('scroll', _.throttle(updatePosition, 100));
|
|
161
|
+
*
|
|
162
|
+
* // Invoke `renewToken` when the click event is fired, but not more than once every 5 minutes.
|
|
163
|
+
* var throttled = _.throttle(renewToken, 300000, { 'trailing': false });
|
|
164
|
+
* jQuery(element).on('click', throttled);
|
|
165
|
+
*
|
|
166
|
+
* // Cancel the trailing throttled invocation.
|
|
167
|
+
* jQuery(window).on('popstate', throttled.cancel);
|
|
168
|
+
*/
|
|
169
|
+
function throttle(func, wait, options) {
|
|
170
|
+
var leading = true,
|
|
171
|
+
trailing = true;
|
|
172
|
+
|
|
173
|
+
if (typeof func != 'function') {
|
|
174
|
+
throw new TypeError(FUNC_ERROR_TEXT);
|
|
175
|
+
}
|
|
176
|
+
if (isObject(options)) {
|
|
177
|
+
leading = 'leading' in options ? !!options.leading : leading;
|
|
178
|
+
trailing = 'trailing' in options ? !!options.trailing : trailing;
|
|
179
|
+
}
|
|
180
|
+
return debounce(func, wait, {
|
|
181
|
+
'leading': leading,
|
|
182
|
+
'maxWait': wait,
|
|
183
|
+
'trailing': trailing
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
var css_248z = ".dots{width:32px;height:32px}.dot{fill:none;r:0;stroke-width:0;transform:translateY(0)}[data-carbon-theme=g10] .dot,[data-carbon-theme=white] .dot{stroke:#001d6c}[data-carbon-theme=g100] .dot,[data-carbon-theme=g90] .dot{stroke:#f4f4f4}.linear .dot--left{transform-origin:25% 50%;animation:linear-load-size,linear-load-stroke,linear-loop-size,linear-loop-stroke;animation-duration:1s;animation-iteration-count:1,1,infinite,infinite;animation-fill-mode:forwards;animation-delay:1s,1s,2s,2s}.linear .dot--center{transform-origin:50% 50%;animation:linear-load-size,linear-load-stroke,linear-loop-size,linear-loop-stroke;animation-duration:1s;animation-iteration-count:1,1,infinite,infinite;animation-fill-mode:forwards;animation-delay:1.167s,1.167s,2.167s,2.167s}.linear .dot--right{transform-origin:75% 50%;animation:linear-load-size,linear-load-stroke,linear-loop-size,linear-loop-stroke;animation-duration:1s;animation-iteration-count:1,1,infinite,infinite;animation-fill-mode:forwards;animation-delay:1.334s,1.334s,2.334s,2.334s}[dir=rtl] .linear .dot--left{animation-delay:1.334s,1.334s,2.334s,2.334s,7.334s,7.334s}[dir=rtl] .linear .dot--center{animation-delay:1.167s,1.167s,2.167s,2.167s,7.167s,7.167s}[dir=rtl] .linear .dot--right{animation-delay:1s,1s,2s,2s,7s,7s}.linear--no-loop .dot--left{transform-origin:25% 50%;animation:linear-load-size,linear-load-stroke,linear-unload-size,linear-unload-stroke;animation-duration:1s;animation-iteration-count:1;animation-fill-mode:forwards;animation-delay:1s,1s,2s,2s}.linear--no-loop .dot--center{transform-origin:50% 50%;animation:linear-load-size,linear-load-stroke,linear-unload-size,linear-unload-stroke;animation-duration:1s;animation-iteration-count:1;animation-fill-mode:forwards;animation-delay:1.167s,1.167s,2.167s,2.167s}.linear--no-loop .dot--right{transform-origin:75% 50%;animation:linear-load-size,linear-load-stroke,linear-unload-size,linear-unload-stroke;animation-duration:1s;animation-iteration-count:1;animation-fill-mode:forwards}.linear--no-loop .dot--right,[dir=rtl] .linear--no-loop .dot--left{animation-delay:1.334s,1.334s,2.334s,2.334s}[dir=rtl] .linear--no-loop .dot--center{animation-delay:1.167s,1.167s,2.167s,2.167s}[dir=rtl] .linear--no-loop .dot--right{animation-delay:1s,1s,2s,2s}@keyframes linear-load-size{0%{r:0;animation-timing-function:cubic-bezier(0,0,.3,1)}25%{r:2.5px;animation-timing-function:cubic-bezier(0,0,.3,1)}83.3%{r:.875px}to{r:.875px}}@keyframes linear-load-stroke{0%{stroke-width:0;animation-timing-function:cubic-bezier(0,0,.3,1)}8.33%{stroke-width:1.72}to{stroke-width:1.72}}@keyframes linear-loop-size{0%{r:.875px;animation-timing-function:cubic-bezier(0,0,.3,1)}25%{r:2.5px;animation-timing-function:cubic-bezier(0,0,.3,1)}91.66%{r:.875px}to{r:.875px}}@keyframes linear-loop-stroke{0%{stroke-width:1.72;animation-timing-function:cubic-bezier(.4,.14,1,1)}to{stroke-width:1.72}}@keyframes linear-unload-size{0%{r:.875px}8.33%{r:.875px}33.33%{r:2.5px;animation-timing-function:cubic-bezier(.4,.14,1,1)}58.33%{r:0}to{r:0}}@keyframes linear-unload-stroke{0%{stroke-width:1.72}50%{stroke-width:1.72}58.33%{stroke-width:0}to{stroke-width:0}}@media (prefers-reduced-motion:reduce){.dot--center,.dot--left,.dot--right{transition:none;animation:none}}";
|
|
188
|
+
|
|
189
|
+
const INLINE_LOADING_TAG_NAME = `${WEB_COMPONENT_PREFIX}-inline-loading`;
|
|
190
|
+
let CDSInlineLoadingElement = class CDSInlineLoadingElement extends LitElement {
|
|
191
|
+
constructor() {
|
|
192
|
+
super(...arguments);
|
|
193
|
+
this.bounce = false;
|
|
194
|
+
this.loop = false;
|
|
195
|
+
this.quickLoad = false;
|
|
196
|
+
this.carbonTheme = 'g10';
|
|
197
|
+
}
|
|
198
|
+
static { this.styles = css `
|
|
199
|
+
${unsafeCSS(css_248z)}
|
|
200
|
+
`; }
|
|
201
|
+
getAnimationEffect() {
|
|
202
|
+
const classNames = [];
|
|
203
|
+
if (this.quickLoad === true) {
|
|
204
|
+
classNames.push('quick-load');
|
|
205
|
+
}
|
|
206
|
+
if (this.bounce === true && this.loop === true) {
|
|
207
|
+
classNames.push('vertical');
|
|
208
|
+
}
|
|
209
|
+
if (this.bounce === false && this.loop === true) {
|
|
210
|
+
classNames.push('linear');
|
|
211
|
+
}
|
|
212
|
+
if (this.bounce === true && this.loop === false) {
|
|
213
|
+
classNames.push('vertical--no-loop');
|
|
214
|
+
}
|
|
215
|
+
if (classNames.length) {
|
|
216
|
+
return classNames.join(' ');
|
|
217
|
+
}
|
|
218
|
+
return 'linear--no-loop';
|
|
219
|
+
}
|
|
220
|
+
render() {
|
|
221
|
+
return html `<div data-carbon-theme=${this.carbonTheme} class=${this.getAnimationEffect()}>
|
|
222
|
+
<svg class="dots" viewBox="0 0 32 32">
|
|
223
|
+
<circle class="dot dot--left" cx="8" cy="16" />
|
|
224
|
+
<circle class="dot dot--center" cx="16" cy="16" r="2" />
|
|
225
|
+
<circle class="dot dot--right" cx="24" cy="16" r="2" />
|
|
226
|
+
</svg>
|
|
227
|
+
</div>`;
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
__decorate([
|
|
231
|
+
property({ type: Boolean })
|
|
232
|
+
], CDSInlineLoadingElement.prototype, "bounce", void 0);
|
|
233
|
+
__decorate([
|
|
234
|
+
property({ type: Boolean })
|
|
235
|
+
], CDSInlineLoadingElement.prototype, "loop", void 0);
|
|
236
|
+
__decorate([
|
|
237
|
+
property({ type: Boolean })
|
|
238
|
+
], CDSInlineLoadingElement.prototype, "quickLoad", void 0);
|
|
239
|
+
__decorate([
|
|
240
|
+
property({ type: String })
|
|
241
|
+
], CDSInlineLoadingElement.prototype, "carbonTheme", void 0);
|
|
242
|
+
CDSInlineLoadingElement = __decorate([
|
|
243
|
+
carbonElement(INLINE_LOADING_TAG_NAME)
|
|
244
|
+
], CDSInlineLoadingElement);
|
|
245
|
+
var CDSInlineLoadingElement$1 = CDSInlineLoadingElement;
|
|
246
|
+
|
|
247
|
+
const InlineLoadingComponent = o({
|
|
248
|
+
tagName: INLINE_LOADING_TAG_NAME,
|
|
249
|
+
elementClass: CDSInlineLoadingElement$1,
|
|
250
|
+
react: React__default,
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
function AgentAvatar(props) {
|
|
254
|
+
const { agentProfile, languagePack, width, height } = props;
|
|
255
|
+
const agentName = agentProfile?.nickname;
|
|
256
|
+
const avatarStyle = { width, height };
|
|
257
|
+
const avatarUrl = agentProfile?.profile_picture_url;
|
|
258
|
+
// Indicates if the avatar for a human agent failed to load.
|
|
259
|
+
const [hasError, setHasError] = useState(false);
|
|
260
|
+
let component;
|
|
261
|
+
// If the avatar Url changes, then hasError should reset to allow an attempt at loading the new avatar url.
|
|
262
|
+
useEffect(() => {
|
|
263
|
+
setHasError(false);
|
|
264
|
+
}, [avatarUrl]);
|
|
265
|
+
if (!hasError && avatarUrl) {
|
|
266
|
+
component = React__default.createElement("img", { src: avatarUrl, alt: languagePack.agent_ariaAgentAvatar, onError: () => setHasError(true) });
|
|
267
|
+
}
|
|
268
|
+
else if (agentName?.match(/^[\x20-\xFE]+$/)) {
|
|
269
|
+
// If the agentName only contains ASCII characters (and at least one), then show the first letter of the agentName
|
|
270
|
+
// as the agentAvatar. For most Latin languages, we can infer that the first letter of the name is an appropriate
|
|
271
|
+
// representation for that person. For other languages such as Chinese, it's not clear what the correct letter
|
|
272
|
+
// would be so if we see any such characters at all, we'll just fall back to showing a picture instead of a letter.
|
|
273
|
+
// We're only accepting ASCII (and extended ASCII) because proper browser detection for Latin characters is lacking.
|
|
274
|
+
component = (React__default.createElement("div", { "aria-label": languagePack.agent_ariaAgentAvatar, className: "WACAgentAvatar__Circle", style: avatarStyle },
|
|
275
|
+
React__default.createElement("div", { className: "WACAgentAvatar__Letter" }, agentName.charAt(0))));
|
|
276
|
+
}
|
|
277
|
+
else {
|
|
278
|
+
// If the agentName contains any non-ASCII characters, then show the default agent avatar.
|
|
279
|
+
component = (React__default.createElement(UserAvatar, { size: 32, width: width ? Number(width.replace('px', '')) : undefined, height: height ? Number(height.replace('px', '')) : undefined, "aria-label": languagePack.agent_ariaAgentAvatar }));
|
|
280
|
+
}
|
|
281
|
+
return React__default.createElement("div", { className: "WACAgentAvatar" }, component);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* This component is a banner that appears at the top of the screen when the user is connecting or connected to a
|
|
286
|
+
* live agent. It will display a cancel button in the case where the user is waiting for an agent or an "end chat"
|
|
287
|
+
* button for when the user is connected to an agent.
|
|
288
|
+
*/
|
|
289
|
+
function AgentBanner(props, ref) {
|
|
290
|
+
const { onButtonClick } = props;
|
|
291
|
+
const languagePack = useLanguagePack();
|
|
292
|
+
const serviceManager = useServiceManager();
|
|
293
|
+
const persistedAgentState = useSelector((state) => state.persistedToBrowserStorage.chatState.agentState);
|
|
294
|
+
const agentState = useSelector((state) => state.agentState);
|
|
295
|
+
const { isConnecting, availability, isScreenSharing } = agentState;
|
|
296
|
+
const displayState = useSelector(selectAgentDisplayState);
|
|
297
|
+
const { agentProfile } = persistedAgentState;
|
|
298
|
+
const buttonRef = useRef();
|
|
299
|
+
let line1;
|
|
300
|
+
let line2;
|
|
301
|
+
let avatar;
|
|
302
|
+
let buttonLabel;
|
|
303
|
+
let animation;
|
|
304
|
+
if (isConnecting) {
|
|
305
|
+
animation = React__default.createElement("div", { className: "WACLoadingBar__ConnectingAnimation" });
|
|
306
|
+
line1 = languagePack.agent_connecting;
|
|
307
|
+
line2 = (React__default.createElement(AnnounceOnMountComponentExport, { announceOnce: languagePack.agent_connecting },
|
|
308
|
+
React__default.createElement(AvailabilityMessage, { availability: availability, fallbackText: languagePack.agent_connectWaiting })));
|
|
309
|
+
buttonLabel = languagePack.agent_connectButtonCancel;
|
|
310
|
+
}
|
|
311
|
+
else {
|
|
312
|
+
line1 = agentProfile?.nickname || languagePack.agent_noName;
|
|
313
|
+
buttonLabel = languagePack.agent_connectedButtonEndChat;
|
|
314
|
+
avatar = React__default.createElement(AgentAvatar, { agentProfile: agentProfile, languagePack: languagePack, width: "32px", height: "32px" });
|
|
315
|
+
}
|
|
316
|
+
const onStopSharing = () => {
|
|
317
|
+
serviceManager.humanAgentService.screenShareStop();
|
|
318
|
+
};
|
|
319
|
+
// Add a "requestFocus" imperative function to the ref so other components can trigger focus here.
|
|
320
|
+
useImperativeHandle(ref, () => ({
|
|
321
|
+
requestFocus: () => {
|
|
322
|
+
if (buttonRef.current) {
|
|
323
|
+
doFocusRef(buttonRef);
|
|
324
|
+
return true;
|
|
325
|
+
}
|
|
326
|
+
return false;
|
|
327
|
+
},
|
|
328
|
+
}));
|
|
329
|
+
return (React__default.createElement("div", { className: cx('WACAgentBanner', { 'WACAgentBanner--connected': !isConnecting }) },
|
|
330
|
+
displayState.isConnectingOrConnected && (React__default.createElement("div", { className: "WACAgentBanner__Body" },
|
|
331
|
+
avatar,
|
|
332
|
+
React__default.createElement("div", { className: "WACAgentBanner__AgentInfo" },
|
|
333
|
+
React__default.createElement("div", { className: "WACAgentBanner__AgentLine1" }, line1),
|
|
334
|
+
line2 && React__default.createElement("div", { className: "WACAgentBanner__AgentLine2" }, line2)),
|
|
335
|
+
React__default.createElement(Button, { ref: buttonRef, className: "WACAgentBanner__Button WACAgentBanner__CancelButton", onClick: onButtonClick, size: "sm" }, buttonLabel))),
|
|
336
|
+
isScreenSharing && (React__default.createElement(Button, { className: "WACAgentBanner__Button WACAgentBanner__StopSharingButton", kind: "danger", size: "sm", renderIcon: ScreenOff, onClick: onStopSharing }, languagePack.agent_sharingStopSharingButton)),
|
|
337
|
+
animation));
|
|
338
|
+
}
|
|
339
|
+
const AgentBannerExport = React__default.memo(forwardRef(AgentBanner));
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* A simple container for the agent banner that avoids rendering it if it is hidden.
|
|
343
|
+
*/
|
|
344
|
+
function AgentBannerContainer({ onButtonClick, bannerRef }) {
|
|
345
|
+
const agentState = useSelector((state) => state.agentState);
|
|
346
|
+
const displayState = useSelector(selectAgentDisplayState);
|
|
347
|
+
if (displayState.isConnectingOrConnected || agentState.isScreenSharing) {
|
|
348
|
+
return React__default.createElement(AgentBannerExport, { ref: bannerRef, onButtonClick: onButtonClick });
|
|
349
|
+
}
|
|
350
|
+
return null;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function QuestionMarkIcon(props) {
|
|
354
|
+
return (React__default.createElement("svg", { className: props.className, xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 12.75 20.64" },
|
|
355
|
+
React__default.createElement("circle", { cx: "6.07", cy: "18.82", r: "1.82" }),
|
|
356
|
+
React__default.createElement("path", { d: "M7.28,0H5.46A5.45,5.45,0,0,0,0,5.44v.63H2.43V5.46a3,3,0,0,1,3-3H7.28a3,3,0,1,1,0,6.07H4.86V14H7.28v-3A5.47,5.47,0,1,0,7.28,0Z" })));
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function AlternateSuggestionsButton() {
|
|
360
|
+
const serviceManager = useServiceManager();
|
|
361
|
+
const alternateSuggestionsState = useSelector((state) => state.alternateSuggestionsState);
|
|
362
|
+
const { isReadonly } = useSelector(selectInputState);
|
|
363
|
+
const languagePack = useSelector((state) => state.languagePack);
|
|
364
|
+
const { showButton } = alternateSuggestionsState;
|
|
365
|
+
const buttonRef = useRef();
|
|
366
|
+
const buttonLabel = languagePack.suggestions_ariaButtonToOpen;
|
|
367
|
+
if (!showButton) {
|
|
368
|
+
return null;
|
|
369
|
+
}
|
|
370
|
+
function onMenuClick() {
|
|
371
|
+
serviceManager.store.dispatch(actions.setAlternateSuggestionsPanelState(AlternateSuggestionsPanelState.OPEN_FULL, 'User'));
|
|
372
|
+
}
|
|
373
|
+
return (React__default.createElement("div", { className: "WACAlternateSuggestionsButton" },
|
|
374
|
+
React__default.createElement("div", { className: "WACAlternateSuggestionsButton__ButtonContainer" },
|
|
375
|
+
React__default.createElement(Button, { type: "button", className: "WACAlternateSuggestionsButton__button", ref: buttonRef, onClick: onMenuClick, "aria-label": buttonLabel, disabled: isReadonly },
|
|
376
|
+
React__default.createElement(QuestionMarkIcon, null)))));
|
|
377
|
+
}
|
|
378
|
+
const AlternateSuggestionsButtonExport = React__default.memo(AlternateSuggestionsButton);
|
|
379
|
+
|
|
380
|
+
function LatestWelcomeNodes({ welcomeNodeBeforeElement, children }) {
|
|
381
|
+
const { namespace } = useServiceManager();
|
|
382
|
+
return (React__default.createElement(React__default.Fragment, null,
|
|
383
|
+
React__default.createElement(WriteableElement, { slotName: WriteableElementName.WELCOME_NODE_BEFORE_ELEMENT, id: `welcomeNodeBeforeElement${namespace.suffix}`, element: welcomeNodeBeforeElement }),
|
|
384
|
+
children));
|
|
385
|
+
}
|
|
386
|
+
var LatestWelcomeNodes$1 = React__default.memo(LatestWelcomeNodes);
|
|
387
|
+
|
|
388
|
+
function Notifications({ notifications, serviceManager }) {
|
|
389
|
+
const languagePack = useLanguagePack();
|
|
390
|
+
if (notifications.length) {
|
|
391
|
+
return (React__default.createElement("div", { className: "WACNotifications" }, notifications.map((notification) => {
|
|
392
|
+
const item = notification.notification;
|
|
393
|
+
const onClose = () => {
|
|
394
|
+
serviceManager.store.dispatch(actions.removeNotifications({ notificationID: notification.id }));
|
|
395
|
+
};
|
|
396
|
+
let onActionButtonClick;
|
|
397
|
+
let actionButtonLabel;
|
|
398
|
+
if (item.actionButtonLabel && item.onActionButtonClick) {
|
|
399
|
+
onActionButtonClick = () => {
|
|
400
|
+
item.onActionButtonClick();
|
|
401
|
+
onClose();
|
|
402
|
+
};
|
|
403
|
+
actionButtonLabel = item.actionButtonLabel;
|
|
404
|
+
}
|
|
405
|
+
return (React__default.createElement("div", { className: "WACNotifications__Notification", key: notification.id },
|
|
406
|
+
React__default.createElement(ActionableNotification, { "aria-label": languagePack.notifications_toastClose, actionButtonLabel: actionButtonLabel, onActionButtonClick: onActionButtonClick, kind: item.kind, onClose: () => {
|
|
407
|
+
onClose();
|
|
408
|
+
item.onCloseButtonClick?.();
|
|
409
|
+
}, subtitle: item.message, title: item.title,
|
|
410
|
+
// Instead of this property, we are supposed to use the StaticNotification component which does not
|
|
411
|
+
// steal focus but it's still experimental and does not provide the functionality we need (it does not
|
|
412
|
+
// have a close button).
|
|
413
|
+
hasFocus: false })));
|
|
414
|
+
})));
|
|
415
|
+
}
|
|
416
|
+
return null;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function IconHolder(props) {
|
|
420
|
+
return React__default.createElement("div", { className: "WACIconHolder" }, props.icon);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function ImageWithFallback(props) {
|
|
424
|
+
const { url, alt, fallback } = props;
|
|
425
|
+
// Indicates if the image failed to load.
|
|
426
|
+
const [hasError, setHasError] = useState(false);
|
|
427
|
+
// If the url changes, then hasError should reset to allow an attempt at loading the new image.
|
|
428
|
+
useEffect(() => {
|
|
429
|
+
setHasError(false);
|
|
430
|
+
}, [url]);
|
|
431
|
+
let component;
|
|
432
|
+
if (!hasError && url) {
|
|
433
|
+
component = React__default.createElement("img", { src: url, alt: alt, onError: () => setHasError(true) });
|
|
434
|
+
}
|
|
435
|
+
else {
|
|
436
|
+
component = fallback;
|
|
437
|
+
}
|
|
438
|
+
return React__default.createElement("div", { className: "WACImageWithFallback" }, component);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* This component with render an element in the DOM and on mount, will fire a bus event with the given element.
|
|
443
|
+
*/
|
|
444
|
+
function UICustomizationElement(props) {
|
|
445
|
+
const { event, zIndex = 1 } = props;
|
|
446
|
+
const ref = useRef();
|
|
447
|
+
const serviceManager = useServiceManager();
|
|
448
|
+
useOnMount(() => {
|
|
449
|
+
const busEvent = {
|
|
450
|
+
...event,
|
|
451
|
+
element: ref.current,
|
|
452
|
+
};
|
|
453
|
+
serviceManager.eventBus.fireSync(busEvent, serviceManager.instance);
|
|
454
|
+
});
|
|
455
|
+
return (React__default.createElement("div", { className: cx('WACUICustomizationElement', {
|
|
456
|
+
'WACUICustomizationElement--request': event.type === "customization:message:request:item:left" /* BusEventType.CUSTOMIZATION_REQUEST_LEFT */,
|
|
457
|
+
'WACUICustomizationElement--response': event.type === "customization:message:response:item:left" /* BusEventType.CUSTOMIZATION_RESPONSE_LEFT */,
|
|
458
|
+
}), style: { zIndex }, ref: ref }));
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
class MessageComponent extends PureComponent {
|
|
462
|
+
constructor() {
|
|
463
|
+
super(...arguments);
|
|
464
|
+
/**
|
|
465
|
+
* Default state.
|
|
466
|
+
*/
|
|
467
|
+
this.state = { didRenderErrorOccur: false, focusHandleHasFocus: false };
|
|
468
|
+
/**
|
|
469
|
+
* A reference to the root element in this component.
|
|
470
|
+
*/
|
|
471
|
+
this.ref = React__default.createRef();
|
|
472
|
+
/**
|
|
473
|
+
* A reference to the pure message element in this component.
|
|
474
|
+
*/
|
|
475
|
+
this.messageRef = React__default.createRef();
|
|
476
|
+
/**
|
|
477
|
+
* A reference to the focus handle element in this component.
|
|
478
|
+
*/
|
|
479
|
+
this.focusHandleRef = React__default.createRef();
|
|
480
|
+
/**
|
|
481
|
+
* Returns the value of the local message for the component.
|
|
482
|
+
*/
|
|
483
|
+
this.getLocalMessage = () => {
|
|
484
|
+
return this.props.localMessageItem;
|
|
485
|
+
};
|
|
486
|
+
/**
|
|
487
|
+
* Called when the focus handle gets focus.
|
|
488
|
+
*/
|
|
489
|
+
this.onHandleFocus = () => {
|
|
490
|
+
this.setState({ focusHandleHasFocus: true });
|
|
491
|
+
};
|
|
492
|
+
/**
|
|
493
|
+
* Called when the focus handle loses focus.
|
|
494
|
+
*/
|
|
495
|
+
this.onHandleBlur = () => {
|
|
496
|
+
this.setState({ focusHandleHasFocus: false });
|
|
497
|
+
};
|
|
498
|
+
/**
|
|
499
|
+
* Sets the z-index value used for the message.
|
|
500
|
+
*/
|
|
501
|
+
this.setZIndex = (zIndex) => {
|
|
502
|
+
this.setState({ zIndex });
|
|
503
|
+
};
|
|
504
|
+
/**
|
|
505
|
+
* Called when a key down event occurs while the focus handle has focus.
|
|
506
|
+
*/
|
|
507
|
+
this.onHandleKeyDown = (event) => {
|
|
508
|
+
if (event.altKey || event.metaKey || event.ctrlKey || event.shiftKey) {
|
|
509
|
+
// Don't do anything if any modifiers are present.
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
let moveFocus;
|
|
513
|
+
if (event.key === 'ArrowUp') {
|
|
514
|
+
moveFocus = MoveFocusType.PREVIOUS;
|
|
515
|
+
}
|
|
516
|
+
else if (event.key === 'ArrowDown') {
|
|
517
|
+
moveFocus = MoveFocusType.NEXT;
|
|
518
|
+
}
|
|
519
|
+
else if (event.key === 'Home') {
|
|
520
|
+
moveFocus = MoveFocusType.FIRST;
|
|
521
|
+
}
|
|
522
|
+
else if (event.key === 'End') {
|
|
523
|
+
moveFocus = MoveFocusType.LAST;
|
|
524
|
+
}
|
|
525
|
+
else if (event.key === 'Escape') {
|
|
526
|
+
moveFocus = MoveFocusType.INPUT;
|
|
527
|
+
}
|
|
528
|
+
else if (event.key === 'Enter' || event.key === ' ') {
|
|
529
|
+
// Prevent native scrolling on Space
|
|
530
|
+
event.preventDefault();
|
|
531
|
+
this.reAnnounceFocusHandle(); // Re-announce message content
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
if (moveFocus) {
|
|
535
|
+
// This will stop the scroll panel from moving as a result of the keypress. We only want it to move as a
|
|
536
|
+
// result of the focus change.
|
|
537
|
+
event.preventDefault();
|
|
538
|
+
this.props.requestMoveFocus(moveFocus, this.props.messagesIndex);
|
|
539
|
+
}
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
/**
|
|
543
|
+
* Returns an ARIA message that can be used to indicate that the widget (either bot or agent) was responsible for
|
|
544
|
+
* saying a specific message.
|
|
545
|
+
*/
|
|
546
|
+
getWidgetSaidMessage() {
|
|
547
|
+
const { intl, botName, localMessageItem } = this.props;
|
|
548
|
+
let messageId;
|
|
549
|
+
if (localMessageItem.item.agent_message_type) {
|
|
550
|
+
// For the human agent view, we only want to say "agent said" for messages that are text. The status messages
|
|
551
|
+
// do not need this announcement.
|
|
552
|
+
if (localMessageItem.item.response_type === MessageResponseTypes.TEXT) {
|
|
553
|
+
messageId = 'messages_agentSaid';
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
else {
|
|
557
|
+
messageId = 'messages_botSaid';
|
|
558
|
+
}
|
|
559
|
+
return messageId ? intl.formatMessage({ id: messageId }, { botName }) : null;
|
|
560
|
+
}
|
|
561
|
+
componentDidCatch(error, errorInfo) {
|
|
562
|
+
this.props.serviceManager.actions.errorOccurred(createDidCatchErrorData('Message', error, errorInfo));
|
|
563
|
+
this.setState({ didRenderErrorOccur: true });
|
|
564
|
+
}
|
|
565
|
+
componentDidMount() {
|
|
566
|
+
const uiState = this.props.localMessageItem.ui_state;
|
|
567
|
+
if (uiState.needsAnnouncement) {
|
|
568
|
+
this.props.ariaAnnouncer(this.ref.current);
|
|
569
|
+
this.props.serviceManager.store.dispatch(actions.setMessageWasAnnounced(uiState.id));
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
componentDidUpdate() {
|
|
573
|
+
const uiState = this.props.localMessageItem.ui_state;
|
|
574
|
+
if (uiState.needsAnnouncement) {
|
|
575
|
+
this.props.ariaAnnouncer(this.ref.current);
|
|
576
|
+
this.props.serviceManager.store.dispatch(actions.setMessageWasAnnounced(uiState.id));
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* Indicates if we should render the failed message instead of the actual message.
|
|
581
|
+
*/
|
|
582
|
+
shouldRenderFailedMessage() {
|
|
583
|
+
if (this.state.didRenderErrorOccur) {
|
|
584
|
+
return true;
|
|
585
|
+
}
|
|
586
|
+
const { localMessageItem, message, config } = this.props;
|
|
587
|
+
const { connectToAgentBehavior } = config.public.__ibm__;
|
|
588
|
+
// If the message is a CTA, has a service desk error, and we're supposed to report service desk errors, then we
|
|
589
|
+
// need to render the failed message.
|
|
590
|
+
return (isConnectToAgent(localMessageItem.item) &&
|
|
591
|
+
message.history?.agent_no_service_desk &&
|
|
592
|
+
connectToAgentBehavior === ConnectToAgentBehaviorType.ERROR_MISSING);
|
|
593
|
+
}
|
|
594
|
+
reAnnounceFocusHandle() {
|
|
595
|
+
const handle = this.focusHandleRef.current;
|
|
596
|
+
if (!handle) {
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
this.props.ariaAnnouncer(handle.getAttribute('aria-label'));
|
|
600
|
+
}
|
|
601
|
+
/**
|
|
602
|
+
* Moves focus to this message's focus handle.
|
|
603
|
+
*
|
|
604
|
+
* @see renderFocusHandle
|
|
605
|
+
*/
|
|
606
|
+
requestHandleFocus() {
|
|
607
|
+
const { languagePack, intl, message, botName } = this.props;
|
|
608
|
+
// Announce who said it and then the actual message. The "Bot said" text is normally only read once per
|
|
609
|
+
// MessageResponse instead of once per LocalMessage but since we're moving focus between each LocalMessage, go
|
|
610
|
+
// ahead and announce the "who" part for each one.
|
|
611
|
+
const whoAnnouncement = isRequest(message)
|
|
612
|
+
? languagePack.messages_youSaid
|
|
613
|
+
: intl.formatMessage({ id: 'messages_botSaid' }, { botName });
|
|
614
|
+
const strings = [whoAnnouncement];
|
|
615
|
+
nodeToText(this.messageRef.current, strings);
|
|
616
|
+
// Using this aria-label allows us to make sure that this text is read out loud before JAWS reads its "1 of 2"
|
|
617
|
+
// list item message that it adds after reading the aria-label.
|
|
618
|
+
this.focusHandleRef.current.setAttribute('aria-label', strings.join(' '));
|
|
619
|
+
doFocusRef(this.focusHandleRef, true);
|
|
620
|
+
}
|
|
621
|
+
/**
|
|
622
|
+
* Renders the error state version of this message. This code carefully avoids touching the message data as it
|
|
623
|
+
* could be data that doesn't match what we were expecting.
|
|
624
|
+
*/
|
|
625
|
+
renderFailedRenderMessage() {
|
|
626
|
+
const { messagesIndex } = this.props;
|
|
627
|
+
return (React__default.createElement("div", { className: `WAC__message WAC__message--inlineError WAC__message-${messagesIndex} ${this.props.className || ''}`, ref: this.ref },
|
|
628
|
+
React__default.createElement("div", { className: "WAC__message--padding" },
|
|
629
|
+
React__default.createElement("div", { className: "WAC__bot-message" },
|
|
630
|
+
React__default.createElement(VisuallyHidden, null, this.getWidgetSaidMessage()),
|
|
631
|
+
React__default.createElement("div", { className: "WAC__received WAC__message-vertical-padding WAC__received--text" },
|
|
632
|
+
React__default.createElement(InlineError, { text: this.props.languagePack.errors_singleMessage }))))));
|
|
633
|
+
}
|
|
634
|
+
/**
|
|
635
|
+
* This will render the avatar for the bot message. This is normally the little blue bar next to the message but
|
|
636
|
+
* in the case of a human agent, it can be the agent's avatar image.
|
|
637
|
+
*/
|
|
638
|
+
renderCustomLeft(eventType) {
|
|
639
|
+
const { serviceManager } = this.props;
|
|
640
|
+
const { zIndex } = this.state;
|
|
641
|
+
if (serviceManager.enableUICustomEvents.has(eventType)) {
|
|
642
|
+
const { message, localMessageItem } = this.props;
|
|
643
|
+
const event = {
|
|
644
|
+
element: null, // This is filled in when the component is mounted.
|
|
645
|
+
type: eventType,
|
|
646
|
+
messageItem: localMessageItem.item,
|
|
647
|
+
message,
|
|
648
|
+
setItemZIndex: this.setZIndex,
|
|
649
|
+
};
|
|
650
|
+
return React__default.createElement(UICustomizationElement, { event: event, zIndex: zIndex });
|
|
651
|
+
}
|
|
652
|
+
return null;
|
|
653
|
+
}
|
|
654
|
+
/**
|
|
655
|
+
* Renders the avatar line that appears above each message that has the avatar (for responses) and timestamps.
|
|
656
|
+
*/
|
|
657
|
+
renderAvatarLine(localMessageItem, message) {
|
|
658
|
+
let avatar;
|
|
659
|
+
const { languagePack, botAvatarURL, useAITheme, carbonTheme } = this.props;
|
|
660
|
+
const timestamp = timestampToTimeString(message.history.timestamp);
|
|
661
|
+
let label;
|
|
662
|
+
let actorName;
|
|
663
|
+
let iconClassName = '';
|
|
664
|
+
if (isResponse(message)) {
|
|
665
|
+
// We'll use the first message item for deciding if we should show the agent's avatar.
|
|
666
|
+
const agentMessageType = localMessageItem.item.agent_message_type;
|
|
667
|
+
const agentProfile = message.history.agent_profile;
|
|
668
|
+
if (isAgentStatusMessage(agentMessageType) || agentProfile?.hidden) {
|
|
669
|
+
// These messages don't show an avatar line.
|
|
670
|
+
return null;
|
|
671
|
+
}
|
|
672
|
+
const fromAgent = agentMessageType === AgentMessageType.FROM_AGENT;
|
|
673
|
+
if (fromAgent || agentProfile?.profile_picture_url) {
|
|
674
|
+
avatar = (React__default.createElement(ImageWithFallback, { url: agentProfile?.profile_picture_url, alt: fromAgent ? languagePack.agent_ariaAgentAvatar : languagePack.agent_ariaGenericAvatar, fallback: React__default.createElement(IconHolder, { icon: React__default.createElement(Headset, null) }) }));
|
|
675
|
+
iconClassName = 'WACMessage__Avatar--agent';
|
|
676
|
+
}
|
|
677
|
+
else {
|
|
678
|
+
const icon = useAITheme ? React__default.createElement(AvatarExport, { theme: carbonTheme }) : React__default.createElement(IconHolder, { icon: React__default.createElement(ChatBot, null) });
|
|
679
|
+
const imageUrl = useAITheme ? undefined : botAvatarURL;
|
|
680
|
+
avatar = React__default.createElement(ImageWithFallback, { url: imageUrl, alt: languagePack.agent_ariaGenericBotAvatar, fallback: icon });
|
|
681
|
+
iconClassName = 'WACMessage__Avatar--bot';
|
|
682
|
+
}
|
|
683
|
+
if (fromAgent || agentProfile?.nickname) {
|
|
684
|
+
actorName = agentProfile?.nickname || languagePack.agent_agentNoNameTitle;
|
|
685
|
+
}
|
|
686
|
+
else if (useAITheme) {
|
|
687
|
+
actorName = 'watsonx';
|
|
688
|
+
}
|
|
689
|
+
label = (React__default.createElement("span", { "data-wac-exclude-node-read": true },
|
|
690
|
+
React__default.createElement(FormattedMessage, { id: "message_labelBot", values: { timestamp, actorName } })));
|
|
691
|
+
}
|
|
692
|
+
else {
|
|
693
|
+
label = React__default.createElement(FormattedMessage, { id: "message_labelYou", values: { timestamp } });
|
|
694
|
+
}
|
|
695
|
+
return (React__default.createElement("div", { className: "WACMessage__AvatarLine", key: `${message.id}-avatar-line` },
|
|
696
|
+
avatar && React__default.createElement("div", { className: `WACMessage__Avatar ${iconClassName}` }, avatar),
|
|
697
|
+
React__default.createElement("div", { className: "WACMessage__Label" }, label)));
|
|
698
|
+
}
|
|
699
|
+
/**
|
|
700
|
+
* Renders the state indicator for the message sent by the user. This can appear on the left of message or beneath the
|
|
701
|
+
* message.
|
|
702
|
+
*/
|
|
703
|
+
renderMessageState(message) {
|
|
704
|
+
const { languagePack } = this.props;
|
|
705
|
+
let element;
|
|
706
|
+
let className;
|
|
707
|
+
let showBelowMessage = false;
|
|
708
|
+
const errorState = message.history?.error_state;
|
|
709
|
+
const fileStatus = message.history?.file_upload_status;
|
|
710
|
+
if (errorState === MessageErrorState.FAILED) {
|
|
711
|
+
element = React__default.createElement(InlineError, { text: languagePack.errors_singleMessage });
|
|
712
|
+
className = 'WAC__message-error-failed';
|
|
713
|
+
showBelowMessage = true;
|
|
714
|
+
}
|
|
715
|
+
else if (fileStatus === FileStatusValue.UPLOADING) {
|
|
716
|
+
element = React__default.createElement(Loading, { withOverlay: false, small: true, "aria-label": languagePack.fileSharing_statusUploading });
|
|
717
|
+
className = 'WAC__message-status-file-uploading';
|
|
718
|
+
}
|
|
719
|
+
else if (fileStatus === 'success') {
|
|
720
|
+
element = React__default.createElement(CheckmarkFilled, { "aria-label": languagePack.fileSharing_statusUploading });
|
|
721
|
+
className = 'WAC__message-status-file-success';
|
|
722
|
+
}
|
|
723
|
+
// We probably should include an aria-label here but since we explicit announce state changes in the message
|
|
724
|
+
// service and this icon is contained in a live region, that would result in duplicate text being announced. We
|
|
725
|
+
// can't rely solely on the aria-label here in this live region because the SRs don't seem to reliably announce
|
|
726
|
+
// what we want to announce, moving to success for example. Our a11y expert says it's okay to leave it out here.
|
|
727
|
+
return (element && {
|
|
728
|
+
element: React__default.createElement("div", { className: `WAC__message-status ${className}` }, element),
|
|
729
|
+
showBelowMessage,
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
/**
|
|
733
|
+
* Renders a focus "handle" for this message. When this message gets focus, we actually move focus to an element
|
|
734
|
+
* inside it instead of the entire message. This is only done when the user clicks the scroll handle button on the
|
|
735
|
+
* scroll container that moves focus into the scroll panel or when focus moves from one message to another. We move
|
|
736
|
+
* focus to the handle which is inside the message instead of the message itself because if we make the whole message
|
|
737
|
+
* actually focusable then a screen reader will read the entire message whenever any item inside it gets focus which
|
|
738
|
+
* is not desirable.
|
|
739
|
+
*/
|
|
740
|
+
renderFocusHandle() {
|
|
741
|
+
return (
|
|
742
|
+
// The aria-label is dynamically added when focused.
|
|
743
|
+
// eslint-disable-next-line jsx-a11y/control-has-associated-label
|
|
744
|
+
React__default.createElement("div", { className: "WACMessage--focusHandle", ref: this.focusHandleRef, tabIndex: -1, onFocus: this.onHandleFocus, onBlur: this.onHandleBlur, onKeyDown: event => this.onHandleKeyDown(event), onClick: () => this.reAnnounceFocusHandle(), role: "button" }));
|
|
745
|
+
}
|
|
746
|
+
render() {
|
|
747
|
+
if (this.shouldRenderFailedMessage()) {
|
|
748
|
+
// If an error occurred, don't attempt to do anything with the message. Just show an error.
|
|
749
|
+
return this.renderFailedRenderMessage();
|
|
750
|
+
}
|
|
751
|
+
const { serviceManager, config, localMessageItem, message, languagePack, requestInputFocus, toolingType, messagesIndex, disableUserInputs, showAvatarLine, className, doAutoScroll, isMessageForInput, scrollElementIntoView, isFirstMessageItem, hideFeedback, allowNewFeedback, } = this.props;
|
|
752
|
+
const { isIntermediateStreaming, isWelcomeResponse, disableFadeAnimation } = localMessageItem.ui_state;
|
|
753
|
+
const messageItem = localMessageItem.item;
|
|
754
|
+
const responseType = messageItem.response_type;
|
|
755
|
+
const agentMessageType = messageItem.agent_message_type;
|
|
756
|
+
const fromHistory = message.history.from_history;
|
|
757
|
+
const readWidgetSaid = isFirstMessageItem;
|
|
758
|
+
if (isIntermediateStreaming && !canRenderIntermediateStreaming(messageItem.response_type)) {
|
|
759
|
+
return false;
|
|
760
|
+
}
|
|
761
|
+
const messageComponent = (React__default.createElement(MessageTypeComponent, { serviceManager: serviceManager, toolingType: toolingType, languagePack: languagePack, requestInputFocus: requestInputFocus, message: localMessageItem, originalMessage: message, disableUserInputs: disableUserInputs, isMessageForInput: isMessageForInput, config: config, doAutoScroll: doAutoScroll, scrollElementIntoView: scrollElementIntoView, hideFeedback: hideFeedback, allowNewFeedback: allowNewFeedback }));
|
|
762
|
+
const isCustomMessage = renderAsUserDefinedMessage(localMessageItem.item);
|
|
763
|
+
const isChatStatusMessage = isChatStatusResponseType(localMessageItem);
|
|
764
|
+
let styleObject;
|
|
765
|
+
if (this.state.zIndex !== null && this.state.zIndex !== undefined) {
|
|
766
|
+
styleObject = { zIndex: this.state.zIndex };
|
|
767
|
+
}
|
|
768
|
+
// Don't show animation on the welcome node or for messages that explicitly turn it off.
|
|
769
|
+
const noAnimation = isWelcomeResponse || disableFadeAnimation;
|
|
770
|
+
// If this is a user_defined response type with silent set, we don't want to render all the extra cruft around it.
|
|
771
|
+
const agentClassName = getAgentMessageClassName(agentMessageType, responseType, isCustomMessage);
|
|
772
|
+
const messageIsRequest = isRequest(message);
|
|
773
|
+
const isSystemMessage = isAgentStatusMessage(localMessageItem.item.agent_message_type);
|
|
774
|
+
let isOptionResponseWithoutTitleOrDescription = false;
|
|
775
|
+
if (isOptionItem(localMessageItem.item)) {
|
|
776
|
+
if (!localMessageItem.item.title && !localMessageItem.item.description) {
|
|
777
|
+
isOptionResponseWithoutTitleOrDescription = true;
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
let messageState;
|
|
781
|
+
if (messageIsRequest) {
|
|
782
|
+
messageState = this.renderMessageState(message);
|
|
783
|
+
}
|
|
784
|
+
return (React__default.createElement("div", { id: `WAC__message-${messagesIndex}${serviceManager.namespace.suffix}`, className: cx(`WAC__message WAC__message-${messagesIndex}`, className, agentMessageType && 'WAC__message--agentMessage', {
|
|
785
|
+
'WAC__message--withAvatarLine': showAvatarLine,
|
|
786
|
+
'WAC__message--request': messageIsRequest,
|
|
787
|
+
'WAC__message--systemMessage': isSystemMessage,
|
|
788
|
+
'WAC__message--response': !messageIsRequest,
|
|
789
|
+
'WAC__message--no-animation': noAnimation,
|
|
790
|
+
'WAC__message--custom': isCustomMessage,
|
|
791
|
+
'WAC__message--disabled-inputs': disableUserInputs,
|
|
792
|
+
'WAC__message--has-focus': this.state.focusHandleHasFocus,
|
|
793
|
+
'WAC__message--option-response-without-title-or-description': isOptionResponseWithoutTitleOrDescription,
|
|
794
|
+
}), ref: this.ref, style: styleObject },
|
|
795
|
+
this.renderFocusHandle(),
|
|
796
|
+
showAvatarLine && this.renderAvatarLine(localMessageItem, message),
|
|
797
|
+
React__default.createElement("div", { className: "WAC__message--padding" },
|
|
798
|
+
isResponse(message) && (React__default.createElement("div", { className: "WAC__bot-message" },
|
|
799
|
+
readWidgetSaid && React__default.createElement(VisuallyHidden, null, this.getWidgetSaidMessage()),
|
|
800
|
+
this.renderCustomLeft("customization:message:response:item:left" /* BusEventType.CUSTOMIZATION_RESPONSE_LEFT */),
|
|
801
|
+
React__default.createElement("div", { className: cx('WAC__received', 'WAC__message-vertical-padding', agentClassName, {
|
|
802
|
+
'WAC__received--text': responseType === MessageResponseTypes.TEXT,
|
|
803
|
+
'WAC__received--image': responseType === MessageResponseTypes.IMAGE,
|
|
804
|
+
'WAC__received--options': responseType === MessageResponseTypes.OPTION,
|
|
805
|
+
'WAC__received--search': responseType === MessageResponseTypes.SEARCH,
|
|
806
|
+
'WAC__received--suggestion': responseType === MessageResponseTypes.SUGGESTION,
|
|
807
|
+
'WAC__received--welcome': responseType === 'welcome',
|
|
808
|
+
'WAC__received--inlineError': responseType === MessageResponseTypes.INLINE_ERROR,
|
|
809
|
+
'WAC__received--streamLoading': responseType === InternalMessageResponseType.STREAM_LOADING,
|
|
810
|
+
'WAC__received--channelTransfer': responseType === MessageResponseTypes.CHANNEL_TRANSFER,
|
|
811
|
+
'WAC__received--iframePreviewCard': responseType === MessageResponseTypes.IFRAME,
|
|
812
|
+
'WAC__received--video': responseType === MessageResponseTypes.VIDEO,
|
|
813
|
+
'WAC__received--audio': responseType === MessageResponseTypes.AUDIO,
|
|
814
|
+
'WAC__received--date': responseType === MessageResponseTypes.DATE,
|
|
815
|
+
'WAC__received--card': responseType === MessageResponseTypes.CARD,
|
|
816
|
+
'WAC__received--carousel': responseType === MessageResponseTypes.CAROUSEL,
|
|
817
|
+
'WAC__received--conversationalSearch': responseType === MessageResponseTypes.CONVERSATIONAL_SEARCH,
|
|
818
|
+
'WAC__received--carouselSingle': isSingleItemCarousel(localMessageItem.item),
|
|
819
|
+
'WAC__received--button': responseType === MessageResponseTypes.BUTTON,
|
|
820
|
+
'WAC__received--grid': responseType === MessageResponseTypes.GRID,
|
|
821
|
+
'WAC__received--chatStatusMessage': isChatStatusMessage,
|
|
822
|
+
'WAC__received--fullWidth': localMessageItem.ui_state.fullWidth,
|
|
823
|
+
'WAC__message--historical': fromHistory,
|
|
824
|
+
}), ref: this.messageRef },
|
|
825
|
+
React__default.createElement("div", { className: "WAC__received--inner" }, messageComponent)))),
|
|
826
|
+
messageIsRequest && (React__default.createElement("div", { className: "WAC__sent-container" },
|
|
827
|
+
this.renderCustomLeft("customization:message:request:item:left" /* BusEventType.CUSTOMIZATION_REQUEST_LEFT */),
|
|
828
|
+
React__default.createElement("div", { className: cx('WAC__sentAndMessageState-container', 'WAC__message-vertical-padding', {
|
|
829
|
+
'WAC__sentAndMessageState--belowMessage': messageState?.showBelowMessage,
|
|
830
|
+
}) },
|
|
831
|
+
!messageState?.showBelowMessage && messageState?.element,
|
|
832
|
+
React__default.createElement("div", { className: "WAC__sent" },
|
|
833
|
+
React__default.createElement(VisuallyHidden, null, languagePack.messages_youSaid),
|
|
834
|
+
React__default.createElement("div", { className: "WAC__sent--bubble" },
|
|
835
|
+
React__default.createElement("div", { ref: this.messageRef }, messageComponent))),
|
|
836
|
+
messageState?.showBelowMessage && messageState?.element))))));
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
/**
|
|
840
|
+
* Returns the class name to add to messages with the given agent message type.
|
|
841
|
+
*/
|
|
842
|
+
function getAgentMessageClassName(agentMessageType, messageResponseType, isUserDefinedResponse) {
|
|
843
|
+
if (isUserDefinedResponse) {
|
|
844
|
+
return 'WAC__received--agentCustom';
|
|
845
|
+
}
|
|
846
|
+
if (!messageResponseType ||
|
|
847
|
+
(messageResponseType !== MessageResponseTypes.TEXT && messageResponseType !== MessageResponseTypes.BUTTON)) {
|
|
848
|
+
return '';
|
|
849
|
+
}
|
|
850
|
+
switch (agentMessageType) {
|
|
851
|
+
case null:
|
|
852
|
+
case undefined:
|
|
853
|
+
case AgentMessageType.FROM_USER:
|
|
854
|
+
return null;
|
|
855
|
+
case AgentMessageType.RELOAD_WARNING:
|
|
856
|
+
case AgentMessageType.DISCONNECTED:
|
|
857
|
+
return 'WAC__received--chatStatusMessage';
|
|
858
|
+
case AgentMessageType.FROM_AGENT:
|
|
859
|
+
return 'WAC__received--fromAgent';
|
|
860
|
+
default:
|
|
861
|
+
return 'WAC__received--agentStatusMessage';
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
/**
|
|
865
|
+
* Indicates if this message is a status message. These are messages that are centered in the view.
|
|
866
|
+
*/
|
|
867
|
+
function isAgentStatusMessage(agentMessageType) {
|
|
868
|
+
switch (agentMessageType) {
|
|
869
|
+
case null:
|
|
870
|
+
case undefined:
|
|
871
|
+
case AgentMessageType.FROM_USER:
|
|
872
|
+
case AgentMessageType.RELOAD_WARNING:
|
|
873
|
+
case AgentMessageType.DISCONNECTED:
|
|
874
|
+
case AgentMessageType.FROM_AGENT:
|
|
875
|
+
case AgentMessageType.INLINE_ERROR:
|
|
876
|
+
return false;
|
|
877
|
+
default:
|
|
878
|
+
return true;
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
/**
|
|
882
|
+
* Indicates if an item with the given response type is allowed to be rendered in an intermediate stream state.
|
|
883
|
+
*/
|
|
884
|
+
function canRenderIntermediateStreaming(type) {
|
|
885
|
+
switch (type) {
|
|
886
|
+
case MessageResponseTypes.IMAGE:
|
|
887
|
+
case MessageResponseTypes.VIDEO:
|
|
888
|
+
case MessageResponseTypes.AUDIO:
|
|
889
|
+
case MessageResponseTypes.OPTION:
|
|
890
|
+
case MessageResponseTypes.IFRAME:
|
|
891
|
+
case MessageResponseTypes.INLINE_ERROR:
|
|
892
|
+
case MessageResponseTypes.SUGGESTION:
|
|
893
|
+
case MessageResponseTypes.CONVERSATIONAL_SEARCH:
|
|
894
|
+
case MessageResponseTypes.USER_DEFINED:
|
|
895
|
+
case MessageResponseTypes.TEXT:
|
|
896
|
+
return true;
|
|
897
|
+
default:
|
|
898
|
+
return false;
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
var MessageComponent$1 = withAriaAnnouncer(injectIntl(MessageComponent, { forwardRef: true }));
|
|
902
|
+
|
|
903
|
+
var MoveFocusType;
|
|
904
|
+
(function (MoveFocusType) {
|
|
905
|
+
/**
|
|
906
|
+
* Indicates that focus should be moved to the first message.
|
|
907
|
+
*/
|
|
908
|
+
MoveFocusType[MoveFocusType["FIRST"] = 1] = "FIRST";
|
|
909
|
+
/**
|
|
910
|
+
* Indicates that focus should be moved to the last message.
|
|
911
|
+
*/
|
|
912
|
+
MoveFocusType[MoveFocusType["LAST"] = 2] = "LAST";
|
|
913
|
+
/**
|
|
914
|
+
* Indicates that focus should be moved to the next message.
|
|
915
|
+
*/
|
|
916
|
+
MoveFocusType[MoveFocusType["NEXT"] = 3] = "NEXT";
|
|
917
|
+
/**
|
|
918
|
+
* Indicates that focus should be moved to the previous message.
|
|
919
|
+
*/
|
|
920
|
+
MoveFocusType[MoveFocusType["PREVIOUS"] = 4] = "PREVIOUS";
|
|
921
|
+
/**
|
|
922
|
+
* Indicates that focus should be moved back to the input field.
|
|
923
|
+
*/
|
|
924
|
+
MoveFocusType[MoveFocusType["INPUT"] = 5] = "INPUT";
|
|
925
|
+
})(MoveFocusType || (MoveFocusType = {}));
|
|
926
|
+
class MessagesComponent extends PureComponent {
|
|
927
|
+
constructor() {
|
|
928
|
+
super(...arguments);
|
|
929
|
+
/**
|
|
930
|
+
* Default state.
|
|
931
|
+
*/
|
|
932
|
+
this.state = {
|
|
933
|
+
scrollHandleHasFocus: false,
|
|
934
|
+
};
|
|
935
|
+
/**
|
|
936
|
+
* A registry of references to the child {@link MessageComponent} instances. The keys of the map are the IDs of
|
|
937
|
+
* each message item and the value is the ref to the component.
|
|
938
|
+
*/
|
|
939
|
+
this.messageRefs = new Map();
|
|
940
|
+
/**
|
|
941
|
+
* A ref to the scrollable container that contains the messages.
|
|
942
|
+
*/
|
|
943
|
+
this.messagesContainerWithScrollingRef = React__default.createRef();
|
|
944
|
+
/**
|
|
945
|
+
* A ref to the element that acts as a handle for scrolling.
|
|
946
|
+
*/
|
|
947
|
+
this.scrollHandleRef = React__default.createRef();
|
|
948
|
+
/**
|
|
949
|
+
* A ref to the element that acts as a handle for scrolling.
|
|
950
|
+
*/
|
|
951
|
+
this.agentBannerRef = React__default.createRef();
|
|
952
|
+
/**
|
|
953
|
+
* This will check to see if the messages list is anchored to the bottom of the panel and if so, ensure that the
|
|
954
|
+
* list is still scrolled to the bottom.
|
|
955
|
+
*/
|
|
956
|
+
this.onResize = () => {
|
|
957
|
+
if (this.props.messageState.isScrollAnchored) {
|
|
958
|
+
const element = this.messagesContainerWithScrollingRef.current;
|
|
959
|
+
if (element) {
|
|
960
|
+
element.scrollTop = element.scrollHeight;
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
};
|
|
964
|
+
/**
|
|
965
|
+
* This will execute an auto-scroll operation based on the current state of messages in the component. This should
|
|
966
|
+
* be called whenever the messages change.
|
|
967
|
+
*
|
|
968
|
+
* The scrolling rules are as follows.
|
|
969
|
+
*
|
|
970
|
+
* 1. If the last message is a welcome node, auto-scroll to the top of the message without animating. This
|
|
971
|
+
* means the user has just started a new chat, and we want to just jump to the top.
|
|
972
|
+
* 2. If the component has just mounted and the last message is not a welcome node, just jump to the bottom
|
|
973
|
+
* without animating.
|
|
974
|
+
* 3. If the typing indicator is visible, then scroll that into view.
|
|
975
|
+
* 4. Scroll to the top of the last user message. This means that the bot messages will auto-scroll until the user's
|
|
976
|
+
* last message reaches the top of the window, and then they'll stop and not scroll anymore.
|
|
977
|
+
* 5. If the there is no user message that can be scrolled to, scroll to the last bot message.
|
|
978
|
+
* 6. If the last bot message has an empty output. Just scroll to bottom.
|
|
979
|
+
*
|
|
980
|
+
* @param options The options to control how the scrolling should occur.
|
|
981
|
+
*/
|
|
982
|
+
this.doAutoScroll = throttle((options = {}) => {
|
|
983
|
+
try {
|
|
984
|
+
debugAutoScroll('[doAutoScroll] Running doAutoScroll', options);
|
|
985
|
+
const { scrollToTop, scrollToBottom } = options;
|
|
986
|
+
const { localMessageItems, messageState, allMessagesByID } = this.props;
|
|
987
|
+
const { isTypingCounter, isLoadingCounter } = messageState;
|
|
988
|
+
const { isAgentTyping } = selectAgentDisplayState(this.props);
|
|
989
|
+
const scrollElement = this.messagesContainerWithScrollingRef.current;
|
|
990
|
+
if (scrollToTop !== undefined) {
|
|
991
|
+
doScrollElement(scrollElement, scrollToTop, 0, false);
|
|
992
|
+
return;
|
|
993
|
+
}
|
|
994
|
+
if (scrollToBottom !== undefined) {
|
|
995
|
+
const scrollTop = scrollElement.scrollHeight - scrollElement.offsetHeight - scrollToBottom;
|
|
996
|
+
doScrollElement(scrollElement, scrollTop, 0, false);
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
let animate = true;
|
|
1000
|
+
let setScrollTop;
|
|
1001
|
+
const lastLocalItemIndex = localMessageItems.length - 1;
|
|
1002
|
+
const lastLocalItem = localMessageItems.length ? localMessageItems[lastLocalItemIndex] : null;
|
|
1003
|
+
const lastMessage = allMessagesByID[lastLocalItem?.fullMessageID];
|
|
1004
|
+
if (!lastLocalItem) {
|
|
1005
|
+
debugAutoScroll('[doAutoScroll] No last time');
|
|
1006
|
+
// No messages, so set the scroll position to the top. If we don't set this explicitly, the browser may
|
|
1007
|
+
// decide it remembers the previous scroll position and set it for us.
|
|
1008
|
+
animate = false;
|
|
1009
|
+
setScrollTop = 0;
|
|
1010
|
+
}
|
|
1011
|
+
else if (isTypingCounter > 0 || isLoadingCounter > 0 || isAgentTyping) {
|
|
1012
|
+
// The typing indicator is visible, so scroll to the bottom.
|
|
1013
|
+
setScrollTop = scrollElement.scrollHeight;
|
|
1014
|
+
debugAutoScroll('[doAutoScroll] isTyping visible', isTypingCounter);
|
|
1015
|
+
}
|
|
1016
|
+
else {
|
|
1017
|
+
/**
|
|
1018
|
+
* Determines if the message should be scrolled to. The last user message should be scrolled to by default.
|
|
1019
|
+
* However, the last bot message should be scrolled to if there is no user message that can be scrolled to.
|
|
1020
|
+
*/
|
|
1021
|
+
const shouldScrollToMessage = (localItem, message) => {
|
|
1022
|
+
if (isResponse(message)) {
|
|
1023
|
+
const messageRequest = allMessagesByID[message?.request_id];
|
|
1024
|
+
if (isChatStatusResponseType(localItem)) {
|
|
1025
|
+
// Scroll to chat status messages.
|
|
1026
|
+
return true;
|
|
1027
|
+
}
|
|
1028
|
+
// If the request for this response was silent, then scroll to it instead of scrolling to where the
|
|
1029
|
+
// silent user message would be. But don't do this if it's an empty message (which happens with a
|
|
1030
|
+
// skip_use_input message from an extension).
|
|
1031
|
+
return messageRequest?.history?.silent && messageRequest.input?.text !== '';
|
|
1032
|
+
}
|
|
1033
|
+
return isRequest(message);
|
|
1034
|
+
};
|
|
1035
|
+
// Iterate backwards until we find the last message to scroll to. By default, the user's last message should be
|
|
1036
|
+
// scrolled to. However, if the user's message was silent, the last response should be scrolled to.
|
|
1037
|
+
let messageIndex = localMessageItems.length - 1;
|
|
1038
|
+
let localItem = localMessageItems[messageIndex];
|
|
1039
|
+
let lastScrollableMessageComponent = this.messageRefs.get(localItem?.ui_state.id);
|
|
1040
|
+
while (messageIndex >= 1) {
|
|
1041
|
+
localItem = localMessageItems[messageIndex];
|
|
1042
|
+
const message = allMessagesByID[localItem?.fullMessageID];
|
|
1043
|
+
if (shouldScrollToMessage(localItem, message)) {
|
|
1044
|
+
lastScrollableMessageComponent = this.messageRefs.get(localItem?.ui_state.id);
|
|
1045
|
+
debugAutoScroll(`[doAutoScroll] lastScrollableMessageComponent=${messageIndex}`, localMessageItems[messageIndex], message);
|
|
1046
|
+
break;
|
|
1047
|
+
}
|
|
1048
|
+
messageIndex--;
|
|
1049
|
+
}
|
|
1050
|
+
if (lastScrollableMessageComponent) {
|
|
1051
|
+
// Scroll to the top of the user's message. Those messages have 28px of padding on the top so let's cut
|
|
1052
|
+
// that down to just 8 by scrolling a little bit more.
|
|
1053
|
+
const offsetTop = lastScrollableMessageComponent.ref.current?.offsetTop;
|
|
1054
|
+
setScrollTop = offsetTop + AUTO_SCROLL_EXTRA;
|
|
1055
|
+
debugAutoScroll(`[doAutoScroll] Scrolling to message offsetTop=${offsetTop}`);
|
|
1056
|
+
}
|
|
1057
|
+
else {
|
|
1058
|
+
// No message found.
|
|
1059
|
+
setScrollTop = -1;
|
|
1060
|
+
debugAutoScroll('[doAutoScroll] No message found');
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
if (setScrollTop !== -1) {
|
|
1064
|
+
if (setScrollTop >= scrollElement.scrollTop) {
|
|
1065
|
+
// If this is from history, we don't want to animate.
|
|
1066
|
+
if (lastMessage?.history?.from_history) {
|
|
1067
|
+
animate = false;
|
|
1068
|
+
}
|
|
1069
|
+
debugAutoScroll(`[doAutoScroll] doScrollElement`, scrollElement, setScrollTop, animate);
|
|
1070
|
+
doScrollElement(scrollElement, setScrollTop, 0, animate);
|
|
1071
|
+
// Update the scroll anchor setting based on this new position.
|
|
1072
|
+
this.checkScrollAnchor(true, setScrollTop);
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
catch (error) {
|
|
1077
|
+
// Just ignore any errors. It's not the end of the world if scrolling doesn't work for any reason.
|
|
1078
|
+
consoleError('An error occurred while attempting to scroll.', error);
|
|
1079
|
+
}
|
|
1080
|
+
}, AUTO_SCROLL_THROTTLE_TIMEOUT);
|
|
1081
|
+
/**
|
|
1082
|
+
* Returns the current scrollBottom value for the message scroll panel.
|
|
1083
|
+
*/
|
|
1084
|
+
this.getContainerScrollBottom = () => {
|
|
1085
|
+
return getScrollBottom(this.messagesContainerWithScrollingRef?.current);
|
|
1086
|
+
};
|
|
1087
|
+
/**
|
|
1088
|
+
* Scrolls the given element into view so that it is fully visible. If the element is already visible, then no
|
|
1089
|
+
* scrolling will be done.
|
|
1090
|
+
*
|
|
1091
|
+
* @param element The element to scroll into view.
|
|
1092
|
+
* @param paddingTop An additional pixel value that will over scroll by this amount to give a little padding between
|
|
1093
|
+
* the element and the top of the scroll area.
|
|
1094
|
+
* @param paddingBottom An additional pixel value that will over scroll by this amount to give a little padding
|
|
1095
|
+
* between the element and the top of the scroll area.
|
|
1096
|
+
*/
|
|
1097
|
+
this.scrollElementIntoView = (element, paddingTop = 8, paddingBottom = 8) => {
|
|
1098
|
+
const scrollElement = this.messagesContainerWithScrollingRef.current;
|
|
1099
|
+
const scrollRect = scrollElement.getBoundingClientRect();
|
|
1100
|
+
const elementRect = element.getBoundingClientRect();
|
|
1101
|
+
// The distance the top and bottom of the element is from the top of the message list.
|
|
1102
|
+
const topDistanceFromTop = elementRect.top - scrollRect.top + scrollElement.scrollTop - paddingTop;
|
|
1103
|
+
const bottomDistanceFromTop = elementRect.bottom - scrollRect.top + scrollElement.scrollTop + paddingBottom;
|
|
1104
|
+
const elementHeight = element.offsetHeight + paddingTop + paddingBottom;
|
|
1105
|
+
if (topDistanceFromTop < scrollElement.scrollTop || elementHeight > scrollElement.offsetHeight) {
|
|
1106
|
+
// The top of the element is above the fold or the element doesn't fully fit. Scroll it so its top is at the top
|
|
1107
|
+
// of the scroll panel.
|
|
1108
|
+
doScrollElement(scrollElement, topDistanceFromTop, 0);
|
|
1109
|
+
}
|
|
1110
|
+
else if (bottomDistanceFromTop > scrollElement.scrollTop + scrollElement.offsetHeight) {
|
|
1111
|
+
// The bottom of the element is below the fold. Scroll it so its bottom is at the bottom of the scroll panel.
|
|
1112
|
+
doScrollElement(scrollElement, bottomDistanceFromTop - scrollElement.offsetHeight, 0);
|
|
1113
|
+
}
|
|
1114
|
+
};
|
|
1115
|
+
/**
|
|
1116
|
+
* This is a callback called by a child message component to request that it move focus to a different message.
|
|
1117
|
+
*/
|
|
1118
|
+
this.requestMoveFocus = (moveFocusType, currentMessageIndex) => {
|
|
1119
|
+
if (moveFocusType === MoveFocusType.INPUT) {
|
|
1120
|
+
this.props.requestInputFocus();
|
|
1121
|
+
}
|
|
1122
|
+
else {
|
|
1123
|
+
const { localMessageItems } = this.props;
|
|
1124
|
+
let index;
|
|
1125
|
+
switch (moveFocusType) {
|
|
1126
|
+
case MoveFocusType.LAST:
|
|
1127
|
+
index = localMessageItems.length - 1;
|
|
1128
|
+
break;
|
|
1129
|
+
case MoveFocusType.NEXT:
|
|
1130
|
+
index = currentMessageIndex + 1;
|
|
1131
|
+
if (index >= localMessageItems.length) {
|
|
1132
|
+
index = 0;
|
|
1133
|
+
}
|
|
1134
|
+
break;
|
|
1135
|
+
case MoveFocusType.PREVIOUS:
|
|
1136
|
+
index = currentMessageIndex - 1;
|
|
1137
|
+
if (index < 0) {
|
|
1138
|
+
index = localMessageItems.length - 1;
|
|
1139
|
+
}
|
|
1140
|
+
break;
|
|
1141
|
+
default:
|
|
1142
|
+
index = 0;
|
|
1143
|
+
break;
|
|
1144
|
+
}
|
|
1145
|
+
const messageItem = localMessageItems[index];
|
|
1146
|
+
const ref = this.messageRefs.get(messageItem?.ui_state.id);
|
|
1147
|
+
if (ref) {
|
|
1148
|
+
ref.requestHandleFocus();
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
};
|
|
1152
|
+
}
|
|
1153
|
+
componentDidMount() {
|
|
1154
|
+
this.scrollPanelObserver = new ResizeObserver(this.onResize);
|
|
1155
|
+
this.scrollPanelObserver.observe(this.messagesContainerWithScrollingRef.current);
|
|
1156
|
+
this.previousScrollOffsetHeight = this.messagesContainerWithScrollingRef.current.offsetHeight;
|
|
1157
|
+
}
|
|
1158
|
+
componentDidUpdate(oldProps) {
|
|
1159
|
+
const newProps = this.props;
|
|
1160
|
+
// If the number of messages changes (usually because of new messages) or the state of the "is typing" indicator
|
|
1161
|
+
// changes, then we need to check to see if we want to perform some auto-scrolling behavior.
|
|
1162
|
+
const numMessagesChanged = oldProps.localMessageItems.length !== newProps.localMessageItems.length;
|
|
1163
|
+
const oldAgentDisplayState = selectAgentDisplayState(oldProps);
|
|
1164
|
+
const newAgentDisplayState = selectAgentDisplayState(newProps);
|
|
1165
|
+
const typingChanged = oldProps.messageState.isTypingCounter !== newProps.messageState.isTypingCounter ||
|
|
1166
|
+
oldProps.messageState.isLoadingCounter !== newProps.messageState.isLoadingCounter ||
|
|
1167
|
+
oldAgentDisplayState.isAgentTyping !== newAgentDisplayState.isAgentTyping;
|
|
1168
|
+
if (numMessagesChanged || typingChanged) {
|
|
1169
|
+
const newLastItem = arrayLastValue(newProps.localMessageItems);
|
|
1170
|
+
const oldLastItem = arrayLastValue(oldProps.localMessageItems);
|
|
1171
|
+
// If the last message has changed, then do an auto scroll.
|
|
1172
|
+
const lastItemChanged = newLastItem !== oldLastItem;
|
|
1173
|
+
if (lastItemChanged || typingChanged) {
|
|
1174
|
+
this.doAutoScroll();
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
componentWillUnmount() {
|
|
1179
|
+
// Remove the listeners and observer we added previously.
|
|
1180
|
+
this.scrollPanelObserver.unobserve(this.messagesContainerWithScrollingRef.current);
|
|
1181
|
+
}
|
|
1182
|
+
/**
|
|
1183
|
+
* This function is called when the scrollable messages list is scrolled. It will determine if the scroll panel
|
|
1184
|
+
* has been scrolled all the way to the bottom and if so, it will enable the scroll anchor that will keep it there.
|
|
1185
|
+
* Note that this callback is not attached via the normal react method with an `onScroll` prop as that doesn't
|
|
1186
|
+
* work with under a shadow DOM. This callback is attached directly in {@link componentDidMount}.
|
|
1187
|
+
*
|
|
1188
|
+
* This function will also make a somewhat crude attempt to distinguish if a scroll event has occurred because the
|
|
1189
|
+
* user initiated a scroll or if the application initiated a scroll as the result of a changing in size of the
|
|
1190
|
+
* widget. If the user initiates a scroll, then we use that event to anchor or un-anchor the scroll panel. If the
|
|
1191
|
+
* application did the scroll, we want the anchor state to remain unchanged.
|
|
1192
|
+
*
|
|
1193
|
+
* @param fromAutoScroll Indicates if the reason we are checking the anchor is due to an auto-scroll action.
|
|
1194
|
+
* @param assumeScrollTop A value to assume the scroll panel is (or will be) scrolled to. This can be useful when
|
|
1195
|
+
* an animation is occurring and the current scroll position isn't the final scroll position.
|
|
1196
|
+
*/
|
|
1197
|
+
checkScrollAnchor(fromAutoScroll, assumeScrollTop) {
|
|
1198
|
+
const scrollElement = this.messagesContainerWithScrollingRef.current;
|
|
1199
|
+
// If we're checking because of auto-scrolling, we want check the scroll position even if the scroll detection
|
|
1200
|
+
// is normally suspended because of something like an animation in progress.
|
|
1201
|
+
if (fromAutoScroll ||
|
|
1202
|
+
(this.previousScrollOffsetHeight === scrollElement.offsetHeight && !this.props.suspendScrollDetection)) {
|
|
1203
|
+
// If the scroll panel has been scrolled all the way to the bottom, turn on the anchor.
|
|
1204
|
+
const assumedScrollTop = assumeScrollTop !== undefined ? assumeScrollTop : scrollElement.scrollTop;
|
|
1205
|
+
const isScrollAnchored = assumedScrollTop >= scrollElement.scrollHeight - scrollElement.offsetHeight;
|
|
1206
|
+
if (isScrollAnchored !== this.props.messageState.isScrollAnchored) {
|
|
1207
|
+
this.props.serviceManager.store.dispatch(actions.setChatMessagesStateProperty('isScrollAnchored', isScrollAnchored));
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
this.previousScrollOffsetHeight = scrollElement.offsetHeight;
|
|
1211
|
+
}
|
|
1212
|
+
/**
|
|
1213
|
+
* Moves focus to the button in the agent header.
|
|
1214
|
+
*/
|
|
1215
|
+
requestAgentBannerFocus() {
|
|
1216
|
+
if (this.agentBannerRef.current) {
|
|
1217
|
+
return this.agentBannerRef.current.requestFocus();
|
|
1218
|
+
}
|
|
1219
|
+
return false;
|
|
1220
|
+
}
|
|
1221
|
+
/**
|
|
1222
|
+
* Scrolls to the (full) message with the given ID. Since there may be multiple message items in a given
|
|
1223
|
+
* message, this will scroll the first message to the top of the message window.
|
|
1224
|
+
*
|
|
1225
|
+
* @param messageID The (full) message ID to scroll to.
|
|
1226
|
+
* @param animate Whether or not the scroll should be animated. Defaults to true.
|
|
1227
|
+
*/
|
|
1228
|
+
doScrollToMessage(messageID, animate = true) {
|
|
1229
|
+
try {
|
|
1230
|
+
// Find the component that has the message we want to scroll to.
|
|
1231
|
+
const { localMessageItems } = this.props;
|
|
1232
|
+
let panelComponent;
|
|
1233
|
+
for (let index = 0; index <= localMessageItems.length; index++) {
|
|
1234
|
+
const messageItem = localMessageItems[index];
|
|
1235
|
+
if (messageItem.fullMessageID === messageID) {
|
|
1236
|
+
panelComponent = this.messageRefs.get(messageItem.ui_state.id);
|
|
1237
|
+
break;
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
if (panelComponent) {
|
|
1241
|
+
const scrollElement = this.messagesContainerWithScrollingRef.current;
|
|
1242
|
+
// Scroll to the top of the message.
|
|
1243
|
+
const setScrollTop = panelComponent.ref.current.offsetTop;
|
|
1244
|
+
// Do the scrolling.
|
|
1245
|
+
doScrollElement(scrollElement, setScrollTop, 0, animate);
|
|
1246
|
+
// Update the scroll anchor setting based on this new position.
|
|
1247
|
+
this.checkScrollAnchor(true, setScrollTop);
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
catch (error) {
|
|
1251
|
+
// Just ignore any errors. It's not the end of the world if scrolling doesn't work for any reason.
|
|
1252
|
+
consoleError('An error occurred while attempting to scroll.', error);
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
/**
|
|
1256
|
+
* Get all the elements inside the lastBotMessageGroupID.
|
|
1257
|
+
*/
|
|
1258
|
+
getLastOutputMessageElements() {
|
|
1259
|
+
const { localMessageItems, allMessagesByID } = this.props;
|
|
1260
|
+
const lastMessageItem = arrayLastValue(localMessageItems);
|
|
1261
|
+
const lastMessage = allMessagesByID[lastMessageItem?.fullMessageID];
|
|
1262
|
+
if (isResponse(lastMessage)) {
|
|
1263
|
+
const elements = [];
|
|
1264
|
+
let hasFoundLastBotMessageGroupID = false;
|
|
1265
|
+
// Loop from end of messages array until we find the elements with the lastBotMessageGroupID.
|
|
1266
|
+
for (let index = localMessageItems.length - 1; index >= 0; index--) {
|
|
1267
|
+
const messageItem = localMessageItems[index];
|
|
1268
|
+
const componentRef = this.messageRefs.get(messageItem?.ui_state.id);
|
|
1269
|
+
if (componentRef) {
|
|
1270
|
+
const { getLocalMessage } = componentRef;
|
|
1271
|
+
if (getLocalMessage().fullMessageID === lastMessage.id) {
|
|
1272
|
+
hasFoundLastBotMessageGroupID = true;
|
|
1273
|
+
const element = componentRef.ref?.current;
|
|
1274
|
+
if (element) {
|
|
1275
|
+
elements.push(element);
|
|
1276
|
+
}
|
|
1277
|
+
else {
|
|
1278
|
+
// If there are no refs to the elements yet, there is nothing to do here.
|
|
1279
|
+
break;
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
else if (hasFoundLastBotMessageGroupID) {
|
|
1283
|
+
break;
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
// Reverse so the older messages are first.
|
|
1288
|
+
return elements.reverse();
|
|
1289
|
+
}
|
|
1290
|
+
return [];
|
|
1291
|
+
}
|
|
1292
|
+
/**
|
|
1293
|
+
* JSX to show typing indicator.
|
|
1294
|
+
*
|
|
1295
|
+
* @param isTypingMessage The aria label for the typing indicator.
|
|
1296
|
+
* @param index The index of this message.
|
|
1297
|
+
*/
|
|
1298
|
+
renderTypingIndicator(isTypingMessage, index) {
|
|
1299
|
+
return (React__default.createElement("div", { className: `WAC__message WAC__message-${index} WAC__message--lastMessage` },
|
|
1300
|
+
React__default.createElement("div", { className: "WAC__message--padding" },
|
|
1301
|
+
isTypingMessage && React__default.createElement(AriaLiveMessageExport, { message: isTypingMessage }),
|
|
1302
|
+
React__default.createElement("div", { className: "WAC__bot-message" },
|
|
1303
|
+
React__default.createElement("div", { className: "WAC__received WAC__received--loading WAC__message-vertical-padding" },
|
|
1304
|
+
React__default.createElement("div", { className: "WAC__received--inner" },
|
|
1305
|
+
React__default.createElement(InlineLoadingComponent, { loop: true, carbonTheme: this.props.carbonTheme })))))));
|
|
1306
|
+
}
|
|
1307
|
+
/**
|
|
1308
|
+
* Renders the given message.
|
|
1309
|
+
*
|
|
1310
|
+
* @param localMessage The localMessage to be processed.
|
|
1311
|
+
* @param fullMessage The full message to be processed.
|
|
1312
|
+
* @param messagesIndex The index of the message.
|
|
1313
|
+
* @param showBeforeWelcomeNodeElement Boolean indicating if this is the first message in the most recent welcome
|
|
1314
|
+
* node.
|
|
1315
|
+
* @param isMessageForInput Indicates if this message is part the most recent message response that allows for input.
|
|
1316
|
+
* @param isFirstMessageItem Indicates if this message item is the first item in a message response.
|
|
1317
|
+
* @param lastMessageID The ID of the last full message shown.
|
|
1318
|
+
*/
|
|
1319
|
+
renderMessage(localMessage, fullMessage, messagesIndex, showBeforeWelcomeNodeElement, isMessageForInput, isFirstMessageItem, lastMessageID) {
|
|
1320
|
+
const { serviceManager, config, languagePack, requestInputFocus, toolingType, persistedToBrowserStorage, botName, messageState, alternateSuggestionsState, locale, botAvatarURL, carbonTheme, useAITheme, } = this.props;
|
|
1321
|
+
const inputState = selectInputState(this.props);
|
|
1322
|
+
const { isAgentTyping } = selectAgentDisplayState(this.props);
|
|
1323
|
+
const { isTypingCounter, isLoadingCounter } = messageState;
|
|
1324
|
+
const { chatState } = persistedToBrowserStorage;
|
|
1325
|
+
const { disclaimersAccepted } = chatState;
|
|
1326
|
+
// If there is a disclaimer, messages should only be rendered once it's accepted.
|
|
1327
|
+
if (config.public.disclaimer?.isOn && !disclaimersAccepted[window.location.hostname]) {
|
|
1328
|
+
return null;
|
|
1329
|
+
}
|
|
1330
|
+
const totalMessagesWithTyping = this.props.localMessageItems.length + (isTypingCounter > 0 || isLoadingCounter > 0 || isAgentTyping ? 1 : 0);
|
|
1331
|
+
const isLastMessage = messagesIndex === totalMessagesWithTyping - 1;
|
|
1332
|
+
const className = cx({
|
|
1333
|
+
'WAC__message--firstMessage': messagesIndex === 0,
|
|
1334
|
+
'WAC__message--lastMessage': isLastMessage,
|
|
1335
|
+
'WAC__message--suggestionsPadding': isLastMessage &&
|
|
1336
|
+
alternateSuggestionsState.config.is_on &&
|
|
1337
|
+
!persistedToBrowserStorage.chatState.agentState.isConnected,
|
|
1338
|
+
});
|
|
1339
|
+
// The user can only provide feedback on the last message.
|
|
1340
|
+
const allowNewFeedback = localMessage.fullMessageID === lastMessageID;
|
|
1341
|
+
// We hide all feedback messages in the agent app.
|
|
1342
|
+
const hideFeedback = this.props.config.public.agentAppConfig?.is_on;
|
|
1343
|
+
const messageItemID = localMessage.ui_state.id;
|
|
1344
|
+
const message = (React__default.createElement(MessageComponent$1, { ref: (component) => {
|
|
1345
|
+
if (component) {
|
|
1346
|
+
this.messageRefs.set(messageItemID, component);
|
|
1347
|
+
}
|
|
1348
|
+
else {
|
|
1349
|
+
this.messageRefs.delete(messageItemID);
|
|
1350
|
+
}
|
|
1351
|
+
}, className: className, config: config, localMessageItem: localMessage, message: fullMessage, languagePack: languagePack, requestInputFocus: requestInputFocus, serviceManager: serviceManager, toolingType: toolingType, messagesIndex: messagesIndex, botName: botName, disableUserInputs: inputState.isReadonly, isMessageForInput: isMessageForInput, showAvatarLine: isFirstMessageItem, botAvatarURL: botAvatarURL, requestMoveFocus: this.requestMoveFocus, doAutoScroll: this.doAutoScroll, scrollElementIntoView: this.scrollElementIntoView, isFirstMessageItem: isFirstMessageItem, locale: locale, carbonTheme: carbonTheme, useAITheme: useAITheme, allowNewFeedback: allowNewFeedback, hideFeedback: hideFeedback }));
|
|
1352
|
+
if (showBeforeWelcomeNodeElement) {
|
|
1353
|
+
return (React__default.createElement(LatestWelcomeNodes$1, { welcomeNodeBeforeElement: serviceManager.writeableElements[WriteableElementName.WELCOME_NODE_BEFORE_ELEMENT], key: messageItemID }, message));
|
|
1354
|
+
}
|
|
1355
|
+
return React__default.createElement(Fragment, { key: messageItemID }, message);
|
|
1356
|
+
}
|
|
1357
|
+
/**
|
|
1358
|
+
* Renders the agent banner that appears at the top of the messages list when connecting to an agent.
|
|
1359
|
+
*/
|
|
1360
|
+
renderAgentBanner() {
|
|
1361
|
+
return React__default.createElement(AgentBannerContainer, { bannerRef: this.agentBannerRef, onButtonClick: this.props.onEndAgentChat });
|
|
1362
|
+
}
|
|
1363
|
+
/**
|
|
1364
|
+
* Renders an element that acts as a "handle" for the scroll panel. This is provided to allow the scroll panel to be
|
|
1365
|
+
* moved using the keyboard. When this element gets focus the keyboard can be used. Normally we would add
|
|
1366
|
+
* tabIndex=0 to the scroll panel itself but that has the unfortunate consequence of causing the scroll panel
|
|
1367
|
+
* to get focus when you click on it which we don't want. When this element gets focus it causes an extra class
|
|
1368
|
+
* name to be added to the scroll panel which displays a focus indicator on the scroll panel even though it
|
|
1369
|
+
* doesn't actually have focus. This element is not actually visible.
|
|
1370
|
+
*
|
|
1371
|
+
* In addition to providing the ability to scroll the panel, this acts as a button that will move focus to one of
|
|
1372
|
+
* the messages inside the scroll panel to provide additional navigation options.
|
|
1373
|
+
*
|
|
1374
|
+
* @param atTop Indicates if we're rendering the scroll handle at the top or bottom of the scroll panel.
|
|
1375
|
+
*/
|
|
1376
|
+
renderScrollHandle(atTop) {
|
|
1377
|
+
const { languagePack } = this.props;
|
|
1378
|
+
let labelKey;
|
|
1379
|
+
if (IS_MOBILE) {
|
|
1380
|
+
labelKey = atTop ? 'messages_scrollHandle' : 'messages_scrollHandleEnd';
|
|
1381
|
+
}
|
|
1382
|
+
else {
|
|
1383
|
+
labelKey = atTop ? 'messages_scrollHandleDetailed' : 'messages_scrollHandleEndDetailed';
|
|
1384
|
+
}
|
|
1385
|
+
const onClick = IS_MOBILE
|
|
1386
|
+
? undefined
|
|
1387
|
+
: () => this.requestMoveFocus(atTop ? MoveFocusType.FIRST : MoveFocusType.LAST, 0);
|
|
1388
|
+
return (React__default.createElement("button", { type: "button", className: "WACMessages--scrollHandle", ref: this.scrollHandleRef, tabIndex: 0, "aria-label": languagePack[labelKey] || languagePack.messages_scrollHandle, onClick: onClick, onFocus: () => this.setState({ scrollHandleHasFocus: true }), onBlur: () => this.setState({ scrollHandleHasFocus: false }) }));
|
|
1389
|
+
}
|
|
1390
|
+
/**
|
|
1391
|
+
* As soon as the user sends a message, we want to disable all the previous message responses to prevent the user
|
|
1392
|
+
* from interacting with them again. However, if the user's message results in an error, we want to re-enable the
|
|
1393
|
+
* last response from the bot to prevent the user from getting stuck in the case where the input bar is disabled.
|
|
1394
|
+
* This function returns the id of the last message that is permitted to be enabled.
|
|
1395
|
+
*/
|
|
1396
|
+
getMessageIDForUserInput() {
|
|
1397
|
+
const { localMessageItems, allMessagesByID } = this.props;
|
|
1398
|
+
for (let index = localMessageItems.length - 1; index >= 0; index--) {
|
|
1399
|
+
const message = localMessageItems[index];
|
|
1400
|
+
const originalMessage = allMessagesByID[message.fullMessageID];
|
|
1401
|
+
if (isRequest(originalMessage) && originalMessage?.history?.error_state !== MessageErrorState.FAILED) {
|
|
1402
|
+
// If we find a request that was not an error, then we need to disable everything.
|
|
1403
|
+
return null;
|
|
1404
|
+
}
|
|
1405
|
+
if (isResponse(originalMessage)) {
|
|
1406
|
+
// If we didn't find a successful request, then the first response we find can be enabled.
|
|
1407
|
+
return message.fullMessageID;
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
// Nothing should be enabled.
|
|
1411
|
+
return null;
|
|
1412
|
+
}
|
|
1413
|
+
/**
|
|
1414
|
+
* Returns an array of React elements created by this.renderMessage starting from a given index and until the end of
|
|
1415
|
+
* the array OR optionally until we hit a new welcome node.
|
|
1416
|
+
*
|
|
1417
|
+
* @param messageIDForInput The ID of the last message response that can receive input.
|
|
1418
|
+
*/
|
|
1419
|
+
renderMessages(messageIDForInput) {
|
|
1420
|
+
const { localMessageItems, allMessagesByID } = this.props;
|
|
1421
|
+
const renderMessageArray = [];
|
|
1422
|
+
const lastMessageID = arrayLastValue(localMessageItems)?.fullMessageID;
|
|
1423
|
+
let previousMessageID = null;
|
|
1424
|
+
for (let currentIndex = 0; currentIndex < localMessageItems.length; currentIndex++) {
|
|
1425
|
+
const localMessageItem = localMessageItems[currentIndex];
|
|
1426
|
+
const fullMessage = allMessagesByID[localMessageItem.fullMessageID];
|
|
1427
|
+
const isMessageForInput = messageIDForInput === localMessageItem.fullMessageID;
|
|
1428
|
+
const isFirstMessageItem = previousMessageID !== localMessageItem.fullMessageID;
|
|
1429
|
+
const showBeforeWelcomeNodeElement = localMessageItem.ui_state.isWelcomeResponse && isFirstMessageItem;
|
|
1430
|
+
previousMessageID = localMessageItem.fullMessageID;
|
|
1431
|
+
renderMessageArray.push(this.renderMessage(localMessageItem, fullMessage, currentIndex, showBeforeWelcomeNodeElement, isMessageForInput, isFirstMessageItem, lastMessageID));
|
|
1432
|
+
}
|
|
1433
|
+
return renderMessageArray;
|
|
1434
|
+
}
|
|
1435
|
+
render() {
|
|
1436
|
+
const { localMessageItems, messageState, intl, botName, serviceManager, notifications, persistedToBrowserStorage } = this.props;
|
|
1437
|
+
const isConnectedToAgent = persistedToBrowserStorage.chatState.agentState.isConnected;
|
|
1438
|
+
const { isTypingCounter, isLoadingCounter } = messageState;
|
|
1439
|
+
const { isAgentTyping } = selectAgentDisplayState(this.props);
|
|
1440
|
+
const { scrollHandleHasFocus } = this.state;
|
|
1441
|
+
const messageIDForInput = this.getMessageIDForUserInput();
|
|
1442
|
+
const regularMessages = this.renderMessages(messageIDForInput);
|
|
1443
|
+
let isTypingMessage;
|
|
1444
|
+
if (isAgentTyping) {
|
|
1445
|
+
isTypingMessage = intl.formatMessage({ id: 'messages_agentIsTyping' });
|
|
1446
|
+
}
|
|
1447
|
+
else if (isTypingCounter) {
|
|
1448
|
+
isTypingMessage = intl.formatMessage({ id: 'messages_botIsTyping' }, { botName });
|
|
1449
|
+
}
|
|
1450
|
+
else if (isLoadingCounter) {
|
|
1451
|
+
isTypingMessage = intl.formatMessage({ id: 'messages_botIsLoading' }, { botName });
|
|
1452
|
+
}
|
|
1453
|
+
return (React__default.createElement("div", { id: `WACMessages--holder${serviceManager.namespace.suffix}`, className: "WACMessages--holder" },
|
|
1454
|
+
this.renderAgentBanner(),
|
|
1455
|
+
React__default.createElement("div", { className: cx('WACMessages__Wrapper', {
|
|
1456
|
+
'WACMessages__Wrapper--scrollHandleHasFocus': scrollHandleHasFocus,
|
|
1457
|
+
}) },
|
|
1458
|
+
React__default.createElement("div", { id: `WAC__messages${serviceManager.namespace.suffix}`, className: "WAC__messages", ref: this.messagesContainerWithScrollingRef, onScroll: () => this.checkScrollAnchor() },
|
|
1459
|
+
this.renderScrollHandle(true),
|
|
1460
|
+
regularMessages,
|
|
1461
|
+
(Boolean(isTypingCounter) || Boolean(isLoadingCounter) || isAgentTyping) &&
|
|
1462
|
+
this.renderTypingIndicator(isTypingMessage, localMessageItems.length),
|
|
1463
|
+
React__default.createElement(Notifications, { serviceManager: serviceManager, notifications: notifications }),
|
|
1464
|
+
this.renderScrollHandle(false))),
|
|
1465
|
+
!isConnectedToAgent && React__default.createElement(AlternateSuggestionsButtonExport, null)));
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
function debugAutoScroll(message, ...args) {
|
|
1469
|
+
}
|
|
1470
|
+
var MessagesComponent$1 = withServiceManager(connect((state) => state, null, null, {
|
|
1471
|
+
forwardRef: true,
|
|
1472
|
+
})(MessagesComponent));
|
|
1473
|
+
|
|
1474
|
+
/**
|
|
1475
|
+
* Creates a memoizer that will take an array of keys and a map of those keys to values and return an array of
|
|
1476
|
+
* values that corresponds to those keys.
|
|
1477
|
+
*
|
|
1478
|
+
* This is optimized to only return a new array of values if the array would actually contain different values
|
|
1479
|
+
* (either in a different order or different values). It performs element-wise comparisons of both the requested
|
|
1480
|
+
* array of keys as well as only the specific values from the map. If the map has extra values that are not used,
|
|
1481
|
+
* those will be ignored.
|
|
1482
|
+
*
|
|
1483
|
+
* For example:
|
|
1484
|
+
* ['key1'], {key1: 'value1', key2: 'value2'} => ['value1']
|
|
1485
|
+
*/
|
|
1486
|
+
function createUnmappingMemoizer() {
|
|
1487
|
+
return memoizeOne((keys, map) => keys.map(key => map[key]), isUnmappingEqual);
|
|
1488
|
+
}
|
|
1489
|
+
/**
|
|
1490
|
+
* This function will determine if any of the specific messages used by this class have changed. Note: this could
|
|
1491
|
+
* be a little more efficient if it was pushed up higher in the component chain.
|
|
1492
|
+
*/
|
|
1493
|
+
function isUnmappingEqual(newArgs, oldArgs) {
|
|
1494
|
+
const [keys1, map1] = newArgs;
|
|
1495
|
+
const [keys2, map2] = oldArgs;
|
|
1496
|
+
if (keys1 === keys2 && map1 === map2) {
|
|
1497
|
+
// Both sets of arguments are the same.
|
|
1498
|
+
return true;
|
|
1499
|
+
}
|
|
1500
|
+
if (keys1.length !== keys2.length) {
|
|
1501
|
+
// The array are different sizes, so the values will not be the same.
|
|
1502
|
+
return false;
|
|
1503
|
+
}
|
|
1504
|
+
// Check each value one by one.
|
|
1505
|
+
for (let index = 0; index <= keys1.length; index++) {
|
|
1506
|
+
const key1 = keys1[index];
|
|
1507
|
+
const key2 = keys2[index];
|
|
1508
|
+
if (key1 !== key2) {
|
|
1509
|
+
// If a key is different, the values will be different.
|
|
1510
|
+
return false;
|
|
1511
|
+
}
|
|
1512
|
+
const value1 = map1[key1];
|
|
1513
|
+
const value2 = map2[key2];
|
|
1514
|
+
if (value1 !== value2) {
|
|
1515
|
+
// A value was found to be different.
|
|
1516
|
+
return false;
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
// Everything found to be the same.
|
|
1520
|
+
return true;
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
function AlternateSuggestionsBackground() {
|
|
1524
|
+
const panelState = useSelector(state => state.alternateSuggestionsState.panelState);
|
|
1525
|
+
const serviceManager = useServiceManager();
|
|
1526
|
+
if (panelState === AlternateSuggestionsPanelState.CLOSED) {
|
|
1527
|
+
return null;
|
|
1528
|
+
}
|
|
1529
|
+
// If the user clicks on this background, then auto-close the panel.
|
|
1530
|
+
const onClick = () => serviceManager.store.dispatch(actions.setAlternateSuggestionsPanelState(AlternateSuggestionsPanelState.CLOSED, 'Event'));
|
|
1531
|
+
// eslint-disable-next-line
|
|
1532
|
+
return React__default.createElement("div", { className: "WACBackgroundCover", onClick: onClick });
|
|
1533
|
+
}
|
|
1534
|
+
const AlternateSuggestionsBackgroundExport = React__default.memo(AlternateSuggestionsBackground);
|
|
1535
|
+
|
|
1536
|
+
function useLayoutEffectDidUpdate(effect, deps) {
|
|
1537
|
+
const hasRunRef = useRef(false);
|
|
1538
|
+
useLayoutEffect(() => {
|
|
1539
|
+
if (hasRunRef.current) {
|
|
1540
|
+
return effect();
|
|
1541
|
+
}
|
|
1542
|
+
hasRunRef.current = true;
|
|
1543
|
+
return undefined;
|
|
1544
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1545
|
+
}, deps);
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
/**
|
|
1549
|
+
* Compares two numbers in a way that is compatible with the sort() method; it will result in an ascending sort order.
|
|
1550
|
+
*
|
|
1551
|
+
* @param {number} number1 First number to be compared.
|
|
1552
|
+
* @param {number} number2 Second number to be compared.
|
|
1553
|
+
* @returns {number} -1 if the first number is smaller than the second one; 1 if the reverse is true;
|
|
1554
|
+
* or 0 if both numbers are equal.
|
|
1555
|
+
*/
|
|
1556
|
+
function compareNumbers(number1, number2) {
|
|
1557
|
+
if (number1 < number2) {
|
|
1558
|
+
return -1;
|
|
1559
|
+
}
|
|
1560
|
+
if (number1 > number2) {
|
|
1561
|
+
return 1;
|
|
1562
|
+
}
|
|
1563
|
+
return 0;
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
const PDFViewerContainer = lazyPDFViewer();
|
|
1567
|
+
function OpenPDFButton(props) {
|
|
1568
|
+
const { buttonLabel, documentTitle, url, tooltipText } = props;
|
|
1569
|
+
const direction = document.dir;
|
|
1570
|
+
const [showPDFViewer, setShowPDFViewer] = useState(false);
|
|
1571
|
+
const onClose = useCallback(() => setShowPDFViewer(false), []);
|
|
1572
|
+
return (React__default.createElement("div", { className: "WACOpenPDFButton" },
|
|
1573
|
+
React__default.createElement(Button, { className: "WACOpenPDFButton__Button", kind: "tertiary", iconDescription: tooltipText || buttonLabel, hasIconOnly: true, size: "sm", tooltipPosition: direction === 'rtl' ? 'left' : 'right', "aria-label": buttonLabel, onClick: () => setShowPDFViewer(true) },
|
|
1574
|
+
React__default.createElement(DocumentView, null)),
|
|
1575
|
+
React__default.createElement("div", { className: "WACOpenPDFButton__Text WACWidget__textEllipsis" }, buttonLabel),
|
|
1576
|
+
showPDFViewer && (React__default.createElement(Suspense, { fallback: React__default.createElement(LoadingPDFViewer, null) },
|
|
1577
|
+
React__default.createElement(PDFViewerContainer, { key: url, url: url, title: documentTitle, onClose: onClose })))));
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
const trackProps = {
|
|
1581
|
+
eventName: 'Assistant Message Received from User',
|
|
1582
|
+
type: 'search',
|
|
1583
|
+
};
|
|
1584
|
+
const suggestionsTrackProps = {
|
|
1585
|
+
eventName: 'Suggestion Selected',
|
|
1586
|
+
eventDescription: 'User clicks on search suggestions bookmark',
|
|
1587
|
+
type: 'Search',
|
|
1588
|
+
};
|
|
1589
|
+
function SearchResultComponentBody(props) {
|
|
1590
|
+
const { answer, title, titleIsURL, body, shouldTruncateBody, url, innerRef, showBookmarkButton, urlIsValid, disablePDFViewer, } = props;
|
|
1591
|
+
const languagePack = useLanguagePack();
|
|
1592
|
+
const serviceManager = useServiceManager();
|
|
1593
|
+
const intl = useIntl();
|
|
1594
|
+
const windowWidth = useWindowSize().width;
|
|
1595
|
+
const direction = document.dir;
|
|
1596
|
+
const [bodyExpanded, setBodyExpanded] = useState(props.isBookmark);
|
|
1597
|
+
const toggleExpandedState = () => {
|
|
1598
|
+
setBodyExpanded(!bodyExpanded);
|
|
1599
|
+
};
|
|
1600
|
+
function onBookmarkClick() {
|
|
1601
|
+
serviceManager.actions.track(suggestionsTrackProps);
|
|
1602
|
+
if (props.onBookmarkClick) {
|
|
1603
|
+
props.onBookmarkClick();
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
function onLinkClick(event) {
|
|
1607
|
+
serviceManager.actions.track(trackProps);
|
|
1608
|
+
if (!props.url) {
|
|
1609
|
+
// If the link doesn't actually have any content, we need to stop the browser's normal open behavior
|
|
1610
|
+
event.preventDefault();
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
function renderLink() {
|
|
1614
|
+
if (urlIsValid) {
|
|
1615
|
+
const label = getSearchLinkLabel(title, url, titleIsURL);
|
|
1616
|
+
if (shouldShowAsPDFButton(url, windowWidth, disablePDFViewer)) {
|
|
1617
|
+
const buttonLabel = intl.formatMessage({ id: 'messages_searchResultsOpenDocumentWithLabel' }, { documentName: label });
|
|
1618
|
+
return (React__default.createElement(OpenPDFButton, { buttonLabel: buttonLabel, documentTitle: label, url: url, tooltipText: languagePack.messages_searchResultsOpenDocument }));
|
|
1619
|
+
}
|
|
1620
|
+
return (React__default.createElement("a", { onClick: onLinkClick, className: "WAC__button--link", href: url || '#', target: "_blank", rel: "noopener noreferrer" },
|
|
1621
|
+
React__default.createElement("div", { className: "WACSearchMessage__TitleUrl WACWidget__textEllipsis" }, label)));
|
|
1622
|
+
}
|
|
1623
|
+
return React__default.createElement("div", { className: "WACSearchMessage__TitleUrl WACWidget__textEllipsis" }, title || url);
|
|
1624
|
+
}
|
|
1625
|
+
// If the user interacts with a search result we send an event to the back-end for tracking.
|
|
1626
|
+
// App looks for clicks on this attribute to send that event.
|
|
1627
|
+
const bodyContent = body && (React__default.createElement("div", { className: cx({
|
|
1628
|
+
WAC__searchResponseBody: shouldTruncateBody,
|
|
1629
|
+
'WACSearchMessage__Result--full': bodyExpanded,
|
|
1630
|
+
}) }, body));
|
|
1631
|
+
let icon;
|
|
1632
|
+
if (showBookmarkButton) {
|
|
1633
|
+
icon = (React__default.createElement(Button, { renderIcon: Bookmark, iconDescription: languagePack.suggestions_searchBookmarkButtonDescription, hasIconOnly: true, tooltipPosition: direction === 'rtl' ? 'right' : 'left', onClick: onBookmarkClick, kind: "ghost", size: "sm" }));
|
|
1634
|
+
}
|
|
1635
|
+
else if (body && shouldTruncateBody) {
|
|
1636
|
+
icon = (React__default.createElement(Button, { renderIcon: shouldTruncateBody && !bodyExpanded ? ChevronDown : ChevronUp, iconDescription: shouldTruncateBody && !bodyExpanded
|
|
1637
|
+
? languagePack.messages_searchResultsExpand
|
|
1638
|
+
: languagePack.messages_searchResultsCollapse, hasIconOnly: true, tooltipPosition: direction === 'rtl' ? 'right' : 'left', onClick: toggleExpandedState, kind: "ghost", size: "sm" }));
|
|
1639
|
+
}
|
|
1640
|
+
// Content is the actual content of the result cards
|
|
1641
|
+
let content;
|
|
1642
|
+
// ResultBottom is the title/url combination to display at the bottom of the card.
|
|
1643
|
+
let resultBottom;
|
|
1644
|
+
if (title || url) {
|
|
1645
|
+
resultBottom = renderLink();
|
|
1646
|
+
content = bodyContent;
|
|
1647
|
+
}
|
|
1648
|
+
else if (body) {
|
|
1649
|
+
content = bodyContent;
|
|
1650
|
+
}
|
|
1651
|
+
return (React__default.createElement("div", { className: cx('WAC__searchResult', {
|
|
1652
|
+
'WACSearchMessage--hasAnswer': answer,
|
|
1653
|
+
'WACSearchMessage--hasContent': content,
|
|
1654
|
+
}), ref: innerRef },
|
|
1655
|
+
content && React__default.createElement("div", { className: "WACSearchMessage__ResultContent" }, content),
|
|
1656
|
+
React__default.createElement("div", { className: "WACSearchMessage__ResultBottom-wrapper" },
|
|
1657
|
+
resultBottom && React__default.createElement("div", { className: "WACSearchMessage__ResultBottom" }, resultBottom),
|
|
1658
|
+
icon && React__default.createElement("div", { className: "WACSearchMessage__ResultIcon" }, icon))));
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
/**
|
|
1662
|
+
* Displays a single search result from a search response message that can have multiple results.
|
|
1663
|
+
*/
|
|
1664
|
+
// This is set to how many characters can show BEFORE we include the ellipsis styling. Note that the actual
|
|
1665
|
+
// number of characters displayed may be more or less than this number because the position of the ellipsis is
|
|
1666
|
+
// decided by the browser.
|
|
1667
|
+
const CHARACTERS_FOR_ELLIPSIS = 200;
|
|
1668
|
+
function SearchResultComponent(props) {
|
|
1669
|
+
const { searchResult, announceOnMount, onBookmarkClick, showBookmarkButton, isBookmark } = props;
|
|
1670
|
+
const disablePDFViewer = useSelector((state) => state.config.public.disablePDFViewer);
|
|
1671
|
+
const charactersForEllipsis = props.charactersForEllipsis || CHARACTERS_FOR_ELLIPSIS;
|
|
1672
|
+
// If requested, make sure this element gets announced when it is mounted.
|
|
1673
|
+
const ref = useRef();
|
|
1674
|
+
const ariaAnnouncer = useContext(AriaAnnouncerContext);
|
|
1675
|
+
useEffect(() => {
|
|
1676
|
+
if (ref.current && announceOnMount) {
|
|
1677
|
+
ariaAnnouncer(ref.current);
|
|
1678
|
+
}
|
|
1679
|
+
}, [announceOnMount, ariaAnnouncer]);
|
|
1680
|
+
const onBookmarkClickInternal = useCallback(() => {
|
|
1681
|
+
if (onBookmarkClick) {
|
|
1682
|
+
onBookmarkClick(searchResult);
|
|
1683
|
+
}
|
|
1684
|
+
}, [onBookmarkClick, searchResult]);
|
|
1685
|
+
const { title, url, titleIsURL, urlIsValid, answer } = getSearchResultMetaData(searchResult);
|
|
1686
|
+
return ((title || hasSearchResultBody(searchResult)) && (React__default.createElement(SearchResultComponentBody, { answer: answer, title: title, titleIsURL: titleIsURL, body: React__default.createElement(SearchResultBodyExport, { searchResult: searchResult }), shouldTruncateBody: shouldTruncateBody(searchResult, charactersForEllipsis), urlIsValid: urlIsValid, url: url, innerRef: ref, onBookmarkClick: onBookmarkClickInternal, showBookmarkButton: showBookmarkButton, isBookmark: isBookmark, disablePDFViewer: disablePDFViewer })));
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
/**
|
|
1690
|
+
* The default number of results to show. Any additional results are dropped and not displayed.
|
|
1691
|
+
*/
|
|
1692
|
+
const DEFAULT_RESULTS_TO_SHOW = 3;
|
|
1693
|
+
class SearchMessage extends PureComponent {
|
|
1694
|
+
constructor() {
|
|
1695
|
+
super(...arguments);
|
|
1696
|
+
this.state = { showMoreResults: false };
|
|
1697
|
+
/**
|
|
1698
|
+
* A (memoized) sorted list of the results in this component. This is sorted by confidence with the highest score
|
|
1699
|
+
* being first in the array. This is likely unneeded as the search results appear to come back already sorted by
|
|
1700
|
+
* confidence. An issue has been created to re-visit this code and remove if unneeded when not under time crunch.
|
|
1701
|
+
*/
|
|
1702
|
+
this.sortedResults = memoizeOne((results) => filterAndSortResults(results));
|
|
1703
|
+
this.toggleShowMore = () => {
|
|
1704
|
+
this.setState(state => ({ showMoreResults: !state.showMoreResults }));
|
|
1705
|
+
};
|
|
1706
|
+
}
|
|
1707
|
+
/**
|
|
1708
|
+
* Creates components for each of the responses that we want to render.
|
|
1709
|
+
*/
|
|
1710
|
+
getSearchResultComponents(results, countToShow, areAdditionResults) {
|
|
1711
|
+
const { charactersForEllipsis, onBookmarkClick, showBookmarkButton, isBookmark } = this.props;
|
|
1712
|
+
const responseComponents = [];
|
|
1713
|
+
for (let index = 0; index < countToShow; index++) {
|
|
1714
|
+
const item = results[index];
|
|
1715
|
+
const searchResultComponent = (React__default.createElement(SearchResultComponent, { searchResult: item, announceOnMount: areAdditionResults, charactersForEllipsis: charactersForEllipsis, onBookmarkClick: onBookmarkClick, showBookmarkButton: showBookmarkButton, isBookmark: isBookmark }));
|
|
1716
|
+
responseComponents.push(React__default.createElement("li", { key: index },
|
|
1717
|
+
React__default.createElement("div", null,
|
|
1718
|
+
React__default.createElement("div", { className: "WACSearchMessage__ResultDivider" }),
|
|
1719
|
+
searchResultComponent)));
|
|
1720
|
+
}
|
|
1721
|
+
return responseComponents;
|
|
1722
|
+
}
|
|
1723
|
+
/**
|
|
1724
|
+
* Creates components for each of the responses that we want to render.
|
|
1725
|
+
*/
|
|
1726
|
+
renderSearchResults() {
|
|
1727
|
+
const { primary_results = [], results = [] } = this.props.searchResultItem;
|
|
1728
|
+
let sortedResults;
|
|
1729
|
+
// Figure out how many results to show.
|
|
1730
|
+
let countToShow;
|
|
1731
|
+
// The API proposal https://github.ibm.com/Watson/developer-cloud--api-proposals/pull/293 introduced a breaking
|
|
1732
|
+
// change to switch to display primary and additional search results instead of results. We need to also support
|
|
1733
|
+
// the old behavior so check to see if we've got any of the new results.
|
|
1734
|
+
if (primary_results.length > 0) {
|
|
1735
|
+
sortedResults = this.sortedResults(primary_results);
|
|
1736
|
+
countToShow = sortedResults.length;
|
|
1737
|
+
}
|
|
1738
|
+
else {
|
|
1739
|
+
// Handle the legacy behavior.
|
|
1740
|
+
sortedResults = this.sortedResults(results);
|
|
1741
|
+
countToShow = Math.min(sortedResults.length, DEFAULT_RESULTS_TO_SHOW);
|
|
1742
|
+
}
|
|
1743
|
+
return this.getSearchResultComponents(sortedResults, countToShow, false);
|
|
1744
|
+
}
|
|
1745
|
+
/**
|
|
1746
|
+
* Creates components for each of the responses for additional_results that we want to render.
|
|
1747
|
+
*/
|
|
1748
|
+
renderAdditionalResults() {
|
|
1749
|
+
const sortedAdditionalResults = this.sortedResults(this.props.searchResultItem.additional_results || []);
|
|
1750
|
+
return this.getSearchResultComponents(sortedAdditionalResults, sortedAdditionalResults.length, true);
|
|
1751
|
+
}
|
|
1752
|
+
render() {
|
|
1753
|
+
const { searchResultItem, languagePack, hideHeader } = this.props;
|
|
1754
|
+
const { showMoreResults } = this.state;
|
|
1755
|
+
const { header, title } = searchResultItem;
|
|
1756
|
+
const primaryResults = this.renderSearchResults();
|
|
1757
|
+
return (React__default.createElement("div", { className: "WACSearchMessage" },
|
|
1758
|
+
!hideHeader && (React__default.createElement("div", { className: "WAC__searchMessage--metablock" }, header || title || languagePack.messages_searchResults)),
|
|
1759
|
+
primaryResults.length > 0 && (React__default.createElement(Tile, { className: "WACSearchMessage__ResultsTile" },
|
|
1760
|
+
React__default.createElement("ul", { className: "WACSearchMessage__Results" },
|
|
1761
|
+
primaryResults,
|
|
1762
|
+
showMoreResults && this.renderAdditionalResults()))),
|
|
1763
|
+
!showMoreResults && Boolean(searchResultItem.additional_results?.length) && (React__default.createElement("button", { onClick: this.toggleShowMore, className: "WACSearchMessage__ToggleShowMore WAC__button--link", type: "button" },
|
|
1764
|
+
React__default.createElement("span", null, languagePack.showMoreResults),
|
|
1765
|
+
React__default.createElement(ChevronDown, null)))));
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
/**
|
|
1769
|
+
* Filters and sorts a copy of the given search results. This is sorted by confidence from highest to lowest.
|
|
1770
|
+
* first in the array. The original array is not modified.
|
|
1771
|
+
*/
|
|
1772
|
+
function filterAndSortResults(results) {
|
|
1773
|
+
if (!results) {
|
|
1774
|
+
return [];
|
|
1775
|
+
}
|
|
1776
|
+
const filteredResults = results.filter(isNotEmptyResult);
|
|
1777
|
+
return filteredResults.sort(compareResults);
|
|
1778
|
+
}
|
|
1779
|
+
/**
|
|
1780
|
+
* Compares two search results by confidence with the highest confidence being first.
|
|
1781
|
+
*/
|
|
1782
|
+
function compareResults(result1, result2) {
|
|
1783
|
+
const score1 = result1 && result1.result_metadata ? result1.result_metadata.confidence : 0;
|
|
1784
|
+
const score2 = result2 && result2.result_metadata ? result2.result_metadata.confidence : 0;
|
|
1785
|
+
return -compareNumbers(score1, score2);
|
|
1786
|
+
}
|
|
1787
|
+
/**
|
|
1788
|
+
* Determines if the given search result has any content.
|
|
1789
|
+
*/
|
|
1790
|
+
function isNotEmptyResult(result) {
|
|
1791
|
+
const isEmpty = !result ||
|
|
1792
|
+
(isEmptyString(result.body) &&
|
|
1793
|
+
isEmptyString(result.url) &&
|
|
1794
|
+
isEmptyString(result.title) &&
|
|
1795
|
+
isEmptyString(result.answers?.[0]?.text) &&
|
|
1796
|
+
isEmptyString(result.highlight?.title?.[0]) &&
|
|
1797
|
+
isEmptyString(result.highlight?.body?.[0]));
|
|
1798
|
+
return !isEmpty;
|
|
1799
|
+
}
|
|
1800
|
+
const SearchMessageExport = React__default.memo(SearchMessage);
|
|
1801
|
+
|
|
1802
|
+
/**
|
|
1803
|
+
* This components renders a tile that looks like a search tile in a skeleton state.
|
|
1804
|
+
*/
|
|
1805
|
+
function SearchTileSkeleton() {
|
|
1806
|
+
return (React__default.createElement(Tile, { className: "WACSearchTileSkeleton" },
|
|
1807
|
+
React__default.createElement("div", { className: "WACSearchTileSkeleton__Title" },
|
|
1808
|
+
React__default.createElement(SkeletonText, { heading: true })),
|
|
1809
|
+
React__default.createElement("div", { className: "WACSearchTileSkeleton__Text" },
|
|
1810
|
+
React__default.createElement(SkeletonText, { paragraph: true, lineCount: 2 })),
|
|
1811
|
+
React__default.createElement("div", { className: "WACSearchTileSkeleton__ResultDivider" })));
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1814
|
+
const { CLOSED: CLOSED$1, OPEN_FULL: OPEN_FULL$1, OPEN_STRIKE: OPEN_STRIKE$1 } = AlternateSuggestionsPanelState;
|
|
1815
|
+
const { CONTACT_OPTIONS, ALTERNATE_RESPONSES, STARTERS } = AlternateSuggestionsSection;
|
|
1816
|
+
function AlternateSuggestionsOptionsPanel(props) {
|
|
1817
|
+
const { requestInputFocus, alternateSuggestionsState, showSearchSkeleton, languagePack } = props;
|
|
1818
|
+
const closeButtonRef = useRef();
|
|
1819
|
+
const serviceManager = useServiceManager();
|
|
1820
|
+
const { panelState, suggestionListItems, sourceResponse, starters, contactItem, searchResults, allowCollapse, showSearchSection, } = alternateSuggestionsState;
|
|
1821
|
+
const { suggestions_sectionTitleAlternateResponses, suggestions_sectionTitleSearch, suggestions_sectionTitleContact, suggestions_sectionTitleStarters, suggestions_ariaPanelButtonToggle, suggestions_ariaButtonToClose, suggestions_title, } = languagePack;
|
|
1822
|
+
const onCloseClick = useCallback(() => {
|
|
1823
|
+
serviceManager.store.dispatch(actions.setAlternateSuggestionsPanelState(CLOSED$1, 'User'));
|
|
1824
|
+
}, [serviceManager]);
|
|
1825
|
+
const onToggleClick = useCallback(() => {
|
|
1826
|
+
const newState = panelState === OPEN_FULL$1 ? OPEN_STRIKE$1 : OPEN_FULL$1;
|
|
1827
|
+
serviceManager.store.dispatch(actions.setAlternateSuggestionsPanelState(newState, 'User'));
|
|
1828
|
+
}, [serviceManager, panelState]);
|
|
1829
|
+
const onBookmarkClick = useCallback((searchResult) => {
|
|
1830
|
+
injectSearchResultMessage(searchResult, serviceManager);
|
|
1831
|
+
}, [serviceManager]);
|
|
1832
|
+
const onButtonClick = (selectedItem, index, section) => {
|
|
1833
|
+
serviceManager.actions.sendSuggestionChoice(selectedItem, sourceResponse, index, section);
|
|
1834
|
+
// Move focus back to the input field.
|
|
1835
|
+
setTimeout(requestInputFocus);
|
|
1836
|
+
};
|
|
1837
|
+
useLayoutEffect(() => {
|
|
1838
|
+
doFocus(closeButtonRef.current);
|
|
1839
|
+
}, []);
|
|
1840
|
+
function renderSearchResults() {
|
|
1841
|
+
if (!showSearchSection) {
|
|
1842
|
+
return null;
|
|
1843
|
+
}
|
|
1844
|
+
let content;
|
|
1845
|
+
if (showSearchSkeleton) {
|
|
1846
|
+
content = (React__default.createElement("div", { className: "WACAlternateSuggestionsOptionsPanel__SearchSkeleton" },
|
|
1847
|
+
React__default.createElement(SearchTileSkeleton, null),
|
|
1848
|
+
React__default.createElement(SearchTileSkeleton, null),
|
|
1849
|
+
React__default.createElement(SearchTileSkeleton, null)));
|
|
1850
|
+
}
|
|
1851
|
+
else if (!searchResults) {
|
|
1852
|
+
content = (React__default.createElement("div", { className: "WACAlternateSuggestionsOptionsPanel__NoSearchResults" },
|
|
1853
|
+
React__default.createElement(SatelliteRadar, { size: 24 }),
|
|
1854
|
+
React__default.createElement("div", null, languagePack.suggestions_noSearchResults)));
|
|
1855
|
+
}
|
|
1856
|
+
else {
|
|
1857
|
+
content = (React__default.createElement(SearchMessageExport, { serviceManager: serviceManager, searchResultItem: searchResults, languagePack: languagePack, charactersForEllipsis: 100, onBookmarkClick: onBookmarkClick, showBookmarkButton: true, hideHeader: true }));
|
|
1858
|
+
}
|
|
1859
|
+
return (React__default.createElement(PanelSection, { label: suggestions_sectionTitleSearch, testId: "Search" }, content));
|
|
1860
|
+
}
|
|
1861
|
+
return (React__default.createElement("div", { className: cx('WACAlternateSuggestionsOptionsPanel', {
|
|
1862
|
+
'WACAlternateSuggestionsOptionsPanel--Full': panelState === OPEN_FULL$1,
|
|
1863
|
+
'WACAlternateSuggestionsOptionsPanel--Strike': panelState === OPEN_STRIKE$1,
|
|
1864
|
+
}) },
|
|
1865
|
+
React__default.createElement("div", { className: "WACAlternateSuggestionsOptionsPanel__Header" },
|
|
1866
|
+
React__default.createElement("div", { className: "WACAlternateSuggestionsOptionsPanel__Icon" },
|
|
1867
|
+
React__default.createElement(QuestionMarkIcon, null)),
|
|
1868
|
+
React__default.createElement("div", { className: "WACAlternateSuggestionsOptionsPanel__Title" }, suggestions_title),
|
|
1869
|
+
allowCollapse && (React__default.createElement(Button, { type: "button", kind: ButtonKindEnum.GHOST, className: "WACAlternateSuggestionsOptionsPanel__HeaderButton", onClick: onToggleClick, "aria-label": suggestions_ariaPanelButtonToggle, "data-test-id": "Toggle" },
|
|
1870
|
+
panelState === OPEN_STRIKE$1 && React__default.createElement(UpToTop, { size: 20 }),
|
|
1871
|
+
panelState === OPEN_FULL$1 && React__default.createElement(DownToBottom, { size: 20 }))),
|
|
1872
|
+
React__default.createElement(Button, { type: "button", kind: ButtonKindEnum.GHOST, className: "WACAlternateSuggestionsOptionsPanel__HeaderButton", onClick: onCloseClick, "aria-label": suggestions_ariaButtonToClose, ref: closeButtonRef, "data-test-id": "Close" },
|
|
1873
|
+
React__default.createElement(Close, { size: 20 }))),
|
|
1874
|
+
React__default.createElement("div", { className: "WACAlternateSuggestionsOptionsPanel__ScrollPanel" },
|
|
1875
|
+
Boolean(suggestionListItems?.length) && (React__default.createElement(ButtonsPanel, { items: suggestionListItems, label: suggestions_sectionTitleAlternateResponses, onButtonClick: onButtonClick, section: ALTERNATE_RESPONSES, testId: "AlternateResponses" })),
|
|
1876
|
+
panelState === OPEN_FULL$1 && renderSearchResults(),
|
|
1877
|
+
contactItem && (React__default.createElement(ContactOptionButton, { item: contactItem, label: suggestions_sectionTitleContact, onButtonClick: onButtonClick })),
|
|
1878
|
+
Boolean(starters?.length) && (React__default.createElement(ButtonsPanel, { items: starters, label: suggestions_sectionTitleStarters, onButtonClick: onButtonClick, section: STARTERS, testId: "Starters" })))));
|
|
1879
|
+
}
|
|
1880
|
+
/**
|
|
1881
|
+
* This panel renders a list of buttons.
|
|
1882
|
+
*/
|
|
1883
|
+
function ButtonsPanel(props) {
|
|
1884
|
+
const { items, label, onButtonClick, section, testId } = props;
|
|
1885
|
+
return (React__default.createElement(PanelSection, { label: label, testId: testId },
|
|
1886
|
+
React__default.createElement("ul", { className: "WAC__button-holder" }, items &&
|
|
1887
|
+
items.map((item, index) => {
|
|
1888
|
+
let clickSection = section;
|
|
1889
|
+
let label;
|
|
1890
|
+
if (isSuggestionListItem(item)) {
|
|
1891
|
+
label = item.option.label;
|
|
1892
|
+
// If the user clicks on the contact option (which can appear either in its own section or added to the
|
|
1893
|
+
// end of the alternate responses section) we need to use a different index and section.
|
|
1894
|
+
if (item.isContactSuggestion) {
|
|
1895
|
+
clickSection = AlternateSuggestionsSection.CONTACT_OPTIONS;
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
else {
|
|
1899
|
+
label = item.label;
|
|
1900
|
+
}
|
|
1901
|
+
return (
|
|
1902
|
+
// eslint-disable-next-line react/no-array-index-key
|
|
1903
|
+
React__default.createElement("li", { key: index },
|
|
1904
|
+
React__default.createElement(unstable__ChatButton, { type: "button", size: ButtonSizeEnum.SMALL, kind: ButtonKindEnum.TERTIARY, isQuickAction: true, disabled: item.isSelected, isSelected: item.isSelected, onClick: () => onButtonClick(item, index, clickSection) }, label)));
|
|
1905
|
+
}))));
|
|
1906
|
+
}
|
|
1907
|
+
/**
|
|
1908
|
+
* The wrapper for a section of the panel.
|
|
1909
|
+
*/
|
|
1910
|
+
function PanelSection(props) {
|
|
1911
|
+
return (React__default.createElement("div", { className: "WACAlternateSuggestionsOptionsPanel__Section", "data-test-id": props.testId },
|
|
1912
|
+
React__default.createElement("div", { className: "WACAlternateSuggestionsOptionsPanel__SectionTitle" },
|
|
1913
|
+
React__default.createElement("div", { className: "WACAlternateSuggestionsOptionsPanel__SectionTitleLabel" }, props.label),
|
|
1914
|
+
React__default.createElement("div", { className: "WACAlternateSuggestionsOptionsPanel__SectionTitleDivider" })),
|
|
1915
|
+
props.children));
|
|
1916
|
+
}
|
|
1917
|
+
/**
|
|
1918
|
+
* The contact option button.
|
|
1919
|
+
*/
|
|
1920
|
+
function ContactOptionButton(props) {
|
|
1921
|
+
const { item, label, onButtonClick } = props;
|
|
1922
|
+
return (React__default.createElement(PanelSection, { label: label, testId: "Contact" },
|
|
1923
|
+
React__default.createElement("div", { className: "WACAlternateSuggestionsOptionsPanel__ContactOption" },
|
|
1924
|
+
React__default.createElement(Button, { type: "button", kind: ButtonKindEnum.GHOST, disabled: item.isSelected, onClick: () => onButtonClick(item, 0, CONTACT_OPTIONS) },
|
|
1925
|
+
React__default.createElement("div", { className: "WACAlternateSuggestionsOptionsPanel__ContactOptionInner" },
|
|
1926
|
+
React__default.createElement("div", { className: "WACAlternateSuggestionsOptionsPanel__ContactOptionInnerColor" }),
|
|
1927
|
+
React__default.createElement("div", { className: "WACAlternateSuggestionsOptionsPanel__ContactOptionInnerText" }, item.option.label),
|
|
1928
|
+
React__default.createElement("div", { className: "WACAlternateSuggestionsOptionsPanel__ContactOptionInnerIcon" },
|
|
1929
|
+
React__default.createElement(HelpDesk, { size: 20 })))))));
|
|
1930
|
+
}
|
|
1931
|
+
/**
|
|
1932
|
+
* This will inject a message for the given search result into the main message list.
|
|
1933
|
+
*/
|
|
1934
|
+
function injectSearchResultMessage(searchResult, serviceManager) {
|
|
1935
|
+
// Clicking a search result will insert this search result into the main message list (and also send and update
|
|
1936
|
+
// event to the back-end to make sure it gets stored in history).
|
|
1937
|
+
const { suggestions_searchBookmarkHeader, suggestions_searchBookmarkFooter } = serviceManager.store.getState().languagePack;
|
|
1938
|
+
const newMessage = {
|
|
1939
|
+
id: null,
|
|
1940
|
+
thread_id: THREAD_ID_MAIN,
|
|
1941
|
+
output: {
|
|
1942
|
+
generic: [
|
|
1943
|
+
assertType({
|
|
1944
|
+
response_type: MessageResponseTypes.SEARCH,
|
|
1945
|
+
primary_results: [searchResult],
|
|
1946
|
+
header: suggestions_searchBookmarkHeader,
|
|
1947
|
+
}),
|
|
1948
|
+
createTextItem(suggestions_searchBookmarkFooter),
|
|
1949
|
+
],
|
|
1950
|
+
},
|
|
1951
|
+
history: {
|
|
1952
|
+
is_bookmark: true,
|
|
1953
|
+
},
|
|
1954
|
+
};
|
|
1955
|
+
serviceManager.actions.insertLocalMessageResponse(newMessage, true);
|
|
1956
|
+
// Close the suggestions menu.
|
|
1957
|
+
serviceManager.store.dispatch(actions.setAlternateSuggestionsPanelState(AlternateSuggestionsPanelState.CLOSED, 'Search'));
|
|
1958
|
+
}
|
|
1959
|
+
|
|
1960
|
+
const { CLOSED, OPEN_FULL, OPEN_STRIKE } = AlternateSuggestionsPanelState;
|
|
1961
|
+
let shouldDisableFocusTrap = false;
|
|
1962
|
+
function AlternateSuggestionsContainer(props) {
|
|
1963
|
+
const { requestInputFocus } = props;
|
|
1964
|
+
const alternateSuggestionsState = useSelector((state) => state.alternateSuggestionsState);
|
|
1965
|
+
const languagePack = useSelector((state) => state.languagePack);
|
|
1966
|
+
const isChatOpen = useSelector((state) => state.persistedToBrowserStorage.launcherState.viewState.mainWindow);
|
|
1967
|
+
const isHidden = useContext(HideComponentContext);
|
|
1968
|
+
const { panelState, searchResultsLoading } = alternateSuggestionsState;
|
|
1969
|
+
const serviceManager = useServiceManager();
|
|
1970
|
+
const containerRef = useRef();
|
|
1971
|
+
const previousHeightRef = useRef(0);
|
|
1972
|
+
const prevStateRef = useRef(alternateSuggestionsState);
|
|
1973
|
+
// This is called when the focus trap becomes deactivated (like if the user presses Escape). We'll close the panel
|
|
1974
|
+
// when that happens.
|
|
1975
|
+
const onDeactivate = useCallback(() => {
|
|
1976
|
+
serviceManager.store.dispatch(actions.setAlternateSuggestionsPanelState(CLOSED, 'User'));
|
|
1977
|
+
}, [serviceManager]);
|
|
1978
|
+
const newClassName = getPanelClassName(panelState);
|
|
1979
|
+
useLayoutEffectDidUpdate(() => {
|
|
1980
|
+
// At this point the components have been rendered to the DOM in the desired end state. We need to examine the
|
|
1981
|
+
// previous state to determine what changed and decide if we need to animate a transition from that previous
|
|
1982
|
+
// state to the current state. If so, we'll use the current state to tells us the end goal and then we'll reset
|
|
1983
|
+
// the elements manually to the previous state and execute the animation between them.
|
|
1984
|
+
const { panelState: prevPanelState } = prevStateRef.current;
|
|
1985
|
+
if (panelState !== prevPanelState) {
|
|
1986
|
+
if (panelState !== CLOSED) {
|
|
1987
|
+
// The panel is currently in its end state so we'll grab the current height and then animate from the
|
|
1988
|
+
// previous height to the current height.
|
|
1989
|
+
const startHeight = `${previousHeightRef.current}px`;
|
|
1990
|
+
const endHeight = `${containerRef.current.offsetHeight}px`;
|
|
1991
|
+
if (containerRef.current?.style?.setProperty) {
|
|
1992
|
+
containerRef.current.style.setProperty('--cds-chat-Animate-Start-Height', startHeight);
|
|
1993
|
+
containerRef.current.style.setProperty('--cds-chat-Animate-End-Height', endHeight);
|
|
1994
|
+
animateWithClass(containerRef.current, 'WACAlternateSuggestionsContainer--animate', 333, () => {
|
|
1995
|
+
if (containerRef.current) {
|
|
1996
|
+
containerRef.current.style.height = null;
|
|
1997
|
+
previousHeightRef.current = containerRef.current.offsetHeight;
|
|
1998
|
+
}
|
|
1999
|
+
});
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
else {
|
|
2003
|
+
previousHeightRef.current = 0;
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
2006
|
+
prevStateRef.current = alternateSuggestionsState;
|
|
2007
|
+
});
|
|
2008
|
+
return (panelState !== CLOSED && (React__default.createElement("div", { className: `WACAlternateSuggestionsContainer ${newClassName}`, ref: containerRef },
|
|
2009
|
+
React__default.createElement(FocusTrap, { active: isChatOpen && !isHidden && !shouldDisableFocusTrap, focusTrapOptions: { clickOutsideDeactivates: true, onDeactivate, returnFocusOnDeactivate: !IS_MOBILE } },
|
|
2010
|
+
React__default.createElement("div", { className: "WACAlternateSuggestionsContainer__FocusTrap" },
|
|
2011
|
+
React__default.createElement(AlternateSuggestionsOptionsPanel, { alternateSuggestionsState: alternateSuggestionsState, requestInputFocus: requestInputFocus, panelState: panelState, showSearchSkeleton: searchResultsLoading && panelState === OPEN_FULL, languagePack: languagePack }))))));
|
|
2012
|
+
}
|
|
2013
|
+
/**
|
|
2014
|
+
* Returns the classname for the container that represents the given panel state.
|
|
2015
|
+
*/
|
|
2016
|
+
function getPanelClassName(panelState) {
|
|
2017
|
+
switch (panelState) {
|
|
2018
|
+
case OPEN_STRIKE:
|
|
2019
|
+
return 'WACAlternateSuggestionsContainer--Strike';
|
|
2020
|
+
case OPEN_FULL:
|
|
2021
|
+
return 'WACAlternateSuggestionsContainer--Full';
|
|
2022
|
+
default:
|
|
2023
|
+
return 'WACAlternateSuggestionsContainer--Closed';
|
|
2024
|
+
}
|
|
2025
|
+
}
|
|
2026
|
+
const AlternateSuggestionsContainerExport = React__default.memo(AlternateSuggestionsContainer);
|
|
2027
|
+
|
|
2028
|
+
/**
|
|
2029
|
+
* Displays a modal asking if the user wants to end a chat with an agent. This also covers the case where the user
|
|
2030
|
+
* cancels a request for an agent before an agent has joined.
|
|
2031
|
+
*/
|
|
2032
|
+
function RequestScreenShareModal() {
|
|
2033
|
+
const serviceManager = useServiceManager();
|
|
2034
|
+
const languagePack = useLanguagePack();
|
|
2035
|
+
const onConfirm = () => {
|
|
2036
|
+
serviceManager.humanAgentService?.screenShareUpdateRequestState(ScreenShareState.ACCEPTED);
|
|
2037
|
+
};
|
|
2038
|
+
const onCancel = () => {
|
|
2039
|
+
serviceManager.humanAgentService?.screenShareUpdateRequestState(ScreenShareState.DECLINED);
|
|
2040
|
+
};
|
|
2041
|
+
const title = languagePack.agent_sharingRequestTitle;
|
|
2042
|
+
const message = languagePack.agent_sharingRequestMessage;
|
|
2043
|
+
const cancelButtonLabel = languagePack.agent_sharingDeclineButton;
|
|
2044
|
+
const confirmButtonLabel = languagePack.agent_sharingAcceptButton;
|
|
2045
|
+
return (React__default.createElement(ConfirmModal, { title: title, message: message, onConfirm: onConfirm, onCancel: onCancel, cancelButtonLabel: cancelButtonLabel, confirmButtonLabel: confirmButtonLabel, modalAnnounceMessage: message, serviceManager: serviceManager }));
|
|
2046
|
+
}
|
|
2047
|
+
|
|
2048
|
+
class Chat extends Component {
|
|
2049
|
+
constructor() {
|
|
2050
|
+
super(...arguments);
|
|
2051
|
+
/**
|
|
2052
|
+
* Default state.
|
|
2053
|
+
*/
|
|
2054
|
+
this.state = {
|
|
2055
|
+
showEndChatConfirmation: false,
|
|
2056
|
+
hasCaughtError: false,
|
|
2057
|
+
};
|
|
2058
|
+
/**
|
|
2059
|
+
* A React ref to the Input component.
|
|
2060
|
+
*/
|
|
2061
|
+
this.inputRef = React__default.createRef();
|
|
2062
|
+
/**
|
|
2063
|
+
* A React ref to the Header component.
|
|
2064
|
+
*/
|
|
2065
|
+
this.headerRef = React__default.createRef();
|
|
2066
|
+
/**
|
|
2067
|
+
* A React ref to the Messages component.
|
|
2068
|
+
*/
|
|
2069
|
+
this.messagesRef = React__default.createRef();
|
|
2070
|
+
/**
|
|
2071
|
+
* This is the memoized messages used in this component. This is the step that pulls the messages from
|
|
2072
|
+
* the map and puts them in the right order in an array.
|
|
2073
|
+
*/
|
|
2074
|
+
this.messagesToArray = createUnmappingMemoizer();
|
|
2075
|
+
/**
|
|
2076
|
+
* The callback that can be called when the end chat confirmation panel should be hidden.
|
|
2077
|
+
*/
|
|
2078
|
+
this.hideConfirmEndChat = () => {
|
|
2079
|
+
// Hide the modal and then move focus back to the input field.
|
|
2080
|
+
this.setState({ showEndChatConfirmation: false });
|
|
2081
|
+
setTimeout(() => {
|
|
2082
|
+
// The input field may still be disabled until the state change is re-rendered, so defer the focus move.
|
|
2083
|
+
this.requestInputFocus();
|
|
2084
|
+
});
|
|
2085
|
+
};
|
|
2086
|
+
/**
|
|
2087
|
+
* The callback that can be called when the end chat confirmation panel should be shown.
|
|
2088
|
+
*/
|
|
2089
|
+
this.showConfirmEndChat = () => {
|
|
2090
|
+
this.setState({ showEndChatConfirmation: true });
|
|
2091
|
+
};
|
|
2092
|
+
/**
|
|
2093
|
+
* The callback that can be called when the end agent chat confirmation panel should be shown.
|
|
2094
|
+
*/
|
|
2095
|
+
this.confirmAgentEndChat = () => {
|
|
2096
|
+
this.hideConfirmEndChat();
|
|
2097
|
+
this.props.serviceManager.humanAgentService.endChat(true);
|
|
2098
|
+
};
|
|
2099
|
+
/**
|
|
2100
|
+
* If we have nothing to focus on correctly in the chat window, we focus on the header. If the header has no buttons
|
|
2101
|
+
* available, we can fall back to focusing on the messages scroll handle.
|
|
2102
|
+
*/
|
|
2103
|
+
this.requestDefaultFocus = () => {
|
|
2104
|
+
if (!this.headerRef?.current?.requestFocus()) {
|
|
2105
|
+
doFocusRef(this.messagesRef.current?.scrollHandleRef);
|
|
2106
|
+
}
|
|
2107
|
+
};
|
|
2108
|
+
/**
|
|
2109
|
+
* This is a callback function that will request that focus be moved to the main input field where the user types
|
|
2110
|
+
* in their requests to the assistant. If the input is currently disabled or hidden, then we will try to focus on
|
|
2111
|
+
* a focusable option in the latest responses from the assistant. If that doesn't exist then focus will be moved
|
|
2112
|
+
* to the close button instead.
|
|
2113
|
+
*
|
|
2114
|
+
*/
|
|
2115
|
+
this.requestInputFocus = () => {
|
|
2116
|
+
const { agentDisplayState } = this.props;
|
|
2117
|
+
try {
|
|
2118
|
+
// If the agent banner is visible and the input field is disabled, move focus there.
|
|
2119
|
+
if (agentDisplayState.isConnectingOrConnected && agentDisplayState.disableInput) {
|
|
2120
|
+
if (this.messagesRef.current.requestAgentBannerFocus()) {
|
|
2121
|
+
return;
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
2124
|
+
if (this.inputRef.current) {
|
|
2125
|
+
if (this.props.inputState.fieldVisible && !this.shouldDisableInput()) {
|
|
2126
|
+
this.inputRef.current.takeFocus();
|
|
2127
|
+
}
|
|
2128
|
+
else {
|
|
2129
|
+
// This will attempt to move focus to the last output message in the message list. This is intended to cover
|
|
2130
|
+
// the use case where the customer has disabled the input field. When the assistant returns a response and
|
|
2131
|
+
// there is no input field, the expectation is that something in that response will be focusable and we'll
|
|
2132
|
+
// try to move focus to it. If the last message is not a response and the input field is disabled, that
|
|
2133
|
+
// means the assistant is processing a request. In that case, we don't want to move focus to a message but
|
|
2134
|
+
// rather fallback to something else like the close button.
|
|
2135
|
+
const htmlElements = this.messagesRef.current.getLastOutputMessageElements();
|
|
2136
|
+
if (!focusOnFirstFocusableItemInArrayOfElements(htmlElements)) {
|
|
2137
|
+
this.requestDefaultFocus();
|
|
2138
|
+
}
|
|
2139
|
+
}
|
|
2140
|
+
}
|
|
2141
|
+
else {
|
|
2142
|
+
this.requestDefaultFocus();
|
|
2143
|
+
}
|
|
2144
|
+
}
|
|
2145
|
+
catch (error) {
|
|
2146
|
+
consoleError('An error occurred in Chat.requestInputFocus', error);
|
|
2147
|
+
}
|
|
2148
|
+
};
|
|
2149
|
+
/**
|
|
2150
|
+
* Requests an auto scroll operation to happen on the messages panel. See {@link MessagesComponent#doAutoScroll} for
|
|
2151
|
+
* more information.
|
|
2152
|
+
*/
|
|
2153
|
+
this.doAutoScroll = (options) => {
|
|
2154
|
+
this.messagesRef.current?.doAutoScroll(options);
|
|
2155
|
+
};
|
|
2156
|
+
/**
|
|
2157
|
+
* Returns the current scrollBottom value for the message scroll panel.
|
|
2158
|
+
*/
|
|
2159
|
+
this.getMessagesScrollBottom = () => {
|
|
2160
|
+
return this.messagesRef?.current?.getContainerScrollBottom();
|
|
2161
|
+
};
|
|
2162
|
+
/**
|
|
2163
|
+
* The callback that is called when the user selects one or more files to be uploaded.
|
|
2164
|
+
*/
|
|
2165
|
+
this.onFilesSelectedForUpload = (uploads) => {
|
|
2166
|
+
const isInputToAgent = this.props.agentDisplayState.isConnectingOrConnected;
|
|
2167
|
+
if (isInputToAgent) {
|
|
2168
|
+
this.props.serviceManager.humanAgentService.filesSelectedForUpload(uploads);
|
|
2169
|
+
// If the user chose a file and multiple files are not allowed, the file input will become disabled so we need to
|
|
2170
|
+
// move focus back to the text input.
|
|
2171
|
+
if (!this.props.inputState.allowMultipleFileUploads) {
|
|
2172
|
+
this.requestInputFocus();
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
};
|
|
2176
|
+
}
|
|
2177
|
+
async scrollOnHydrationComplete() {
|
|
2178
|
+
// Once the hydration is complete, we want to scroll to the very bottom.
|
|
2179
|
+
if (this.props.config.public.__ibm__.disableInitialScroll) {
|
|
2180
|
+
// If the initial scrolling is disabled, scroll to the top anyway. This overrides the browser's default
|
|
2181
|
+
// behavior where it will remember the last scroll position.
|
|
2182
|
+
this.doAutoScroll({ scrollToTop: 0 });
|
|
2183
|
+
}
|
|
2184
|
+
else {
|
|
2185
|
+
this.doAutoScroll();
|
|
2186
|
+
}
|
|
2187
|
+
}
|
|
2188
|
+
componentDidMount() {
|
|
2189
|
+
if (this.props.isHydrationAnimationComplete) {
|
|
2190
|
+
setTimeout(() => {
|
|
2191
|
+
// We need to make sure React has finished rendering updates before we scroll.
|
|
2192
|
+
this.scrollOnHydrationComplete();
|
|
2193
|
+
});
|
|
2194
|
+
}
|
|
2195
|
+
}
|
|
2196
|
+
componentDidUpdate(prevProps) {
|
|
2197
|
+
const { isHydrationAnimationComplete, agentState } = this.props;
|
|
2198
|
+
// If we don't wait for the animation to complete, the auto scroll can not work correctly.
|
|
2199
|
+
// Thankfully, we have this property already to look at and kick off the autoscroll.
|
|
2200
|
+
if (isHydrationAnimationComplete && !prevProps.isHydrationAnimationComplete) {
|
|
2201
|
+
setTimeout(() => {
|
|
2202
|
+
// We need to make sure React has finished rendering updates before we scroll.
|
|
2203
|
+
this.scrollOnHydrationComplete();
|
|
2204
|
+
});
|
|
2205
|
+
}
|
|
2206
|
+
// This covers the case where an agent joins while the confirmation modal is visible.
|
|
2207
|
+
const connectingChanged = agentState.isConnecting !== prevProps.agentState.isConnecting;
|
|
2208
|
+
if (this.state.showEndChatConfirmation && connectingChanged) {
|
|
2209
|
+
this.hideConfirmEndChat();
|
|
2210
|
+
}
|
|
2211
|
+
}
|
|
2212
|
+
componentDidCatch(error, errorInfo) {
|
|
2213
|
+
this.props.serviceManager.actions.errorOccurred(createDidCatchErrorData('Chat', error, errorInfo));
|
|
2214
|
+
this.setState({ hasCaughtError: true });
|
|
2215
|
+
}
|
|
2216
|
+
/**
|
|
2217
|
+
* Scrolls to the (full) message with the given ID. Since there may be multiple message items in a given
|
|
2218
|
+
* message, this will scroll the first message to the top of the message window.
|
|
2219
|
+
*
|
|
2220
|
+
* @param messageID The (full) message ID to scroll to.
|
|
2221
|
+
* @param animate Whether or not the scroll should be animated. Defaults to true.
|
|
2222
|
+
*/
|
|
2223
|
+
doScrollToMessage(messageID, animate = true) {
|
|
2224
|
+
this.messagesRef.current?.doScrollToMessage(messageID, animate);
|
|
2225
|
+
}
|
|
2226
|
+
getMessageInput() {
|
|
2227
|
+
return this.inputRef.current?.getMessageInput();
|
|
2228
|
+
}
|
|
2229
|
+
/**
|
|
2230
|
+
* Determines if the input field should be disabled based on the current props.
|
|
2231
|
+
*/
|
|
2232
|
+
shouldDisableInput() {
|
|
2233
|
+
return this.props.inputState.isReadonly || this.props.agentDisplayState.disableInput;
|
|
2234
|
+
}
|
|
2235
|
+
/**
|
|
2236
|
+
* Determines if the send button should be disabled based on the current props. The send button is disabled if the
|
|
2237
|
+
* input field should be disabled or if the messages are not yet hydrated (ready to be shown). We let the user
|
|
2238
|
+
* type into the input field before the messages are ready to avoid messing with moving focus around, but we don't
|
|
2239
|
+
* let them send the message.
|
|
2240
|
+
*/
|
|
2241
|
+
shouldDisableSend() {
|
|
2242
|
+
const { isHydrated } = this.props;
|
|
2243
|
+
return this.shouldDisableInput() || !isHydrated;
|
|
2244
|
+
}
|
|
2245
|
+
renderMessagesAndInput() {
|
|
2246
|
+
const { languagePack, toolingType, messageState, intl, allMessageItemsByID, isHydrated, serviceManager, inputState, onUserTyping, agentState, botName, onSendInput, locale, useAITheme, carbonTheme, agentDisplayState, } = this.props;
|
|
2247
|
+
const { fieldVisible, files, allowFileUploads, allowedFileUploadTypes, allowMultipleFileUploads, stopStreamingButtonState, } = inputState;
|
|
2248
|
+
const { fileUploadInProgress } = agentState;
|
|
2249
|
+
const { inputPlaceholderKey } = agentDisplayState;
|
|
2250
|
+
const inputCustomizations = this.props.config.public.messaging.inputs?.mainWindow;
|
|
2251
|
+
// If there are any files currently selected or being uploaded and multiple files are not allowed, then show the
|
|
2252
|
+
// button as disabled.
|
|
2253
|
+
const numFiles = files?.length ?? 0;
|
|
2254
|
+
const anyCurrentFiles = numFiles > 0 || fileUploadInProgress;
|
|
2255
|
+
const showUploadButtonDisabled = anyCurrentFiles && !allowMultipleFileUploads;
|
|
2256
|
+
return (React__default.createElement(React__default.Fragment, null,
|
|
2257
|
+
isHydrated && (React__default.createElement("div", { className: "WACMessagesContainer__NonInputContainer" },
|
|
2258
|
+
React__default.createElement(MessagesComponent$1, { ref: this.messagesRef, messageState: messageState, localMessageItems: this.messagesToArray(messageState.localMessageIDs, allMessageItemsByID), requestInputFocus: this.requestInputFocus, toolingType: toolingType, botName: botName, intl: intl, onEndAgentChat: this.showConfirmEndChat, locale: locale, useAITheme: useAITheme, carbonTheme: carbonTheme }))),
|
|
2259
|
+
React__default.createElement(WriteableElement, { slotName: WriteableElementName.BEFORE_INPUT_ELEMENT, id: `beforeInputElement${serviceManager.namespace.suffix}`, element: serviceManager.writeableElements[WriteableElementName.BEFORE_INPUT_ELEMENT] }),
|
|
2260
|
+
React__default.createElement(InputExport, { ref: this.inputRef, languagePack: languagePack, serviceManager: serviceManager, disableInput: this.shouldDisableInput(), disableSend: this.shouldDisableSend(), isInputVisible: fieldVisible, onSendInput: onSendInput, onUserTyping: onUserTyping, showUploadButton: allowFileUploads, disableUploadButton: showUploadButtonDisabled, allowedFileUploadTypes: allowedFileUploadTypes, allowMultipleFileUploads: allowMultipleFileUploads, pendingUploads: files, onFilesSelectedForUpload: this.onFilesSelectedForUpload, placeholder: languagePack[inputPlaceholderKey], inputCustomizations: inputCustomizations, isStopStreamingButtonVisible: stopStreamingButtonState.isVisible, isStopStreamingButtonDisabled: stopStreamingButtonState.isDisabled }),
|
|
2261
|
+
this.state.showEndChatConfirmation && (React__default.createElement(EndAgentChatModal, { onConfirm: this.confirmAgentEndChat, onCancel: this.hideConfirmEndChat })),
|
|
2262
|
+
this.props.agentState.showScreenShareRequest && React__default.createElement(RequestScreenShareModal, null)));
|
|
2263
|
+
}
|
|
2264
|
+
render() {
|
|
2265
|
+
const { serviceManager, languagePack, onClose, onCloseAndRestart, onRestart, config, onToggleHomeScreen, botName, headerDisplayName, headerAvatarConfig, } = this.props;
|
|
2266
|
+
const { hasCaughtError } = this.state;
|
|
2267
|
+
const className = `WAC ${config.public.agentAppConfig.is_on ? 'WACAgentApp' : ''}`;
|
|
2268
|
+
return (React__default.createElement("div", { className: className },
|
|
2269
|
+
React__default.createElement(BotHeaderExport, { ref: this.headerRef, onClose: onClose, onCloseAndRestart: onCloseAndRestart, onRestart: onRestart, headerDisplayName: headerDisplayName, headerAvatarConfig: headerAvatarConfig, onToggleHomeScreen: onToggleHomeScreen, enableChatHeaderConfig: true, includeWriteableElement: true }),
|
|
2270
|
+
React__default.createElement(NonHeaderBackground, null),
|
|
2271
|
+
React__default.createElement("div", { className: "WACPanelContent WAC__ChatNonHeaderContainer" },
|
|
2272
|
+
hasCaughtError && (React__default.createElement("div", { className: "WAC__MessagesErrorHandler" },
|
|
2273
|
+
React__default.createElement(CatastrophicErrorExport, { serviceManager: serviceManager, languagePack: languagePack, onRestart: onRestart, showHeader: false, botName: botName, headerDisplayName: headerDisplayName, catastrophicErrorType: CatastrophicErrorType.FAILED }))),
|
|
2274
|
+
!hasCaughtError && React__default.createElement("div", { className: "WAC__messagesAndInputContainer" }, this.renderMessagesAndInput()),
|
|
2275
|
+
React__default.createElement(AlternateSuggestionsContainerExport, { requestInputFocus: this.requestInputFocus })),
|
|
2276
|
+
React__default.createElement(AlternateSuggestionsBackgroundExport, null)));
|
|
2277
|
+
}
|
|
2278
|
+
}
|
|
2279
|
+
/**
|
|
2280
|
+
* Displays a background that covers the non-header area of the chat.
|
|
2281
|
+
*/
|
|
2282
|
+
function NonHeaderBackground() {
|
|
2283
|
+
const isVisible = useSelector(state => state.showNonHeaderBackgroundCover);
|
|
2284
|
+
return isVisible ? React__default.createElement("div", { className: "WACBackgroundCover WACBackgroundCover__NonHeader" }) : null;
|
|
2285
|
+
}
|
|
2286
|
+
var Chat$1 = injectIntl(Chat, { forwardRef: true });
|
|
2287
|
+
|
|
2288
|
+
export { Chat as ChatClass, Chat$1 as default };
|