QAndA.vue 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311
  1. <template>
  2. <div class="container">
  3. <!-- 左侧知识库列表 -->
  4. <div class="knowledge-base">
  5. <h3>选择知识库</h3>
  6. <div
  7. v-for="item in knowledgeBaseList"
  8. :key="item.dataset_id"
  9. :class="['knowledge-item', { 'active': selectedDatasetIds.includes(item.dataset_id) }]"
  10. @click="toggleDatasetSelection(item.dataset_id)"
  11. >
  12. {{ item.name }}
  13. </div>
  14. </div>
  15. <!-- 右侧搜索框和搜索结果 -->
  16. <div class="search-area">
  17. <!-- 搜索框 + 提问按钮 -->
  18. <div class="search-container">
  19. <el-input
  20. v-model="query"
  21. placeholder="请输入提问内容"
  22. suffix-icon="el-icon-search"
  23. class="search-input"
  24. ></el-input>
  25. <!-- 当 loading 时禁用按钮 -->
  26. <el-button
  27. @click="chat"
  28. type="primary"
  29. :disabled="loading"
  30. style="margin-left: 15px"
  31. >
  32. {{ "提问" }}
  33. </el-button>
  34. </div>
  35. <!-- 搜索中的提示 -->
  36. <div v-if="loading" class="loading-spinner">回答中...</div>
  37. <!-- 展示 chat_res 数据 -->
  38. <div v-if="chatSummary" class="chat-summary">
  39. <h4>回答内容</h4>
  40. <div v-html="parsedChatSummary"></div> <!-- 这里将解析后的内容渲染到页面 -->
  41. </div>
  42. <div class="search-results">
  43. <!-- 只有当 searchResults 不为空时才显示外层的卡片 -->
  44. <el-card v-if="searchResults.length > 0" class="search-card">
  45. <h4>搜索内容</h4> <!-- 新增标题 "搜索内容" -->
  46. <!-- 现有的搜索结果卡片循环 -->
  47. <div v-for="result in searchResults" :key="result.contentSummary">
  48. <el-card
  49. class="result-card"
  50. @click="handleDetails(result)"
  51. >
  52. <h3>{{ result.contentSummary }}</h3>
  53. <p>{{ result.content.substring(0, 100) }}...</p>
  54. <div class="meta">
  55. <span>相似度: {{ result.score.toFixed(2) }}</span>
  56. <span>知识库: {{ result.datasetName }}</span>
  57. </div>
  58. </el-card>
  59. </div>
  60. </el-card>
  61. </div>
  62. </div>
  63. </div>
  64. <!-- 弹窗:展示完整内容 -->
  65. <el-dialog v-model="dialogVisible" width="80%">
  66. <div>
  67. <h3>{{ selectedResult.contentSummary }}</h3>
  68. <p>{{ selectedResult.content }}</p>
  69. <hr />
  70. <div>
  71. <h4>原文内容:</h4>
  72. <p>{{ originalContent }}</p>
  73. </div>
  74. </div>
  75. <template #footer>
  76. <el-button @click="dialogVisible = false">关闭</el-button>
  77. </template>
  78. </el-dialog>
  79. </template>
  80. <script setup>
  81. import { ref, onMounted, computed } from 'vue';
  82. import { ElMessage } from 'element-plus';
  83. import { marked } from 'marked';
  84. import {API_BASE_URL} from "@/config"; // 使用命名导入
  85. // 存储选择的知识库数据
  86. const knowledgeBaseList = ref([]);
  87. const selectedDatasetIds = ref([]);
  88. // 搜索框输入内容
  89. const query = ref('');
  90. // 存储搜索结果
  91. const searchResults = ref([]);
  92. // 存储chat_res总结
  93. const chatSummary = ref('');
  94. // 弹窗显示状态
  95. const dialogVisible = ref(false);
  96. // 存储选中的搜索结果
  97. const selectedResult = ref({});
  98. // 存储原文内容
  99. const originalContent = ref('');
  100. // 搜索加载状态
  101. const loading = ref(false);
  102. // 请求知识库列表
  103. const getKnowledgeBaseList = async () => {
  104. try {
  105. const response = await fetch(`${API_BASE_URL}/dataset/list`);
  106. const data = await response.json();
  107. knowledgeBaseList.value = data.data;
  108. } catch (error) {
  109. ElMessage.error('获取知识库列表失败');
  110. }
  111. };
  112. // 选择或取消选择知识库
  113. const toggleDatasetSelection = (datasetId) => {
  114. const index = selectedDatasetIds.value.indexOf(datasetId);
  115. if (index === -1) {
  116. selectedDatasetIds.value.push(datasetId);
  117. } else {
  118. selectedDatasetIds.value.splice(index, 1);
  119. }
  120. };
  121. // 执行搜索操作
  122. const chat = async () => {
  123. if (loading.value) return; // 防止重复调用
  124. if (!query.value.trim()) {
  125. ElMessage.warning('请输入提问内容');
  126. return;
  127. }
  128. if (selectedDatasetIds.value.length === 0) {
  129. ElMessage.warning('请先选择知识库');
  130. return;
  131. }
  132. chatSummary.value = '';
  133. searchResults.value = [];
  134. selectedResult.value = {};
  135. originalContent.value = '';
  136. loading.value = true; // 开始搜索时显示加载提示
  137. const datasetIds = selectedDatasetIds.value.join(',');
  138. try {
  139. const response = await fetch(`${API_BASE_URL}/chat?query=${query.value}&datasetIds=${datasetIds}`);
  140. const data = await response.json();
  141. searchResults.value = data.data.results.map((item) => ({
  142. ...item,
  143. }));
  144. // 获取并设置 chat_res
  145. if (data.data.chat_res) {
  146. chatSummary.value = data.data.chat_res;
  147. }
  148. } catch (error) {
  149. ElMessage.error('搜索失败');
  150. } finally {
  151. loading.value = false; // 搜索结束后隐藏加载提示
  152. }
  153. };
  154. // 展示选中的搜索结果的完整内容
  155. const handleDetails = async (result) => {
  156. selectedResult.value = result;
  157. dialogVisible.value = true; // 打开弹窗
  158. // 请求完整内容
  159. try {
  160. const response = await fetch(`${API_BASE_URL}/content/get?docId=${result.docId}`);
  161. const data = await response.json();
  162. if (data.status_code === 200) {
  163. originalContent.value = data.data.text; // 显示原文内容
  164. } else {
  165. ElMessage.error('获取原文内容失败');
  166. }
  167. } catch (error) {
  168. ElMessage.error('请求原文内容失败');
  169. }
  170. };
  171. // 计算属性:解析 chatSummary(如果是 Markdown 则解析)
  172. const parsedChatSummary = computed(() => {
  173. if (chatSummary.value) {
  174. // 分开检查 Markdown 格式的常见符号
  175. const markdownSymbols = /[#*+\-`>!]/; // 检测 Markdown 中常见的特殊字符
  176. const isMarkdown = markdownSymbols.test(chatSummary.value);
  177. // 如果检测到 Markdown 符号,解析为 Markdown 格式
  178. if (isMarkdown) {
  179. return marked(chatSummary.value); // 使用 marked 库解析 Markdown
  180. }
  181. }
  182. return chatSummary.value; // 如果不是 Markdown 格式,直接返回文本
  183. });
  184. // 页面初始化加载知识库列表
  185. onMounted(() => {
  186. getKnowledgeBaseList();
  187. });
  188. </script>
  189. <style scoped>
  190. .container {
  191. display: flex;
  192. justify-content: space-between;
  193. padding: 20px;
  194. height: 100vh; /* 设置容器高度为视口高度 */
  195. }
  196. .knowledge-base {
  197. width: 20%;
  198. background-color: #f9f9f9;
  199. padding: 15px;
  200. border-radius: 8px;
  201. height: 100%; /* 确保高度为100% */
  202. overflow-y: auto; /* 启用垂直滚动 */
  203. }
  204. .knowledge-item {
  205. padding: 10px;
  206. border-radius: 5px;
  207. cursor: pointer;
  208. margin: 5px 0;
  209. transition: background-color 0.3s ease;
  210. }
  211. .knowledge-item:hover {
  212. background-color: #e6f7ff;
  213. }
  214. .knowledge-item.active {
  215. background-color: #b3d8ff;
  216. }
  217. .search-area {
  218. width: 75%;
  219. background-color: #ffffff;
  220. padding: 20px;
  221. border-radius: 8px;
  222. box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
  223. height: 100%; /* 确保高度为100% */
  224. overflow-y: auto; /* 启用垂直滚动 */
  225. }
  226. .search-container {
  227. display: flex;
  228. justify-content: center;
  229. align-items: center;
  230. margin-bottom: 20px;
  231. }
  232. .search-input {
  233. width: 60%; /* 控制搜索框的宽度 */
  234. }
  235. .result-card {
  236. margin-top: 10px;
  237. cursor: pointer;
  238. }
  239. .result-card:hover {
  240. background-color: #f5f5f5;
  241. }
  242. .meta {
  243. display: flex;
  244. justify-content: space-between;
  245. font-size: 12px;
  246. color: #888;
  247. }
  248. .loading-spinner {
  249. text-align: center;
  250. font-size: 16px;
  251. color: #888;
  252. margin-top: 20px;
  253. }
  254. .chat-summary {
  255. background-color: #f0f8ff;
  256. padding: 15px;
  257. border-radius: 8px;
  258. margin-bottom: 20px;
  259. }
  260. .chat-summary h4 {
  261. font-size: 18px;
  262. font-weight: bold;
  263. }
  264. .chat-summary p {
  265. font-size: 14px;
  266. line-height: 1.5;
  267. }
  268. </style>