浏览代码

新增知识库对话界面

余思翰 2 天前
父节点
当前提交
1dabd105f3

+ 7
- 1
llm-back/ruoyi-llm/src/main/java/com/ruoyi/web/llm/controller/RagController.java 查看文件

1
+/*
2
+ * @Author: ysh
3
+ * @Date: 2025-07-08 15:10:42
4
+ * @LastEditors: 
5
+ * @LastEditTime: 2025-07-09 11:37:02
6
+ */
1
 package com.ruoyi.web.llm.controller;
7
 package com.ruoyi.web.llm.controller;
2
 
8
 
3
 import com.ruoyi.web.llm.service.ILangChainMilvusService;
9
 import com.ruoyi.web.llm.service.ILangChainMilvusService;
43
     public AjaxResult answer(String question, String collectionName) throws IOException {
49
     public AjaxResult answer(String question, String collectionName) throws IOException {
44
 
50
 
45
         List<String> contexts = langChainMilvusService.retrieveFromMilvus(milvusClient, embeddingModel, collectionName, question, 1);
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
         return success(result);
53
         return success(result);
48
     }
54
     }
49
 
55
 

+ 1
- 1
llm-back/ruoyi-llm/src/main/java/com/ruoyi/web/llm/service/ILangChainMilvusService.java 查看文件

29
     /**
29
     /**
30
      * 调用LLM生成回答
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 查看文件

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
 package com.ruoyi.web.llm.service.impl;
7
 package com.ruoyi.web.llm.service.impl;
2
 
8
 
3
 import com.alibaba.fastjson2.JSONObject;
9
 import com.alibaba.fastjson2.JSONObject;
157
         }
163
         }
158
         sb.append("问题: ").append(question).append("\n回答: ");
164
         sb.append("问题: ").append(question).append("\n回答: ");
159
         // 构建带动态参数的URL
165
         // 构建带动态参数的URL
160
-        return generateAnswer(llmServiceUrl, sb.toString());
166
+        return generateAnswer(sb.toString(),llmServiceUrl);
161
     }
167
     }
162
 
168
 
163
     /**
169
     /**
164
      * 调用LLM生成回答
170
      * 调用LLM生成回答
165
      */
171
      */
166
     @Override
172
     @Override
167
-    public String generateAnswer(String llmServiceUrl, String prompt) throws IOException {
173
+    public String generateAnswer(String prompt, String llmServiceUrl) throws IOException {
168
         HttpUrl url = HttpUrl.parse(llmServiceUrl)
174
         HttpUrl url = HttpUrl.parse(llmServiceUrl)
169
                 .newBuilder()
175
                 .newBuilder()
170
                 .addQueryParameter("prompt", prompt)
176
                 .addQueryParameter("prompt", prompt)

+ 1
- 2
llm-ui/src/api/llm/knowLedge.js 查看文件

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

+ 16
- 0
llm-ui/src/api/llm/rag.js 查看文件

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 查看文件

80
         </div>
80
         </div>
81
       </div>
81
       </div>
82
 
82
 
83
-      <!-- 右侧文件列表 -->
83
+      <!-- 右侧面板 -->
84
       <div class="right-panel">
84
       <div class="right-panel">
85
         <div class="panel-header">
85
         <div class="panel-header">
86
-          <h3>文件列表</h3>
86
+          <h3>{{ isChatMode ? '知识库对话' : '文件列表' }}</h3>
87
           <div class="header-actions">
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
               :disabled="!selectedKnowledge" v-hasPermi="['llm:knowledge:upload']">
93
               :disabled="!selectedKnowledge" v-hasPermi="['llm:knowledge:upload']">
90
               上传文件
94
               上传文件
91
             </el-button>
95
             </el-button>
96
+            <el-button v-if="isChatMode" type="default" :icon="Document" @click="switchToFileMode">
97
+              返回文件列表
98
+            </el-button>
92
           </div>
99
           </div>
93
         </div>
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
           <div v-if="selectedKnowledge" class="selected-knowledge">
104
           <div v-if="selectedKnowledge" class="selected-knowledge">
97
             <el-icon class="folder-icon">
105
             <el-icon class="folder-icon">
98
               <Folder />
106
               <Folder />
136
             </div>
144
             </div>
137
           </div>
145
           </div>
138
         </div>
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
       </div>
229
       </div>
140
     </div>
230
     </div>
141
 
231
 
196
 
286
 
197
 <script setup>
287
 <script setup>
198
 import { ref, reactive, getCurrentInstance, onMounted, nextTick, watch, computed } from 'vue'
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
 import { listKnowledge, addKnowledge, updateKnowledge, delKnowledge, insertKnowledgeFile, listKnowledgeDocument, deleteKnowledgeFile } from "@/api/llm/knowLedge";
290
 import { listKnowledge, addKnowledge, updateKnowledge, delKnowledge, insertKnowledgeFile, listKnowledgeDocument, deleteKnowledgeFile } from "@/api/llm/knowLedge";
291
+import { getAnswer } from '@/api/llm/rag';
201
 import { getToken } from "@/utils/auth";
292
 import { getToken } from "@/utils/auth";
202
 
293
 
203
 const { proxy } = getCurrentInstance();
294
 const { proxy } = getCurrentInstance();
217
 const selectedKnowledge = ref(null);
308
 const selectedKnowledge = ref(null);
218
 const fileList = ref([]);
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
 const form = reactive({
319
 const form = reactive({
222
   collectionName: '',
320
   collectionName: '',
281
 /** 选择知识库 */
379
 /** 选择知识库 */
282
 function selectKnowledge(knowledge) {
380
 function selectKnowledge(knowledge) {
283
   selectedKnowledge.value = knowledge;
381
   selectedKnowledge.value = knowledge;
382
+  // 重置聊天模式
383
+  isChatMode.value = false;
384
+  chatMessages.value = [];
385
+  chatInput.value = '';
386
+
284
   fileLoading.value = true;
387
   fileLoading.value = true;
285
   listKnowledgeDocument(knowledge.collectionName).then(response => {
388
   listKnowledgeDocument(knowledge.collectionName).then(response => {
286
     fileList.value = response.data;
389
     fileList.value = response.data;
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
 function handleCardAction(command) {
508
 function handleCardAction(command) {
375
   handleQuery();
586
   handleQuery();
376
 }
587
 }
377
 
588
 
378
-
379
 /** 新增按钮操作 */
589
 /** 新增按钮操作 */
380
 function handleAdd() {
590
 function handleAdd() {
381
   reset();
591
   reset();
441
     if (selectedKnowledge.value?.collectionName === collectionNames) {
651
     if (selectedKnowledge.value?.collectionName === collectionNames) {
442
       selectedKnowledge.value = null;
652
       selectedKnowledge.value = null;
443
       fileList.value = [];
653
       fileList.value = [];
654
+      isChatMode.value = false;
655
+      chatMessages.value = [];
444
     }
656
     }
445
     proxy.$modal.msgSuccess("删除成功");
657
     proxy.$modal.msgSuccess("删除成功");
446
   }).catch((error) => {
658
   }).catch((error) => {
698
   overflow-y: auto;
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
 .selected-knowledge {
921
 .selected-knowledge {
702
   display: flex;
922
   display: flex;
703
   align-items: center;
923
   align-items: center;
705
   padding: 12px 16px;
925
   padding: 12px 16px;
706
   background: #f0f9ff;
926
   background: #f0f9ff;
707
   border-radius: 6px;
927
   border-radius: 6px;
708
-  margin-bottom: 20px;
928
+  margin: 20px 20px 0 20px;
709
 
929
 
710
   .folder-icon {
930
   .folder-icon {
711
     color: #409eff;
931
     color: #409eff;
716
     font-weight: 600;
936
     font-weight: 600;
717
     color: #303133;
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
 .file-list {
1091
 .file-list {
863
   .file-content {
1233
   .file-content {
864
     padding: 16px;
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
 </style>
1285
 </style>

正在加载...
取消
保存