Procházet zdrojové kódy

新增知识库对话界面

余思翰 před 2 dny
rodič
revize
1dabd105f3

+ 7
- 1
llm-back/ruoyi-llm/src/main/java/com/ruoyi/web/llm/controller/RagController.java Zobrazit soubor

@@ -1,3 +1,9 @@
1
+/*
2
+ * @Author: ysh
3
+ * @Date: 2025-07-08 15:10:42
4
+ * @LastEditors: 
5
+ * @LastEditTime: 2025-07-09 11:37:02
6
+ */
1 7
 package com.ruoyi.web.llm.controller;
2 8
 
3 9
 import com.ruoyi.web.llm.service.ILangChainMilvusService;
@@ -43,7 +49,7 @@ public class RagController extends BaseController
43 49
     public AjaxResult answer(String question, String collectionName) throws IOException {
44 50
 
45 51
         List<String> contexts = langChainMilvusService.retrieveFromMilvus(milvusClient, embeddingModel, collectionName, question, 1);
46
-        String result = langChainMilvusService.generateAnswerWithRag(question, contexts, "http://192.168.28.188:8080/generate");
52
+        String result = langChainMilvusService.generateAnswerWithRag(question, contexts, "http://192.168.28.188:8000/generate");
47 53
         return success(result);
48 54
     }
49 55
 

+ 1
- 1
llm-back/ruoyi-llm/src/main/java/com/ruoyi/web/llm/service/ILangChainMilvusService.java Zobrazit soubor

@@ -29,5 +29,5 @@ public interface ILangChainMilvusService {
29 29
     /**
30 30
      * 调用LLM生成回答
31 31
      */
32
-    public String generateAnswer(String llmServiceUrl, String question) throws IOException;
32
+    public String generateAnswer(String question, String llmServiceUrl) throws IOException;
33 33
 }

+ 8
- 2
llm-back/ruoyi-llm/src/main/java/com/ruoyi/web/llm/service/impl/LangChainMilvusServiceImpl.java Zobrazit soubor

@@ -1,3 +1,9 @@
1
+/*
2
+ * @Author: ysh
3
+ * @Date: 2025-07-08 15:10:42
4
+ * @LastEditors: Please set LastEditors
5
+ * @LastEditTime: 2025-07-09 11:39:18
6
+ */
1 7
 package com.ruoyi.web.llm.service.impl;
2 8
 
3 9
 import com.alibaba.fastjson2.JSONObject;
@@ -157,14 +163,14 @@ public class LangChainMilvusServiceImpl implements ILangChainMilvusService
157 163
         }
158 164
         sb.append("问题: ").append(question).append("\n回答: ");
159 165
         // 构建带动态参数的URL
160
-        return generateAnswer(llmServiceUrl, sb.toString());
166
+        return generateAnswer(sb.toString(),llmServiceUrl);
161 167
     }
162 168
 
163 169
     /**
164 170
      * 调用LLM生成回答
165 171
      */
166 172
     @Override
167
-    public String generateAnswer(String llmServiceUrl, String prompt) throws IOException {
173
+    public String generateAnswer(String prompt, String llmServiceUrl) throws IOException {
168 174
         HttpUrl url = HttpUrl.parse(llmServiceUrl)
169 175
                 .newBuilder()
170 176
                 .addQueryParameter("prompt", prompt)

+ 1
- 2
llm-ui/src/api/llm/knowLedge.js Zobrazit soubor

@@ -66,7 +66,6 @@ export function listKnowledgeDocument(collectionName, fileType) {
66 66
   })
67 67
 }
68 68
 
69
-
70 69
 // 删除知识库文件
71 70
 export function deleteKnowledgeFile(fileName,collectionName) {
72 71
   return request({
@@ -74,4 +73,4 @@ export function deleteKnowledgeFile(fileName,collectionName) {
74 73
     method: 'delete',
75 74
     params: { fileName, collectionName }
76 75
   })
77
-}
76
+}

+ 16
- 0
llm-ui/src/api/llm/rag.js Zobrazit soubor

@@ -0,0 +1,16 @@
1
+/*
2
+ * @Author: ysh
3
+ * @Date: 2025-07-08 16:43:22
4
+ * @LastEditors: 
5
+ * @LastEditTime: 2025-07-08 16:45:06
6
+ */
7
+import request from '@/utils/request'
8
+
9
+// 查询cmc聊天记录列表
10
+export function getAnswer(question, collectionName) {
11
+  return request({
12
+    url: '/llm/rag/answer',
13
+    method: 'get',
14
+    params: { question, collectionName }
15
+  })
16
+}

+ 425
- 7
llm-ui/src/views/llm/knowledge/index.vue Zobrazit soubor

@@ -80,19 +80,27 @@
80 80
         </div>
81 81
       </div>
82 82
 
83
-      <!-- 右侧文件列表 -->
83
+      <!-- 右侧面板 -->
84 84
       <div class="right-panel">
85 85
         <div class="panel-header">
86
-          <h3>文件列表</h3>
86
+          <h3>{{ isChatMode ? '知识库对话' : '文件列表' }}</h3>
87 87
           <div class="header-actions">
88
-            <el-button type="primary" :icon="Upload" @click="handleUpload(selectedKnowledge)"
88
+            <el-button v-if="!isChatMode" type="primary" :icon="ChatRound" @click="handleChat(selectedKnowledge)"
89
+              :disabled="!selectedKnowledge" v-hasPermi="['llm:knowledge:chat']">
90
+              开始对话
91
+            </el-button>
92
+            <el-button v-if="!isChatMode" type="primary" :icon="Upload" @click="handleUpload(selectedKnowledge)"
89 93
               :disabled="!selectedKnowledge" v-hasPermi="['llm:knowledge:upload']">
90 94
               上传文件
91 95
             </el-button>
96
+            <el-button v-if="isChatMode" type="default" :icon="Document" @click="switchToFileMode">
97
+              返回文件列表
98
+            </el-button>
92 99
           </div>
93 100
         </div>
94 101
 
95
-        <div class="file-content" v-loading="fileLoading">
102
+        <!-- 文件列表模式 -->
103
+        <div v-if="!isChatMode" class="file-content" v-loading="fileLoading">
96 104
           <div v-if="selectedKnowledge" class="selected-knowledge">
97 105
             <el-icon class="folder-icon">
98 106
               <Folder />
@@ -136,6 +144,88 @@
136 144
             </div>
137 145
           </div>
138 146
         </div>
147
+
148
+        <!-- 聊天模式 -->
149
+        <div v-else class="chat-content">
150
+          <div v-if="selectedKnowledge" class="selected-knowledge">
151
+            <el-icon class="folder-icon">
152
+              <Folder />
153
+            </el-icon>
154
+            <span class="knowledge-name">{{ selectedKnowledge.collectionName }}</span>
155
+            <span class="chat-mode-badge">对话模式</span>
156
+          </div>
157
+
158
+          <!-- 聊天消息区域 -->
159
+          <div class="chat-messages" ref="chatMessagesRef">
160
+            <div v-if="chatMessages.length === 0" class="welcome-message">
161
+              <div class="welcome-content">
162
+                <div class="welcome-icon">🤖</div>
163
+                <h3>知识库助手</h3>
164
+                <p>我是基于「{{ selectedKnowledge?.collectionName }}」知识库的AI助手</p>
165
+                <p>您可以询问关于该知识库内容的问题,我会为您提供准确的答案</p>
166
+              </div>
167
+            </div>
168
+
169
+            <div v-else class="message-list">
170
+              <div v-for="(message, index) in chatMessages" :key="index" class="message-item" :class="message.type">
171
+                <div class="message-avatar">
172
+                  <div v-if="message.type === 'user'" class="user-avatar">
173
+                    <el-icon>
174
+                      <User />
175
+                    </el-icon>
176
+                  </div>
177
+                  <div v-else class="ai-avatar">
178
+                    🤖
179
+                  </div>
180
+                </div>
181
+                <div class="message-content">
182
+                  <div class="message-bubble" :class="message.type">
183
+                    <div class="message-text" v-html="formatMessage(message.content)"></div>
184
+                    <div class="message-time">{{ formatMessageTime(message.time) }}</div>
185
+                  </div>
186
+                </div>
187
+              </div>
188
+
189
+              <!-- AI回答loading状态 -->
190
+              <div v-if="isSending" class="message-item ai">
191
+                <div class="message-avatar">
192
+                  <div class="ai-avatar">
193
+                    <el-icon>
194
+                      <ChatDotRound />
195
+                    </el-icon>
196
+                  </div>
197
+                </div>
198
+                <div class="message-content">
199
+                  <div class="message-bubble ai">
200
+                    <div class="loading-dots">
201
+                      <span></span>
202
+                      <span></span>
203
+                      <span></span>
204
+                    </div>
205
+                  </div>
206
+                </div>
207
+              </div>
208
+            </div>
209
+          </div>
210
+
211
+          <!-- 输入区域 -->
212
+          <div class="chat-input-area">
213
+            <div class="input-container">
214
+              <el-input v-model="chatInput" type="textarea" :rows="3" placeholder="请输入您的问题..."
215
+                @keydown.enter="handleEnter" @keydown.ctrl.enter="handleCtrlEnter" :disabled="isSending"
216
+                resize="none" />
217
+              <div class="input-actions">
218
+                <el-button type="primary" :icon="Promotion" @click="sendMessage" :loading="isSending"
219
+                  :disabled="!chatInput.trim() || isSending">
220
+                  发送
221
+                </el-button>
222
+              </div>
223
+            </div>
224
+            <div class="input-tips">
225
+              <span>按 Enter 发送,Ctrl + Enter 换行</span>
226
+            </div>
227
+          </div>
228
+        </div>
139 229
       </div>
140 230
     </div>
141 231
 
@@ -196,8 +286,9 @@
196 286
 
197 287
 <script setup>
198 288
 import { ref, reactive, getCurrentInstance, onMounted, nextTick, watch, computed } from 'vue'
199
-import { Search, Refresh, Plus, Edit, Delete, Upload, UploadFilled, Folder, MoreFilled, FolderOpened, Document } from '@element-plus/icons-vue'
289
+import { Search, Refresh, Plus, Edit, Delete, Upload, UploadFilled, Folder, MoreFilled, FolderOpened, Document, ChatRound, User, ChatDotRound, Promotion } from '@element-plus/icons-vue'
200 290
 import { listKnowledge, addKnowledge, updateKnowledge, delKnowledge, insertKnowledgeFile, listKnowledgeDocument, deleteKnowledgeFile } from "@/api/llm/knowLedge";
291
+import { getAnswer } from '@/api/llm/rag';
201 292
 import { getToken } from "@/utils/auth";
202 293
 
203 294
 const { proxy } = getCurrentInstance();
@@ -217,6 +308,13 @@ const isModify = ref(false);
217 308
 const selectedKnowledge = ref(null);
218 309
 const fileList = ref([]);
219 310
 
311
+// 聊天相关状态
312
+const isChatMode = ref(false);
313
+const chatMessages = ref([]);
314
+const chatInput = ref('');
315
+const isSending = ref(false);
316
+const chatMessagesRef = ref(null);
317
+
220 318
 // 表单数据
221 319
 const form = reactive({
222 320
   collectionName: '',
@@ -281,6 +379,11 @@ function getList() {
281 379
 /** 选择知识库 */
282 380
 function selectKnowledge(knowledge) {
283 381
   selectedKnowledge.value = knowledge;
382
+  // 重置聊天模式
383
+  isChatMode.value = false;
384
+  chatMessages.value = [];
385
+  chatInput.value = '';
386
+
284 387
   fileLoading.value = true;
285 388
   listKnowledgeDocument(knowledge.collectionName).then(response => {
286 389
     fileList.value = response.data;
@@ -291,7 +394,115 @@ function selectKnowledge(knowledge) {
291 394
   });
292 395
 }
293 396
 
397
+/** 开始对话 */
398
+function handleChat(knowledge) {
399
+  if (!knowledge) {
400
+    proxy.$modal.msgWarning("请先选择要对话的知识库");
401
+    return;
402
+  }
403
+  isChatMode.value = true;
404
+  chatMessages.value = [];
405
+  chatInput.value = '';
406
+  // 滚动到底部
407
+  nextTick(() => {
408
+    scrollToBottom();
409
+  });
410
+}
294 411
 
412
+/** 切换到文件模式 */
413
+function switchToFileMode() {
414
+  isChatMode.value = false;
415
+}
416
+
417
+/** 发送消息 */
418
+function sendMessage() {
419
+  if (!chatInput.value.trim() || isSending.value) return;
420
+
421
+  const userMessage = {
422
+    type: 'user',
423
+    content: chatInput.value.trim(),
424
+    time: new Date()
425
+  };
426
+
427
+  chatMessages.value.push(userMessage);
428
+  const currentInput = chatInput.value;
429
+  chatInput.value = '';
430
+  isSending.value = true;
431
+
432
+  // 滚动到底部
433
+  nextTick(() => {
434
+    scrollToBottom();
435
+  });
436
+
437
+  // 延迟一点时间让loading动画显示
438
+  setTimeout(() => {
439
+    getAnswer(currentInput, selectedKnowledge.value.collectionName).then(response => {
440
+      console.log(response);
441
+      const aiMessage = {
442
+        type: 'ai',
443
+        content: response.msg,
444
+        time: new Date()
445
+      };
446
+      chatMessages.value.push(aiMessage);
447
+      isSending.value = false;
448
+      // 滚动到底部
449
+      nextTick(() => {
450
+        scrollToBottom();
451
+      });
452
+    }).catch(error => {
453
+      console.log(error);
454
+      // 显示错误消息
455
+      const errorMessage = {
456
+        type: 'ai',
457
+        content: '抱歉,我暂时无法回答您的问题,请稍后再试。',
458
+        time: new Date()
459
+      };
460
+      chatMessages.value.push(errorMessage);
461
+      isSending.value = false;
462
+      // 滚动到底部
463
+      nextTick(() => {
464
+        scrollToBottom();
465
+      });
466
+    });
467
+  }, 300);
468
+}
469
+
470
+/** 滚动到底部 */
471
+function scrollToBottom() {
472
+  if (chatMessagesRef.value) {
473
+    const container = chatMessagesRef.value;
474
+    container.scrollTop = container.scrollHeight;
475
+  }
476
+}
477
+
478
+/** 处理Enter键 */
479
+function handleEnter(event) {
480
+  if (!event.shiftKey) {
481
+    event.preventDefault();
482
+    sendMessage();
483
+  }
484
+}
485
+
486
+/** 处理Ctrl+Enter键 */
487
+function handleCtrlEnter() {
488
+  chatInput.value += '\n';
489
+}
490
+
491
+/** 格式化消息内容 */
492
+function formatMessage(content) {
493
+  // 简单的换行处理
494
+  return content.replace(/\n/g, '<br>');
495
+}
496
+
497
+/** 格式化消息时间 */
498
+function formatMessageTime(time) {
499
+  if (!time) return '';
500
+  const date = new Date(time);
501
+  return date.toLocaleTimeString('zh-CN', {
502
+    hour: '2-digit',
503
+    minute: '2-digit'
504
+  });
505
+}
295 506
 
296 507
 /** 处理卡片操作 */
297 508
 function handleCardAction(command) {
@@ -375,7 +586,6 @@ function resetQuery() {
375 586
   handleQuery();
376 587
 }
377 588
 
378
-
379 589
 /** 新增按钮操作 */
380 590
 function handleAdd() {
381 591
   reset();
@@ -441,6 +651,8 @@ function handleDelete(row) {
441 651
     if (selectedKnowledge.value?.collectionName === collectionNames) {
442 652
       selectedKnowledge.value = null;
443 653
       fileList.value = [];
654
+      isChatMode.value = false;
655
+      chatMessages.value = [];
444 656
     }
445 657
     proxy.$modal.msgSuccess("删除成功");
446 658
   }).catch((error) => {
@@ -698,6 +910,14 @@ onMounted(() => {
698 910
   overflow-y: auto;
699 911
 }
700 912
 
913
+// 聊天模式样式
914
+.chat-content {
915
+  flex: 1;
916
+  display: flex;
917
+  flex-direction: column;
918
+  overflow: hidden;
919
+}
920
+
701 921
 .selected-knowledge {
702 922
   display: flex;
703 923
   align-items: center;
@@ -705,7 +925,7 @@ onMounted(() => {
705 925
   padding: 12px 16px;
706 926
   background: #f0f9ff;
707 927
   border-radius: 6px;
708
-  margin-bottom: 20px;
928
+  margin: 20px 20px 0 20px;
709 929
 
710 930
   .folder-icon {
711 931
     color: #409eff;
@@ -716,6 +936,156 @@ onMounted(() => {
716 936
     font-weight: 600;
717 937
     color: #303133;
718 938
   }
939
+
940
+  .chat-mode-badge {
941
+    background: #409eff;
942
+    color: white;
943
+    padding: 2px 8px;
944
+    border-radius: 12px;
945
+    font-size: 12px;
946
+    margin-left: auto;
947
+  }
948
+}
949
+
950
+.chat-messages {
951
+  flex: 1;
952
+  padding: 20px;
953
+  overflow-y: auto;
954
+  background: #fafbfc;
955
+}
956
+
957
+.welcome-message {
958
+  display: flex;
959
+  align-items: center;
960
+  justify-content: center;
961
+  height: 100%;
962
+  text-align: center;
963
+
964
+  .welcome-content {
965
+    max-width: 400px;
966
+
967
+    .welcome-icon {
968
+      font-size: 48px;
969
+      margin-bottom: 16px;
970
+    }
971
+
972
+    h3 {
973
+      margin: 0 0 12px 0;
974
+      color: #303133;
975
+      font-size: 18px;
976
+    }
977
+
978
+    p {
979
+      margin: 0 0 8px 0;
980
+      color: #606266;
981
+      font-size: 14px;
982
+      line-height: 1.6;
983
+    }
984
+  }
985
+}
986
+
987
+.message-list {
988
+  display: flex;
989
+  flex-direction: column;
990
+  gap: 20px;
991
+}
992
+
993
+.message-item {
994
+  display: flex;
995
+  gap: 12px;
996
+
997
+  &.user {
998
+    flex-direction: row-reverse;
999
+
1000
+    .message-content {
1001
+      align-items: flex-end;
1002
+    }
1003
+
1004
+    .message-bubble {
1005
+      background: #409eff;
1006
+      color: white;
1007
+      border-radius: 18px 18px 4px 18px;
1008
+    }
1009
+  }
1010
+
1011
+  &.ai {
1012
+    .message-bubble {
1013
+      background: white;
1014
+      color: #303133;
1015
+      border: 1px solid #e4e7ed;
1016
+      border-radius: 18px 18px 18px 4px;
1017
+    }
1018
+  }
1019
+}
1020
+
1021
+.message-avatar {
1022
+  width: 40px;
1023
+  height: 40px;
1024
+  border-radius: 50%;
1025
+  display: flex;
1026
+  align-items: center;
1027
+  justify-content: center;
1028
+  flex-shrink: 0;
1029
+
1030
+  .user-avatar {
1031
+    // background: #409eff;
1032
+    color: #409eff;
1033
+    font-size: 18px;
1034
+  }
1035
+
1036
+  .ai-avatar {
1037
+    // background: #67c23a;
1038
+    color: white;
1039
+    font-size: 18px;
1040
+  }
1041
+}
1042
+
1043
+.message-content {
1044
+  display: flex;
1045
+  flex-direction: column;
1046
+  max-width: 70%;
1047
+}
1048
+
1049
+.message-bubble {
1050
+  padding: 12px 16px;
1051
+  line-height: 1.5;
1052
+  word-wrap: break-word;
1053
+
1054
+  .message-text {
1055
+    margin-bottom: 8px;
1056
+  }
1057
+
1058
+  .message-time {
1059
+    font-size: 12px;
1060
+    opacity: 0.7;
1061
+  }
1062
+}
1063
+
1064
+.chat-input-area {
1065
+  padding: 20px;
1066
+  border-top: 1px solid #e4e7ed;
1067
+  background: white;
1068
+}
1069
+
1070
+.input-container {
1071
+  display: flex;
1072
+  gap: 12px;
1073
+  align-items: flex-end;
1074
+
1075
+  .el-textarea {
1076
+    flex: 1;
1077
+  }
1078
+
1079
+  .input-actions {
1080
+    flex-shrink: 0;
1081
+  }
1082
+}
1083
+
1084
+.input-tips {
1085
+  margin-top: 8px;
1086
+  text-align: center;
1087
+  font-size: 12px;
1088
+  color: #999;
719 1089
 }
720 1090
 
721 1091
 .file-list {
@@ -863,5 +1233,53 @@ onMounted(() => {
863 1233
   .file-content {
864 1234
     padding: 16px;
865 1235
   }
1236
+
1237
+  .chat-messages {
1238
+    padding: 16px;
1239
+  }
1240
+
1241
+  .chat-input-area {
1242
+    padding: 16px;
1243
+  }
1244
+
1245
+  .message-content {
1246
+    max-width: 85%;
1247
+  }
1248
+}
1249
+
1250
+// Loading动画样式
1251
+.loading-dots {
1252
+  display: flex;
1253
+  gap: 4px;
1254
+  padding: 8px 0;
1255
+
1256
+  span {
1257
+    width: 8px;
1258
+    height: 8px;
1259
+    border-radius: 50%;
1260
+    background-color: #ccc;
1261
+    animation: loading 1.4s infinite ease-in-out;
1262
+
1263
+    &:nth-child(1) {
1264
+      animation-delay: -0.32s;
1265
+    }
1266
+
1267
+    &:nth-child(2) {
1268
+      animation-delay: -0.16s;
1269
+    }
1270
+  }
1271
+}
1272
+
1273
+@keyframes loading {
1274
+
1275
+  0%,
1276
+  80%,
1277
+  100% {
1278
+    transform: scale(0);
1279
+  }
1280
+
1281
+  40% {
1282
+    transform: scale(1);
1283
+  }
866 1284
 }
867 1285
 </style>

Loading…
Zrušit
Uložit