Преглед изворни кода

新增功能:在智能体发送消息若出错,则可点击重新发送

余思翰 пре 1 дан
родитељ
комит
3854a66ffd
1 измењених фајлова са 128 додато и 33 уклоњено
  1. 128
    33
      llm-ui/src/views/llm/agent/AgentDetail.vue

+ 128
- 33
llm-ui/src/views/llm/agent/AgentDetail.vue Прегледај датотеку

2
  * @Author: wrh
2
  * @Author: wrh
3
  * @Date: 2025-01-01 00:00:00
3
  * @Date: 2025-01-01 00:00:00
4
  * @LastEditors: Please set LastEditors
4
  * @LastEditors: Please set LastEditors
5
- * @LastEditTime: 2025-07-30 16:06:36
5
+ * @LastEditTime: 2025-08-01 14:33:27
6
 -->
6
 -->
7
 <template>
7
 <template>
8
   <div class="agent-detail-container" v-loading="loading">
8
   <div class="agent-detail-container" v-loading="loading">
38
                 <div class="topic-time">{{ topic.createTime }}</div>
38
                 <div class="topic-time">{{ topic.createTime }}</div>
39
               </div>
39
               </div>
40
               <div class="topic-actions">
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
               </div>
43
               </div>
51
             </div>
44
             </div>
52
           </div>
45
           </div>
127
               <div class="message-content">
120
               <div class="message-content">
128
                 <div v-if="message.isHtml" class="message-text" v-html="message.content"></div>
121
                 <div v-if="message.isHtml" class="message-text" v-html="message.content"></div>
129
                 <div v-else class="message-text">{{ message.content }}</div>
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
               </div>
130
               </div>
132
             </div>
131
             </div>
133
 
132
 
149
           <!-- 输入区域 -->
148
           <!-- 输入区域 -->
150
           <div class="chat-input-area">
149
           <div class="chat-input-area">
151
             <el-input v-model="inputMessage" type="textarea" :autosize="{ minRows: 2, maxRows: 4 }"
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
             <div class="input-actions">
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
               </el-button>
156
               </el-button>
158
             </div>
157
             </div>
172
 <script setup>
171
 <script setup>
173
 import { ref, reactive, watch, nextTick, computed, getCurrentInstance } from 'vue';
172
 import { ref, reactive, watch, nextTick, computed, getCurrentInstance } from 'vue';
174
 import { ElMessage, ElMessageBox } from 'element-plus';
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
 import { getAgent, opening, uploadFile } from '@/api/llm/agent';
175
 import { getAgent, opening, uploadFile } from '@/api/llm/agent';
177
 import { answer } from '@/api/llm/mcp';
176
 import { answer } from '@/api/llm/mcp';
178
 import { listTopic, getTopic, delTopic, addTopic, updateTopic } from "@/api/llm/topic";
177
 import { listTopic, getTopic, delTopic, addTopic, updateTopic } from "@/api/llm/topic";
292
             role: 'user',
291
             role: 'user',
293
             content: chat.input,
292
             content: chat.input,
294
             timestamp: chat.inputTime || new Date(chat.createTime).toLocaleTimeString(),
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
 
360
 
360
     // 调用删除API
361
     // 调用删除API
361
     await delTopic(topic.topicId)
362
     await delTopic(topic.topicId)
362
-    
363
+
363
     // 如果删除的是当前选中的话题,则重置对话状态
364
     // 如果删除的是当前选中的话题,则重置对话状态
364
     if (currentTopicId.value === topic.topicId) {
365
     if (currentTopicId.value === topic.topicId) {
365
       currentTopicId.value = null
366
       currentTopicId.value = null
369
 
370
 
370
     // 重新加载话题列表
371
     // 重新加载话题列表
371
     await loadTopic()
372
     await loadTopic()
372
-    
373
+
373
     ElMessage.success('话题删除成功')
374
     ElMessage.success('话题删除成功')
374
   } catch (error) {
375
   } catch (error) {
375
     if (error !== 'cancel') {
376
     if (error !== 'cancel') {
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
   if (!message || isTyping.value) return
400
   if (!message || isTyping.value) return
386
 
401
 
387
   // 添加用户消息
402
   // 添加用户消息
389
     role: 'user',
404
     role: 'user',
390
     content: message,
405
     content: message,
391
     timestamp: new Date().toLocaleTimeString(),
406
     timestamp: new Date().toLocaleTimeString(),
392
-    isHtml: false // 用户消息不使用HTML渲染
407
+    isHtml: false, // 用户消息不使用HTML渲染
408
+    originalContent: message, // 保存原始内容用于重试
409
+    canRetry: false // 初始时不显示重试按钮
393
   }
410
   }
394
   chatMessages.value.push(userMessage)
411
   chatMessages.value.push(userMessage)
395
-  inputMessage.value = ''
412
+  
413
+  // 只有在非重试模式下才清空输入框
414
+  if (!retryContent) {
415
+    inputMessage.value = ''
416
+  }
417
+  
396
   isTyping.value = true
418
   isTyping.value = true
397
 
419
 
398
   nextTick(() => {
420
   nextTick(() => {
405
       topicId: currentTopicId.value,
427
       topicId: currentTopicId.value,
406
       question: message
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
     const assistantMessage = {
438
     const assistantMessage = {
411
       role: 'assistant',
439
       role: 'assistant',
412
-      content: response.resultContent || '抱歉,我暂时无法回答这个问题。',
440
+      content: finalContent,
413
       timestamp: new Date().toLocaleTimeString(),
441
       timestamp: new Date().toLocaleTimeString(),
414
-      isHtml: false // 普通聊天消息不使用HTML渲染
442
+      isHtml: true // 普通聊天消息使用HTML渲染
415
     }
443
     }
416
     chatMessages.value.push(assistantMessage)
444
     chatMessages.value.push(assistantMessage)
445
+    
446
+    // 如果是默认失败回复,为用户消息添加重试按钮
447
+    if (finalContent === defaultFailureMessage) {
448
+      userMessage.canRetry = true
449
+    }
417
   } catch (error) {
450
   } catch (error) {
418
     console.error('发送消息失败:', error)
451
     console.error('发送消息失败:', error)
419
     ElMessage.error('发送消息失败')
452
     ElMessage.error('发送消息失败')
426
       isHtml: false
459
       isHtml: false
427
     }
460
     }
428
     chatMessages.value.push(errorMessage)
461
     chatMessages.value.push(errorMessage)
462
+    
463
+    // 为用户消息添加重试按钮
464
+    userMessage.canRetry = true
429
   } finally {
465
   } finally {
430
     isTyping.value = false
466
     isTyping.value = false
431
     nextTick(() => {
467
     nextTick(() => {
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
 const scrollToBottom = () => {
495
 const scrollToBottom = () => {
439
   if (messagesContainer.value) {
496
   if (messagesContainer.value) {
750
           cursor: pointer;
807
           cursor: pointer;
751
           min-width: 0; // 允许flex项目缩小到内容以下
808
           min-width: 0; // 允许flex项目缩小到内容以下
752
           max-width: calc(100% - 40px); // 为删除按钮预留40px空间
809
           max-width: calc(100% - 40px); // 为删除按钮预留40px空间
753
-          
810
+
754
           .topic-name {
811
           .topic-name {
755
             font-size: 13px;
812
             font-size: 13px;
756
             font-weight: 500;
813
             font-weight: 500;
786
           justify-content: center;
843
           justify-content: center;
787
           opacity: 0;
844
           opacity: 0;
788
           transition: opacity 0.3s ease;
845
           transition: opacity 0.3s ease;
789
-          
846
+
790
           .delete-btn {
847
           .delete-btn {
791
             width: 24px;
848
             width: 24px;
792
             height: 24px;
849
             height: 24px;
793
             padding: 0;
850
             padding: 0;
794
             border: none;
851
             border: none;
795
             background: #ff4757;
852
             background: #ff4757;
796
-            
853
+
797
             &:hover {
854
             &:hover {
798
               background: #ff3742;
855
               background: #ff3742;
799
               transform: scale(1.1);
856
               transform: scale(1.1);
800
             }
857
             }
801
-            
858
+
802
             .el-icon {
859
             .el-icon {
803
               font-size: 12px;
860
               font-size: 12px;
804
             }
861
             }
872
               background: #1890ff;
929
               background: #1890ff;
873
               color: white;
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
             }
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
             margin-top: 4px;
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
 

Loading…
Откажи
Сачувај