123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825 |
- <template>
- <div class="interpretation">
- <progress-bar :num="processNum" />
- <div class="header">
- <home-header sub-title="自然资源大模型" />
- </div>
- <div class="content-box">
- <div class="left-panel">
- <div class="content-top">
- <div class="left">
- <a-input :value="fileDetail?.name" style="width: 400px; margin-right: 10px" />
- <a-upload
- class="upload"
- action="/aisKnowledge/infra/file/upload"
- @change="handleChange"
- :showUploadList="false"
- :data="{ type: 'temp' }"
- accept=".pdf"
- >
- <img src="/images/zcjd/upload-icon.png" />上传本地文件
- </a-upload>
- </div>
- <div class="right" v-if="false">
- <span @click="downloadFile"><img src="/images/zcjd/download-icon.png" />下载</span>
- <span><img src="/images/zcjd/star-icon.png" />收藏</span>
- </div>
- </div>
- <div class="content-detail">
- <div class="file-preview">
- <!-- <img :src="fileDetail?.content" alt="" v-if="fileDetail?.content" /> -->
- <!-- <div class="spin" v-if="loading"><a-spin></a-spin></div> -->
- <PDFViewer :src="fileUrl" ref="ifRef"></PDFViewer>
- </div>
- </div>
- </div>
- <div class="affix_box" :offsetTop="20">
- <div class="right-panel">
- <div class="ai-interpretation">
- <div class="check-type">
- <div class="check">
- <a-radio-group v-model:value="checkType" button-style="solid">
- <a-radio-button value="question">AI解读</a-radio-button>
- </a-radio-group>
- </div>
- </div>
- <div class="result">
- <div class="content" id="scrollArea" v-if="checkType == 'interpretation'">
- <a-spin class="spin" v-if="aiLoading"></a-spin>
- <template v-else>
- <div class="desc">
- <div class="chain">
- <vue-markdown-it
- :source="isChain === '0' ? content : content.substring(0, content.indexOf('###'))"
- v-if="content"
- :options="{
- html: true,
- linkify: true
- }"
- />
- </div>
- <vue-markdown-it
- :source="isChain === '1' ? content.substring(content.indexOf('###')) : ''"
- v-if="content"
- :options="{
- html: true,
- linkify: true
- }"
- />
- </div>
- </template>
- </div>
- <div class="question" v-else>
- <div class="chat_box" id="chat_box">
- <div class="item">
- <div class="value-panel">
- <div class="name">
- 文件内容太多,读起来太累了?可以试着让我来帮您解读。<span
- >你可以试着这样问我:</span
- >
- </div>
- <div
- class="citem"
- v-for="(item, index) in question"
- :key="index"
- @click="toggleQuestion(index)"
- >
- <div class="value">{{ item }}</div>
- </div>
- </div>
- </div>
- <div class="chat">
- <div class="item" v-for="(item, index) in chatDesc" :key="index">
- <div class="user-panel">
- <div class="name">{{ item.name }}</div>
- <img src="/images/zczk/user.png" />
- </div>
- <div class="ai-panel">
- <img src="/images/zczk/icon-ai-title.png" />
- <div class="desc" v-if="item.content" id="scrollArea-1">
- <div class="chain">
- <vue-markdown-it
- :source="item.isChain === '0' ? item.content : item.content.substring(0, item.content.indexOf('###'))"
- :options="{
- html: true,
- linkify: true
- }"
- />
- </div>
- <vue-markdown-it
- :source="item.isChain === '1' ? item.content.substring(item.content.indexOf('###')) : ''"
- :options="{
- html: true,
- linkify: true
- }"
- />
- </div>
- <div class="desc animation" v-else>{{ status === 2 ? '网络错误': '正在思考中' }}<span class="dots" v-if="status !== 2"></span></div>
- </div>
- </div>
- </div>
- </div>
- <div class="input-panel">
- <a-textarea
- v-model:value="keyword"
- placeholder="请输入"
- @keydown.enter="keydownEnter"
- >
- </a-textarea>
- <div class="send" @click="send">
- 发送<img src="/images/zczk/icon-white-send.png" />
- </div>
- </div>
- </div>
- </div>
- </div>
- </div>
- </div>
- </div>
- </div>
- </template>
- <script setup>
- /**
- * @description 解读详情
- */
- import { ref, onMounted } from 'vue';
- import { useRoute } from 'vue-router';
- import HomeHeader from '@/views/home/components/HomeHeader.vue';
- import PDFViewer from '@/components/pdf/PDFViewerSearch.vue';
- import ProgressBar from '@/components/ProgressBar.vue';
- import api from '@/utils/policy-api';
- import { getNumAll } from '@/utils/common';
- import { message } from 'ant-design-vue';
- import { fetchEventSource } from '@microsoft/fetch-event-source';
- import { VueMarkdownIt } from '@f3ve/vue-markdown-it';
- const route = useRoute();
- const chatDesc = ref([]);
- const keyword = ref('');
- const ctr = new AbortController();
- const fileDetail = ref(null);
- const fileUrl = ref('');
- const aiLoading = ref(false);
- const queryText = ref('分别给出该文章的全文综述和要点总结');
- const content = ref('');
- const loading = ref(false);
- const ifRef = ref(null);
- const docs = ref([]);
- const question = [
- '帮我总结一下核心内容?',
- '有没有类似的政策文件?',
- '文件中提到了哪些信息化内容?'
- ];
- const checkType = ref('question');
- onMounted(async () => {
- const id = route.query.id;
- const url = route.query.url;
- //政策跳转 请求文件
- if (id) {
- getFileByID(id);
- }
- //政策解读 请求文件
- if (url) {
- fileUrl.value = url;
- var name = url.substring(url.lastIndexOf('/') + 1);
- var file = await fetchPdfFileStream(fileUrl.value, name);
- if (file) {
- setTimeout(() => {
- fileDetail.value = file;
- query();
- }, 100);
- }
- }
- });
- const getFileByID = (id) => {
- loading.value = true;
- var url = '/policy/query/' + id;
- api.get(url, {}, this, false).then(async (res) => {
- if (res && res.code == 200) {
- if (res.data.item) {
- var fileName = res.data.file_name;
- fileName = res.data.file_fullname;
- fileUrl.value = '/knowledge/file/' + fileName;
- loading.value = false;
- var file = await fetchPdfFileStream(fileUrl.value, fileName);
- if (file) {
- setTimeout(() => {
- fileDetail.value = file;
- query();
- }, 100);
- }
- }
- }
- });
- };
- import axios from 'axios';
- //在线地址转文件
- const fetchPdfFileStream = (pdfUrl, name) => {
- return new Promise((resolve, reject) => {
- var type = 'application/pdf';
- if (name.indexOf('pdf') > -1) {
- type = 'application/pdf';
- } else {
- type = 'application/docx';
- }
- axios
- .get(pdfUrl, { responseType: 'blob' })
- .then((response) => {
- const blob = new Blob([response.data], { type: type });
- const file = new File([blob], name, { type: type });
- resolve(file);
- // 接下来可以使用URL.createObjectURL(blob)创建一个可以用于<iframe>的URL
- })
- .catch((error) => {
- console.error('Error loading PDF:', error)
- reject(new Error('网络请求失败,请检查网络连接')); // 处理网络错误
- });
- });
- };
- const send = () => {
- if (!keyword.value) {
- message.info('请输入!');
- return;
- }
- queryText.value = keyword.value;
- keyword.value = '';
- var item = { name: queryText.value, content: '', isChain: '0' };
- chatDesc.value.push(item);
- query();
- scrollToBottom('chat_box')
- };
- const toggleQuestion = (index) => {
- keyword.value = question[index];
- };
- const keydownEnter = () => {
- send();
- };
- const processNum = ref(0)
- //文件上传
- const handleChange = (info) => {
- loading.value = true;
- fileDetail.value = null;
- const status = info.file.status;
- processNum.value = 1
- if (status === 'done') {
- processNum.value = 100;
- fileDetail.value = info.file.originFileObj;
- fileUrl.value = 'https://ai.zrzyt.zj.gov.cn/aisKnowledge' + info.file.response.data;
- chatDesc.value = [];
- loading.value = false;
- content.value = '';
- checkType.value = 'question';
- // uploadFile();
- } else if (status === 'error') {
- message.error(`${info.file.name} file upload failed.`);
- }
- };
- const downloadFile = () => {
- let a = document.createElement('a');
- var name = fileDetail.value.name;
- var type = fileUrl.value.indexOf('temp') > -1 ? 'temp' : '1';
- a.href = '/knowledge/policy/download/' + name + '/' + type;
- a.click();
- };
- const uploadFile = () => {
- queryText.value = '分别给出该文章的全文综述和要点总结';
- query();
- };
- const scrollToBottom = async (idDom) => {
- // 滚动到底部
- if (!idDom) return;
- await nextTick(() => {
- const scrollEle = document.getElementById(idDom);
- if (scrollEle != null) {
- scrollEle.scrollTo({
- top: scrollEle.scrollHeight - scrollEle.clientHeight + 50,
- behavior: 'smooth'
- });
- }
- });
- };
- const goLocation = (content) => {
- nextTick(() => {
- ifRef.value.goSourceLocation(content, 2);
- });
- };
- const openDocByIndex = (ind) => {
- var flag = false;
- docs.value.forEach((t) => {
- if (t.index == ind) {
- flag = true;
- goLocation(t.content);
- }
- });
- if (!flag) {
- message.info('未找到出处!');
- }
- };
- window.openDocByIndex = openDocByIndex;
- const handleDocs = (data) => {
- if (data) {
- docs.value = data.map((v, i) => {
- if (v.indexOf('.pdf') > 0) {
- return {
- index: v.substring(v.indexOf('[[') + 2, v.indexOf(']]')),
- doc: v.substring(v.indexOf('] [') + 3, v.indexOf('.pdf]') + 4),
- link: v.substring(v.indexOf('.pdf]') + 6, v.indexOf('.pdf)') + 4),
- content: v.substring(v.indexOf('.pdf]') + 6),
- showContent: false,
- type: 'pdf'
- };
- } else if (v.indexOf('.txt') > 0) {
- return {
- index: i++,
- doc: v.substring(v.indexOf('] [') + 3, v.indexOf('.txt]') + 4),
- link: v.substring(v.indexOf('.txt]') + 6, v.indexOf('.txt)') + 4),
- content: v.substring(v.indexOf('.txt)') + 6),
- showContent: false,
- type: 'txt'
- };
- }
- });
- }
- };
- const timers = ref([]) //缓存定时器
- const timerIndex = ref(0)
- const status = ref('0'); //0 未请求 1 请求成功 2 请求错误
- const isChain = ref(0); //0 未开始思维链 1 已经开始思维链
- const query = async () => {
- var params = new FormData();
- aiLoading.value = true;
- params.append('files', fileDetail.value);
- params.append('stream', true);
- params.append('query', queryText.value);
- timers.value = []
- timerIndex.value = 0
- status.value = 0
- isChain.value = '0'
- await fetchEventSource(window.AppGlobalConfig.aiServer + '/chat/complete_file_chat', {
- method: 'POST',
- openWhenHidden: true,
- body: params,
- signal: ctr.signal,
- async onmessage(msg) {
- if (msg) {
- try {
- aiLoading.value = false;
- const rData = JSON.parse(msg.data);
- if (rData) {
- var con = rData.choices[0].delta.content;
- if (rData.status == 3) {
- timers.value.forEach((timer) => {
- clearTimeout(timer);
- })
- let nums = getNumAll(con);
- nums.forEach(num => {
- con = con.replace(
- `[[${num}]]`,
- ''
- // `<span onclick="window.openDocByIndex(${num})" class="poi" style=" cursor: pointer; display: inline-block; width: 20px; height: 20px; font-size: 14px; line-height: 20px; text-align: center; margin: 0 5px; border-radius: 4px;background: #fff;color:#435C80;border: 1px solid #BACAE3">${num}</span>`
- )
- })
- if (checkType.value == 'interpretation') {
- content.value = con;
- scrollToBottom('scrollArea');
- } else {
- chatDesc.value[chatDesc.value.length - 1].isChain = '1'
- chatDesc.value[chatDesc.value.length - 1].content = con + '\n\n #### 以上是我思考的内容!\n\n';
- scrollToBottom('chat_box');
- }
- if (rData.docs) {
- handleDocs(rData.docs);
- }
- } else if (rData.status == 2) {
- let timer = null;
- if (checkType.value == 'interpretation') {
- timer = setTimeout(() => {
- content.value += con;
- if (content.value.indexOf('###') !== -1) {
- isChain.value = '1'
- }
- scrollToBottom('scrollArea');
- clearTimeout(timer)
- }, timerIndex.value * 15)
- } else {
- timer = setTimeout(() => {
- chatDesc.value[chatDesc.value.length - 1].content += con;
- if (chatDesc.value[chatDesc.value.length - 1].content.indexOf('###') !== -1) {
- chatDesc.value[chatDesc.value.length - 1].isChain = '1'
- }
- scrollToBottom('chat_box');
- clearTimeout(timer)
- }, timerIndex.value * 15)
- }
- timers.value.push(timer)
- timerIndex.value++;
- }
- }
- } catch (e) {
- console.log('===' + e.message);
- }
- }
- },
- onclose() {
- aiLoading.value = false;
- status.value = 1
- },
- onerror(err) {
- aiLoading.value = false;
- status.value = 2
- throw err;
- },
- onopen() {}
- });
- };
- </script>
- <style scoped lang="scss">
- .interpretation {
- width: 100%;
- height: 100%;
- position: relative;
- z-index: 1;
- background: #f0f4f7;
- .content-box {
- width: calc(100% - 300px);
- height: calc(100% - 75px);
- margin: auto;
- margin-top: 15px;
- overflow-y: auto;
- box-shadow: 0px 1px 8px 0px #e4e4e4;
- display: flex;
- .left-panel {
- border-radius: 6px;
- padding: 10px 0px 0px 0px;
- background-color: #fff;
- width: calc(100% - 600px);
- }
- .affix_box {
- position: fixed;
- right: 150px;
- top: 75px;
- }
- .right-panel {
- width: 580px;
- height: calc(100vh - 86px) !important;
- border-radius: 6px;
- background-color: #fff;
- margin-left: 20px;
- .ai-interpretation {
- width: 100%;
- height: 100%;
- .check-type {
- width: 100%;
- display: flex;
- align-items: center;
- height: 50px;
- .check {
- height: 50px;
- line-height: 50px;
- padding-left: 10px;
- }
- justify-content: space-between;
- }
- .icon {
- width: 30px;
- cursor: pointer;
- height: 30px;
- margin-right: 12px;
- background: #e0efff;
- display: flex;
- justify-content: center;
- align-items: center;
- border-radius: 5px 5px 5px 5px;
- img {
- width: 17px;
- height: 17px;
- }
- }
- .result {
- width: 100%;
- height: calc(100% - 50px);
- padding: 0px 0px 20px 0px;
- .content {
- padding: 0px 27px 0px 23px;
- height: 100%;
- background: transparent;
- box-shadow: none;
- overflow-y: scroll;
- .title {
- font-family: PingFang SC;
- font-weight: bold;
- font-size: 16px;
- margin-bottom: 23px;
- color: #000;
- }
- .desc {
- font-family: PingFang SC;
- font-weight: normal;
- font-size: 14px;
- //color: #85909e;
- line-height: 28px;
- margin-bottom: 36px;
- >.chain {
- color: #666;
- border-left: 1px solid #ececec;
- padding-left: 15px;
- }
- }
- .spin {
- height: 100%;
- display: flex;
- align-items: center;
- justify-content: center;
- }
- }
- .question {
- height: 100%;
- padding: 0px 0px 6px 0px;
- position: relative;
- display: flex;
- flex-direction: column;
- .item {
- display: flex;
- width: 100%;
- background: #fff;
- padding: 4px 20px 15px 20px;
- img {
- width: 40px;
- height: 29px;
- margin-right: 13px;
- }
- .value-panel {
- flex: 1;
- .name {
- font-family: PingFang SC, PingFang SC;
- font-weight: 400;
- font-size: 14px;
- color: #2c2c2c;
- line-height: 21px;
- span {
- color: #5a5b5e;
- }
- }
- .citem {
- margin-top: 0px;
- display: flex;
- cursor: pointer;
- align-items: center;
- padding: 16px 0px 0px 0px;
- img {
- width: 18px;
- height: 18px;
- margin-right: 9px;
- }
- .value {
- flex: 1;
- font-family: PingFang SC, PingFang SC;
- font-weight: 400;
- font-size: 14px;
- color: #ef7217;
- line-height: 21px;
- }
- }
- }
- }
- .scale-panel {
- display: flex;
- justify-content: flex-end;
- font-family: PingFang SC, PingFang SC;
- font-weight: 400;
- margin-top: 7px;
- margin-bottom: 27px;
- cursor: pointer;
- width: calc(100% - 30px);
- font-size: 13px;
- color: #778490;
- span {
- font-family: PingFang SC, PingFang SC;
- font-weight: 500;
- font-size: 13px;
- color: #3c7bff;
- }
- }
- .chat_box {
- flex: 1;
- height: 0px;
- overflow-y: auto;
- }
- .chat {
- font-family: PingFang SC;
- font-weight: 500;
- margin: 10px 0px;
- line-height: 28px;
- font-size: 16px;
- color: #333333;
- .item {
- display: flex;
- flex-direction: column;
- padding: 0px;
- background: transparent;
- width: unset;
- .user-panel {
- display: flex;
- margin: 20px 0px;
- justify-content: flex-end;
- img {
- width: 36px;
- margin-left: 7px;
- height: 36px;
- }
- .name {
- max-width: calc(100% - 43px);
- background: #3c7bff;
- border-radius: 5px 5px 5px 5px;
- padding: 7px 24px 7px 11px;
- font-family: PingFang SC, PingFang SC;
- font-weight: 500;
- font-size: 14px;
- color: #ffffff;
- line-height: 21px;
- }
- }
- .ai-panel {
- display: flex;
- img {
- width: 32px;
- margin-left: 13px;
- height: 32px;
- }
- .desc {
- max-width: calc(100% - 95px);
- background: #f5f8fc;
- border-radius: 6px 6px 6px 6px;
- padding: 10px 18px 10px 13px;
- font-weight: 500;
- overflow-y: auto;
- font-size: 14px;
- color: #333333;
- line-height: 28px;
- >.chain {
- color: #666;
- border-left: 1px solid #ccc;
- padding-left: 15px;
- }
- }
- .animation {
- @keyframes dot {
- 0%,
- 20% {
- content: '';
- }
- 40% {
- content: '.';
- }
- 60% {
- content: '..';
- }
- 80%,
- 100% {
- content: '...';
- }
- }
- .dots::after {
- display: inline-block;
- animation: dot 1.5s steps(1, end) infinite;
- content: '';
- }
- }
- }
- }
- }
- .input-panel {
- position: relative;
- bottom: 0px;
- background: #f5f8fc;
- border-radius: 6px;
- height: 111px;
- padding: 2px;
- width: calc(100% - 54px);
- margin-left: 25px;
- textarea {
- border: none;
- padding: 16px 22px;
- height: 100%;
- background: #f5f8fc;
- font-weight: 400;
- font-size: 16px;
- color: #333333;
- &:focus-visible {
- border: none !important;
- outline: none;
- box-shadow: none;
- }
- &:focus {
- border: none !important;
- outline: none;
- box-shadow: none;
- }
- }
- .send {
- right: 17px;
- bottom: 14px;
- cursor: pointer;
- width: 79px;
- height: 32px;
- background: linear-gradient(134deg, #1ba3fb 0%, #0745f6 100%);
- border-radius: 16px 16px 16px 16px;
- z-index: 2;
- position: absolute;
- display: flex;
- align-items: center;
- justify-content: center;
- img {
- width: 13px;
- height: 13px;
- margin-left: 6px;
- }
- font-family: PingFang SC, PingFang SC;
- font-weight: 400;
- font-size: 14px;
- color: #ffffff;
- }
- }
- }
- }
- }
- }
- .zm {
- width: 640px;
- }
- }
- .content-top {
- width: 100%;
- height: 80px;
- padding: 0 30px;
- display: flex;
- justify-content: space-between;
- align-items: center;
- border-bottom: 1px solid #e9e9e9;
- .left {
- .upload {
- font-size: 16px;
- color: #2185f2;
- cursor: pointer;
- img {
- margin-right: 5px;
- }
- }
- .ai-btn {
- width: 120px;
- height: 40px;
- background: url('/images/zcjd/ai-btn-icon.png');
- background-repeat: no-repeat;
- background-size: 100% 100%;
- border-radius: 12px;
- padding-left: 35px;
- margin-left: 20px;
- font-size: 18px;
- font-weight: 600;
- color: #ffffff;
- }
- }
- .right {
- height: 80px;
- line-height: 80px;
- color: #666666;
- span {
- margin-left: 20px;
- cursor: pointer;
- }
- span:hover {
- box-shadow: 1px 1px 1px 1px rgba(140, 140, 140, 0.2);
- }
- img {
- margin-right: 4px;
- width: 20px;
- height: 20px;
- }
- }
- }
- .content-detail {
- width: 100%;
- min-height: calc(100% - 80px);
- display: flex;
- .file-preview {
- flex: 1;
- text-align: center;
- .spin {
- display: flex;
- align-items: center;
- height: 100%;
- justify-content: center;
- }
- }
- }
- ::-webkit-scrollbar {
- width: 3px !important;
- }
- }
- </style>
|