|
|
@@ -71,24 +71,31 @@
|
|
71
|
71
|
<!-- 右侧面板 -->
|
|
72
|
72
|
<div class="right-panel">
|
|
73
|
73
|
<div class="panel-header">
|
|
74
|
|
- <h3>{{ isChatMode ? '知识库对话' : '文件列表' }}</h3>
|
|
|
74
|
+ <h3>{{ isChatMode ? '知识库对话' : (isTitleMode ? '标题列表' : '文件列表') }}</h3>
|
|
75
|
75
|
<div class="header-actions">
|
|
76
|
|
- <el-button v-if="!isChatMode" type="primary" icon="el-icon-chat-round" @click="handleChat(selectedKnowledge)"
|
|
|
76
|
+ <el-button v-if="!isChatMode && !isTitleMode" type="primary" icon="el-icon-chat-round" @click="handleChat(selectedKnowledge)"
|
|
77
|
77
|
:disabled="!selectedKnowledge" v-hasPermi="['llm:knowledge:chat']">
|
|
78
|
78
|
开始对话
|
|
79
|
79
|
</el-button>
|
|
80
|
|
- <el-button v-if="!isChatMode" type="primary" icon="el-icon-upload" @click="handleUpload(selectedKnowledge)"
|
|
|
80
|
+ <el-button v-if="!isChatMode && !isTitleMode" type="primary" icon="el-icon-upload" @click="handleUpload(selectedKnowledge)"
|
|
81
|
81
|
:disabled="!selectedKnowledge" v-hasPermi="['llm:knowledge:upload']">
|
|
82
|
82
|
上传文件
|
|
83
|
83
|
</el-button>
|
|
|
84
|
+ <el-button v-if="!isChatMode && !isTitleMode" type="default" icon="el-icon-notebook-2" @click="switchToTitleMode"
|
|
|
85
|
+ :disabled="!selectedKnowledge">
|
|
|
86
|
+ 查看内容
|
|
|
87
|
+ </el-button>
|
|
84
|
88
|
<el-button v-if="isChatMode" type="default" icon="el-icon-document" @click="switchToFileMode">
|
|
85
|
89
|
返回文件列表
|
|
86
|
90
|
</el-button>
|
|
|
91
|
+ <el-button v-if="isTitleMode" type="default" icon="el-icon-document" @click="switchToFileMode">
|
|
|
92
|
+ 返回文件列表
|
|
|
93
|
+ </el-button>
|
|
87
|
94
|
</div>
|
|
88
|
95
|
</div>
|
|
89
|
96
|
|
|
90
|
97
|
<!-- 文件列表模式 -->
|
|
91
|
|
- <div v-if="!isChatMode" class="file-content" v-loading="fileLoading">
|
|
|
98
|
+ <div v-if="!isChatMode && !isTitleMode" class="file-content" v-loading="fileLoading">
|
|
92
|
99
|
<div v-if="selectedKnowledge" class="selected-knowledge">
|
|
93
|
100
|
<i class="el-icon-folder folder-icon">
|
|
94
|
101
|
</i>
|
|
|
@@ -133,6 +140,62 @@
|
|
133
|
140
|
@pagination="handleFilePagination" :autoScroll="false" />
|
|
134
|
141
|
</div>
|
|
135
|
142
|
|
|
|
143
|
+ <!-- 标题列表模式 -->
|
|
|
144
|
+ <div v-if="!isChatMode && isTitleMode" class="title-content" v-loading="titleLoading">
|
|
|
145
|
+ <div v-if="selectedKnowledge" class="selected-knowledge">
|
|
|
146
|
+ <i class="el-icon-folder folder-icon">
|
|
|
147
|
+ </i>
|
|
|
148
|
+ <span class="knowledge-name">{{ selectedKnowledge.collectionName }}</span>
|
|
|
149
|
+ <span class="title-mode-badge">标题模式</span>
|
|
|
150
|
+ </div>
|
|
|
151
|
+
|
|
|
152
|
+ <div v-if="!selectedKnowledge" class="empty-state">
|
|
|
153
|
+ <i class="el-icon-notebook-2 empty-icon">
|
|
|
154
|
+ </i>
|
|
|
155
|
+ <p>请选择一个知识库查看内容</p>
|
|
|
156
|
+ </div>
|
|
|
157
|
+
|
|
|
158
|
+ <div v-else-if="titleList.length === 0" class="empty-state">
|
|
|
159
|
+ <i class="el-icon-notebook-2 empty-icon">
|
|
|
160
|
+ </i>
|
|
|
161
|
+ <p>该知识库暂无标题</p>
|
|
|
162
|
+ </div>
|
|
|
163
|
+
|
|
|
164
|
+ <div v-else class="title-content-container">
|
|
|
165
|
+ <!-- 左侧标题列表 -->
|
|
|
166
|
+ <div class="title-list">
|
|
|
167
|
+ <div v-for="(title, index) in titleList" :key="index" class="title-item"
|
|
|
168
|
+ :class="{ 'active': selectedTitle === title }"
|
|
|
169
|
+ @click="selectTitle(title)">
|
|
|
170
|
+ <i class="el-icon-notebook-2 title-icon"></i>
|
|
|
171
|
+ <span class="title-text">{{ title }}</span>
|
|
|
172
|
+ </div>
|
|
|
173
|
+ </div>
|
|
|
174
|
+
|
|
|
175
|
+ <!-- 右侧内容列表 -->
|
|
|
176
|
+ <div class="content-list" v-loading="contentLoading">
|
|
|
177
|
+ <div v-if="!selectedTitle" class="empty-state">
|
|
|
178
|
+ <i class="el-icon-document empty-icon"></i>
|
|
|
179
|
+ <p>请选择一个标题查看内容</p>
|
|
|
180
|
+ </div>
|
|
|
181
|
+ <div v-else-if="contentList.length === 0" class="empty-state">
|
|
|
182
|
+ <i class="el-icon-document empty-icon"></i>
|
|
|
183
|
+ <p>该标题暂无内容</p>
|
|
|
184
|
+ </div>
|
|
|
185
|
+ <div v-else class="content-items">
|
|
|
186
|
+ <div v-for="(content, index) in contentList" :key="index" class="content-item">
|
|
|
187
|
+ <div class="content-header">
|
|
|
188
|
+ <span class="content-index">{{ index + 1 }}</span>
|
|
|
189
|
+ </div>
|
|
|
190
|
+ <div class="content-body">
|
|
|
191
|
+ {{ content }}
|
|
|
192
|
+ </div>
|
|
|
193
|
+ </div>
|
|
|
194
|
+ </div>
|
|
|
195
|
+ </div>
|
|
|
196
|
+ </div>
|
|
|
197
|
+ </div>
|
|
|
198
|
+
|
|
136
|
199
|
<!-- 聊天模式 -->
|
|
137
|
200
|
<div v-else class="chat-content">
|
|
138
|
201
|
<div v-if="selectedKnowledge" class="selected-knowledge">
|
|
|
@@ -302,7 +365,7 @@
|
|
302
|
365
|
import { parseTime } from "@/utils/ruoyi";
|
|
303
|
366
|
import { Message } from 'element-ui'
|
|
304
|
367
|
import { getToken } from "@/utils/auth";
|
|
305
|
|
-import { listKnowledge, listKnowLedgeByCollectionName, addKnowledge, updateKnowledge, delKnowledge, insertKnowledgeFile, listKnowledgeDocument, deleteKnowledgeFile, getProcessValue } from "@/api/llm/knowLedge";
|
|
|
368
|
+import { listKnowledge, listKnowLedgeByCollectionName, addKnowledge, updateKnowledge, delKnowledge, insertKnowledgeFile, listKnowledgeDocument, deleteKnowledgeFile, getProcessValue, listTiles, listByTitle } from "@/api/llm/knowLedge";
|
|
306
|
369
|
import { getAnswer, getAnswerStream, getContextFile } from '@/api/llm/rag';
|
|
307
|
370
|
import { marked } from 'marked';
|
|
308
|
371
|
|
|
|
@@ -451,10 +514,28 @@ export default {
|
|
451
|
514
|
},
|
|
452
|
515
|
|
|
453
|
516
|
// 定时器
|
|
454
|
|
- timer: null
|
|
|
517
|
+ timer: null,
|
|
|
518
|
+
|
|
|
519
|
+ // 标题和内容相关状态
|
|
|
520
|
+ isTitleMode: false, // 是否显示标题模式
|
|
|
521
|
+ titleList: [], // 标题列表
|
|
|
522
|
+ contentList: [], // 内容列表
|
|
|
523
|
+ selectedTitle: null, // 选中的标题
|
|
|
524
|
+ titleLoading: false, // 标题加载状态
|
|
|
525
|
+ contentLoading: false // 内容加载状态
|
|
455
|
526
|
}
|
|
456
|
527
|
},
|
|
457
|
528
|
mounted() {
|
|
|
529
|
+ // 重置状态,确保页面刷新后不会停留在对话模式或标题模式
|
|
|
530
|
+ this.isChatMode = false;
|
|
|
531
|
+ this.isTitleMode = false;
|
|
|
532
|
+ this.chatMessages = [];
|
|
|
533
|
+ this.chatInput = '';
|
|
|
534
|
+ this.streamingStarted = false;
|
|
|
535
|
+ this.titleList = [];
|
|
|
536
|
+ this.contentList = [];
|
|
|
537
|
+ this.selectedTitle = null;
|
|
|
538
|
+
|
|
458
|
539
|
this.getList();
|
|
459
|
540
|
},
|
|
460
|
541
|
|
|
|
@@ -581,6 +662,50 @@ export default {
|
|
581
|
662
|
}
|
|
582
|
663
|
|
|
583
|
664
|
this.isChatMode = false;
|
|
|
665
|
+ this.isTitleMode = false;
|
|
|
666
|
+ },
|
|
|
667
|
+
|
|
|
668
|
+ /** 切换到标题模式 */
|
|
|
669
|
+ switchToTitleMode() {
|
|
|
670
|
+ this.isTitleMode = true;
|
|
|
671
|
+ this.isChatMode = false;
|
|
|
672
|
+ this.titleList = [];
|
|
|
673
|
+ this.contentList = [];
|
|
|
674
|
+ this.selectedTitle = null;
|
|
|
675
|
+
|
|
|
676
|
+ // 获取标题列表
|
|
|
677
|
+ this.titleLoading = true;
|
|
|
678
|
+ listTiles(this.selectedKnowledge.collectionName).then(response => {
|
|
|
679
|
+ if (Array.isArray(response.data)) {
|
|
|
680
|
+ this.titleList = response.data;
|
|
|
681
|
+ } else {
|
|
|
682
|
+ this.titleList = [];
|
|
|
683
|
+ }
|
|
|
684
|
+ this.titleLoading = false;
|
|
|
685
|
+ }).catch(error => {
|
|
|
686
|
+ this.titleLoading = false;
|
|
|
687
|
+ this.$modal.msgError("获取标题列表失败:" + error.message);
|
|
|
688
|
+ });
|
|
|
689
|
+ },
|
|
|
690
|
+
|
|
|
691
|
+ /** 选择标题 */
|
|
|
692
|
+ selectTitle(title) {
|
|
|
693
|
+ this.selectedTitle = title;
|
|
|
694
|
+ this.contentList = [];
|
|
|
695
|
+
|
|
|
696
|
+ // 获取内容列表
|
|
|
697
|
+ this.contentLoading = true;
|
|
|
698
|
+ listByTitle(this.selectedKnowledge.collectionName, title).then(response => {
|
|
|
699
|
+ if (Array.isArray(response.data)) {
|
|
|
700
|
+ this.contentList = response.data;
|
|
|
701
|
+ } else {
|
|
|
702
|
+ this.contentList = [];
|
|
|
703
|
+ }
|
|
|
704
|
+ this.contentLoading = false;
|
|
|
705
|
+ }).catch(error => {
|
|
|
706
|
+ this.contentLoading = false;
|
|
|
707
|
+ this.$modal.msgError("获取内容列表失败:" + error.message);
|
|
|
708
|
+ });
|
|
584
|
709
|
},
|
|
585
|
710
|
|
|
586
|
711
|
/** 发送消息 */
|
|
|
@@ -1282,6 +1407,131 @@ export default {
|
|
1282
|
1407
|
overflow-y: auto;
|
|
1283
|
1408
|
}
|
|
1284
|
1409
|
|
|
|
1410
|
+.title-content {
|
|
|
1411
|
+ flex: 1;
|
|
|
1412
|
+ padding: 20px;
|
|
|
1413
|
+ overflow: hidden;
|
|
|
1414
|
+}
|
|
|
1415
|
+
|
|
|
1416
|
+.title-content-container {
|
|
|
1417
|
+ display: flex;
|
|
|
1418
|
+ height: calc(100vh - 240px);
|
|
|
1419
|
+ gap: 20px;
|
|
|
1420
|
+ overflow: hidden;
|
|
|
1421
|
+}
|
|
|
1422
|
+
|
|
|
1423
|
+.title-list {
|
|
|
1424
|
+ width: 300px;
|
|
|
1425
|
+ background: #fafbfc;
|
|
|
1426
|
+ border-radius: 8px;
|
|
|
1427
|
+ border: 1px solid #e4e7ed;
|
|
|
1428
|
+ padding: 12px;
|
|
|
1429
|
+ overflow-y: auto;
|
|
|
1430
|
+}
|
|
|
1431
|
+
|
|
|
1432
|
+.title-item {
|
|
|
1433
|
+ display: flex;
|
|
|
1434
|
+ align-items: center;
|
|
|
1435
|
+ gap: 8px;
|
|
|
1436
|
+ padding: 10px 12px;
|
|
|
1437
|
+ margin-bottom: 8px;
|
|
|
1438
|
+ border-radius: 6px;
|
|
|
1439
|
+ cursor: pointer;
|
|
|
1440
|
+ transition: all 0.3s ease;
|
|
|
1441
|
+ background: white;
|
|
|
1442
|
+ border: 1px solid #e4e7ed;
|
|
|
1443
|
+
|
|
|
1444
|
+ &:hover {
|
|
|
1445
|
+ border-color: #409eff;
|
|
|
1446
|
+ background: #f0f9ff;
|
|
|
1447
|
+ }
|
|
|
1448
|
+
|
|
|
1449
|
+ &.active {
|
|
|
1450
|
+ border-color: #409eff;
|
|
|
1451
|
+ background: #e6f7ff;
|
|
|
1452
|
+ box-shadow: 0 2px 8px rgba(64, 158, 255, 0.15);
|
|
|
1453
|
+ }
|
|
|
1454
|
+
|
|
|
1455
|
+ .title-icon {
|
|
|
1456
|
+ color: #409eff;
|
|
|
1457
|
+ font-size: 16px;
|
|
|
1458
|
+ }
|
|
|
1459
|
+
|
|
|
1460
|
+ .title-text {
|
|
|
1461
|
+ flex: 1;
|
|
|
1462
|
+ font-size: 14px;
|
|
|
1463
|
+ color: #303133;
|
|
|
1464
|
+ line-height: 1.4;
|
|
|
1465
|
+ overflow: hidden;
|
|
|
1466
|
+ text-overflow: ellipsis;
|
|
|
1467
|
+ white-space: nowrap;
|
|
|
1468
|
+ }
|
|
|
1469
|
+}
|
|
|
1470
|
+
|
|
|
1471
|
+.content-list {
|
|
|
1472
|
+ flex: 1;
|
|
|
1473
|
+ background: #fafbfc;
|
|
|
1474
|
+ border-radius: 8px;
|
|
|
1475
|
+ border: 1px solid #e4e7ed;
|
|
|
1476
|
+ padding: 20px;
|
|
|
1477
|
+ overflow-y: auto;
|
|
|
1478
|
+}
|
|
|
1479
|
+
|
|
|
1480
|
+.content-items {
|
|
|
1481
|
+ display: flex;
|
|
|
1482
|
+ flex-direction: column;
|
|
|
1483
|
+ gap: 16px;
|
|
|
1484
|
+}
|
|
|
1485
|
+
|
|
|
1486
|
+.content-item {
|
|
|
1487
|
+ background: white;
|
|
|
1488
|
+ border: 1px solid #e4e7ed;
|
|
|
1489
|
+ border-radius: 8px;
|
|
|
1490
|
+ padding: 16px;
|
|
|
1491
|
+ transition: all 0.3s ease;
|
|
|
1492
|
+
|
|
|
1493
|
+ &:hover {
|
|
|
1494
|
+ border-color: #409eff;
|
|
|
1495
|
+ box-shadow: 0 2px 8px rgba(64, 158, 255, 0.15);
|
|
|
1496
|
+ }
|
|
|
1497
|
+
|
|
|
1498
|
+ .content-header {
|
|
|
1499
|
+ display: flex;
|
|
|
1500
|
+ align-items: center;
|
|
|
1501
|
+ margin-bottom: 12px;
|
|
|
1502
|
+
|
|
|
1503
|
+ .content-index {
|
|
|
1504
|
+ background: #409eff;
|
|
|
1505
|
+ color: white;
|
|
|
1506
|
+ width: 24px;
|
|
|
1507
|
+ height: 24px;
|
|
|
1508
|
+ border-radius: 50%;
|
|
|
1509
|
+ display: flex;
|
|
|
1510
|
+ align-items: center;
|
|
|
1511
|
+ justify-content: center;
|
|
|
1512
|
+ font-size: 12px;
|
|
|
1513
|
+ font-weight: 600;
|
|
|
1514
|
+ }
|
|
|
1515
|
+ }
|
|
|
1516
|
+
|
|
|
1517
|
+ .content-body {
|
|
|
1518
|
+ font-size: 14px;
|
|
|
1519
|
+ color: #303133;
|
|
|
1520
|
+ line-height: 1.6;
|
|
|
1521
|
+ white-space: pre-wrap;
|
|
|
1522
|
+ word-break: break-word;
|
|
|
1523
|
+ }
|
|
|
1524
|
+}
|
|
|
1525
|
+
|
|
|
1526
|
+.title-mode-badge {
|
|
|
1527
|
+ background: #67c23a;
|
|
|
1528
|
+ color: white;
|
|
|
1529
|
+ padding: 2px 8px;
|
|
|
1530
|
+ border-radius: 12px;
|
|
|
1531
|
+ font-size: 12px;
|
|
|
1532
|
+ margin-left: auto;
|
|
|
1533
|
+}
|
|
|
1534
|
+
|
|
1285
|
1535
|
// 聊天模式样式
|
|
1286
|
1536
|
.chat-content {
|
|
1287
|
1537
|
flex: 1;
|