@tencentcloud/ai-desk-customer-vue 1.7.0 → 1.7.2
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/CHANGELOG.md +17 -0
- package/components/CustomerServiceChat/feedback-modal/index.vue +3 -0
- package/components/CustomerServiceChat/index-web.vue +32 -4
- package/components/CustomerServiceChat/message-input/index-web.vue +18 -1
- package/components/CustomerServiceChat/message-list/index-web.vue +3 -2
- package/components/CustomerServiceChat/message-list/message-elements/message-desk/message-desk-elements/marked.ts +3 -15
- package/components/CustomerServiceChat/message-list/message-elements/message-desk/message-desk-elements/message-desk.vue +9 -1
- package/components/CustomerServiceChat/message-list/message-elements/message-desk/message-desk-elements/message-multi-form/component-pc/input-pc.vue +1 -1
- package/components/CustomerServiceChat/message-list/message-elements/message-desk/message-desk-elements/message-multi-form/form-mobile.vue +2 -1
- package/components/CustomerServiceChat/message-list/message-elements/message-desk/message-desk-elements/message-queue-confirmation.vue +96 -0
- package/components/CustomerServiceChat/message-list/message-elements/message-desk/message-desk-elements/message-stream.vue +314 -20
- package/components/CustomerServiceChat/message-list/message-elements/message-desk/message-desk-elements/styles/common.scss +87 -84
- package/components/CustomerServiceChat/message-list/message-elements/message-large-stream.vue +6 -1
- package/components/CustomerServiceChat/message-list/message-elements/message-text.vue +2 -4
- package/components/CustomerServiceChat/style/common.scss +65 -62
- package/components/common/common.scss +49 -47
- package/constant.ts +9 -1
- package/locales/en/aidesk.ts +2 -0
- package/locales/es/aidesk.ts +2 -0
- package/locales/fil/aidesk.ts +2 -0
- package/locales/id/aidesk.ts +2 -0
- package/locales/ja/aidesk.ts +2 -0
- package/locales/ko/aidesk.ts +2 -0
- package/locales/ms/aidesk.ts +2 -0
- package/locales/ru/aidesk.ts +2 -0
- package/locales/th/aidesk.ts +2 -0
- package/locales/vi/aidesk.ts +2 -0
- package/locales/zh_cn/aidesk.ts +2 -0
- package/locales/zh_tw/aidesk.ts +2 -0
- package/package.json +1 -1
- package/utils/heartbeat-handler.ts +65 -0
- package/utils/index.ts +3 -3
- package/utils/utils.ts +23 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
|
|
2
|
+
<div :class="['tui-chat', !isPC && 'tui-chat-h5','ai-desk-customer']" :key="currentLanguage">
|
|
3
3
|
<div
|
|
4
4
|
v-if="currentConversationID && (!props.showQueuePage || queueNumber < 0 || queueNumber === undefined || hasLeftQueue)"
|
|
5
|
-
:class="['tui-chat', !isPC && 'tui-chat-h5']"
|
|
5
|
+
:class="['tui-chat', !isPC && 'tui-chat-h5','ai-desk-customer']"
|
|
6
6
|
>
|
|
7
7
|
<ChatHeader
|
|
8
8
|
:class="[
|
|
@@ -120,10 +120,11 @@ import Log from '../../utils/logger';
|
|
|
120
120
|
import MessageToolbarButton from './message-toolbar-button/index.vue';
|
|
121
121
|
import TUILocales from '../../locales';
|
|
122
122
|
import state from '../../utils/state.js';
|
|
123
|
-
import { switchReadStatus,isNonEmptyObject } from '../../utils/utils';
|
|
123
|
+
import { switchReadStatus, isNonEmptyObject } from '../../utils/utils';
|
|
124
124
|
import { getCountryForTimezone } from 'countries-and-timezones';
|
|
125
125
|
import FeedbackModal from './feedback-modal/index.vue';
|
|
126
126
|
import CustomerQueuePage from './customer-queue-page/index.vue';
|
|
127
|
+
import { startHeartbeat, stopHeartbeat, sendHeartbeat } from '../../utils/heartbeat-handler';
|
|
127
128
|
const { ref, onMounted, onUnmounted, computed } = vue;
|
|
128
129
|
|
|
129
130
|
interface IProps {
|
|
@@ -176,7 +177,8 @@ const currentLanguage = ref('');
|
|
|
176
177
|
const languageForShowList = ref<Array<string>>([]);
|
|
177
178
|
const feedbackModalRef = ref();
|
|
178
179
|
const showFeedbackModal = ref(false);
|
|
179
|
-
const queueNumber = ref(-1);
|
|
180
|
+
const queueNumber = ref(-1);
|
|
181
|
+
const queueLeavePageTimeoutEnable = ref(false);
|
|
180
182
|
const hasLeftQueue = ref(false);
|
|
181
183
|
let timezone = '';
|
|
182
184
|
let countryID = '';
|
|
@@ -341,7 +343,9 @@ onMounted(() => {
|
|
|
341
343
|
});
|
|
342
344
|
TUIStore.watch(StoreName.CUSTOM, {
|
|
343
345
|
isQueuing: onIsQueuingUpdate,
|
|
346
|
+
queueLeavePageTimeoutEnable: onQueueLeavePageTimeoutEnable,
|
|
344
347
|
});
|
|
348
|
+
document.addEventListener('visibilitychange', handleVisibilityChange);
|
|
345
349
|
});
|
|
346
350
|
|
|
347
351
|
onUnmounted(() => {
|
|
@@ -356,7 +360,10 @@ onUnmounted(() => {
|
|
|
356
360
|
});
|
|
357
361
|
TUIStore.unwatch(StoreName.CUSTOM, {
|
|
358
362
|
isQueuing: onIsQueuingUpdate,
|
|
363
|
+
queueLeavePageTimeoutEnable: onQueueLeavePageTimeoutEnable,
|
|
359
364
|
});
|
|
365
|
+
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
|
366
|
+
stopHeartbeat();
|
|
360
367
|
});
|
|
361
368
|
|
|
362
369
|
const isInputToolbarShow = computed<boolean>(() => {
|
|
@@ -503,6 +510,27 @@ function onIsQueuingUpdate(data: {conversationID: string, value: number}) {
|
|
|
503
510
|
if (data && data.conversationID === currentConversationID.value) {
|
|
504
511
|
queueNumber.value = data.value;
|
|
505
512
|
hasLeftQueue.value = false;
|
|
513
|
+
if (queueNumber.value >= 0) {
|
|
514
|
+
startHeartbeat();
|
|
515
|
+
} else {
|
|
516
|
+
stopHeartbeat();
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function onQueueLeavePageTimeoutEnable(data: boolean) {
|
|
522
|
+
queueLeavePageTimeoutEnable.value = data;
|
|
523
|
+
startHeartbeat();
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
const handleVisibilityChange = () => {
|
|
527
|
+
if (document.visibilityState === 'hidden') {
|
|
528
|
+
if (queueLeavePageTimeoutEnable.value && queueNumber.value >= 0) {
|
|
529
|
+
sendHeartbeat(currentConversationID.value, "leaveSessionPage");
|
|
530
|
+
}
|
|
531
|
+
stopHeartbeat();
|
|
532
|
+
} else if (document.visibilityState === 'visible') {
|
|
533
|
+
startHeartbeat();
|
|
506
534
|
}
|
|
507
535
|
}
|
|
508
536
|
|
|
@@ -69,6 +69,7 @@ import audioIcon from '../../../assets/audio-blue.svg';
|
|
|
69
69
|
import keyboardIcon from '../../../assets/keyboard-icon.svg';
|
|
70
70
|
import { INPUT_TOOLBAR_TYPE } from '../../../constant';
|
|
71
71
|
import Log from '../../../utils/logger';
|
|
72
|
+
import { Toast, TOAST_TYPE } from '../../common/Toast/index-web';
|
|
72
73
|
const { ref, onMounted, onBeforeUnmount, computed, onUnmounted } = vue;
|
|
73
74
|
|
|
74
75
|
const props = defineProps({
|
|
@@ -169,14 +170,30 @@ const onTyping = (inputContentEmpty: boolean, inputBlur: boolean) => {
|
|
|
169
170
|
const sendMessage = async () => {
|
|
170
171
|
const _editorContentList = editor.value?.getEditorContent();
|
|
171
172
|
if (!_editorContentList || !currentConversation.value) return;
|
|
173
|
+
let hasValidContent = false;
|
|
172
174
|
const editorContentList = _editorContentList.map((editor: any) => {
|
|
173
175
|
if (editor.type === 'text') {
|
|
174
176
|
editor.payload.text = transformTextWithEmojiNamesToKeys(
|
|
175
177
|
editor.payload.text,
|
|
176
178
|
);
|
|
179
|
+
const isEmptyText = !editor.payload.text || editor.payload.text.trim() === '';
|
|
180
|
+
if (!isEmptyText) {
|
|
181
|
+
hasValidContent = true;
|
|
182
|
+
}
|
|
183
|
+
return !isEmptyText ? editor : undefined;
|
|
177
184
|
}
|
|
185
|
+
hasValidContent = true;
|
|
178
186
|
return editor;
|
|
179
|
-
});
|
|
187
|
+
}).filter(Boolean);
|
|
188
|
+
if (!hasValidContent) {
|
|
189
|
+
editor.value?.resetEditor();
|
|
190
|
+
Toast({
|
|
191
|
+
message: TUITranslateService.t('AIDesk.不能发送空白消息'),
|
|
192
|
+
type: TOAST_TYPE.ERROR,
|
|
193
|
+
duration: 3000,
|
|
194
|
+
});
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
180
197
|
await sendMessages(editorContentList, currentConversation.value, quoteMessageCloudCustomData);
|
|
181
198
|
// 注意这里不要 emit 'sendMessage',避免写出死循环
|
|
182
199
|
emit('messageSent');
|
|
@@ -410,8 +410,9 @@ function onNewMessageList(list: IMessageModel[]) {
|
|
|
410
410
|
} else if (data.src === CUSTOM_MESSAGE_SRC.NO_SEAT_ONLINE || data.src === CUSTOM_MESSAGE_SRC.TIMEOUT || data.src === CUSTOM_MESSAGE_SRC.END) {
|
|
411
411
|
updateCustomStore("canEndConversation", { conversationID, value: false });
|
|
412
412
|
updateCustomStore("isQueuing", { conversationID, value: -1 });
|
|
413
|
-
} else if (data.src === CUSTOM_MESSAGE_SRC.
|
|
413
|
+
} else if (data.src === CUSTOM_MESSAGE_SRC.GET_SETTINGS) {
|
|
414
414
|
TUIStore.update(StoreName.CUSTOM, "feedbackTags", data.content.menu);
|
|
415
|
+
TUIStore.update(StoreName.CUSTOM, "queueLeavePageTimeoutEnable", data.content.queueLeavePageTimeoutEnable);
|
|
415
416
|
}
|
|
416
417
|
}
|
|
417
418
|
}
|
|
@@ -427,7 +428,7 @@ async function onMessageListUpdated(list: IMessageModel[]) {
|
|
|
427
428
|
const oldLastMessage = currentLastMessage.value;
|
|
428
429
|
let hasEmojiReaction = false;
|
|
429
430
|
allMessageList.value = list;
|
|
430
|
-
|
|
431
|
+
|
|
431
432
|
messageList.value = list.filter((message, index) => {
|
|
432
433
|
if (message.reactionList?.length && !message.isDeleted) {
|
|
433
434
|
hasEmojiReaction = true;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import {Marked} from 'marked';
|
|
2
|
-
import
|
|
2
|
+
import { getStyledATagFromText } from '../../../../../../utils/utils';
|
|
3
3
|
import DOMPurify from 'dompurify';
|
|
4
4
|
|
|
5
5
|
export const marked = new Marked(
|
|
@@ -32,19 +32,7 @@ export const marked = new Marked(
|
|
|
32
32
|
},
|
|
33
33
|
link(this: any, href: string | null, title: string | null, text: string) {
|
|
34
34
|
if (href) {
|
|
35
|
-
|
|
36
|
-
let ret = href.replace(/&/g, '&').replace(/https?:\/\/[\w\-./?=&:#]+(?=[^\w\-./?=&:#]|$)/g, (matchedUrl) => {
|
|
37
|
-
let isURLInText = false;
|
|
38
|
-
if (matchedUrl !== href) {
|
|
39
|
-
// 如果 text 里包含 url,我们就用 matchedUrl 作为 a 标签的值;否则用 text 作为值
|
|
40
|
-
isURLInText = true;
|
|
41
|
-
}
|
|
42
|
-
return `<a target="_blank" rel="noreferrer noopenner" class="message-marked_link" href="${matchedUrl || ''}" title="${title || ''}">${isURLInText ? matchedUrl : text}</a>`;
|
|
43
|
-
});
|
|
44
|
-
if (ret === href) {
|
|
45
|
-
Log.w(`Unable to extract url, href:${href}`);
|
|
46
|
-
}
|
|
47
|
-
return ret;
|
|
35
|
+
return getStyledATagFromText(href, "message-marked_link", undefined, title || text || '');
|
|
48
36
|
}
|
|
49
37
|
return text;
|
|
50
38
|
},
|
|
@@ -64,7 +52,7 @@ export const parseMarkdown = (text: string) => {
|
|
|
64
52
|
if (typeof ret === 'string') {
|
|
65
53
|
const purified = DOMPurify.sanitize(ret,{
|
|
66
54
|
USE_PROFILES: { html: true,},
|
|
67
|
-
ADD_ATTR: ['onclick'],
|
|
55
|
+
ADD_ATTR: ['onclick', 'target'],
|
|
68
56
|
FORBID_TAGS: ['style']
|
|
69
57
|
});
|
|
70
58
|
return purified;
|
|
@@ -74,6 +74,12 @@
|
|
|
74
74
|
<div v-if="payload.src === CUSTOM_MESSAGE_SRC.TRANSFER_TO_HUMAN || payload.src === CUSTOM_MESSAGE_SRC.TRANSFER_TO_TASK_FLOW">
|
|
75
75
|
<MessageTransferWithDesc :payload="payload" />
|
|
76
76
|
</div>
|
|
77
|
+
<div v-if="payload.src === CUSTOM_MESSAGE_SRC.QUEUE_CONFIRMATION">
|
|
78
|
+
<MessageQueueConfirmation
|
|
79
|
+
:payload="payload"
|
|
80
|
+
@sendMessage="sendCustomMessage"
|
|
81
|
+
/>
|
|
82
|
+
</div>
|
|
77
83
|
</div>
|
|
78
84
|
</div>
|
|
79
85
|
</template>
|
|
@@ -95,6 +101,7 @@ import MessageMultiForm from './message-multi-form/index.vue';
|
|
|
95
101
|
import MessageConcurrencyLimit from "./message-concurrency-limit.vue";
|
|
96
102
|
import MessageOrder from './message-order.vue';
|
|
97
103
|
import MessageTransferWithDesc from './message-transfer-with-desc.vue';
|
|
104
|
+
import MessageQueueConfirmation from './message-queue-confirmation.vue';
|
|
98
105
|
import {
|
|
99
106
|
IMessageModel,
|
|
100
107
|
TUIChatService,
|
|
@@ -116,7 +123,8 @@ export default {
|
|
|
116
123
|
MessageRating,
|
|
117
124
|
MessageConcurrencyLimit,
|
|
118
125
|
MessageOrder,
|
|
119
|
-
MessageTransferWithDesc
|
|
126
|
+
MessageTransferWithDesc,
|
|
127
|
+
MessageQueueConfirmation
|
|
120
128
|
},
|
|
121
129
|
props: {
|
|
122
130
|
message: {
|
|
@@ -197,7 +197,7 @@ export default {
|
|
|
197
197
|
}
|
|
198
198
|
}
|
|
199
199
|
</script>
|
|
200
|
-
<style lang="scss">
|
|
200
|
+
<style lang="scss" scoped>
|
|
201
201
|
@import "../styles/common.scss";
|
|
202
202
|
|
|
203
203
|
.edit-profile-container {
|
|
@@ -382,6 +382,7 @@ export default {
|
|
|
382
382
|
.dialog-show-content {
|
|
383
383
|
overflow-y: auto;
|
|
384
384
|
max-height: 68vh;
|
|
385
|
+
padding: 0 20px;
|
|
385
386
|
}
|
|
386
387
|
|
|
387
388
|
.variable-value-container-mobile {
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="message-queue-confirmation">
|
|
3
|
+
<div class="message-queue-confirmation-tip">
|
|
4
|
+
<span v-for="(part, index) in formattedTips" :key="index">
|
|
5
|
+
<span v-if="part.isNumber" class="message-queue-confirmation-tip-number">{{ part.content }}</span>
|
|
6
|
+
<span v-else>{{ part.content }}</span>
|
|
7
|
+
</span>
|
|
8
|
+
</div>
|
|
9
|
+
<div v-if="!props.payload.status && !isConfirmed" class="message-queue-confirmation-button" @click="sendMessage">
|
|
10
|
+
{{ TUITranslateService.t('AIDesk.确认转人工') }}
|
|
11
|
+
</div>
|
|
12
|
+
</div>
|
|
13
|
+
</template>
|
|
14
|
+
|
|
15
|
+
<script lang="ts">
|
|
16
|
+
import { TUITranslateService } from '../../../../../../@aidesk/uikit-engine';
|
|
17
|
+
import { CUSTOM_MESSAGE_SRC } from '../../../../../../constant';
|
|
18
|
+
import vue from '../../../../../../adapter-vue';
|
|
19
|
+
const { ref, computed } = vue;
|
|
20
|
+
import { customerServicePayloadType } from '../../../../../../interface';
|
|
21
|
+
|
|
22
|
+
interface Props {
|
|
23
|
+
payload: customerServicePayloadType;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export default {
|
|
27
|
+
props: {
|
|
28
|
+
payload: {
|
|
29
|
+
type: Object as () => customerServicePayloadType,
|
|
30
|
+
default: () => ({}),
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
emits:['sendMessage'],
|
|
34
|
+
setup(props: Props, {emit}) {
|
|
35
|
+
const isConfirmed = ref<boolean>(false);
|
|
36
|
+
const { transferConfirmTips, waitingQueueLength, sessionId, skillGroupId, transferConfirmId } = props.payload.content;
|
|
37
|
+
const formattedTips = computed(() => {
|
|
38
|
+
if (!transferConfirmTips) return [];
|
|
39
|
+
|
|
40
|
+
const parts = transferConfirmTips.split('${WaitNo}');
|
|
41
|
+
const result: { content: any; isNumber: boolean; }[] = [];
|
|
42
|
+
|
|
43
|
+
parts.forEach((part, index) => {
|
|
44
|
+
if (part) {
|
|
45
|
+
result.push({ content: part, isNumber: false });
|
|
46
|
+
}
|
|
47
|
+
if (index < parts.length - 1) {
|
|
48
|
+
result.push({ content: waitingQueueLength, isNumber: true });
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
return result;
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const sendMessage = () => {
|
|
56
|
+
let data = {
|
|
57
|
+
data: JSON.stringify({
|
|
58
|
+
src: CUSTOM_MESSAGE_SRC.QUEUE_CONFIRMATION,
|
|
59
|
+
content: {
|
|
60
|
+
sessionId: sessionId,
|
|
61
|
+
skillGroupId: skillGroupId,
|
|
62
|
+
transferConfirmId: transferConfirmId,
|
|
63
|
+
}
|
|
64
|
+
})
|
|
65
|
+
};
|
|
66
|
+
emit('sendMessage', data);
|
|
67
|
+
isConfirmed.value = true;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
props,
|
|
72
|
+
formattedTips,
|
|
73
|
+
isConfirmed,
|
|
74
|
+
sendMessage,
|
|
75
|
+
TUITranslateService
|
|
76
|
+
};
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
</script>
|
|
80
|
+
<style lang="scss" scoped>
|
|
81
|
+
.message-queue-confirmation-tip-number {
|
|
82
|
+
color: #006AF6;
|
|
83
|
+
}
|
|
84
|
+
.message-queue-confirmation-button {
|
|
85
|
+
color: #006AF6;
|
|
86
|
+
cursor: pointer;
|
|
87
|
+
margin-top: 12px;
|
|
88
|
+
border: 1px solid #adcfff;
|
|
89
|
+
width: fit-content;
|
|
90
|
+
padding: 0px 16px;
|
|
91
|
+
height: 32px;
|
|
92
|
+
border-radius: 999px;
|
|
93
|
+
line-height: 32px;
|
|
94
|
+
background-color: #fff;
|
|
95
|
+
}
|
|
96
|
+
</style>
|