@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.
- package/assets/loading_message.svg +1 -0
- package/components/CustomerServiceChat/index-web.vue +7 -1
- package/components/CustomerServiceChat/message-list/index-web.vue +16 -7
- package/components/CustomerServiceChat/message-list/message-elements/message-desk/message-desk-elements/message-stream.vue +51 -52
- package/components/CustomerServiceChat/message-list/message-elements/message-desk/message-desk-elements/styles/common.scss +1 -1
- package/components/CustomerServiceChat/message-list/message-elements/message-desk/message-desk-elements/type-writer.ts +189 -0
- package/components/CustomerServiceChat/message-list/message-elements/message-thinking.vue +59 -0
- package/components/CustomerServiceChat/message-toolbar-button/index.vue +69 -0
- package/constant.ts +1 -0
- package/interface.ts +8 -0
- package/package.json +1 -1
- package/server.ts +23 -8
- package/utils/index.ts +37 -2
|
@@ -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 }
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
14
|
+
const { ref, computed, withDefaults, defineProps, watch } = vue;
|
|
17
15
|
|
|
18
16
|
interface Props {
|
|
19
17
|
payload: customerServicePayloadType;
|
|
20
18
|
}
|
|
21
19
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
30
|
-
const displayedContent = ref<string>('');
|
|
31
|
-
const isFinished = ref<boolean>(false);
|
|
32
|
-
let currentIndex = 0;
|
|
39
|
+
});
|
|
33
40
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
isFinished
|
|
55
|
-
|
|
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>
|
|
@@ -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
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
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(
|
|
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
|
-
|
|
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 =
|
|
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
|
+
}
|