Sfoglia il codice sorgente

知识库新增引用文件;

余思翰 1 settimana fa
parent
commit
46affa7d34

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

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

+ 10
- 1
llm-ui/src/api/llm/rag.js Vedi File

@@ -2,7 +2,7 @@
2 2
  * @Author: ysh
3 3
  * @Date: 2025-07-08 16:43:22
4 4
  * @LastEditors: Please set LastEditors
5
- * @LastEditTime: 2025-07-11 14:15:01
5
+ * @LastEditTime: 2025-07-16 15:49:45
6 6
  */
7 7
 import request from '@/utils/request'
8 8
 import { getToken } from '@/utils/auth'
@@ -16,6 +16,15 @@ export function getAnswer(question, collectionName) {
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 28
 // 流式回答API - 使用fetch API处理流式响应
20 29
 export function getAnswerStream(question, collectionName, onMessage, onError, onComplete) {
21 30
   const baseURL = import.meta.env.VITE_APP_BASE_API

+ 395
- 46
llm-ui/src/views/llm/knowledge/index.vue Vedi File

@@ -124,7 +124,7 @@
124 124
           </div>
125 125
 
126 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 128
               <div class="file-info">
129 129
                 <el-icon class="file-icon">
130 130
                   <Document />
@@ -143,6 +143,10 @@
143 143
               </div>
144 144
             </div>
145 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 150
         </div>
147 151
 
148 152
         <!-- 聊天模式 -->
@@ -167,7 +171,7 @@
167 171
             </div>
168 172
 
169 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 175
                 :class="[message.type, { 'typing': message.type === 'ai' && isSending && index === chatMessages.length - 1 }]">
172 176
                 <div class="message-avatar">
173 177
                   <div v-if="message.type === 'user'" class="user-avatar">
@@ -182,13 +186,47 @@
182 186
                 <div class="message-content">
183 187
                   <div class="message-bubble" :class="message.type">
184 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 222
                   </div>
187 223
                 </div>
188 224
               </div>
189 225
 
190 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 230
                 <div class="message-avatar">
193 231
                   <div class="ai-avatar">
194 232
                     <el-icon>
@@ -212,10 +250,9 @@
212 250
           <!-- 输入区域 -->
213 251
           <div class="chat-input-area">
214 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 256
               <div class="input-actions">
220 257
                 <el-button v-if="!isSending" type="primary" :icon="Promotion" @click="sendMessage"
221 258
                   :disabled="!chatInput.trim() || chatInput.length > 2000">
@@ -297,7 +334,7 @@
297 334
 import { ref, reactive, getCurrentInstance, onMounted, onUnmounted, nextTick, watch, computed } from 'vue'
298 335
 import { Search, Refresh, Plus, Edit, Delete, Upload, UploadFilled, Folder, MoreFilled, FolderOpened, Document, ChatRound, User, ChatDotRound, Promotion } from '@element-plus/icons-vue'
299 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 338
 import { getToken } from "@/utils/auth";
302 339
 
303 340
 const { proxy } = getCurrentInstance();
@@ -317,6 +354,16 @@ const isModify = ref(false);
317 354
 const selectedKnowledge = ref(null);
318 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 368
 const isChatMode = ref(false);
322 369
 const chatMessages = ref([]);
@@ -375,6 +422,11 @@ function getList() {
375 422
     // 确保返回的数据是数组格式
376 423
     if (Array.isArray(response.data)) {
377 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 430
     } else {
379 431
       knowledgeList.value = [];
380 432
     }
@@ -391,16 +443,21 @@ function selectKnowledge(knowledge) {
391 443
   if (isSending.value) {
392 444
     stopGenerating();
393 445
   }
394
-  
446
+
395 447
   selectedKnowledge.value = knowledge;
396 448
   // 重置聊天模式
397 449
   isChatMode.value = false;
398 450
   chatMessages.value = [];
399 451
   chatInput.value = '';
400 452
 
453
+  // 重置文件分页状态
454
+  filePageNum.value = 1;
455
+  filePageSize.value = 10;
456
+
401 457
   fileLoading.value = true;
402 458
   listKnowledgeDocument(knowledge.collectionName).then(response => {
403 459
     fileList.value = response.data;
460
+    fileTotal.value = response.data.length;
404 461
     fileLoading.value = false;
405 462
   }).catch(error => {
406 463
     fileLoading.value = false;
@@ -418,7 +475,7 @@ function handleChat(knowledge) {
418 475
   if (isSending.value) {
419 476
     stopGenerating();
420 477
   }
421
-  
478
+
422 479
   isChatMode.value = true;
423 480
   chatMessages.value = [];
424 481
   chatInput.value = '';
@@ -434,14 +491,14 @@ function switchToFileMode() {
434 491
   if (isSending.value) {
435 492
     stopGenerating();
436 493
   }
437
-  
494
+
438 495
   isChatMode.value = false;
439 496
 }
440 497
 
441 498
 /** 发送消息 */
442 499
 function sendMessage() {
443 500
   if (!chatInput.value.trim()) return;
444
-  
501
+
445 502
   // 如果正在发送,先停止当前请求
446 503
   if (isSending.value) {
447 504
     stopGenerating();
@@ -450,7 +507,7 @@ function sendMessage() {
450 507
   const userMessage = {
451 508
     type: 'user',
452 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 513
   chatMessages.value.push(userMessage);
@@ -467,33 +524,53 @@ function sendMessage() {
467 524
   const aiMessage = reactive({
468 525
     type: 'ai',
469 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 531
   chatMessages.value.push(aiMessage);
473 532
 
474 533
   // 使用流式API获取回答
475 534
   const eventSource = getAnswerStream(
476
-    currentInput, 
535
+    currentInput,
477 536
     selectedKnowledge.value.collectionName,
478
-        // onMessage: 接收到每个字符时的回调
537
+    // onMessage: 接收到每个字符时的回调
479 538
     (content) => {
480 539
       // 处理接收到的内容
481 540
       console.log('=== 前端接收到内容 ===', content)
482
-      
541
+
483 542
       // 清理内容中的</think>标签
484 543
       let cleanContent = content.replace(/<\/?think>/g, '');
485
-      
544
+
486 545
       // 如果内容为空或只包含空白字符,跳过
487 546
       if (!cleanContent.trim()) {
488 547
         return;
489 548
       }
490
-      
549
+
491 550
       // 直接替换内容,避免重复叠加
492 551
       aiMessage.content = cleanContent;
493
-      
552
+      aiMessage.time = proxy.parseTime(new Date(), '{y}-{m}-{d} {h}:{i}:{s}');
494 553
       console.log('=== 清理后的内容 ===', cleanContent)
495 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 575
       nextTick(() => {
499 576
         scrollToBottom();
@@ -502,6 +579,13 @@ function sendMessage() {
502 579
     // onError: 发生错误时的回调
503 580
     (error) => {
504 581
       console.error('=== 流式回答错误 ===', error);
582
+      
583
+      // 清除超时定时器
584
+      if (window.responseTimeout) {
585
+        clearTimeout(window.responseTimeout);
586
+        window.responseTimeout = null;
587
+      }
588
+      
505 589
       if (aiMessage.content === '') {
506 590
         aiMessage.content = '抱歉,我暂时无法回答您的问题,请稍后再试。';
507 591
       } else {
@@ -509,6 +593,12 @@ function sendMessage() {
509 593
       }
510 594
       isSending.value = false;
511 595
       console.log('=== 错误时isSending设置为false ===')
596
+      
597
+      // 清理控制器
598
+      if (window.currentController) {
599
+        window.currentController = null;
600
+      }
601
+      
512 602
       // 滚动到底部
513 603
       nextTick(() => {
514 604
         scrollToBottom();
@@ -516,10 +606,22 @@ function sendMessage() {
516 606
     },
517 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 626
       nextTick(() => {
525 627
         console.log('=== 最终isSending状态 ===', isSending.value)
@@ -527,28 +629,51 @@ function sendMessage() {
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 646
   if (window.currentController) {
533 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 657
     if (isSending.value) {
540
-      console.log('=== 超时强制结束 ===')
658
+      console.log('=== 初始连接超时强制结束 ===')
541 659
       isSending.value = false;
542 660
       if (window.currentController) {
543 661
         window.currentController.abort();
544 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 670
 function stopGenerating() {
671
+  // 清除超时定时器
672
+  if (window.responseTimeout) {
673
+    clearTimeout(window.responseTimeout);
674
+    window.responseTimeout = null;
675
+  }
676
+  
552 677
   if (window.currentController) {
553 678
     window.currentController.abort();
554 679
     window.currentController = null;
@@ -583,14 +708,43 @@ function formatMessage(content) {
583 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,6 +773,14 @@ function handleDeleteFile(file) {
619 773
         fileLoading.value = true;
620 774
         listKnowledgeDocument(selectedKnowledge.value.collectionName).then(response => {
621 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 784
           fileLoading.value = false;
623 785
         }).catch(error => {
624 786
           fileLoading.value = false;
@@ -645,6 +807,12 @@ function getFileType(fileName) {
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 817
 function cancel() {
650 818
   open.value = false;
@@ -786,6 +954,7 @@ function handleFileSuccess(response, file, fileList) {
786 954
     fileLoading.value = true;
787 955
     listKnowledgeDocument(selectedKnowledge.value.collectionName).then(response => {
788 956
       fileList.value = response.data;
957
+      fileTotal.value = response.data.length;
789 958
       fileLoading.value = false;
790 959
     }).catch(error => {
791 960
       fileLoading.value = false;
@@ -816,12 +985,16 @@ onMounted(() => {
816 985
   getList();
817 986
 });
818 987
 
819
-// 组件卸载时清理请求控制器
988
+// 组件卸载时清理请求控制器和超时定时器
820 989
 onUnmounted(() => {
821 990
   if (window.currentController) {
822 991
     window.currentController.abort();
823 992
     window.currentController = null;
824 993
   }
994
+  if (window.responseTimeout) {
995
+    clearTimeout(window.responseTimeout);
996
+    window.responseTimeout = null;
997
+  }
825 998
 });
826 999
 </script>
827 1000
 
@@ -1025,7 +1198,7 @@ onUnmounted(() => {
1025 1198
   padding: 12px 16px;
1026 1199
   background: #f0f9ff;
1027 1200
   border-radius: 6px;
1028
-  margin: 20px 20px 0 20px;
1201
+  margin-bottom: 20px;
1029 1202
 
1030 1203
   .folder-icon {
1031 1204
     color: #409eff;
@@ -1156,6 +1329,94 @@ onUnmounted(() => {
1156 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 1420
   .message-time {
1160 1421
     font-size: 12px;
1161 1422
     opacity: 0.7;
@@ -1170,8 +1431,16 @@ onUnmounted(() => {
1170 1431
 }
1171 1432
 
1172 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 1446
 .chat-input-area {
@@ -1202,15 +1471,15 @@ onUnmounted(() => {
1202 1471
   display: flex;
1203 1472
   justify-content: space-between;
1204 1473
   align-items: center;
1205
-  
1474
+
1206 1475
   .char-count {
1207 1476
     color: #409eff;
1208 1477
     font-weight: 500;
1209
-    
1478
+
1210 1479
     &.warning {
1211 1480
       color: #e6a23c;
1212 1481
     }
1213
-    
1482
+
1214 1483
     .error-text {
1215 1484
       color: #f56c6c;
1216 1485
       font-size: 11px;
@@ -1283,6 +1552,16 @@ onUnmounted(() => {
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 1565
 .empty-state {
1287 1566
   display: flex;
1288 1567
   flex-direction: column;
@@ -1315,7 +1594,7 @@ onUnmounted(() => {
1315 1594
 }
1316 1595
 
1317 1596
 :deep(.el-upload-dragger) {
1318
-  width: 100%;
1597
+  width: 350px;
1319 1598
 }
1320 1599
 
1321 1600
 :deep(.el-upload__text) {
@@ -1330,6 +1609,47 @@ onUnmounted(() => {
1330 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 1654
 @media (max-width: 1200px) {
1335 1655
   .main-content {
@@ -1375,6 +1695,35 @@ onUnmounted(() => {
1375 1695
   .message-content {
1376 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 1729
 // Loading动画样式

Loading…
Annulla
Salva