QAndA.vue 7.7 KB

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