index.vue 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429
  1. <template>
  2. <div class="intelligentQA">
  3. <div class="history">
  4. <div class="historyTitle">历史记录</div>
  5. <div class="historyContent">
  6. <div v-for="item in historyData" :key="item.id" class="historyItem">
  7. <span class="question"> {{ item.question }}</span>
  8. <i class="el-icon-delete historyMore" style="margin-left: 10px" @click="deleteHistory(item.id)"></i>
  9. <i class="el-icon-more historyMore" @click="historyDetail(item.id)"></i>
  10. </div>
  11. </div>
  12. </div>
  13. <div class="chat">
  14. <!-- <div class="header">Header</div> -->
  15. <div ref="main" class="main">
  16. <div class="chatLine">
  17. <div v-for="(item, index) in chatInfo" :key="index">
  18. <div class="chatRow chatQ" v-if="item.question">
  19. <div class="questionContent">{{ item.question }}</div>
  20. </div>
  21. <!-- 回答 -->
  22. <div v-else class="chatRow chatA">
  23. <!--提示 -->
  24. <div v-if="item.answer" class="tipAnswer">
  25. {{ item.answer }}
  26. </div>
  27. <!-- 图谱数据库 -->
  28. <div class="answerData" v-if="item.graphAnswer">
  29. <h2>图谱数据库</h2>
  30. <div class="answer">{{ item.graphAnswer.answer }}</div>
  31. <div class="graph">
  32. <graphECharts v-if="item.graphAnswer.graph" :graphData="item.graphAnswer.graph" class="charts"></graphECharts>
  33. <i v-if="item.userId && item.graphAnswer.ossId" class="el-icon-more more" @click="handleMore(item)"></i>
  34. </div>
  35. </div>
  36. <!-- 知识库 -->
  37. <div class="answerData" v-if="item.llmAnswer">
  38. <h2>知识库</h2>
  39. <div class="answer">
  40. <el-collapse v-model="collapseActiveNames">
  41. <el-collapse-item title="知识库匹配结果" :name="index">
  42. <div v-for="(item, index) in item.llmAnswer.docs" :key="index">
  43. <div class="markdown" v-html="renderMarkdown(item)"></div>
  44. </div>
  45. </el-collapse-item>
  46. </el-collapse>
  47. <!-- <div style="margin-top: 10px">{{ item.llmAnswer.answer }}</div> -->
  48. <div v-if="item.llmAnswer.think" class="think" v-html="renderMarkdown(item.llmAnswer.think)"></div>
  49. <div style="margin-top: 10px" v-html="renderMarkdown(item.llmAnswer.answer)"></div>
  50. </div>
  51. </div>
  52. <!-- sql -->
  53. <div class="answerData" v-if="item.sqlAnswer">
  54. <h2>数据库</h2>
  55. <div class="answer">
  56. <el-table :data="item.sqlAnswer" style="width: 100%; height: 250px; overflow: scroll">
  57. <el-table-column v-for="item in sqlAnswerKey" :prop="item" :key="item" :label="item" align="center"> </el-table-column>
  58. </el-table>
  59. </div>
  60. </div>
  61. </div>
  62. </div>
  63. </div>
  64. </div>
  65. <div class="footer">
  66. <div class="footerContent">
  67. <textarea v-model="questionInput" :style="{ height: `${currentHeight}px`, overflowY: isScrollable ? 'auto' : 'hidden' }" placeholder="请输入您的问题..." @keydown="handleKeydown"> </textarea>
  68. <i class="el-icon-s-promotion icon" @click="sendQuestion"></i>
  69. </div>
  70. </div>
  71. </div>
  72. <div class="statistics">
  73. <div class="statisticsTitle">统计列表</div>
  74. <ul class="statisticsContent">
  75. <li v-for="item in statisticsData" :key="item.question" class="statisticsItem">
  76. <span style="padding-left: 30px"> {{ item.question }}</span>
  77. </li>
  78. </ul>
  79. </div>
  80. <el-dialog title="更多" :visible.sync="dialogVisible" width="1500px" :before-close="handleClose">
  81. <div class="dialogContent">
  82. <div class="contentLeft">
  83. <div>
  84. <div class="moreAnswer">{{ moreData.graphAnswer.answer }}</div>
  85. <graphECharts :graphAnswer="moreData.graphAnswer.graph"></graphECharts>
  86. </div>
  87. </div>
  88. <div class="contentRight">
  89. <div>
  90. <div class="source">
  91. <el-link :href="`${fileInfo.url}`" style="color: #209cc1" :underline="false" target="_blank">
  92. <el-tooltip class="item" effect="dark" content="点击可下载" placement="top">
  93. <span> 来源:《{{ moreData.graphAnswer.fileName }}》</span>
  94. </el-tooltip>
  95. </el-link>
  96. </div>
  97. <div class="fileContent">
  98. <div v-if="fileInfo.fileSuffix == '.docx' || fileInfo.fileSuffix == '.doc'">
  99. <VueOfficeDocx ref="docxShow" style="width: 100%; height: calc(100vh - 400px)" :src="fileInfo.url" />
  100. </div>
  101. <div v-if="fileInfo.fileSuffix == '.pdf'">
  102. <!-- <VueOfficePdf style="width: 100%; height: calc(100vh - 400px)" :src="fileInfo.url" /> -->
  103. <pdfvuer :src="fileInfo.url" class="pdf" :page="moreData.graphAnswer.filePage"> </pdfvuer>
  104. </div>
  105. <el-pagination style="margin-top: 5px" v-if="fileInfo.fileSuffix == '.pdf'" background small layout="prev, pager, next" :current-page="moreData.graphAnswer.filePage" :page-count="fileTotalPage" @current-change="handleIndexChange">
  106. </el-pagination>
  107. </div>
  108. </div>
  109. </div>
  110. </div>
  111. <!-- <span slot="footer" class="dialog-footer">
  112. <el-button @click="dialogVisible = false">取 消</el-button>
  113. <el-button type="primary" @click="dialogVisible = false">确 定</el-button>
  114. </span> -->
  115. </el-dialog>
  116. </div>
  117. </template>
  118. <script>
  119. import store from '@/store'
  120. import { handlerAsk, getQAHistoryList, getGroup, getQAHistoryListAll, getQAHistoryDetail, removeQAHistory } from '@/api/als/intelligentQA'
  121. import graphECharts from '@/views/als/components/Charts/graph.vue'
  122. import { getListByIdsApi } from '@/api/als/oss'
  123. //引入VueOfficeDocx组件
  124. import VueOfficeDocx from '@vue-office/docx'
  125. // import VueOfficePdf from '@vue-office/pdf'
  126. import '@vue-office/docx/lib/index.css'
  127. import pdfvuer from 'pdfvuer' // pdfvuer 版本为@1.6.1
  128. import 'pdfjs-dist/build/pdf.worker.entry'
  129. import * as marked from 'marked'
  130. export default {
  131. name: 'IntelligentQA',
  132. // components: { graphECharts, VueOfficeDocx, VueOfficePdf },
  133. components: { graphECharts, VueOfficeDocx, pdfvuer },
  134. data() {
  135. return {
  136. dialogVisible: false,
  137. chatInfo: [],
  138. questionInput: '', //202310150010
  139. currentHeight: 'auto',
  140. lastScrollHeight: 0,
  141. isScrollable: false,
  142. moreData: {
  143. userId: '',
  144. graphAnswer: {},
  145. llmAnswer: {
  146. docs: '',
  147. answer: ''
  148. },
  149. sqlAnswer: []
  150. },
  151. historyData: [],
  152. fileInfo: {},
  153. statisticsData: [],
  154. // TODO 问答给数据后将该字段替换
  155. filePage: '2',
  156. fileTotalPage: null,
  157. sqlAnswerKey: [],
  158. collapseActiveNames: [],
  159. main: null,
  160. askUrl: '/als/algorithm/execute/qa'
  161. }
  162. },
  163. mounted() {
  164. this.main = document.getElementsByClassName('main')[0]
  165. this.getHistoryAll()
  166. this.getGroupAPI()
  167. this.adjustHeight()
  168. },
  169. watch: {
  170. chatInfo: {
  171. handler() {
  172. // 当原始列变化时,更新本地列
  173. setTimeout(() => {
  174. this.main.scrollTop = this.main.scrollHeight
  175. }, 0)
  176. },
  177. deep: true
  178. }
  179. },
  180. created() {},
  181. methods: {
  182. adjustHeight() {
  183. const textarea = this.$el.querySelector('textarea')
  184. this.currentHeight = 'auto'
  185. if (textarea.scrollHeight < this.lastScrollHeight) {
  186. this.lastScrollHeight = this.lastScrollHeight - 16
  187. }
  188. this.$nextTick(() => {
  189. this.currentHeight = Math.min(this.lastScrollHeight, 200)
  190. this.lastScrollHeight = textarea.scrollHeight
  191. if (textarea.scrollHeight >= 200) {
  192. this.isScrollable = true
  193. } else {
  194. this.isScrollable = false
  195. }
  196. })
  197. },
  198. async getHistoryAll() {
  199. try {
  200. const { data } = await getQAHistoryListAll()
  201. this.historyData = data
  202. // console.log('this.historyData', this.historyData)
  203. } catch (error) {}
  204. },
  205. async getGroupAPI() {
  206. try {
  207. const { data } = await getGroup()
  208. this.statisticsData = data
  209. } catch (error) {}
  210. },
  211. handleKeydown(event) {
  212. if (event.key === 'Enter') {
  213. if (event.ctrlKey) {
  214. // 按下了 Ctrl+Enter,则插入换行符
  215. this.questionInput += '\n'
  216. this.adjustHeight()
  217. } else {
  218. // 只是按下了 Enter,阻止默认行为并发送消息
  219. event.preventDefault()
  220. this.sendQuestion()
  221. }
  222. }
  223. if (event.key === 'Backspace') {
  224. if (this.questionInput) {
  225. this.adjustHeight()
  226. } else {
  227. this.currentHeight = '50'
  228. }
  229. }
  230. },
  231. async sendQuestion() {
  232. // 等处理过返回数据后,调用使视图保持在最底部
  233. // let main = this.$refs.main
  234. // let main = document.getElementsByClassName('main')
  235. const sendInput = {
  236. question: this.questionInput,
  237. userId: String(store.state.user.userInfo.user.userId)
  238. }
  239. try {
  240. if (this.questionInput.trim() === '') {
  241. return
  242. }
  243. this.chatInfo.push(sendInput, {
  244. answer: '正在解析您的问题,请稍后......'
  245. })
  246. this.questionInput = ''
  247. // const eventSource = new EventSource(this.askUrl)
  248. // eventSource.onmessage = (event) => {
  249. // console.log('数据', event.data)
  250. // }
  251. const { code, data } = await handlerAsk(sendInput)
  252. if (code == 200) {
  253. const newData = this.handleData(JSON.parse(data))
  254. // const newData = this.handleData(data)
  255. this.chatInfo.pop()
  256. this.chatInfo.push(newData)
  257. // 获取sql回答的键
  258. this.sqlAnswerKey = Object.keys(JSON.parse(data).sqlAnswer[0])
  259. }
  260. } catch (error) {}
  261. },
  262. handleData(data) {
  263. if (data.graphAnswer?.graph) {
  264. const graphAnswer = eval('(' + data.graphAnswer.graph + ')')
  265. data.graphAnswer.graph = graphAnswer
  266. const categories = []
  267. data.graphAnswer.graph.data.forEach((node) => {
  268. const flag = categories.find((item) => {
  269. return item.name === node.category
  270. })
  271. if (!flag) {
  272. categories.push({ name: node.category })
  273. }
  274. })
  275. data.graphAnswer.graph.categories = categories
  276. }
  277. return data
  278. },
  279. handleMore(data) {
  280. this.getListById(data.graphAnswer.ossId)
  281. this.moreData = data
  282. this.dialogVisible = true
  283. },
  284. async getListById(id) {
  285. const { data } = await getListByIdsApi(id)
  286. // pdf测试:250091300051144704,word测试:250122767632355328
  287. // const { data } = await getListByIdsApi('254798739581349888')
  288. this.fileInfo = data[0]
  289. if (this.fileInfo?.fileSuffix == '.pdf') {
  290. pdfvuer
  291. .createLoadingTask(this.fileInfo?.url)
  292. .then((pdf) => {
  293. this.fileTotalPage = pdf.numPages
  294. })
  295. .catch((err) => {
  296. console.error('PDF 加载失败:', err)
  297. })
  298. } else if (this.fileInfo?.fileSuffix == '.docx' || this.fileInfo?.fileSuffix == '.doc') {
  299. }
  300. },
  301. handleClose() {
  302. this.dialogVisible = false
  303. this.fileInfo = {}
  304. },
  305. async historyDetail(id) {
  306. try {
  307. const { code, data } = await getQAHistoryDetail(id)
  308. if (code == 200) {
  309. const newData = this.handleData(JSON.parse(data.answer))
  310. this.chatInfo = []
  311. this.chatInfo.push({
  312. question: data.question,
  313. userId: data.userId
  314. })
  315. this.sqlAnswerKey = Object.keys(newData.sqlAnswer[0])
  316. this.chatInfo.push(newData)
  317. }
  318. } catch (error) {}
  319. },
  320. deleteHistory(id) {
  321. this.$confirm('是否删除该条记录?', '提示', {
  322. confirmButtonText: '确定',
  323. cancelButtonText: '取消',
  324. type: 'warning'
  325. })
  326. .then(() => {
  327. this.removeQAHistoryAPI(id)
  328. })
  329. .catch(() => {})
  330. },
  331. async removeQAHistoryAPI(params) {
  332. try {
  333. const { code } = await removeQAHistory(params)
  334. if (code === 200) {
  335. this.$message({
  336. type: 'success',
  337. message: '操作成功!'
  338. })
  339. this.getHistoryAll()
  340. }
  341. } catch (error) {}
  342. },
  343. // currentPage 改变时触发事件
  344. handleIndexChange(current) {
  345. this.moreData.graphAnswer.filePage = current
  346. // this.fetch()
  347. },
  348. renderMarkdown(item) {
  349. // console.log('marked(item)', marked(item))
  350. // return marked(item)
  351. const html = marked.parse(item)
  352. return html
  353. }
  354. // const { code, data } = {
  355. // code: 200,
  356. // msg: '',
  357. // data: {
  358. // userId: 'user',
  359. // graphAnswer: {
  360. // answer: '解决办法为:更换电池或遥控器',
  361. // fileName: '排故手册',
  362. // ossId: '227701077942149120', //pdf
  363. // // ossID: '227692224508796928', //word
  364. // filePage: 2,
  365. // graph: {
  366. // data: [
  367. // { name: '202310150010', category: 'HMC' },
  368. // { name: '电视', category: '成品' },
  369. // { name: '电视遥控器失灵', category: '故障描述' },
  370. // { name: '家用电器', category: '系统' },
  371. // { name: '更换电池或遥控器', category: '维修策略' }
  372. // ],
  373. // links: [
  374. // { source: '202310150010', target: '电视', value: '成品' },
  375. // { source: '202310150010', target: '电视遥控器失灵', value: '故障描述' },
  376. // { source: '202310150010', target: '家用电器', value: '系统' },
  377. // { source: '202310150010', target: '更换电池或遥控器', value: '维修策略' }
  378. // ]
  379. // }
  380. // },
  381. // llmAnswer: {
  382. // docs: [
  383. // '出处 [1] [大模型方案.docx](http://10.67.81.103:7861/knowledge_base/download_doc?knowledge_base_name=lqbz&file_name=%E5%A4%A7%E6%A8%A1%E5%9E%8B%E6%96%B9%E6%A1%88.docx) \n\nERNIE的部分版本如ERNIE 3.0等提供了API服务,允许开发者通过调用接口的方式使用模型。\n腾讯混元 (Hunyuan)\n腾讯发布的多模态预训练模型,虽然以多模态为主,但也具备较强的文本生成能力。\n腾讯的混元模型通常不是完全开源的,但提供了API服务供开发者使用。\n华为盘古 (Pangu)\n华为研发的大规模预训练模型,专门针对中文场景进行了优化。在中文文本生成、问答等方面表现优秀。\n华为盘古模型本身没有开源,但华为提供了相关的API服务,使开发者能够使用这些模型的能力。\n定义模型规模\n确定模型的参数量大小,这将直接影响到模型的能力和计算资源的需求。\n超参数设定\n设定学习率、批次、损失函数等超参数。\n训练环境搭建\n安装必要软件\n安装Python、CUDA、Anaconda、深度学习框架(如Tensorflow或者Pytorch)等。\n配置GPU/CPU资源\n确保有足够的算力资源来支持模型的训练。\n模型训练\n初始化模型\n根据选定的架构初始化模型。\n数据加载\n编写代码来加载和处理训练数据。\n训练循环\n实现训练循环,包括前向传播、损失计算、反向传播和权重更新。\n保存检查点\n定期保存检查点、以便后续使用或恢复训练。\n模型评估于优化\n定义评估指标\n选择合适的评估指标,如准确率、BLEU得分等。\n验证与测试\n使用验证集和测试集来评估模型性能。\n模型微调\n根据评估结果进行模型调整或优化。\n模型部署\n模型导出\n将训练好的模型转换成可部署的格式\n部署环境\n在本地服务器上部署环境,可能需要使用轻量化推理框架(如Tensorflow Lite、ONNX Runtime等)。\n接口开发\n\n',
  384. // '出处 [2] [大模型方案.docx](http://10.67.81.103:7861/knowledge_base/download_doc?knowledge_base_name=lqbz&file_name=%E5%A4%A7%E6%A8%A1%E5%9E%8B%E6%96%B9%E6%A1%88.docx) \n\n使用验证集和测试集来评估模型性能。\n模型微调\n根据评估结果进行模型调整或优化。\n模型部署\n模型导出\n将训练好的模型转换成可部署的格式\n部署环境\n在本地服务器上部署环境,可能需要使用轻量化推理框架(如Tensorflow Lite、ONNX Runtime等)。\n接口开发\n开发API接口,使模型可以接受输入并返回预测结果。\n特定任务的微调\n任务数据准备\n收集针对特定任务的数据集。\n微调训练\n使用新数据集对模型进行微调,使其更擅长完成特定任务。\n评估与调整\n再次评估模型性能,并根据需要进行调整。\n持续改进\n用户反馈\n收集用户反馈,根据反馈进行模型的持续改进。\n版本控制\n使用版本控制系统来管理模型的不同版本。\n\n',
  385. // '出处 [3] [大模型方案.docx](http://10.67.81.103:7861/knowledge_base/download_doc?knowledge_base_name=lqbz&file_name=%E5%A4%A7%E6%A8%A1%E5%9E%8B%E6%96%B9%E6%A1%88.docx) \n\n分词\n将文本分割成更小的单元(token),如单词或子词。\n编码\n将分词后的文本转换为模型可以理解的数值表示。\n序列截断\n如果文本过长,可能需要将其截断为固定长度的序列。\n填充\n如果序列不够长,则需要用特殊标记填充至固定长度。\n模型架构设计\n选择模型类型\n目前在中文问答领域较好的大语言模型包括但不限于以下几个:\n通义千问 (Qwen)\n由阿里云开发,提供了公开的API接口供开发者调用,也可以通过官方网站直接与模型交互。支持多种任务,包括开放域问答、代码生成、文本生成等。\n文心一言 (ERNIE Bot)\n百度推出的大型语言模型,虽然模型本身没有完全开源,但提供了API服务,允许开发者通过调用接口来使用模型的功能。其针对中文语境进行了优化。在多项中文NLP任务上表现出色,包括问答和对话。\n悟道·天鹰 (M6)\n来自达摩院的大规模多模态预训练模型,虽然主要应用于多模态任务,但在语言理解和生成方面也有很好的表现。特别是在跨模态理解和生成方面有独特的优势。\n达摩院的M6系列模型没有完全开源,但其技术细节和部分应用可以通过论文和官方文档获取。\n百度文心系列 (ERNIE)\n包括多个版本,如 ERNIE 3.0 Titan 和 ERNIE 3.0 Zeus,这些模型在多项中文NLP任务上取得了非常好的成绩。\n具有强大的语言理解能力,适用于问答、摘要等多种任务。\nERNIE的部分版本如ERNIE 3.0等提供了API服务,允许开发者通过调用接口的方式使用模型。\n腾讯混元 (Hunyuan)\n腾讯发布的多模态预训练模型,虽然以多模态为主,但也具备较强的文本生成能力。\n腾讯的混元模型通常不是完全开源的,但提供了API服务供开发者使用。\n\n'
  386. // ],
  387. // answer: '根据已知信息,无法回答该问题。'
  388. // },
  389. // sqlAnswer: [
  390. // {
  391. // 日期: '2016-05-02',
  392. // name: '王小虎',
  393. // 地址: '上海市普陀区金沙江路 1518 弄',
  394. // sex: '男'
  395. // },
  396. // {
  397. // 日期: '2016-05-02',
  398. // name: '王小虎',
  399. // 地址: '上海市普陀区金沙江路 1518 弄',
  400. // sex: '男'
  401. // }
  402. // ]
  403. // }
  404. // }
  405. }
  406. }
  407. </script>
  408. <style scoped lang="scss">
  409. @import './index.scss';
  410. </style>