|
@@ -2,7 +2,7 @@
|
2
|
2
|
* @Author: wrh
|
3
|
3
|
* @Date: 2025-01-01 00:00:00
|
4
|
4
|
* @LastEditors: Please set LastEditors
|
5
|
|
- * @LastEditTime: 2025-07-30 16:06:36
|
|
5
|
+ * @LastEditTime: 2025-08-01 14:33:27
|
6
|
6
|
-->
|
7
|
7
|
<template>
|
8
|
8
|
<div class="agent-detail-container" v-loading="loading">
|
|
@@ -38,15 +38,8 @@
|
38
|
38
|
<div class="topic-time">{{ topic.createTime }}</div>
|
39
|
39
|
</div>
|
40
|
40
|
<div class="topic-actions">
|
41
|
|
- <el-button
|
42
|
|
- type="danger"
|
43
|
|
- size="small"
|
44
|
|
- :icon="Delete"
|
45
|
|
- circle
|
46
|
|
- @click.stop="handleDeleteTopic(topic)"
|
47
|
|
- class="delete-btn"
|
48
|
|
- title="删除对话"
|
49
|
|
- />
|
|
41
|
+ <el-button type="danger" size="small" :icon="Delete" circle @click.stop="handleDeleteTopic(topic)"
|
|
42
|
+ class="delete-btn" title="删除对话" />
|
50
|
43
|
</div>
|
51
|
44
|
</div>
|
52
|
45
|
</div>
|
|
@@ -127,7 +120,13 @@
|
127
|
120
|
<div class="message-content">
|
128
|
121
|
<div v-if="message.isHtml" class="message-text" v-html="message.content"></div>
|
129
|
122
|
<div v-else class="message-text">{{ message.content }}</div>
|
130
|
|
- <div class="message-time">{{ message.timestamp }}</div>
|
|
123
|
+ <div class="message-actions">
|
|
124
|
+ <span class="message-time">{{ message.timestamp }}</span>
|
|
125
|
+ <el-button v-if="message.canRetry" type="text" size="small" @click="retryMessage(message)" class="retry-btn">
|
|
126
|
+ <el-icon><Refresh /></el-icon>
|
|
127
|
+ 重新发送
|
|
128
|
+ </el-button>
|
|
129
|
+ </div>
|
131
|
130
|
</div>
|
132
|
131
|
</div>
|
133
|
132
|
|
|
@@ -149,10 +148,10 @@
|
149
|
148
|
<!-- 输入区域 -->
|
150
|
149
|
<div class="chat-input-area">
|
151
|
150
|
<el-input v-model="inputMessage" type="textarea" :autosize="{ minRows: 2, maxRows: 4 }"
|
152
|
|
- placeholder="请输入您的问题..." @keyup.ctrl.enter="sendMessage" />
|
|
151
|
+ placeholder="请输入您的问题..." @keydown.enter="handleEnterKeydown" />
|
153
|
152
|
<div class="input-actions">
|
154
|
|
- <span class="input-tip">Ctrl + Enter 发送</span>
|
155
|
|
- <el-button type="primary" @click="sendMessage" :disabled="!inputMessage.trim() || isTyping">
|
|
153
|
+ <span class="input-tip">Enter 发送,Shift + Enter 换行</span>
|
|
154
|
+ <el-button type="primary" @click="sendMessage()" :disabled="!inputMessage.trim() || isTyping">
|
156
|
155
|
发送
|
157
|
156
|
</el-button>
|
158
|
157
|
</div>
|
|
@@ -172,7 +171,7 @@
|
172
|
171
|
<script setup>
|
173
|
172
|
import { ref, reactive, watch, nextTick, computed, getCurrentInstance } from 'vue';
|
174
|
173
|
import { ElMessage, ElMessageBox } from 'element-plus';
|
175
|
|
-import { Delete } from '@element-plus/icons-vue';
|
|
174
|
+import { Delete, Refresh } from '@element-plus/icons-vue';
|
176
|
175
|
import { getAgent, opening, uploadFile } from '@/api/llm/agent';
|
177
|
176
|
import { answer } from '@/api/llm/mcp';
|
178
|
177
|
import { listTopic, getTopic, delTopic, addTopic, updateTopic } from "@/api/llm/topic";
|
|
@@ -292,7 +291,9 @@ const loadChatMessages = async (topicId) => {
|
292
|
291
|
role: 'user',
|
293
|
292
|
content: chat.input,
|
294
|
293
|
timestamp: chat.inputTime || new Date(chat.createTime).toLocaleTimeString(),
|
295
|
|
- isHtml: false
|
|
294
|
+ isHtml: false,
|
|
295
|
+ originalContent: chat.input, // 为历史消息添加原始内容
|
|
296
|
+ canRetry: false // 历史消息默认不显示重试按钮
|
296
|
297
|
})
|
297
|
298
|
}
|
298
|
299
|
// 添加助手回复消息
|
|
@@ -359,7 +360,7 @@ const handleDeleteTopic = async (topic) => {
|
359
|
360
|
|
360
|
361
|
// 调用删除API
|
361
|
362
|
await delTopic(topic.topicId)
|
362
|
|
-
|
|
363
|
+
|
363
|
364
|
// 如果删除的是当前选中的话题,则重置对话状态
|
364
|
365
|
if (currentTopicId.value === topic.topicId) {
|
365
|
366
|
currentTopicId.value = null
|
|
@@ -369,7 +370,7 @@ const handleDeleteTopic = async (topic) => {
|
369
|
370
|
|
370
|
371
|
// 重新加载话题列表
|
371
|
372
|
await loadTopic()
|
372
|
|
-
|
|
373
|
+
|
373
|
374
|
ElMessage.success('话题删除成功')
|
374
|
375
|
} catch (error) {
|
375
|
376
|
if (error !== 'cancel') {
|
|
@@ -379,9 +380,23 @@ const handleDeleteTopic = async (topic) => {
|
379
|
380
|
}
|
380
|
381
|
}
|
381
|
382
|
|
382
|
|
-// 发送消息
|
383
|
|
-const sendMessage = async () => {
|
384
|
|
- const message = inputMessage.value.trim()
|
|
383
|
+// 处理 Enter 键事件
|
|
384
|
+const handleEnterKeydown = (event) => {
|
|
385
|
+ // 如果按住 Shift 键,允许换行
|
|
386
|
+ if (event.shiftKey) {
|
|
387
|
+ return
|
|
388
|
+ }
|
|
389
|
+ // 阻止默认的换行行为
|
|
390
|
+ event.preventDefault()
|
|
391
|
+ // 发送消息
|
|
392
|
+ sendMessage()
|
|
393
|
+}
|
|
394
|
+
|
|
395
|
+// 发送消息(支持重试)
|
|
396
|
+const sendMessage = async (retryContent = null) => {
|
|
397
|
+ // 确保 retryContent 是字符串类型,如果是事件对象则忽略
|
|
398
|
+ const validRetryContent = (typeof retryContent === 'string') ? retryContent : null
|
|
399
|
+ const message = validRetryContent || inputMessage.value.trim()
|
385
|
400
|
if (!message || isTyping.value) return
|
386
|
401
|
|
387
|
402
|
// 添加用户消息
|
|
@@ -389,10 +404,17 @@ const sendMessage = async () => {
|
389
|
404
|
role: 'user',
|
390
|
405
|
content: message,
|
391
|
406
|
timestamp: new Date().toLocaleTimeString(),
|
392
|
|
- isHtml: false // 用户消息不使用HTML渲染
|
|
407
|
+ isHtml: false, // 用户消息不使用HTML渲染
|
|
408
|
+ originalContent: message, // 保存原始内容用于重试
|
|
409
|
+ canRetry: false // 初始时不显示重试按钮
|
393
|
410
|
}
|
394
|
411
|
chatMessages.value.push(userMessage)
|
395
|
|
- inputMessage.value = ''
|
|
412
|
+
|
|
413
|
+ // 只有在非重试模式下才清空输入框
|
|
414
|
+ if (!retryContent) {
|
|
415
|
+ inputMessage.value = ''
|
|
416
|
+ }
|
|
417
|
+
|
396
|
418
|
isTyping.value = true
|
397
|
419
|
|
398
|
420
|
nextTick(() => {
|
|
@@ -405,15 +427,26 @@ const sendMessage = async () => {
|
405
|
427
|
topicId: currentTopicId.value,
|
406
|
428
|
question: message
|
407
|
429
|
})
|
408
|
|
-
|
|
430
|
+ let content = JSON.parse(response.resultContent).content;
|
|
431
|
+ console.log(content);
|
|
432
|
+
|
|
433
|
+ // 检查是否是默认的失败回复
|
|
434
|
+ const defaultFailureMessage = '抱歉,我暂时无法回答这个问题。';
|
|
435
|
+ const finalContent = content || defaultFailureMessage;
|
|
436
|
+
|
409
|
437
|
// 添加助手回复
|
410
|
438
|
const assistantMessage = {
|
411
|
439
|
role: 'assistant',
|
412
|
|
- content: response.resultContent || '抱歉,我暂时无法回答这个问题。',
|
|
440
|
+ content: finalContent,
|
413
|
441
|
timestamp: new Date().toLocaleTimeString(),
|
414
|
|
- isHtml: false // 普通聊天消息不使用HTML渲染
|
|
442
|
+ isHtml: true // 普通聊天消息使用HTML渲染
|
415
|
443
|
}
|
416
|
444
|
chatMessages.value.push(assistantMessage)
|
|
445
|
+
|
|
446
|
+ // 如果是默认失败回复,为用户消息添加重试按钮
|
|
447
|
+ if (finalContent === defaultFailureMessage) {
|
|
448
|
+ userMessage.canRetry = true
|
|
449
|
+ }
|
417
|
450
|
} catch (error) {
|
418
|
451
|
console.error('发送消息失败:', error)
|
419
|
452
|
ElMessage.error('发送消息失败')
|
|
@@ -426,6 +459,9 @@ const sendMessage = async () => {
|
426
|
459
|
isHtml: false
|
427
|
460
|
}
|
428
|
461
|
chatMessages.value.push(errorMessage)
|
|
462
|
+
|
|
463
|
+ // 为用户消息添加重试按钮
|
|
464
|
+ userMessage.canRetry = true
|
429
|
465
|
} finally {
|
430
|
466
|
isTyping.value = false
|
431
|
467
|
nextTick(() => {
|
|
@@ -434,6 +470,27 @@ const sendMessage = async () => {
|
434
|
470
|
}
|
435
|
471
|
}
|
436
|
472
|
|
|
473
|
+// 重新发送消息
|
|
474
|
+const retryMessage = async (message) => {
|
|
475
|
+ // 找到当前消息在数组中的索引
|
|
476
|
+ const messageIndex = chatMessages.value.findIndex(msg => msg === message)
|
|
477
|
+ if (messageIndex === -1) return
|
|
478
|
+
|
|
479
|
+ // 获取原始消息内容
|
|
480
|
+ const originalContent = message.originalContent || message.content
|
|
481
|
+ if (!originalContent || typeof originalContent !== 'string') {
|
|
482
|
+ ElMessage.error('无法获取原始消息内容')
|
|
483
|
+ return
|
|
484
|
+ }
|
|
485
|
+
|
|
486
|
+ // 移除从当前用户消息开始的所有消息(包括后续的助手回复)
|
|
487
|
+ const messagesToKeep = chatMessages.value.slice(0, messageIndex)
|
|
488
|
+ chatMessages.value = messagesToKeep
|
|
489
|
+
|
|
490
|
+ // 重新发送原始消息
|
|
491
|
+ await sendMessage(originalContent)
|
|
492
|
+}
|
|
493
|
+
|
437
|
494
|
// 滚动到底部
|
438
|
495
|
const scrollToBottom = () => {
|
439
|
496
|
if (messagesContainer.value) {
|
|
@@ -750,7 +807,7 @@ const formatContentLinks = (content) => {
|
750
|
807
|
cursor: pointer;
|
751
|
808
|
min-width: 0; // 允许flex项目缩小到内容以下
|
752
|
809
|
max-width: calc(100% - 40px); // 为删除按钮预留40px空间
|
753
|
|
-
|
|
810
|
+
|
754
|
811
|
.topic-name {
|
755
|
812
|
font-size: 13px;
|
756
|
813
|
font-weight: 500;
|
|
@@ -786,19 +843,19 @@ const formatContentLinks = (content) => {
|
786
|
843
|
justify-content: center;
|
787
|
844
|
opacity: 0;
|
788
|
845
|
transition: opacity 0.3s ease;
|
789
|
|
-
|
|
846
|
+
|
790
|
847
|
.delete-btn {
|
791
|
848
|
width: 24px;
|
792
|
849
|
height: 24px;
|
793
|
850
|
padding: 0;
|
794
|
851
|
border: none;
|
795
|
852
|
background: #ff4757;
|
796
|
|
-
|
|
853
|
+
|
797
|
854
|
&:hover {
|
798
|
855
|
background: #ff3742;
|
799
|
856
|
transform: scale(1.1);
|
800
|
857
|
}
|
801
|
|
-
|
|
858
|
+
|
802
|
859
|
.el-icon {
|
803
|
860
|
font-size: 12px;
|
804
|
861
|
}
|
|
@@ -872,6 +929,22 @@ const formatContentLinks = (content) => {
|
872
|
929
|
background: #1890ff;
|
873
|
930
|
color: white;
|
874
|
931
|
}
|
|
932
|
+
|
|
933
|
+ .message-actions {
|
|
934
|
+ justify-content: flex-end;
|
|
935
|
+
|
|
936
|
+ .retry-btn {
|
|
937
|
+ order: -1; // 将重试按钮放在时间前面
|
|
938
|
+ margin-left: 0;
|
|
939
|
+ margin-right: 8px;
|
|
940
|
+ color: #1890ff;
|
|
941
|
+ background-color: rgba(255, 255, 255, 0.9);
|
|
942
|
+
|
|
943
|
+ &:hover {
|
|
944
|
+ background-color: white;
|
|
945
|
+ }
|
|
946
|
+ }
|
|
947
|
+ }
|
875
|
948
|
}
|
876
|
949
|
}
|
877
|
950
|
|
|
@@ -952,10 +1025,32 @@ const formatContentLinks = (content) => {
|
952
|
1025
|
}
|
953
|
1026
|
}
|
954
|
1027
|
|
955
|
|
- .message-time {
|
956
|
|
- font-size: 11px;
|
957
|
|
- color: #999;
|
|
1028
|
+ .message-actions {
|
|
1029
|
+ display: flex;
|
|
1030
|
+ align-items: center;
|
|
1031
|
+ justify-content: space-between;
|
958
|
1032
|
margin-top: 4px;
|
|
1033
|
+
|
|
1034
|
+ .message-time {
|
|
1035
|
+ font-size: 11px;
|
|
1036
|
+ color: #999;
|
|
1037
|
+ }
|
|
1038
|
+
|
|
1039
|
+ .retry-btn {
|
|
1040
|
+ font-size: 11px;
|
|
1041
|
+ color: #1890ff;
|
|
1042
|
+ padding: 2px 6px;
|
|
1043
|
+ margin-left: 8px;
|
|
1044
|
+
|
|
1045
|
+ &:hover {
|
|
1046
|
+ background-color: #f0f8ff;
|
|
1047
|
+ }
|
|
1048
|
+
|
|
1049
|
+ .el-icon {
|
|
1050
|
+ font-size: 12px;
|
|
1051
|
+ margin-right: 2px;
|
|
1052
|
+ }
|
|
1053
|
+ }
|
959
|
1054
|
}
|
960
|
1055
|
}
|
961
|
1056
|
|