123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429 |
- <template>
- <div class="intelligentQA">
- <div class="history">
- <div class="historyTitle">历史记录</div>
- <div class="historyContent">
- <div v-for="item in historyData" :key="item.id" class="historyItem">
- <span class="question"> {{ item.question }}</span>
- <i class="el-icon-delete historyMore" style="margin-left: 10px" @click="deleteHistory(item.id)"></i>
- <i class="el-icon-more historyMore" @click="historyDetail(item.id)"></i>
- </div>
- </div>
- </div>
- <div class="chat">
- <!-- <div class="header">Header</div> -->
- <div ref="main" class="main">
- <div class="chatLine">
- <div v-for="(item, index) in chatInfo" :key="index">
- <div class="chatRow chatQ" v-if="item.question">
- <div class="questionContent">{{ item.question }}</div>
- </div>
- <!-- 回答 -->
- <div v-else class="chatRow chatA">
- <!--提示 -->
- <div v-if="item.answer" class="tipAnswer">
- {{ item.answer }}
- </div>
- <!-- 图谱数据库 -->
- <div class="answerData" v-if="item.graphAnswer">
- <h2>图谱数据库</h2>
- <div class="answer">{{ item.graphAnswer.answer }}</div>
- <div class="graph">
- <graphECharts v-if="item.graphAnswer.graph" :graphData="item.graphAnswer.graph" class="charts"></graphECharts>
- <i v-if="item.userId && item.graphAnswer.ossId" class="el-icon-more more" @click="handleMore(item)"></i>
- </div>
- </div>
- <!-- 知识库 -->
- <div class="answerData" v-if="item.llmAnswer">
- <h2>知识库</h2>
- <div class="answer">
- <el-collapse v-model="collapseActiveNames">
- <el-collapse-item title="知识库匹配结果" :name="index">
- <div v-for="(item, index) in item.llmAnswer.docs" :key="index">
- <div class="markdown" v-html="renderMarkdown(item)"></div>
- </div>
- </el-collapse-item>
- </el-collapse>
- <!-- <div style="margin-top: 10px">{{ item.llmAnswer.answer }}</div> -->
- <div v-if="item.llmAnswer.think" class="think" v-html="renderMarkdown(item.llmAnswer.think)"></div>
- <div style="margin-top: 10px" v-html="renderMarkdown(item.llmAnswer.answer)"></div>
- </div>
- </div>
- <!-- sql -->
- <div class="answerData" v-if="item.sqlAnswer">
- <h2>数据库</h2>
- <div class="answer">
- <el-table :data="item.sqlAnswer" style="width: 100%; height: 250px; overflow: scroll">
- <el-table-column v-for="item in sqlAnswerKey" :prop="item" :key="item" :label="item" align="center"> </el-table-column>
- </el-table>
- </div>
- </div>
- </div>
- </div>
- </div>
- </div>
- <div class="footer">
- <div class="footerContent">
- <textarea v-model="questionInput" :style="{ height: `${currentHeight}px`, overflowY: isScrollable ? 'auto' : 'hidden' }" placeholder="请输入您的问题..." @keydown="handleKeydown"> </textarea>
- <i class="el-icon-s-promotion icon" @click="sendQuestion"></i>
- </div>
- </div>
- </div>
- <div class="statistics">
- <div class="statisticsTitle">统计列表</div>
- <ul class="statisticsContent">
- <li v-for="item in statisticsData" :key="item.question" class="statisticsItem">
- <span style="padding-left: 30px"> {{ item.question }}</span>
- </li>
- </ul>
- </div>
- <el-dialog title="更多" :visible.sync="dialogVisible" width="1500px" :before-close="handleClose">
- <div class="dialogContent">
- <div class="contentLeft">
- <div>
- <div class="moreAnswer">{{ moreData.graphAnswer.answer }}</div>
- <graphECharts :graphAnswer="moreData.graphAnswer.graph"></graphECharts>
- </div>
- </div>
- <div class="contentRight">
- <div>
- <div class="source">
- <el-link :href="`${fileInfo.url}`" style="color: #209cc1" :underline="false" target="_blank">
- <el-tooltip class="item" effect="dark" content="点击可下载" placement="top">
- <span> 来源:《{{ moreData.graphAnswer.fileName }}》</span>
- </el-tooltip>
- </el-link>
- </div>
- <div class="fileContent">
- <div v-if="fileInfo.fileSuffix == '.docx' || fileInfo.fileSuffix == '.doc'">
- <VueOfficeDocx ref="docxShow" style="width: 100%; height: calc(100vh - 400px)" :src="fileInfo.url" />
- </div>
- <div v-if="fileInfo.fileSuffix == '.pdf'">
- <!-- <VueOfficePdf style="width: 100%; height: calc(100vh - 400px)" :src="fileInfo.url" /> -->
- <pdfvuer :src="fileInfo.url" class="pdf" :page="moreData.graphAnswer.filePage"> </pdfvuer>
- </div>
- <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">
- </el-pagination>
- </div>
- </div>
- </div>
- </div>
- <!-- <span slot="footer" class="dialog-footer">
- <el-button @click="dialogVisible = false">取 消</el-button>
- <el-button type="primary" @click="dialogVisible = false">确 定</el-button>
- </span> -->
- </el-dialog>
- </div>
- </template>
- <script>
- import store from '@/store'
- import { handlerAsk, getQAHistoryList, getGroup, getQAHistoryListAll, getQAHistoryDetail, removeQAHistory } from '@/api/als/intelligentQA'
- import graphECharts from '@/views/als/components/Charts/graph.vue'
- import { getListByIdsApi } from '@/api/als/oss'
- //引入VueOfficeDocx组件
- import VueOfficeDocx from '@vue-office/docx'
- // import VueOfficePdf from '@vue-office/pdf'
- import '@vue-office/docx/lib/index.css'
- import pdfvuer from 'pdfvuer' // pdfvuer 版本为@1.6.1
- import 'pdfjs-dist/build/pdf.worker.entry'
- import * as marked from 'marked'
- export default {
- name: 'IntelligentQA',
- // components: { graphECharts, VueOfficeDocx, VueOfficePdf },
- components: { graphECharts, VueOfficeDocx, pdfvuer },
- data() {
- return {
- dialogVisible: false,
- chatInfo: [],
- questionInput: '', //202310150010
- currentHeight: 'auto',
- lastScrollHeight: 0,
- isScrollable: false,
- moreData: {
- userId: '',
- graphAnswer: {},
- llmAnswer: {
- docs: '',
- answer: ''
- },
- sqlAnswer: []
- },
- historyData: [],
- fileInfo: {},
- statisticsData: [],
- // TODO 问答给数据后将该字段替换
- filePage: '2',
- fileTotalPage: null,
- sqlAnswerKey: [],
- collapseActiveNames: [],
- main: null,
- askUrl: '/als/algorithm/execute/qa'
- }
- },
- mounted() {
- this.main = document.getElementsByClassName('main')[0]
- this.getHistoryAll()
- this.getGroupAPI()
- this.adjustHeight()
- },
- watch: {
- chatInfo: {
- handler() {
- // 当原始列变化时,更新本地列
- setTimeout(() => {
- this.main.scrollTop = this.main.scrollHeight
- }, 0)
- },
- deep: true
- }
- },
- created() {},
- methods: {
- adjustHeight() {
- const textarea = this.$el.querySelector('textarea')
- this.currentHeight = 'auto'
- if (textarea.scrollHeight < this.lastScrollHeight) {
- this.lastScrollHeight = this.lastScrollHeight - 16
- }
- this.$nextTick(() => {
- this.currentHeight = Math.min(this.lastScrollHeight, 200)
- this.lastScrollHeight = textarea.scrollHeight
- if (textarea.scrollHeight >= 200) {
- this.isScrollable = true
- } else {
- this.isScrollable = false
- }
- })
- },
- async getHistoryAll() {
- try {
- const { data } = await getQAHistoryListAll()
- this.historyData = data
- // console.log('this.historyData', this.historyData)
- } catch (error) {}
- },
- async getGroupAPI() {
- try {
- const { data } = await getGroup()
- this.statisticsData = data
- } catch (error) {}
- },
- handleKeydown(event) {
- if (event.key === 'Enter') {
- if (event.ctrlKey) {
- // 按下了 Ctrl+Enter,则插入换行符
- this.questionInput += '\n'
- this.adjustHeight()
- } else {
- // 只是按下了 Enter,阻止默认行为并发送消息
- event.preventDefault()
- this.sendQuestion()
- }
- }
- if (event.key === 'Backspace') {
- if (this.questionInput) {
- this.adjustHeight()
- } else {
- this.currentHeight = '50'
- }
- }
- },
- async sendQuestion() {
- // 等处理过返回数据后,调用使视图保持在最底部
- // let main = this.$refs.main
- // let main = document.getElementsByClassName('main')
- const sendInput = {
- question: this.questionInput,
- userId: String(store.state.user.userInfo.user.userId)
- }
- try {
- if (this.questionInput.trim() === '') {
- return
- }
- this.chatInfo.push(sendInput, {
- answer: '正在解析您的问题,请稍后......'
- })
- this.questionInput = ''
- // const eventSource = new EventSource(this.askUrl)
- // eventSource.onmessage = (event) => {
- // console.log('数据', event.data)
- // }
- const { code, data } = await handlerAsk(sendInput)
- if (code == 200) {
- const newData = this.handleData(JSON.parse(data))
- // const newData = this.handleData(data)
- this.chatInfo.pop()
- this.chatInfo.push(newData)
- // 获取sql回答的键
- this.sqlAnswerKey = Object.keys(JSON.parse(data).sqlAnswer[0])
- }
- } catch (error) {}
- },
- handleData(data) {
- if (data.graphAnswer?.graph) {
- const graphAnswer = eval('(' + data.graphAnswer.graph + ')')
- data.graphAnswer.graph = graphAnswer
- const categories = []
- data.graphAnswer.graph.data.forEach((node) => {
- const flag = categories.find((item) => {
- return item.name === node.category
- })
- if (!flag) {
- categories.push({ name: node.category })
- }
- })
- data.graphAnswer.graph.categories = categories
- }
- return data
- },
- handleMore(data) {
- this.getListById(data.graphAnswer.ossId)
- this.moreData = data
- this.dialogVisible = true
- },
- async getListById(id) {
- const { data } = await getListByIdsApi(id)
- // pdf测试:250091300051144704,word测试:250122767632355328
- // const { data } = await getListByIdsApi('254798739581349888')
- this.fileInfo = data[0]
- if (this.fileInfo?.fileSuffix == '.pdf') {
- pdfvuer
- .createLoadingTask(this.fileInfo?.url)
- .then((pdf) => {
- this.fileTotalPage = pdf.numPages
- })
- .catch((err) => {
- console.error('PDF 加载失败:', err)
- })
- } else if (this.fileInfo?.fileSuffix == '.docx' || this.fileInfo?.fileSuffix == '.doc') {
- }
- },
- handleClose() {
- this.dialogVisible = false
- this.fileInfo = {}
- },
- async historyDetail(id) {
- try {
- const { code, data } = await getQAHistoryDetail(id)
- if (code == 200) {
- const newData = this.handleData(JSON.parse(data.answer))
- this.chatInfo = []
- this.chatInfo.push({
- question: data.question,
- userId: data.userId
- })
- this.sqlAnswerKey = Object.keys(newData.sqlAnswer[0])
- this.chatInfo.push(newData)
- }
- } catch (error) {}
- },
- deleteHistory(id) {
- this.$confirm('是否删除该条记录?', '提示', {
- confirmButtonText: '确定',
- cancelButtonText: '取消',
- type: 'warning'
- })
- .then(() => {
- this.removeQAHistoryAPI(id)
- })
- .catch(() => {})
- },
- async removeQAHistoryAPI(params) {
- try {
- const { code } = await removeQAHistory(params)
- if (code === 200) {
- this.$message({
- type: 'success',
- message: '操作成功!'
- })
- this.getHistoryAll()
- }
- } catch (error) {}
- },
- // currentPage 改变时触发事件
- handleIndexChange(current) {
- this.moreData.graphAnswer.filePage = current
- // this.fetch()
- },
- renderMarkdown(item) {
- // console.log('marked(item)', marked(item))
- // return marked(item)
- const html = marked.parse(item)
- return html
- }
- // const { code, data } = {
- // code: 200,
- // msg: '',
- // data: {
- // userId: 'user',
- // graphAnswer: {
- // answer: '解决办法为:更换电池或遥控器',
- // fileName: '排故手册',
- // ossId: '227701077942149120', //pdf
- // // ossID: '227692224508796928', //word
- // filePage: 2,
- // graph: {
- // data: [
- // { name: '202310150010', category: 'HMC' },
- // { name: '电视', category: '成品' },
- // { name: '电视遥控器失灵', category: '故障描述' },
- // { name: '家用电器', category: '系统' },
- // { name: '更换电池或遥控器', category: '维修策略' }
- // ],
- // links: [
- // { source: '202310150010', target: '电视', value: '成品' },
- // { source: '202310150010', target: '电视遥控器失灵', value: '故障描述' },
- // { source: '202310150010', target: '家用电器', value: '系统' },
- // { source: '202310150010', target: '更换电池或遥控器', value: '维修策略' }
- // ]
- // }
- // },
- // llmAnswer: {
- // docs: [
- // '出处 [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',
- // '出处 [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',
- // '出处 [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'
- // ],
- // answer: '根据已知信息,无法回答该问题。'
- // },
- // sqlAnswer: [
- // {
- // 日期: '2016-05-02',
- // name: '王小虎',
- // 地址: '上海市普陀区金沙江路 1518 弄',
- // sex: '男'
- // },
- // {
- // 日期: '2016-05-02',
- // name: '王小虎',
- // 地址: '上海市普陀区金沙江路 1518 弄',
- // sex: '男'
- // }
- // ]
- // }
- // }
- }
- }
- </script>
- <style scoped lang="scss">
- @import './index.scss';
- </style>
|