@tencentcloud/ai-desk-customer-vue 1.7.0 → 1.7.1
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 +9 -0
- package/components/CustomerServiceChat/index-web.vue +30 -2
- 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 +2 -14
- 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-queue-confirmation.vue +96 -0
- package/components/CustomerServiceChat/message-list/message-elements/message-text.vue +2 -4
- 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 +28 -0
package/CHANGELOG.md
CHANGED
|
@@ -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 || '');
|
|
48
36
|
}
|
|
49
37
|
return text;
|
|
50
38
|
},
|
|
@@ -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: {
|
|
@@ -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>
|
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
CUSTOM_BASIC_EMOJI_URL_MAPPING,
|
|
30
30
|
} from '../../emoji-config';
|
|
31
31
|
import { isPC } from '../../../../utils/env';
|
|
32
|
+
import { getStyledATagFromText } from '../../../../utils/utils';
|
|
32
33
|
import state from '../../../../utils/state.js';
|
|
33
34
|
const {ref, computed } = vue;
|
|
34
35
|
interface IProps {
|
|
@@ -64,10 +65,7 @@ const textMessageData = computed(() => {
|
|
|
64
65
|
+ CUSTOM_BASIC_EMOJI_URL_MAPPING[item.emojiKey];
|
|
65
66
|
}
|
|
66
67
|
} else if (item.name === 'text' && enableURLDetection.value) {
|
|
67
|
-
|
|
68
|
-
item.text = item.text.replace(/<[^>]+>/g, '').replace(/https?:\/\/[\w\-./?=&:#]+(?=[^\w\-./?=&:#]|$)/g, (url) => {
|
|
69
|
-
return `<a href="${url}" target="_blank" rel="noopener noreferrer" class="message-text-link" style="color: ${linkColor.value}; text-decoration: underline;">${url}</a>`;
|
|
70
|
-
}) || '';
|
|
68
|
+
item.text = getStyledATagFromText(item.text || '', "message-text-link", linkColor.value, '');
|
|
71
69
|
}
|
|
72
70
|
},
|
|
73
71
|
);
|
package/constant.ts
CHANGED
|
@@ -37,8 +37,10 @@ export const CUSTOM_MESSAGE_SRC = {
|
|
|
37
37
|
CONCURRENCY_LIMIT: '36',
|
|
38
38
|
TIMEOUT_WARNING: '37',
|
|
39
39
|
TRANSFER_TO_HUMAN: '39',
|
|
40
|
-
|
|
40
|
+
GET_SETTINGS: '42',
|
|
41
41
|
SEND_FEEDBACK: '43',
|
|
42
|
+
SEND_HEARTBEAT: '44',
|
|
43
|
+
QUEUE_CONFIRMATION: '45',
|
|
42
44
|
};
|
|
43
45
|
|
|
44
46
|
// im message extra type
|
|
@@ -149,6 +151,12 @@ export const WHITE_LIST = [
|
|
|
149
151
|
CUSTOM_MESSAGE_SRC.TIMEOUT_WARNING,
|
|
150
152
|
CUSTOM_MESSAGE_SRC.TRANSFER_TO_TASK_FLOW,
|
|
151
153
|
CUSTOM_MESSAGE_SRC.TRANSFER_TO_HUMAN,
|
|
154
|
+
CUSTOM_MESSAGE_SRC.QUEUE_CONFIRMATION,
|
|
155
|
+
];
|
|
156
|
+
|
|
157
|
+
export const SHOW_IF_FLOW_IN = [
|
|
158
|
+
CUSTOM_MESSAGE_SRC.MULTI_FORM,
|
|
159
|
+
CUSTOM_MESSAGE_SRC.QUEUE_CONFIRMATION,
|
|
152
160
|
];
|
|
153
161
|
|
|
154
162
|
export const TOOLBAR_BUTTON_TYPE = {
|
package/locales/en/aidesk.ts
CHANGED
|
@@ -40,5 +40,7 @@ const AIDesk = {
|
|
|
40
40
|
"当前前方排队人数": "Queue Position",
|
|
41
41
|
"结束排队": "Leave Queue",
|
|
42
42
|
"评价提交超过3分钟,无法更改。感谢您的反馈!": "Once a review has been submitted for more than 3 minutes, it cannot be modified.",
|
|
43
|
+
"不能发送空白消息": "Unable to send blank message",
|
|
44
|
+
"确认转人工": "Confirm transfer to agent",
|
|
43
45
|
}
|
|
44
46
|
export default AIDesk;
|
package/locales/es/aidesk.ts
CHANGED
|
@@ -40,5 +40,7 @@ const AIDesk = {
|
|
|
40
40
|
"当前前方排队人数": "Posición en la cola",
|
|
41
41
|
"结束排队": "Salir de la cola",
|
|
42
42
|
"评价提交超过3分钟,无法更改。感谢您的反馈!": "Una valoración enviada hace más de 3 minutos no puede modificarse.",
|
|
43
|
+
"不能发送空白消息": "No se puede enviar un mensaje vacío",
|
|
44
|
+
"确认转人工": "Confirmar transferencia a agente",
|
|
43
45
|
}
|
|
44
46
|
export default AIDesk;
|
package/locales/fil/aidesk.ts
CHANGED
|
@@ -39,5 +39,7 @@ const AIDesk = {
|
|
|
39
39
|
"当前前方排队人数": "Bilang ng mga tao sa unahan sa pila",
|
|
40
40
|
"结束排队": "Umalis sa pila",
|
|
41
41
|
"评价提交超过3分钟,无法更改。感谢您的反馈!": "Kapag lumipas na ang 3 minuto mula sa pagsusumite ng review, hindi na ito mababago.",
|
|
42
|
+
"不能发送空白消息": "Hindi maaaring magpadala ng blangkong mensahe",
|
|
43
|
+
"确认转人工": "Kumpirmahin ang paglipat sa ahente",
|
|
42
44
|
}
|
|
43
45
|
export default AIDesk;
|
package/locales/id/aidesk.ts
CHANGED
|
@@ -39,5 +39,7 @@ const AIDesk = {
|
|
|
39
39
|
"当前前方排队人数": "Jumlah orang di depan antrian saat ini",
|
|
40
40
|
"结束排队": "Keluar dari antrian",
|
|
41
41
|
"评价提交超过3分钟,无法更改。感谢您的反馈!": "Ulasan yang dikirim lebih dari 3 menit tidak dapat diubah.",
|
|
42
|
+
"不能发送空白消息": "Tidak dapat mengirim pesan kosong",
|
|
43
|
+
"确认转人工": "Konfirmasi transfer ke agen",
|
|
42
44
|
}
|
|
43
45
|
export default AIDesk;
|
package/locales/ja/aidesk.ts
CHANGED
package/locales/ko/aidesk.ts
CHANGED
package/locales/ms/aidesk.ts
CHANGED
|
@@ -39,5 +39,7 @@ const AIDesk = {
|
|
|
39
39
|
"当前前方排队人数": "Bilangan orang di hadapan dalam barisan sekarang",
|
|
40
40
|
"结束排队": "Tinggalkan barisan",
|
|
41
41
|
"评价提交超过3分钟,无法更改。感谢您的反馈!": "Ulasan yang dihantar lebih dari 3 minit tidak boleh diubah.",
|
|
42
|
+
"不能发送空白消息": "Tidak dapat menghantar mesej kosong",
|
|
43
|
+
"确认转人工": "Sahkan pindah kepada ejen",
|
|
42
44
|
}
|
|
43
45
|
export default AIDesk;
|
package/locales/ru/aidesk.ts
CHANGED
|
@@ -39,5 +39,7 @@ const AIDesk = {
|
|
|
39
39
|
"当前前方排队人数": "Текущее количество человек впереди в очереди",
|
|
40
40
|
"结束排队": "Выйти из очереди",
|
|
41
41
|
"评价提交超过3分钟,无法更改。感谢您的反馈!": "Отзыв нельзя изменить после истечения 3 минут с момента отправки.",
|
|
42
|
+
"不能发送空白消息": "Невозможно отправить пустое сообщение",
|
|
43
|
+
"确认转人工": "Подтвердить перевод на оператора",
|
|
42
44
|
}
|
|
43
45
|
export default AIDesk;
|
package/locales/th/aidesk.ts
CHANGED
|
@@ -39,5 +39,7 @@ const AIDesk = {
|
|
|
39
39
|
"当前前方排队人数": "จำนวนผู้รอคิวด้านหน้าในขณะนี้",
|
|
40
40
|
"结束排队": "ออกจากคิว",
|
|
41
41
|
"评价提交超过3分钟,无法更改。感谢您的反馈!": "รีวิวที่ส่งไปแล้วเกิน 3 นาทีจะไม่สามารถแก้ไขได้",
|
|
42
|
+
"不能发送空白消息": "ไม่สามารถส่งข้อความว่างได้",
|
|
43
|
+
"确认转人工": "ยืนยันการโอนย้ายไปยังเจ้าหน้าที่",
|
|
42
44
|
}
|
|
43
45
|
export default AIDesk;
|
package/locales/vi/aidesk.ts
CHANGED
|
@@ -39,5 +39,7 @@ const AIDesk = {
|
|
|
39
39
|
"当前前方排队人数": "Số người đang chờ trước bạn trong hàng",
|
|
40
40
|
"结束排队": "Rời khỏi hàng",
|
|
41
41
|
"评价提交超过3分钟,无法更改。感谢您的反馈!": "Đánh giá sau khi gửi hơn 3 phút sẽ không thể chỉnh sửa.",
|
|
42
|
+
"不能发送空白消息": "Không thể gửi tin nhắn trống",
|
|
43
|
+
"确认转人工": "Xác nhận chuyển tiếp sang nhân viên",
|
|
42
44
|
}
|
|
43
45
|
export default AIDesk;
|
package/locales/zh_cn/aidesk.ts
CHANGED
package/locales/zh_tw/aidesk.ts
CHANGED
package/package.json
CHANGED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { CUSTOM_MESSAGE_SRC } from "../constant";
|
|
2
|
+
import TUIChatEngine, { TUIStore, StoreName, TUIChatService } from "../@aidesk/uikit-engine";
|
|
3
|
+
|
|
4
|
+
let heartbeatTimer: ReturnType<typeof setTimeout> | null = null;
|
|
5
|
+
let isHeartbeatRunning = false;
|
|
6
|
+
let currentConversationID = TUIStore.getData(StoreName.CONV, 'currentConversationID');
|
|
7
|
+
const HEARTBEAT_INTERVAL = 30000; // 30 秒
|
|
8
|
+
|
|
9
|
+
// 在人工排队时发送心跳
|
|
10
|
+
export async function sendHeartbeat(conversationID: string, eventName: string) {
|
|
11
|
+
await TUIChatService.sendCustomMessage({
|
|
12
|
+
to: conversationID.slice(3),
|
|
13
|
+
conversationType: TUIChatEngine.TYPES.CONV_C2C,
|
|
14
|
+
payload: {
|
|
15
|
+
data: JSON.stringify({
|
|
16
|
+
customerServicePlugin: 0,
|
|
17
|
+
src: CUSTOM_MESSAGE_SRC.SEND_HEARTBEAT,
|
|
18
|
+
userClientEvent: {
|
|
19
|
+
event: eventName,
|
|
20
|
+
},
|
|
21
|
+
}),
|
|
22
|
+
},
|
|
23
|
+
needReadReceipt: false,
|
|
24
|
+
},{ onlineUserOnly: true });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function canSendHeartbeat() {
|
|
28
|
+
const isQueuing = TUIStore.getData(StoreName.CUSTOM, 'isQueuing');
|
|
29
|
+
const queueLeavePageTimeoutEnable = TUIStore.getData(StoreName.CUSTOM, 'queueLeavePageTimeoutEnable');
|
|
30
|
+
currentConversationID = TUIStore.getData(StoreName.CONV, 'currentConversationID');
|
|
31
|
+
const queueNumber = isQueuing && isQueuing.conversationID === currentConversationID ? isQueuing.value : -1;
|
|
32
|
+
if (queueNumber < 0 || !queueLeavePageTimeoutEnable || !currentConversationID) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function startHeartbeat() {
|
|
39
|
+
if (!canSendHeartbeat() || isHeartbeatRunning) {
|
|
40
|
+
stopHeartbeat();
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
isHeartbeatRunning = true;
|
|
45
|
+
(async function loop() {
|
|
46
|
+
if (!canSendHeartbeat() || !isHeartbeatRunning) {
|
|
47
|
+
stopHeartbeat();
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
await sendHeartbeat(currentConversationID, "sessionPageHeartbeat");
|
|
51
|
+
|
|
52
|
+
heartbeatTimer = setTimeout(() => {
|
|
53
|
+
heartbeatTimer = null;
|
|
54
|
+
loop();
|
|
55
|
+
}, HEARTBEAT_INTERVAL);
|
|
56
|
+
})();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function stopHeartbeat() {
|
|
60
|
+
if (heartbeatTimer !== null) {
|
|
61
|
+
clearTimeout(heartbeatTimer!);
|
|
62
|
+
heartbeatTimer = null;
|
|
63
|
+
}
|
|
64
|
+
isHeartbeatRunning = false;
|
|
65
|
+
}
|
package/utils/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { customerServicePayloadType } from '../interface';
|
|
2
|
-
import { CUSTOM_MESSAGE_SRC, TYPES, WHITE_LIST } from '../constant';
|
|
2
|
+
import { CUSTOM_MESSAGE_SRC, SHOW_IF_FLOW_IN, TYPES, WHITE_LIST } from '../constant';
|
|
3
3
|
import { IMessageModel } from '../@aidesk/uikit-engine';
|
|
4
4
|
|
|
5
5
|
// Determine if it is a JSON string
|
|
@@ -71,9 +71,9 @@ export const isMessageInvisible = (message: IMessageModel): boolean => {
|
|
|
71
71
|
const isCustomerMessage = message?.type === TYPES.MSG_CUSTOM;
|
|
72
72
|
const isGroupTipMessage = message?.type === TYPES.MSG_GROUP_TIP;
|
|
73
73
|
const isCustomerInvisible = customerServicePayload?.src && !WHITE_LIST.includes(customerServicePayload?.src);
|
|
74
|
-
const
|
|
74
|
+
const isShowIfFlowInMessage: boolean = customerServicePayload?.src !== null && SHOW_IF_FLOW_IN.includes(customerServicePayload?.src as string) && message.flow === 'out';
|
|
75
75
|
const isRobot = customerServicePayload?.src === CUSTOM_MESSAGE_SRC.ROBOT && robotCommandArray.indexOf(customerServicePayload?.content?.command) !== -1;
|
|
76
|
-
return (isCustomerMessage && (isCustomerInvisible || isRobot ||
|
|
76
|
+
return (isCustomerMessage && (isCustomerInvisible || isRobot || isShowIfFlowInMessage)) || isGroupTipMessage;
|
|
77
77
|
};
|
|
78
78
|
|
|
79
79
|
// 如果用户选择 block cookies,此时访问 localStorage 浏览器会抛错
|
package/utils/utils.ts
CHANGED
|
@@ -420,4 +420,32 @@ export function throttle(
|
|
|
420
420
|
func.apply(this, args);
|
|
421
421
|
}
|
|
422
422
|
};
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
export function getStyledATagFromText(text, className, linkColor, title) {
|
|
426
|
+
// 开启 url 识别时先移除 html 标签,避免 xss 漏洞
|
|
427
|
+
// 预处理文本:移除换行符和制表符
|
|
428
|
+
const preprocessedText = text.replace(/[\n\t]/g, '').trim();
|
|
429
|
+
|
|
430
|
+
// 先移除HTML标签避免XSS漏洞
|
|
431
|
+
const sanitizedText = preprocessedText.replace(/<[^>]+>/g, '');
|
|
432
|
+
|
|
433
|
+
// 匹配URL:从http://或https://开始,匹配尽可能长的有效URL字符序列
|
|
434
|
+
// 包括:字母数字、-._~:/?#[]@!$&'()*+,;=% 等URL允许的字符
|
|
435
|
+
const ret = sanitizedText.replace(/https?:\/\/[\w\-._~:/?#\[\]@!$&'()*+,;=%]+/g, (matchedUrl) => {
|
|
436
|
+
// 检查URL是否包含中文字符,如果包含则跳过处理
|
|
437
|
+
const chineseRegex = /[\u4e00-\u9fff]/;
|
|
438
|
+
if (chineseRegex.test(matchedUrl)) {
|
|
439
|
+
return matchedUrl; // 不处理包含中文的URL
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
let isURLInText = false;
|
|
443
|
+
if (matchedUrl !== preprocessedText) {
|
|
444
|
+
// 如果 text 里包含 url,我们就用 matchedUrl 作为 a 标签的值;否则用 text 作为值
|
|
445
|
+
isURLInText = true;
|
|
446
|
+
}
|
|
447
|
+
const encodedUrl = encodeURI(matchedUrl);
|
|
448
|
+
return `<a target="_blank" rel="noreferrer noopener" class="${className}" style="color:${linkColor}; text-decoration:underline" href="${encodedUrl}" title="${title || ''}">${isURLInText ? matchedUrl : text}</a>`;
|
|
449
|
+
});
|
|
450
|
+
return ret;
|
|
423
451
|
}
|