index.vue 44 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203
  1. <template>
  2. <div class="home_box">
  3. <draggable-splitter rightWidht="680px">
  4. <template #left>
  5. <div class="boxs">
  6. <div class="left_box">
  7. <button @click="startNewSessionHandle">
  8. <span class="icon"></span>
  9. <span>开启新对话</span>
  10. </button>
  11. <div class="history">
  12. <div class="menu_down">
  13. <span class="title">历史问答</span>
  14. <span class="icon" @click="toggleCardHandle('history')">
  15. <UpOutlined v-if="visibleMap['history']" />
  16. <DownOutlined v-else />
  17. </span>
  18. </div>
  19. <div class="history_box" v-show="visibleMap['history']">
  20. <ul>
  21. <template v-for="(session, index) in sessionLists">
  22. <li :key="index" v-if="session['datas'].length > 0">
  23. <span class="title">{{session['title']}}</span>
  24. <ul>
  25. <li
  26. v-for="(item, cindex) in session['datas']"
  27. :key="cindex"
  28. :class="{'active': item['id'] === cSessionId}"
  29. @mouseenter="sessionDelId = item['id']"
  30. @mouseleave="sessionDelId = ''">
  31. <span @click="switchSession(item)">{{item['tittle']}}</span>
  32. <span v-if="sessionDelId === item['id']" @click="onSessionDeleteHandle(item['id'])">
  33. <DeleteOutlined />
  34. </span>
  35. </li>
  36. </ul>
  37. </li>
  38. </template>
  39. </ul>
  40. </div>
  41. </div>
  42. <div class="ai_tools">
  43. <div class="menu_down">
  44. <span class="title">AI工具</span>
  45. <span class="icon" @click="toggleCardHandle('tool')">
  46. <UpOutlined v-if="visibleMap['tool']" />
  47. <DownOutlined v-else />
  48. </span>
  49. </div>
  50. <ul v-show="visibleMap['tool']">
  51. <li @click="toToolPage('./#/policy/interpret')">
  52. <span class="icon">
  53. <span class="iconfont icon-a-lujing8796"></span>
  54. </span>
  55. <span class="txt">政策解读</span>
  56. </li>
  57. <li @click="toToolPage('./#/policy/smart')">
  58. <span class="icon">
  59. <span class="iconfont icon-a-lujing8794"></span>
  60. </span>
  61. <span class="txt">政策比对</span>
  62. </li>
  63. </ul>
  64. </div>
  65. </div>
  66. <div class="right_box">
  67. <div class="chat-container">
  68. <div v-if="historys.length === 0" class="messages-container">
  69. <p class="n_tips">Hi~,我是自然资源大模型,您身边的智能助手!</p>
  70. </div>
  71. <div v-else class="messages-container" ref="msgContainer">
  72. <template v-for="(history,index) in historys" :key="index">
  73. <div class="message user">
  74. {{ history.question }}
  75. </div>
  76. <div class="message assistant">
  77. <div class="ai-search-detail">
  78. <div class="search-panel" id="pageContainer">
  79. <div :class="`search-detail search-detail-${askType}`">
  80. <div
  81. class="search-result"
  82. id="searchResult"
  83. style="overflow-x: hidden; height: auto"
  84. ref="messageContainer"
  85. >
  86. <div :class="`result-panel result-panel-${askType}`">
  87. <div class="result">
  88. <template v-if="askType === 'zcfg'">
  89. <div class="result-view">
  90. <div class="q-r">
  91. <div class="ds-content-box">
  92. <div class="icon">
  93. <img src="/images/icon-ds.png" />
  94. </div>
  95. <div class="ds-panel">
  96. <div class="ds-loading" v-if="history.currentResponse.hintTxt" @click="history.currentResponse.showChain = !history.currentResponse.showChain">
  97. {{ history.currentResponse.hintTxt }}
  98. <DownOutlined class="icon-arrow" :class="{ rotate: history.currentResponse.showChain }" />
  99. </div>
  100. <div class="ds-con" v-if="history.currentResponse.showChain">
  101. <vue-markdown-it
  102. id="dsMarkdown"
  103. :source="
  104. history.currentResponse.streamMock
  105. ? history.currentResponse.streamMsg.indexOf('###') > -1
  106. ? history.currentResponse.streamMsg.substring(
  107. 0,
  108. history.currentResponse.streamMsg.indexOf('###')
  109. )
  110. : history.currentResponse.streamMsg
  111. : history.currentResponse.msg.indexOf('</think>') > -1
  112. ? history.currentResponse.msg.substring(
  113. 0,
  114. history.currentResponse.msg.indexOf('</think>')
  115. )
  116. : history.currentResponse.msg
  117. "
  118. :options="{
  119. html: true,
  120. linkify: true
  121. }"
  122. />
  123. </div>
  124. <a-spin :indicator="indicator" v-if="history.currentResponse.loading" />
  125. </div>
  126. </div>
  127. <vue-markdown-it
  128. id="resMarkdown"
  129. :source="
  130. history.currentResponse.streamMock
  131. ? history.currentResponse.streamMsg.indexOf('###') > -1
  132. ? history.currentResponse.streamMsg.substring(
  133. history.currentResponse.streamMsg.indexOf('###')
  134. )
  135. : ''
  136. : history.dsChecked
  137. ? history.currentResponse.msg.indexOf('</think>') > -1
  138. ? history.currentResponse.msg.substring(history.currentResponse.msg.indexOf('</think>'))
  139. : ''
  140. : history.currentResponse.msg
  141. "
  142. :options="{
  143. html: true,
  144. linkify: true
  145. }"
  146. />
  147. </div>
  148. <div class="source" v-if="activeTab !== 'original' && history.currentResponse.docs.length > 0" v-show="activeIndex === 5">
  149. <div class="title">
  150. <span>基于{{ history.currentResponse.docs.length }}个参考来源</span>
  151. <span class="icon" @click="history.sourceVisible = !history.sourceVisible">
  152. <UpOutlined v-if="history.sourceVisible" />
  153. <DownOutlined v-else />
  154. </span>
  155. </div>
  156. <div class="items" v-show="history.sourceVisible">
  157. <div
  158. class="item"
  159. v-if="activeTab !== 'net'"
  160. v-for="(doc, i) in history.currentResponse.docs"
  161. :key="'doc-' + i"
  162. >
  163. <div class="doc">
  164. <p>
  165. <span
  166. class="ma"
  167. style="font-weight: bolder; color: #000000; margin-right: 10px"
  168. >出处 [{{ i + 1 }}] </span
  169. ><span class="doc-link" @click="openDoc(doc, i)">{{ doc.doc }}</span>
  170. </p>
  171. <div
  172. :class="`doc-icon${
  173. !doc.showContent ? ' doc-icon-show' : ' doc-icon-hide'
  174. }`"
  175. @click="doc.showContent = !doc.showContent"
  176. ></div>
  177. </div>
  178. <div :class="`content${doc.showContent ? '' : ' content-hide'}`">
  179. <p>{{ doc.content }}</p>
  180. </div>
  181. </div>
  182. <div
  183. class="item item-url"
  184. v-if="activeTab === 'net'"
  185. v-for="(doc, i) in history.currentResponse.docs"
  186. :key="'doc-' + i"
  187. >
  188. <div class="doc">
  189. <p>
  190. <span class="doc-link" @click="openUrl(doc.link)">{{ doc.title }}</span>
  191. </p>
  192. </div>
  193. <div class="bottom">
  194. <div class="title-icon">
  195. <div class="icon"></div>
  196. <div class="title">{{ doc.doc }}</div>
  197. </div>
  198. <div class="index">{{ i + 1 }}</div>
  199. </div>
  200. </div>
  201. </div>
  202. </div>
  203. </div>
  204. </template>
  205. <template v-else>
  206. <div v-if="activeIndex >= 1" class="map-answer">
  207. <div class="left-panel">
  208. <div class="content">
  209. <div class="summary-card">
  210. <div class="summary-title" id="tdscSummaryTitle">总结</div>
  211. <div class="summary-content">
  212. <a-skeleton active v-if="!history.currentResponse.msg" />
  213. <vue-markdown-it
  214. :source="
  215. history.currentResponse.streamMock
  216. ? history.currentResponse.streamMsg
  217. : history.currentResponse.msg
  218. "
  219. :options="{
  220. html: true,
  221. linkify: true
  222. }"
  223. />
  224. </div>
  225. </div>
  226. <div class="chart-card" v-if="history.currentResponse.hasChart" id="tdscChartCard">
  227. <div class="chart-title">生成图表</div>
  228. <a-skeleton
  229. active
  230. style="height: 100%"
  231. v-if="!history.currentResponse.chartOption"
  232. />
  233. <div v-else class="chart" id="summaryChart"></div>
  234. </div>
  235. </div>
  236. </div>
  237. </div>
  238. </template>
  239. </div>
  240. </div>
  241. </div>
  242. </div>
  243. </div>
  244. </div>
  245. </div>
  246. </template>
  247. </div>
  248. <div class="input-container">
  249. <textarea
  250. v-model="cQuestion"
  251. class="input-box"
  252. ref="msgInput"
  253. placeholder="请输入对话内容,换行请使用Shift+Enter"
  254. rows="5"
  255. @keydown="onKeydownHandle"
  256. ></textarea>
  257. <div class="bottom_box">
  258. <div class="tools_box">
  259. <div class="tool_box_1">
  260. <a-dropdown>
  261. <template #overlay>
  262. <a-menu @click="({key})=>onChange(key)">
  263. <a-menu-item key="0">
  264. <div :class="{tool_1: true, active: modelType === '0'}">
  265. DeepSeek
  266. </div>
  267. </a-menu-item>
  268. <a-menu-item key="1">
  269. <div :class="{tool_1: true, active: modelType === '1'}">
  270. 通义千问
  271. </div>
  272. </a-menu-item>
  273. </a-menu>
  274. </template>
  275. <a-button style="width: 120px">
  276. {{ modelType === '0' ? 'DeepSeek' : '通义千问'}}
  277. <DownOutlined />
  278. </a-button>
  279. </a-dropdown>
  280. </div>
  281. <div class="tool_box_2">
  282. <a-dropdown>
  283. <template #overlay>
  284. <a-menu @click="changeAnswerType">
  285. <a-menu-item key="0">
  286. <div :class="{tool_1: true, active: answerType === '0'}">
  287. 简洁
  288. </div>
  289. </a-menu-item>
  290. <a-menu-item key="1">
  291. <div :class="{tool_1: true, active: answerType === '1'}">
  292. 深入
  293. </div>
  294. </a-menu-item>
  295. <a-menu-item key="2">
  296. <div :class="{tool_1: true, active: answerType === '2'}">
  297. 研究
  298. </div>
  299. </a-menu-item>
  300. </a-menu>
  301. </template>
  302. <a-button style="width: 80px">
  303. {{ answerType === '0' ? '简洁' : answerType === '1' ? '深入' : '研究'}}
  304. <DownOutlined />
  305. </a-button>
  306. </a-dropdown>
  307. </div>
  308. <div class="tool_box_3">
  309. <a-dropdown>
  310. <template #overlay>
  311. <a-menu @click="({key})=>changeTab(key)">
  312. <a-menu-item
  313. v-for="t in tabs"
  314. :key="t.key"
  315. >
  316. <div :class="{tool_1: true, active: activeTab === t.key}">
  317. {{t.name}}
  318. </div>
  319. </a-menu-item>
  320. </a-menu>
  321. </template>
  322. <a-button style="width: 90px">
  323. {{ activeTab === 'knowledge' ? '知识库' : activeTab === 'net' ? '全网' : '原生'}}
  324. <DownOutlined />
  325. </a-button>
  326. </a-dropdown>
  327. </div>
  328. </div>
  329. <div class="send_btn">
  330. <div v-if="historys.length > 0 && historys[historyIndex].currentResponse.loading" @click="onSendHandle(false)">
  331. <i class="stop"></i>
  332. </div>
  333. <div v-else @click="onSendHandle">
  334. <i class="iconfont icon-a-lujing9250"></i>
  335. </div>
  336. </div>
  337. </div>
  338. </div>
  339. </div>
  340. </div>
  341. </div>
  342. </template>
  343. <template #right v-if="showDoc">
  344. <div
  345. class="docs_box"
  346. style="background-color: white;"
  347. >
  348. <p-d-f-viewer
  349. v-if="fileType === 'pdf'"
  350. :src="pdfSrc"
  351. @close="closeDoc"
  352. :content="pdfContent"
  353. :num="pdfNum"
  354. />
  355. <word-viewer
  356. v-if="fileType === 'docx'"
  357. :src="pdfSrc"
  358. @close="closeDoc"
  359. :content="pdfContent"
  360. :num="pdfNum"
  361. >
  362. </word-viewer>
  363. <txt-viewer v-if="fileType === 'txt'" :src="pdfSrc" @close="closeDoc" :txt="pdfContent" />
  364. </div>
  365. </template>
  366. </draggable-splitter>
  367. </div>
  368. </template>
  369. <script setup>
  370. import {
  371. LoadingOutlined,
  372. DeleteOutlined,
  373. UpOutlined,
  374. DownOutlined
  375. } from "@ant-design/icons-vue";
  376. import { Modal } from 'ant-design-vue';
  377. import dayjs from 'dayjs';
  378. import { fetchEventSource } from '@microsoft/fetch-event-source';
  379. import PDFViewer from '@/components/pdf/PdfCanvas.vue';
  380. import WordViewer from '@/components/pdf/WordViewer.vue';
  381. import { VueMarkdownIt } from '@f3ve/vue-markdown-it';
  382. import TxtViewer from '@/components/pdf/TxtViewer.vue';
  383. import { message } from 'ant-design-vue';
  384. import { h, ref, reactive, watch } from 'vue';
  385. import ManagerAPI from '@/api/manager';
  386. import PubsubService from '@/utils/PubsubService';
  387. import { getNumAll } from '@/utils/common';
  388. import { useUserStore } from '@/stores';
  389. import DraggableSplitter from "./components/DraggableSplitter.vue";
  390. let ctr = null;
  391. const userStore = useUserStore();
  392. const visibleMap = reactive({
  393. history: true,
  394. tool: true
  395. })
  396. const toggleCardHandle = (type) => {
  397. visibleMap[type] = !visibleMap[type]
  398. }
  399. const msgInput = ref(null)
  400. const msgContainer = ref(null)
  401. function scrollToBottom() {
  402. msgContainer.value.scrollTop = msgContainer.value.scrollHeight;
  403. }
  404. const historys = ref([])
  405. let historyIndex = -1;
  406. const cQuestion = ref('')
  407. const cSessionId = ref('')
  408. const sessionDelId = ref('')
  409. const isSessionNew = ref(true)
  410. const startNewSessionHandle = () => {
  411. cQuestion.value = ''
  412. historys.value = []
  413. historyIndex = -1
  414. isSessionNew.value = true
  415. cSessionId.value = ''
  416. }
  417. const onKeydownHandle = (e) => {
  418. if (e.key === 'Enter' && !e.shiftKey) {
  419. e.preventDefault();
  420. if (historys.value.length > 0 && historys.value[historyIndex].currentResponse.loading) {
  421. message.error('回答输出中,请稍后操作或点击停止回答');
  422. return;
  423. }
  424. onSendHandle();
  425. }
  426. }
  427. const onSendHandle = (status = true) => {
  428. if (!cQuestion.value) {
  429. message.error('请输入问题');
  430. return;
  431. }
  432. const isFllow = historys.value.length > 0
  433. if (!status) {
  434. historys.value[historyIndex].currentResponse.loading = false;
  435. stopAI();
  436. return;
  437. }
  438. historyIndex++;
  439. historys.value.push({
  440. question: cQuestion.value,
  441. dsChecked: true,
  442. sourceVisible: false,
  443. currentResponse: {
  444. loading: true,
  445. hintTxt: '',
  446. streamMsg: '',
  447. streamMock: false,
  448. msg: '',
  449. originAnswer: '',
  450. showChain: true,
  451. oDocs: '',
  452. docs: []
  453. }
  454. })
  455. ask(decodeURIComponent(cQuestion.value), isFllow);
  456. cQuestion.value = '';
  457. const timer = setTimeout(() => {
  458. scrollToBottom()
  459. clearTimeout(timer)
  460. }, 50)
  461. }
  462. const toToolPage = (path) => {
  463. window.open(path, '_blank')
  464. }
  465. const aiLoading = ref(false);
  466. const indicator = h(LoadingOutlined, {
  467. style: {
  468. fontSize: '24px'
  469. },
  470. spin: true
  471. });
  472. const modelType = ref('0')
  473. const answerType = ref('0');
  474. const question = ref('国有土地的使用方式有哪些?');
  475. const statusText = ref('检索中');
  476. const activeIndex = ref(0);
  477. const activeTab = ref('knowledge');
  478. const evaluate = ref(null);
  479. const open = ref(true);
  480. const showDoc = ref(false);
  481. const pdfSrc = ref('');
  482. const pdfContent = ref('');
  483. const pdfNum = ref(1);
  484. const fileType = ref('pdf');
  485. const startTime = ref(0);
  486. const endTime = ref(0);
  487. const times = ref(0);
  488. const timers = ref([]);
  489. let streamMockInterval = null;
  490. const streamToAnswer = () => {
  491. historys.value[historyIndex].currentResponse.index = 0;
  492. streamMockInterval = setInterval(() => {
  493. const { originAnswer = '', msg, streamMsg = '', id, index = 0 } = historys.value[historyIndex].currentResponse;
  494. if (historys.value[historyIndex].currentResponse.mockStart || index <= originAnswer.length) {
  495. historys.value[historyIndex].currentResponse.streamMsg = originAnswer.substr(0, index + 2).replaceAll('\n', ' \n');
  496. if (originAnswer) {
  497. historys.value[historyIndex].currentResponse.index += 2;
  498. }
  499. let num = getNum(historys.value[historyIndex].currentResponse.streamMsg);
  500. while (num) {
  501. const docsNum = historys.value[historyIndex].currentResponse.docs.length;
  502. historys.value[historyIndex].currentResponse.streamMsg = historys.value[historyIndex].currentResponse.streamMsg.replace(
  503. `[[${num}]]`,
  504. `<span onclick="window.openDocByIndex(${num}, ${id}, ${historyIndex})" class="poi" style=" cursor: pointer; display: inline-block; width: 20px; height: 20px; font-size: 12px; line-height: 20px; text-align: center; margin: 0 5px; border-radius: 10px;background: #d0d5dd; width: 20px;
  505. height: 20px;
  506. background: #FFFFFF;
  507. border-radius: 4px 4px 4px 4px;
  508. border: 1px solid #BACAE3;">${num}</span>`
  509. );
  510. num = getNum(historys.value[historyIndex].currentResponse.streamMsg);
  511. }
  512. } else {
  513. if (streamMockInterval) {
  514. clearInterval(streamMockInterval);
  515. streamMockInterval = null;
  516. }
  517. activeIndex.value = 5;
  518. }
  519. }, 50);
  520. };
  521. const tabs = [
  522. { key: 'knowledge', name: '知识库' },
  523. // { key: 'net', name: '全网' },
  524. { key: 'original', name: '原生' },
  525. ];
  526. const changeStatusText = () => {
  527. let i = 0;
  528. setInterval(() => {
  529. statusText.value = '检索中' + '.'.repeat(i);
  530. if (i === 3) {
  531. i = 0;
  532. }
  533. i++;
  534. }, 500);
  535. };
  536. changeStatusText();
  537. const askType = ref('zcfg');
  538. const ask = async (q, isFllow) => {
  539. if (ctr) {
  540. if (streamMockInterval) {
  541. clearInterval(streamMockInterval);
  542. streamMockInterval = null;
  543. }
  544. ctr.abort();
  545. }
  546. times.value = 0;
  547. if (timers.value) {
  548. timers.value.forEach((t) => {
  549. clearTimeout(t);
  550. t = null;
  551. });
  552. }
  553. timers.value = [];
  554. if (historys.value[historyIndex].dsChecked) {
  555. historys.value[historyIndex].currentResponse.hintTxt = '';
  556. historys.value[historyIndex].currentResponse.loading = true;
  557. }
  558. question.value = q;
  559. open.value = false;
  560. showDoc.value = false;
  561. askType.value = 'zcfg';
  562. quest(isFllow);
  563. };
  564. let questionUrl = '/chat/kb_chat';
  565. const changeTab = (tab) => {
  566. times.value = 0;
  567. if (timers.value) {
  568. timers.value.forEach((t) => {
  569. clearTimeout(t);
  570. t = null;
  571. });
  572. }
  573. timers.value = [];
  574. if (tab === 'net') {
  575. questionUrl = '/chat/bing_chat';
  576. } else if(tab === 'knowledge'){
  577. questionUrl = '/chat/kb_chat';
  578. }else if(tab === 'original') {
  579. questionUrl = '/chat/chat';
  580. }
  581. activeTab.value = tab;
  582. };
  583. const onChange = (type) => {
  584. modelType.value = type;
  585. dsChange(type);
  586. changeAnswerType({key: '0'});
  587. };
  588. //ds绑定用户改变
  589. const dsChange = (type) => {
  590. localStorage.setItem("_isDeepSeek", type);
  591. if (timers.value) {
  592. timers.value.forEach((t) => {
  593. clearTimeout(t);
  594. t = null;
  595. });
  596. timers.value = [];
  597. }
  598. times.value = 0;
  599. //打字机效果 切换会打印
  600. aiLoading.value = true;
  601. };
  602. const changeAnswerType = ({ key }) => {
  603. answerType.value = key
  604. };
  605. const questHistories = ref([]);
  606. let scb = null;
  607. const quest = async (isFllow) => {
  608. startTime.value = Date.now();
  609. window.scroll({ top: 0, behavior: 'smooth' });
  610. evaluate.value = null;
  611. activeIndex.value = 0;
  612. if (!isFllow) {
  613. showDoc.value = false;
  614. }
  615. if (isFllow) {
  616. const { id, question, msg, docs, originAnswer, keywords = [] } = historys.value[historyIndex].currentResponse;
  617. questHistories.value.push({ id, question, msg, docs: docs, originAnswer, keywords });
  618. } else {
  619. questHistories.value = [];
  620. }
  621. aiLoading.value = true;
  622. if (scb !== null) {
  623. clearInterval(scb);
  624. scb = null;
  625. } else {
  626. scb = setInterval(() => {
  627. }, 500);
  628. }
  629. ctr = new AbortController();
  630. const id = questHistories.value.length;
  631. historys.value[historyIndex].currentResponse = {
  632. loading: true,
  633. time: 0,
  634. hintTxt: '',
  635. question: question.value,
  636. id,
  637. msg: '',
  638. originAnswer: '',
  639. streamMock: false,
  640. streamMsg: '',
  641. oDocs: '',
  642. showChain: true,
  643. docs: []
  644. };
  645. activeIndex.value = 0;
  646. if (isSessionNew.value) {
  647. sessionCreate();
  648. isSessionNew.value = false
  649. }
  650. if (activeTab.value === 'net') {
  651. questionUrl = '/chat/bing_chat';
  652. } else if(activeTab.value === 'knowledge'){
  653. questionUrl = '/chat/kb_chat';
  654. }else if(activeTab.value === 'original') {
  655. questionUrl = '/chat/chat';
  656. }
  657. const topKs = window?.AppGlobalConfig?.topKs || {
  658. 0: 5,
  659. 1: 10,
  660. 2: 15
  661. };
  662. let body = null
  663. if (activeTab.value === 'net') {
  664. body = {
  665. query: question.value,
  666. stream: true,
  667. model: modelType.value === '0' ? 'deepseek-r1' : '',
  668. search_type: answerType.value
  669. }
  670. } else if(activeTab.value === 'knowledge'){
  671. body = {
  672. query: question.value,
  673. mode: 'local_kb',
  674. kb_name: activeTab.value === 'paper' ? 'compose_paper_material_total' : modelType.value === '1' ? window?.AppGlobalConfig?.llm?.kb_name : 'policy',
  675. top_k: topKs[answerType.value],
  676. search_type: answerType.value,
  677. score_threshold: 0.5,
  678. model: historys.value[historyIndex].dsChecked ? 'deepseek-r1' : '',
  679. history: [],
  680. stream: true,
  681. prompt_name: 'rag_context_qa.md',
  682. return_direct: false
  683. }
  684. } else if (activeTab.value === 'original') {
  685. body = {
  686. query: question.value,
  687. history: [],
  688. stream: true
  689. }
  690. }
  691. if (isFllow) {
  692. if (activeTab.value === 'knowledge') {
  693. if (questHistories.value.length > 0) {
  694. body.history_keyword = questHistories.value[questHistories.value.length - 1].keywords;
  695. const arrs = historys.value.map((history) => {
  696. return {
  697. "role": "user",
  698. "content": history.question
  699. }
  700. })
  701. body.history = arrs
  702. }
  703. } else if (activeTab.value === 'original') {
  704. for (let i = 0; i < historys.value.length; i++){
  705. const history = historys.value[i]
  706. if (history && history['currentResponse'] && history['currentResponse']['msg']) {
  707. body.history.push([
  708. history['currentResponse']['question'],
  709. history['currentResponse']['msg'].substring(history['currentResponse']['msg'].indexOf('</think>')+8)
  710. ])
  711. }
  712. }
  713. }
  714. }
  715. if (activeTab.value !== 'net' && answerType.value !== '0') {
  716. historys.value[historyIndex].currentResponse.streamMock = true;
  717. historys.value[historyIndex].currentResponse.mockStart = true;
  718. streamToAnswer();
  719. }
  720. const rootUrl = modelType.value === '1' ? window.AppGlobalConfig.aisChat : window.AppGlobalConfig.aiServer
  721. await fetchEventSource(rootUrl + questionUrl, {
  722. method: 'POST',
  723. openWhenHidden: true,
  724. timeout: 300000,
  725. headers: {
  726. 'Content-Type': 'application/json'
  727. },
  728. body: JSON.stringify(body),
  729. signal: ctr.signal,
  730. async onmessage(msg) {
  731. if (activeIndex.value !== 3) {
  732. activeIndex.value = 3;
  733. }
  734. activeTab.value === 'net' ? handleNetResponse(msg, id) : handleKnowledgeResponse(msg, id);
  735. },
  736. onclose () {
  737. if (scb !== null) {
  738. clearInterval(scb);
  739. scb = null;
  740. collectQuestion();
  741. }
  742. if (historys.value[historyIndex].currentResponse.streamMock) {
  743. historys.value[historyIndex].currentResponse.mockStart = false;
  744. } else {
  745. activeIndex.value = 5;
  746. }
  747. },
  748. onerror (err) {
  749. historys.value[historyIndex].currentResponse.loading = false
  750. throw err;
  751. }
  752. });
  753. };
  754. const handleKnowledgeResponse = (msg, id) => {
  755. if (!msg || !msg.data) {
  756. return;
  757. }
  758. const rData = JSON.parse(msg.data);
  759. if (rData?.choices && rData.choices.length > 0) {
  760. if (activeTab.value === 'net' && rData.status !== 2) {
  761. return;
  762. }
  763. if (rData.status == 3) {
  764. if (timers.value) {
  765. timers.value.forEach((t) => {
  766. clearTimeout(t);
  767. t = null;
  768. });
  769. timers.value = [];
  770. }
  771. endTime.value = Date.now();
  772. var time = ((endTime.value - startTime.value) / 1000).toFixed(0);
  773. historys.value[historyIndex].currentResponse.hintTxt = `已深度思考(用时 ${time} 秒)`;
  774. historys.value[historyIndex].currentResponse.time = time;
  775. historys.value[historyIndex].currentResponse.loading = false;
  776. aiLoading.value=true;
  777. historys.value[historyIndex].currentResponse.originAnswer = rData.choices[0]?.delta?.content.replaceAll(
  778. '\n',
  779. ` \n`
  780. );
  781. historys.value[historyIndex].currentResponse.msg = rData.choices[0]?.delta?.content.replaceAll('\n', ` \n`);
  782. let num = getNum(historys.value[historyIndex].currentResponse.msg);
  783. while (num) {
  784. const docsNum = historys.value[historyIndex].currentResponse.docs.length;
  785. historys.value[historyIndex].currentResponse.msg = historys.value[historyIndex].currentResponse.msg.replace(
  786. `[[${num}]]`,
  787. `<span onclick="window.openDocByIndex(${num}, ${id}, ${historyIndex})" class="poi" style=" cursor: pointer; display: inline-block; width: 20px; height: 20px; font-size: 12px; line-height: 20px; text-align: center; margin: 0 5px; border-radius: 10px;background: #d0d5dd; width: 20px;
  788. height: 20px;
  789. background: #FFFFFF;
  790. border-radius: 4px 4px 4px 4px;
  791. border: 1px solid #BACAE3;">${num}</span>`
  792. );
  793. num = getNum(historys.value[historyIndex].currentResponse.msg);
  794. }
  795. } else {
  796. aiLoading.value = false;
  797. historys.value[historyIndex].currentResponse.hintTxt = '思考中...';
  798. //ds模式打字机效果输出
  799. const timer = setTimeout(() => {
  800. if (!aiLoading.value) {
  801. historys.value[historyIndex].currentResponse.originAnswer += rData.choices[0]?.delta?.content.replaceAll(
  802. '\n',
  803. ` \n`
  804. );
  805. historys.value[historyIndex].currentResponse.msg += rData.choices[0]?.delta?.content.replaceAll('\n', ` \n`);
  806. let num = getNum(historys.value[historyIndex].currentResponse.msg);
  807. while (num) {
  808. const docsNum = historys.value[historyIndex].currentResponse.docs.length;
  809. historys.value[historyIndex].currentResponse.msg = historys.value[historyIndex].currentResponse.msg.replace(
  810. `[[${num}]]`,
  811. `<span onclick="window.openDocByIndex(${num}, ${id}, ${historyIndex})" class="poi" style=" cursor: pointer; display: inline-block; width: 20px; height: 20px; font-size: 12px; line-height: 20px; text-align: center; margin: 0 5px; border-radius: 10px;background: #d0d5dd; width: 20px;
  812. height: 20px;
  813. background: #FFFFFF;
  814. border-radius: 4px 4px 4px 4px;
  815. border: 1px solid #BACAE3;">${num}</span>`
  816. );
  817. num = getNum(historys.value[historyIndex].currentResponse.msg);
  818. }
  819. }
  820. clearTimeout(timer);
  821. timers.value.push(timer);
  822. }, times.value * 15);
  823. }
  824. }
  825. if (!historys.value[historyIndex].currentResponse.docs.length) {
  826. historys.value[historyIndex].currentResponse.oDocs = JSON.stringify(rData.docs)
  827. if (rData.docs && rData.docs.length) {
  828. handleDocs(rData.docs);
  829. }
  830. }
  831. times.value++;
  832. setTimeout(() => {
  833. scrollToBottom()
  834. }, 50)
  835. };
  836. const handleNetResponse = (msg, id) => {
  837. const rData = msg.data;
  838. if (!!rData && rData !== '[DONE]') {
  839. try {
  840. const res = JSON.parse(rData);
  841. if (res.error) {
  842. activeIndex.value = 4;
  843. message.error(res.error);
  844. historys.value[historyIndex].currentResponse.msg = res.error;
  845. return;
  846. }
  847. if (res.rag_finish) {
  848. if (activeIndex.value < 4) {
  849. activeIndex.value = 4;
  850. }
  851. if (historys.value[historyIndex].dsChecked) {
  852. endTime.value = Date.now();
  853. var time = ((endTime.value - startTime.value) / 1000).toFixed(0);
  854. historys.value[historyIndex].currentResponse.hintTxt = `已深度思考(用时 ${time} 秒)`;
  855. historys.value[historyIndex].currentResponse.loading = false;
  856. }
  857. } else {
  858. if (historys.value[historyIndex].dsChecked && historys.value[historyIndex].currentResponse.hintTxt != '思考中...') {
  859. historys.value[historyIndex].currentResponse.hintTxt = '思考中...';
  860. }
  861. }
  862. if (res.result) {
  863. historys.value[historyIndex].currentResponse.originAnswer = res.result;
  864. historys.value[historyIndex].currentResponse.msg = res.result.replaceAll('\n', ` \n`);
  865. let num = getNum(historys.value[historyIndex].currentResponse.msg);
  866. while (num) {
  867. const docsNum = historys.value[historyIndex].currentResponse.docs.length;
  868. if (docsNum && num > docsNum + 1) {
  869. historys.value[historyIndex].currentResponse.msg = historys.value[historyIndex].currentResponse.msg.replace(`[[${num}]]`, ``);
  870. }
  871. historys.value[historyIndex].currentResponse.msg = historys.value[historyIndex].currentResponse.msg.replace(
  872. `[[${num}]]`,
  873. `<span onclick="window.openDocByIndex(${num}, ${id}, ${historyIndex})" class="poi" style=" cursor: pointer; display: inline-block; width: 20px; height: 20px; font-size: 12px; line-height: 20px; text-align: center; margin: 0 5px; border-radius: 10px;background: #d0d5dd">${num}</span>`
  874. );
  875. num = getNum(historys.value[historyIndex].currentResponse.msg);
  876. }
  877. }
  878. if (res.source_list && res.source_list.length && !historys.value[historyIndex].currentResponse.docs.length) {
  879. handleDocs(res.source_list);
  880. }
  881. } catch (e) {}
  882. }
  883. if (rData === '[DONE]') {
  884. console.log(historys.value[historyIndex].currentResponse.msg);
  885. }
  886. };
  887. const handleDocs = (docs) => {
  888. if (
  889. docs.length === 1 &&
  890. "<span style='color:red'>未找到相关文档,该回答为大模型自身能力解答!</span>" === docs[0]
  891. ) {
  892. return;
  893. }
  894. historys.value[historyIndex].currentResponse.docs = docs.map((v, i) => {
  895. if (activeTab.value === 'net') {
  896. return {
  897. index: v.num,
  898. doc: v.name,
  899. link: v.url,
  900. title: v.title,
  901. summary: v.summary,
  902. content: '',
  903. showContent: false,
  904. type: 'url'
  905. };
  906. }
  907. if (v.toLowerCase().indexOf('.pdf') > 0) {
  908. return {
  909. index: i++,
  910. doc: v.substring(v.toLowerCase().indexOf('] [') + 3, v.toLowerCase().indexOf('.pdf]') + 4),
  911. link: v.substring(v.toLowerCase().indexOf('.pdf]') + 6, v.toLowerCase().indexOf('.pdf)') + 4),
  912. content: v.substring(v.toLowerCase().indexOf('.pdf)') + 5),
  913. showContent: false,
  914. type: 'pdf'
  915. };
  916. } else if (v.toLowerCase().indexOf('.txt') > 0) {
  917. return {
  918. index: i++,
  919. doc: v.toLowerCase().substring(v.indexOf('] [') + 3, v.toLowerCase().indexOf('.txt]') + 4),
  920. link: v.toLowerCase().substring(v.indexOf('.txt]') + 6, v.toLowerCase().indexOf('.txt)') + 4),
  921. content: v.toLowerCase().substring(v.indexOf('.txt)') + 5),
  922. showContent: false,
  923. type: 'txt'
  924. };
  925. } else if (v.toLowerCase().indexOf('.docx') > 0) {
  926. return {
  927. index: i++,
  928. doc: v.substring(v.toLowerCase().indexOf('] [') + 3, v.toLowerCase().indexOf('.docx]') + 5),
  929. link: v.substring(v.toLowerCase().indexOf('.docx]') + 7, v.toLowerCase().indexOf('.docx)') + 5),
  930. content: v.substring(v.toLowerCase().indexOf('.docx)') + 6),
  931. showContent: false,
  932. type: 'docx'
  933. };
  934. }
  935. });
  936. };
  937. const openDoc = (doc, i) => {
  938. var link = doc.link;
  939. var type = doc.type;
  940. pdfSrc.value = window.formatDocUrl(link, modelType.value)
  941. showDoc.value = true;
  942. fileType.value = type;
  943. pdfContent.value = doc.content;
  944. pdfNum.value = i;
  945. };
  946. const closeDoc = () => {
  947. showDoc.value = false;
  948. };
  949. watch(
  950. () => showDoc.value,
  951. (newVal) => {
  952. // 发布打开关闭,在关闭文档的时候打开相关案例
  953. PubsubService.publish('switch-relevant-cases-box', newVal);
  954. }
  955. );
  956. const openUrl = (url) => {
  957. window.open(url, '_blank');
  958. };
  959. const openDocByIndex = (ind, id, hIndex = null) => {
  960. if (hIndex === null) {
  961. hIndex = historyIndex
  962. }
  963. showDoc.value = false;
  964. let link = null;
  965. if (id !== historys.value[hIndex].currentResponse.id) {
  966. if (questHistories.value[id].docs[ind - 1].type === 'url') {
  967. openUrl(questHistories.value[id].docs[ind - 1].link);
  968. return;
  969. }
  970. link = questHistories.value[id].docs[ind - 1].link;
  971. fileType.value = questHistories.value[id].docs[ind - 1].type;
  972. } else {
  973. if (historys.value[hIndex].currentResponse.docs[ind - 1].type === 'url') {
  974. openUrl(historys.value[hIndex].currentResponse.docs[ind - 1].link);
  975. return;
  976. }
  977. link = historys.value[hIndex].currentResponse.docs[ind - 1].link;
  978. fileType.value = historys.value[hIndex].currentResponse.docs[ind - 1].type;
  979. pdfContent.value = historys.value[hIndex].currentResponse.docs[ind - 1].content;
  980. pdfNum.value = historys.value[hIndex].currentResponse.docs[ind - 1].num;
  981. }
  982. pdfNum.value = ind;
  983. pdfSrc.value = window.formatDocUrl(link, modelType.value)
  984. showDoc.value = true;
  985. };
  986. window.openDocByIndex = openDocByIndex;
  987. const getNum = (str) => {
  988. const matches = str.match(/\[\[(\d+)\]\]/);
  989. if (matches) {
  990. return matches[1]; // 输出: 2
  991. } else {
  992. return null;
  993. }
  994. };
  995. const sessionId = ref('');
  996. const sessionCreate = () => {
  997. const sendData = {
  998. tittle: historys.value[historyIndex].currentResponse.question,
  999. creator: userStore.user.syUser.Id,
  1000. userName: userStore.user.syUser.Name,
  1001. deptName: userStore.user.syUser.OrganizationLine
  1002. }
  1003. ManagerAPI.create(sendData).then((res) => {
  1004. if (res.data) {
  1005. sessionId.value = res.data
  1006. }
  1007. });
  1008. }
  1009. const sessionDelete = (id) => {
  1010. const sendData = {
  1011. id
  1012. }
  1013. ManagerAPI.delete(sendData).then((res) => {
  1014. if (res.data) {
  1015. initSessionLists();
  1016. startNewSessionHandle()
  1017. }
  1018. });
  1019. }
  1020. const getSessionList = async (times) => {
  1021. const res = await ManagerAPI.list(userStore.user.syUser.Id, times);
  1022. return res.data
  1023. }
  1024. const sessionLists = reactive([
  1025. {
  1026. title: '今天',
  1027. times: [
  1028. dayjs().startOf('day').format('YYYY-MM-DD HH:mm:ss'),
  1029. dayjs().endOf('day').format('YYYY-MM-DD HH:mm:ss')
  1030. ],
  1031. datas: []
  1032. },
  1033. {
  1034. title: '昨天',
  1035. times: [
  1036. dayjs().subtract(1, 'day').startOf('day').format('YYYY-MM-DD HH:mm:ss'),
  1037. dayjs().subtract(1, 'day').endOf('day').format('YYYY-MM-DD HH:mm:ss')
  1038. ],
  1039. datas: []
  1040. },
  1041. {
  1042. title: '30天内',
  1043. times: [
  1044. dayjs().subtract(30, 'day').startOf('day').format('YYYY-MM-DD HH:mm:ss'),
  1045. dayjs().subtract(2, 'day').endOf('day').format('YYYY-MM-DD HH:mm:ss')
  1046. ],
  1047. datas: []
  1048. }
  1049. ])
  1050. const initSessionLists = () => {
  1051. sessionLists.forEach(async (session) => {
  1052. session['datas'] = await getSessionList(session['times'])
  1053. })
  1054. }
  1055. initSessionLists();
  1056. const getQuestionList = async (chatId) => {
  1057. const res = await ManagerAPI.getQuestionList({
  1058. chatId
  1059. });
  1060. return res.data
  1061. }
  1062. const switchSession = async (item) => {
  1063. cSessionId.value = sessionId.value = item['id']
  1064. isSessionNew.value = false;
  1065. const results = await getQuestionList(item['id'])
  1066. historys.value = []
  1067. results.forEach((item,index) => {
  1068. let nums = getNumAll(item['answer']);
  1069. let msg = item['answer'];
  1070. const id = questHistories.value.length;
  1071. nums.forEach(num => {
  1072. msg = msg.replace(
  1073. `[[${num}]]`,
  1074. ''
  1075. )
  1076. })
  1077. const docs = JSON.parse(item['answerSources']) || []
  1078. historys.value.push({
  1079. question: item['question'],
  1080. dsChecked: true,
  1081. sourceVisible: false,
  1082. currentResponse: {
  1083. id,
  1084. loading: false,
  1085. hintTxt: `已深度思考(用时 ${item['thinkTime']} 秒)`,
  1086. msg: msg,
  1087. sourceVisible: false,
  1088. showChain: true,
  1089. docs: []
  1090. }
  1091. })
  1092. activeIndex.value = 5
  1093. historyIndex = index;
  1094. // handleDocs(docs)
  1095. })
  1096. }
  1097. const onSessionDeleteHandle = (id) => {
  1098. Modal.confirm({
  1099. content: () => '确定删除该条记录,删除将无法恢复!',
  1100. onOk () {
  1101. sessionDelete(id);
  1102. },
  1103. cancelText: '取消',
  1104. okText: '确定',
  1105. });
  1106. }
  1107. // 埋点采集数据
  1108. const collectQuestion = () => {
  1109. const { question, originAnswer, oDocs, time, keywords = [] } = historys.value[historyIndex].currentResponse;
  1110. const param = {
  1111. question,
  1112. answer: originAnswer,
  1113. answerSources: oDocs,
  1114. thinkTime: time,
  1115. questionType: askType.value === 'zcfg' ? '政策法规' : '土地市场',
  1116. keywords: Array.isArray(keywords) ? keywords.join(',') : keywords,
  1117. creator: userStore.user.syUser.Id,
  1118. chatId: sessionId.value
  1119. };
  1120. if (userStore.isLogin) {
  1121. const { id = '-1', displayName = '游客' } = userStore?.user?.user || {};
  1122. param.user = displayName;
  1123. param.userId = id;
  1124. }
  1125. ManagerAPI.collect(param).then((res) => {
  1126. if (res.data) {
  1127. // 记录日志,用来反馈
  1128. sessionLists.forEach(async (session) => {
  1129. session['datas'] = await getSessionList(session['times'])
  1130. })
  1131. }
  1132. });
  1133. };
  1134. const stopAI = () => {
  1135. if (ctr) {
  1136. ctr.abort();
  1137. }
  1138. };
  1139. </script>
  1140. <style lang="scss" scoped>
  1141. @import './index.scss';
  1142. </style>