ai-search.vue 82 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692
  1. <template>
  2. <div class="ai-search-detail">
  3. <a-affix>
  4. <div class="header">
  5. <home-header @login="emits('login')" sub-title="自然资源大模型" />
  6. </div>
  7. </a-affix>
  8. <div class="search-panel" id="pageContainer">
  9. <a-affix
  10. ref="markdownTocAffixRef"
  11. :style="`width: 20%;margin-left: 13%;max-height: ${tocHeight}px;`"
  12. :offset-top="110"
  13. v-show="!showDoc"
  14. >
  15. <markdown-toc :no-data="activeTab === 'original'" :toc="tocs" :class="`markdown-toc-${askType}`" />
  16. </a-affix>
  17. <div :class="`search-detail search-detail-${askType}`" ref="searchRef">
  18. <div
  19. class="search-result"
  20. id="searchResult"
  21. style="overflow-x: hidden; height: auto"
  22. ref="messageContainer"
  23. >
  24. <div class="top">
  25. <div class="title">
  26. <div class="icon"></div>
  27. <div class="question" style="cursor: pointer" @click="openAskModal">
  28. {{ question }}
  29. </div>
  30. <div v-show="askType === 'tdsc'" class="graph-switch">
  31. <a-switch v-model:checked="graph" @change="questLandMark" />
  32. 生成图表
  33. </div>
  34. </div>
  35. </div>
  36. <div :class="`result-panel result-panel-${askType}`">
  37. <div class="search-steps" v-show="activeIndex !== -1 && activeIndex < 5">
  38. <div
  39. v-show="st.key <= activeIndex"
  40. :class="`step ${st.key < activeIndex ? 'step-done' : ''}`"
  41. v-for="(st, i) in steps"
  42. :key="`step-${i}`"
  43. >
  44. <div class="info">
  45. <div class="icon"></div>
  46. <div class="title">{{ st.name }}</div>
  47. <div class="tags">
  48. <div
  49. v-if="st.key === 2 && askType === 'tdsc'"
  50. style="width: 20%; font-size: 14px; line-height: 22px"
  51. class="tag-last"
  52. >
  53. {{ st.agent }}
  54. </div>
  55. <div
  56. :class="`tag${st.key === 0 ? ' tag-first' : ''}`"
  57. v-if="st.key < 3 && askType !== 'tdsc'"
  58. :title="t"
  59. v-for="(t, i) in st.tags"
  60. :key="`tag-${i}`"
  61. >
  62. {{ t }}
  63. </div>
  64. <div
  65. class="d"
  66. style="
  67. width: 80%;
  68. overflow: hidden;
  69. animation: scroll-text-992e3d76 25s linear infinite;
  70. "
  71. v-if="st.key < 3 && askType === 'tdsc'"
  72. >
  73. <div
  74. :title="t"
  75. :class="`tag${askType === 'tdsc' ? ' tag-rol' : ''}`"
  76. v-for="(t, i) in st.tags"
  77. :key="`tag-${i}`"
  78. >
  79. <div>{{ t }}</div>
  80. </div>
  81. </div>
  82. </div>
  83. </div>
  84. <div class="progress">
  85. <a-progress size="small" :show-info="false" :percent="st.prog" />
  86. </div>
  87. </div>
  88. </div>
  89. <div class="result">
  90. <template v-if="askType === 'zcfg'">
  91. <div class="tabs" style="position: relative">
  92. <div
  93. :class="`tab${activeTab === t.key ? ' tab-active' : ''}`"
  94. v-for="t in tabs"
  95. :key="t.key"
  96. @click="changeTab(t.key)"
  97. >
  98. <div class="title">{{ t.name }}</div>
  99. <div class="bottom"></div>
  100. </div>
  101. <div
  102. class="scope-type"
  103. style="
  104. display: flex;
  105. align-items: center;
  106. position: absolute;
  107. right: 5px;
  108. top: 10px;
  109. "
  110. >
  111. <div class="label">搜索模式:</div>
  112. <a-radio-group
  113. v-model:value="answerType"
  114. @change="changeAnswerType"
  115. name="radioGroup"
  116. >
  117. <a-radio value="0">简洁</a-radio>
  118. <a-radio value="1">深入</a-radio>
  119. <a-radio value="2">研究</a-radio>
  120. </a-radio-group>
  121. <div class="ds-box">
  122. <div @click="onChange('1')" :class="{active: dsChecked === true}">
  123. <i class="iconfont icon-deepsee"></i>
  124. </div>
  125. <div @click="onChange('0')" :class="{active: dsChecked === false}">
  126. <i class="iconfont icon-tongyi"></i>
  127. </div>
  128. </div>
  129. </div>
  130. </div>
  131. <div class="result-view">
  132. <div class="q-r">
  133. <div class="ds-content-box" v-if="dsChecked">
  134. <div class="icon">
  135. <img src="/images/icon-ds.png" />
  136. </div>
  137. <div class="ds-panel">
  138. <div class="ds-loading" v-if="dsHintTxt" @click="dsUp = !dsUp">
  139. {{ dsHintTxt }}
  140. <DownOutlined class="icon-arrow" :class="{ rotate: dsUp }" />
  141. </div>
  142. <div class="ds-con" v-if="!dsUp">
  143. <vue-markdown-it
  144. id="dsMarkdown"
  145. :source="
  146. currentResponse.streamMock
  147. ? currentResponse.streamMsg.indexOf('###') > -1
  148. ? currentResponse.streamMsg.substring(
  149. 0,
  150. currentResponse.streamMsg.indexOf('###')
  151. )
  152. : currentResponse.streamMsg
  153. : currentResponse.msg.indexOf('###') > -1
  154. ? currentResponse.msg.substring(
  155. 0,
  156. currentResponse.msg.indexOf('###')
  157. )
  158. : currentResponse.msg
  159. "
  160. :options="{
  161. html: true,
  162. linkify: true
  163. }"
  164. />
  165. </div>
  166. <a-spin :indicator="indicator" v-if="dsLoading" />
  167. </div>
  168. </div>
  169. <vue-markdown-it
  170. id="resMarkdown"
  171. :source="
  172. currentResponse.streamMock
  173. ? currentResponse.streamMsg.indexOf('###') > -1
  174. ? currentResponse.streamMsg.substring(
  175. currentResponse.streamMsg.indexOf('###')
  176. )
  177. : ''
  178. : dsChecked
  179. ? currentResponse.msg.indexOf('###') > -1
  180. ? currentResponse.msg.substring(currentResponse.msg.indexOf('###'))
  181. : ''
  182. : currentResponse.msg
  183. "
  184. :options="{
  185. html: true,
  186. linkify: true
  187. }"
  188. />
  189. </div>
  190. <a-skeleton
  191. active
  192. v-if="!dsLoading && (activeIndex < 2 || !currentResponse.msg)"
  193. />
  194. <div v-if="activeIndex >= 5" class="follow">
  195. <div class="follow-btn" style="cursor: pointer">
  196. <!-- <a-button v-show="activeTab === 'knowledge'" style="margin-right: 10px;float: left" type="link" color="green" size="small" @click="reAnswer">-->
  197. <a-button
  198. style="
  199. margin-right: 35px;
  200. float: left;
  201. font-family: Microsoft YaHei;
  202. font-weight: 400;
  203. font-size: 16px;
  204. color: #465d7c;
  205. "
  206. type="link"
  207. color="green"
  208. size="small"
  209. @click="changeTab(activeTab)"
  210. >
  211. 重新生成
  212. </a-button>
  213. <div class="eval-card" style="float: left">
  214. <div
  215. :class="`eval-icon eval-icon-like${evaluate === 'like' ? '-s' : ''}`"
  216. @click="evalResponse('like')"
  217. ></div>
  218. <a-divider type="vertical" />
  219. <div
  220. :class="`eval-icon eval-icon-dislike${
  221. evaluate === 'dislike' ? '-s' : ''
  222. }`"
  223. @click="evalResponse('dislike')"
  224. ></div>
  225. <a-divider type="vertical" />
  226. <a-dropdown :trigger="['click']">
  227. <a class="ant-dropdown-link" @click.prevent>
  228. <div :class="`eval-icon eval-icon-copy`"></div>
  229. </a>
  230. <template #overlay>
  231. <a-menu>
  232. <a-menu-item key="0" @click="copy()"> 复制到剪贴板</a-menu-item>
  233. <a-menu-divider />
  234. <a-menu-item key="1" @click="exportAnswer('doc')">
  235. 导出 Word
  236. </a-menu-item>
  237. <a-menu-divider />
  238. <a-menu-item key="2" @click="exportAnswer('pdf')">
  239. 导出 PDF
  240. </a-menu-item>
  241. </a-menu>
  242. </template>
  243. </a-dropdown>
  244. </div>
  245. </div>
  246. <div class="follow-input" v-show="followVisible">
  247. <a-textarea
  248. v-model:value="followQuestion"
  249. class="input-text"
  250. v-model="followQuestion"
  251. placeholder="继续提问"
  252. />
  253. <div class="button" @click="followAsk">发送</div>
  254. </div>
  255. </div>
  256. <!-- <div class="more-questions" v-if="activeTab === 'knowledge' && activeIndex >= 4">-->
  257. <div class="more-questions" v-if="activeIndex >= 4">
  258. <div class="title" style="margin-bottom: 14px">
  259. 你可以继续问我:
  260. <div class="change-title" @click="changeRecommendedQuestions">
  261. <div class="change-icon"></div>
  262. 换一批
  263. </div>
  264. </div>
  265. <div class="questions" style="cursor: pointer">
  266. <div
  267. class="question"
  268. @click="openRecommendedQuestion(q)"
  269. v-for="(q, i) in questions"
  270. :key="i"
  271. style="
  272. line-height: 14px;
  273. text-align: left;
  274. font-style: normal;
  275. text-transform: none;
  276. width: fit-content;
  277. margin-bottom: 12px;
  278. background: #ffffff;
  279. box-shadow: 0px 1px 8px 0px #e4e4e4;
  280. border-radius: 10px;
  281. border: 1px solid #e9e9e9;
  282. padding: 16px;
  283. font-family: PingFang SC;
  284. font-weight: 500;
  285. font-size: 14px;
  286. color: #2185f2;
  287. "
  288. >
  289. {{ q }}
  290. </div>
  291. </div>
  292. </div>
  293. <div class="source" v-if="activeTab !== 'original'" v-show="activeIndex === 5">
  294. <div class="title">
  295. <div class="icon"></div>
  296. <div class="text">来源</div>
  297. <div class="num">({{ currentResponse.docs.length }})</div>
  298. </div>
  299. <div class="items">
  300. <div
  301. class="item"
  302. v-if="activeTab !== 'net'"
  303. v-for="(doc, i) in currentResponse.docs"
  304. :key="'doc-' + i"
  305. >
  306. <div class="doc">
  307. <p>
  308. <span
  309. class="ma"
  310. style="font-weight: bolder; color: #000000; margin-right: 10px"
  311. >出处 [{{ i + 1 }}] </span
  312. ><span class="doc-link" @click="openDoc(doc, i)">{{ doc.doc }}</span>
  313. </p>
  314. <div
  315. :class="`doc-icon${
  316. !doc.showContent ? ' doc-icon-show' : ' doc-icon-hide'
  317. }`"
  318. @click="doc.showContent = !doc.showContent"
  319. ></div>
  320. </div>
  321. <div :class="`content${doc.showContent ? '' : ' content-hide'}`">
  322. <p>{{ doc.content }}</p>
  323. </div>
  324. </div>
  325. <div
  326. class="item item-url"
  327. v-if="activeTab === 'net'"
  328. v-for="(doc, i) in currentResponse.docs"
  329. :key="'doc-' + i"
  330. >
  331. <div class="doc">
  332. <p>
  333. <span class="doc-link" @click="openUrl(doc.link)">{{ doc.title }}</span>
  334. </p>
  335. </div>
  336. <div class="bottom">
  337. <div class="title-icon">
  338. <div class="icon"></div>
  339. <div class="title">{{ doc.doc }}</div>
  340. </div>
  341. <div class="index">{{ i + 1 }}</div>
  342. </div>
  343. </div>
  344. </div>
  345. </div>
  346. </div>
  347. </template>
  348. <template v-else>
  349. <div v-if="activeIndex >= 1" class="map-answer">
  350. <div class="left-panel">
  351. <div class="content">
  352. <div class="summary-card">
  353. <div class="summary-title" id="tdscSummaryTitle">总结</div>
  354. <div class="summary-content">
  355. <a-skeleton active v-if="!currentResponse.msg" />
  356. <vue-markdown-it
  357. :source="
  358. currentResponse.streamMock
  359. ? currentResponse.streamMsg
  360. : currentResponse.msg
  361. "
  362. :options="{
  363. html: true,
  364. linkify: true
  365. }"
  366. />
  367. <!-- {{currentResponse.msg}}-->
  368. </div>
  369. </div>
  370. <div class="chart-card" v-if="currentResponse.hasChart" id="tdscChartCard">
  371. <div class="chart-title">生成图表</div>
  372. <a-skeleton
  373. active
  374. style="height: 100%"
  375. v-if="!currentResponse.chartOption"
  376. />
  377. <div v-else class="chart" id="summaryChart"></div>
  378. </div>
  379. </div>
  380. </div>
  381. <!-- <div class="map-panel" id="tdscMapPanel">-->
  382. <!-- <div class="map-title">空间位置</div>-->
  383. <!-- <div class="map">-->
  384. <!-- <a-i-map />-->
  385. <!-- </div>-->
  386. <!-- </div>-->
  387. </div>
  388. </template>
  389. </div>
  390. </div>
  391. </div>
  392. <!-- 历史回答 -->
  393. <div
  394. v-show="askType === 'zcfg'"
  395. class="search-result result-history"
  396. style="overflow-y: hidden; height: auto"
  397. v-if="questHistories.length > 0"
  398. id="messageContainer"
  399. >
  400. <template v-for="(qh, i) in [...questHistories].reverse()" :key="`qh-${i}`">
  401. <div class="top">
  402. <div class="title">
  403. <div class="icon"></div>
  404. <div class="question" style="cursor: pointer">{{ qh.question }}</div>
  405. </div>
  406. </div>
  407. <div class="result-panel">
  408. <div class="result">
  409. <div class="tabs">
  410. <div :class="`tab tab-active`">
  411. <div class="title">回答</div>
  412. <div class="bottom"></div>
  413. </div>
  414. </div>
  415. </div>
  416. <div class="result-view">
  417. <div class="q-r">
  418. <vue-markdown-it
  419. :source="qh.msg"
  420. :toc="true"
  421. :options="{
  422. html: true,
  423. linkify: true
  424. }"
  425. />
  426. </div>
  427. </div>
  428. </div>
  429. </template>
  430. </div>
  431. </div>
  432. <a-affix v-if="showDoc" style="width: 52%; font-size: 16px" :offset-top="210">
  433. <div
  434. class="doc"
  435. style="width: 100%; background-color: white; height: calc(100vh - 190px); overflow: auto"
  436. >
  437. <p-d-f-viewer
  438. v-if="fileType === 'pdf'"
  439. :src="pdfSrc"
  440. @close="closeDoc"
  441. :content="pdfContent"
  442. :num="pdfNum"
  443. />
  444. <word-viewer
  445. v-if="fileType === 'docx'"
  446. :src="pdfSrc"
  447. @close="closeDoc"
  448. :content="pdfContent"
  449. :num="pdfNum"
  450. >
  451. </word-viewer>
  452. <txt-viewer v-if="fileType === 'txt'" :src="pdfSrc" @close="closeDoc" :txt="pdfContent" />
  453. </div>
  454. </a-affix>
  455. </div>
  456. <a-modal force-render class="input-modal" v-model:open="open" :closable="false" width="700px">
  457. <template #footer>
  458. <div class="footer" style="height: 26px; margin: 0 auto">
  459. <div class="left" style="float: left; cursor: pointer" @click="clearModalTextArea">
  460. 清空
  461. </div>
  462. <a-button style="float: right" type="primary" size="small" @click="sendModalTextArea">
  463. 发送
  464. </a-button>
  465. </div>
  466. </template>
  467. <div class="modal-que">
  468. <ai-textarea :q="question" ref="aiTextAreaRef" @enter="ask" />
  469. </div>
  470. </a-modal>
  471. </div>
  472. </template>
  473. <script setup>
  474. import { fetchEventSource } from '@microsoft/fetch-event-source';
  475. import PDFViewer from '@/components/pdf/PdfCanvas.vue';
  476. import WordViewer from '@/components/pdf/WordViewer.vue';
  477. import AiTextarea from '@/components/TextArea/AiTextarea.vue';
  478. import { VueMarkdownIt } from '@f3ve/vue-markdown-it';
  479. import TxtViewer from '@/components/pdf/TxtViewer.vue';
  480. import { message } from 'ant-design-vue';
  481. import { ref, defineProps, watch } from 'vue';
  482. import CommonAPI from '@/api/common';
  483. import ManagerAPI from '@/api/manager';
  484. import AIMap from '@/components/Map/AIMap.vue';
  485. import * as echarts from 'echarts';
  486. import PubsubService from '@/utils/PubsubService';
  487. import MarkdownToc from '@/components/markdown-toc/MarkdownToc.vue';
  488. import HomeHeader from '@/views/home/components/HomeHeader.vue';
  489. import { useUserStore } from '@/stores';
  490. const { user: u = {}, updateUser } = useUserStore();
  491. const { user = {}, token } = u || {};
  492. import UserAPI from '@/api/user';
  493. const aiLoading = ref(false);
  494. const router = useRouter();
  495. const markdownTocAffixRef = ref(null);
  496. const tocHeight = ref(530);
  497. import { LoadingOutlined, DownOutlined } from '@ant-design/icons-vue';
  498. import { h } from 'vue';
  499. const indicator = h(LoadingOutlined, {
  500. style: {
  501. fontSize: '24px'
  502. },
  503. spin: true
  504. });
  505. const props = defineProps({
  506. searchType: '',
  507. askType: {
  508. type: String,
  509. default: 'zcfg'
  510. }
  511. });
  512. const answerType = ref('0');
  513. const dsChecked = ref(true);
  514. const scope = ref('net');
  515. watch(
  516. () => router.currentRoute.value,
  517. (value) => {
  518. const { query } = value;
  519. if (query.q) {
  520. ask(decodeURIComponent(query.q));
  521. answerType.value = query.type || '0';
  522. scope.value = query.scope || 'net';
  523. dsHintTxt.value = '';
  524. }
  525. }
  526. );
  527. watch(
  528. () => router.currentRoute.value,
  529. (value) => {
  530. const { query } = value;
  531. if (query.q) {
  532. var ds = query.ds;
  533. if (ds && ds == '1') {
  534. ds = true;
  535. } else {
  536. ds = false;
  537. }
  538. dsChecked.value = ds;
  539. }
  540. },
  541. {
  542. immediate: true
  543. }
  544. );
  545. watch(props.searchType, (val) => {
  546. activeTab.value = val;
  547. });
  548. const question = ref('国有土地的使用方式有哪些?');
  549. const followQuestion = ref('');
  550. const statusText = ref('检索中');
  551. const activeIndex = ref(0);
  552. const activeTab = ref(props.searchType);
  553. let ctr = null;
  554. const aiTextAreaRef = ref(null);
  555. const searchRef = ref(null);
  556. const evaluate = ref(null);
  557. const scrollToBottom = () => {
  558. const messageContainer = document.getElementById('messageContainer');
  559. if (messageContainer && searchRef.value) {
  560. }
  561. };
  562. const clearModalTextArea = () => {
  563. aiTextAreaRef.value.clear();
  564. };
  565. const sendModalTextArea = () => {
  566. aiTextAreaRef.value.send();
  567. };
  568. const tocs = ref([]);
  569. const open = ref(false);
  570. const showDoc = ref(false);
  571. const pdfSrc = ref('');
  572. const pdfContent = ref('');
  573. const pdfNum = ref(1);
  574. const fileType = ref('pdf');
  575. const dsLoading = ref(true);
  576. const startTime = ref(0);
  577. const endTime = ref(0);
  578. const dsHintTxt = ref('');
  579. const dsUp = ref(false);
  580. const times = ref(0);
  581. const timers = ref([]);
  582. const currentResponse = ref({
  583. streamMsg: '',
  584. streamMock: false,
  585. msg: '',
  586. originAnswer: '',
  587. docs: []
  588. });
  589. let streamMockInterval = null;
  590. const streamToAnswer = () => {
  591. console.log('streamToAnswer');
  592. currentResponse.value.index = 0;
  593. streamMockInterval = setInterval(() => {
  594. const { originAnswer = '', msg, streamMsg = '', id, index = 0 } = currentResponse.value;
  595. if (currentResponse.value.mockStart || index <= originAnswer.length) {
  596. currentResponse.value.streamMsg = originAnswer.substr(0, index + 2).replaceAll('\n', ' \n');
  597. if (originAnswer) {
  598. currentResponse.value.index += 2;
  599. }
  600. let num = getNum(currentResponse.value.streamMsg);
  601. while (num) {
  602. const docsNum = currentResponse.value.docs.length;
  603. if (docsNum && num > docsNum + 1) {
  604. }
  605. currentResponse.value.streamMsg = currentResponse.value.streamMsg.replace(
  606. `[[${num}]]`,
  607. `<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;
  608. height: 20px;
  609. background: #FFFFFF;
  610. border-radius: 4px 4px 4px 4px;
  611. border: 1px solid #BACAE3;">${num}</span>`
  612. );
  613. num = getNum(currentResponse.value.streamMsg);
  614. }
  615. } else {
  616. if (streamMockInterval) {
  617. clearInterval(streamMockInterval);
  618. streamMockInterval = null;
  619. }
  620. activeIndex.value = 5;
  621. searchRef.value.scrollTop = 0;
  622. generateToc();
  623. console.log('mock 结束');
  624. }
  625. }, 50);
  626. };
  627. const tabs = [
  628. { key: 'knowledge', name: '知识库' },
  629. // { key: 'net', name: '全网' },
  630. { key: 'original', name: '原生' },
  631. ];
  632. const steps = ref([
  633. {
  634. key: 0,
  635. name: '问题分析',
  636. tags: [],
  637. prog: 0
  638. },
  639. {
  640. key: 1,
  641. name: '知识搜索',
  642. tags: [],
  643. prog: 0
  644. },
  645. {
  646. key: 2,
  647. name: '整理答案',
  648. tags: [],
  649. prog: 0
  650. },
  651. {
  652. key: 3,
  653. name: '回答完成',
  654. tags: [],
  655. prog: 0
  656. }
  657. ]);
  658. const doStep = () => {
  659. setInterval(() => {
  660. steps.value.forEach((v) => {
  661. if (v.key < activeIndex.value) {
  662. v.prog = 100;
  663. } else if (v.key === activeIndex.value) {
  664. if (v.prog + 1 === 100) {
  665. } else {
  666. v.prog += 1;
  667. }
  668. } else {
  669. v.prog = 0;
  670. }
  671. });
  672. }, 100);
  673. };
  674. // 相关案例关键词
  675. const caseKeyWords = ref([]);
  676. const setActiveIndexTags = (index, tags) => {
  677. steps.value[index].tags = [];
  678. // 把问题分析部分的关键词带出去
  679. if (index == 0 && tags.length > 0) {
  680. caseKeyWords.value = tags;
  681. }
  682. for (let i = 0; i < tags.length; i++) {
  683. setTimeout(function () {
  684. steps.value[index].tags = tags.slice(0, i + 1);
  685. }, 500 * (i + 1));
  686. }
  687. };
  688. const evalResponse = (evalType) => {
  689. // ManagerAPI.feedback(currentResponse.value.logId, evalType === 'like' ? 1 : 2).then((res) => {
  690. // evaluate.value = evalType;
  691. // message.info('感谢你的反馈!');
  692. // });
  693. };
  694. const changeStatusText = () => {
  695. let i = 0;
  696. setInterval(() => {
  697. statusText.value = '检索中' + '.'.repeat(i);
  698. if (i === 3) {
  699. i = 0;
  700. }
  701. i++;
  702. }, 500);
  703. };
  704. changeStatusText();
  705. const askType = ref('zcfg');
  706. const search = (q, type = 'zcfg') => {
  707. question.value = q;
  708. askType.value = type;
  709. if (ctr) {
  710. ctr.abort();
  711. ctr = null;
  712. if (streamMockInterval) {
  713. clearInterval(streamMockInterval);
  714. streamMockInterval = null;
  715. }
  716. }
  717. if (type === 'zcfg') {
  718. quest();
  719. } else {
  720. questLandMark();
  721. }
  722. };
  723. const graph = ref(false);
  724. const questLandMark = async () => {
  725. activeIndex.value = 0;
  726. tocs.value = [];
  727. steps.value = [
  728. {
  729. key: 0,
  730. name: '问题分析',
  731. tags: [],
  732. prog: 0
  733. },
  734. {
  735. key: 1,
  736. name: '任务拆解',
  737. tags: [],
  738. prog: 0
  739. },
  740. {
  741. key: 2,
  742. name: 'Agent调用',
  743. tags: [],
  744. prog: 0
  745. },
  746. {
  747. key: 3,
  748. name: '回答完成',
  749. tags: [],
  750. prog: 0
  751. }
  752. ];
  753. currentResponse.value = {
  754. question: question.value,
  755. id: 1,
  756. msg: '',
  757. originAnswer: '',
  758. docs: [],
  759. msgFinished: false,
  760. hasChart: false
  761. };
  762. await getQuestionKeyWords();
  763. ctr = new AbortController();
  764. await fetchEventSource(window.AppGlobalConfig.landMarketUrl, {
  765. method: 'POST',
  766. openWhenHidden: true,
  767. timeout: 300000,
  768. // redirect: 'follow',
  769. headers: {
  770. 'Content-Type': 'application/json'
  771. },
  772. body: JSON.stringify({
  773. data: question.value + (graph.value ? ',需要生成图表' : ',不需要生成图表')
  774. }),
  775. // mode: 'no-cors',
  776. signal: ctr.signal,
  777. async onmessage(msg) {
  778. const { data } = msg;
  779. if (!['', '[FINISH]', '[DONE]'].includes(data)) {
  780. try {
  781. currentResponse.value.originAnswer = data;
  782. const response = JSON.parse(data);
  783. if (response) {
  784. const { agent_responses = [] } = response;
  785. for (let agent of agent_responses) {
  786. // 问题分析
  787. if (agent.agent_name === 'plan_dispatcher' && activeIndex.value === 0) {
  788. if (
  789. agent.choices.length > 0 &&
  790. agent.choices.filter((v) => v.role === 'assistant').length > 0
  791. ) {
  792. const chio = agent.choices.filter((v) => v.role === 'assistant')[0];
  793. if (chio.finished) {
  794. activeIndex.value = 1;
  795. setActiveIndexTags(0, [chio.content]);
  796. }
  797. }
  798. }
  799. if (agent.agent_name === 'land_supply_planner' && activeIndex.value === 1) {
  800. if (
  801. agent.choices.length > 0 &&
  802. agent.choices.filter((v) => v.role === 'assistant').length > 0
  803. ) {
  804. const chio = agent.choices.filter((v) => v.role === 'assistant')[0];
  805. if (chio.finished) {
  806. activeIndex.value = 2;
  807. setActiveIndexTags(1, [chio.content]);
  808. if (chio.content.includes('generate_chart')) {
  809. currentResponse.value.hasChart = true;
  810. }
  811. }
  812. }
  813. }
  814. if (
  815. ['generate_chart', 'LandSupplySqlAgent', 'summary'].includes(agent.agent_name) &&
  816. activeIndex.value === 2
  817. ) {
  818. if (
  819. agent.choices.length > 0 &&
  820. agent.choices.filter((v) => v.role === 'assistant').length > 0
  821. ) {
  822. const chio = agent.choices.filter((v) => v.role === 'assistant')[0];
  823. if (chio.finished && steps.value[2].agent !== agent.agent_name) {
  824. steps.value[2].agent = agent.agent_name;
  825. setActiveIndexTags(2, [chio.content]);
  826. }
  827. }
  828. }
  829. if (agent.agent_name === 'summary') {
  830. steps.value[2].agent = 'summary';
  831. if (activeIndex.value === 2) {
  832. activeIndex.value = 3;
  833. }
  834. if (agent.choices.length === 3) {
  835. if (!currentResponse.value.msgFinished) {
  836. currentResponse.value.msg = agent.choices[2].content;
  837. currentResponse.value.msgFinished = agent.choices[2].finished;
  838. }
  839. }
  840. }
  841. if (agent.agent_name === 'generate_chart') {
  842. currentResponse.value.hasChart = true;
  843. if (agent.executed) {
  844. const option = JSON.parse(
  845. agent.exec_res.replace('```json', '').replace('```', '').trim()
  846. );
  847. currentResponse.value.chartOption = option;
  848. setTimeout(() => {
  849. initChart(option);
  850. }, 1000);
  851. }
  852. }
  853. }
  854. }
  855. } catch (e) {
  856. console.error(e, data);
  857. }
  858. }
  859. if (data === '[FINISH]') {
  860. activeIndex.value = 5;
  861. }
  862. },
  863. onclose() {
  864. activeIndex.value = 5;
  865. generateToc();
  866. collectQuestion();
  867. },
  868. onerror(err) {
  869. throw err;
  870. },
  871. onopen() {}
  872. });
  873. };
  874. const openAskModal = () => {
  875. open.value = true;
  876. nextTick(() => {
  877. if (aiTextAreaRef.value) {
  878. aiTextAreaRef.value.setText(question.value);
  879. }
  880. });
  881. };
  882. const ask = async (q, isFllow) => {
  883. if (ctr) {
  884. if (streamMockInterval) {
  885. clearInterval(streamMockInterval);
  886. streamMockInterval = null;
  887. }
  888. ctr.abort();
  889. }
  890. times.value = 0;
  891. if (timers.value) {
  892. timers.value.forEach((t) => {
  893. clearTimeout(t);
  894. t = null;
  895. });
  896. }
  897. timers.value = [];
  898. if (dsChecked.value) {
  899. dsHintTxt.value = '';
  900. dsLoading.value = true;
  901. }
  902. question.value = q;
  903. open.value = false;
  904. showDoc.value = false;
  905. askType.value = 'zcfg';
  906. // activeTab.value = 'knowledge';
  907. quest(isFllow);
  908. };
  909. const followAsk = () => {
  910. ask(followQuestion.value, true);
  911. followVisible.value = false;
  912. followQuestion.value = '';
  913. showDoc.value = false;
  914. };
  915. let questionUrl = '/chat/kb_chat';
  916. const changeTab = (tab) => {
  917. dsLoading.value = dsChecked.value;
  918. dsHintTxt.value = '';
  919. times.value = 0;
  920. if (timers.value) {
  921. timers.value.forEach((t) => {
  922. clearTimeout(t);
  923. t = null;
  924. });
  925. }
  926. timers.value = [];
  927. if (tab === 'net') {
  928. questionUrl = '/chat/bing_chat';
  929. } else if(tab === 'knowledge'){
  930. questionUrl = '/chat/kb_chat';
  931. }else if(tab === 'original') {
  932. questionUrl = '/chat/chat';
  933. }
  934. activeTab.value = tab;
  935. if (ctr) {
  936. if (streamMockInterval) {
  937. clearInterval(streamMockInterval);
  938. streamMockInterval = null;
  939. }
  940. ctr.abort();
  941. }
  942. quest();
  943. };
  944. const onChange = (type) => {
  945. dsChange(type);
  946. changeAnswerType();
  947. };
  948. //ds绑定用户改变
  949. const dsChange = (type) => {
  950. dsChecked.value = type === '0' ? false : true;
  951. localStorage.setItem("_isDeepSeek", type);
  952. if (timers.value) {
  953. timers.value.forEach((t) => {
  954. clearTimeout(t);
  955. t = null;
  956. });
  957. timers.value = [];
  958. }
  959. times.value = 0;
  960. dsHintTxt.value = '';
  961. //打字机效果 切换会打印
  962. aiLoading.value = true;
  963. };
  964. const changeAnswerType = () => {
  965. if (!dsChecked.value) {
  966. dsLoading.value = false;
  967. dsHintTxt.value = '';
  968. } else {
  969. dsLoading.value = true;
  970. dsHintTxt.value = '';
  971. }
  972. if (ctr) {
  973. if (streamMockInterval) {
  974. clearInterval(streamMockInterval);
  975. streamMockInterval = null;
  976. }
  977. ctr.abort();
  978. }
  979. quest();
  980. };
  981. const questHistories = ref([]);
  982. const followVisible = ref(false);
  983. let scb = null;
  984. const quest = async (isFllow) => {
  985. startTime.value = Date.now();
  986. window.scroll({ top: 0, behavior: 'smooth' });
  987. tocs.value = [];
  988. question.value = (isFllow ? '追问: ' : '') + question.value;
  989. evaluate.value = null;
  990. activeIndex.value = 0;
  991. steps.value = [
  992. {
  993. key: 0,
  994. name: '问题分析',
  995. tags: [],
  996. prog: 0
  997. },
  998. {
  999. key: 1,
  1000. name: '知识搜索',
  1001. tags: [],
  1002. prog: 0
  1003. },
  1004. {
  1005. key: 2,
  1006. name: '整理答案',
  1007. tags: [],
  1008. prog: 0
  1009. },
  1010. {
  1011. key: 3,
  1012. name: '正在生成答案...',
  1013. tags: [],
  1014. prog: 0
  1015. }
  1016. ];
  1017. if (!isFllow) {
  1018. showDoc.value = false;
  1019. }
  1020. searchRef.value.scrollTop = 0;
  1021. if (isFllow) {
  1022. const { id, question, msg, docs, originAnswer, keywords = [] } = currentResponse.value;
  1023. questHistories.value.push({ id, question, msg, docs: docs, originAnswer, keywords });
  1024. } else {
  1025. questHistories.value = [];
  1026. }
  1027. aiLoading.value = true;
  1028. if (scb !== null) {
  1029. clearInterval(scb);
  1030. scb = null;
  1031. } else {
  1032. scb = setInterval(() => {
  1033. scrollToBottom();
  1034. }, 500);
  1035. }
  1036. ctr = new AbortController();
  1037. const id = questHistories.value.length;
  1038. currentResponse.value = {
  1039. question: question.value,
  1040. id,
  1041. msg: '',
  1042. originAnswer: '',
  1043. streamMock: false,
  1044. streamMsg: '',
  1045. docs: []
  1046. };
  1047. activeIndex.value = 0;
  1048. getQuestionKeyWords();
  1049. if (activeTab.value === 'net') {
  1050. questionUrl = '/chat/bing_chat';
  1051. } else if(activeTab.value === 'knowledge'){
  1052. questionUrl = '/chat/kb_chat';
  1053. }else if(activeTab.value === 'original') {
  1054. questionUrl = '/chat/chat';
  1055. }
  1056. const topKs = window?.AppGlobalConfig?.topKs || {
  1057. 0: 5,
  1058. 1: 10,
  1059. 2: 15
  1060. };
  1061. let body = null
  1062. if (activeTab.value === 'net') {
  1063. body = {
  1064. query: question.value,
  1065. stream: true,
  1066. model: dsChecked.value ? 'deepseek-r1' : '',
  1067. search_type: answerType.value
  1068. }
  1069. } else if(activeTab.value === 'knowledge'){
  1070. body = {
  1071. query: question.value,
  1072. mode: 'local_kb',
  1073. kb_name: activeTab.value === 'paper' ? 'compose_paper_material_total' : dsChecked.value ? window?.AppGlobalConfig?.llm?.kb_name : 'policy',
  1074. top_k: topKs[answerType.value],
  1075. search_type: answerType.value,
  1076. score_threshold: 0.5,
  1077. model: dsChecked.value ? 'deepseek-r1' : '',
  1078. history: isFllow ? getFlowHistory() : [],
  1079. stream: true,
  1080. prompt_name: 'rag_context_qa.md',
  1081. return_direct: false
  1082. }
  1083. } else if (activeTab.value === 'original') {
  1084. body = {
  1085. query: question.value,
  1086. stream: true
  1087. }
  1088. }
  1089. if (activeTab.value === 'knowledge' && isFllow) {
  1090. if (questHistories.value.length > 0) {
  1091. body.history_keyword = questHistories.value[questHistories.value.length - 1].keywords;
  1092. }
  1093. }
  1094. if (activeTab.value !== 'net' && answerType.value !== '0') {
  1095. currentResponse.value.streamMock = true;
  1096. currentResponse.value.mockStart = true;
  1097. streamToAnswer();
  1098. }
  1099. const rootUrl = dsChecked.value ? window.AppGlobalConfig.knowledgeServer : window.AppGlobalConfig.aiServer
  1100. await fetchEventSource(rootUrl + questionUrl, {
  1101. method: 'POST',
  1102. openWhenHidden: true,
  1103. timeout: 300000,
  1104. headers: {
  1105. 'Content-Type': 'application/json'
  1106. },
  1107. body: JSON.stringify(body),
  1108. signal: ctr.signal,
  1109. async onmessage(msg) {
  1110. if (activeIndex.value !== 3) {
  1111. activeIndex.value = 3;
  1112. setActiveIndexTags(2, ['知识整理', '要点归纳', '总结摘要']);
  1113. }
  1114. activeTab.value === 'net' ? handleNetResponse(msg, id) : handleKnowledgeResponse(msg, id);
  1115. },
  1116. onclose() {
  1117. if (scb !== null) {
  1118. clearInterval(scb);
  1119. scb = null;
  1120. collectQuestion();
  1121. }
  1122. if (currentResponse.value.streamMock) {
  1123. currentResponse.value.mockStart = false;
  1124. } else {
  1125. activeIndex.value = 5;
  1126. searchRef.value.scrollTop = 0;
  1127. generateToc();
  1128. }
  1129. },
  1130. onerror(err) {
  1131. throw err;
  1132. },
  1133. onopen() {
  1134. setActiveIndexTags(1, ['国家法律法规', '地方性法规', '政府规章', '政策解读', '经典案例']);
  1135. }
  1136. });
  1137. };
  1138. const getFlowHistory = () => {
  1139. const lastHistory = [...questHistories.value].splice(-1);
  1140. const parm = [];
  1141. lastHistory.forEach((item) => {
  1142. parm.push({ role: 'user', content: item.question });
  1143. });
  1144. return parm;
  1145. };
  1146. const handleKnowledgeResponse = (msg, id) => {
  1147. if (!msg || !msg.data) {
  1148. return;
  1149. }
  1150. const rData = JSON.parse(msg.data);
  1151. if (rData?.choices && rData.choices.length > 0) {
  1152. if (activeTab.value === 'net' && rData.status !== 2) {
  1153. return;
  1154. }
  1155. if (rData.status == 3) {
  1156. if (timers.value) {
  1157. timers.value.forEach((t) => {
  1158. clearTimeout(t);
  1159. t = null;
  1160. });
  1161. timers.value = [];
  1162. }
  1163. if (dsChecked.value) {
  1164. endTime.value = Date.now();
  1165. var time = ((endTime.value - startTime.value) / 1000).toFixed(0);
  1166. dsHintTxt.value = `已深度思考(用时 ${time} 秒)`;
  1167. dsLoading.value = false;
  1168. aiLoading.value=true;
  1169. }
  1170. currentResponse.value.originAnswer = rData.choices[0]?.delta?.content.replaceAll(
  1171. '\n',
  1172. ` \n`
  1173. );
  1174. currentResponse.value.msg = rData.choices[0]?.delta?.content.replaceAll('\n', ` \n`);
  1175. let num = getNum(currentResponse.value.msg);
  1176. while (num) {
  1177. const docsNum = currentResponse.value.docs.length;
  1178. if (docsNum && num > docsNum + 1) {
  1179. }
  1180. currentResponse.value.msg = currentResponse.value.msg.replace(
  1181. `[[${num}]]`,
  1182. `<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;
  1183. height: 20px;
  1184. background: #FFFFFF;
  1185. border-radius: 4px 4px 4px 4px;
  1186. border: 1px solid #BACAE3;">${num}</span>`
  1187. );
  1188. num = getNum(currentResponse.value.msg);
  1189. }
  1190. } else {
  1191. aiLoading.value = false;
  1192. if (dsChecked.value) {
  1193. dsHintTxt.value = '思考中...';
  1194. //ds模式打字机效果输出
  1195. const timer = setTimeout(() => {
  1196. if (!aiLoading.value) {
  1197. currentResponse.value.originAnswer += rData.choices[0]?.delta?.content.replaceAll(
  1198. '\n',
  1199. ` \n`
  1200. );
  1201. currentResponse.value.msg += rData.choices[0]?.delta?.content.replaceAll('\n', ` \n`);
  1202. let num = getNum(currentResponse.value.msg);
  1203. while (num) {
  1204. const docsNum = currentResponse.value.docs.length;
  1205. if (docsNum && num > docsNum + 1) {
  1206. }
  1207. currentResponse.value.msg = currentResponse.value.msg.replace(
  1208. `[[${num}]]`,
  1209. `<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;
  1210. height: 20px;
  1211. background: #FFFFFF;
  1212. border-radius: 4px 4px 4px 4px;
  1213. border: 1px solid #BACAE3;">${num}</span>`
  1214. );
  1215. num = getNum(currentResponse.value.msg);
  1216. }
  1217. }
  1218. clearTimeout(timer);
  1219. timers.value.push(timer);
  1220. }, times.value * 15);
  1221. } else {
  1222. currentResponse.value.originAnswer += rData.choices[0]?.delta?.content.replaceAll(
  1223. '\n',
  1224. ` \n`
  1225. );
  1226. currentResponse.value.msg += rData.choices[0]?.delta?.content.replaceAll('\n', ` \n`);
  1227. let num = getNum(currentResponse.value.msg);
  1228. while (num) {
  1229. const docsNum = currentResponse.value.docs.length;
  1230. if (docsNum && num > docsNum + 1) {
  1231. }
  1232. currentResponse.value.msg = currentResponse.value.msg.replace(
  1233. `[[${num}]]`,
  1234. `<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;
  1235. height: 20px;
  1236. background: #FFFFFF;
  1237. border-radius: 4px 4px 4px 4px;
  1238. border: 1px solid #BACAE3;">${num}</span>`
  1239. );
  1240. num = getNum(currentResponse.value.msg);
  1241. }
  1242. }
  1243. // currentResponse.value.msg += rData.choices[0]?.delta?.content.replaceAll('\n', ` \n`);
  1244. }
  1245. }
  1246. if (!currentResponse.value.docs.length) {
  1247. if (rData.docs && rData.docs.length) {
  1248. handleDocs(rData.docs);
  1249. }
  1250. }
  1251. times.value++;
  1252. };
  1253. const handleNetResponse = (msg, id) => {
  1254. const rData = msg.data;
  1255. if (!!rData && rData !== '[DONE]') {
  1256. try {
  1257. const res = JSON.parse(rData);
  1258. if (res.error) {
  1259. activeIndex.value = 4;
  1260. message.error(res.error);
  1261. currentResponse.value.msg = res.error;
  1262. return;
  1263. }
  1264. if (res.rag_finish) {
  1265. if (activeIndex.value < 4) {
  1266. activeIndex.value = 4;
  1267. }
  1268. if (dsChecked.value) {
  1269. endTime.value = Date.now();
  1270. var time = ((endTime.value - startTime.value) / 1000).toFixed(0);
  1271. dsHintTxt.value = `已深度思考(用时 ${time} 秒)`;
  1272. dsLoading.value = false;
  1273. }
  1274. } else {
  1275. if (dsChecked.value && dsHintTxt.value != '思考中...') {
  1276. dsHintTxt.value = '思考中...';
  1277. }
  1278. }
  1279. if (res.result) {
  1280. currentResponse.value.originAnswer = res.result;
  1281. currentResponse.value.msg = res.result.replaceAll('\n', ` \n`);
  1282. let num = getNum(currentResponse.value.msg);
  1283. while (num) {
  1284. const docsNum = currentResponse.value.docs.length;
  1285. if (docsNum && num > docsNum + 1) {
  1286. currentResponse.value.msg = currentResponse.value.msg.replace(`[[${num}]]`, ``);
  1287. }
  1288. currentResponse.value.msg = currentResponse.value.msg.replace(
  1289. `[[${num}]]`,
  1290. `<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>`
  1291. );
  1292. num = getNum(currentResponse.value.msg);
  1293. }
  1294. }
  1295. if (res.source_list && res.source_list.length && !currentResponse.value.docs.length) {
  1296. handleDocs(res.source_list);
  1297. }
  1298. } catch (e) {}
  1299. }
  1300. if (rData === '[DONE]') {
  1301. console.log(currentResponse.value.msg);
  1302. }
  1303. };
  1304. const handleDocs = (docs) => {
  1305. if (
  1306. docs.length === 1 &&
  1307. "<span style='color:red'>未找到相关文档,该回答为大模型自身能力解答!</span>" === docs[0]
  1308. ) {
  1309. // message.warn("知识库中未收录相关内容,将为您自动切换全网模式,请稍候......");
  1310. // changeTab('net');
  1311. return;
  1312. }
  1313. currentResponse.value.docs = docs.map((v, i) => {
  1314. if (activeTab.value === 'net') {
  1315. return {
  1316. index: v.num,
  1317. doc: v.name,
  1318. link: v.url,
  1319. title: v.title,
  1320. summary: v.summary,
  1321. content: '',
  1322. showContent: false,
  1323. type: 'url'
  1324. };
  1325. }
  1326. if (v.toLowerCase().indexOf('.pdf') > 0) {
  1327. return {
  1328. index: i++,
  1329. doc: v.substring(v.toLowerCase().indexOf('] [') + 3, v.toLowerCase().indexOf('.pdf]') + 4),
  1330. link: v.substring(v.toLowerCase().indexOf('.pdf]') + 6, v.toLowerCase().indexOf('.pdf)') + 4),
  1331. content: v.substring(v.toLowerCase().indexOf('.pdf)') + 5),
  1332. showContent: false,
  1333. type: 'pdf'
  1334. };
  1335. } else if (v.toLowerCase().indexOf('.txt') > 0) {
  1336. return {
  1337. index: i++,
  1338. doc: v.toLowerCase().substring(v.indexOf('] [') + 3, v.toLowerCase().indexOf('.txt]') + 4),
  1339. link: v.toLowerCase().substring(v.indexOf('.txt]') + 6, v.toLowerCase().indexOf('.txt)') + 4),
  1340. content: v.toLowerCase().substring(v.indexOf('.txt)') + 5),
  1341. showContent: false,
  1342. type: 'txt'
  1343. };
  1344. } else if (v.toLowerCase().indexOf('.docx') > 0) {
  1345. return {
  1346. index: i++,
  1347. doc: v.substring(v.toLowerCase().indexOf('] [') + 3, v.toLowerCase().indexOf('.docx]') + 5),
  1348. link: v.substring(v.toLowerCase().indexOf('.docx]') + 7, v.toLowerCase().indexOf('.docx)') + 5),
  1349. content: v.substring(v.toLowerCase().indexOf('.docx)') + 6),
  1350. showContent: false,
  1351. type: 'docx'
  1352. };
  1353. }
  1354. });
  1355. };
  1356. const openDoc = (doc, i) => {
  1357. if (window.AppGlobalConfig.isDisabledSource) return;
  1358. var link = doc.link;
  1359. var type = doc.type;
  1360. pdfSrc.value = link.replace(
  1361. window.AppGlobalConfig.knowledgeDocUrl.replace(
  1362. '=policy&',
  1363. activeTab.value === 'paper' ? '=compose_paper_material_total&' : '=policy&'
  1364. ),
  1365. window.AppGlobalConfig.knowledgeDocUrlProxy.replace(
  1366. '=policy&',
  1367. activeTab.value === 'paper' ? '=compose_paper_material_total&' : '=policy&'
  1368. )
  1369. );
  1370. showDoc.value = true;
  1371. fileType.value = type;
  1372. pdfContent.value = doc.content;
  1373. pdfNum.value = i;
  1374. };
  1375. const closeDoc = () => {
  1376. showDoc.value = false;
  1377. };
  1378. watch(
  1379. () => showDoc.value,
  1380. (newVal) => {
  1381. // 发布打开关闭,在关闭文档的时候打开相关案例
  1382. PubsubService.publish('switch-relevant-cases-box', newVal);
  1383. }
  1384. );
  1385. const openUrl = (url) => {
  1386. window.open(url, '_blank');
  1387. };
  1388. const openDocByIndex = (ind, id) => {
  1389. showDoc.value = false;
  1390. let link = null;
  1391. if (id !== currentResponse.value.id) {
  1392. if (questHistories.value[id].docs[ind - 1].type === 'url') {
  1393. openUrl(questHistories.value[id].docs[ind - 1].link);
  1394. return;
  1395. }
  1396. link = questHistories.value[id].docs[ind - 1].link;
  1397. fileType.value = questHistories.value[id].docs[ind - 1].type;
  1398. } else {
  1399. if (currentResponse.value.docs[ind - 1].type === 'url') {
  1400. openUrl(currentResponse.value.docs[ind - 1].link);
  1401. return;
  1402. }
  1403. link = currentResponse.value.docs[ind - 1].link;
  1404. fileType.value = currentResponse.value.docs[ind - 1].type;
  1405. pdfContent.value = currentResponse.value.docs[ind - 1].content;
  1406. pdfNum.value = currentResponse.value.docs[ind - 1].num;
  1407. }
  1408. pdfNum.value = ind;
  1409. pdfSrc.value = link.replace(
  1410. window.AppGlobalConfig.knowledgeDocUrl.replace(
  1411. '=policy&',
  1412. activeTab.value === 'paper' ? '=compose_paper_material_total&' : '=policy&'
  1413. ),
  1414. window.AppGlobalConfig.knowledgeDocUrlProxy.replace(
  1415. '=policy&',
  1416. activeTab.value === 'paper' ? '=compose_paper_material_total&' : '=policy&'
  1417. )
  1418. );
  1419. showDoc.value = true;
  1420. };
  1421. window.openDocByIndex = openDocByIndex;
  1422. const getNum = (str) => {
  1423. const matches = str.match(/\[\[(\d+)\]\]/);
  1424. if (matches) {
  1425. return matches[1]; // 输出: 2
  1426. } else {
  1427. return null;
  1428. }
  1429. };
  1430. // 埋点采集数据
  1431. const collectQuestion = () => {
  1432. const { question, originAnswer, keywords = [] } = currentResponse.value;
  1433. const userStore = useUserStore();
  1434. const param = {
  1435. question,
  1436. answer: originAnswer,
  1437. questionType: askType.value === 'zcfg' ? '政策法规' : '土地市场',
  1438. keywords: Array.isArray(keywords) ? keywords.join(',') : keywords,
  1439. user: '游客',
  1440. userId: '-1'
  1441. };
  1442. if (userStore.isLogin) {
  1443. const { id = '-1', displayName = '游客' } = userStore?.user?.user || {};
  1444. param.user = displayName;
  1445. param.userId = id;
  1446. }
  1447. ManagerAPI.collect(param).then((res) => {
  1448. if (res.data) {
  1449. // 记录日志,用来反馈
  1450. currentResponse.value.logId = res.data;
  1451. }
  1452. });
  1453. };
  1454. const questions = ref([]);
  1455. let recommendedQuestions = [];
  1456. let recommendedQuestionsIndex = 0;
  1457. const getRecommendedQuestion = async (b = false) => {
  1458. const { question } = currentResponse.value;
  1459. const myHeaders = new Headers();
  1460. myHeaders.append('Content-Type', 'application/json');
  1461. questions.value = [];
  1462. if (!b) {
  1463. recommendedQuestions = [];
  1464. }
  1465. recommendedQuestionsIndex = 0;
  1466. const raw = JSON.stringify({
  1467. query: `你会收到一个用户提问,请根据问题延伸出10个子问题。
  1468. 在你回答问题的时候,还需要注意推荐给用户的问题必须以列表的形式返回。
  1469. for example:
  1470. [
  1471. "1、南京市在推进产业用地高质量利用方面采取了哪些具体措施?这些措施的效果如何?",
  1472. "2、杭州市创新型产业用地管理的具体实施细则是什么?这些细则如何促进传统产业转型升级?",
  1473. "3、南京市和杭州市在土地供应方式上有哪些不同?这些不同如何影响各自的产业发展?",
  1474. "4、南京市如何通过政策支持和激励措施吸引重大投资项目?这些措施的实际效果如何?",
  1475. "5、杭州市“工业上楼”项目的实施情况如何?这一政策对提升土地利用效率有何影响?",
  1476. ]
  1477. 以下是用户提问:${question}`,
  1478. // model: 'qwen1.5-chat',
  1479. stream: false
  1480. });
  1481. const requestOptions = {
  1482. method: 'POST',
  1483. headers: myHeaders,
  1484. body: raw,
  1485. redirect: 'follow'
  1486. };
  1487. const rootUrl = dsChecked.value ? window.AppGlobalConfig.knowledgeServer : window.AppGlobalConfig.aiServer
  1488. fetch(rootUrl + '/chat/chat', requestOptions)
  1489. .then((response) => {
  1490. return response.json()
  1491. })
  1492. .then((msgStr) => {
  1493. const msg = JSON.parse(msgStr)
  1494. const str = msg.choices[0]?.message?.content;
  1495. if (str) {
  1496. const str1 = str.slice(str.indexOf("</think>") + 7)
  1497. recommendedQuestions = formatRecommendedQuestions(str1);
  1498. questions.value = recommendedQuestions.slice(0, 5);
  1499. }
  1500. })
  1501. .catch((error) => console.error(error));
  1502. };
  1503. const formatRecommendedQuestions = (str) => {
  1504. return str
  1505. .replace(/`|$$|$$|"|,/g, '') // 清除引号、反引号、方括号和逗号
  1506. .split('\n') // 按换行分割
  1507. .map(line => line.trim()) // 去除首尾空格
  1508. .filter(line => { // 双条件过滤
  1509. return (
  1510. line.length > 0 && // 过滤空行
  1511. /^\d+[、.]/.test(line) // 匹配"1、"或"1."开头
  1512. )
  1513. })
  1514. .map(line => {
  1515. // 统一标点为中文顿号并保留末尾空格
  1516. return line
  1517. .replace(/^\d+\./, m => m.replace('.', '、')) // 英文标点转中文
  1518. .replace(/\s*$/, ' ') // 强制保留末尾3空格
  1519. });
  1520. }
  1521. const changeRecommendedQuestions = () => {
  1522. recommendedQuestionsIndex++;
  1523. if (recommendedQuestionsIndex % 2 === 0) {
  1524. questions.value = recommendedQuestions.slice(0, 5);
  1525. } else {
  1526. questions.value = recommendedQuestions.slice(5);
  1527. getRecommendedQuestion(true)
  1528. }
  1529. };
  1530. const getQuestionKeyWords = async () => {
  1531. const { question } = currentResponse.value;
  1532. const myHeaders = new Headers();
  1533. myHeaders.append('Content-Type', 'application/json');
  1534. questions.value = [];
  1535. const raw = JSON.stringify({
  1536. query: `请从以下文本中提取核心关键词,确保关键词简洁明了且准确反映文本的主要内容。 请按照以下格式输出:关键词:关键词1,关键词2。文本如下:“${question}”`,
  1537. stream: false
  1538. });
  1539. const requestOptions = {
  1540. method: 'POST',
  1541. headers: myHeaders,
  1542. body: raw,
  1543. redirect: 'follow'
  1544. };
  1545. const rootUrl = dsChecked.value ? window.AppGlobalConfig.knowledgeServer : window.AppGlobalConfig.aiServer
  1546. return fetch(rootUrl + '/chat/chat', requestOptions)
  1547. .then((response) => response.json())
  1548. .then((msgStr) => {
  1549. const msg = JSON.parse(msgStr)
  1550. activeIndex.value = 1;
  1551. const str = msg.choices[0]?.message?.content;
  1552. if (str) {
  1553. const str1 = str.slice(str.indexOf("</think>")+7)
  1554. const keywords = splitWords(str1).slice(0, 3);
  1555. setActiveIndexTags(0, keywords);
  1556. currentResponse.value.keywords = keywords;
  1557. }
  1558. })
  1559. .catch((error) => console.error(error));
  1560. };
  1561. const splitWords = (word) => {
  1562. if (word) {
  1563. word = word
  1564. .replaceAll('核心词为:', '')
  1565. .replaceAll('问题核心词:', '')
  1566. .replaceAll('>', '')
  1567. .replaceAll('。', '')
  1568. .trim();
  1569. if (word.indexOf('\n\n') !== -1) {
  1570. return word.split('\n\n');
  1571. }
  1572. if (word.indexOf(',') !== -1) {
  1573. return word.split(',');
  1574. }
  1575. if (word.indexOf('、') !== -1) {
  1576. return word.split('、');
  1577. }
  1578. if (word.indexOf(',') !== -1) {
  1579. return word.split(',');
  1580. }
  1581. return [word];
  1582. }
  1583. return [];
  1584. };
  1585. const exportAnswer = (type) => {
  1586. const { question, originAnswer } = currentResponse.value;
  1587. CommonAPI.answerExport(type, { question, answer: originAnswer });
  1588. };
  1589. const copy = async () => {
  1590. try {
  1591. const input = document.createElement('input');
  1592. input.value = currentResponse.value.msg;
  1593. document.body.appendChild(input);
  1594. input.select();
  1595. document.execCommand('copy');
  1596. document.body.removeChild(input);
  1597. message.info('内容已复制到剪贴板');
  1598. } catch (err) {
  1599. console.error(err);
  1600. // console.error('复制到剪贴板失败', err);
  1601. }
  1602. };
  1603. const changeActiveTab = (tab) => {
  1604. activeTab.value = tab;
  1605. };
  1606. const stopAI = () => {
  1607. if (ctr) {
  1608. ctr.abort();
  1609. }
  1610. };
  1611. const openRecommendedQuestion = (q) => {
  1612. if (q) {
  1613. if (q.charAt(1) === '、') {
  1614. q = q.substring(2);
  1615. }
  1616. window.open(
  1617. `/aisearch/#/ai-search?q=${encodeURI(q.trim())}&scope=${activeTab.value}&type=${answerType.value}`,
  1618. '_blank'
  1619. );
  1620. }
  1621. };
  1622. function initChart(option = {}) {
  1623. let chart = echarts.init(document.getElementById('summaryChart'));
  1624. if (option.option) {
  1625. chart.setOption(option.option);
  1626. } else {
  1627. chart.setOption(option);
  1628. }
  1629. }
  1630. const generateToc = () => {
  1631. if (askType.value === 'tdsc') {
  1632. const summaryTitle = document.getElementById('tdscSummaryTitle');
  1633. const chartCard = document.getElementById('tdscChartCard');
  1634. const mapPanel = document.getElementById('tdscMapPanel');
  1635. tocs.value = [];
  1636. const offset = 550 + 210;
  1637. tocs.value.push({
  1638. id: 'tdscSummaryTitle',
  1639. children: [],
  1640. text: '总结',
  1641. top: summaryTitle.offsetTop + offset
  1642. });
  1643. if (chartCard.style.display !== 'none') {
  1644. tocs.value.push({
  1645. id: 'tdscChartCard',
  1646. children: [],
  1647. text: '生成图表',
  1648. top: chartCard.offsetTop + offset
  1649. });
  1650. }
  1651. // tocs.value.push({
  1652. // id: 'tdscMapPanel',
  1653. // children: [],
  1654. // text: '空间位置',
  1655. // top: mapPanel.offsetTop + offset
  1656. // });
  1657. return;
  1658. }
  1659. const resMarkdownDom = document.getElementById("resMarkdown");
  1660. let thinkDoms = resMarkdownDom.getElementsByTagName("think");
  1661. let markdowns = null;
  1662. if (!thinkDoms || thinkDoms.length === 0) {
  1663. markdowns = resMarkdownDom.children;
  1664. } else {
  1665. markdowns = thinkDoms[0].children
  1666. }
  1667. let index = 1;
  1668. let children = [];
  1669. let toc = [];
  1670. for (const markdown of markdowns) {
  1671. if (markdown.tagName.length === 2 && markdown.tagName.toLocaleUpperCase().startsWith('H')) {
  1672. const id = 'title' + index++ + new Date().getTime();
  1673. markdown.setAttribute('id', id);
  1674. const tocItem = {
  1675. id,
  1676. text: markdown.innerText.trim(),
  1677. children: [],
  1678. top: markdown.offsetTop
  1679. };
  1680. children = tocItem.children;
  1681. toc.push(tocItem);
  1682. }
  1683. if (markdown.tagName.toLocaleUpperCase().trim() === 'P') {
  1684. const pChildren = markdown.children;
  1685. for (const pChild of pChildren) {
  1686. if (pChild.tagName.toLocaleUpperCase().trim() === 'STRONG') {
  1687. const id = 'strong' + index++ + new Date().getTime();
  1688. pChild.setAttribute('id', id);
  1689. const strongItem = {
  1690. id,
  1691. text: pChild?.innerText?.trim(),
  1692. children: [],
  1693. top: pChild.offsetTop
  1694. };
  1695. children.push(strongItem);
  1696. // children = strongItem.children
  1697. }
  1698. }
  1699. }
  1700. }
  1701. tocs.value = toc;
  1702. console.log('toc', toc);
  1703. };
  1704. onMounted(() => {
  1705. tocHeight.value = window.document.body.offsetHeight - 210 - 51;
  1706. doStep();
  1707. const { query } = router.currentRoute.value;
  1708. if (query.q) {
  1709. answerType.value = query.type || '0';
  1710. scope.value = query.scope || 'net';
  1711. var ds = '0';
  1712. if (!useUserStore().isLogin) {
  1713. ds = query.ds;
  1714. } else {
  1715. ds = useUserStore().user.user.enableDeepseek
  1716. ? useUserStore().user.user.enableDeepseek + ''
  1717. : query.ds;
  1718. }
  1719. dsHintTxt.value = '';
  1720. if (ds && ds == '1') {
  1721. ds = true;
  1722. dsLoading.value = true;
  1723. } else {
  1724. ds = false;
  1725. dsLoading.value = false;
  1726. }
  1727. dsChecked.value = ds;
  1728. activeTab.value = scope.value;
  1729. ask(decodeURIComponent(query.q));
  1730. }
  1731. window.addEventListener(
  1732. 'scroll',
  1733. () => {
  1734. if (markdownTocAffixRef.value && markdownTocAffixRef.value.updatePosition) {
  1735. markdownTocAffixRef.value.updatePosition();
  1736. }
  1737. },
  1738. true
  1739. );
  1740. });
  1741. defineExpose({ search, changeActiveTab, stopAI });
  1742. </script>
  1743. <style scoped lang="scss">
  1744. @import 'src/assets/scss/variables';
  1745. .ai-search-detail {
  1746. width: 100%;
  1747. background: $background_color;
  1748. height: 100%;
  1749. .header {
  1750. width: 100%;
  1751. height: 60px;
  1752. background: #F4F6F9;
  1753. }
  1754. .search-panel {
  1755. width: 100%;
  1756. height: calc(100% - 60px - 50px);
  1757. display: flex;
  1758. margin-top: 50px;
  1759. padding-bottom: 50px;
  1760. .markdown-toc-tdsc {
  1761. :deep(.toc-list) {
  1762. margin: 30px 0;
  1763. }
  1764. }
  1765. .search-detail {
  1766. width: calc(52%);
  1767. background: #ffffff;
  1768. margin-left: 52px;
  1769. &-tdsc {
  1770. width: calc(52%);
  1771. }
  1772. .search-result {
  1773. height: calc(100% - 40px);
  1774. .top {
  1775. width: 100%;
  1776. //height: 105px;
  1777. background: #fff;
  1778. //margin-bottom: 5px;
  1779. border-radius: 4px 4px 4px 4px;
  1780. .title {
  1781. width: calc(100% - 7px - 2px);
  1782. display: flex;
  1783. //margin-bottom: 24PX;
  1784. align-items: center;
  1785. min-height: 33px;
  1786. .icon {
  1787. width: 33px;
  1788. height: 29px;
  1789. background: url('@/assets/images/ai-search/question-prefixicon.png') no-repeat;
  1790. background-size: 100% 100%;
  1791. margin-right: 16px;
  1792. }
  1793. .question {
  1794. width: calc(100% - 32px - 150px);
  1795. //line-height: 20px;
  1796. font-family: PingFang SC, PingFang SC;
  1797. font-weight: 600;
  1798. font-size: 24px;
  1799. color: #212121;
  1800. text-align: left;
  1801. font-style: normal;
  1802. text-transform: none;
  1803. }
  1804. .graph-switch {
  1805. display: flex;
  1806. align-items: center;
  1807. margin-left: 10px;
  1808. width: 110px;
  1809. button {
  1810. margin-right: 2px;
  1811. }
  1812. }
  1813. }
  1814. .status {
  1815. display: flex;
  1816. align-items: center;
  1817. margin-top: 15px;
  1818. justify-content: center;
  1819. @keyframes shake {
  1820. 0% {
  1821. transform: translateX(0);
  1822. }
  1823. 25% {
  1824. transform: translateX(0px);
  1825. }
  1826. 50% {
  1827. transform: translateX(2px);
  1828. }
  1829. 75% {
  1830. transform: translateX(0px);
  1831. }
  1832. 100% {
  1833. transform: translateX(2px);
  1834. }
  1835. }
  1836. .text {
  1837. width: 55px;
  1838. font-family: Alibaba PuHuiTi 3, Alibaba PuHuiTi 30;
  1839. font-weight: normal;
  1840. font-size: 14px;
  1841. color: #9da6b5;
  1842. line-height: 20px;
  1843. text-stroke: 1px rgba(0, 0, 0, 0);
  1844. text-align: left;
  1845. font-style: normal;
  1846. text-transform: none;
  1847. -webkit-text-stroke: 1px rgba(0, 0, 0, 0);
  1848. }
  1849. }
  1850. }
  1851. .result-panel {
  1852. width: 100%;
  1853. background: #fff;
  1854. padding: 29px 0;
  1855. .search-steps {
  1856. width: 100%;
  1857. padding: 4px 0;
  1858. display: flex;
  1859. align-items: center;
  1860. margin-bottom: 30px;
  1861. flex-direction: column;
  1862. .step {
  1863. width: 100%;
  1864. .info {
  1865. width: 100%;
  1866. padding: 0;
  1867. display: flex;
  1868. align-items: center;
  1869. .icon {
  1870. width: 18px;
  1871. height: 18px;
  1872. background: url('@/assets/images/ai-search/step-wait-icon.png') no-repeat;
  1873. background-size: 100% 100%;
  1874. margin-right: 7px;
  1875. }
  1876. .title {
  1877. width: 120px;
  1878. font-family: PingFang SC, PingFang SC;
  1879. font-weight: 600;
  1880. font-size: 16px;
  1881. color: #212121;
  1882. text-align: left;
  1883. font-style: normal;
  1884. text-transform: none;
  1885. }
  1886. .tags {
  1887. width: calc(100% - 10px - 27px - 100px);
  1888. display: flex;
  1889. justify-content: end;
  1890. overflow: hidden;
  1891. height: 30px;
  1892. align-items: center;
  1893. .tag {
  1894. padding: 4px 14px;
  1895. margin-left: 8px;
  1896. line-height: 14px;
  1897. text-align: left;
  1898. font-style: normal;
  1899. text-transform: none;
  1900. white-space: nowrap;
  1901. overflow: hidden;
  1902. text-overflow: ellipsis;
  1903. max-width: 200px;
  1904. cursor: pointer;
  1905. font-weight: 400;
  1906. font-size: 14px;
  1907. color: #2185f2;
  1908. height: 24px;
  1909. background: #f5f9fc;
  1910. border: 1px solid #e9eef5;
  1911. font-family: PingFang SC, PingFang SC;
  1912. font-weight: 400;
  1913. font-size: 14px;
  1914. color: #525b74;
  1915. /* line-height: 0px; */
  1916. text-align: left;
  1917. font-style: normal;
  1918. text-transform: none;
  1919. &-first {
  1920. background: none;
  1921. font-family: Microsoft YaHei;
  1922. font-weight: 400;
  1923. font-size: 16px;
  1924. color: #666666;
  1925. }
  1926. }
  1927. .tag-rol {
  1928. display: inline-block;
  1929. white-space: nowrap;
  1930. box-sizing: border-box;
  1931. //animation: scroll-text 25s linear infinite;
  1932. //width: 100%;
  1933. background: none;
  1934. height: 22px;
  1935. max-width: none;
  1936. overflow: unset;
  1937. }
  1938. @keyframes scroll-text {
  1939. 0% {
  1940. transform: translateX(100%);
  1941. }
  1942. 100% {
  1943. transform: translateX(-100%);
  1944. }
  1945. }
  1946. }
  1947. }
  1948. &-done {
  1949. .info {
  1950. .icon {
  1951. width: 18px;
  1952. height: 18px;
  1953. background: url('@/assets/images/ai-search/step-done-icon.png') no-repeat;
  1954. background-size: 100% 100%;
  1955. margin-right: 7px;
  1956. }
  1957. .title {
  1958. font-family: Microsoft YaHei;
  1959. font-weight: 400;
  1960. font-size: 16px;
  1961. color: #333333;
  1962. line-height: 30px;
  1963. }
  1964. }
  1965. }
  1966. }
  1967. }
  1968. .result {
  1969. width: 100%;
  1970. .tabs {
  1971. display: flex;
  1972. border-bottom: 1px solid #ccc;
  1973. .ds-box {
  1974. display: flex;
  1975. height: 36px;
  1976. align-items: center;
  1977. border: 1px solid #E3E4E4;
  1978. background: #f0f0f0;
  1979. border-radius: 5px;
  1980. overflow: hidden;
  1981. >div {
  1982. height: 100%;
  1983. display: flex;
  1984. align-items: center;
  1985. justify-content: center;
  1986. width: 108px;
  1987. cursor: pointer;
  1988. &.active {
  1989. background: #fff;
  1990. &:nth-child(1) {
  1991. .iconfont {
  1992. color: #4f6bfe;
  1993. }
  1994. }
  1995. &:nth-child(2) {
  1996. .iconfont {
  1997. color: #605BEC;
  1998. }
  1999. }
  2000. }
  2001. .iconfont {
  2002. color: #898D93;
  2003. margin-top: 2px;
  2004. }
  2005. &:nth-child(1) {
  2006. .iconfont {
  2007. font-size: 16px;
  2008. }
  2009. }
  2010. &:nth-child(2) {
  2011. .iconfont {
  2012. font-size: 22px;
  2013. }
  2014. }
  2015. }
  2016. .sw {
  2017. :deep(.ant-switch) {
  2018. background-color: #eaeaea;
  2019. }
  2020. :deep(.ant-switch-handle::before) {
  2021. background-color: #fff;
  2022. }
  2023. }
  2024. .sw-checked {
  2025. :deep(.ant-switch-checked) {
  2026. background-color: #fff;
  2027. }
  2028. :deep(.ant-switch-handle::before) {
  2029. background-color: #1890ff;
  2030. }
  2031. }
  2032. }
  2033. .tab {
  2034. font-family: PingFang SC, PingFang SC;
  2035. font-weight: 400;
  2036. font-size: 24px;
  2037. color: #212121;
  2038. text-align: left;
  2039. font-style: normal;
  2040. text-transform: none;
  2041. margin-right: 28px;
  2042. display: flex;
  2043. justify-content: center;
  2044. flex-direction: column;
  2045. align-items: center;
  2046. cursor: pointer;
  2047. .title {
  2048. margin-bottom: 9px;
  2049. }
  2050. .bottom {
  2051. height: 0;
  2052. margin-top: 10px;
  2053. }
  2054. &-active,
  2055. &:hover {
  2056. font-family: PingFang SC, PingFang SC;
  2057. font-weight: 400;
  2058. font-size: 24px;
  2059. color: #1586fa;
  2060. text-align: left;
  2061. font-style: normal;
  2062. text-transform: none;
  2063. .bottom {
  2064. width: 100%;
  2065. height: 4px;
  2066. background: #3987F4;
  2067. border-radius: 2px;
  2068. }
  2069. }
  2070. }
  2071. }
  2072. .result-view {
  2073. width: 100%;
  2074. height: 50%;
  2075. overflow-y: auto;
  2076. .q-r {
  2077. padding: 20px;
  2078. line-height: 30px;
  2079. background: #f5f8fa;
  2080. border-radius: 4px 4px 4px 4px;
  2081. margin: 20px 0;
  2082. .ds-content-box {
  2083. display: flex;
  2084. .icon {
  2085. width: 26px;
  2086. height: 26px;
  2087. display: flex;
  2088. align-items: center;
  2089. justify-content: center;
  2090. border-radius: 50%;
  2091. background: white;
  2092. border: 1px solid #117ff9;
  2093. img {
  2094. width: 18px;
  2095. height: 18px;
  2096. }
  2097. }
  2098. .ds-panel {
  2099. margin-top: -5px;
  2100. margin-left: 10px;
  2101. width: calc(100% - 40px);
  2102. .ds-loading {
  2103. display: flex;
  2104. align-items: center;
  2105. .icon-arrow {
  2106. padding-left: 5px;
  2107. cursor: pointer;
  2108. }
  2109. }
  2110. .rotate {
  2111. transform: rotate(180deg);
  2112. }
  2113. }
  2114. .ds-con {
  2115. border-left: 1px solid #ccc;
  2116. padding-left: 10px;
  2117. }
  2118. }
  2119. }
  2120. // 追问
  2121. .follow {
  2122. padding: 0 20px 20px 0;
  2123. width: 100%;
  2124. text-align: right;
  2125. height: auto;
  2126. min-height: 40px;
  2127. .eval-card {
  2128. width: auto;
  2129. display: flex;
  2130. /* float: left; */
  2131. justify-content: center;
  2132. align-items: center;
  2133. .eval-icon {
  2134. width: 22px;
  2135. height: 22px;
  2136. &-like {
  2137. background: url('@/assets/images/ai-search/eval-like-icon.png') no-repeat;
  2138. background-size: 100% 100%;
  2139. &-s,
  2140. &:hover {
  2141. background: url('@/assets/images/ai-search/eval-like-icon.png') no-repeat;
  2142. background-size: 100% 100%;
  2143. }
  2144. }
  2145. &-dislike {
  2146. background: url('@/assets/images/ai-search/eval-dislike-icon.png') no-repeat;
  2147. background-size: 100% 100%;
  2148. &-s,
  2149. &:hover {
  2150. background: url('@/assets/images/ai-search/eval-dislike-icon.png') no-repeat;
  2151. background-size: 100% 100%;
  2152. }
  2153. }
  2154. &-copy {
  2155. background: url('@/assets/images/ai-search/eval-copy-icon.png') no-repeat;
  2156. background-size: 100% 100%;
  2157. &-s,
  2158. &:hover {
  2159. background: url('@/assets/images/ai-search/eval-copy-icon.png') no-repeat;
  2160. background-size: 100% 100%;
  2161. }
  2162. }
  2163. }
  2164. }
  2165. .follow-input {
  2166. margin-top: 10px;
  2167. position: relative;
  2168. textarea {
  2169. width: 100%;
  2170. height: 120px;
  2171. border-color: white;
  2172. border-radius: 10px;
  2173. resize: none;
  2174. padding: 10px;
  2175. background: transparent;
  2176. font-size: 19px;
  2177. border-color: #4096ff;
  2178. box-shadow: 0 0 0 2px rgba(5, 145, 255, 0.1);
  2179. textarea:focus-visible,
  2180. textarea:focus {
  2181. border: none !important;
  2182. outline: none;
  2183. }
  2184. textarea:focus {
  2185. border: none !important;
  2186. outline: none;
  2187. }
  2188. }
  2189. .button {
  2190. width: 110px;
  2191. height: 40px;
  2192. background: linear-gradient(90deg, #5c62ea, #517de2);
  2193. border-radius: 20px;
  2194. position: absolute;
  2195. right: 10px;
  2196. bottom: 12px;
  2197. display: flex;
  2198. justify-content: center;
  2199. align-items: center;
  2200. font-family: Alibaba PuHuiTi 2;
  2201. font-weight: normal;
  2202. font-size: 16px;
  2203. color: #ffffff;
  2204. cursor: pointer;
  2205. }
  2206. }
  2207. }
  2208. .more-questions {
  2209. .title {
  2210. display: flex;
  2211. margin-bottom: 14px;
  2212. .change-title {
  2213. margin-left: 20px;
  2214. display: flex;
  2215. cursor: pointer;
  2216. font-size: 13px;
  2217. color: #3c3c3c;
  2218. .change-icon {
  2219. width: 16px;
  2220. height: 16px;
  2221. margin-right: 4px;
  2222. background: url('@/assets/images/ai-search/icon-chang-question.png') no-repeat;
  2223. background-size: 100% 100%;
  2224. }
  2225. }
  2226. }
  2227. }
  2228. .source {
  2229. width: 100%;
  2230. .title {
  2231. display: flex;
  2232. align-items: center;
  2233. font-family: PingFang SC;
  2234. font-weight: bold;
  2235. font-size: 20px;
  2236. color: #333333;
  2237. line-height: 30px;
  2238. .icon {
  2239. width: 20px;
  2240. height: 20px;
  2241. margin-right: 8px;
  2242. background: url('@/assets/images/ai-search/source-icon.png') no-repeat;
  2243. background-size: 100% 100%;
  2244. }
  2245. .text {
  2246. font-family: Alibaba PuHuiTi 3, Alibaba PuHuiTi 30;
  2247. font-weight: normal;
  2248. //font-size: 16px;
  2249. color: #404557;
  2250. line-height: 30px;
  2251. text-align: left;
  2252. font-style: normal;
  2253. text-transform: none;
  2254. margin-right: 5px;
  2255. }
  2256. }
  2257. .items {
  2258. .item {
  2259. width: calc(100% - 10px);
  2260. min-height: 82px;
  2261. background: rgba(255, 255, 255, 0.5);
  2262. border-radius: 10px;
  2263. display: flex;
  2264. flex-direction: column;
  2265. align-items: center;
  2266. padding: 20px;
  2267. color: #888;
  2268. display: flex;
  2269. line-height: 25px;
  2270. margin-bottom: 20px;
  2271. margin: 12px 0;
  2272. background: #f1f9ff;
  2273. border-radius: 8px 8px 8px 8px;
  2274. .doc {
  2275. display: flex;
  2276. width: 100%;
  2277. p {
  2278. width: calc(100% - 25px);
  2279. .doc-link {
  2280. cursor: pointer;
  2281. color: #3987F4;
  2282. &:hover {
  2283. font-weight: bold;
  2284. }
  2285. }
  2286. }
  2287. .doc-icon {
  2288. width: 20px;
  2289. height: 20px;
  2290. margin-left: 5px;
  2291. cursor: pointer;
  2292. &-show {
  2293. background: url('/images/zcbd/ai-search/icon-doc-show.png') no-repeat;
  2294. background-size: 100% 100%;
  2295. }
  2296. &-hide {
  2297. background: url('/images/zcbd/ai-search/icon-doc-hide.png') no-repeat;
  2298. background-size: 100% 100%;
  2299. }
  2300. }
  2301. }
  2302. .content {
  2303. //white-space: pre-line;
  2304. width: 100%;
  2305. font-family: Alibaba PuHuiTi 3, Alibaba PuHuiTi 30;
  2306. font-weight: normal;
  2307. font-size: 14px;
  2308. color: #757575;
  2309. line-height: 28px;
  2310. text-align: left;
  2311. font-style: normal;
  2312. text-transform: none;
  2313. &-hide {
  2314. display: none;
  2315. }
  2316. }
  2317. }
  2318. .item-url {
  2319. margin: 12px 0;
  2320. background: #f1f9ff;
  2321. border-radius: 8px 8px 8px 8px;
  2322. .bottom {
  2323. width: 100%;
  2324. .title-icon {
  2325. display: flex;
  2326. justify-content: left;
  2327. align-items: center;
  2328. width: 90%;
  2329. float: left;
  2330. .icon {
  2331. width: 23px;
  2332. height: 23px;
  2333. background: url('/images/zcbd/ai-search/icon-net-url.png') no-repeat;
  2334. background-size: 100% 100%;
  2335. }
  2336. .title {
  2337. color: #a9a3a3cc;
  2338. font-size: 16px;
  2339. margin-left: 10px;
  2340. }
  2341. }
  2342. .index {
  2343. float: right;
  2344. width: 20px;
  2345. height: 20px;
  2346. font-size: 12px;
  2347. line-height: 20px;
  2348. text-align: center;
  2349. margin: 0 5px;
  2350. border-radius: 10px;
  2351. background: #d0d5dd;
  2352. color: #000;
  2353. }
  2354. }
  2355. }
  2356. }
  2357. }
  2358. }
  2359. // 土地市场
  2360. .map-answer {
  2361. position: relative;
  2362. width: 100%;
  2363. min-height: 600px;
  2364. .left-panel {
  2365. width: 100%;
  2366. background: #ffffff;
  2367. z-index: 1000;
  2368. //position: relative;
  2369. left: 12px;
  2370. top: 15px;
  2371. border-radius: 10px;
  2372. .title {
  2373. width: 100%;
  2374. height: 30px;
  2375. text-align: left;
  2376. line-height: 30px;
  2377. font-size: 18px;
  2378. font-weight: bolder;
  2379. }
  2380. .content {
  2381. width: 100%;
  2382. height: auto;
  2383. .chart-card {
  2384. width: 100%;
  2385. height: 330px;
  2386. .chart-title {
  2387. width: 100%;
  2388. height: 30px;
  2389. font-size: 18px;
  2390. font-weight: bolder;
  2391. padding: 0 5px;
  2392. line-height: 30px;
  2393. margin: 10px 0;
  2394. }
  2395. .chart {
  2396. width: 100%;
  2397. height: 300px;
  2398. }
  2399. //background: #0a84ff;
  2400. }
  2401. .summary-card {
  2402. width: 100%;
  2403. height: 50%;
  2404. .summary-title {
  2405. width: 100%;
  2406. height: 30px;
  2407. font-size: 18px;
  2408. font-weight: bolder;
  2409. padding: 0 5px;
  2410. line-height: 30px;
  2411. margin: 10px 0;
  2412. }
  2413. .summary-content {
  2414. width: calc(100% - 20px);
  2415. height: calc(100% - 30px);
  2416. padding: 0;
  2417. white-space: pre-wrap;
  2418. overflow-y: auto;
  2419. border-radius: 10px;
  2420. margin: 10px;
  2421. font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Noto Sans, Helvetica,
  2422. Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji';
  2423. font-size: 16px;
  2424. line-height: 1.5;
  2425. }
  2426. }
  2427. }
  2428. }
  2429. .map-panel {
  2430. width: 100%;
  2431. height: 630px;
  2432. .map-title {
  2433. width: 100%;
  2434. height: 30px;
  2435. font-size: 18px;
  2436. font-weight: bolder;
  2437. padding: 0 5px;
  2438. line-height: 30px;
  2439. margin: 10px 0;
  2440. }
  2441. .map {
  2442. width: 100%;
  2443. height: 600px;
  2444. }
  2445. }
  2446. }
  2447. }
  2448. }
  2449. }
  2450. }
  2451. }
  2452. ::-webkit-scrollbar {
  2453. width: 8px;
  2454. }
  2455. ::-webkit-scrollbar-thumb {
  2456. background-color: rgb(0 0 0 / 10%);
  2457. border-radius: 10px;
  2458. }
  2459. }
  2460. ::v-deep .input-modal {
  2461. .modal-que {
  2462. background: #0e91fd;
  2463. textarea {
  2464. width: 100%;
  2465. height: 257px !important;
  2466. border: none;
  2467. border-radius: 10px;
  2468. resize: none;
  2469. padding: 10px;
  2470. background: transparent;
  2471. font-size: 19px;
  2472. &:focus-visible {
  2473. border: none !important;
  2474. outline: none;
  2475. }
  2476. }
  2477. }
  2478. }
  2479. </style>