Bläddra i källkod

知识库新增引用文件;

余思翰 1 vecka sedan
förälder
incheckning
46affa7d34

+ 2
- 2
llm-back/ruoyi-llm/src/main/java/com/ruoyi/web/llm/service/impl/LangChainMilvusServiceImpl.java Visa fil

2
  * @Author: ysh
2
  * @Author: ysh
3
  * @Date: 2025-07-08 15:10:42
3
  * @Date: 2025-07-08 15:10:42
4
  * @LastEditors: Please set LastEditors
4
  * @LastEditors: Please set LastEditors
5
- * @LastEditTime: 2025-07-09 11:39:18
5
+ * @LastEditTime: 2025-07-21 15:42:26
6
  */
6
  */
7
 package com.ruoyi.web.llm.service.impl;
7
 package com.ruoyi.web.llm.service.impl;
8
 
8
 
144
                     return result;
144
                     return result;
145
                 })
145
                 })
146
                 .collect(Collectors.toList());
146
                 .collect(Collectors.toList());
147
-        wrapperList.removeIf(jsonObject -> jsonObject.getDouble("distance") < 0.7);
147
+        wrapperList.removeIf(jsonObject -> jsonObject.getDouble("distance") < 0.75);
148
         return wrapperList;
148
         return wrapperList;
149
     }
149
     }
150
 
150
 

+ 10
- 1
llm-ui/src/api/llm/rag.js Visa fil

2
  * @Author: ysh
2
  * @Author: ysh
3
  * @Date: 2025-07-08 16:43:22
3
  * @Date: 2025-07-08 16:43:22
4
  * @LastEditors: Please set LastEditors
4
  * @LastEditors: Please set LastEditors
5
- * @LastEditTime: 2025-07-11 14:15:01
5
+ * @LastEditTime: 2025-07-16 15:49:45
6
  */
6
  */
7
 import request from '@/utils/request'
7
 import request from '@/utils/request'
8
 import { getToken } from '@/utils/auth'
8
 import { getToken } from '@/utils/auth'
16
   })
16
   })
17
 }
17
 }
18
 
18
 
19
+// 查询上下文引用文件
20
+export function getContextFile(question, collectionName) {
21
+  return request({
22
+    url: '/llm/rag/context',
23
+    method: 'get',
24
+    params: { question, collectionName }
25
+  })
26
+}
27
+
19
 // 流式回答API - 使用fetch API处理流式响应
28
 // 流式回答API - 使用fetch API处理流式响应
20
 export function getAnswerStream(question, collectionName, onMessage, onError, onComplete) {
29
 export function getAnswerStream(question, collectionName, onMessage, onError, onComplete) {
21
   const baseURL = import.meta.env.VITE_APP_BASE_API
30
   const baseURL = import.meta.env.VITE_APP_BASE_API

+ 395
- 46
llm-ui/src/views/llm/knowledge/index.vue Visa fil

124
           </div>
124
           </div>
125
 
125
 
126
           <div v-else class="file-list">
126
           <div v-else class="file-list">
127
-            <div v-for="(file, index) in fileList" :key="index" class="file-item">
127
+            <div v-for="(file, index) in pagedFileList" :key="index" class="file-item">
128
               <div class="file-info">
128
               <div class="file-info">
129
                 <el-icon class="file-icon">
129
                 <el-icon class="file-icon">
130
                   <Document />
130
                   <Document />
143
               </div>
143
               </div>
144
             </div>
144
             </div>
145
           </div>
145
           </div>
146
+
147
+          <!-- 文件列表分页 -->
148
+          <pagination v-show="fileTotal > 0" :total="fileTotal" v-model:page="filePageNum" v-model:limit="filePageSize" :autoScroll="false"
149
+            @pagination="handleFilePagination" />
146
         </div>
150
         </div>
147
 
151
 
148
         <!-- 聊天模式 -->
152
         <!-- 聊天模式 -->
167
             </div>
171
             </div>
168
 
172
 
169
             <div v-else class="message-list">
173
             <div v-else class="message-list">
170
-              <div v-for="(message, index) in chatMessages" :key="index" class="message-item" 
174
+              <div v-for="(message, index) in chatMessages" :key="index" class="message-item"
171
                 :class="[message.type, { 'typing': message.type === 'ai' && isSending && index === chatMessages.length - 1 }]">
175
                 :class="[message.type, { 'typing': message.type === 'ai' && isSending && index === chatMessages.length - 1 }]">
172
                 <div class="message-avatar">
176
                 <div class="message-avatar">
173
                   <div v-if="message.type === 'user'" class="user-avatar">
177
                   <div v-if="message.type === 'user'" class="user-avatar">
182
                 <div class="message-content">
186
                 <div class="message-content">
183
                   <div class="message-bubble" :class="message.type">
187
                   <div class="message-bubble" :class="message.type">
184
                     <div class="message-text" v-html="formatMessage(message.content)"></div>
188
                     <div class="message-text" v-html="formatMessage(message.content)"></div>
185
-                    <div class="message-time">{{ formatMessageTime(message.time) }}</div>
189
+                    
190
+                    <!-- AI回答的引用文件信息 -->
191
+                    <div v-if="message.type === 'ai' && message.references && message.references.length > 0" class="message-references">
192
+                      <div class="references-header">
193
+                        <el-icon class="reference-icon"><Document /></el-icon>
194
+                        <span>参考文件 ({{ message.references.length }})</span>
195
+                        <el-button 
196
+                          v-if="message.references.length > 3"
197
+                          type="text" 
198
+                          size="small" 
199
+                          class="toggle-references"
200
+                          @click="toggleReferences(index)"
201
+                        >
202
+                          {{ message.showAllReferences ? '收起' : '展开全部' }}
203
+                        </el-button>
204
+                      </div>
205
+                      <div class="references-list">
206
+                        <div 
207
+                          v-for="(ref, refIndex) in getDisplayedReferences(message)" 
208
+                          :key="refIndex" 
209
+                          class="reference-item"
210
+                        >
211
+                          <div class="reference-info">
212
+                            <span class="reference-filename" :title="ref.fileName">{{ ref.fileName }}</span>
213
+                            <span class="reference-similarity" :class="getSimilarityClass(ref.similarity)">
214
+                              相似度: {{ formatSimilarity(ref.similarity) }}%
215
+                            </span>
216
+                          </div>
217
+                        </div>
218
+                      </div>
219
+                    </div>
220
+                    
221
+                    <div class="message-time">{{ message.time }}</div>
186
                   </div>
222
                   </div>
187
                 </div>
223
                 </div>
188
               </div>
224
               </div>
189
 
225
 
190
               <!-- AI回答loading状态 -->
226
               <!-- AI回答loading状态 -->
191
-              <div v-if="isSending && chatMessages.length > 0 && chatMessages[chatMessages.length - 1].type === 'ai' && chatMessages[chatMessages.length - 1].content === ''" class="message-item ai">
227
+              <div
228
+                v-if="isSending && chatMessages.length > 0 && chatMessages[chatMessages.length - 1].type === 'ai' && chatMessages[chatMessages.length - 1].content === ''"
229
+                class="message-item ai">
192
                 <div class="message-avatar">
230
                 <div class="message-avatar">
193
                   <div class="ai-avatar">
231
                   <div class="ai-avatar">
194
                     <el-icon>
232
                     <el-icon>
212
           <!-- 输入区域 -->
250
           <!-- 输入区域 -->
213
           <div class="chat-input-area">
251
           <div class="chat-input-area">
214
             <div class="input-container">
252
             <div class="input-container">
215
-              <el-input v-model="chatInput" type="textarea" :rows="3" 
216
-                :placeholder="isSending ? 'AI正在思考中...' : '请输入您的问题...'"
217
-                @keydown.enter="handleEnter" @keydown.ctrl.enter="handleCtrlEnter" :disabled="isSending"
218
-                resize="none" />
253
+              <el-input v-model="chatInput" type="textarea" :rows="3"
254
+                :placeholder="isSending ? 'AI正在思考中...' : '请输入您的问题...'" @keydown.enter="handleEnter"
255
+                @keydown.ctrl.enter="handleCtrlEnter" :disabled="isSending" resize="none" />
219
               <div class="input-actions">
256
               <div class="input-actions">
220
                 <el-button v-if="!isSending" type="primary" :icon="Promotion" @click="sendMessage"
257
                 <el-button v-if="!isSending" type="primary" :icon="Promotion" @click="sendMessage"
221
                   :disabled="!chatInput.trim() || chatInput.length > 2000">
258
                   :disabled="!chatInput.trim() || chatInput.length > 2000">
297
 import { ref, reactive, getCurrentInstance, onMounted, onUnmounted, nextTick, watch, computed } from 'vue'
334
 import { ref, reactive, getCurrentInstance, onMounted, onUnmounted, nextTick, watch, computed } from 'vue'
298
 import { Search, Refresh, Plus, Edit, Delete, Upload, UploadFilled, Folder, MoreFilled, FolderOpened, Document, ChatRound, User, ChatDotRound, Promotion } from '@element-plus/icons-vue'
335
 import { Search, Refresh, Plus, Edit, Delete, Upload, UploadFilled, Folder, MoreFilled, FolderOpened, Document, ChatRound, User, ChatDotRound, Promotion } from '@element-plus/icons-vue'
299
 import { listKnowledge, addKnowledge, updateKnowledge, delKnowledge, insertKnowledgeFile, listKnowledgeDocument, deleteKnowledgeFile } from "@/api/llm/knowLedge";
336
 import { listKnowledge, addKnowledge, updateKnowledge, delKnowledge, insertKnowledgeFile, listKnowledgeDocument, deleteKnowledgeFile } from "@/api/llm/knowLedge";
300
-import { getAnswer, getAnswerStream } from '@/api/llm/rag';
337
+import { getAnswer, getAnswerStream,getContextFile } from '@/api/llm/rag';
301
 import { getToken } from "@/utils/auth";
338
 import { getToken } from "@/utils/auth";
302
 
339
 
303
 const { proxy } = getCurrentInstance();
340
 const { proxy } = getCurrentInstance();
317
 const selectedKnowledge = ref(null);
354
 const selectedKnowledge = ref(null);
318
 const fileList = ref([]);
355
 const fileList = ref([]);
319
 
356
 
357
+// 文件分页相关状态
358
+const fileTotal = ref(0);
359
+const filePageNum = ref(1);
360
+const filePageSize = ref(10);
361
+const pagedFileList = computed(() => {
362
+  const start = (filePageNum.value - 1) * filePageSize.value;
363
+  const end = start + filePageSize.value;
364
+  return fileList.value.slice(start, end);
365
+});
366
+
320
 // 聊天相关状态
367
 // 聊天相关状态
321
 const isChatMode = ref(false);
368
 const isChatMode = ref(false);
322
 const chatMessages = ref([]);
369
 const chatMessages = ref([]);
375
     // 确保返回的数据是数组格式
422
     // 确保返回的数据是数组格式
376
     if (Array.isArray(response.data)) {
423
     if (Array.isArray(response.data)) {
377
       knowledgeList.value = response.data;
424
       knowledgeList.value = response.data;
425
+      knowledgeList.value.map(item => {
426
+        listKnowledgeDocument(item.collectionName).then(response => {
427
+          item.fileCount = response.data.length;
428
+        })
429
+      })
378
     } else {
430
     } else {
379
       knowledgeList.value = [];
431
       knowledgeList.value = [];
380
     }
432
     }
391
   if (isSending.value) {
443
   if (isSending.value) {
392
     stopGenerating();
444
     stopGenerating();
393
   }
445
   }
394
-  
446
+
395
   selectedKnowledge.value = knowledge;
447
   selectedKnowledge.value = knowledge;
396
   // 重置聊天模式
448
   // 重置聊天模式
397
   isChatMode.value = false;
449
   isChatMode.value = false;
398
   chatMessages.value = [];
450
   chatMessages.value = [];
399
   chatInput.value = '';
451
   chatInput.value = '';
400
 
452
 
453
+  // 重置文件分页状态
454
+  filePageNum.value = 1;
455
+  filePageSize.value = 10;
456
+
401
   fileLoading.value = true;
457
   fileLoading.value = true;
402
   listKnowledgeDocument(knowledge.collectionName).then(response => {
458
   listKnowledgeDocument(knowledge.collectionName).then(response => {
403
     fileList.value = response.data;
459
     fileList.value = response.data;
460
+    fileTotal.value = response.data.length;
404
     fileLoading.value = false;
461
     fileLoading.value = false;
405
   }).catch(error => {
462
   }).catch(error => {
406
     fileLoading.value = false;
463
     fileLoading.value = false;
418
   if (isSending.value) {
475
   if (isSending.value) {
419
     stopGenerating();
476
     stopGenerating();
420
   }
477
   }
421
-  
478
+
422
   isChatMode.value = true;
479
   isChatMode.value = true;
423
   chatMessages.value = [];
480
   chatMessages.value = [];
424
   chatInput.value = '';
481
   chatInput.value = '';
434
   if (isSending.value) {
491
   if (isSending.value) {
435
     stopGenerating();
492
     stopGenerating();
436
   }
493
   }
437
-  
494
+
438
   isChatMode.value = false;
495
   isChatMode.value = false;
439
 }
496
 }
440
 
497
 
441
 /** 发送消息 */
498
 /** 发送消息 */
442
 function sendMessage() {
499
 function sendMessage() {
443
   if (!chatInput.value.trim()) return;
500
   if (!chatInput.value.trim()) return;
444
-  
501
+
445
   // 如果正在发送,先停止当前请求
502
   // 如果正在发送,先停止当前请求
446
   if (isSending.value) {
503
   if (isSending.value) {
447
     stopGenerating();
504
     stopGenerating();
450
   const userMessage = {
507
   const userMessage = {
451
     type: 'user',
508
     type: 'user',
452
     content: chatInput.value.trim(),
509
     content: chatInput.value.trim(),
453
-    time: new Date()
510
+    time: proxy.parseTime(new Date(), '{y}-{m}-{d} {h}:{i}:{s}')
454
   };
511
   };
455
 
512
 
456
   chatMessages.value.push(userMessage);
513
   chatMessages.value.push(userMessage);
467
   const aiMessage = reactive({
524
   const aiMessage = reactive({
468
     type: 'ai',
525
     type: 'ai',
469
     content: '',
526
     content: '',
470
-    time: new Date()
527
+    time: proxy.parseTime(new Date(), '{y}-{m}-{d} {h}:{i}:{s}'),
528
+    references: [], // 引用的文件信息
529
+    showAllReferences: false // 是否显示所有引用文件
471
   });
530
   });
472
   chatMessages.value.push(aiMessage);
531
   chatMessages.value.push(aiMessage);
473
 
532
 
474
   // 使用流式API获取回答
533
   // 使用流式API获取回答
475
   const eventSource = getAnswerStream(
534
   const eventSource = getAnswerStream(
476
-    currentInput, 
535
+    currentInput,
477
     selectedKnowledge.value.collectionName,
536
     selectedKnowledge.value.collectionName,
478
-        // onMessage: 接收到每个字符时的回调
537
+    // onMessage: 接收到每个字符时的回调
479
     (content) => {
538
     (content) => {
480
       // 处理接收到的内容
539
       // 处理接收到的内容
481
       console.log('=== 前端接收到内容 ===', content)
540
       console.log('=== 前端接收到内容 ===', content)
482
-      
541
+
483
       // 清理内容中的</think>标签
542
       // 清理内容中的</think>标签
484
       let cleanContent = content.replace(/<\/?think>/g, '');
543
       let cleanContent = content.replace(/<\/?think>/g, '');
485
-      
544
+
486
       // 如果内容为空或只包含空白字符,跳过
545
       // 如果内容为空或只包含空白字符,跳过
487
       if (!cleanContent.trim()) {
546
       if (!cleanContent.trim()) {
488
         return;
547
         return;
489
       }
548
       }
490
-      
549
+
491
       // 直接替换内容,避免重复叠加
550
       // 直接替换内容,避免重复叠加
492
       aiMessage.content = cleanContent;
551
       aiMessage.content = cleanContent;
493
-      
552
+      aiMessage.time = proxy.parseTime(new Date(), '{y}-{m}-{d} {h}:{i}:{s}');
494
       console.log('=== 清理后的内容 ===', cleanContent)
553
       console.log('=== 清理后的内容 ===', cleanContent)
495
       console.log('=== 当前AI消息完整内容 ===', aiMessage.content)
554
       console.log('=== 当前AI消息完整内容 ===', aiMessage.content)
555
+
556
+      // 每次接收到消息都重置超时定时器
557
+      if (window.responseTimeout) {
558
+        clearTimeout(window.responseTimeout);
559
+      }
496
       
560
       
561
+      // 重新设置超时定时器(30秒无新消息才超时)
562
+      window.responseTimeout = setTimeout(() => {
563
+        if (isSending.value) {
564
+          console.log('=== 响应超时强制结束 ===')
565
+          isSending.value = false;
566
+          if (window.currentController) {
567
+            window.currentController.abort();
568
+            window.currentController = null;
569
+          }
570
+          window.responseTimeout = null;
571
+        }
572
+      }, 30000); // 30秒无响应超时
573
+
497
       // 滚动到底部
574
       // 滚动到底部
498
       nextTick(() => {
575
       nextTick(() => {
499
         scrollToBottom();
576
         scrollToBottom();
502
     // onError: 发生错误时的回调
579
     // onError: 发生错误时的回调
503
     (error) => {
580
     (error) => {
504
       console.error('=== 流式回答错误 ===', error);
581
       console.error('=== 流式回答错误 ===', error);
582
+      
583
+      // 清除超时定时器
584
+      if (window.responseTimeout) {
585
+        clearTimeout(window.responseTimeout);
586
+        window.responseTimeout = null;
587
+      }
588
+      
505
       if (aiMessage.content === '') {
589
       if (aiMessage.content === '') {
506
         aiMessage.content = '抱歉,我暂时无法回答您的问题,请稍后再试。';
590
         aiMessage.content = '抱歉,我暂时无法回答您的问题,请稍后再试。';
507
       } else {
591
       } else {
509
       }
593
       }
510
       isSending.value = false;
594
       isSending.value = false;
511
       console.log('=== 错误时isSending设置为false ===')
595
       console.log('=== 错误时isSending设置为false ===')
596
+      
597
+      // 清理控制器
598
+      if (window.currentController) {
599
+        window.currentController = null;
600
+      }
601
+      
512
       // 滚动到底部
602
       // 滚动到底部
513
       nextTick(() => {
603
       nextTick(() => {
514
         scrollToBottom();
604
         scrollToBottom();
516
     },
606
     },
517
     // onComplete: 回答完成时的回调
607
     // onComplete: 回答完成时的回调
518
     () => {
608
     () => {
519
-      console.log('=== 回答完成 ===')
520
-      isSending.value = false;
521
-      console.log('=== isSending设置为false ===')
609
+      console.log('=== 回答完成,正确进入onComplete ===')
610
+      
611
+      // 清除超时定时器
612
+      if (window.responseTimeout) {
613
+        clearTimeout(window.responseTimeout);
614
+        window.responseTimeout = null;
615
+      }
522
       
616
       
617
+      isSending.value = false;
618
+      console.log('=== onComplete中isSending设置为false ===')
619
+
620
+      // 清理控制器
621
+      if (window.currentController) {
622
+        window.currentController = null;
623
+      }
624
+
523
       // 确保状态更新
625
       // 确保状态更新
524
       nextTick(() => {
626
       nextTick(() => {
525
         console.log('=== 最终isSending状态 ===', isSending.value)
627
         console.log('=== 最终isSending状态 ===', isSending.value)
527
       });
629
       });
528
     }
630
     }
529
   );
631
   );
530
-
632
+  // 获取上下文引用文件
633
+  getContextFile(currentInput, selectedKnowledge.value.collectionName).then(response => {
634
+    console.log('=== 上下文文件 ===', response)
635
+    if (response && Array.isArray(response)) {
636
+      aiMessage.references = response.map(item => ({
637
+        fileName: item.file_name,
638
+        similarity: item.distance,
639
+        content: item.content
640
+      }));
641
+    }
642
+  }).catch(error => {
643
+    console.error('获取上下文文件失败:', error);
644
+  });
531
   // 如果用户快速发送多条消息,取消之前的请求
645
   // 如果用户快速发送多条消息,取消之前的请求
532
   if (window.currentController) {
646
   if (window.currentController) {
533
     window.currentController.abort();
647
     window.currentController.abort();
534
   }
648
   }
535
-  window.currentController = eventSource;
649
+  if (window.responseTimeout) {
650
+    clearTimeout(window.responseTimeout);
651
+  }
536
   
652
   
537
-  // 添加超时机制,确保状态能够正确切换
538
-  setTimeout(() => {
653
+  window.currentController = eventSource;
654
+
655
+  // 设置初始超时定时器(30秒无任何响应才超时)
656
+  window.responseTimeout = setTimeout(() => {
539
     if (isSending.value) {
657
     if (isSending.value) {
540
-      console.log('=== 超时强制结束 ===')
658
+      console.log('=== 初始连接超时强制结束 ===')
541
       isSending.value = false;
659
       isSending.value = false;
542
       if (window.currentController) {
660
       if (window.currentController) {
543
         window.currentController.abort();
661
         window.currentController.abort();
544
         window.currentController = null;
662
         window.currentController = null;
545
       }
663
       }
664
+      window.responseTimeout = null;
546
     }
665
     }
547
-  }, 10000); // 10秒超时
666
+  }, 30000); // 30秒超时
548
 }
667
 }
549
 
668
 
550
 /** 停止生成回答 */
669
 /** 停止生成回答 */
551
 function stopGenerating() {
670
 function stopGenerating() {
671
+  // 清除超时定时器
672
+  if (window.responseTimeout) {
673
+    clearTimeout(window.responseTimeout);
674
+    window.responseTimeout = null;
675
+  }
676
+  
552
   if (window.currentController) {
677
   if (window.currentController) {
553
     window.currentController.abort();
678
     window.currentController.abort();
554
     window.currentController = null;
679
     window.currentController = null;
583
   return content.replace(/\n/g, '<br>');
708
   return content.replace(/\n/g, '<br>');
584
 }
709
 }
585
 
710
 
586
-/** 格式化消息时间 */
587
-function formatMessageTime(time) {
588
-  if (!time) return '';
589
-  const date = new Date(time);
590
-  return date.toLocaleTimeString('zh-CN', {
591
-    hour: '2-digit',
592
-    minute: '2-digit'
593
-  });
711
+/** 格式化相似度 */
712
+function formatSimilarity(similarity) {
713
+  // 如果相似度已经是百分比形式(>1),直接返回
714
+  if (similarity > 1) {
715
+    return similarity.toFixed(1);
716
+  }
717
+  // 如果是0-1之间的值,转换为百分比
718
+  return (similarity * 100).toFixed(1);
719
+}
720
+
721
+/** 获取相似度的CSS类 */
722
+function getSimilarityClass(similarity) {
723
+  const percentage = similarity > 1 ? similarity : similarity * 100;
724
+  if (percentage >= 80) return 'high-similarity';
725
+  if (percentage >= 60) return 'medium-similarity';
726
+  return 'low-similarity';
727
+}
728
+
729
+/** 切换引用文件的展开/收起状态 */
730
+function toggleReferences(messageIndex) {
731
+  const message = chatMessages.value[messageIndex];
732
+  message.showAllReferences = !message.showAllReferences;
733
+}
734
+
735
+/** 获取要显示的引用文件列表 */
736
+function getDisplayedReferences(message) {
737
+  if (!message.references || message.references.length === 0) {
738
+    return [];
739
+  }
740
+  
741
+  // 如果引用文件数量 <= 3 或者设置为显示全部,则显示所有
742
+  if (message.references.length <= 3 || message.showAllReferences) {
743
+    return message.references;
744
+  }
745
+  
746
+  // 否则只显示前3个
747
+  return message.references.slice(0, 3);
594
 }
748
 }
595
 
749
 
596
 /** 处理卡片操作 */
750
 /** 处理卡片操作 */
619
         fileLoading.value = true;
773
         fileLoading.value = true;
620
         listKnowledgeDocument(selectedKnowledge.value.collectionName).then(response => {
774
         listKnowledgeDocument(selectedKnowledge.value.collectionName).then(response => {
621
           fileList.value = response.data;
775
           fileList.value = response.data;
776
+          fileTotal.value = response.data.length;
777
+
778
+          // 如果当前页没有数据了,回到上一页
779
+          const maxPage = Math.ceil(fileTotal.value / filePageSize.value);
780
+          if (filePageNum.value > maxPage && maxPage > 0) {
781
+            filePageNum.value = maxPage;
782
+          }
783
+
622
           fileLoading.value = false;
784
           fileLoading.value = false;
623
         }).catch(error => {
785
         }).catch(error => {
624
           fileLoading.value = false;
786
           fileLoading.value = false;
645
   }
807
   }
646
 }
808
 }
647
 
809
 
810
+/** 文件分页处理 */
811
+function handleFilePagination(pagination) {
812
+  filePageNum.value = pagination.page;
813
+  filePageSize.value = pagination.limit;
814
+}
815
+
648
 // 取消按钮
816
 // 取消按钮
649
 function cancel() {
817
 function cancel() {
650
   open.value = false;
818
   open.value = false;
786
     fileLoading.value = true;
954
     fileLoading.value = true;
787
     listKnowledgeDocument(selectedKnowledge.value.collectionName).then(response => {
955
     listKnowledgeDocument(selectedKnowledge.value.collectionName).then(response => {
788
       fileList.value = response.data;
956
       fileList.value = response.data;
957
+      fileTotal.value = response.data.length;
789
       fileLoading.value = false;
958
       fileLoading.value = false;
790
     }).catch(error => {
959
     }).catch(error => {
791
       fileLoading.value = false;
960
       fileLoading.value = false;
816
   getList();
985
   getList();
817
 });
986
 });
818
 
987
 
819
-// 组件卸载时清理请求控制器
988
+// 组件卸载时清理请求控制器和超时定时器
820
 onUnmounted(() => {
989
 onUnmounted(() => {
821
   if (window.currentController) {
990
   if (window.currentController) {
822
     window.currentController.abort();
991
     window.currentController.abort();
823
     window.currentController = null;
992
     window.currentController = null;
824
   }
993
   }
994
+  if (window.responseTimeout) {
995
+    clearTimeout(window.responseTimeout);
996
+    window.responseTimeout = null;
997
+  }
825
 });
998
 });
826
 </script>
999
 </script>
827
 
1000
 
1025
   padding: 12px 16px;
1198
   padding: 12px 16px;
1026
   background: #f0f9ff;
1199
   background: #f0f9ff;
1027
   border-radius: 6px;
1200
   border-radius: 6px;
1028
-  margin: 20px 20px 0 20px;
1201
+  margin-bottom: 20px;
1029
 
1202
 
1030
   .folder-icon {
1203
   .folder-icon {
1031
     color: #409eff;
1204
     color: #409eff;
1156
     white-space: pre-wrap;
1329
     white-space: pre-wrap;
1157
   }
1330
   }
1158
 
1331
 
1332
+  .message-references {
1333
+    margin: 12px 0;
1334
+    padding: 12px;
1335
+    background: rgba(0, 0, 0, 0.05);
1336
+    border-radius: 6px;
1337
+    border-left: 3px solid #409eff;
1338
+
1339
+    .references-header {
1340
+      display: flex;
1341
+      align-items: center;
1342
+      justify-content: space-between;
1343
+      gap: 6px;
1344
+      margin-bottom: 8px;
1345
+      font-size: 13px;
1346
+      font-weight: 600;
1347
+      color: #409eff;
1348
+
1349
+      .reference-icon {
1350
+        font-size: 14px;
1351
+      }
1352
+
1353
+      .toggle-references {
1354
+        font-size: 11px;
1355
+        color: #909399;
1356
+        padding: 0;
1357
+        height: auto;
1358
+        
1359
+        &:hover {
1360
+          color: #409eff;
1361
+        }
1362
+      }
1363
+    }
1364
+
1365
+    .references-list {
1366
+      display: flex;
1367
+      flex-direction: column;
1368
+      gap: 6px;
1369
+    }
1370
+
1371
+    .reference-item {
1372
+      padding: 6px 8px;
1373
+      background: rgba(255, 255, 255, 0.8);
1374
+      border-radius: 4px;
1375
+      border: 1px solid rgba(0, 0, 0, 0.1);
1376
+
1377
+      .reference-info {
1378
+        display: flex;
1379
+        justify-content: space-between;
1380
+        align-items: center;
1381
+        gap: 8px;
1382
+
1383
+        .reference-filename {
1384
+          font-size: 12px;
1385
+          color: #303133;
1386
+          font-weight: 500;
1387
+          flex: 1;
1388
+          overflow: hidden;
1389
+          text-overflow: ellipsis;
1390
+          white-space: nowrap;
1391
+        }
1392
+
1393
+                 .reference-similarity {
1394
+           font-size: 11px;
1395
+           padding: 2px 6px;
1396
+           border-radius: 10px;
1397
+           white-space: nowrap;
1398
+           font-weight: 500;
1399
+           transition: all 0.3s ease;
1400
+
1401
+           &.high-similarity {
1402
+             color: #67c23a;
1403
+             background: #e1f3d8;
1404
+           }
1405
+
1406
+           &.medium-similarity {
1407
+             color: #e6a23c;
1408
+             background: #fdf5e6;
1409
+           }
1410
+
1411
+           &.low-similarity {
1412
+             color: #f56c6c;
1413
+             background: #fef0f0;
1414
+           }
1415
+         }
1416
+      }
1417
+    }
1418
+  }
1419
+
1159
   .message-time {
1420
   .message-time {
1160
     font-size: 12px;
1421
     font-size: 12px;
1161
     opacity: 0.7;
1422
     opacity: 0.7;
1170
 }
1431
 }
1171
 
1432
 
1172
 @keyframes blink {
1433
 @keyframes blink {
1173
-  0%, 50% { opacity: 1; }
1174
-  51%, 100% { opacity: 0; }
1434
+
1435
+  0%,
1436
+  50% {
1437
+    opacity: 1;
1438
+  }
1439
+
1440
+  51%,
1441
+  100% {
1442
+    opacity: 0;
1443
+  }
1175
 }
1444
 }
1176
 
1445
 
1177
 .chat-input-area {
1446
 .chat-input-area {
1202
   display: flex;
1471
   display: flex;
1203
   justify-content: space-between;
1472
   justify-content: space-between;
1204
   align-items: center;
1473
   align-items: center;
1205
-  
1474
+
1206
   .char-count {
1475
   .char-count {
1207
     color: #409eff;
1476
     color: #409eff;
1208
     font-weight: 500;
1477
     font-weight: 500;
1209
-    
1478
+
1210
     &.warning {
1479
     &.warning {
1211
       color: #e6a23c;
1480
       color: #e6a23c;
1212
     }
1481
     }
1213
-    
1482
+
1214
     .error-text {
1483
     .error-text {
1215
       color: #f56c6c;
1484
       color: #f56c6c;
1216
       font-size: 11px;
1485
       font-size: 11px;
1283
   }
1552
   }
1284
 }
1553
 }
1285
 
1554
 
1555
+// 文件列表分页样式
1556
+.file-content {
1557
+  :deep(.pagination-container) {
1558
+    margin-top: 20px;
1559
+    padding: 20px 0;
1560
+    text-align: center;
1561
+    background: transparent;
1562
+  }
1563
+}
1564
+
1286
 .empty-state {
1565
 .empty-state {
1287
   display: flex;
1566
   display: flex;
1288
   flex-direction: column;
1567
   flex-direction: column;
1315
 }
1594
 }
1316
 
1595
 
1317
 :deep(.el-upload-dragger) {
1596
 :deep(.el-upload-dragger) {
1318
-  width: 100%;
1597
+  width: 350px;
1319
 }
1598
 }
1320
 
1599
 
1321
 :deep(.el-upload__text) {
1600
 :deep(.el-upload__text) {
1330
   margin-top: 7px;
1609
   margin-top: 7px;
1331
 }
1610
 }
1332
 
1611
 
1612
+// 修复上传文件列表中文件名过长的问题
1613
+:deep(.el-upload-list) {
1614
+  max-width: 100%;
1615
+
1616
+  .el-upload-list__item {
1617
+    max-width: 100%;
1618
+    overflow: hidden;
1619
+
1620
+    .el-upload-list__item-name {
1621
+      width: 350px; // 为删除按钮等留出空间
1622
+      white-space: nowrap;
1623
+      overflow: hidden;
1624
+      text-overflow: ellipsis;
1625
+      display: inline-block;
1626
+      vertical-align: middle;
1627
+    }
1628
+  }
1629
+}
1630
+
1631
+// 修复拖拽区域内文件名显示问题
1632
+:deep(.el-upload-dragger) {
1633
+  .el-upload-list {
1634
+    .el-upload-list__item {
1635
+      margin: 0 10px 10px 10px;
1636
+      padding: 8px 12px;
1637
+      background: #f5f7fa;
1638
+      border-radius: 4px;
1639
+
1640
+      .el-upload-list__item-name {
1641
+        color: #606266;
1642
+        font-size: 13px;
1643
+        max-width: calc(100% - 60px);
1644
+        word-wrap: break-word;
1645
+        word-break: break-all;
1646
+        white-space: normal;
1647
+        line-height: 1.4;
1648
+      }
1649
+    }
1650
+  }
1651
+}
1652
+
1333
 // 响应式设计
1653
 // 响应式设计
1334
 @media (max-width: 1200px) {
1654
 @media (max-width: 1200px) {
1335
   .main-content {
1655
   .main-content {
1375
   .message-content {
1695
   .message-content {
1376
     max-width: 85%;
1696
     max-width: 85%;
1377
   }
1697
   }
1698
+
1699
+  .message-references {
1700
+    .references-header {
1701
+      flex-direction: column;
1702
+      align-items: flex-start;
1703
+      gap: 4px;
1704
+
1705
+      .toggle-references {
1706
+        font-size: 10px;
1707
+      }
1708
+    }
1709
+
1710
+    .reference-item {
1711
+      .reference-info {
1712
+        flex-direction: column;
1713
+        align-items: flex-start;
1714
+        gap: 4px;
1715
+
1716
+        .reference-filename {
1717
+          font-size: 11px;
1718
+        }
1719
+
1720
+        .reference-similarity {
1721
+          font-size: 10px;
1722
+          align-self: flex-end;
1723
+        }
1724
+      }
1725
+    }
1726
+  }
1378
 }
1727
 }
1379
 
1728
 
1380
 // Loading动画样式
1729
 // Loading动画样式

Loading…
Avbryt
Spara