Browse Source

AI问答改版和功能优化

songxy 2 months ago
parent
commit
e44dcfed1f

+ 0 - 4
ais_search_zj/web/src/App.vue

@@ -132,10 +132,6 @@ const vectorCreate = (userName, userId) => {
 }
 getByUser();
 getUrlParamByToken()
-setTimeout(() => {
-  console.log('33333333333323424234--------------')
-  store.updateVectorId('23424234--------------')
-}, 2000)
 </script>
 
 <style lang="scss" scoped>

+ 5 - 0
ais_search_zj/web/src/assets/scss/common.scss

@@ -8,3 +8,8 @@
     background: #F7F9FC;
 
 }
+.tool_1 {
+  &.active {
+    color: #605BEC;
+  }
+}

+ 0 - 1
ais_search_zj/web/src/global.css

@@ -10,7 +10,6 @@ div.ant-popconfirm {
     font-weight: bolder !important;
     margin-top: 0.7em;
 }
-
 .vue-md-it-wrapper  > p {
     margin-bottom: 0.6em;
 }

+ 12 - 0
ais_search_zj/web/src/router/routes.js

@@ -17,6 +17,12 @@ export default [
             meta: { title: 'AI搜索' },
             component: () => import('@/views/home/home.vue'),
           },
+          {
+            path: 'home2',
+            name: 'Home2',
+            meta: { title: 'AI搜索2' },
+            component: () => import('@/views/ai-search/index.vue'),
+          },
           {
             path: 'newDocument',
             name: 'NewDocument',
@@ -35,6 +41,12 @@ export default [
             meta: { title: '智能找图' },
             component: () => import('@/views/znzt/index.vue'),
           },
+          {
+            path: 'ztzs',
+            name: 'Ztzs',
+            meta: { title: '找图找数' },
+            component: () => import('@/views/ztzs/index.vue'),
+          },
           {
             path: 'gdfx',
             name: 'Gdfx',

+ 2 - 2
ais_search_zj/web/src/views/ai-search/ai-search.vue

@@ -2,7 +2,7 @@
   <div class="ai-search-detail">
     <a-affix>
       <div class="header">
-        <home-header @login="emits('login')"  sub-title="自然资源大模型工具" />
+        <home-header @login="emits('login')"  sub-title="自然资源大模型" />
       </div>
     </a-affix>
     <div class="search-panel" id="pageContainer">
@@ -647,7 +647,7 @@ const streamToAnswer = () => {
 
 const tabs = [
   { key: 'knowledge', name: '知识库' },
-  { key: 'net', name: '全网' },
+  // { key: 'net', name: '全网' },
   { key: 'original', name: '原生' },
 ];
 

+ 1897 - 0
ais_search_zj/web/src/views/ai-search/index.vue

@@ -0,0 +1,1897 @@
+<template>
+  <div class="home_box">
+    <div class="left_box">
+      <button>
+        <span class="icon"></span>
+        <span>开启新对话</span>
+      </button>
+      <div class="history">
+        <div class="menu_down">
+          <span class="title">历史问答</span>
+          <span class="icon" @click="toggleCardHandle('history')">
+            <UpOutlined v-if="visibleMap['history']" />
+            <DownOutlined v-else />
+          </span>
+        </div>
+        <div class="history_box" v-show="visibleMap['history']">
+          <ul>
+            <li>
+              <span class="title">今天</span>
+              <ul>
+                <li>什么是闲置土地</li>
+                <li>什么是闲置土地</li>
+                <li>什么是闲置土地</li>
+              </ul>
+            </li>
+            <li>
+              <span class="title">昨天</span>
+              <ul>
+                <li>什么是闲置土地</li>
+                <li>什么是闲置土地</li>
+                <li>什么是闲置土地</li>
+              </ul>
+            </li>
+            <li>
+              <span class="title">30天内</span>
+              <ul>
+                <li>什么是闲置土地</li>
+                <li>什么是闲置土地</li>
+                <li>什么是闲置土地</li>
+              </ul>
+            </li>
+          </ul>
+        </div>
+      </div>
+      <div class="ai_tools">
+        <div class="menu_down">
+          <span class="title">AI工具</span>
+          <span class="icon" @click="toggleCardHandle('tool')">
+            <UpOutlined v-if="visibleMap['tool']" />
+            <DownOutlined v-else />
+          </span>
+        </div>
+        <ul v-show="visibleMap['tool']">
+          <li @click="toToolPage('./#/policy/interpret')">
+            <span class="icon">
+              <span class="iconfont icon-a-lujing8796"></span>
+            </span>
+            <span class="txt">政策解读</span>
+          </li>
+          <li @click="toToolPage('./#/policy/smart')">
+            <span class="icon">
+              <span class="iconfont icon-a-lujing8794"></span>
+            </span>
+            <span class="txt">政策比对</span>
+          </li>
+        </ul>
+      </div>
+    </div>
+    <div class="right_box">
+      <div class="chat-container">
+        <div class="messages-container" ref="msgContainer">
+          <template v-for="(history,index) in historys" :key="index">
+            <div class="message user">
+              {{ history.question }}
+            </div>
+            <div class="message assistant">
+              <div class="ai-search-detail">
+                <div class="search-panel" id="pageContainer">
+                  <div :class="`search-detail search-detail-${askType}`">
+                    <div
+                      class="search-result"
+                      id="searchResult"
+                      style="overflow-x: hidden; height: auto"
+                      ref="messageContainer"
+                    >
+
+                      <div :class="`result-panel result-panel-${askType}`">
+
+                        <div class="result">
+                          <template v-if="askType === 'zcfg'">
+                            <div class="result-view">
+                              <div class="q-r">
+                                <div class="ds-content-box">
+                                  <div class="icon">
+                                    <img src="/images/icon-ds.png" />
+                                  </div>
+                                  <div class="ds-panel">
+                                    <div class="ds-loading" v-if="history.currentResponse.hintTxt" @click="dsUp = !dsUp">
+                                      {{ history.currentResponse.hintTxt }}
+                                      <DownOutlined class="icon-arrow" :class="{ rotate: dsUp }" />
+                                    </div>
+                                    <div class="ds-con" v-if="!dsUp">
+                                      <vue-markdown-it
+                                        id="dsMarkdown"
+                                        :source="
+                                          history.currentResponse.streamMock
+                                            ? history.currentResponse.streamMsg.indexOf('###') > -1
+                                              ? history.currentResponse.streamMsg.substring(
+                                                  0,
+                                                  history.currentResponse.streamMsg.indexOf('###')
+                                                )
+                                              : history.currentResponse.streamMsg
+                                            : history.currentResponse.msg.indexOf('###') > -1
+                                            ? history.currentResponse.msg.substring(
+                                                0,
+                                                history.currentResponse.msg.indexOf('###')
+                                              )
+                                            : history.currentResponse.msg
+                                        "
+                                        :options="{
+                                          html: true,
+                                          linkify: true
+                                        }"
+                                      />
+                                    </div>
+                                    <a-spin :indicator="indicator" v-if="history.currentResponse.loading" />
+                                  </div>
+                                </div>
+                                <vue-markdown-it
+                                  id="resMarkdown"
+                                  :source="
+                                    history.currentResponse.streamMock
+                                      ? history.currentResponse.streamMsg.indexOf('###') > -1
+                                        ? history.currentResponse.streamMsg.substring(
+                                            history.currentResponse.streamMsg.indexOf('###')
+                                          )
+                                        : ''
+                                      : history.dsChecked
+                                      ? history.currentResponse.msg.indexOf('###') > -1
+                                        ? history.currentResponse.msg.substring(history.currentResponse.msg.indexOf('###'))
+                                        : ''
+                                      : history.currentResponse.msg
+                                  "
+                                  :options="{
+                                    html: true,
+                                    linkify: true
+                                  }"
+                                />
+                              </div>
+                              <div class="more-questions" v-if="activeIndex >= 4">
+                                <div class="title" style="margin-bottom: 14px">
+                                  你可以继续问我:
+                                  <div class="change-title" @click="changeRecommendedQuestions">
+                                    <div class="change-icon"></div>
+                                    换一批
+                                  </div>
+                                </div>
+                                <div class="questions" style="cursor: pointer">
+                                  <div
+                                    class="question"
+                                    @click="openRecommendedQuestion(q)"
+                                    v-for="(q, i) in questions"
+                                    :key="i"
+                                    style="
+                                      line-height: 14px;
+                                      text-align: left;
+                                      font-style: normal;
+                                      text-transform: none;
+                                      width: fit-content;
+                                      margin-bottom: 12px;
+                                      background: #ffffff;
+                                      box-shadow: 0px 1px 8px 0px #e4e4e4;
+                                      border-radius: 10px;
+                                      border: 1px solid #e9e9e9;
+                                      padding: 16px;
+                                      font-family: PingFang SC;
+                                      font-weight: 500;
+                                      font-size: 14px;
+                                      color: #2185f2;
+                                    "
+                                  >
+                                    {{ q }}
+                                  </div>
+                                </div>
+                              </div>
+                              <div class="source" v-if="activeTab !== 'original'" v-show="activeIndex === 5">
+                                <div class="title">基于{{ history.currentResponse.docs.length }}个参考来源</div>
+                                <div class="items">
+                                  <div
+                                    class="item"
+                                    v-if="activeTab !== 'net'"
+                                    v-for="(doc, i) in history.currentResponse.docs"
+                                    :key="'doc-' + i"
+                                  >
+                                    <div class="doc">
+                                      <p>
+                                        <span
+                                          class="ma"
+                                          style="font-weight: bolder; color: #000000; margin-right: 10px"
+                                          >出处 [{{ i + 1 }}] </span
+                                        ><span class="doc-link" @click="openDoc(doc, i)">{{ doc.doc }}</span>
+                                      </p>
+                                      <div
+                                        :class="`doc-icon${
+                                          !doc.showContent ? ' doc-icon-show' : ' doc-icon-hide'
+                                        }`"
+                                        @click="doc.showContent = !doc.showContent"
+                                      ></div>
+                                    </div>
+
+                                    <div :class="`content${doc.showContent ? '' : ' content-hide'}`">
+                                      <p>{{ doc.content }}</p>
+                                    </div>
+                                  </div>
+                                  <div
+                                    class="item item-url"
+                                    v-if="activeTab === 'net'"
+                                    v-for="(doc, i) in history.currentResponse.docs"
+                                    :key="'doc-' + i"
+                                  >
+                                    <div class="doc">
+                                      <p>
+                                        <span class="doc-link" @click="openUrl(doc.link)">{{ doc.title }}</span>
+                                      </p>
+                                    </div>
+                                    <div class="bottom">
+                                      <div class="title-icon">
+                                        <div class="icon"></div>
+                                        <div class="title">{{ doc.doc }}</div>
+                                      </div>
+                                      <div class="index">{{ i + 1 }}</div>
+                                    </div>
+                                  </div>
+                                </div>
+                              </div>
+                            </div>
+                          </template>
+                          <template v-else>
+                            <div v-if="activeIndex >= 1" class="map-answer">
+                              <div class="left-panel">
+                                <div class="content">
+                                  <div class="summary-card">
+                                    <div class="summary-title" id="tdscSummaryTitle">总结</div>
+                                    <div class="summary-content">
+                                      <a-skeleton active v-if="!history.currentResponse.msg" />
+                                      <vue-markdown-it
+                                        :source="
+                                          history.currentResponse.streamMock
+                                            ? history.currentResponse.streamMsg
+                                            : history.currentResponse.msg
+                                        "
+                                        :options="{
+                                          html: true,
+                                          linkify: true
+                                        }"
+                                      />
+                                    </div>
+                                  </div>
+                                  <div class="chart-card" v-if="history.currentResponse.hasChart" id="tdscChartCard">
+                                    <div class="chart-title">生成图表</div>
+                                    <a-skeleton
+                                      active
+                                      style="height: 100%"
+                                      v-if="!history.currentResponse.chartOption"
+                                    />
+                                    <div v-else class="chart" id="summaryChart"></div>
+                                  </div>
+                                </div>
+                              </div>
+                            </div>
+                          </template>
+                        </div>
+                      </div>
+                    </div>
+                    <!--   历史回答     -->
+                    <div
+                      v-show="askType === 'zcfg'"
+                      class="search-result result-history"
+                      style="overflow-y: hidden; height: auto"
+                      v-if="questHistories.length > 0"
+                      id="messageContainer"
+                    >
+                      <template v-for="(qh, i) in [...questHistories].reverse()" :key="`qh-${i}`">
+                        <div class="top">
+                          <div class="title">
+                            <div class="icon"></div>
+                            <div class="question" style="cursor: pointer">{{ qh.question }}</div>
+                          </div>
+                        </div>
+
+                        <div class="result-panel">
+                          <div class="result">
+                            <div class="tabs">
+                              <div :class="`tab tab-active`">
+                                <div class="title">回答</div>
+                                <div class="bottom"></div>
+                              </div>
+                            </div>
+                          </div>
+                          <div class="result-view">
+                            <div class="q-r">
+                              <vue-markdown-it
+                                :source="qh.msg"
+                                :toc="true"
+                                :options="{
+                                  html: true,
+                                  linkify: true
+                                }"
+                              />
+                            </div>
+                          </div>
+                        </div>
+                      </template>
+                    </div>
+                  </div>
+                  <a-affix v-if="showDoc" style="width: 52%; font-size: 16px" :offset-top="210">
+                    <div
+                      class="doc"
+                      style="width: 100%; background-color: white; height: calc(100vh - 190px); overflow: auto"
+                    >
+                      <p-d-f-viewer
+                        v-if="fileType === 'pdf'"
+                        :src="pdfSrc"
+                        @close="closeDoc"
+                        :content="pdfContent"
+                        :num="pdfNum"
+                      />
+                      <word-viewer
+                        v-if="fileType === 'docx'"
+                        :src="pdfSrc"
+                        @close="closeDoc"
+                        :content="pdfContent"
+                        :num="pdfNum"
+                      >
+                      </word-viewer>
+                      <txt-viewer v-if="fileType === 'txt'" :src="pdfSrc" @close="closeDoc" :txt="pdfContent" />
+                    </div>
+                  </a-affix>
+                </div>
+              </div>
+            </div>
+          </template>
+        </div>
+        
+        <div class="input-container">
+            <textarea 
+              v-model="cQuestion"
+              class="input-box" 
+              ref="msgInput" 
+              placeholder="请输入对话内容,换行请使用Shift+Enter" 
+              rows="5"
+              @keydown="onKeydownHandle"
+            ></textarea>
+            <div class="bottom_box">
+              <div class="tools_box">
+                <div class="tool_box_1">
+                  <a-dropdown>
+                    <template #overlay>
+                      <a-menu @click="({key})=>onChange(key)">
+                        <a-menu-item key="1">
+                          <div :class="{tool_1: true, active: modelType === '1'}">
+                            DeepSeek
+                          </div>
+                        </a-menu-item>
+                        <a-menu-item key="0">
+                          <div :class="{tool_1: true, active: modelType === '0'}">
+                            通义千问
+                          </div>
+                        </a-menu-item>
+                      </a-menu>
+                    </template>
+                    <a-button style="width: 120px">
+                      {{ modelType === '1' ? 'DeepSeek' : '通义千问'}}
+                      <DownOutlined />
+                    </a-button>
+                  </a-dropdown>
+                </div>
+                <div class="tool_box_2">
+                  <a-dropdown>
+                    <template #overlay>
+                      <a-menu @click="changeAnswerType">
+                        <a-menu-item key="0">
+                          <div :class="{tool_1: true, active: answerType === '0'}">
+                            简洁
+                          </div>
+                        </a-menu-item>
+                        <a-menu-item key="1">
+                          <div :class="{tool_1: true, active: answerType === '1'}">
+                            深入
+                          </div>
+                        </a-menu-item>
+                        <a-menu-item key="2">
+                          <div :class="{tool_1: true, active: answerType === '2'}">
+                            研究
+                          </div>
+                        </a-menu-item>
+                      </a-menu>
+                    </template>
+                    <a-button style="width: 80px">
+                      {{ answerType === '0' ? '简洁' : answerType === '1' ? '深入' : '研究'}}
+                      <DownOutlined />
+                    </a-button>
+                  </a-dropdown>
+                </div>
+                <div class="tool_box_3">
+                  <a-dropdown>
+                    <template #overlay>
+                      <a-menu @click="({key})=>changeTab(key)">
+                        <a-menu-item 
+                          v-for="t in tabs"
+                          :key="t.key"
+                        >
+                          <div :class="{tool_1: true, active: activeTab === t.key}">
+                            {{t.name}}
+                          </div>
+                        </a-menu-item>
+                      </a-menu>
+                    </template>
+                    <a-button style="width: 90px">
+                      {{ activeTab === 'knowledge' ? '知识库' : activeTab === 'net' ? '全网' : '原生'}}
+                      <DownOutlined />
+                    </a-button>
+                  </a-dropdown>
+                </div>
+              </div>
+              <div class="send_btn" @click="onSendHandle">
+                <i class="iconfont icon-a-lujing9250"></i>
+              </div>
+            </div>
+        </div>
+    </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import {
+  LoadingOutlined,
+  UpOutlined,
+  DownOutlined
+} from "@ant-design/icons-vue";
+import { fetchEventSource } from '@microsoft/fetch-event-source';
+import PDFViewer from '@/components/pdf/PdfCanvas.vue';
+import WordViewer from '@/components/pdf/WordViewer.vue';
+import AiTextarea from '@/components/TextArea/AiTextarea.vue';
+import { VueMarkdownIt } from '@f3ve/vue-markdown-it';
+import TxtViewer from '@/components/pdf/TxtViewer.vue';
+import { message } from 'ant-design-vue';
+import { h, ref, reactive, defineProps, watch } from 'vue';
+import CommonAPI from '@/api/common';
+import ManagerAPI from '@/api/manager';
+import PubsubService from '@/utils/PubsubService';
+import { useUserStore } from '@/stores';
+
+const visibleMap = reactive({
+  history: true,
+  tool: true
+})
+const toggleCardHandle = (type) => {
+  visibleMap[type] = !visibleMap[type]
+}
+const msgInput = ref(null)
+const msgContainer = ref(null)
+
+function scrollToBottom() {
+  msgContainer.value.scrollTop = msgContainer.value.scrollHeight;
+}
+
+const historys = ref([])
+let historyIndex = -1;
+const cQuestion = ref('')
+const onKeydownHandle = (e) => {
+    if (e.key === 'Enter' && !e.shiftKey) {
+      e.preventDefault();
+      onSendHandle();
+    }
+}
+
+const onSendHandle = () => {
+  if (historys.value.length > 0 && historys.value[historyIndex].currentResponse.loading) {
+    message.error('回答输出中,请稍后操作');
+    return;
+  }
+  if (!cQuestion.value) {
+    message.error('请输入问题');
+    return;
+  }
+  historyIndex++;
+  historys.value.push({
+    question: cQuestion.value,
+    dsChecked: true,
+    currentResponse: {
+      loading: true,
+      hintTxt: '',
+      streamMsg: '',
+      streamMock: false,
+      msg: '',
+      originAnswer: '',
+      docs: []
+    }
+  })
+  ask(decodeURIComponent(cQuestion.value));
+  cQuestion.value = '';
+}
+
+const toToolPage = (path) => {
+  window.open(path, '_blank')
+}
+
+const aiLoading = ref(false);
+const router = useRouter();
+const indicator = h(LoadingOutlined, {
+  style: {
+    fontSize: '24px'
+  },
+  spin: true
+});
+const props = defineProps({
+  searchType: '',
+  askType: {
+    type: String,
+    default: 'zcfg'
+  }
+});
+
+const modelType = ref('1')
+const answerType = ref('0');
+const scope = ref('net');
+
+watch(props.searchType, (val) => {
+  activeTab.value = val;
+});
+
+const question = ref('国有土地的使用方式有哪些?');
+const statusText = ref('检索中');
+const activeIndex = ref(0);
+const activeTab = ref(props.searchType);
+let ctr = null;
+
+const evaluate = ref(null);
+
+const open = ref(true);
+const showDoc = ref(false);
+const pdfSrc = ref('');
+const pdfContent = ref('');
+const pdfNum = ref(1);
+const fileType = ref('pdf');
+const startTime = ref(0);
+const endTime = ref(0);
+const dsUp = ref(false);
+const times = ref(0);
+const timers = ref([]);
+let streamMockInterval = null;
+const streamToAnswer = () => {
+  console.log('streamToAnswer');
+  historys.value[historyIndex].currentResponse.index = 0;
+  streamMockInterval = setInterval(() => {
+    const { originAnswer = '', msg, streamMsg = '', id, index = 0 } = historys.value[historyIndex].currentResponse;
+
+    if (historys.value[historyIndex].currentResponse.mockStart || index <= originAnswer.length) {
+      historys.value[historyIndex].currentResponse.streamMsg = originAnswer.substr(0, index + 2).replaceAll('\n', '  \n');
+      if (originAnswer) {
+        historys.value[historyIndex].currentResponse.index += 2;
+      }
+      let num = getNum(historys.value[historyIndex].currentResponse.streamMsg);
+
+      while (num) {
+        const docsNum = historys.value[historyIndex].currentResponse.docs.length;
+        historys.value[historyIndex].currentResponse.streamMsg = historys.value[historyIndex].currentResponse.streamMsg.replace(
+          `[[${num}]]`,
+          `<span onclick="window.openDocByIndex(${num}, ${id})" 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;
+    height: 20px;
+    background: #FFFFFF;
+    border-radius: 4px 4px 4px 4px;
+    border: 1px solid #BACAE3;">${num}</span>`
+        );
+
+        num = getNum(historys.value[historyIndex].currentResponse.streamMsg);
+      }
+    } else {
+      if (streamMockInterval) {
+        clearInterval(streamMockInterval);
+        streamMockInterval = null;
+      }
+
+      activeIndex.value = 5;
+
+      console.log('mock 结束');
+    }
+  }, 50);
+};
+
+const tabs = [
+  { key: 'knowledge', name: '知识库' },
+  // { key: 'net', name: '全网' },
+  { key: 'original', name: '原生' },
+];
+
+
+const changeStatusText = () => {
+  let i = 0;
+  setInterval(() => {
+    statusText.value = '检索中' + '.'.repeat(i);
+
+    if (i === 3) {
+      i = 0;
+    }
+    i++;
+  }, 500);
+};
+changeStatusText();
+
+const askType = ref('zcfg');
+const ask = async (q, isFllow) => {
+  if (ctr) {
+    if (streamMockInterval) {
+      clearInterval(streamMockInterval);
+      streamMockInterval = null;
+    }
+    ctr.abort();
+  }
+  times.value = 0;
+  if (timers.value) {
+    timers.value.forEach((t) => {
+      clearTimeout(t);
+      t = null;
+    });
+  }
+  timers.value = [];
+  if (historys.value[historyIndex].dsChecked) {
+    historys.value[historyIndex].currentResponse.hintTxt = '';
+    historys.value[historyIndex].currentResponse.loading = true;
+  }
+  question.value = q;
+  open.value = false;
+  showDoc.value = false;
+  askType.value = 'zcfg';
+  // activeTab.value = 'knowledge';
+  quest(isFllow);
+};
+
+let questionUrl = '/chat/kb_chat';
+
+const changeTab = (tab) => {
+  times.value = 0;
+  if (timers.value) {
+    timers.value.forEach((t) => {
+      clearTimeout(t);
+      t = null;
+    });
+  }
+  timers.value = [];
+  if (tab === 'net') {
+    questionUrl = '/chat/bing_chat';
+  } else if(tab === 'knowledge'){
+    questionUrl = '/chat/kb_chat';
+  }else if(tab === 'original') {
+    questionUrl = '/chat/chat';
+  }
+
+  activeTab.value = tab;
+};
+const onChange = (type) => {
+  modelType.value = type;
+  dsChange(type);
+  changeAnswerType({key: '0'});
+};
+//ds绑定用户改变
+const dsChange = (type) => {
+  localStorage.setItem("_isDeepSeek", type);
+  if (timers.value) {
+    timers.value.forEach((t) => {
+      clearTimeout(t);
+      t = null;
+    });
+    timers.value = [];
+  }
+  times.value = 0;
+  //打字机效果 切换会打印
+  aiLoading.value = true;
+};
+const changeAnswerType = ({ key }) => {
+  answerType.value = key
+};
+
+const questHistories = ref([]);
+
+let scb = null;
+const quest = async (isFllow) => {
+  startTime.value = Date.now();
+  window.scroll({ top: 0, behavior: 'smooth' });
+  question.value = (isFllow ? '追问: ' : '') + question.value;
+  evaluate.value = null;
+  activeIndex.value = 0;
+
+  if (!isFllow) {
+    showDoc.value = false;
+  }
+  if (isFllow) {
+    const { id, question, msg, docs, originAnswer, keywords = [] } = historys.value[historyIndex].currentResponse;
+    questHistories.value.push({ id, question, msg, docs: docs, originAnswer, keywords });
+  } else {
+    questHistories.value = [];
+  }
+  aiLoading.value = true;
+  if (scb !== null) {
+    clearInterval(scb);
+    scb = null;
+  } else {
+    scb = setInterval(() => {
+    }, 500);
+  }
+  ctr = new AbortController();
+
+  const id = questHistories.value.length;
+  activeIndex.value = 0;
+  getQuestionKeyWords();
+  if (activeTab.value === 'net') {
+    questionUrl = '/chat/bing_chat';
+  } else if(activeTab.value === 'knowledge'){
+    questionUrl = '/chat/kb_chat';
+  }else if(activeTab.value === 'original') {
+    questionUrl = '/chat/chat';
+  }
+  const topKs = window?.AppGlobalConfig?.topKs || {
+    0: 5,
+    1: 10,
+    2: 15
+  };
+  let body = null
+  if (activeTab.value === 'net') {
+    body = {
+      query: question.value,
+      stream: true,
+      model: modelType.value === '1' ? 'deepseek-r1' : '',
+      search_type: answerType.value
+    }
+  } else if(activeTab.value === 'knowledge'){
+    body = {
+      query: question.value,
+      mode: 'local_kb',
+      kb_name: activeTab.value === 'paper' ? 'compose_paper_material_total' : modelType.value === '1' ? window?.AppGlobalConfig?.llm?.kb_name : 'policy',
+      top_k: topKs[answerType.value],
+      search_type: answerType.value,
+      score_threshold: 0.5,
+      model: historys.value[historyIndex].dsChecked ? 'deepseek-r1' : '',
+      history: isFllow ? getFlowHistory() : [],
+      stream: true,
+      prompt_name: 'rag_context_qa.md',
+      return_direct: false
+    }
+  } else if (activeTab.value === 'original') {
+    body = {
+      query: question.value,
+      stream: true
+    }
+  }
+  if (activeTab.value === 'knowledge' && isFllow) {
+    if (questHistories.value.length > 0) {
+      body.history_keyword = questHistories.value[questHistories.value.length - 1].keywords;
+    }
+  }
+  if (activeTab.value !== 'net' && answerType.value !== '0') {
+    historys.value[historyIndex].currentResponse.streamMock = true;
+    historys.value[historyIndex].currentResponse.mockStart = true;
+    streamToAnswer();
+  }
+  const rootUrl = modelType.value === '1' ? window.AppGlobalConfig.knowledgeServer : window.AppGlobalConfig.aiServer
+  await fetchEventSource(rootUrl + questionUrl, {
+    method: 'POST',
+    openWhenHidden: true,
+    timeout: 300000,
+    headers: {
+      'Content-Type': 'application/json'
+    },
+
+    body: JSON.stringify(body),
+    signal: ctr.signal,
+    async onmessage(msg) {
+      if (activeIndex.value !== 3) {
+        activeIndex.value = 3;
+      }
+
+      activeTab.value === 'net' ? handleNetResponse(msg, id) : handleKnowledgeResponse(msg, id);
+    },
+    onclose() {
+      if (scb !== null) {
+        clearInterval(scb);
+        scb = null;
+
+        collectQuestion();
+      }
+
+      if (historys.value[historyIndex].currentResponse.streamMock) {
+        historys.value[historyIndex].currentResponse.mockStart = false;
+      } else {
+        activeIndex.value = 5;
+      }
+    },
+    onerror(err) {
+      throw err;
+    }
+  });
+};
+
+const getFlowHistory = () => {
+  const lastHistory = [...questHistories.value].splice(-1);
+  const parm = [];
+  lastHistory.forEach((item) => {
+    parm.push({ role: 'user', content: item.question });
+  });
+
+  return parm;
+};
+
+const handleKnowledgeResponse = (msg, id) => {
+  if (!msg || !msg.data) {
+    return;
+  }
+  const rData = JSON.parse(msg.data);
+  if (rData?.choices && rData.choices.length > 0) {
+    if (activeTab.value === 'net' && rData.status !== 2) {
+      return;
+    }
+    if (rData.status == 3) {
+      if (timers.value) {
+        timers.value.forEach((t) => {
+          clearTimeout(t);
+          t = null;
+        });
+        timers.value = [];
+      }
+        endTime.value = Date.now();
+        var time = ((endTime.value - startTime.value) / 1000).toFixed(0);
+        historys.value[historyIndex].currentResponse.hintTxt = `已深度思考(用时 ${time} 秒)`;
+        historys.value[historyIndex].currentResponse.loading = false;
+        aiLoading.value=true;
+        historys.value[historyIndex].currentResponse.originAnswer = rData.choices[0]?.delta?.content.replaceAll(
+          '\n',
+          `  \n`
+        );
+        historys.value[historyIndex].currentResponse.msg = rData.choices[0]?.delta?.content.replaceAll('\n', `  \n`);
+        let num = getNum(historys.value[historyIndex].currentResponse.msg);
+        while (num) {
+          const docsNum = historys.value[historyIndex].currentResponse.docs.length;
+          if (docsNum && num > docsNum + 1) {
+          }
+
+          historys.value[historyIndex].currentResponse.msg = historys.value[historyIndex].currentResponse.msg.replace(
+            `[[${num}]]`,
+            `<span onclick="window.openDocByIndex(${num}, ${id})" 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;
+    height: 20px;
+    background: #FFFFFF;
+    border-radius: 4px 4px 4px 4px;
+    border: 1px solid #BACAE3;">${num}</span>`
+          );
+
+          num = getNum(historys.value[historyIndex].currentResponse.msg);
+        }
+    } else {
+      
+      aiLoading.value = false;
+      historys.value[historyIndex].currentResponse.hintTxt = '思考中...';
+      //ds模式打字机效果输出
+      const timer = setTimeout(() => {
+        if (!aiLoading.value) {
+          historys.value[historyIndex].currentResponse.originAnswer += rData.choices[0]?.delta?.content.replaceAll(
+            '\n',
+            `  \n`
+          );
+          historys.value[historyIndex].currentResponse.msg += rData.choices[0]?.delta?.content.replaceAll('\n', `  \n`);
+          let num = getNum(historys.value[historyIndex].currentResponse.msg);
+          while (num) {
+            const docsNum = historys.value[historyIndex].currentResponse.docs.length;
+            if (docsNum && num > docsNum + 1) {
+            }
+
+            historys.value[historyIndex].currentResponse.msg = historys.value[historyIndex].currentResponse.msg.replace(
+              `[[${num}]]`,
+              `<span onclick="window.openDocByIndex(${num}, ${id})" 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;
+  height: 20px;
+  background: #FFFFFF;
+  border-radius: 4px 4px 4px 4px;
+  border: 1px solid #BACAE3;">${num}</span>`
+            );
+
+            num = getNum(historys.value[historyIndex].currentResponse.msg);
+          }
+        }
+        clearTimeout(timer);
+        timers.value.push(timer);
+      }, times.value * 15);
+    }
+  }
+
+  if (!historys.value[historyIndex].currentResponse.docs.length) {
+    if (rData.docs && rData.docs.length) {
+      handleDocs(rData.docs);
+    }
+  }
+  times.value++;
+  setTimeout(() => {
+    scrollToBottom()
+  }, 50)
+};
+
+const handleNetResponse = (msg, id) => {
+  const rData = msg.data;
+  if (!!rData && rData !== '[DONE]') {
+    try {
+      const res = JSON.parse(rData);
+      if (res.error) {
+        activeIndex.value = 4;
+        message.error(res.error);
+        historys.value[historyIndex].currentResponse.msg = res.error;
+
+        return;
+      }
+      if (res.rag_finish) {
+        if (activeIndex.value < 4) {
+          activeIndex.value = 4;
+        }
+        if (historys.value[historyIndex].dsChecked) {
+          endTime.value = Date.now();
+          var time = ((endTime.value - startTime.value) / 1000).toFixed(0);
+          historys.value[historyIndex].currentResponse.hintTxt = `已深度思考(用时 ${time} 秒)`;
+          historys.value[historyIndex].currentResponse.loading = false;
+        }
+      } else {
+        if (historys.value[historyIndex].dsChecked && historys.value[historyIndex].currentResponse.hintTxt != '思考中...') {
+          historys.value[historyIndex].currentResponse.hintTxt = '思考中...';
+        }
+      }
+      if (res.result) {
+        historys.value[historyIndex].currentResponse.originAnswer = res.result;
+        historys.value[historyIndex].currentResponse.msg = res.result.replaceAll('\n', `  \n`);
+
+        let num = getNum(historys.value[historyIndex].currentResponse.msg);
+
+        while (num) {
+          const docsNum = historys.value[historyIndex].currentResponse.docs.length;
+          if (docsNum && num > docsNum + 1) {
+            historys.value[historyIndex].currentResponse.msg = historys.value[historyIndex].currentResponse.msg.replace(`[[${num}]]`, ``);
+          }
+          historys.value[historyIndex].currentResponse.msg = historys.value[historyIndex].currentResponse.msg.replace(
+            `[[${num}]]`,
+            `<span onclick="window.openDocByIndex(${num}, ${id})" 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>`
+          );
+
+          num = getNum(historys.value[historyIndex].currentResponse.msg);
+        }
+      }
+      if (res.source_list && res.source_list.length && !historys.value[historyIndex].currentResponse.docs.length) {
+        handleDocs(res.source_list);
+      }
+    } catch (e) {}
+  }
+
+  if (rData === '[DONE]') {
+    console.log(historys.value[historyIndex].currentResponse.msg);
+  }
+};
+
+const handleDocs = (docs) => {
+  if (
+    docs.length === 1 &&
+    "<span style='color:red'>未找到相关文档,该回答为大模型自身能力解答!</span>" === docs[0]
+  ) {
+    return;
+  }
+
+  historys.value[historyIndex].currentResponse.docs = docs.map((v, i) => {
+    if (activeTab.value === 'net') {
+      return {
+        index: v.num,
+        doc: v.name,
+        link: v.url,
+        title: v.title,
+        summary: v.summary,
+        content: '',
+        showContent: false,
+        type: 'url'
+      };
+    }
+
+    if (v.toLowerCase().indexOf('.pdf') > 0) {
+      return {
+        index: i++,
+        doc: v.substring(v.toLowerCase().indexOf('] [') + 3, v.toLowerCase().indexOf('.pdf]') + 4),
+        link: v.substring(v.toLowerCase().indexOf('.pdf]') + 6, v.toLowerCase().indexOf('.pdf)') + 4),
+        content: v.substring(v.toLowerCase().indexOf('.pdf)') + 5),
+        showContent: false,
+        type: 'pdf'
+      };
+    } else if (v.toLowerCase().indexOf('.txt') > 0) {
+      return {
+        index: i++,
+        doc: v.toLowerCase().substring(v.indexOf('] [') + 3, v.toLowerCase().indexOf('.txt]') + 4),
+        link: v.toLowerCase().substring(v.indexOf('.txt]') + 6, v.toLowerCase().indexOf('.txt)') + 4),
+        content: v.toLowerCase().substring(v.indexOf('.txt)') + 5),
+        showContent: false,
+        type: 'txt'
+      };
+    } else if (v.toLowerCase().indexOf('.docx') > 0) {
+      return {
+        index: i++,
+        doc: v.substring(v.toLowerCase().indexOf('] [') + 3, v.toLowerCase().indexOf('.docx]') + 5),
+        link: v.substring(v.toLowerCase().indexOf('.docx]') + 7, v.toLowerCase().indexOf('.docx)') + 5),
+        content: v.substring(v.toLowerCase().indexOf('.docx)') + 6),
+        showContent: false,
+        type: 'docx'
+      };
+    }
+  });
+};
+
+const openDoc = (doc, i) => {
+  if (window.AppGlobalConfig.isDisabledSource) return;
+  var link = doc.link;
+  var type = doc.type;
+  pdfSrc.value = link.replace(
+    window.AppGlobalConfig.knowledgeDocUrl.replace(
+      '=policy&',
+      activeTab.value === 'paper' ? '=compose_paper_material_total&' : '=policy&'
+    ),
+    window.AppGlobalConfig.knowledgeDocUrlProxy.replace(
+      '=policy&',
+      activeTab.value === 'paper' ? '=compose_paper_material_total&' : '=policy&'
+    )
+  );
+  showDoc.value = true;
+  fileType.value = type;
+  pdfContent.value = doc.content;
+  pdfNum.value = i;
+};
+
+const closeDoc = () => {
+  showDoc.value = false;
+};
+
+watch(
+  () => showDoc.value,
+  (newVal) => {
+    // 发布打开关闭,在关闭文档的时候打开相关案例
+    PubsubService.publish('switch-relevant-cases-box', newVal);
+  }
+);
+
+const openUrl = (url) => {
+  window.open(url, '_blank');
+};
+
+const openDocByIndex = (ind, id) => {
+  showDoc.value = false;
+  let link = null;
+  if (id !== historys.value[historyIndex].currentResponse.id) {
+    if (questHistories.value[id].docs[ind - 1].type === 'url') {
+      openUrl(questHistories.value[id].docs[ind - 1].link);
+      return;
+    }
+
+    link = questHistories.value[id].docs[ind - 1].link;
+    fileType.value = questHistories.value[id].docs[ind - 1].type;
+  } else {
+    if (historys.value[historyIndex].currentResponse.docs[ind - 1].type === 'url') {
+      openUrl(historys.value[historyIndex].currentResponse.docs[ind - 1].link);
+      return;
+    }
+    link = historys.value[historyIndex].currentResponse.docs[ind - 1].link;
+    fileType.value = historys.value[historyIndex].currentResponse.docs[ind - 1].type;
+    pdfContent.value = historys.value[historyIndex].currentResponse.docs[ind - 1].content;
+    pdfNum.value = historys.value[historyIndex].currentResponse.docs[ind - 1].num;
+  }
+  pdfNum.value = ind;
+  pdfSrc.value = link.replace(
+    window.AppGlobalConfig.knowledgeDocUrl.replace(
+      '=policy&',
+      activeTab.value === 'paper' ? '=compose_paper_material_total&' : '=policy&'
+    ),
+    window.AppGlobalConfig.knowledgeDocUrlProxy.replace(
+      '=policy&',
+      activeTab.value === 'paper' ? '=compose_paper_material_total&' : '=policy&'
+    )
+  );
+  showDoc.value = true;
+};
+window.openDocByIndex = openDocByIndex;
+const getNum = (str) => {
+  const matches = str.match(/\[\[(\d+)\]\]/);
+
+  if (matches) {
+    return matches[1]; // 输出: 2
+  } else {
+    return null;
+  }
+};
+
+// 埋点采集数据
+const collectQuestion = () => {
+  const { question, originAnswer, keywords = [] } = historys.value[historyIndex].currentResponse;
+  const userStore = useUserStore();
+  const param = {
+    question,
+    answer: originAnswer,
+    questionType: askType.value === 'zcfg' ? '政策法规' : '土地市场',
+    keywords: Array.isArray(keywords) ? keywords.join(',') : keywords,
+    user: '游客',
+    userId: '-1'
+  };
+  if (userStore.isLogin) {
+    const { id = '-1', displayName = '游客' } = userStore?.user?.user || {};
+    param.user = displayName;
+    param.userId = id;
+  }
+
+  ManagerAPI.collect(param).then((res) => {
+    if (res.data) {
+      // 记录日志,用来反馈
+      historys.value[historyIndex].currentResponse.logId = res.data;
+    }
+  });
+};
+
+const questions = ref([]);
+let recommendedQuestions = [];
+let recommendedQuestionsIndex = 0;
+const getRecommendedQuestion = async (b = false) => {
+  const { question } = historys.value[historyIndex].currentResponse;
+  const myHeaders = new Headers();
+  myHeaders.append('Content-Type', 'application/json');
+  questions.value = [];
+  if (!b) {
+    recommendedQuestions = [];
+  }
+  recommendedQuestionsIndex = 0;
+  const raw = JSON.stringify({
+    query: `你会收到一个用户提问,请根据问题延伸出10个子问题。
+
+在你回答问题的时候,还需要注意推荐给用户的问题必须以列表的形式返回。
+for example:
+[
+    "1、南京市在推进产业用地高质量利用方面采取了哪些具体措施?这些措施的效果如何?",
+    "2、杭州市创新型产业用地管理的具体实施细则是什么?这些细则如何促进传统产业转型升级?",
+    "3、南京市和杭州市在土地供应方式上有哪些不同?这些不同如何影响各自的产业发展?",
+    "4、南京市如何通过政策支持和激励措施吸引重大投资项目?这些措施的实际效果如何?",
+    "5、杭州市“工业上楼”项目的实施情况如何?这一政策对提升土地利用效率有何影响?",
+]
+
+以下是用户提问:${question}`,
+    // model: 'qwen1.5-chat',
+    stream: false
+  });
+
+  const requestOptions = {
+    method: 'POST',
+    headers: myHeaders,
+    body: raw,
+    redirect: 'follow'
+  };
+
+  const rootUrl = modelType.value === '1' ? window.AppGlobalConfig.knowledgeServer : window.AppGlobalConfig.aiServer
+  fetch(rootUrl + '/chat/chat', requestOptions)
+    .then((response) => {
+      return response.json()
+    })
+    .then((msgStr) => {
+      const msg = JSON.parse(msgStr)
+      const str = msg.choices[0]?.message?.content;
+      if (str) {
+        const str1 = str.slice(str.indexOf("</think>") + 7)
+        recommendedQuestions = formatRecommendedQuestions(str1);
+        questions.value = recommendedQuestions.slice(0, 5);
+      }
+    })
+    .catch((error) => console.error(error));
+};
+const formatRecommendedQuestions = (str) => {
+  return str
+    .replace(/`|$$|$$|"|,/g, '')  // 清除引号、反引号、方括号和逗号
+    .split('\n')                  // 按换行分割
+    .map(line => line.trim())     // 去除首尾空格
+    .filter(line => {             // 双条件过滤
+      return (
+        line.length > 0 &&         // 过滤空行
+        /^\d+[、.]/.test(line)     // 匹配"1、"或"1."开头
+      )
+    })
+    .map(line => {
+      // 统一标点为中文顿号并保留末尾空格
+      return line
+        .replace(/^\d+\./, m => m.replace('.', '、')) // 英文标点转中文
+        .replace(/\s*$/, '   ')                       // 强制保留末尾3空格
+    });
+}
+const changeRecommendedQuestions = () => {
+  recommendedQuestionsIndex++;
+  if (recommendedQuestionsIndex % 2 === 0) {
+    questions.value = recommendedQuestions.slice(0, 5);
+  } else {
+    questions.value = recommendedQuestions.slice(5);
+    getRecommendedQuestion(true)
+  }
+};
+
+const getQuestionKeyWords = async () => {
+  const { question } = historys.value[historyIndex].currentResponse;
+  const myHeaders = new Headers();
+  myHeaders.append('Content-Type', 'application/json');
+  questions.value = [];
+  const raw = JSON.stringify({
+    query: `请从以下文本中提取核心关键词,确保关键词简洁明了且准确反映文本的主要内容。 请按照以下格式输出:关键词:关键词1,关键词2。文本如下:“${question}”`,
+    stream: false
+  });
+
+  const requestOptions = {
+    method: 'POST',
+    headers: myHeaders,
+    body: raw,
+    redirect: 'follow'
+  };
+
+  const rootUrl = modelType.value === '1' ? window.AppGlobalConfig.knowledgeServer : window.AppGlobalConfig.aiServer
+  return fetch(rootUrl + '/chat/chat', requestOptions)
+    .then((response) => response.json())
+    .then((msgStr) => {
+      const msg = JSON.parse(msgStr)
+      activeIndex.value = 1;
+      const str = msg.choices[0]?.message?.content;
+      if (str) {
+        const str1 = str.slice(str.indexOf("</think>")+7)
+        const keywords = splitWords(str1).slice(0, 3);
+        historys.value[historyIndex].currentResponse.keywords = keywords;
+      }
+    })
+    .catch((error) => console.error(error));
+};
+
+const splitWords = (word) => {
+  if (word) {
+    word = word
+      .replaceAll('核心词为:', '')
+      .replaceAll('问题核心词:', '')
+      .replaceAll('>', '')
+      .replaceAll('。', '')
+      .trim();
+
+    if (word.indexOf('\n\n') !== -1) {
+      return word.split('\n\n');
+    }
+
+    if (word.indexOf(',') !== -1) {
+      return word.split(',');
+    }
+
+    if (word.indexOf('、') !== -1) {
+      return word.split('、');
+    }
+
+    if (word.indexOf(',') !== -1) {
+      return word.split(',');
+    }
+
+    return [word];
+  }
+
+  return [];
+};
+
+
+const changeActiveTab = (tab) => {
+  activeTab.value = tab;
+};
+
+const stopAI = () => {
+  if (ctr) {
+    ctr.abort();
+  }
+};
+
+const openRecommendedQuestion = (q) => {
+  if (q) {
+    if (q.charAt(1) === '、') {
+      q = q.substring(2);
+    }
+    window.open(
+      `/aisearch/#/ai-search?q=${encodeURI(q.trim())}&scope=${activeTab.value}&type=${answerType.value}`,
+      '_blank'
+    );
+  }
+};
+onMounted(() => {
+  const { query } = router.currentRoute.value;
+  if (query.q) {
+    answerType.value = query.type || '0';
+    scope.value = query.scope || 'net';
+    var ds = '0';
+    if (!useUserStore().isLogin) {
+      ds = query.ds;
+    } else {
+      ds = useUserStore().user.user.enableDeepseek
+        ? useUserStore().user.user.enableDeepseek + ''
+        : query.ds;
+    }
+    activeTab.value = scope.value;
+  }
+
+});
+
+defineExpose({ changeActiveTab, stopAI });
+
+</script>
+
+<style lang="scss" scoped>
+.home_box {
+  height: 100%;
+  display: flex;
+  >div {
+    height: 100%;
+    &.left_box {
+      padding: 30px 20px;
+      background: #fff;
+      width: 260px;
+      border-right: 1px solid #D9E3F2;
+      >button {
+        width: 100%;
+        background: #EAF2FF;
+        border-radius: 10px 10px 10px 10px;
+        border: 1px solid #C5DAFD;
+        color: #3A81EF;
+        padding: 10px;
+        margin-bottom: 10px;
+      }
+      >div {
+        >.menu_down {
+          display: flex;
+          align-content: center;
+          justify-content: space-between;
+          padding: 10px 0px 10px 0px;
+          font-size: 15px;
+          color: #111;
+          >.icon {
+            color: #646370;
+            font-size: 14px;
+            cursor: pointer;
+          }
+        }
+        &.history {
+          >.history_box {
+            max-height: 600px;
+            overflow-y: auto;
+            >ul {
+              >li {
+                margin-top: 5px;
+                >.title {
+                  font-size: 14px;
+                  color: #666666;
+                }
+                >ul {
+                  padding-left: 15px;
+                  >li {
+                    font-size: 15px;
+                    color: #222222;
+                    padding: 8px 0px;
+                  }
+                }
+              }
+            }
+          }
+        }
+        &.ai_tools {
+          >ul {
+            >li {
+              display: flex;
+              align-items: center;
+              padding: 5px 0px;
+              cursor: pointer;
+              >.icon {
+                display: inline-block;
+                padding: 5px;
+                border-radius: 5px;
+                margin-right: 10px;
+              }
+              &:nth-child(1) {
+                >.icon {
+                  background: #D4F3F7;
+                  >.iconfont {
+                    color: #29BECD;
+                  }
+                }
+              }
+              &:nth-child(2) {
+                >.icon {
+                  background: #E5EFFC;
+                  >.iconfont {
+                    color: #4F7FFF;
+                  }
+                }
+              }
+              >.txt {
+                font-size: 15px;
+                color: #222222;
+              }
+            }
+          }
+        }
+      }
+    }
+    &.right_box {
+      flex: 1;
+      background: linear-gradient( 180deg, #EFF6FF 0%, #FAFCFF 29%, #FFFFFF 100%);
+      .chat-container {
+        width: 100%;
+        max-width: 800px;
+        height: 100%;
+        margin: 0 auto;
+        display: flex;
+        flex-direction: column;
+        flex: 1;
+        .messages-container {
+            flex: 1;
+            overflow-y: auto;
+            display: flex;
+            flex-direction: column;
+            gap: 12px;
+            flex: 1;
+            padding: 15px 0px;
+        }
+
+        .message {
+            border-radius: 12px;
+            position: relative;
+            animation: messageAppear 0.3s ease-out;
+            word-wrap: break-word;
+            line-height: 1.5;
+        }
+
+        .message.user {
+          background: #E3EEFF;
+            color: white;
+            align-self: flex-end;
+            border-radius: 10px;
+            color: #111111;
+            padding: 10px 20px;
+        }
+
+        .message.assistant {
+            align-self: flex-start;
+            border-radius: 12px 12px 12px 0;
+        }
+
+        .message.loading {
+            background: #f8f9fa;
+            padding: 16px;
+            width: 80px;
+            height: 40px;
+        }
+      }
+
+      .input-container {
+          padding: 10px;
+          background: #FFFFFF;
+          box-shadow: 0px 4px 10px 1px rgba(0,0,0,0.1);
+          border-radius: 12px 12px 12px 12px;
+          border: 1px solid #E7E8EA;
+          margin-bottom: 15px;
+          >textarea {
+            border: 0px;
+            outline: none;
+          }
+          >.bottom_box {
+            display: flex;
+            justify-content: space-between;
+            >.tools_box {
+              display: flex;
+              >div {
+                margin-right: 10px;
+              }
+            }
+            >.send_btn {
+              background: linear-gradient( 124deg, #505DFF 0%, #418CFF 100%);
+              border-radius: 5px;
+              width: 40px;
+              height: 40px;
+              line-height: 40px;
+              text-align: center;
+              color: #fff;
+              cursor: pointer;
+              >i {
+                font-size: 20px;
+              }
+
+            }
+          }
+      }
+
+      .input-box {
+          width: 100%;
+          padding: 12px 16px;
+          border: 1px solid #e9ecef;
+          border-radius: 8px;
+          resize: none;
+          min-height: 44px;
+          max-height: 200px;
+          font-size: 16px;
+          transition: border-color 0.2s;
+      }
+
+      .loading-dots {
+          display: flex;
+          align-items: center;
+          height: 100%;
+      }
+
+      .loading-dots span {
+          width: 6px;
+          height: 6px;
+          margin: 0 3px;
+          background: #adb5bd;
+          border-radius: 50%;
+          animation: bounce 1.4s infinite ease-in-out;
+      }
+
+      .loading-dots span:nth-child(2) {
+          animation-delay: 0.2s;
+      }
+
+      .loading-dots span:nth-child(3) {
+          animation-delay: 0.4s;
+      }
+
+      @keyframes messageAppear {
+          from {
+              opacity: 0;
+              transform: translateY(10px);
+          }
+          to {
+              opacity: 1;
+              transform: translateY(0);
+          }
+      }
+
+      @keyframes bounce {
+          0%, 80%, 100% { 
+              transform: translateY(0);
+          }
+          40% {
+              transform: translateY(-8px);
+          }
+      }
+    }
+  }
+}
+</style>
+<style scoped lang="scss">
+@import 'src/assets/scss/variables';
+.ai-search-detail {
+  width: 100%;
+  height: 100%;
+
+
+  .search-panel {
+    width: 100%;
+    display: flex;
+    .search-detail {
+      &-tdsc {
+        width: calc(52%);
+      }
+
+      .search-result {
+        height: calc(100% - 40px);
+
+
+        .result-panel {
+          width: 100%;
+          .result {
+            width: 100%;
+
+            .result-view {
+              width: 100%;
+              height: 50%;
+              overflow-y: auto;
+              overflow-x: hidden;
+              .q-r {
+                line-height: 30px;
+                border-radius: 4px 4px 4px 4px;
+                margin: 20px 0;
+                .ds-content-box {
+                  display: flex;
+                  .icon {
+                    width: 26px;
+                    height: 26px;
+                    display: flex;
+                    align-items: center;
+                    justify-content: center;
+                    border-radius: 50%;
+                    background: white;
+                    border: 1px solid #117ff9;
+                    img {
+                      width: 18px;
+                      height: 18px;
+                    }
+                  }
+                  .ds-panel {
+                    margin-top: -5px;
+                    margin-left: 10px;
+                    width: calc(100% - 30px);
+                    .ds-loading {
+                      display: flex;
+                      align-items: center;
+                      .icon-arrow {
+                        padding-left: 5px;
+                        cursor: pointer;
+                      }
+                    }
+                    .rotate {
+                      transform: rotate(180deg);
+                    }
+                  }
+                  .ds-con {
+                    border-left: 2px solid #eee;
+                    padding-left: 15px;
+                    color: rgba(0,0,0,0.6) !important;
+                  }
+                }
+              }
+
+              .more-questions {
+                .title {
+                  display: flex;
+                  margin-bottom: 14px;
+
+                  .change-title {
+                    margin-left: 20px;
+                    display: flex;
+                    cursor: pointer;
+                    font-size: 13px;
+                    color: #3c3c3c;
+
+                    .change-icon {
+                      width: 16px;
+                      height: 16px;
+                      margin-right: 4px;
+                      background: url('@/assets/images/ai-search/icon-chang-question.png') no-repeat;
+                      background-size: 100% 100%;
+                    }
+                  }
+                }
+              }
+
+              .source {
+                width: 100%;
+                background: #eff4ff;
+                padding: 30px;
+                padding-bottom: 0px;
+                border-radius: 10px;
+                .title {
+                    font-weight: bold;
+                    font-size: 17px;
+                    color: #0F0F0F;
+                    line-height: 13px;
+                    text-align: left;
+                    font-style: normal;
+                    text-transform: none;
+                    margin-bottom: 20px;
+                }
+
+                .items {
+                  .item {
+                    width: calc(100% - 10px);
+                    min-height: 82px;
+                    border-radius: 10px;
+                    display: flex;
+                    flex-direction: column;
+                    align-items: center;
+                    color: #888;
+                    display: flex;
+                    line-height: 25px;
+                    border-radius: 8px 8px 8px 8px;
+
+                    .doc {
+                      display: flex;
+                      width: 100%;
+
+                      p {
+                        width: calc(100% - 25px);
+
+                        .doc-link {
+                          cursor: pointer;
+                          color: #3082F4;
+                          font-size: 14px;
+                          &:hover {
+                            font-weight: bold;
+                          }
+                        }
+                      }
+
+                      .doc-icon {
+                        width: 20px;
+                        height: 20px;
+                        margin-left: 5px;
+                        cursor: pointer;
+
+                        &-show {
+                          background: url('/images/zcbd/ai-search/icon-doc-show.png') no-repeat;
+                          background-size: 100% 100%;
+                        }
+
+                        &-hide {
+                          background: url('/images/zcbd/ai-search/icon-doc-hide.png') no-repeat;
+                          background-size: 100% 100%;
+                        }
+                      }
+                    }
+
+                    .content {
+                      //white-space: pre-line;
+                      width: 100%;
+                      font-family: Alibaba PuHuiTi 3, Alibaba PuHuiTi 30;
+                      font-weight: normal;
+                      font-size: 14px;
+                      color: #757575;
+                      line-height: 28px;
+                      text-align: left;
+                      font-style: normal;
+                      text-transform: none;
+
+                      &-hide {
+                        display: none;
+                      }
+                    }
+                  }
+
+                  .item-url {
+                    margin: 12px 0;
+                    background: #f1f9ff;
+                    border-radius: 8px 8px 8px 8px;
+
+                    .bottom {
+                      width: 100%;
+
+                      .title-icon {
+                        display: flex;
+                        justify-content: left;
+                        align-items: center;
+                        width: 90%;
+                        float: left;
+
+                        .icon {
+                          width: 23px;
+                          height: 23px;
+                          background: url('/images/zcbd/ai-search/icon-net-url.png') no-repeat;
+                          background-size: 100% 100%;
+                        }
+
+                        .title {
+                          color: #a9a3a3cc;
+                          font-size: 16px;
+                          margin-left: 10px;
+                        }
+                      }
+
+                      .index {
+                        float: right;
+                        width: 20px;
+                        height: 20px;
+                        font-size: 12px;
+                        line-height: 20px;
+                        text-align: center;
+                        margin: 0 5px;
+                        border-radius: 10px;
+                        background: #d0d5dd;
+                        color: #000;
+                      }
+                    }
+                  }
+                }
+              }
+            }
+
+            // 土地市场
+            .map-answer {
+              position: relative;
+              width: 100%;
+              min-height: 600px;
+
+              .left-panel {
+                width: 100%;
+                background: #ffffff;
+                z-index: 1000;
+                //position: relative;
+                left: 12px;
+                top: 15px;
+                border-radius: 10px;
+
+                .title {
+                  width: 100%;
+                  height: 30px;
+                  text-align: left;
+                  line-height: 30px;
+                  font-size: 18px;
+                  font-weight: bolder;
+                }
+
+                .content {
+                  width: 100%;
+                  height: auto;
+
+                  .chart-card {
+                    width: 100%;
+                    height: 330px;
+
+                    .chart-title {
+                      width: 100%;
+                      height: 30px;
+                      font-size: 18px;
+                      font-weight: bolder;
+                      padding: 0 5px;
+                      line-height: 30px;
+                      margin: 10px 0;
+                    }
+
+                    .chart {
+                      width: 100%;
+                      height: 300px;
+                    }
+
+                    //background: #0a84ff;
+                  }
+
+                  .summary-card {
+                    width: 100%;
+                    height: 50%;
+
+                    .summary-title {
+                      width: 100%;
+                      height: 30px;
+                      font-size: 18px;
+                      font-weight: bolder;
+                      padding: 0 5px;
+                      line-height: 30px;
+                      margin: 10px 0;
+                    }
+
+                    .summary-content {
+                      width: calc(100% - 20px);
+                      height: calc(100% - 30px);
+                      padding: 0;
+                      white-space: pre-wrap;
+                      overflow-y: auto;
+                      border-radius: 10px;
+                      margin: 10px;
+                      font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Noto Sans, Helvetica,
+                        Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji';
+                      font-size: 16px;
+                      line-height: 1.5;
+                    }
+                  }
+                }
+              }
+
+              .map-panel {
+                width: 100%;
+                height: 630px;
+
+                .map-title {
+                  width: 100%;
+                  height: 30px;
+                  font-size: 18px;
+                  font-weight: bolder;
+                  padding: 0 5px;
+                  line-height: 30px;
+                  margin: 10px 0;
+                }
+
+                .map {
+                  width: 100%;
+                  height: 600px;
+                }
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+
+  ::-webkit-scrollbar {
+    width: 8px;
+  }
+
+  ::-webkit-scrollbar-thumb {
+    background-color: rgb(0 0 0 / 10%);
+    border-radius: 10px;
+  }
+}
+
+</style>

+ 1810 - 0
ais_search_zj/web/src/views/ai-search/index3.vue

@@ -0,0 +1,1810 @@
+<template>
+  <div class="ai-search-detail">
+    <div class="search-panel" id="pageContainer">
+      <div :class="`search-detail search-detail-${askType}`" ref="searchRef">
+        <div
+          class="search-result"
+          id="searchResult"
+          style="overflow-x: hidden; height: auto"
+          ref="messageContainer"
+        >
+
+          <div :class="`result-panel result-panel-${askType}`">
+
+            <div class="result">
+              <template v-if="askType === 'zcfg'">
+                <div class="tabs" style="position: relative">
+                  <div
+                    :class="`tab${activeTab === t.key ? ' tab-active' : ''}`"
+                    v-for="t in tabs"
+                    :key="t.key"
+                    @click="changeTab(t.key)"
+                  >
+                    <div class="title">{{ t.name }}</div>
+                    <div class="bottom"></div>
+                  </div>
+                  <div
+                    class="scope-type"
+                    style="
+                      display: flex;
+                      align-items: center;
+                      position: absolute;
+                      right: 5px;
+                      top: 10px;
+                    "
+                  >
+                    <div class="label">搜索模式:</div>
+                    <a-radio-group
+                      v-model:value="answerType"
+                      @change="changeAnswerType"
+                      name="radioGroup"
+                    >
+                      <a-radio value="0">简洁</a-radio>
+                      <a-radio value="1">深入</a-radio>
+                      <a-radio value="2">研究</a-radio>
+                    </a-radio-group>
+                    <div class="ds-box">
+                      <div @click="onChange('1')" :class="{active: dsChecked === true}">
+                        <i class="iconfont icon-deepsee"></i>
+                      </div>
+                      <div @click="onChange('0')" :class="{active: dsChecked === false}">
+                        <i class="iconfont icon-tongyi"></i>
+                      </div>
+                    </div>
+                  </div>
+                </div>
+                <div class="result-view">
+                  <div class="q-r">
+                    <div class="ds-content-box" v-if="dsChecked">
+                      <div class="icon">
+                        <img src="/images/icon-ds.png" />
+                      </div>
+                      <div class="ds-panel">
+                        <div class="ds-loading" v-if="dsHintTxt" @click="dsUp = !dsUp">
+                          {{ dsHintTxt }}
+                          <DownOutlined class="icon-arrow" :class="{ rotate: dsUp }" />
+                        </div>
+                        <div class="ds-con" v-if="!dsUp">
+                          <vue-markdown-it
+                            id="dsMarkdown"
+                            :source="
+                              currentResponse.streamMock
+                                ? currentResponse.streamMsg.indexOf('###') > -1
+                                  ? currentResponse.streamMsg.substring(
+                                      0,
+                                      currentResponse.streamMsg.indexOf('###')
+                                    )
+                                  : currentResponse.streamMsg
+                                : currentResponse.msg.indexOf('###') > -1
+                                ? currentResponse.msg.substring(
+                                    0,
+                                    currentResponse.msg.indexOf('###')
+                                  )
+                                : currentResponse.msg
+                            "
+                            :options="{
+                              html: true,
+                              linkify: true
+                            }"
+                          />
+                        </div>
+                        <a-spin :indicator="indicator" v-if="dsLoading" />
+                      </div>
+                    </div>
+                    <vue-markdown-it
+                      id="resMarkdown"
+                      :source="
+                        currentResponse.streamMock
+                          ? currentResponse.streamMsg.indexOf('###') > -1
+                            ? currentResponse.streamMsg.substring(
+                                currentResponse.streamMsg.indexOf('###')
+                              )
+                            : ''
+                          : dsChecked
+                          ? currentResponse.msg.indexOf('###') > -1
+                            ? currentResponse.msg.substring(currentResponse.msg.indexOf('###'))
+                            : ''
+                          : currentResponse.msg
+                      "
+                      :options="{
+                        html: true,
+                        linkify: true
+                      }"
+                    />
+                  </div>
+                  <a-skeleton
+                    active
+                    v-if="!dsLoading && (activeIndex < 2 || !currentResponse.msg)"
+                  />
+                  <div class="more-questions" v-if="activeIndex >= 4">
+                    <div class="title" style="margin-bottom: 14px">
+                      你可以继续问我:
+                      <div class="change-title" @click="changeRecommendedQuestions">
+                        <div class="change-icon"></div>
+                        换一批
+                      </div>
+                    </div>
+                    <div class="questions" style="cursor: pointer">
+                      <div
+                        class="question"
+                        @click="openRecommendedQuestion(q)"
+                        v-for="(q, i) in questions"
+                        :key="i"
+                        style="
+                          line-height: 14px;
+                          text-align: left;
+                          font-style: normal;
+                          text-transform: none;
+                          width: fit-content;
+                          margin-bottom: 12px;
+                          background: #ffffff;
+                          box-shadow: 0px 1px 8px 0px #e4e4e4;
+                          border-radius: 10px;
+                          border: 1px solid #e9e9e9;
+                          padding: 16px;
+                          font-family: PingFang SC;
+                          font-weight: 500;
+                          font-size: 14px;
+                          color: #2185f2;
+                        "
+                      >
+                        {{ q }}
+                      </div>
+                    </div>
+                  </div>
+                  <div class="source" v-if="activeTab !== 'original'" v-show="activeIndex === 5">
+                    <div class="title">
+                      <div class="icon"></div>
+                      <div class="text">来源</div>
+                      <div class="num">({{ currentResponse.docs.length }})</div>
+                    </div>
+                    <div class="items">
+                      <div
+                        class="item"
+                        v-if="activeTab !== 'net'"
+                        v-for="(doc, i) in currentResponse.docs"
+                        :key="'doc-' + i"
+                      >
+                        <div class="doc">
+                          <p>
+                            <span
+                              class="ma"
+                              style="font-weight: bolder; color: #000000; margin-right: 10px"
+                              >出处 [{{ i + 1 }}] </span
+                            ><span class="doc-link" @click="openDoc(doc, i)">{{ doc.doc }}</span>
+                          </p>
+                          <div
+                            :class="`doc-icon${
+                              !doc.showContent ? ' doc-icon-show' : ' doc-icon-hide'
+                            }`"
+                            @click="doc.showContent = !doc.showContent"
+                          ></div>
+                        </div>
+
+                        <div :class="`content${doc.showContent ? '' : ' content-hide'}`">
+                          <p>{{ doc.content }}</p>
+                        </div>
+                      </div>
+                      <div
+                        class="item item-url"
+                        v-if="activeTab === 'net'"
+                        v-for="(doc, i) in currentResponse.docs"
+                        :key="'doc-' + i"
+                      >
+                        <div class="doc">
+                          <p>
+                            <span class="doc-link" @click="openUrl(doc.link)">{{ doc.title }}</span>
+                          </p>
+                        </div>
+                        <div class="bottom">
+                          <div class="title-icon">
+                            <div class="icon"></div>
+                            <div class="title">{{ doc.doc }}</div>
+                          </div>
+                          <div class="index">{{ i + 1 }}</div>
+                        </div>
+                      </div>
+                    </div>
+                  </div>
+                </div>
+              </template>
+              <template v-else>
+                <div v-if="activeIndex >= 1" class="map-answer">
+                  <div class="left-panel">
+                    <div class="content">
+                      <div class="summary-card">
+                        <div class="summary-title" id="tdscSummaryTitle">总结</div>
+                        <div class="summary-content">
+                          <a-skeleton active v-if="!currentResponse.msg" />
+                          <vue-markdown-it
+                            :source="
+                              currentResponse.streamMock
+                                ? currentResponse.streamMsg
+                                : currentResponse.msg
+                            "
+                            :options="{
+                              html: true,
+                              linkify: true
+                            }"
+                          />
+                        </div>
+                      </div>
+                      <div class="chart-card" v-if="currentResponse.hasChart" id="tdscChartCard">
+                        <div class="chart-title">生成图表</div>
+                        <a-skeleton
+                          active
+                          style="height: 100%"
+                          v-if="!currentResponse.chartOption"
+                        />
+                        <div v-else class="chart" id="summaryChart"></div>
+                      </div>
+                    </div>
+                  </div>
+                </div>
+              </template>
+            </div>
+          </div>
+        </div>
+        <!--   历史回答     -->
+        <div
+          v-show="askType === 'zcfg'"
+          class="search-result result-history"
+          style="overflow-y: hidden; height: auto"
+          v-if="questHistories.length > 0"
+          id="messageContainer"
+        >
+          <template v-for="(qh, i) in [...questHistories].reverse()" :key="`qh-${i}`">
+            <div class="top">
+              <div class="title">
+                <div class="icon"></div>
+                <div class="question" style="cursor: pointer">{{ qh.question }}</div>
+              </div>
+            </div>
+
+            <div class="result-panel">
+              <div class="result">
+                <div class="tabs">
+                  <div :class="`tab tab-active`">
+                    <div class="title">回答</div>
+                    <div class="bottom"></div>
+                  </div>
+                </div>
+              </div>
+              <div class="result-view">
+                <div class="q-r">
+                  <vue-markdown-it
+                    :source="qh.msg"
+                    :toc="true"
+                    :options="{
+                      html: true,
+                      linkify: true
+                    }"
+                  />
+                </div>
+              </div>
+            </div>
+          </template>
+        </div>
+      </div>
+      <a-affix v-if="showDoc" style="width: 52%; font-size: 16px" :offset-top="210">
+        <div
+          class="doc"
+          style="width: 100%; background-color: white; height: calc(100vh - 190px); overflow: auto"
+        >
+          <p-d-f-viewer
+            v-if="fileType === 'pdf'"
+            :src="pdfSrc"
+            @close="closeDoc"
+            :content="pdfContent"
+            :num="pdfNum"
+          />
+          <word-viewer
+            v-if="fileType === 'docx'"
+            :src="pdfSrc"
+            @close="closeDoc"
+            :content="pdfContent"
+            :num="pdfNum"
+          >
+          </word-viewer>
+          <txt-viewer v-if="fileType === 'txt'" :src="pdfSrc" @close="closeDoc" :txt="pdfContent" />
+        </div>
+      </a-affix>
+    </div>
+
+    <a-modal force-render class="input-modal" v-model:open="open" :closable="false" width="700px">
+      <template #footer>
+        <div class="footer" style="height: 26px; margin: 0 auto">
+          <div class="left" style="float: left; cursor: pointer" @click="clearModalTextArea">
+            清空
+          </div>
+          <a-button style="float: right" type="primary" size="small" @click="sendModalTextArea">
+            发送
+          </a-button>
+        </div>
+      </template>
+      <div class="modal-que">
+        <ai-textarea :q="question" ref="aiTextAreaRef" @enter="ask" />
+      </div>
+    </a-modal>
+  </div>
+</template>
+<script setup>
+import { fetchEventSource } from '@microsoft/fetch-event-source';
+import PDFViewer from '@/components/pdf/PdfCanvas.vue';
+import WordViewer from '@/components/pdf/WordViewer.vue';
+import AiTextarea from '@/components/TextArea/AiTextarea.vue';
+import { VueMarkdownIt } from '@f3ve/vue-markdown-it';
+import TxtViewer from '@/components/pdf/TxtViewer.vue';
+import { message } from 'ant-design-vue';
+import { ref, defineProps, watch } from 'vue';
+import CommonAPI from '@/api/common';
+import ManagerAPI from '@/api/manager';
+import PubsubService from '@/utils/PubsubService';
+import { useUserStore } from '@/stores';
+const aiLoading = ref(false);
+const router = useRouter();
+import { LoadingOutlined, DownOutlined } from '@ant-design/icons-vue';
+import { h } from 'vue';
+const indicator = h(LoadingOutlined, {
+  style: {
+    fontSize: '24px'
+  },
+  spin: true
+});
+const props = defineProps({
+  searchType: '',
+  askType: {
+    type: String,
+    default: 'zcfg'
+  }
+});
+
+const answerType = ref('0');
+const dsChecked = ref(true);
+const scope = ref('net');
+watch(
+  () => router.currentRoute.value,
+  (value) => {
+    const { query } = value;
+    if (query.q) {
+      ask(decodeURIComponent(query.q));
+      answerType.value = query.type || '0';
+      scope.value = query.scope || 'net';
+      dsHintTxt.value = '';
+    }
+  }
+);
+
+watch(
+  () => router.currentRoute.value,
+  (value) => {
+    const { query } = value;
+    if (query.q) {
+      var ds = query.ds;
+      if (ds && ds == '1') {
+        ds = true;
+      } else {
+        ds = false;
+      }
+      dsChecked.value = ds;
+    }
+  },
+  {
+    immediate: true
+  }
+);
+
+watch(props.searchType, (val) => {
+  activeTab.value = val;
+});
+
+const question = ref('国有土地的使用方式有哪些?');
+const statusText = ref('检索中');
+const activeIndex = ref(0);
+const activeTab = ref(props.searchType);
+let ctr = null;
+const aiTextAreaRef = ref(null);
+const searchRef = ref(null);
+
+const evaluate = ref(null);
+
+const clearModalTextArea = () => {
+  aiTextAreaRef.value.clear();
+};
+
+const sendModalTextArea = () => {
+  aiTextAreaRef.value.send();
+};
+
+const open = ref(false);
+const showDoc = ref(false);
+const pdfSrc = ref('');
+const pdfContent = ref('');
+const pdfNum = ref(1);
+const fileType = ref('pdf');
+const dsLoading = ref(true);
+const startTime = ref(0);
+const endTime = ref(0);
+const dsHintTxt = ref('');
+const dsUp = ref(false);
+const times = ref(0);
+const timers = ref([]);
+const currentResponse = ref({
+  streamMsg: '',
+  streamMock: false,
+  msg: '',
+  originAnswer: '',
+  docs: []
+});
+let streamMockInterval = null;
+const streamToAnswer = () => {
+  console.log('streamToAnswer');
+  currentResponse.value.index = 0;
+  streamMockInterval = setInterval(() => {
+    const { originAnswer = '', msg, streamMsg = '', id, index = 0 } = currentResponse.value;
+
+    if (currentResponse.value.mockStart || index <= originAnswer.length) {
+      currentResponse.value.streamMsg = originAnswer.substr(0, index + 2).replaceAll('\n', '  \n');
+      if (originAnswer) {
+        currentResponse.value.index += 2;
+      }
+      let num = getNum(currentResponse.value.streamMsg);
+
+      while (num) {
+        const docsNum = currentResponse.value.docs.length;
+        currentResponse.value.streamMsg = currentResponse.value.streamMsg.replace(
+          `[[${num}]]`,
+          `<span onclick="window.openDocByIndex(${num}, ${id})" 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;
+    height: 20px;
+    background: #FFFFFF;
+    border-radius: 4px 4px 4px 4px;
+    border: 1px solid #BACAE3;">${num}</span>`
+        );
+
+        num = getNum(currentResponse.value.streamMsg);
+      }
+    } else {
+      if (streamMockInterval) {
+        clearInterval(streamMockInterval);
+        streamMockInterval = null;
+      }
+
+      activeIndex.value = 5;
+
+      searchRef.value.scrollTop = 0;
+      console.log('mock 结束');
+    }
+  }, 50);
+};
+
+const tabs = [
+  { key: 'knowledge', name: '知识库' },
+  { key: 'net', name: '全网' },
+  { key: 'original', name: '原生' },
+];
+
+
+const changeStatusText = () => {
+  let i = 0;
+  setInterval(() => {
+    statusText.value = '检索中' + '.'.repeat(i);
+
+    if (i === 3) {
+      i = 0;
+    }
+    i++;
+  }, 500);
+};
+changeStatusText();
+const askType = ref('zcfg');
+const search = (q, type = 'zcfg') => {
+  question.value = q;
+  askType.value = type;
+  if (ctr) {
+    ctr.abort();
+    ctr = null;
+    if (streamMockInterval) {
+      clearInterval(streamMockInterval);
+      streamMockInterval = null;
+    }
+  }
+  if (type === 'zcfg') {
+    quest();
+  } else {
+  }
+};
+
+const ask = async (q, isFllow) => {
+  if (ctr) {
+    if (streamMockInterval) {
+      clearInterval(streamMockInterval);
+      streamMockInterval = null;
+    }
+    ctr.abort();
+  }
+  times.value = 0;
+  if (timers.value) {
+    timers.value.forEach((t) => {
+      clearTimeout(t);
+      t = null;
+    });
+  }
+  timers.value = [];
+  if (dsChecked.value) {
+    dsHintTxt.value = '';
+    dsLoading.value = true;
+  }
+  question.value = q;
+  open.value = false;
+  showDoc.value = false;
+  askType.value = 'zcfg';
+  // activeTab.value = 'knowledge';
+  quest(isFllow);
+};
+
+let questionUrl = '/chat/kb_chat';
+
+const changeTab = (tab) => {
+  dsLoading.value = dsChecked.value;
+  dsHintTxt.value = '';
+  times.value = 0;
+  if (timers.value) {
+    timers.value.forEach((t) => {
+      clearTimeout(t);
+      t = null;
+    });
+  }
+  timers.value = [];
+  if (tab === 'net') {
+    questionUrl = '/chat/bing_chat';
+  } else if(tab === 'knowledge'){
+    questionUrl = '/chat/kb_chat';
+  }else if(tab === 'original') {
+    questionUrl = '/chat/chat';
+  }
+
+  activeTab.value = tab;
+  if (ctr) {
+    if (streamMockInterval) {
+      clearInterval(streamMockInterval);
+      streamMockInterval = null;
+    }
+    ctr.abort();
+  }
+  quest();
+};
+const onChange = (type) => {
+  dsChange(type);
+  changeAnswerType();
+};
+//ds绑定用户改变
+const dsChange = (type) => {
+  dsChecked.value = type === '0' ? false : true;
+  localStorage.setItem("_isDeepSeek", type);
+  if (timers.value) {
+    timers.value.forEach((t) => {
+      clearTimeout(t);
+      t = null;
+    });
+    timers.value = [];
+  }
+  times.value = 0;
+  dsHintTxt.value = '';
+  //打字机效果 切换会打印
+  aiLoading.value = true;
+};
+const changeAnswerType = () => {
+  if (!dsChecked.value) {
+    dsLoading.value = false;
+    dsHintTxt.value = '';
+  } else {
+    dsLoading.value = true;
+    dsHintTxt.value = '';
+  }
+  if (ctr) {
+    if (streamMockInterval) {
+      clearInterval(streamMockInterval);
+      streamMockInterval = null;
+    }
+    ctr.abort();
+  }
+  quest();
+};
+
+const questHistories = ref([]);
+
+let scb = null;
+const quest = async (isFllow) => {
+  startTime.value = Date.now();
+  window.scroll({ top: 0, behavior: 'smooth' });
+  question.value = (isFllow ? '追问: ' : '') + question.value;
+  evaluate.value = null;
+  activeIndex.value = 0;
+
+  if (!isFllow) {
+    showDoc.value = false;
+  }
+  searchRef.value.scrollTop = 0;
+  if (isFllow) {
+    const { id, question, msg, docs, originAnswer, keywords = [] } = currentResponse.value;
+    questHistories.value.push({ id, question, msg, docs: docs, originAnswer, keywords });
+  } else {
+    questHistories.value = [];
+  }
+  aiLoading.value = true;
+  if (scb !== null) {
+    clearInterval(scb);
+    scb = null;
+  } else {
+    scb = setInterval(() => {
+    }, 500);
+  }
+  ctr = new AbortController();
+
+  const id = questHistories.value.length;
+  currentResponse.value = {
+    question: question.value,
+    id,
+    msg: '',
+    originAnswer: '',
+    streamMock: false,
+    streamMsg: '',
+    docs: []
+  };
+  activeIndex.value = 0;
+  getQuestionKeyWords();
+  if (activeTab.value === 'net') {
+    questionUrl = '/chat/bing_chat';
+  } else if(activeTab.value === 'knowledge'){
+    questionUrl = '/chat/kb_chat';
+  }else if(activeTab.value === 'original') {
+    questionUrl = '/chat/chat';
+  }
+  const topKs = window?.AppGlobalConfig?.topKs || {
+    0: 5,
+    1: 10,
+    2: 15
+  };
+  let body = null
+  if (activeTab.value === 'net') {
+    body = {
+      query: question.value,
+      stream: true,
+      model: dsChecked.value ? 'deepseek-r1' : '',
+      search_type: answerType.value
+    }
+  } else if(activeTab.value === 'knowledge'){
+    body = {
+      query: question.value,
+      mode: 'local_kb',
+      kb_name: activeTab.value === 'paper' ? 'compose_paper_material_total' : dsChecked.value ? window?.AppGlobalConfig?.llm?.kb_name : 'policy',
+      top_k: topKs[answerType.value],
+      search_type: answerType.value,
+      score_threshold: 0.5,
+      model: dsChecked.value ? 'deepseek-r1' : '',
+      history: isFllow ? getFlowHistory() : [],
+      stream: true,
+      prompt_name: 'rag_context_qa.md',
+      return_direct: false
+    }
+  } else if (activeTab.value === 'original') {
+    body = {
+      query: question.value,
+      stream: true
+    }
+  }
+  if (activeTab.value === 'knowledge' && isFllow) {
+    if (questHistories.value.length > 0) {
+      body.history_keyword = questHistories.value[questHistories.value.length - 1].keywords;
+    }
+  }
+  if (activeTab.value !== 'net' && answerType.value !== '0') {
+    currentResponse.value.streamMock = true;
+    currentResponse.value.mockStart = true;
+    streamToAnswer();
+  }
+  const rootUrl = dsChecked.value ? window.AppGlobalConfig.knowledgeServer : window.AppGlobalConfig.aiServer
+  await fetchEventSource(rootUrl + questionUrl, {
+    method: 'POST',
+    openWhenHidden: true,
+    timeout: 300000,
+    headers: {
+      'Content-Type': 'application/json'
+    },
+
+    body: JSON.stringify(body),
+    signal: ctr.signal,
+    async onmessage(msg) {
+      if (activeIndex.value !== 3) {
+        activeIndex.value = 3;
+      }
+
+      activeTab.value === 'net' ? handleNetResponse(msg, id) : handleKnowledgeResponse(msg, id);
+    },
+    onclose() {
+      if (scb !== null) {
+        clearInterval(scb);
+        scb = null;
+
+        collectQuestion();
+      }
+
+      if (currentResponse.value.streamMock) {
+        currentResponse.value.mockStart = false;
+      } else {
+        activeIndex.value = 5;
+
+        searchRef.value.scrollTop = 0;
+      }
+    },
+    onerror(err) {
+      throw err;
+    }
+  });
+};
+
+const getFlowHistory = () => {
+  const lastHistory = [...questHistories.value].splice(-1);
+  const parm = [];
+  lastHistory.forEach((item) => {
+    parm.push({ role: 'user', content: item.question });
+  });
+
+  return parm;
+};
+
+const handleKnowledgeResponse = (msg, id) => {
+  if (!msg || !msg.data) {
+    return;
+  }
+  const rData = JSON.parse(msg.data);
+  if (rData?.choices && rData.choices.length > 0) {
+    if (activeTab.value === 'net' && rData.status !== 2) {
+      return;
+    }
+    if (rData.status == 3) {
+      if (timers.value) {
+        timers.value.forEach((t) => {
+          clearTimeout(t);
+          t = null;
+        });
+        timers.value = [];
+      }
+      if (dsChecked.value) {
+        endTime.value = Date.now();
+        var time = ((endTime.value - startTime.value) / 1000).toFixed(0);
+        dsHintTxt.value = `已深度思考(用时 ${time} 秒)`;
+        dsLoading.value = false;
+        aiLoading.value=true;
+      }
+        currentResponse.value.originAnswer = rData.choices[0]?.delta?.content.replaceAll(
+          '\n',
+          `  \n`
+        );
+        currentResponse.value.msg = rData.choices[0]?.delta?.content.replaceAll('\n', `  \n`);
+        let num = getNum(currentResponse.value.msg);
+        while (num) {
+          const docsNum = currentResponse.value.docs.length;
+          if (docsNum && num > docsNum + 1) {
+          }
+
+          currentResponse.value.msg = currentResponse.value.msg.replace(
+            `[[${num}]]`,
+            `<span onclick="window.openDocByIndex(${num}, ${id})" 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;
+    height: 20px;
+    background: #FFFFFF;
+    border-radius: 4px 4px 4px 4px;
+    border: 1px solid #BACAE3;">${num}</span>`
+          );
+
+          num = getNum(currentResponse.value.msg);
+        }
+    } else {
+      aiLoading.value = false;
+      if (dsChecked.value) {
+        dsHintTxt.value = '思考中...';
+        //ds模式打字机效果输出
+        const timer = setTimeout(() => {
+          if (!aiLoading.value) {
+            currentResponse.value.originAnswer += rData.choices[0]?.delta?.content.replaceAll(
+              '\n',
+              `  \n`
+            );
+            currentResponse.value.msg += rData.choices[0]?.delta?.content.replaceAll('\n', `  \n`);
+            let num = getNum(currentResponse.value.msg);
+            while (num) {
+              const docsNum = currentResponse.value.docs.length;
+              if (docsNum && num > docsNum + 1) {
+              }
+
+              currentResponse.value.msg = currentResponse.value.msg.replace(
+                `[[${num}]]`,
+                `<span onclick="window.openDocByIndex(${num}, ${id})" 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;
+    height: 20px;
+    background: #FFFFFF;
+    border-radius: 4px 4px 4px 4px;
+    border: 1px solid #BACAE3;">${num}</span>`
+              );
+
+              num = getNum(currentResponse.value.msg);
+            }
+          }
+          clearTimeout(timer);
+          timers.value.push(timer);
+        }, times.value * 15);
+      } else {
+        currentResponse.value.originAnswer += rData.choices[0]?.delta?.content.replaceAll(
+          '\n',
+          `  \n`
+        );
+        currentResponse.value.msg += rData.choices[0]?.delta?.content.replaceAll('\n', `  \n`);
+        let num = getNum(currentResponse.value.msg);
+        while (num) {
+          const docsNum = currentResponse.value.docs.length;
+          if (docsNum && num > docsNum + 1) {
+          }
+
+          currentResponse.value.msg = currentResponse.value.msg.replace(
+            `[[${num}]]`,
+            `<span onclick="window.openDocByIndex(${num}, ${id})" 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;
+    height: 20px;
+    background: #FFFFFF;
+    border-radius: 4px 4px 4px 4px;
+    border: 1px solid #BACAE3;">${num}</span>`
+          );
+
+          num = getNum(currentResponse.value.msg);
+        }
+      }
+    }
+  }
+
+  if (!currentResponse.value.docs.length) {
+    if (rData.docs && rData.docs.length) {
+      handleDocs(rData.docs);
+    }
+  }
+  times.value++;
+};
+
+const handleNetResponse = (msg, id) => {
+  const rData = msg.data;
+  if (!!rData && rData !== '[DONE]') {
+    try {
+      const res = JSON.parse(rData);
+      if (res.error) {
+        activeIndex.value = 4;
+        message.error(res.error);
+        currentResponse.value.msg = res.error;
+
+        return;
+      }
+      if (res.rag_finish) {
+        if (activeIndex.value < 4) {
+          activeIndex.value = 4;
+        }
+        if (dsChecked.value) {
+          endTime.value = Date.now();
+          var time = ((endTime.value - startTime.value) / 1000).toFixed(0);
+          dsHintTxt.value = `已深度思考(用时 ${time} 秒)`;
+          dsLoading.value = false;
+        }
+      } else {
+        if (dsChecked.value && dsHintTxt.value != '思考中...') {
+          dsHintTxt.value = '思考中...';
+        }
+      }
+      if (res.result) {
+        currentResponse.value.originAnswer = res.result;
+        currentResponse.value.msg = res.result.replaceAll('\n', `  \n`);
+
+        let num = getNum(currentResponse.value.msg);
+
+        while (num) {
+          const docsNum = currentResponse.value.docs.length;
+          if (docsNum && num > docsNum + 1) {
+            currentResponse.value.msg = currentResponse.value.msg.replace(`[[${num}]]`, ``);
+          }
+          currentResponse.value.msg = currentResponse.value.msg.replace(
+            `[[${num}]]`,
+            `<span onclick="window.openDocByIndex(${num}, ${id})" 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>`
+          );
+
+          num = getNum(currentResponse.value.msg);
+        }
+      }
+      if (res.source_list && res.source_list.length && !currentResponse.value.docs.length) {
+        handleDocs(res.source_list);
+      }
+    } catch (e) {}
+  }
+
+  if (rData === '[DONE]') {
+    console.log(currentResponse.value.msg);
+  }
+};
+
+const handleDocs = (docs) => {
+  if (
+    docs.length === 1 &&
+    "<span style='color:red'>未找到相关文档,该回答为大模型自身能力解答!</span>" === docs[0]
+  ) {
+    return;
+  }
+
+  currentResponse.value.docs = docs.map((v, i) => {
+    if (activeTab.value === 'net') {
+      return {
+        index: v.num,
+        doc: v.name,
+        link: v.url,
+        title: v.title,
+        summary: v.summary,
+        content: '',
+        showContent: false,
+        type: 'url'
+      };
+    }
+
+    if (v.toLowerCase().indexOf('.pdf') > 0) {
+      return {
+        index: i++,
+        doc: v.substring(v.toLowerCase().indexOf('] [') + 3, v.toLowerCase().indexOf('.pdf]') + 4),
+        link: v.substring(v.toLowerCase().indexOf('.pdf]') + 6, v.toLowerCase().indexOf('.pdf)') + 4),
+        content: v.substring(v.toLowerCase().indexOf('.pdf)') + 5),
+        showContent: false,
+        type: 'pdf'
+      };
+    } else if (v.toLowerCase().indexOf('.txt') > 0) {
+      return {
+        index: i++,
+        doc: v.toLowerCase().substring(v.indexOf('] [') + 3, v.toLowerCase().indexOf('.txt]') + 4),
+        link: v.toLowerCase().substring(v.indexOf('.txt]') + 6, v.toLowerCase().indexOf('.txt)') + 4),
+        content: v.toLowerCase().substring(v.indexOf('.txt)') + 5),
+        showContent: false,
+        type: 'txt'
+      };
+    } else if (v.toLowerCase().indexOf('.docx') > 0) {
+      return {
+        index: i++,
+        doc: v.substring(v.toLowerCase().indexOf('] [') + 3, v.toLowerCase().indexOf('.docx]') + 5),
+        link: v.substring(v.toLowerCase().indexOf('.docx]') + 7, v.toLowerCase().indexOf('.docx)') + 5),
+        content: v.substring(v.toLowerCase().indexOf('.docx)') + 6),
+        showContent: false,
+        type: 'docx'
+      };
+    }
+  });
+};
+
+const openDoc = (doc, i) => {
+  if (window.AppGlobalConfig.isDisabledSource) return;
+  var link = doc.link;
+  var type = doc.type;
+  pdfSrc.value = link.replace(
+    window.AppGlobalConfig.knowledgeDocUrl.replace(
+      '=policy&',
+      activeTab.value === 'paper' ? '=compose_paper_material_total&' : '=policy&'
+    ),
+    window.AppGlobalConfig.knowledgeDocUrlProxy.replace(
+      '=policy&',
+      activeTab.value === 'paper' ? '=compose_paper_material_total&' : '=policy&'
+    )
+  );
+  showDoc.value = true;
+  fileType.value = type;
+  pdfContent.value = doc.content;
+  pdfNum.value = i;
+};
+
+const closeDoc = () => {
+  showDoc.value = false;
+};
+
+watch(
+  () => showDoc.value,
+  (newVal) => {
+    // 发布打开关闭,在关闭文档的时候打开相关案例
+    PubsubService.publish('switch-relevant-cases-box', newVal);
+  }
+);
+
+const openUrl = (url) => {
+  window.open(url, '_blank');
+};
+
+const openDocByIndex = (ind, id) => {
+  showDoc.value = false;
+  let link = null;
+  if (id !== currentResponse.value.id) {
+    if (questHistories.value[id].docs[ind - 1].type === 'url') {
+      openUrl(questHistories.value[id].docs[ind - 1].link);
+      return;
+    }
+
+    link = questHistories.value[id].docs[ind - 1].link;
+    fileType.value = questHistories.value[id].docs[ind - 1].type;
+  } else {
+    if (currentResponse.value.docs[ind - 1].type === 'url') {
+      openUrl(currentResponse.value.docs[ind - 1].link);
+      return;
+    }
+    link = currentResponse.value.docs[ind - 1].link;
+    fileType.value = currentResponse.value.docs[ind - 1].type;
+    pdfContent.value = currentResponse.value.docs[ind - 1].content;
+    pdfNum.value = currentResponse.value.docs[ind - 1].num;
+  }
+  pdfNum.value = ind;
+  pdfSrc.value = link.replace(
+    window.AppGlobalConfig.knowledgeDocUrl.replace(
+      '=policy&',
+      activeTab.value === 'paper' ? '=compose_paper_material_total&' : '=policy&'
+    ),
+    window.AppGlobalConfig.knowledgeDocUrlProxy.replace(
+      '=policy&',
+      activeTab.value === 'paper' ? '=compose_paper_material_total&' : '=policy&'
+    )
+  );
+  showDoc.value = true;
+};
+window.openDocByIndex = openDocByIndex;
+const getNum = (str) => {
+  const matches = str.match(/\[\[(\d+)\]\]/);
+
+  if (matches) {
+    return matches[1]; // 输出: 2
+  } else {
+    return null;
+  }
+};
+
+// 埋点采集数据
+const collectQuestion = () => {
+  const { question, originAnswer, keywords = [] } = currentResponse.value;
+  const userStore = useUserStore();
+  const param = {
+    question,
+    answer: originAnswer,
+    questionType: askType.value === 'zcfg' ? '政策法规' : '土地市场',
+    keywords: Array.isArray(keywords) ? keywords.join(',') : keywords,
+    user: '游客',
+    userId: '-1'
+  };
+  if (userStore.isLogin) {
+    const { id = '-1', displayName = '游客' } = userStore?.user?.user || {};
+    param.user = displayName;
+    param.userId = id;
+  }
+
+  ManagerAPI.collect(param).then((res) => {
+    if (res.data) {
+      // 记录日志,用来反馈
+      currentResponse.value.logId = res.data;
+    }
+  });
+};
+
+const questions = ref([]);
+let recommendedQuestions = [];
+let recommendedQuestionsIndex = 0;
+const getRecommendedQuestion = async (b = false) => {
+  const { question } = currentResponse.value;
+  const myHeaders = new Headers();
+  myHeaders.append('Content-Type', 'application/json');
+  questions.value = [];
+  if (!b) {
+    recommendedQuestions = [];
+  }
+  recommendedQuestionsIndex = 0;
+  const raw = JSON.stringify({
+    query: `你会收到一个用户提问,请根据问题延伸出10个子问题。
+
+在你回答问题的时候,还需要注意推荐给用户的问题必须以列表的形式返回。
+for example:
+[
+    "1、南京市在推进产业用地高质量利用方面采取了哪些具体措施?这些措施的效果如何?",
+    "2、杭州市创新型产业用地管理的具体实施细则是什么?这些细则如何促进传统产业转型升级?",
+    "3、南京市和杭州市在土地供应方式上有哪些不同?这些不同如何影响各自的产业发展?",
+    "4、南京市如何通过政策支持和激励措施吸引重大投资项目?这些措施的实际效果如何?",
+    "5、杭州市“工业上楼”项目的实施情况如何?这一政策对提升土地利用效率有何影响?",
+]
+
+以下是用户提问:${question}`,
+    // model: 'qwen1.5-chat',
+    stream: false
+  });
+
+  const requestOptions = {
+    method: 'POST',
+    headers: myHeaders,
+    body: raw,
+    redirect: 'follow'
+  };
+
+  const rootUrl = dsChecked.value ? window.AppGlobalConfig.knowledgeServer : window.AppGlobalConfig.aiServer
+  fetch(rootUrl + '/chat/chat', requestOptions)
+    .then((response) => {
+      return response.json()
+    })
+    .then((msgStr) => {
+      const msg = JSON.parse(msgStr)
+      const str = msg.choices[0]?.message?.content;
+      if (str) {
+        const str1 = str.slice(str.indexOf("</think>") + 7)
+        recommendedQuestions = formatRecommendedQuestions(str1);
+        questions.value = recommendedQuestions.slice(0, 5);
+      }
+    })
+    .catch((error) => console.error(error));
+};
+const formatRecommendedQuestions = (str) => {
+  return str
+    .replace(/`|$$|$$|"|,/g, '')  // 清除引号、反引号、方括号和逗号
+    .split('\n')                  // 按换行分割
+    .map(line => line.trim())     // 去除首尾空格
+    .filter(line => {             // 双条件过滤
+      return (
+        line.length > 0 &&         // 过滤空行
+        /^\d+[、.]/.test(line)     // 匹配"1、"或"1."开头
+      )
+    })
+    .map(line => {
+      // 统一标点为中文顿号并保留末尾空格
+      return line
+        .replace(/^\d+\./, m => m.replace('.', '、')) // 英文标点转中文
+        .replace(/\s*$/, '   ')                       // 强制保留末尾3空格
+    });
+}
+const changeRecommendedQuestions = () => {
+  recommendedQuestionsIndex++;
+  if (recommendedQuestionsIndex % 2 === 0) {
+    questions.value = recommendedQuestions.slice(0, 5);
+  } else {
+    questions.value = recommendedQuestions.slice(5);
+    getRecommendedQuestion(true)
+  }
+};
+
+const getQuestionKeyWords = async () => {
+  const { question } = currentResponse.value;
+  const myHeaders = new Headers();
+  myHeaders.append('Content-Type', 'application/json');
+  questions.value = [];
+  const raw = JSON.stringify({
+    query: `请从以下文本中提取核心关键词,确保关键词简洁明了且准确反映文本的主要内容。 请按照以下格式输出:关键词:关键词1,关键词2。文本如下:“${question}”`,
+    stream: false
+  });
+
+  const requestOptions = {
+    method: 'POST',
+    headers: myHeaders,
+    body: raw,
+    redirect: 'follow'
+  };
+
+  const rootUrl = dsChecked.value ? window.AppGlobalConfig.knowledgeServer : window.AppGlobalConfig.aiServer
+  return fetch(rootUrl + '/chat/chat', requestOptions)
+    .then((response) => response.json())
+    .then((msgStr) => {
+      const msg = JSON.parse(msgStr)
+      activeIndex.value = 1;
+      const str = msg.choices[0]?.message?.content;
+      if (str) {
+        const str1 = str.slice(str.indexOf("</think>")+7)
+        const keywords = splitWords(str1).slice(0, 3);
+        currentResponse.value.keywords = keywords;
+      }
+    })
+    .catch((error) => console.error(error));
+};
+
+const splitWords = (word) => {
+  if (word) {
+    word = word
+      .replaceAll('核心词为:', '')
+      .replaceAll('问题核心词:', '')
+      .replaceAll('>', '')
+      .replaceAll('。', '')
+      .trim();
+
+    if (word.indexOf('\n\n') !== -1) {
+      return word.split('\n\n');
+    }
+
+    if (word.indexOf(',') !== -1) {
+      return word.split(',');
+    }
+
+    if (word.indexOf('、') !== -1) {
+      return word.split('、');
+    }
+
+    if (word.indexOf(',') !== -1) {
+      return word.split(',');
+    }
+
+    return [word];
+  }
+
+  return [];
+};
+
+const exportAnswer = (type) => {
+  const { question, originAnswer } = currentResponse.value;
+  CommonAPI.answerExport(type, { question, answer: originAnswer });
+};
+
+const copy = async () => {
+  try {
+    const input = document.createElement('input');
+    input.value = currentResponse.value.msg;
+    document.body.appendChild(input);
+    input.select();
+    document.execCommand('copy');
+    document.body.removeChild(input);
+
+    message.info('内容已复制到剪贴板');
+  } catch (err) {
+    console.error(err);
+    // console.error('复制到剪贴板失败', err);
+  }
+};
+
+const changeActiveTab = (tab) => {
+  activeTab.value = tab;
+};
+
+const stopAI = () => {
+  if (ctr) {
+    ctr.abort();
+  }
+};
+
+const openRecommendedQuestion = (q) => {
+  if (q) {
+    if (q.charAt(1) === '、') {
+      q = q.substring(2);
+    }
+    window.open(
+      `/aisearch/#/ai-search?q=${encodeURI(q.trim())}&scope=${activeTab.value}&type=${answerType.value}`,
+      '_blank'
+    );
+  }
+};
+onMounted(() => {
+  const { query } = router.currentRoute.value;
+  if (query.q) {
+    answerType.value = query.type || '0';
+    scope.value = query.scope || 'net';
+    var ds = '0';
+    if (!useUserStore().isLogin) {
+      ds = query.ds;
+    } else {
+      ds = useUserStore().user.user.enableDeepseek
+        ? useUserStore().user.user.enableDeepseek + ''
+        : query.ds;
+    }
+    dsHintTxt.value = '';
+    if (ds && ds == '1') {
+      ds = true;
+      dsLoading.value = true;
+    } else {
+      ds = false;
+      dsLoading.value = false;
+    }
+    dsChecked.value = ds;
+    activeTab.value = scope.value;
+    ask(decodeURIComponent(query.q));
+  }
+
+});
+
+defineExpose({ search, changeActiveTab, stopAI });
+</script>
+<style scoped lang="scss">
+@import 'src/assets/scss/variables';
+.ai-search-detail {
+  width: 100%;
+  background: $background_color;
+  height: 100%;
+
+
+  .search-panel {
+    width: 100%;
+    height: calc(100% - 60px - 50px);
+    display: flex;
+    margin-top: 50px;
+    padding-bottom: 50px;
+    .search-detail {
+      width: calc(52%);
+      background: #ffffff;
+      margin-left: 52px;
+
+      &-tdsc {
+        width: calc(52%);
+      }
+
+      .search-result {
+        height: calc(100% - 40px);
+
+
+        .result-panel {
+          width: 100%;
+          background: #fff;
+          padding: 29px 0;
+
+
+          .result {
+            width: 100%;
+
+            .tabs {
+              display: flex;
+              border-bottom: 1px solid #ccc;
+              .ds-box {
+                display: flex;
+                height: 36px;
+                align-items: center;
+                border: 1px solid #E3E4E4;
+                background: #f0f0f0;
+                border-radius: 5px;
+                overflow: hidden;
+                >div {
+                  height: 100%;
+                  display: flex;
+                  align-items: center;
+                  justify-content: center;
+                  width: 108px;
+                  cursor: pointer;
+                  &.active {
+                    background: #fff;
+                    &:nth-child(1) {
+                      .iconfont {
+                        color: #4f6bfe;
+                      }
+                    }
+                    &:nth-child(2) {
+                      .iconfont {
+                        color: #605BEC;
+                      }
+                    }
+                  }
+                  .iconfont {
+                    color: #898D93;
+                    margin-top: 2px;
+                  }
+                  &:nth-child(1) {
+                    .iconfont {
+                      font-size: 16px;
+                    }
+                  }
+                  &:nth-child(2) {
+                    .iconfont {
+                      font-size: 22px;
+                    }
+                  }
+                }
+                .sw {
+                  :deep(.ant-switch) {
+                    background-color: #eaeaea;
+                  }
+                  :deep(.ant-switch-handle::before) {
+                    background-color: #fff;
+                  }
+                }
+                .sw-checked {
+                  :deep(.ant-switch-checked) {
+                    background-color: #fff;
+                  }
+                  :deep(.ant-switch-handle::before) {
+                    background-color: #1890ff;
+                  }
+                }
+              }
+              .tab {
+                font-family: PingFang SC, PingFang SC;
+                font-weight: 400;
+                font-size: 24px;
+                color: #212121;
+                text-align: left;
+                font-style: normal;
+                text-transform: none;
+                margin-right: 28px;
+                display: flex;
+                justify-content: center;
+                flex-direction: column;
+                align-items: center;
+                cursor: pointer;
+
+                .title {
+                  margin-bottom: 9px;
+                }
+
+                .bottom {
+                  height: 0;
+                  margin-top: 10px;
+                }
+
+                &-active,
+                &:hover {
+                  font-family: PingFang SC, PingFang SC;
+                  font-weight: 400;
+                  font-size: 24px;
+                  color: #1586fa;
+                  text-align: left;
+                  font-style: normal;
+                  text-transform: none;
+
+                  .bottom {
+                    width: 100%;
+                    height: 4px;
+                    background: #3987F4;
+                    border-radius: 2px;
+                  }
+                }
+              }
+            }
+
+            .result-view {
+              width: 100%;
+              height: 50%;
+              overflow-y: auto;
+
+              .q-r {
+                padding: 20px;
+                line-height: 30px;
+                background: #f5f8fa;
+                border-radius: 4px 4px 4px 4px;
+                margin: 20px 0;
+                .ds-content-box {
+                  display: flex;
+                  .icon {
+                    width: 26px;
+                    height: 26px;
+                    display: flex;
+                    align-items: center;
+                    justify-content: center;
+                    border-radius: 50%;
+                    background: white;
+                    border: 1px solid #117ff9;
+                    img {
+                      width: 18px;
+                      height: 18px;
+                    }
+                  }
+                  .ds-panel {
+                    margin-top: -5px;
+                    margin-left: 10px;
+                    width: calc(100% - 40px);
+                    .ds-loading {
+                      display: flex;
+                      align-items: center;
+                      .icon-arrow {
+                        padding-left: 5px;
+                        cursor: pointer;
+                      }
+                    }
+                    .rotate {
+                      transform: rotate(180deg);
+                    }
+                  }
+                  .ds-con {
+                    border-left: 1px solid #ccc;
+                    padding-left: 10px;
+                  }
+                }
+              }
+
+              .more-questions {
+                .title {
+                  display: flex;
+                  margin-bottom: 14px;
+
+                  .change-title {
+                    margin-left: 20px;
+                    display: flex;
+                    cursor: pointer;
+                    font-size: 13px;
+                    color: #3c3c3c;
+
+                    .change-icon {
+                      width: 16px;
+                      height: 16px;
+                      margin-right: 4px;
+                      background: url('@/assets/images/ai-search/icon-chang-question.png') no-repeat;
+                      background-size: 100% 100%;
+                    }
+                  }
+                }
+              }
+
+              .source {
+                width: 100%;
+
+                .title {
+                  display: flex;
+                  align-items: center;
+                  font-family: PingFang SC;
+                  font-weight: bold;
+                  font-size: 20px;
+                  color: #333333;
+                  line-height: 30px;
+
+                  .icon {
+                    width: 20px;
+                    height: 20px;
+                    margin-right: 8px;
+                    background: url('@/assets/images/ai-search/source-icon.png') no-repeat;
+                    background-size: 100% 100%;
+                  }
+
+                  .text {
+                    font-family: Alibaba PuHuiTi 3, Alibaba PuHuiTi 30;
+                    font-weight: normal;
+                    //font-size: 16px;
+                    color: #404557;
+                    line-height: 30px;
+                    text-align: left;
+                    font-style: normal;
+                    text-transform: none;
+                    margin-right: 5px;
+                  }
+                }
+
+                .items {
+                  .item {
+                    width: calc(100% - 10px);
+                    min-height: 82px;
+                    background: rgba(255, 255, 255, 0.5);
+                    border-radius: 10px;
+                    display: flex;
+                    flex-direction: column;
+                    align-items: center;
+                    padding: 20px;
+                    color: #888;
+                    display: flex;
+                    line-height: 25px;
+                    margin-bottom: 20px;
+                    margin: 12px 0;
+                    background: #f1f9ff;
+                    border-radius: 8px 8px 8px 8px;
+
+                    .doc {
+                      display: flex;
+                      width: 100%;
+
+                      p {
+                        width: calc(100% - 25px);
+
+                        .doc-link {
+                          cursor: pointer;
+                          color: #3987F4;
+
+                          &:hover {
+                            font-weight: bold;
+                          }
+                        }
+                      }
+
+                      .doc-icon {
+                        width: 20px;
+                        height: 20px;
+                        margin-left: 5px;
+                        cursor: pointer;
+
+                        &-show {
+                          background: url('/images/zcbd/ai-search/icon-doc-show.png') no-repeat;
+                          background-size: 100% 100%;
+                        }
+
+                        &-hide {
+                          background: url('/images/zcbd/ai-search/icon-doc-hide.png') no-repeat;
+                          background-size: 100% 100%;
+                        }
+                      }
+                    }
+
+                    .content {
+                      //white-space: pre-line;
+                      width: 100%;
+                      font-family: Alibaba PuHuiTi 3, Alibaba PuHuiTi 30;
+                      font-weight: normal;
+                      font-size: 14px;
+                      color: #757575;
+                      line-height: 28px;
+                      text-align: left;
+                      font-style: normal;
+                      text-transform: none;
+
+                      &-hide {
+                        display: none;
+                      }
+                    }
+                  }
+
+                  .item-url {
+                    margin: 12px 0;
+                    background: #f1f9ff;
+                    border-radius: 8px 8px 8px 8px;
+
+                    .bottom {
+                      width: 100%;
+
+                      .title-icon {
+                        display: flex;
+                        justify-content: left;
+                        align-items: center;
+                        width: 90%;
+                        float: left;
+
+                        .icon {
+                          width: 23px;
+                          height: 23px;
+                          background: url('/images/zcbd/ai-search/icon-net-url.png') no-repeat;
+                          background-size: 100% 100%;
+                        }
+
+                        .title {
+                          color: #a9a3a3cc;
+                          font-size: 16px;
+                          margin-left: 10px;
+                        }
+                      }
+
+                      .index {
+                        float: right;
+                        width: 20px;
+                        height: 20px;
+                        font-size: 12px;
+                        line-height: 20px;
+                        text-align: center;
+                        margin: 0 5px;
+                        border-radius: 10px;
+                        background: #d0d5dd;
+                        color: #000;
+                      }
+                    }
+                  }
+                }
+              }
+            }
+
+            // 土地市场
+            .map-answer {
+              position: relative;
+              width: 100%;
+              min-height: 600px;
+
+              .left-panel {
+                width: 100%;
+                background: #ffffff;
+                z-index: 1000;
+                //position: relative;
+                left: 12px;
+                top: 15px;
+                border-radius: 10px;
+
+                .title {
+                  width: 100%;
+                  height: 30px;
+                  text-align: left;
+                  line-height: 30px;
+                  font-size: 18px;
+                  font-weight: bolder;
+                }
+
+                .content {
+                  width: 100%;
+                  height: auto;
+
+                  .chart-card {
+                    width: 100%;
+                    height: 330px;
+
+                    .chart-title {
+                      width: 100%;
+                      height: 30px;
+                      font-size: 18px;
+                      font-weight: bolder;
+                      padding: 0 5px;
+                      line-height: 30px;
+                      margin: 10px 0;
+                    }
+
+                    .chart {
+                      width: 100%;
+                      height: 300px;
+                    }
+
+                    //background: #0a84ff;
+                  }
+
+                  .summary-card {
+                    width: 100%;
+                    height: 50%;
+
+                    .summary-title {
+                      width: 100%;
+                      height: 30px;
+                      font-size: 18px;
+                      font-weight: bolder;
+                      padding: 0 5px;
+                      line-height: 30px;
+                      margin: 10px 0;
+                    }
+
+                    .summary-content {
+                      width: calc(100% - 20px);
+                      height: calc(100% - 30px);
+                      padding: 0;
+                      white-space: pre-wrap;
+                      overflow-y: auto;
+                      border-radius: 10px;
+                      margin: 10px;
+                      font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Noto Sans, Helvetica,
+                        Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji';
+                      font-size: 16px;
+                      line-height: 1.5;
+                    }
+                  }
+                }
+              }
+
+              .map-panel {
+                width: 100%;
+                height: 630px;
+
+                .map-title {
+                  width: 100%;
+                  height: 30px;
+                  font-size: 18px;
+                  font-weight: bolder;
+                  padding: 0 5px;
+                  line-height: 30px;
+                  margin: 10px 0;
+                }
+
+                .map {
+                  width: 100%;
+                  height: 600px;
+                }
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+
+  ::-webkit-scrollbar {
+    width: 8px;
+  }
+
+  ::-webkit-scrollbar-thumb {
+    background-color: rgb(0 0 0 / 10%);
+    border-radius: 10px;
+  }
+}
+
+::v-deep .input-modal {
+  .modal-que {
+    background: #0e91fd;
+
+    textarea {
+      width: 100%;
+      height: 257px !important;
+      border: none;
+      border-radius: 10px;
+      resize: none;
+      padding: 10px;
+      background: transparent;
+      font-size: 19px;
+
+      &:focus-visible {
+        border: none !important;
+        outline: none;
+      }
+    }
+  }
+}
+</style>
+

+ 0 - 2
ais_search_zj/web/src/views/home/components/HomeHeader.vue

@@ -18,8 +18,6 @@ import { useUserStore } from '@/stores';
 import { computed } from 'vue';
 
 const store = useUserStore();
-console.log("store---------------------------------")
-console.log(store.user)
 const syUser = computed(() => store.user.syUser);
 const systemTitle = ref(AppGlobalConfig.system.name)
 const props = defineProps({

+ 15 - 2
ais_search_zj/web/src/views/home/index.vue

@@ -8,7 +8,7 @@
         <div class="tool">
           <ul>
             <template v-for="(menu,index) in menuRoutes" :key="index">
-              <li  v-if="index < 3" :class="{active: cMenuRoute['title'] === menu['title']}" @click="toSwitchPage(menu)">
+              <li  v-if="index < 4" :class="{active: cMenuRoute['title'] === menu['title']}" @click="toSwitchPage(menu)">
                 <span :class="`iconfont ${menu['icon']}`"></span>
                 <span class="title">{{menu['title']}}</span>
               </li>
@@ -38,17 +38,30 @@ const menuRoutes = [
     icon: 'icon-a-lujing11029',
     name: 'Znxz',
     iframe: true
-  },{
+  },
+  {
     title: '智能找图',
     icon: 'icon-map',
     name: 'Znzt',
     window: true
   },
+  {
+    title: '找图找数',
+    icon: 'icon-zhengcefagui',
+    name: 'Ztzs',
+    iframe: true
+  },
   {
     title: '知识库',
     icon: 'icon-66zhishikuguanli',
     name: 'Document',
     iframe: false
+  },
+  {
+    title: 'AI搜索2',
+    icon: 'icon-66zhishikuguanli',
+    name: 'Home2',
+    iframe: false
   }
 ]
 const { name } = useRoute();

+ 19 - 0
ais_search_zj/web/src/views/ztzs/index.vue

@@ -0,0 +1,19 @@
+<template>
+  <div class="znxz_box">
+    <iframe border="0" frameborder="no" src='https://ai.zrzyt.zj.gov.cn/znxz/#/ztzs' />
+  </div>
+</template>
+
+<script lang="ts">
+
+</script>
+<style lang="scss" scoped>
+.znxz_box {
+  width: 100%;
+  height: 100%;
+  >iframe {
+    width: 100%;
+    height: 100%;
+  }
+}
+</style>