@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.
Files changed (33) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/components/CustomerServiceChat/feedback-modal/index.vue +3 -0
  3. package/components/CustomerServiceChat/index-web.vue +32 -4
  4. package/components/CustomerServiceChat/message-input/index-web.vue +18 -1
  5. package/components/CustomerServiceChat/message-list/index-web.vue +3 -2
  6. package/components/CustomerServiceChat/message-list/message-elements/message-desk/message-desk-elements/marked.ts +3 -15
  7. package/components/CustomerServiceChat/message-list/message-elements/message-desk/message-desk-elements/message-desk.vue +9 -1
  8. package/components/CustomerServiceChat/message-list/message-elements/message-desk/message-desk-elements/message-multi-form/component-pc/input-pc.vue +1 -1
  9. package/components/CustomerServiceChat/message-list/message-elements/message-desk/message-desk-elements/message-multi-form/form-mobile.vue +2 -1
  10. package/components/CustomerServiceChat/message-list/message-elements/message-desk/message-desk-elements/message-queue-confirmation.vue +96 -0
  11. package/components/CustomerServiceChat/message-list/message-elements/message-desk/message-desk-elements/message-stream.vue +314 -20
  12. package/components/CustomerServiceChat/message-list/message-elements/message-desk/message-desk-elements/styles/common.scss +87 -84
  13. package/components/CustomerServiceChat/message-list/message-elements/message-large-stream.vue +6 -1
  14. package/components/CustomerServiceChat/message-list/message-elements/message-text.vue +2 -4
  15. package/components/CustomerServiceChat/style/common.scss +65 -62
  16. package/components/common/common.scss +49 -47
  17. package/constant.ts +9 -1
  18. package/locales/en/aidesk.ts +2 -0
  19. package/locales/es/aidesk.ts +2 -0
  20. package/locales/fil/aidesk.ts +2 -0
  21. package/locales/id/aidesk.ts +2 -0
  22. package/locales/ja/aidesk.ts +2 -0
  23. package/locales/ko/aidesk.ts +2 -0
  24. package/locales/ms/aidesk.ts +2 -0
  25. package/locales/ru/aidesk.ts +2 -0
  26. package/locales/th/aidesk.ts +2 -0
  27. package/locales/vi/aidesk.ts +2 -0
  28. package/locales/zh_cn/aidesk.ts +2 -0
  29. package/locales/zh_tw/aidesk.ts +2 -0
  30. package/package.json +1 -1
  31. package/utils/heartbeat-handler.ts +65 -0
  32. package/utils/index.ts +3 -3
  33. package/utils/utils.ts +23 -0
package/CHANGELOG.md CHANGED
@@ -1,3 +1,20 @@
1
+ ## 1.7.2 @2026.3.30
2
+
3
+ ### Features
4
+ - 优化流式消息展示效果
5
+
6
+ ### Fixed
7
+ - 修复已知问题
8
+
9
+ ## 1.7.1 @2026.3.17
10
+
11
+ ### Features
12
+ - 转人工排队时,新增二次确认。
13
+ - 转人工排队时,支持检测用户离开页面行为。
14
+
15
+ ### Fixed
16
+ - 修复已知问题。
17
+
1
18
  ## 1.7.0 @2026.2.9
2
19
 
3
20
  ### Features
@@ -403,5 +403,8 @@ export default {
403
403
  }
404
404
  .dialog-title {
405
405
  gap: 10px;
406
+ display: flex;
407
+ justify-content: space-between;
408
+ padding: 20px;
406
409
  }
407
410
  </style>
@@ -1,8 +1,8 @@
1
1
  <template>
2
- <div :class="['tui-chat', !isPC && 'tui-chat-h5']" :key="currentLanguage">
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.GET_FEEDBACK_MENU) {
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 Log from '../../../../../../utils/logger';
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
- // 匹配以 http:// https:// 开头,所有 URL 主体字符,遇到第一个非主体字符(如中文括号、空格、表情符号等)时停止
36
- let ret = href.replace(/&amp;/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: {
@@ -63,7 +63,7 @@ export default {
63
63
  }
64
64
  }
65
65
  </script>
66
- <style lang="scss">
66
+ <style lang="scss" scoped>
67
67
  input:disabled {
68
68
  background-color: #dbdbdb;
69
69
  opacity: 0.5;
@@ -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>