@tencentcloud/ai-desk-customer-vue 0.3.0 → 0.7.0

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.
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" height="20px" viewBox="0 -960 960 960" width="20px" fill="#CCCCCC"><path d="M480.2-240Q380-240 310-309.8q-70-69.8-70-170T309.8-650q69.8-70 170-70T650-650.2q70 69.8 70 170T650.2-310q-69.8 70-170 70Z"/></svg>
@@ -18,6 +18,9 @@
18
18
  @handleEditor="handleEditor"
19
19
  @closeInputToolBar="() => changeToolbarDisplayType('none')"
20
20
  />
21
+ <MessageToolbarButton
22
+ :toolbarButtonList="props.toolbarButtonList"
23
+ />
21
24
  <MessageInputToolbar
22
25
  v-if="isInputToolbarShow"
23
26
  :class="[
@@ -87,9 +90,10 @@ import MessageInput from './message-input/index-web.vue';
87
90
  import MessageInputToolbar from './message-input-toolbar/index-web.vue';
88
91
  import EmojiDialog from './message-input-toolbar/emoji-dialog-mobile/emoji-dialog-mobile.vue';
89
92
  import { isPC, isH5} from '../../utils/env';
90
- import { ToolbarDisplayType } from '../../interface';
93
+ import { ToolbarButtonModel, ToolbarDisplayType } from '../../interface';
91
94
  import { isSupportedLang } from '../../utils/';
92
95
  import Log from '../../utils/logger';
96
+ import MessageToolbarButton from './message-toolbar-button/index.vue';
93
97
 
94
98
  const { ref, onMounted, onUnmounted, computed } = vue;
95
99
 
@@ -99,6 +103,7 @@ interface IProps {
99
103
  userID?:string;
100
104
  userSig?:string;
101
105
  robotLang?:string;
106
+ toolbarButtonList?:ToolbarButtonModel[];
102
107
  }
103
108
 
104
109
  const emits = defineEmits(['closeChat']);
@@ -115,6 +120,7 @@ const props = withDefaults(defineProps<IProps>(), {
115
120
  userID: '',
116
121
  userSig: '',
117
122
  robotLang: '',
123
+ toolbarButtonList:() => [] as ToolbarButtonModel[]
118
124
  });
119
125
 
120
126
  const loginCustomerUIKit = () => {
@@ -68,8 +68,9 @@
68
68
  @resendMessage="resendMessage(item)"
69
69
  >
70
70
  <template #messageElement>
71
+ <MessageThinking v-if="isThinkingMessage(item)"/>
71
72
  <MessageText
72
- v-if="item.type === TYPES.MSG_TEXT"
73
+ v-else-if="item.type === TYPES.MSG_TEXT"
73
74
  :content="item.getMessageContent()"
74
75
  />
75
76
  <ProgressMessage
@@ -175,7 +176,7 @@
175
176
 
176
177
  <script lang="ts" setup>
177
178
  import vue from '../../../adapter-vue';
178
- const { ref, nextTick, computed, onMounted, onUnmounted } = vue;
179
+ const { ref, nextTick, computed, onMounted, onUnmounted } = vue;
179
180
  import TUIChatEngine, {
180
181
  IMessageModel,
181
182
  TUIStore,
@@ -203,6 +204,7 @@ import MessageVideo from './message-elements/message-video-web.vue';
203
204
  import MessageTool from './message-tool/index-web.vue';
204
205
  import MessageRevoked from './message-tool/message-revoked.vue';
205
206
  import MessagePlugin from '../message-list/message-elements/message-desk/message-plugin-web.vue';
207
+ import MessageThinking from './message-elements/message-thinking.vue';
206
208
  import ScrollButton from './scroll-button/index.vue';
207
209
  import { isPluginMessage } from './message-elements/message-desk/index';
208
210
  import Dialog from '../../common/Dialog/index.vue';
@@ -214,9 +216,8 @@ import {
214
216
  isEnabledMessageReadReceiptGlobal,
215
217
  deepCopy,
216
218
  } from '../../../utils/utils';
217
- import { isMessageInvisible } from '../../../utils/index';
218
- // import {TUICustomerServer} from '../../../server';
219
- import {isCustomerConversation} from '../../../index';
219
+ import { isMessageInvisible, isThinkingMessage, isThinkingMessageOverTime } from '../../../utils/index';
220
+ import { isCustomerConversation } from '../../../index';
220
221
 
221
222
  interface ScrollConfig {
222
223
  scrollToMessage?: IMessageModel;
@@ -328,19 +329,26 @@ onUnmounted(() => {
328
329
  });
329
330
 
330
331
  async function onMessageListUpdated(list: IMessageModel[]) {
331
- if(!isCustomerConversation(currentConversationID.value)){
332
+ if (!isCustomerConversation(currentConversationID.value)) {
332
333
  return;
333
334
  }
334
335
  observer?.disconnect();
335
336
  const oldLastMessage = currentLastMessage.value;
336
337
  let hasEmojiReaction = false;
337
338
  allMessageList.value = list;
338
- messageList.value = list.filter((message) => {
339
+
340
+ messageList.value = list.filter((message, index) => {
339
341
  if (message.reactionList?.length && !message.isDeleted) {
340
342
  hasEmojiReaction = true;
341
343
  }
344
+
345
+ // 判断是否结束
346
+ if (isThinkingMessage(message)) {
347
+ return isThinkingMessageOverTime(message);
348
+ }
342
349
  return !message.isDeleted && !isMessageInvisible(message as any);
343
350
  });
351
+
344
352
  if (!messageList.value?.length) {
345
353
  currentLastMessage.value = {};
346
354
  return;
@@ -695,6 +703,7 @@ function setAudioPlayed(messageID: string) {
695
703
  };
696
704
  }
697
705
 
706
+
698
707
  defineExpose({
699
708
  scrollToLatestMessage,
700
709
  });
@@ -1,61 +1,74 @@
1
1
  <template>
2
2
  <div class="message-stream">
3
3
  <pre :class="['message-marked']" v-html="displayedContent" />
4
- <span
5
- v-if="!isFinished"
6
- class="blinking-cursor"
7
- />
8
4
  </div>
9
5
  </template>
10
6
 
11
- <script lang="ts">
7
+ <script lang="ts" setup>
12
8
  import vue from '../../../../../../adapter-vue';
13
9
  import { customerServicePayloadType } from '../../../../../../interface';
14
10
  import { parseMarkdown } from './marked'
11
+ import { TypeWriter } from "./type-writer";
12
+ import { JSONToObject } from "../../../../../../utils";
15
13
 
16
- const { ref, watchEffect, onMounted } = vue;
14
+ const { ref, computed, withDefaults, defineProps, watch } = vue;
17
15
 
18
16
  interface Props {
19
17
  payload: customerServicePayloadType;
20
18
  }
21
19
 
22
- export default {
23
- props: {
24
- payload: {
25
- type: Object as () => customerServicePayloadType,
26
- default: () => ({}),
27
- },
20
+ const props = withDefaults(defineProps<Props>(), {
21
+ payload: () => '',
22
+ });
23
+
24
+ const isStreaming = ref<boolean>(false);
25
+ const chunks = ref<string>('');
26
+ const isFinished = ref<boolean>(true);
27
+ const prevChunksLength = ref<number>(0);
28
+ const streamContent = ref<string>('');
29
+ const displayedContent = computed(() => parseMarkdown(streamContent.value));
30
+
31
+ const typeWriter = new TypeWriter({
32
+ onTyping: (item: string) => {
33
+ streamContent.value += item;
34
+ // emits('onStreaming', item, streamContent.value);
35
+ },
36
+ onComplete() {
37
+ isStreaming.value = false;
28
38
  },
29
- setup(props: Props) {
30
- const displayedContent = ref<string>('');
31
- const isFinished = ref<boolean>(false);
32
- let currentIndex = 0;
39
+ });
33
40
 
34
- onMounted(() => {
35
- displayedContent.value = parseMarkdown(props?.payload?.chunks?.join('') ?? '');
36
- currentIndex = displayedContent.value.length;
37
- });
41
+ function startStreaming(content: string[]) {
42
+ if (!isStreaming.value) {
43
+ isStreaming.value = true;
44
+ typeWriter.add(content);
45
+ typeWriter.start();
46
+ } else {
47
+ typeWriter.add(content);
48
+ }
49
+ }
38
50
 
39
- watchEffect(() => {
40
- const newContent = props?.payload?.chunks?.join('') ?? '';
41
- const parsedContent = parseMarkdown(newContent);
42
- if (parsedContent.length > currentIndex) {
43
- displayedContent.value = parsedContent;
44
- currentIndex = parsedContent.length;
51
+ watch(() => props.payload, (newValue: string, oldValue: string) => {
52
+ if (newValue === oldValue) {
53
+ return;
45
54
  }
46
- });
47
-
48
- watchEffect(() => {
49
- isFinished.value = props?.payload?.isFinished === 1;
50
- });
51
55
 
52
- return {
53
- props,
54
- isFinished,
55
- displayedContent,
56
- };
57
- },
58
- };
56
+ const _payloadObject = JSONToObject(props.payload);
57
+ chunks.value = Array.isArray(_payloadObject.chunks) ? _payloadObject.chunks.join('') : _payloadObject.chunks;
58
+ isFinished.value = _payloadObject.isFinished === 1;
59
+ if (newValue && !oldValue && isFinished.value) {
60
+ // disable typeWriter style or history message first load
61
+ streamContent.value = chunks.value;
62
+ } else {
63
+ const _newChunksToAdd = chunks.value?.slice(prevChunksLength.value);
64
+ startStreaming([_newChunksToAdd]);
65
+ }
66
+ prevChunksLength.value = chunks.value?.length;
67
+ }, {
68
+ deep: true,
69
+ immediate: true,
70
+ },
71
+ );
59
72
  </script>
60
73
  <style lang="scss" scoped>
61
74
  .message-stream {
@@ -63,19 +76,5 @@ export default {
63
76
  word-break: keep-all;
64
77
  white-space: normal;
65
78
  font-size: 14px;
66
-
67
- .blinking-cursor {
68
- display: inline-block;
69
- width: 1px;
70
- height: 1em;
71
- background-color: black;
72
- animation: blink 1s step-end infinite;
73
- vertical-align: sub;
74
- }
75
-
76
- @keyframes blink {
77
- 0%, 100% { opacity: 1; }
78
- 50% { opacity: 0; }
79
- }
80
79
  }
81
80
  </style>
@@ -118,7 +118,7 @@
118
118
  .message-marked {
119
119
  overflow: hidden;
120
120
  word-break: break-word;
121
- white-space: normal;
121
+ white-space: pre-wrap;
122
122
  display: flex;
123
123
  flex-direction: column;
124
124
  justify-content: flex-start;
@@ -0,0 +1,189 @@
1
+ const chineseRegex = /[\u4e00-\u9fa5]/;
2
+ const wordAndNonWordRegex = /\b\w+\b|[^\w]+/g;
3
+ const isStringArray = (test: any): boolean => {
4
+ return Array.isArray(test) && !test.some(value => typeof value !== 'string');
5
+ };
6
+
7
+ export class TypeWriter {
8
+ /**
9
+ * @property {array} strings strings to be typed
10
+ */
11
+ public strings: string[] = [];
12
+
13
+ /**
14
+ * @property {boolean} isTyping current typing status
15
+ */
16
+ public isTyping: boolean = false;
17
+
18
+ /**
19
+ * @property {number} typeSpeed type speed in milliseconds. If empty, using dynamic speed.
20
+ */
21
+ public typeSpeed?: number = 0;
22
+
23
+ /**
24
+ * @property {number} curArrayPos current typing string's position of all strings.
25
+ */
26
+ private curArrayPos: number = 0;
27
+
28
+ /**
29
+ * @property {number} curCharPos current typing character's position in current strings.
30
+ */
31
+ private curCharPos: number = 0;
32
+
33
+ /**
34
+ * @property {ReturnType<typeof setTimeout>} timer timer for type writer animation
35
+ */
36
+ private timer?: ReturnType<typeof setTimeout>;
37
+
38
+ /**
39
+ * On string is typing
40
+ * @param {string} curStr
41
+ * @param {number} arrayPos
42
+ * @param {number} characterPos
43
+ * @param {Typed} self
44
+ */
45
+ public onTyping?: (curStr: string, arrayPos: number, characterPos: number, self: any) => void;
46
+
47
+ /**
48
+ * After start
49
+ * @param {number} arrayPos
50
+ * @param {number} characterPos
51
+ * @param {TypeWriter} self
52
+ */
53
+ public onStart?: (arrayPos: number, characterPos: number, self: any) => void;
54
+
55
+ /**
56
+ * After stop
57
+ * @param {number} arrayPos
58
+ * @param {number} characterPos
59
+ * @param {TypeWriter} self
60
+ */
61
+ public onStop?: (arrayPos: number, characterPos: number, self: any) => void;
62
+
63
+ /**
64
+ * All typing is complete
65
+ * @param {Typed} self
66
+ */
67
+ public onComplete?: (self: any) => void;
68
+
69
+ constructor(options: {
70
+ defaultStrings?: string[];
71
+ typeSpeed?: number;
72
+ onTyping?: (curStr: string, arrayPos: number, characterPos: number, self: any) => void;
73
+ onComplete?: (self: any) => void;
74
+ onStart?: (arrayPos: number, characterPos: number, self: any) => void;
75
+ onStop?: (arrayPos: number, characterPos: number, self: any) => void;
76
+ }) {
77
+ const { defaultStrings, typeSpeed, onTyping, onComplete, onStart, onStop } = options;
78
+ if (defaultStrings && isStringArray(defaultStrings)) {
79
+ this.add(defaultStrings);
80
+ }
81
+ if (typeof typeSpeed === 'number') {
82
+ this.typeSpeed = typeSpeed;
83
+ }
84
+ if (typeof onTyping === 'function') {
85
+ this.onTyping = onTyping;
86
+ }
87
+ if (typeof onComplete === 'function') {
88
+ this.onComplete = onComplete;
89
+ }
90
+ if (typeof onStart === 'function') {
91
+ this.onStart = onStart;
92
+ }
93
+ if (typeof onStop === 'function') {
94
+ this.onStop = onStop;
95
+ }
96
+ }
97
+
98
+ add(addStrings: string[]) {
99
+ if (!addStrings || !addStrings.length) return;
100
+ addStrings.forEach((item: string) => {
101
+ if (chineseRegex.test(item)) {
102
+ const newValueArray = item.split('');
103
+ this.strings.push(...newValueArray);
104
+ } else {
105
+ const newValueArray = item.match(wordAndNonWordRegex) || item.split('');
106
+ this.strings.push(...newValueArray);
107
+ }
108
+ });
109
+ }
110
+
111
+ start() {
112
+ if (this.isTyping) {
113
+ return;
114
+ }
115
+ this.isTyping = true;
116
+ this.onStart && this.onStart(this.curArrayPos, this.curCharPos, this);
117
+ this._next();
118
+ }
119
+
120
+ stop() {
121
+ if (!this.isTyping) {
122
+ return;
123
+ }
124
+ this.isTyping = false;
125
+ clearTimeout(this.timer);
126
+ this.onStop && this.onStop(this.curArrayPos, this.curCharPos, this);
127
+ }
128
+
129
+ done() {
130
+ this.stop();
131
+ let _str = this.strings[this.curArrayPos].slice(this.curCharPos);
132
+ _str += this.strings.slice(this.curArrayPos + 1).join('');
133
+ this.curArrayPos = this.strings.length - 1;
134
+ this.curCharPos = this.strings[this.curArrayPos]?.length - 1;
135
+ this.onTyping && this.onTyping(_str, this.curArrayPos, this.curCharPos, this);
136
+ this.strings = [];
137
+ }
138
+
139
+ _consume() {
140
+ if (!this.strings.length) {
141
+ return;
142
+ }
143
+
144
+ if ((this.curArrayPos >= this.strings.length)) {
145
+ this.isTyping = false;
146
+ this.onComplete?.(this);
147
+ return;
148
+ }
149
+
150
+ const item = this.strings[this.curArrayPos]?.[this.curCharPos];
151
+ if (item) {
152
+ this.onTyping && this.onTyping(item, this.curArrayPos, this.curCharPos, this);
153
+ }
154
+
155
+ if (this.curCharPos < this.strings[this.curArrayPos]?.length - 1) {
156
+ this.curCharPos++;
157
+ } else {
158
+ this.curArrayPos++;
159
+ this.curCharPos = 0;
160
+ }
161
+ }
162
+
163
+ _next() {
164
+ this._consume();
165
+ this.timer = setTimeout(() => {
166
+ this._consume();
167
+ if (this.isTyping) {
168
+ this._next();
169
+ }
170
+ }, this.typeSpeed || this._dynamicSpeed());
171
+ }
172
+
173
+ _dynamicSpeed() {
174
+ let length = 0;
175
+ length += (this.strings[this.curArrayPos]?.length || 0) - this.curCharPos - 1;
176
+ for (let i = this.curArrayPos + 1; i < this.strings.length; i++) {
177
+ length += this.strings[i]?.length || 0;
178
+ }
179
+ if (length <= 0) {
180
+ length = 10;
181
+ }
182
+ const speed = 1500 / length;
183
+ if (speed >= 150) {
184
+ return 150;
185
+ } else {
186
+ return speed;
187
+ }
188
+ }
189
+ }
@@ -0,0 +1,59 @@
1
+ <template>
2
+ <div class="message-thinking">
3
+ <div v-for="(icon, index) in icons" :key="index">
4
+ <transition name="fade">
5
+ <Icon v-if="icon" :file="loading_message" width="16px" height="16px"/>
6
+ </transition>
7
+ </div>
8
+ </div>
9
+ </template>
10
+ <script lang="ts">
11
+ import vue from '../../../../adapter-vue';
12
+ import Icon from '../../../common/Icon.vue';
13
+ import loading_message from '../../../../assets/loading_message.svg';
14
+ const { ref, watchEffect,onMounted,onUnmounted} = vue;
15
+ export default {
16
+ components:{
17
+ Icon
18
+ },
19
+ setup() {
20
+ const icons = ref([false, false, false]);
21
+ const index = ref(0);
22
+
23
+ let intervalId:any;
24
+
25
+ onMounted(() => {
26
+ intervalId = setInterval(() => {
27
+ if (index.value < icons.value.length) {
28
+ icons.value = icons.value.map((v, i) => (i === index.value ? true : v));
29
+ index.value += 1;
30
+ } else {
31
+ icons.value = icons.value.map(() => false);
32
+ index.value = 0;
33
+ }
34
+ }, 500);
35
+ });
36
+
37
+ onUnmounted(() => {
38
+ intervalId && clearInterval(intervalId)
39
+ intervalId = null;
40
+ });
41
+
42
+ return { icons,loading_message };
43
+ }
44
+ }
45
+ </script>
46
+ <style lang="scss" scoped>
47
+ .message-thinking{
48
+ display: flex;
49
+ flex-direction: row;
50
+ width: 45px;
51
+ height: 16px;
52
+ }
53
+ .fade-enter-active, .fade-leave-active {
54
+ transition: opacity .5s;
55
+ }
56
+ .fade-enter, .fade-leave-to {
57
+ opacity: 0;
58
+ }
59
+ </style>
@@ -0,0 +1,69 @@
1
+ <template>
2
+ <div class="toolbar-button-container">
3
+ <template v-for="(item, index) in props.toolbarButtonList">
4
+ <div v-if="item.renderCondition()" :key="index"
5
+ :class="['toolbar-button', isH5 ? 'toolbar-button-h5' : '']" @click="onClick(index)">
6
+ <Icon v-if="item.icon" class="toolbar-button-icon" :file="item.icon" width="18px" height="18px"/>
7
+ <div class="toolbar-button-text">
8
+ {{ item.title }}
9
+ </div>
10
+
11
+ </div>
12
+ </template>
13
+ </div>
14
+ </template>
15
+ <script lang="ts" setup>
16
+ import { isH5 } from '../../../utils/env';
17
+ import { ToolbarButtonModel } from '../../../interface';
18
+ import Icon from '../../common/Icon.vue';
19
+
20
+ interface IProps {
21
+ toolbarButtonList?: ToolbarButtonModel[]
22
+ }
23
+
24
+ const props = withDefaults(defineProps<IProps>(), {
25
+ toolbarButtonList: () => [] as ToolbarButtonModel[]
26
+ });
27
+
28
+ function onClick(index: number) {
29
+ props.toolbarButtonList[index].clickEvent();
30
+ }
31
+
32
+ </script>
33
+ <style>
34
+ .toolbar-button-container {
35
+ display: flex;
36
+ flex-direction: row !important;
37
+ margin: 5px !important;
38
+ display: flex ;
39
+ align-items: center ;
40
+ }
41
+
42
+ .toolbar-button {
43
+ border: 1px solid #E7EAEF;
44
+ padding: 5px;
45
+ border-radius: 20px;
46
+ cursor: pointer;
47
+ display: flex;
48
+ align-items: center;
49
+ margin-left: 10px;
50
+ white-space: nowrap;
51
+ }
52
+
53
+ .toolbar-button-h5 {
54
+ border: none;
55
+ background-color: #fff;
56
+ box-shadow: 0px 2px 2px 0px rgba(70, 98, 140, 0.06);
57
+ }
58
+
59
+ .toolbar-button-icon {
60
+ margin-right: 3px;
61
+ }
62
+ .toolbar-button-text {
63
+ font-size: 12px;
64
+ text-overflow: ellipsis;
65
+ max-width: 100px;
66
+ overflow: hidden;
67
+ font-family: PingFangSC-Regular;
68
+ }
69
+ </style>
package/constant.ts CHANGED
@@ -28,6 +28,7 @@ export const CUSTOM_MESSAGE_SRC = {
28
28
  STREAM_TEXT: '31',
29
29
  MULTI_BRANCH:'32',
30
30
  MULTI_FORM:'33',
31
+ THINKING:'35',
31
32
  };
32
33
 
33
34
  // im message extra type
package/interface.ts CHANGED
@@ -8,6 +8,7 @@ export interface customerServicePayloadType {
8
8
  chunks?: string[];
9
9
  status?:number;
10
10
  nodeStatus?:number;
11
+ thinkingStatus?:number;
11
12
  }
12
13
 
13
14
  interface IMenuItem {
@@ -174,3 +175,10 @@ export interface ISendMessagePayload {
174
175
  file?: any;
175
176
  atUserList?: string[];
176
177
  }
178
+
179
+ export interface ToolbarButtonModel {
180
+ title:string,
181
+ icon?:string,
182
+ renderCondition:()=>{},
183
+ clickEvent:()=>void
184
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tencentcloud/ai-desk-customer-vue",
3
- "version": "0.3.0",
3
+ "version": "0.7.0",
4
4
  "description": "chat uikit ai-desk-customer web-mui-uikit",
5
5
  "main": "index",
6
6
  "keywords": [
package/server.ts CHANGED
@@ -1,7 +1,9 @@
1
1
  import TUICore, { TUIConstants } from '@tencentcloud/tui-core';
2
2
  import {
3
3
  isCustomerServiceMessage,
4
+ isThinkingMessage,
4
5
  isMessageInvisible,
6
+ clearChatStorage,
5
7
  } from './utils/index';
6
8
  import TUIChatEngine, { TUIChatService, TUIConversationService,IMessageModel } from '@tencentcloud/chat-uikit-engine';
7
9
  import Log from './utils/logger';
@@ -10,6 +12,7 @@ export default class TUICustomerServer {
10
12
  static isInitialized: boolean;
11
13
  static instance: TUICustomerServer;
12
14
  private customerServiceAccounts: any[];
15
+ static loggedInUserID: string;
13
16
  constructor() {
14
17
  TUICore.registerService(TUIConstants.TUICustomerServicePlugin.SERVICE.NAME, this);
15
18
  TUICore.registerExtension(TUIConstants.TUIContact.EXTENSION.CONTACT_LIST.EXT_ID, this);
@@ -25,13 +28,15 @@ export default class TUICustomerServer {
25
28
  }
26
29
 
27
30
  static loginCustomerUIKit(SDKAppID:number, userID:string, userSig:string) {
31
+ clearChatStorage(SDKAppID, userID);
28
32
  TUIChatEngine.login({
29
33
  SDKAppID,
30
34
  userID,
31
35
  userSig,
32
36
  useUploadPlugin: true,
33
37
  }).then(() => {
34
- Log.i("login success");
38
+ Log.i(`login success. userID:${userID}`);
39
+ TUICustomerServer.loggedInUserID = userID;
35
40
  TUIConversationService.switchConversation('C2C@customer_service_account');
36
41
  TUIChatEngine.chat.callExperimentalAPI('isFeatureEnabledForStat', Math.pow(2, 42));
37
42
  })
@@ -40,19 +45,26 @@ export default class TUICustomerServer {
40
45
  })
41
46
  }
42
47
 
43
- /**
44
- * init
45
- */
46
48
  public static init(SDKAppID:number, userID:string, userSig:string) {
49
+ Log.l(`TUICustomerServer.init SDKAppID:${SDKAppID} userID:${userID} isInitialized:${TUICustomerServer.isInitialized} loggedInUserID:${TUICustomerServer.loggedInUserID}`);
47
50
  // Backward compatibility, the new version executes the init operation by default in index.ts
48
51
  if (TUICustomerServer.isInitialized) {
49
- return;
52
+ if (TUICustomerServer.loggedInUserID === userID) {
53
+ return;
54
+ }
55
+ TUICustomerServer.unInit().finally(() => {
56
+ this.loginCustomerUIKit(SDKAppID, userID, userSig);
57
+ });
58
+ } else {
59
+ TUICustomerServer.isInitialized = true;
60
+ // Execute call server when native plugin TUICallKit exists
61
+ this.loginCustomerUIKit(SDKAppID, userID, userSig);
50
62
  }
51
- TUICustomerServer.isInitialized = true;
52
- // Execute call server when native plugin TUICallKit exists
53
- this.loginCustomerUIKit(SDKAppID, userID, userSig);
54
63
  }
55
64
 
65
+ public static async unInit() {
66
+ return TUIChatEngine.logout();
67
+ }
56
68
 
57
69
  // Determine if the current session is a customer service session
58
70
  public isCustomerConversation(conversationID: string) {
@@ -65,6 +77,9 @@ export default class TUICustomerServer {
65
77
  if (!message || !this.isCustomerConversation(message.conversationID)) {
66
78
  return false;
67
79
  }
80
+ if (isThinkingMessage(message)) {
81
+ return false;
82
+ }
68
83
  return isCustomerServiceMessage(message) || isMessageInvisible(message);
69
84
  }
70
85
 
package/utils/index.ts CHANGED
@@ -37,6 +37,22 @@ export const isMessageRating = (message: IMessageModel): boolean => {
37
37
  return isCustomerServiceMessage(message) && customerServicePayload.src === CUSTOM_MESSAGE_SRC.MENU;
38
38
  };
39
39
 
40
+ export const isThinkingMessage = (message: IMessageModel): boolean => {
41
+ const isCustomerMessage = message?.type === TYPES.MSG_CUSTOM;
42
+ const customerServicePayload: customerServicePayloadType = JSONToObject(message?.payload?.data);
43
+ return isCustomerMessage && customerServicePayload?.src === CUSTOM_MESSAGE_SRC.THINKING && customerServicePayload?.thinkingStatus === 0;
44
+ }
45
+
46
+ export const isThinkingMessageOverTime = (message: IMessageModel): boolean => {
47
+ const messageTime = message.time * 1000;
48
+ const minute = 60 * 1000;
49
+ const now = Date.now();
50
+ if (now - messageTime > minute) {
51
+ return false;
52
+ }
53
+ return true;
54
+ }
55
+
40
56
  export const isMessageInvisible = (message: IMessageModel): boolean => {
41
57
  const customerServicePayload: customerServicePayloadType = JSONToObject(message?.payload?.data);
42
58
  const robotCommandArray = ['feedback', 'updateBotStatus'];
@@ -50,11 +66,11 @@ export const isMessageInvisible = (message: IMessageModel): boolean => {
50
66
  CUSTOM_MESSAGE_SRC.RICH_TEXT,
51
67
  CUSTOM_MESSAGE_SRC.STREAM_TEXT,
52
68
  CUSTOM_MESSAGE_SRC.MULTI_BRANCH,
53
- CUSTOM_MESSAGE_SRC.MULTI_FORM
69
+ CUSTOM_MESSAGE_SRC.MULTI_FORM,
54
70
  ];
55
71
  const isCustomerMessage = message?.type === TYPES.MSG_CUSTOM;
56
72
  const isCustomerInvisible = customerServicePayload?.src && !whiteList.includes(customerServicePayload?.src);
57
- const isMultiFormMessage:boolean = customerServicePayload?.src !== null && customerServicePayload?.src === CUSTOM_MESSAGE_SRC.MULTI_FORM && message.flow === 'out';
73
+ const isMultiFormMessage: boolean = customerServicePayload?.src !== null && customerServicePayload?.src === CUSTOM_MESSAGE_SRC.MULTI_FORM && message.flow === 'out';
58
74
  const isRobot = customerServicePayload?.src === CUSTOM_MESSAGE_SRC.ROBOT && robotCommandArray.indexOf(customerServicePayload?.content?.command) !== -1;
59
75
  return isCustomerMessage && (isCustomerInvisible || isRobot || isMultiFormMessage);
60
76
  };
@@ -70,3 +86,22 @@ export const isSupportedLang = (lang: string): boolean => {
70
86
  'fil' // Filipino菲律宾语:fil
71
87
  ].indexOf(lang) !== -1;
72
88
  }
89
+
90
+ // 如果用户选择 block cookies,此时访问 localStorage 浏览器会抛错
91
+ // Uncaught SecurityError: Failed to read the 'localStorage' property from 'Window': Access is denied for this document
92
+ // 通过 navigator.cookieEnabled 短路逻辑规避
93
+ const canIUseCookies = () => {
94
+ // When the browser is configured to block third-party cookies, and navigator.cookieEnabled is invoked inside a third-party iframe,
95
+ // it returns true in Safari, Edge Spartan and IE (while trying to set a cookie in such scenario would fail).
96
+ // It returns false in Firefox and Chromium-based browsers.
97
+ if (typeof window !== 'undefined') {
98
+ return window.navigator?.cookieEnabled && localStorage;
99
+ }
100
+ return false;
101
+ }
102
+
103
+ export const clearChatStorage = (SDKAppID, userID) => {
104
+ if (canIUseCookies()) {
105
+ localStorage.removeItem(`TIM_${SDKAppID}_${userID}_conversationMap`);
106
+ }
107
+ }