@tencentcloud/ai-desk-customer-vue 1.0.1 → 1.3.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.
Files changed (43) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/assets/send_button_h5.svg +1 -0
  3. package/components/CustomerServiceChat/index-web.vue +3 -3
  4. package/components/CustomerServiceChat/message-input/index-web.vue +16 -7
  5. package/components/CustomerServiceChat/message-input/message-input-editor-web.vue +5 -2
  6. package/components/CustomerServiceChat/message-list/index-web.vue +6 -0
  7. package/components/CustomerServiceChat/message-list/message-elements/message-bubble-web.vue +14 -15
  8. package/components/CustomerServiceChat/message-list/message-elements/message-desk/message-desk-elements/marked.ts +1 -1
  9. package/components/CustomerServiceChat/message-list/message-elements/message-desk/message-desk-elements/message-branch.vue +25 -6
  10. package/components/CustomerServiceChat/message-list/message-elements/message-desk/message-desk-elements/message-concurrency-limit.vue +40 -0
  11. package/components/CustomerServiceChat/message-list/message-elements/message-desk/message-desk-elements/message-desk.vue +32 -8
  12. package/components/CustomerServiceChat/message-list/message-elements/message-desk/message-desk-elements/message-multi-branch/branch-pc.vue +93 -73
  13. package/components/CustomerServiceChat/message-list/message-elements/message-desk/message-desk-elements/message-multi-branch/index.vue +53 -52
  14. package/components/CustomerServiceChat/message-list/message-elements/message-desk/message-desk-elements/message-multi-form/component-mobile/input-mobile.vue +73 -80
  15. package/components/CustomerServiceChat/message-list/message-elements/message-desk/message-desk-elements/message-multi-form/component-mobile/label-mobile.vue +21 -24
  16. package/components/CustomerServiceChat/message-list/message-elements/message-desk/message-desk-elements/message-multi-form/component-mobile/radios-mobile.vue +115 -116
  17. package/components/CustomerServiceChat/message-list/message-elements/message-desk/message-desk-elements/message-multi-form/component-pc/input-pc.vue +69 -73
  18. package/components/CustomerServiceChat/message-list/message-elements/message-desk/message-desk-elements/message-multi-form/component-pc/label-pc.vue +21 -25
  19. package/components/CustomerServiceChat/message-list/message-elements/message-desk/message-desk-elements/message-multi-form/component-pc/radio-pc.vue +87 -77
  20. package/components/CustomerServiceChat/message-list/message-elements/message-desk/message-desk-elements/message-multi-form/form-mobile.vue +213 -200
  21. package/components/CustomerServiceChat/message-list/message-elements/message-desk/message-desk-elements/message-multi-form/form-pc.vue +122 -113
  22. package/components/CustomerServiceChat/message-list/message-elements/message-desk/message-desk-elements/message-multi-form/index.vue +7 -7
  23. package/components/CustomerServiceChat/message-list/message-elements/message-desk/message-desk-elements/message-order.vue +142 -0
  24. package/components/CustomerServiceChat/message-list/message-elements/message-desk/message-desk-elements/message-rating/message-rating-number.vue +2 -1
  25. package/components/CustomerServiceChat/message-list/message-elements/message-desk/message-desk-elements/message-rating/message-rating-star.vue +2 -1
  26. package/components/CustomerServiceChat/message-list/message-elements/message-desk/message-desk-elements/message-rich-text.vue +8 -6
  27. package/components/CustomerServiceChat/message-list/message-elements/message-desk/message-desk-elements/message-stream.vue +89 -6
  28. package/components/CustomerServiceChat/message-list/message-elements/message-desk/message-plugin-web.vue +5 -0
  29. package/components/CustomerServiceChat/message-list/scroll-button/index.vue +15 -3
  30. package/components/common/BottomPopup/index.vue +1 -1
  31. package/constant.ts +10 -4
  32. package/locales/en/aidesk.ts +3 -1
  33. package/locales/fil/aidesk.ts +3 -1
  34. package/locales/id/aidesk.ts +4 -2
  35. package/locales/ja/aidesk.ts +4 -2
  36. package/locales/ms/aidesk.ts +4 -2
  37. package/locales/ru/aidesk.ts +5 -3
  38. package/locales/th/aidesk.ts +3 -1
  39. package/locales/vi/aidesk.ts +4 -2
  40. package/locales/zh_cn/aidesk.ts +4 -3
  41. package/locales/zh_tw/aidesk.ts +3 -1
  42. package/package.json +1 -1
  43. package/server.ts +53 -16
@@ -1,39 +1,37 @@
1
1
  <template>
2
- <div>
2
+ <div class="form-pc-container">
3
3
  <div class="title-container">
4
- <div class="form-title" v-if="checkTip()">
5
- {{ props.payload.content.tip }}
6
- </div>
7
- <div class="form-finish-title-right" v-if="finishSubmit || props.payload.nodeStatus == 2" >
8
- <Icon :src="iconSucess" style="margin:0px 4px"/>
9
- {{TUITranslateService.t("AIDesk.已提交")}}
10
- </div>
4
+ <div class="form-title" v-if="checkTip()">
5
+ {{ props.payload.content.tip }}
6
+ </div>
7
+ <div class="form-finish-title-right" v-if="finishSubmit || props.payload.nodeStatus == 2" >
8
+ <Icon :src="iconSucess" style="margin:0px 4px"/>
9
+ {{ TUITranslateService.t("AIDesk.已提交") }}
10
+ </div>
11
11
  </div>
12
-
12
+
13
13
  <div
14
- v-for="(item, index) in props.payload.content.inputVariables"
15
- :key="index"
14
+ v-for="(item, index) in props.payload.content.inputVariables"
15
+ :key="index"
16
16
  >
17
- <LabelPC :name="item.name" :is-required="item.isRequired" />
18
- <div v-if="!finishSubmit && item.formType == 0 && props.payload.nodeStatus == 0">
19
- <InputPC :placeholder="item.placeholder" :variableValue="item.variableValue" :name="item.name" :isRequired="item.isRequired" @input-change="handleInputChange" :validator="item.isRequired == 1 && isValid(item.name)"/>
20
- </div>
21
- <div v-else-if="!finishSubmit && item.formType == 1 && props.payload.nodeStatus == 0">
22
- <RadioPC :chooseItemList="item.chooseItemList" :name="item.name" :isRequired="item.isRequired" @input-change="handleInputChange" :validator="item.isRequired == 1 && isValid(item.name)"/>
23
- </div>
24
- <div v-else class="variable-value-container">
25
- {{ item.variableValue == '' || item.variableValue == null ? mapValue[item.name] : item.variableValue}}
26
- </div>
17
+ <LabelPC :name="item.name" :is-required="item.isRequired" />
18
+ <div v-if="!finishSubmit && item.formType == 0 && props.payload.nodeStatus !== 2">
19
+ <InputPC :placeholder="item.placeholder" :variableValue="item.variableValue" :name="item.name" :isRequired="item.isRequired" @input-change="handleInputChange" :validator="item.isRequired == 1 && isValid(item.name)" :isDisabled="props.payload.nodeStatus === 1"/>
20
+ </div>
21
+ <div v-else-if="!finishSubmit && item.formType == 1 && props.payload.nodeStatus !== 2">
22
+ <RadioPC :chooseItemList="item.chooseItemList" :name="item.name" :isRequired="item.isRequired" @input-change="handleInputChange" :validator="item.isRequired == 1 && isValid(item.name)" :isDisabled="props.payload.nodeStatus === 1"/>
23
+ </div>
24
+ <div v-else class="variable-value-container">
25
+ {{ item.variableValue == '' || item.variableValue == null ? mapValue[item.name] : item.variableValue}}
26
+ </div>
27
27
  </div>
28
-
29
- <div class="button-container"v-if="!finishSubmit && props.payload.nodeStatus == 0">
30
- <div class="button" @click="handleSendForm">
31
- {{TUITranslateService.t("AIDesk.提交")}}
32
- </div>
33
-
28
+ <div class="button-container"v-if="!finishSubmit && props.payload.nodeStatus !== 2">
29
+ <div :class="props.payload.nodeStatus === 1 ? 'button-disable' : 'button'" @click="handleSendForm">
30
+ {{ TUITranslateService.t("AIDesk.提交") }}
31
+ </div>
34
32
  </div>
35
33
  </div>
36
-
34
+
37
35
  </template>
38
36
  <script lang="ts">
39
37
  import vue from '../../../../../../../adapter-vue';
@@ -50,17 +48,17 @@ interface Props {
50
48
  payload: customerServicePayloadType;
51
49
  }
52
50
  export default {
53
- components: {
54
- LabelPC,
55
- InputPC,
56
- RadioPC,
57
- Icon
58
- },
51
+ components: {
52
+ LabelPC,
53
+ InputPC,
54
+ RadioPC,
55
+ Icon
56
+ },
59
57
  props: {
60
- payload: {
61
- type: Object as () => customerServicePayloadType,
62
- default: () => ({}),
63
- },
58
+ payload: {
59
+ type: Object as () => customerServicePayloadType,
60
+ default: () => ({}),
61
+ },
64
62
  },
65
63
  emits: ['sendMessage'],
66
64
  setup(props: Props, { emit }) {
@@ -69,73 +67,71 @@ export default {
69
67
  const finishSubmit = ref<boolean>(false);
70
68
  const hasNullValue = ref<boolean>(true);
71
69
  onMounted(()=>{
72
- let inputVariables = props.payload.content.inputVariables ?? [];
73
- for(let i=0;i<inputVariables.length;i++){
74
- const name = inputVariables[i].name;
75
- const variableValue = inputVariables[i].variableValue;
76
- mapValue.value[name]=variableValue;
77
- }
70
+ let inputVariables = props.payload.content.inputVariables ?? [];
71
+ for (let i = 0; i < inputVariables.length; i++) {
72
+ const name = inputVariables[i].name;
73
+ const variableValue = inputVariables[i].variableValue;
74
+ mapValue.value[name] = variableValue;
75
+ }
78
76
  });
79
-
80
- const checkTip = () => {
81
- return props.payload.content.tip != '' && props.payload.content.tip != null;
82
- }
83
77
 
78
+ const checkTip = () => {
79
+ return props.payload.content.tip != '' && props.payload.content.tip != null;
80
+ }
84
81
 
85
82
  const checkValidator = (name:string) => {
86
- hasNullValue.value = false;
87
- if(isSubmit.value == true){
88
- if(mapValue.value[name] == null || mapValue.value[name] == ''){
89
- hasNullValue.value = true;
90
- return true;
91
-
92
- }
83
+ hasNullValue.value = false;
84
+ if (isSubmit.value == true) {
85
+ if (mapValue.value[name] == null || mapValue.value[name] == '') {
86
+ hasNullValue.value = true;
87
+ return true;
93
88
  }
94
- return false;
89
+ }
90
+ return false;
95
91
  }
96
92
 
97
-
98
-
99
93
  const handleSendForm = (data: any) => {
100
- isSubmit.value = true;
101
- let list = props.payload.content.inputVariables;
102
- for(let i=0;i<list.length;i++){
103
- let value = mapValue.value[list[i].name];
104
- if(value !='' && value != null){
105
- list[i].variableValue = value;
106
- }else {
107
- if(list[i].isRequired === 1 && checkValidator(list[i].name)){
108
- return;
109
- }
110
- }
94
+ if (props.payload.nodeStatus === 1) {
95
+ return;
96
+ }
97
+ isSubmit.value = true;
98
+ let list = props.payload.content.inputVariables;
99
+ for (let i = 0; i < list.length; i++) {
100
+ let value = mapValue.value[list[i].name];
101
+ if (value !='' && value != null) {
102
+ list[i].variableValue = value;
103
+ } else {
104
+ if (list[i].isRequired === 1 && checkValidator(list[i].name)) {
105
+ return;
106
+ }
111
107
  }
112
- const submitData = {
113
- data: JSON.stringify({
114
- src: CUSTOM_MESSAGE_SRC.MULTI_FORM,
115
- content: {
116
- inputVariables: list
117
- },
118
- customerServicePlugin: 0,
108
+ }
109
+ const submitData = {
110
+ data: JSON.stringify({
111
+ src: CUSTOM_MESSAGE_SRC.MULTI_FORM,
112
+ content: {
113
+ inputVariables: list
114
+ },
115
+ customerServicePlugin: 0,
119
116
  }),
120
- };
121
-
122
- emit('sendMessage', submitData);
123
- finishSubmit.value = true;
124
-
125
- isSubmit.value = false;
117
+ };
118
+ emit('sendMessage', submitData);
119
+ finishSubmit.value = true;
120
+ isSubmit.value = false;
126
121
  };
122
+
127
123
  const handleInputChange = ({name,value}) =>{
128
- mapValue.value[name] = value;
124
+ mapValue.value[name] = value;
129
125
  }
130
126
 
131
127
  const showValue = (name:string,variableValue:string) => {
132
- if(variableValue != null && variableValue != ''){
133
- return variableValue
134
- }
135
- return mapValue.value[name];
128
+ if (variableValue != null && variableValue != '') {
129
+ return variableValue
130
+ }
131
+ return mapValue.value[name];
136
132
  }
137
133
  const isValid = (name:string) => {
138
- return isSubmit.value && (mapValue.value[name] == null || mapValue.value[name] == '' || mapValue.value[name] == undefined)
134
+ return isSubmit.value && (mapValue.value[name] == null || mapValue.value[name] == '' || mapValue.value[name] == undefined);
139
135
  }
140
136
  return {
141
137
  props,
@@ -147,7 +143,7 @@ export default {
147
143
  showValue,
148
144
  mapValue,
149
145
  isSubmit,
150
- checkTip,
146
+ checkTip,
151
147
  isValid,
152
148
  TUITranslateService
153
149
  };
@@ -155,32 +151,45 @@ export default {
155
151
  };
156
152
  </script>
157
153
  <style lang="scss">
158
- .title-container{
159
- display: flex;
160
- justify-content: space-between;
161
- align-items: center;
162
- margin-bottom: 12px;
163
- min-width: 300px;
154
+ .form-pc-container {
155
+ font-family: PingFangSC-Regular;
156
+ max-width: 350px;
157
+ }
158
+ .title-container {
159
+ display: flex;
160
+ justify-content: space-between;
161
+ align-items: center;
162
+ margin-bottom: 12px;
164
163
  }
165
164
  .form-finish-title-right {
166
- display:flex;
167
- }
168
- .button-container{
169
- display:flex;
165
+ display: flex;
166
+ }
167
+ .button-container {
168
+ display: flex;
169
+ justify-content: center;
170
+ margin-top: 15px;
171
+ .button {
172
+ display: flex;
170
173
  justify-content: center;
171
- margin-top:15px;
172
- .button {
173
- display:flex;
174
- justify-content: center;
175
- width: 87px;
176
- padding:6px 10px;
177
- background-color: #1c66e5;
178
- color:white;
179
- border-radius:25px ;
180
- }
174
+ width: 87px;
175
+ padding: 6px 10px;
176
+ background-color: #1c66e5;
177
+ color: white;
178
+ border-radius: 20px;
179
+ cursor: pointer;
180
+ }
181
+ .button-disable {
182
+ background-color: #dbdbdb;
183
+ display: flex;
184
+ justify-content: center;
185
+ width: 87px;
186
+ padding: 6px 10px;
187
+ color: white;
188
+ border-radius: 20px;
189
+ }
181
190
  }
182
- .variable-value-container{
183
- padding-bottom: 8px;
184
- margin-top:3px;
191
+ .variable-value-container {
192
+ padding-bottom: 8px;
193
+ margin-top: 3px;
185
194
  }
186
- </style>
195
+ </style>
@@ -1,10 +1,10 @@
1
1
  <template>
2
- <div v-if="isPC" class="message-input">
3
- <FormPC :payload="payloads" @sendMessage="handleSendForm" />
4
- </div>
5
- <div v-else>
6
- <FormMobile :payload="payloads" @sendMessage="handleSendForm" @showFormPopup="handleShowFormPopup"/>
7
- </div>
2
+ <div v-if="isPC" class="message-input">
3
+ <FormPC :payload="payloads" @sendMessage="handleSendForm" />
4
+ </div>
5
+ <div v-else>
6
+ <FormMobile :payload="payloads" @sendMessage="handleSendForm" @showFormPopup="handleShowFormPopup"/>
7
+ </div>
8
8
  </template>
9
9
 
10
10
  <script lang="ts">
@@ -13,7 +13,7 @@ import FormPC from './form-pc.vue';
13
13
  import FormMobile from './form-mobile.vue';
14
14
  import { customerServicePayloadType } from '../../../../../../../interface';
15
15
  import { isPC } from '../../../../../../../utils/env';
16
- const { computed} = vue;
16
+ const { computed } = vue;
17
17
 
18
18
  interface branchItem {
19
19
  content: string;
@@ -0,0 +1,142 @@
1
+ <template>
2
+ <div
3
+ class="message-order"
4
+ >
5
+ <div class="order-guide">
6
+ {{ props.payload.content.guide }}
7
+ </div>
8
+ <div class="order-main">
9
+ <img
10
+ v-if="props.payload.content.pic"
11
+ class="order-img"
12
+ :src="props.payload.content.pic"
13
+ >
14
+ <div class="order-information">
15
+ <div class="order-name">
16
+ {{ props.payload.content.name }}
17
+ </div>
18
+ <div class="order-description">
19
+ {{ props.payload.content.desc }}
20
+ </div>
21
+ </div>
22
+ </div>
23
+ <div class="order-custom" v-for="item in props.payload.content.customField">
24
+ <div class="order-field">
25
+ <span class="field-name"> {{ item.name }} </span>
26
+ <span class="field-value"> {{ item.value }} </span>
27
+ <span class="field-customer-value"> {{ item.customerValue }} </span>
28
+ </div>
29
+ </div>
30
+ </div>
31
+ </template>
32
+
33
+ <script lang="ts">
34
+ import { customerServicePayloadType } from '../../../../../../interface';
35
+
36
+ interface Props {
37
+ payload: customerServicePayloadType;
38
+ }
39
+
40
+ export default {
41
+ props: {
42
+ payload: {
43
+ type: Object as () => customerServicePayloadType,
44
+ default: () => ({}),
45
+ },
46
+ },
47
+ setup(props: Props) {
48
+ return {
49
+ props,
50
+ };
51
+ },
52
+ };
53
+ </script>
54
+ <style lang="scss" scoped>
55
+ .message-order {
56
+ min-width: 200px;
57
+ max-width: 400px;
58
+ color: #000;
59
+ font-family: PingFangSC-Regular;
60
+
61
+ .order-guide {
62
+ font-size: 14px;
63
+ margin-bottom: 10px;
64
+ overflow: hidden;
65
+ text-overflow: ellipsis;
66
+ white-space: nowrap;
67
+ }
68
+
69
+ .order-main {
70
+ display:flex;
71
+
72
+ .order-img {
73
+ width: 65px;
74
+ height: 65px;
75
+ border-radius: 10px;
76
+ flex-shrink: 0;
77
+ object-fit: cover;
78
+ margin-right: 15px;
79
+ }
80
+
81
+ .order-information {
82
+ width:100%;
83
+ margin-right:5px;
84
+ display: flex;
85
+ flex-direction: column;
86
+ justify-content: space-between;
87
+
88
+ .order-name {
89
+ max-width: 200px;
90
+ min-width: 100px;
91
+ color: #000000;
92
+ font-size: 14px;
93
+ display: -webkit-box;
94
+ overflow: hidden;
95
+ text-overflow: ellipsis;
96
+ -webkit-line-clamp: 2;
97
+ -webkit-box-orient: vertical;
98
+ word-break: break-all;
99
+ }
100
+
101
+ .order-description {
102
+ font-size: 14px;
103
+ max-width: 200px;
104
+ min-width: 100px;
105
+ color: rgba(0, 0, 0, 0.55);
106
+ overflow: hidden;
107
+ text-overflow: ellipsis;
108
+ white-space: nowrap;
109
+ font-weight: 600;
110
+ }
111
+ }
112
+ }
113
+
114
+ .order-field {
115
+ font-size: 12px;
116
+ font-weight: 500;
117
+ margin-top: 5px;
118
+ display: flex;
119
+ gap: 8px;
120
+
121
+ .field-name {
122
+ color: rgba(0, 0, 0, 0.55);
123
+ flex: 0 0 auto;
124
+ width: 70px;
125
+ white-space: nowrap;
126
+ overflow: hidden;
127
+ text-overflow: ellipsis;
128
+ }
129
+
130
+ .field-value {
131
+ color: #333;
132
+ font-weight: 500;
133
+ white-space: nowrap;
134
+ overflow: hidden;
135
+ text-overflow: ellipsis;
136
+ flex: 1;
137
+ min-width: 0;
138
+ padding-left: 4px;
139
+ }
140
+ }
141
+ }
142
+ </style>
@@ -204,13 +204,14 @@ export default {
204
204
 
205
205
  .submit-button {
206
206
  height: 50px;
207
- background-color: #0365f9;
207
+ background-color: #1c66e5;
208
208
  font-size: 18px;
209
209
  font-weight: 400;
210
210
  color: white;
211
211
  border: 0;
212
212
  border-radius: 8px;
213
213
  cursor: pointer;
214
+ width: 62%;
214
215
  }
215
216
 
216
217
  .de-active {
@@ -225,13 +225,14 @@ export default {
225
225
 
226
226
  .submit-button {
227
227
  height: 35px;
228
- background-color: #0365f9;
228
+ background-color: #1c66e5;
229
229
  font-size: 14px;
230
230
  font-weight: 400;
231
231
  color: white;
232
232
  border: 0;
233
233
  border-radius: 8px;
234
234
  cursor: pointer;
235
+ width: 62%;
235
236
  }
236
237
 
237
238
  </style>
@@ -35,7 +35,7 @@ export default {
35
35
  default: () => ({}),
36
36
  },
37
37
  },
38
- setup(props: Props) {
38
+ setup(props: Props, { emit }) {
39
39
  const image = ref(false);
40
40
  const imageSrc = ref('');
41
41
  const imageList = [];
@@ -45,6 +45,10 @@ export default {
45
45
  image.value = !image.value;
46
46
  imageSrc.value = decodeURIComponent(href);
47
47
  }
48
+ // @ts-ignore
49
+ window.onMarkdownImageLoad = function() {
50
+ emit('heightChanged');
51
+ }
48
52
  return parseMarkdown(props.payload.content);
49
53
  });
50
54
 
@@ -70,7 +74,7 @@ export default {
70
74
  z-index: 101;
71
75
  width: 100vw;
72
76
  height: 100vh;
73
- background: rgba(#000, 0.3);
77
+ background: rgba(#000, 0.8);
74
78
  top: 0;
75
79
  left: 0;
76
80
  display: flex;
@@ -81,14 +85,12 @@ export default {
81
85
  }
82
86
 
83
87
  .rich-image-preview {
84
- width: 80%;
85
- height:auto;
88
+ max-width: 90%;
89
+ height: auto;
86
90
  display: flex;
87
91
  align-items: center;
88
92
  justify-content: center;
89
93
  overflow: hidden;
90
- transition: transform 0.1s ease 0s;
91
94
  pointer-events: auto;
92
95
  }
93
-
94
96
  </style>
@@ -1,6 +1,13 @@
1
1
  <template>
2
2
  <div class="message-stream">
3
- <pre :class="['message-marked']" v-html="displayedContent" />
3
+ <span v-if="isCursorBlinking" class="blinking-cursor">|</span>
4
+ <pre ref="preRef" :class="['message-marked']" v-html="displayedContent" />
5
+ </div>
6
+ <div v-if="image" class="rich-image-previewer" @click="closeImage">
7
+ <img
8
+ class="rich-image-preview"
9
+ :src="imageSrc"
10
+ />
4
11
  </div>
5
12
  </template>
6
13
 
@@ -11,7 +18,7 @@ import { parseMarkdown } from './marked'
11
18
  import { TypeWriter } from "./type-writer";
12
19
  import { JSONToObject } from "../../../../../../utils";
13
20
 
14
- const { ref, computed, withDefaults, defineProps, watch } = vue;
21
+ const { ref, computed, withDefaults, defineProps, watch, onMounted, onUnmounted } = vue;
15
22
 
16
23
  interface Props {
17
24
  payload: customerServicePayloadType;
@@ -21,12 +28,34 @@ const props = withDefaults(defineProps<Props>(), {
21
28
  payload: () => '',
22
29
  });
23
30
 
31
+ const isCursorBlinking = ref<boolean>(true);
24
32
  const isStreaming = ref<boolean>(false);
33
+ const image = ref(false);
34
+ const imageSrc = ref('');
25
35
  const chunks = ref<string>('');
26
36
  const isFinished = ref<boolean>(true);
27
37
  const prevChunksLength = ref<number>(0);
28
38
  const streamContent = ref<string>('');
29
- const displayedContent = computed(() => parseMarkdown(streamContent.value));
39
+ const displayedContent = computed(() => {
40
+ // @ts-ignore
41
+ window.onMarkdownImageClicked = function(href:string) {
42
+ image.value = !image.value;
43
+ imageSrc.value = decodeURIComponent(href);
44
+ }
45
+ // @ts-ignore
46
+ window.onMarkdownImageLoad = function() {
47
+ // empty implementation
48
+ // 已经用 ResizeObserver 观测高度,这里不用重复通知高度变化
49
+ }
50
+ return parseMarkdown(streamContent.value);
51
+ });
52
+ const preRef = ref();
53
+ const emits = defineEmits(['heightChanged']);
54
+ // 不支持 ResizeObserver 的浏览器太老旧,默认不处理了
55
+ const canIUseResizeObserver = typeof ResizeObserver === 'undefined' ? false : true;
56
+
57
+ let observer;
58
+ let prevHeight;
30
59
 
31
60
  const typeWriter = new TypeWriter({
32
61
  onTyping: (item: string) => {
@@ -56,19 +85,63 @@ watch(() => props.payload, (newValue: string, oldValue: string) => {
56
85
  const _payloadObject = JSONToObject(props.payload);
57
86
  chunks.value = Array.isArray(_payloadObject.chunks) ? _payloadObject.chunks.join('') : _payloadObject.chunks;
58
87
  isFinished.value = _payloadObject.isFinished === 1;
88
+
89
+ // hide blinking cursor
90
+ if (chunks.value.length > 0) {
91
+ isCursorBlinking.value = false;
92
+ }
93
+
59
94
  if (newValue && !oldValue && isFinished.value) {
60
95
  // disable typeWriter style or history message first load
61
96
  streamContent.value = chunks.value;
97
+ prevChunksLength.value = chunks.value.length;
62
98
  } else {
63
- const _newChunksToAdd = chunks.value?.slice(prevChunksLength.value);
64
- startStreaming([_newChunksToAdd]);
99
+ // 判断长度是为了防御编辑的内容回退和内容重复的异常 case
100
+ if (chunks.value.length > prevChunksLength.value) {
101
+ const _newChunksToAdd = chunks.value?.slice(prevChunksLength.value);
102
+ prevChunksLength.value = chunks.value.length;
103
+ startStreaming([_newChunksToAdd]);
104
+ }
65
105
  }
66
- prevChunksLength.value = chunks.value?.length;
67
106
  }, {
68
107
  deep: true,
69
108
  immediate: true,
70
109
  },
71
110
  );
111
+
112
+ onMounted(() => {
113
+ if (canIUseResizeObserver) {
114
+ observer = new ResizeObserver(entries => {
115
+ for (let entry of entries) {
116
+ observeHeightChanged(entry.contentRect.height);
117
+ }
118
+ });
119
+ // 开始观察
120
+ observer.observe(preRef.value);
121
+ }
122
+ });
123
+
124
+ onUnmounted(() => {
125
+ if (canIUseResizeObserver) {
126
+ if (observer) {
127
+ observer.disconnect();
128
+ observer = null;
129
+ }
130
+ }
131
+ });
132
+
133
+ const observeHeightChanged = (newHeight) => {
134
+ if (prevHeight !== newHeight) {
135
+ prevHeight = newHeight;
136
+ emits('heightChanged');
137
+ }
138
+ };
139
+
140
+ const closeImage = () => {
141
+ image.value = !image.value;
142
+ imageSrc.value = '';
143
+ };
144
+
72
145
  </script>
73
146
  <style lang="scss" scoped>
74
147
  .message-stream {
@@ -77,4 +150,14 @@ watch(() => props.payload, (newValue: string, oldValue: string) => {
77
150
  white-space: normal;
78
151
  font-size: 14px;
79
152
  }
153
+
154
+ .blinking-cursor {
155
+ animation: blink 0.8s step-end infinite;
156
+ color: #000;
157
+ }
158
+
159
+ @keyframes blink {
160
+ from, to { opacity: 1; }
161
+ 50% { opacity: 0; }
162
+ }
80
163
  </style>