|
|
@@ -109,6 +109,20 @@
|
|
109
|
109
|
:class="{ 'is-streaming': m.streaming }"
|
|
110
|
110
|
v-html="renderChatMarkdown(m.content)"
|
|
111
|
111
|
/>
|
|
|
112
|
+ <div
|
|
|
113
|
+ v-else-if="m.msgType === 1 && isUserMessage(m)"
|
|
|
114
|
+ class="chat-bubble__user-body"
|
|
|
115
|
+ >
|
|
|
116
|
+ <el-image
|
|
|
117
|
+ v-for="(img, ii) in userMessageImages(m)"
|
|
|
118
|
+ :key="'uimg-' + ii"
|
|
|
119
|
+ :src="mediaUrl(img)"
|
|
|
120
|
+ :preview-src-list="userMessageImages(m).map((u) => mediaUrl(u))"
|
|
|
121
|
+ fit="cover"
|
|
|
122
|
+ class="chat-bubble__img chat-bubble__img--inline"
|
|
|
123
|
+ />
|
|
|
124
|
+ <div v-if="userMessageText(m)" class="chat-bubble__text">{{ userMessageText(m) }}</div>
|
|
|
125
|
+ </div>
|
|
112
|
126
|
<div v-else-if="m.msgType === 1" class="chat-bubble__text">{{ m.content }}</div>
|
|
113
|
127
|
<el-image
|
|
114
|
128
|
v-else-if="m.msgType === 2"
|
|
|
@@ -175,7 +189,7 @@
|
|
175
|
189
|
:show-file-list="false"
|
|
176
|
190
|
accept=".jpg,.jpeg,.png,.gif"
|
|
177
|
191
|
:before-upload="(f) => beforeMediaUpload(f, 'image')"
|
|
178
|
|
- :on-success="(res, file) => onMediaUploadSuccess(res, file, 2)"
|
|
|
192
|
+ :on-success="(res, file) => onMediaUploadSuccess(res, file)"
|
|
179
|
193
|
:on-error="onUploadError"
|
|
180
|
194
|
>
|
|
181
|
195
|
<el-button
|
|
|
@@ -256,17 +270,29 @@
|
|
256
|
270
|
</button>
|
|
257
|
271
|
</el-popover>
|
|
258
|
272
|
</div>
|
|
259
|
|
- <div v-if="pendingAttachments.length" class="composer-attachments">
|
|
260
|
|
- <el-tag
|
|
|
273
|
+ <div v-if="pendingAttachments.length" class="composer-pending">
|
|
|
274
|
+ <div
|
|
261
|
275
|
v-for="(a, i) in pendingAttachments"
|
|
262
|
|
- :key="i"
|
|
263
|
|
- closable
|
|
264
|
|
- size="small"
|
|
265
|
|
- class="composer-attach-tag"
|
|
266
|
|
- @close="removeAttachment(i)"
|
|
|
276
|
+ :key="a.id || i"
|
|
|
277
|
+ class="composer-pending__item"
|
|
267
|
278
|
>
|
|
268
|
|
- {{ a.kind === "image" ? dtT("uploadImage") : dtT("uploadVideo") }} · {{ a.name }}
|
|
269
|
|
- </el-tag>
|
|
|
279
|
+ <el-image
|
|
|
280
|
+ v-if="a.kind === 'image'"
|
|
|
281
|
+ class="composer-pending__thumb"
|
|
|
282
|
+ :src="a.previewUrl || mediaUrl(a.url)"
|
|
|
283
|
+ fit="cover"
|
|
|
284
|
+ />
|
|
|
285
|
+ <div v-if="a.uploading" class="composer-pending__loading">
|
|
|
286
|
+ <i class="el-icon-loading" />
|
|
|
287
|
+ </div>
|
|
|
288
|
+ <el-button
|
|
|
289
|
+ v-if="!a.uploading"
|
|
|
290
|
+ type="text"
|
|
|
291
|
+ icon="el-icon-close"
|
|
|
292
|
+ class="composer-pending__remove"
|
|
|
293
|
+ @click="removeAttachment(i)"
|
|
|
294
|
+ />
|
|
|
295
|
+ </div>
|
|
270
|
296
|
</div>
|
|
271
|
297
|
<div class="composer-input-row">
|
|
272
|
298
|
<el-input
|
|
|
@@ -417,7 +443,14 @@ export default {
|
|
417
|
443
|
},
|
|
418
|
444
|
sendDisabled() {
|
|
419
|
445
|
const t = (this.draft || "").trim()
|
|
420
|
|
- return this.sending || (!t && !this.pendingAttachments.length)
|
|
|
446
|
+ if (this.sending) {
|
|
|
447
|
+ return true
|
|
|
448
|
+ }
|
|
|
449
|
+ if (this.pendingAttachments.some((a) => a.uploading)) {
|
|
|
450
|
+ return true
|
|
|
451
|
+ }
|
|
|
452
|
+ const hasReadyAttach = this.pendingAttachments.some((a) => a.url)
|
|
|
453
|
+ return !t && !hasReadyAttach
|
|
421
|
454
|
},
|
|
422
|
455
|
showThinking() {
|
|
423
|
456
|
if (!this.sending) {
|
|
|
@@ -498,7 +531,32 @@ export default {
|
|
498
|
531
|
this.model = value
|
|
499
|
532
|
this.modelPickerVisible = false
|
|
500
|
533
|
},
|
|
|
534
|
+ revokeAttachmentPreview(a) {
|
|
|
535
|
+ if (a && a.objectUrl) {
|
|
|
536
|
+ try {
|
|
|
537
|
+ URL.revokeObjectURL(a.objectUrl)
|
|
|
538
|
+ } catch (e) {
|
|
|
539
|
+ /* ignore */
|
|
|
540
|
+ }
|
|
|
541
|
+ }
|
|
|
542
|
+ },
|
|
|
543
|
+ setPendingAttachment(item) {
|
|
|
544
|
+ this.pendingAttachments.forEach((a) => this.revokeAttachmentPreview(a))
|
|
|
545
|
+ this.pendingAttachments = [
|
|
|
546
|
+ {
|
|
|
547
|
+ id: genLocalId(),
|
|
|
548
|
+ kind: item.kind,
|
|
|
549
|
+ name: item.name,
|
|
|
550
|
+ previewUrl: item.previewUrl,
|
|
|
551
|
+ objectUrl: item.objectUrl || "",
|
|
|
552
|
+ uploading: !!item.uploading,
|
|
|
553
|
+ url: item.url || ""
|
|
|
554
|
+ }
|
|
|
555
|
+ ]
|
|
|
556
|
+ },
|
|
501
|
557
|
removeAttachment(i) {
|
|
|
558
|
+ const a = this.pendingAttachments[i]
|
|
|
559
|
+ this.revokeAttachmentPreview(a)
|
|
502
|
560
|
this.pendingAttachments.splice(i, 1)
|
|
503
|
561
|
},
|
|
504
|
562
|
isUserMessage(m) {
|
|
|
@@ -662,7 +720,7 @@ export default {
|
|
662
|
720
|
this.hydrateLlmSessionId(session)
|
|
663
|
721
|
this.noMoreOlder = false
|
|
664
|
722
|
this.messages = []
|
|
665
|
|
- this.draft = ""
|
|
|
723
|
+ this.clearComposer()
|
|
666
|
724
|
this.loadMessages(false)
|
|
667
|
725
|
this.startPolling()
|
|
668
|
726
|
},
|
|
|
@@ -881,6 +939,16 @@ export default {
|
|
881
|
939
|
let content = m.content
|
|
882
|
940
|
if (m.msgType === 2 && content) {
|
|
883
|
941
|
content = [{ type: "image_url", image_url: { url: this.mediaUrl(content) } }]
|
|
|
942
|
+ } else if (m.msgType === 1 && this.userMessageImages(m).length) {
|
|
|
943
|
+ const parts = []
|
|
|
944
|
+ for (const u of this.userMessageImages(m)) {
|
|
|
945
|
+ parts.push({ type: "image_url", image_url: { url: this.mediaUrl(u) } })
|
|
|
946
|
+ }
|
|
|
947
|
+ const text = this.userMessageText(m)
|
|
|
948
|
+ if (text) {
|
|
|
949
|
+ parts.push({ type: "text", text })
|
|
|
950
|
+ }
|
|
|
951
|
+ content = parts.length === 1 && parts[0].type === "text" ? parts[0].text : parts
|
|
884
|
952
|
}
|
|
885
|
953
|
return { role, content: this.contentForLlm(content) }
|
|
886
|
954
|
},
|
|
|
@@ -909,9 +977,44 @@ export default {
|
|
909
|
977
|
}
|
|
910
|
978
|
},
|
|
911
|
979
|
clearComposer() {
|
|
|
980
|
+ this.pendingAttachments.forEach((a) => this.revokeAttachmentPreview(a))
|
|
912
|
981
|
this.draft = ""
|
|
913
|
982
|
this.pendingAttachments = []
|
|
914
|
983
|
},
|
|
|
984
|
+ userMessageText(m) {
|
|
|
985
|
+ if (!m) {
|
|
|
986
|
+ return ""
|
|
|
987
|
+ }
|
|
|
988
|
+ const c = m.content
|
|
|
989
|
+ if (c && typeof c === "object" && !Array.isArray(c)) {
|
|
|
990
|
+ return c.text || ""
|
|
|
991
|
+ }
|
|
|
992
|
+ if (typeof c === "string") {
|
|
|
993
|
+ return c
|
|
|
994
|
+ }
|
|
|
995
|
+ return ""
|
|
|
996
|
+ },
|
|
|
997
|
+ userMessageImages(m) {
|
|
|
998
|
+ if (!m) {
|
|
|
999
|
+ return []
|
|
|
1000
|
+ }
|
|
|
1001
|
+ const c = m.content
|
|
|
1002
|
+ if (c && typeof c === "object" && !Array.isArray(c) && Array.isArray(c.imageUrls)) {
|
|
|
1003
|
+ return c.imageUrls.filter(Boolean)
|
|
|
1004
|
+ }
|
|
|
1005
|
+ return []
|
|
|
1006
|
+ },
|
|
|
1007
|
+ resolveUserDisplayMessage(draftText, pendingSnapshot) {
|
|
|
1008
|
+ const text = (draftText || "").trim()
|
|
|
1009
|
+ const imgs = pendingSnapshot.filter((a) => a.kind === "image" && a.url).map((a) => a.url)
|
|
|
1010
|
+ if (imgs.length && text) {
|
|
|
1011
|
+ return { msgType: 1, content: { text, imageUrls: imgs } }
|
|
|
1012
|
+ }
|
|
|
1013
|
+ if (imgs.length) {
|
|
|
1014
|
+ return { msgType: 2, content: imgs[0] }
|
|
|
1015
|
+ }
|
|
|
1016
|
+ return { msgType: 1, content: text }
|
|
|
1017
|
+ },
|
|
915
|
1018
|
resolvePayloadUserContent(payload) {
|
|
916
|
1019
|
if (payload.msgType === 1) {
|
|
917
|
1020
|
return payload.content
|
|
|
@@ -1186,7 +1289,11 @@ export default {
|
|
1186
|
1289
|
},
|
|
1187
|
1290
|
sendText() {
|
|
1188
|
1291
|
const text = (this.draft || "").trim()
|
|
1189
|
|
- if (!text && !this.pendingAttachments.length) {
|
|
|
1292
|
+ if (this.pendingAttachments.some((a) => a.uploading)) {
|
|
|
1293
|
+ this.$modal.msgWarning(this.dtT("uploading"))
|
|
|
1294
|
+ return
|
|
|
1295
|
+ }
|
|
|
1296
|
+ if (!text && !this.pendingAttachments.some((a) => a.url)) {
|
|
1190
|
1297
|
this.$modal.msgWarning(this.dtT("inputOrAttach"))
|
|
1191
|
1298
|
return
|
|
1192
|
1299
|
}
|
|
|
@@ -1214,9 +1321,9 @@ export default {
|
|
1214
|
1321
|
const msgType = (payload && payload.msgType) || 1
|
|
1215
|
1322
|
const draftText = (this.draft || "").trim()
|
|
1216
|
1323
|
const pendingSnapshot = this.pendingAttachments.slice()
|
|
1217
|
|
- const displayContent = msgType !== 1 ? payload.content : userContent
|
|
1218
|
1324
|
const persistBody = this.buildPersistSendBody(msgType, payload, draftText, pendingSnapshot)
|
|
1219
|
|
- this.appendLocalMessage(SENDER_ROLE_USER, msgType, displayContent)
|
|
|
1325
|
+ const displayMsg = this.resolveUserDisplayMessage(draftText, pendingSnapshot)
|
|
|
1326
|
+ this.appendLocalMessage(SENDER_ROLE_USER, displayMsg.msgType, displayMsg.content)
|
|
1220
|
1327
|
this.clearComposer()
|
|
1221
|
1328
|
this.$nextTick(() => this.scrollChatBottom())
|
|
1222
|
1329
|
|
|
|
@@ -1325,22 +1432,46 @@ export default {
|
|
1325
|
1432
|
this.$modal.msgError(this.dtT(rule.errMb))
|
|
1326
|
1433
|
return false
|
|
1327
|
1434
|
}
|
|
1328
|
|
- this.$modal.loading(this.dtT("uploading"))
|
|
|
1435
|
+ const objectUrl = URL.createObjectURL(file)
|
|
|
1436
|
+ this.setPendingAttachment({
|
|
|
1437
|
+ kind,
|
|
|
1438
|
+ name: file.name,
|
|
|
1439
|
+ previewUrl: objectUrl,
|
|
|
1440
|
+ objectUrl,
|
|
|
1441
|
+ uploading: true,
|
|
|
1442
|
+ url: ""
|
|
|
1443
|
+ })
|
|
1329
|
1444
|
return true
|
|
1330
|
1445
|
},
|
|
1331
|
|
- onMediaUploadSuccess(res, file, msgType) {
|
|
1332
|
|
- this.$modal.closeLoading()
|
|
|
1446
|
+ onMediaUploadSuccess(res, file) {
|
|
1333
|
1447
|
if (res.code === 200 && (res.url || res.fileName)) {
|
|
1334
|
|
- this.sendViaLlm({
|
|
1335
|
|
- msgType,
|
|
1336
|
|
- content: res.url || res.fileName
|
|
1337
|
|
- })
|
|
|
1448
|
+ const url = res.url || res.fileName
|
|
|
1449
|
+ const pending = this.pendingAttachments[0]
|
|
|
1450
|
+ if (pending && pending.uploading) {
|
|
|
1451
|
+ this.$set(pending, "url", url)
|
|
|
1452
|
+ this.$set(pending, "uploading", false)
|
|
|
1453
|
+ this.$set(pending, "previewUrl", this.mediaUrl(url))
|
|
|
1454
|
+ if (pending.objectUrl) {
|
|
|
1455
|
+ this.revokeAttachmentPreview(pending)
|
|
|
1456
|
+ this.$set(pending, "objectUrl", "")
|
|
|
1457
|
+ }
|
|
|
1458
|
+ } else {
|
|
|
1459
|
+ this.setPendingAttachment({
|
|
|
1460
|
+ kind: "image",
|
|
|
1461
|
+ name: file.name,
|
|
|
1462
|
+ previewUrl: this.mediaUrl(url),
|
|
|
1463
|
+ url
|
|
|
1464
|
+ })
|
|
|
1465
|
+ }
|
|
1338
|
1466
|
} else {
|
|
|
1467
|
+ this.pendingAttachments.forEach((a) => this.revokeAttachmentPreview(a))
|
|
|
1468
|
+ this.pendingAttachments = []
|
|
1339
|
1469
|
this.$modal.msgError(res.msg || this.dtT("uploadFail"))
|
|
1340
|
1470
|
}
|
|
1341
|
1471
|
},
|
|
1342
|
1472
|
onUploadError() {
|
|
1343
|
|
- this.$modal.closeLoading()
|
|
|
1473
|
+ this.pendingAttachments.forEach((a) => this.revokeAttachmentPreview(a))
|
|
|
1474
|
+ this.pendingAttachments = []
|
|
1344
|
1475
|
this.$modal.msgError(this.dtT("uploadFail"))
|
|
1345
|
1476
|
},
|
|
1346
|
1477
|
stopVoice() {
|
|
|
@@ -1786,6 +1917,17 @@ export default {
|
|
1786
|
1917
|
border-radius: 6px;
|
|
1787
|
1918
|
}
|
|
1788
|
1919
|
|
|
|
1920
|
+.chat-bubble__user-body {
|
|
|
1921
|
+ display: flex;
|
|
|
1922
|
+ flex-direction: column;
|
|
|
1923
|
+ gap: 8px;
|
|
|
1924
|
+ min-width: 0;
|
|
|
1925
|
+}
|
|
|
1926
|
+
|
|
|
1927
|
+.chat-bubble__img--inline {
|
|
|
1928
|
+ display: block;
|
|
|
1929
|
+}
|
|
|
1930
|
+
|
|
1789
|
1931
|
.chat-bubble__video {
|
|
1790
|
1932
|
max-width: 240px;
|
|
1791
|
1933
|
max-height: 180px;
|
|
|
@@ -1954,12 +2096,55 @@ export default {
|
|
1954
|
2096
|
transition: transform 0.2s;
|
|
1955
|
2097
|
}
|
|
1956
|
2098
|
|
|
1957
|
|
-.composer-attachments {
|
|
1958
|
|
- margin-bottom: 8px;
|
|
|
2099
|
+.composer-pending {
|
|
|
2100
|
+ display: flex;
|
|
|
2101
|
+ flex-wrap: wrap;
|
|
|
2102
|
+ gap: 10px;
|
|
|
2103
|
+ margin-bottom: 10px;
|
|
|
2104
|
+}
|
|
|
2105
|
+
|
|
|
2106
|
+.composer-pending__item {
|
|
|
2107
|
+ position: relative;
|
|
|
2108
|
+ width: 72px;
|
|
|
2109
|
+ height: 72px;
|
|
|
2110
|
+ border-radius: 8px;
|
|
|
2111
|
+ overflow: hidden;
|
|
|
2112
|
+ background: #f0f2f5;
|
|
|
2113
|
+ flex-shrink: 0;
|
|
|
2114
|
+}
|
|
|
2115
|
+
|
|
|
2116
|
+.composer-pending__thumb {
|
|
|
2117
|
+ width: 100%;
|
|
|
2118
|
+ height: 100%;
|
|
|
2119
|
+ display: block;
|
|
|
2120
|
+}
|
|
|
2121
|
+
|
|
|
2122
|
+.composer-pending__loading {
|
|
|
2123
|
+ position: absolute;
|
|
|
2124
|
+ inset: 0;
|
|
|
2125
|
+ display: flex;
|
|
|
2126
|
+ align-items: center;
|
|
|
2127
|
+ justify-content: center;
|
|
|
2128
|
+ background: rgba(0, 0, 0, 0.35);
|
|
|
2129
|
+ color: #fff;
|
|
|
2130
|
+ font-size: 22px;
|
|
1959
|
2131
|
}
|
|
1960
|
2132
|
|
|
1961
|
|
-.composer-attach-tag {
|
|
1962
|
|
- margin-right: 6px;
|
|
|
2133
|
+.composer-pending__remove {
|
|
|
2134
|
+ position: absolute;
|
|
|
2135
|
+ top: 2px;
|
|
|
2136
|
+ right: 2px;
|
|
|
2137
|
+ padding: 0;
|
|
|
2138
|
+ width: 22px;
|
|
|
2139
|
+ height: 22px;
|
|
|
2140
|
+ min-height: 22px;
|
|
|
2141
|
+ border-radius: 50%;
|
|
|
2142
|
+ background: rgba(0, 0, 0, 0.45);
|
|
|
2143
|
+ color: #fff;
|
|
|
2144
|
+
|
|
|
2145
|
+ ::v-deep i {
|
|
|
2146
|
+ color: #fff;
|
|
|
2147
|
+ }
|
|
1963
|
2148
|
}
|
|
1964
|
2149
|
|
|
1965
|
2150
|
.composer-upload {
|